Guides

Property Bags

Overview

Property bags enable you to add custom metadata to your GenAI operations, extending Pay-i's built-in tracking with dimensions specific to your business needs. While Pay-i automatically tracks standard information like use case names and user IDs, property bags let you capture additional context that matters to your application—such as document types, customer tiers, processing stages, or any other business-specific dimensions.

What are Property Bags?

Property bags are collections of custom string key-value pairs that extend Pay-i's tracking capabilities. They work at two levels:

  1. Use Case Properties: Custom dimensions that apply to an entire use case and all its requests
  2. Request Properties: Custom dimensions specific to individual API requests

Property bags integrate seamlessly with Pay-i's context system—they inherit from parent contexts, can be overridden at different levels, and are accessible alongside standard dimensions through the same APIs.

Standard vs. Custom Dimensions

Pay-i automatically tracks standard dimensions that are common across all applications, while property bags let you add custom dimensions specific to your business needs. For an overview of all dimension types available in Pay-i, see the Custom Instrumentation documentation.

Standard Dimensions (Built-in)

Pay-i automatically captures these standard dimensions:

  • use_case_name, use_case_id, use_case_version: Identify and group related requests
  • use_case_step: Identifies specific steps within a use case flow
  • limit_ids: Track budget constraints being applied
  • user_id: Tracks which user generated the request

Reference: For a complete list of all available context properties, see the get_context() documentation.

Custom Dimensions (Property Bags)

Property bags let you define your own dimensions through two types:

  • use_case_properties: Custom dimensions that apply at the use case level
  • request_properties: Custom dimensions that apply to individual requests

These custom dimensions work alongside standard dimensions, giving you complete visibility into both Pay-i's built-in tracking and your application-specific context.

Real-World Scenarios

The following examples demonstrate how property bags work in practice, showing both the technical implementation and the business value they provide.

Document Processing Application

In a document processing system, you need to track costs and performance across different document types and clients. Property bags let you capture this business context alongside Pay-i's standard tracking:

def process_financial_document(document_id, client_id):
    # Use case properties apply to ALL operations on this document
    with track_context(
        use_case_name="document_processing",
        use_case_properties={
            "document_id": document_id,
            "document_type": get_document_type(document_id),
            "client_id": client_id
        }
    ):
        # Extract information - use step for the processing stage
        with track_context(
            use_case_step="extraction",
            request_properties={
                "file_format": get_file_format(document_id),
                "extraction_mode": "full"
            }
        ):
            extraction_response = client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": "user", "content": "Perform a full extraction of all key financial data from this report, including all tables and footnotes."}]
            )
            
        # Analyze data - use step for the processing stage
        with track_context(
            use_case_step="analysis",
            request_properties={
                "analysis_type": "financial_metrics",
                "data_granularity": "quarterly"
            }
        ):
            analysis_response = client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": "user", "content": "Analyze these financial metrics with quarterly granularity. Calculate quarter-over-quarter growth rates and identify seasonal trends."}]
            )

What this approach achieves:

  • Unified tracking: All operations for a document are grouped under one use case
  • Step visibility: Different processing stages are clearly identified with use_case_step
  • Contextual analysis: Request properties capture the specific parameters and modes used for each operation
  • Business insights: You can analyze costs and performance by document type, client, or processing mode

Customer Support Chatbot

A support chatbot needs to track conversation context, issue categories, and interaction quality. Property bags provide the business context needed to analyze support performance and costs:

@track(
    use_case_name="support_assistant",
    use_case_properties={
        "conversation_id": conversation_id,
        "customer_id": customer.id,
        "issue_category": ticket.category
    }
)
def handle_conversation():
    # Initialize conversation history
    conversation_history = [
        {"role": "system", "content": "You are a helpful customer support assistant."}
    ]
    
    # First message - use step for conversation stage
    with track_context(
        use_case_step="initial_greeting",
        request_properties={
            "intent_detected": "greeting",
            "sentiment": "neutral"
        }
    ):
        # Add user message to history
        conversation_history.append(
            {"role": "user", "content": "Hello, I have a question about my bill."}
        )
        
        # Get assistant response - neutral greeting tone based on properties
        response = client.chat.completions.create(
            model="gpt-4",
            messages=conversation_history
        )
        
        # Add assistant response to history
        conversation_history.append(
            {"role": "assistant", "content": response.choices[0].message.content}
        )
    
    # Second message - use step for conversation stage
    with track_context(
        use_case_step="issue_details",
        request_properties={
            "intent_detected": "billing_dispute",
            "priority_score": "high"
        }
    ):
        # Add user message to history
        conversation_history.append(
            {"role": "user", "content": "I was charged twice for my subscription."}
        )
        
        # Get assistant response - high priority billing dispute response
        response = client.chat.completions.create(
            model="gpt-4",
            messages=conversation_history
        )

What this approach achieves:

  • Conversation unity: All API calls in a conversation are grouped under one use case
  • Stage tracking: Different conversation phases are identified with use_case_step
  • Context capture: Request properties record intent, sentiment, and priority for each interaction
  • Performance analysis: You can analyze support costs and effectiveness by issue category, priority, or conversation stage

E-commerce Recommendation Engine

An e-commerce platform uses property bags to track personalization decisions that actually affect the GenAI calls:

@track(
    use_case_name="product_recommendations",
    use_case_properties={
        "platform": "web",
        "recommendation_engine_version": "v3.2"
    }
)
def generate_personalized_recommendations(customer_id, page_context):
    customer_profile = get_customer_profile(customer_id)
    
    # Make operational decisions based on customer data
    personalization_level = "high" if customer_profile["segment"] == "vip" else "standard"
    recommendation_count = 12 if customer_profile["segment"] == "vip" else 8
    model_choice = "gpt-4" if personalization_level == "high" else "gpt-3.5-turbo"
    
    with track_context(
        use_case_properties={
            "customer_id": customer_id,
            "customer_segment": customer_profile["segment"],
            "geographic_region": customer_profile["region"]
        },
        request_properties={
            "personalization_level": personalization_level,        # Track decision
            "recommendation_count": str(recommendation_count),     # Track decision
            "model_selected": model_choice,                       # Track decision
            "page_type": page_context["page"]
        }
    ):
        # Use the tracked values to control actual GenAI behavior
        if personalization_level == "high":
            prompt = f"Generate {recommendation_count} highly personalized product recommendations for VIP customer with purchase history in {customer_profile['region']}. Include premium brands and exclusive items."
        else:
            prompt = f"Generate {recommendation_count} general product recommendations for customer browsing {page_context['page']} page."
            
        response = client.chat.completions.create(
            model=model_choice,  # Use the model choice we tracked
            messages=[{"role": "user", "content": prompt}]
        )
        
        return response

def get_customer_profile(customer_id):
    return {"segment": "vip", "region": "north_america"}

What this achieves: The property bags track the actual operational decisions (personalization level, model choice, recommendation count) that directly affect the GenAI call's behavior and cost.

Using Property Bags

Property bags can be set at multiple levels in your application, with each level inheriting from parent contexts. This section shows you how to set and access property bags effectively.

Setting Property Bags

You can set property bags at three different levels, depending on your needs:

1. Global Level with payi_instrument()

Set application-wide defaults that apply to all operations:

from payi.lib.instrument import payi_instrument

# Set global property bags during initialization
payi_instrument(config={
    "use_case_name": "my_application",
    "use_case_properties": {
        "environment": env.name,
        "product_version": config.version
    },
    "request_properties": {
        "default_service": config.service_name
    }
})

These properties will be inherited by all subsequent operations unless overridden.

2. Function Level with @track

Add properties that apply to a specific function and all operations within it:

from payi.lib.instrument import track

@track(
    use_case_name="data_analysis",
    use_case_step="financial_data",
    use_case_properties={
        "project_id": project.id,
        "dataset": dataset.name
    },
    request_properties={
        "service": service_name
    }
)
def analyze_data(data):
    # All API calls in this function inherit these properties
    response = client.chat.completions.create(...)

Function-level properties combine with global properties, adding more specific context.

3. Block Level with track_context()

Set properties for a specific code block or operation:

from payi.lib.instrument import track_context

# Set properties for a specific code block
with track_context(
    use_case_step="summarization",
    use_case_properties={
        "document_id": document.id,
        "priority": document.priority
    },
    request_properties={
        "intent_detected": "summary_request",
        "session_action": "document_review"
    }
):
    # API calls in this block inherit all previous properties plus these new ones
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": "Summarize this document briefly."}]
    )

Block-level properties provide the most specific context and combine with both global and function-level properties.

Accessing Property Bags

Use the get_context() function to retrieve both property bags and standard dimensions at runtime:

from payi.lib.instrument import get_context

# Get the current Pay-i context
context = get_context()

# Access use case properties safely (always use "or {}" pattern)
use_case_props = context.get('use_case_properties', {})
document_id = use_case_props.get("document_id")
document_type = use_case_props.get("document_type")

# Access request properties safely (always use "or {}" pattern)
request_props = context.get('request_properties', {})
temperature = request_props.get("temperature")
intent = request_props.get("intent_detected")

# Standard dimensions are available directly on the context
use_case_name = context.get('use_case_name')
use_case_step = context.get('use_case_step')
user_id = context.get('user_id')

Important: Always use the or {} pattern when accessing property bags. Property bags can be None if they haven't been set, and this pattern ensures you get an empty dictionary for safe .get() operations.

Property Inheritance

Property bags follow Pay-i's standard parameter inheritance rules. Child contexts merge with parent contexts, and properties with the same key are overridden by the child value.

For detailed inheritance behavior: See Parameter Precedence in Pay-i Instrumentation for complete inheritance flow diagrams, blocking inheritance examples, and interaction with other Pay-i parameters.

Property Bag Relationships

The following diagram illustrates how different types of properties work together in Pay-i's tracking system:

┌────────────────────────────────────────────────────────────┐
│ PAY-I TRACKING CONTEXT                                     │
│                                                            │
│  STANDARD DIMENSIONS (Built-in)                            │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ • use_case_name    • use_case_id                     │  │
│  │ • use_case_version • use_case_step                   │  │
│  │ • user_id          • limit_ids                       │  │
│  └──────────────────────────────────────────────────────┘  │
│                             ↑                              │
│                    ACCESSIBLE VIA                          │
│                    get_context()                           │
│                             ↓                              │
│  CUSTOM DIMENSIONS (Property Bags)                         │
│  ┌──────────────────────────────────────────────────────┐  │
│  │ USE CASE PROPERTIES (1)                              │  │
│  │ ┌─────────────────────────────────────────────────┐  │  │
│  │ │ Scope: Entire use case workflow                 │  │  │
│  │ │ Lifetime: From start to end of use case         │  │  │
│  │ │ Examples:                                       │  │  │
│  │ │ • document_id   • customer_id                   │  │  │
│  │ │ • project_type  • department                    │  │  │
│  │ └─────────────────────────────────────────────────┘  │  │
│  │                        │                             │  │
│  │                        │ 1:* (One-to-Many)           │  │
│  │                        ▼                             │  │
│  │ REQUEST PROPERTIES (Many)                            │  │
│  │ ┌───────────────────┐ ┌──────────────────┐ ┌───────┐ │  │
│  │ │ Request #1        │ │ Request #2       │ │  ...  │ │  │
│  │ │ • intent_detected │ │ • processing_mode│ │       │ │  │
│  │ │ • confidence_score│ │ • retry_attempt  │ │       │ │  │
│  │ └───────────────────┘ └──────────────────┘ └───────┘ │  │
│  │ Scope: Individual API requests                       │  │
│  │ Lifetime: Single request duration                    │  │
│  └──────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────┘

Blocking Inheritance

Sometimes you want to prevent certain properties from being inherited. Use empty dictionaries ({}) to block inheritance from parent contexts:

@track(
    use_case_properties={"project": project.name},
    request_properties={"user_data": "present"}
)
def analyze_data():
    # For system operations, we don't want user-specific request properties
    with track_context(request_properties={}):
        # Result:
        # use_case_properties = {"project": project.name} (still inherited)
        # request_properties = None (inheritance blocked by empty dict)
        system_check = client.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": "Perform system health check."}]
        )

This technique is useful when you have operations that shouldn't inherit certain types of context—like system operations that shouldn't carry user-specific data.

Best Practices

Following these best practices will help you implement property bags effectively and maintain consistent tracking across your application.

1. Use Consistent Property Names

Establish and follow naming conventions across your team to ensure consistent tracking and easier data analysis:

# Define team conventions for property naming
DOCUMENT_ID_KEY = "document_id"
DOCUMENT_TYPE_KEY = "document_type"
CUSTOMER_ID_KEY = "customer_id"
PRIORITY_KEY = "priority"

# Apply conventions consistently throughout your application
with track_context(
    use_case_properties={
        DOCUMENT_ID_KEY: document.id,
        DOCUMENT_TYPE_KEY: document.type,
        CUSTOMER_ID_KEY: customer.id
    },
    request_properties={
        PRIORITY_KEY: "high"
    }
):
    # Consistent naming makes data analysis much easier
    response = client.chat.completions.create(...)

Benefits of consistent naming:

  • Easier querying and filtering of tracking data
  • Reduced confusion when analyzing costs and performance
  • Better team collaboration and code maintainability

2. Handle Property Access Safely

Property bags can be None if they haven't been set. Always use the or {} pattern to ensure safe access:

context = get_context()

# ✅ Correct: Safe access with "or {}" pattern
use_case_props = context.get('use_case_properties', {})
document_id = use_case_props.get("document_id")
project = use_case_props.get("project", "default_project")

request_props = context.get('request_properties', {})
operation = request_props.get("operation")
priority = request_props.get("priority", "normal")

# ❌ Incorrect: Direct access can cause errors
try:
    # This will fail if use_case_properties is None
    document_id = context.get('use_case_properties', {}).get("document_id")
except AttributeError:
    # Handle the error case
    document_id = None

The or {} pattern is the recommended approach because it's concise, safe, and allows you to use .get() with confidence.

3. Convert Values to Strings

Property bags only accept string values. Convert other data types appropriately before adding them to property bags:

from datetime import datetime

# Examples of converting different types to strings
process_id = 12345
confidence_score = 0.85
timestamp = datetime.now()
is_premium = True
document_size = 1024

with track_context(
    use_case_properties={
        "process_id": str(process_id),                    # int -> "12345"
        "document_size_kb": str(document_size),           # int -> "1024"
        "is_premium_customer": str(is_premium).lower(),   # bool -> "true"
    },
    request_properties={
        "confidence_score": str(confidence_score),        # float -> "0.85"
        "created_at": timestamp.isoformat(),              # datetime -> "2024-01-15T10:30:00"
        "date_processed": timestamp.strftime("%Y-%m-%d"), # datetime -> "2024-01-15"
    }
):
    response = client.chat.completions.create(...)

Common conversion patterns:

  • Numbers: Use str() for integers and floats
  • Booleans: Use str(bool_value).lower() for consistent "true"/"false" strings
  • Dates: Use .isoformat() or .strftime() for custom formats
  • Objects: Convert to meaningful string representations of their key attributes

Common Use Cases for Property Bags

Property bags work best when you choose the right level for each type of information. Here are proven patterns for organizing your custom dimensions:

Use Case Properties

Use case properties apply to entire workflows and should capture stable, business-level context:

  • Business Entities: document_id, conversation_id, project_id
  • Customer Information: client_id, customer_tier, account_type
  • Categorization: category, department, team
  • Environment: environment, region, deployment

These properties typically don't change during the use case and help you analyze costs and performance by business dimension.

Request Properties

Request properties capture details specific to individual API calls:

  • Request Context: intent_detected, sentiment, confidence_score
  • Content Characteristics: query_length, request_format, response_format
  • Transaction Details: transaction_id, attempt_number, status_code
  • Operational Metadata: session_action, user_feedback, response_quality

These properties can vary between API calls within the same use case and help you understand performance at the request level.

Related Documentation