Tag: technology

  • Agentic Design Pattern: Trace Tracking

    
        """
    Pattern 094: Trace/Lineage Tracking
    
    Description:
        Trace and Lineage Tracking captures the complete execution path of an agent, including
        all decisions, actions, intermediate states, and data transformations. This pattern is
        essential for debugging complex agent behaviors, understanding how decisions were made,
        ensuring compliance, and optimizing performance. Comprehensive tracing enables
        reproducibility, auditing, and continuous improvement of AI systems.
    
        Trace tracking provides:
        - Complete execution history
        - Decision point visibility
        - Data lineage and provenance
        - Performance metrics per step
        - Error context and stack traces
        - Reproducibility information
    
    Components:
        1. Trace Capture
           - Execution events (start, end, error)
           - Decision points and branches
           - Input/output at each step
           - Timestamps and durations
           - Resource consumption
           - Agent state snapshots
    
        2. Trace Storage
           - Structured trace format
           - Hierarchical relationships
           - Searchable metadata
           - Efficient retrieval
           - Long-term retention
    
        3. Trace Analysis
           - Path visualization
           - Performance analysis
           - Bottleneck identification
           - Error pattern detection
           - Comparison tools
    
        4. Trace Replay
           - Reproduce executions
           - Debug specific paths
           - Test alternative branches
           - Validate fixes
           - Training data generation
    
    Use Cases:
        1. Debugging
           - Understand unexpected behavior
           - Reproduce production issues
           - Identify error causes
           - Validate fixes
           - Root cause analysis
    
        2. Performance Optimization
           - Identify bottlenecks
           - Measure step durations
           - Analyze resource usage
           - Compare implementations
           - Optimize hot paths
    
        3. Compliance & Auditing
           - Document decision making
           - Prove regulatory compliance
           - Audit trails for sensitive operations
           - Accountability tracking
           - Legal requirements
    
        4. Quality Assurance
           - Validate agent behavior
           - Ensure determinism
           - Track test coverage
           - Regression detection
           - Change impact analysis
    
        5. Agent Development
           - Understand agent flow
           - Optimize prompts
           - Refine decision logic
           - Test edge cases
           - Documentation generation
    
    LangChain Implementation:
        LangChain supports tracing through:
        - LangSmith for comprehensive tracing
        - Custom callback handlers
        - OpenTelemetry integration
        - Structured logging
        - Trace context propagation
    
    Key Features:
        1. Comprehensive Capture
           - Every agent step traced
           - Full context preservation
           - Hierarchical relationships
           - Performance metrics
           - Error details
    
        2. Structured Format
           - Consistent trace structure
           - Searchable attributes
           - Machine-readable format
           - Human-readable display
           - Standard schemas
    
        3. Powerful Analysis
           - Path visualization
           - Query capabilities
           - Statistical analysis
           - Comparison tools
           - Pattern detection
    
        4. Production Ready
           - Low overhead ( 1 year
           - Compression for old traces
           - Automatic cleanup
    
        3. Performance Impact
           - Target:  Dict[str, Any]:
            """Convert to dictionary"""
            data = asdict(self)
            data['event_type'] = self.event_type.value
            data['timestamp'] = self.timestamp.isoformat()
            return data
    
    
    @dataclass
    class Trace:
        """Complete trace of an agent execution"""
        trace_id: str
        start_time: datetime
        end_time: Optional[datetime] = None
        events: List[TraceEvent] = field(default_factory=list)
        status: str = "running"  # running, completed, failed
        metadata: Dict[str, Any] = field(default_factory=dict)
        
        @property
        def duration_ms(self) -> float:
            """Calculate total duration"""
            if self.end_time:
                return (self.end_time - self.start_time).total_seconds() * 1000
            return 0.0
        
        def to_dict(self) -> Dict[str, Any]:
            """Convert to dictionary"""
            return {
                'trace_id': self.trace_id,
                'start_time': self.start_time.isoformat(),
                'end_time': self.end_time.isoformat() if self.end_time else None,
                'duration_ms': self.duration_ms,
                'status': self.status,
                'event_count': len(self.events),
                'metadata': self.metadata,
                'events': [e.to_dict() for e in self.events]
            }
    
    
    class TraceCollector:
        """
        Collects and manages traces for agent executions.
        
        Captures events, maintains relationships, and provides query capabilities.
        """
        
        def __init__(self):
            """Initialize trace collector"""
            self.traces: Dict[str, Trace] = {}
            self.current_trace_id: Optional[str] = None
            self.event_stack: List[str] = []  # Stack of event IDs for hierarchy
        
        def start_trace(self, metadata: Optional[Dict[str, Any]] = None) -> str:
            """
            Start a new trace.
            
            Args:
                metadata: Optional trace metadata
                
            Returns:
                Trace ID
            """
            trace_id = str(uuid.uuid4())
            trace = Trace(
                trace_id=trace_id,
                start_time=datetime.now(),
                metadata=metadata or {}
            )
            self.traces[trace_id] = trace
            self.current_trace_id = trace_id
            return trace_id
        
        def end_trace(self, trace_id: str, status: str = "completed"):
            """
            End a trace.
            
            Args:
                trace_id: Trace identifier
                status: Final status (completed, failed)
            """
            if trace_id in self.traces:
                trace = self.traces[trace_id]
                trace.end_time = datetime.now()
                trace.status = status
        
        def log_event(
            self,
            event_type: TraceEventType,
            name: str,
            input_data: Optional[Dict[str, Any]] = None,
            output_data: Optional[Dict[str, Any]] = None,
            metadata: Optional[Dict[str, Any]] = None,
            error: Optional[str] = None,
            duration_ms: Optional[float] = None
        ) -> str:
            """
            Log a trace event.
            
            Args:
                event_type: Type of event
                name: Event name
                input_data: Input data
                output_data: Output data
                metadata: Additional metadata
                error: Error message if applicable
                duration_ms: Event duration
                
            Returns:
                Event ID
            """
            if not self.current_trace_id:
                return ""
            
            event_id = str(uuid.uuid4())
            parent_id = self.event_stack[-1] if self.event_stack else None
            
            event = TraceEvent(
                event_id=event_id,
                trace_id=self.current_trace_id,
                parent_id=parent_id,
                event_type=event_type,
                timestamp=datetime.now(),
                name=name,
                input_data=input_data or {},
                output_data=output_data or {},
                metadata=metadata or {},
                error=error,
                duration_ms=duration_ms
            )
            
            trace = self.traces[self.current_trace_id]
            trace.events.append(event)
            
            return event_id
        
        def start_span(self, name: str, event_type: TraceEventType) -> str:
            """
            Start a span (nested event).
            
            Args:
                name: Span name
                event_type: Event type
                
            Returns:
                Event ID
            """
            event_id = self.log_event(
                event_type=event_type,
                name=name,
                metadata={'span_start': True}
            )
            self.event_stack.append(event_id)
            return event_id
        
        def end_span(self, event_id: str, output_data: Optional[Dict[str, Any]] = None):
            """
            End a span.
            
            Args:
                event_id: Event ID to end
                output_data: Output data
            """
            if self.event_stack and self.event_stack[-1] == event_id:
                self.event_stack.pop()
                
                # Update event with output and duration
                if self.current_trace_id:
                    trace = self.traces[self.current_trace_id]
                    for event in trace.events:
                        if event.event_id == event_id:
                            if output_data:
                                event.output_data = output_data
                            # Calculate duration from event timestamp to now
                            event.duration_ms = (datetime.now() - event.timestamp).total_seconds() * 1000
                            break
        
        def get_trace(self, trace_id: str) -> Optional[Trace]:
            """Get trace by ID"""
            return self.traces.get(trace_id)
        
        def get_all_traces(self) -> List[Trace]:
            """Get all traces"""
            return list(self.traces.values())
        
        def clear_traces(self):
            """Clear all traces"""
            self.traces.clear()
            self.current_trace_id = None
            self.event_stack.clear()
    
    
    class TraceAnalyzer:
        """
        Analyzes traces to extract insights and metrics.
        
        Provides visualization, statistics, and pattern detection.
        """
        
        def __init__(self):
            """Initialize analyzer"""
            pass
        
        def get_execution_path(self, trace: Trace) -> List[str]:
            """
            Get execution path as list of event names.
            
            Args:
                trace: Trace to analyze
                
            Returns:
                List of event names in order
            """
            return [event.name for event in trace.events]
        
        def get_performance_stats(self, trace: Trace) -> Dict[str, Any]:
            """
            Calculate performance statistics.
            
            Args:
                trace: Trace to analyze
                
            Returns:
                Performance statistics
            """
            events_with_duration = [e for e in trace.events if e.duration_ms]
            
            if not events_with_duration:
                return {
                    'total_duration_ms': trace.duration_ms,
                    'event_count': len(trace.events),
                    'avg_event_duration_ms': 0,
                    'max_event_duration_ms': 0,
                    'min_event_duration_ms': 0
                }
            
            durations = [e.duration_ms for e in events_with_duration]
            
            return {
                'total_duration_ms': trace.duration_ms,
                'event_count': len(trace.events),
                'events_with_timing': len(events_with_duration),
                'avg_event_duration_ms': sum(durations) / len(durations),
                'max_event_duration_ms': max(durations),
                'min_event_duration_ms': min(durations),
                'total_event_time_ms': sum(durations)
            }
        
        def get_event_type_stats(self, trace: Trace) -> Dict[str, int]:
            """
            Count events by type.
            
            Args:
                trace: Trace to analyze
                
            Returns:
                Event counts by type
            """
            stats = {}
            for event in trace.events:
                event_type = event.event_type.value
                stats[event_type] = stats.get(event_type, 0) + 1
            return stats
        
        def get_error_summary(self, trace: Trace) -> List[Dict[str, Any]]:
            """
            Get summary of errors in trace.
            
            Args:
                trace: Trace to analyze
                
            Returns:
                List of error details
            """
            errors = []
            for event in trace.events:
                if event.error:
                    errors.append({
                        'event_id': event.event_id,
                        'event_type': event.event_type.value,
                        'name': event.name,
                        'error': event.error,
                        'timestamp': event.timestamp.isoformat()
                    })
            return errors
        
        def visualize_trace(self, trace: Trace) -> str:
            """
            Create text-based visualization of trace.
            
            Args:
                trace: Trace to visualize
                
            Returns:
                Formatted string visualization
            """
            lines = []
            lines.append(f"Trace: {trace.trace_id}")
            lines.append(f"Status: {trace.status}")
            lines.append(f"Duration: {trace.duration_ms:.2f}ms")
            lines.append(f"Events: {len(trace.events)}")
            lines.append("")
            lines.append("Execution Path:")
            
            # Build hierarchy
            event_map = {e.event_id: e for e in trace.events}
            root_events = [e for e in trace.events if e.parent_id is None]
            
            def format_event(event: TraceEvent, indent: int = 0) -> List[str]:
                """Format event with indentation"""
                result = []
                prefix = "  " * indent
                duration = f"({event.duration_ms:.1f}ms)" if event.duration_ms else ""
                error_mark = " ❌" if event.error else ""
                
                result.append(f"{prefix}├─ {event.event_type.value}: {event.name} {duration}{error_mark}")
                
                # Find children
                children = [e for e in trace.events if e.parent_id == event.event_id]
                for child in children:
                    result.extend(format_event(child, indent + 1))
                
                return result
            
            for root in root_events:
                lines.extend(format_event(root))
            
            return "\n".join(lines)
    
    
    class TracedAgent:
        """
        Example agent with comprehensive tracing.
        
        Demonstrates trace collection for all agent operations.
        """
        
        def __init__(self, collector: TraceCollector):
            """
            Initialize traced agent.
            
            Args:
                collector: Trace collector instance
            """
            self.collector = collector
            self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
        
        def process_query(self, query: str, metadata: Optional[Dict[str, Any]] = None) -> str:
            """
            Process query with full tracing.
            
            Args:
                query: User query
                metadata: Optional metadata
                
            Returns:
                Response
            """
            # Start trace
            trace_id = self.collector.start_trace(metadata={'query': query, **(metadata or {})})
            
            try:
                # Log agent start
                agent_span = self.collector.start_span("process_query", TraceEventType.AGENT_START)
                
                # Step 1: Analyze query
                analysis_span = self.collector.start_span("analyze_query", TraceEventType.DECISION)
                query_type = self._analyze_query(query)
                self.collector.end_span(analysis_span, {'query_type': query_type})
                
                # Step 2: Generate response
                llm_span = self.collector.start_span("llm_call", TraceEventType.LLM_START)
                response = self._call_llm(query)
                self.collector.end_span(llm_span, {'response': response[:100]})
                
                # End agent span
                self.collector.end_span(agent_span, {'response': response})
                
                # End trace successfully
                self.collector.end_trace(trace_id, "completed")
                
                return response
                
            except Exception as e:
                # Log error
                self.collector.log_event(
                    event_type=TraceEventType.ERROR,
                    name="agent_error",
                    error=str(e),
                    metadata={'exception_type': type(e).__name__}
                )
                
                # End trace with failure
                self.collector.end_trace(trace_id, "failed")
                
                raise
        
        def _analyze_query(self, query: str) -> str:
            """Analyze query type"""
            time.sleep(0.05)  # Simulate processing
            if '?' in query:
                return "question"
            else:
                return "statement"
        
        def _call_llm(self, query: str) -> str:
            """Call LLM"""
            prompt = ChatPromptTemplate.from_template("Answer concisely: {query}")
            chain = prompt | self.llm | StrOutputParser()
            return chain.invoke({"query": query})
    
    
    def demonstrate_trace_tracking():
        """Demonstrate trace and lineage tracking"""
        print("=" * 80)
        print("TRACE/LINEAGE TRACKING DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic Trace Collection
        print("\n" + "=" * 80)
        print("EXAMPLE 1: Basic Trace Collection")
        print("=" * 80)
        
        collector = TraceCollector()
        
        # Start a trace
        trace_id = collector.start_trace(metadata={'user': 'demo_user', 'session': 'session_001'})
        print(f"\nStarted trace: {trace_id[:8]}...")
        
        # Log some events
        collector.log_event(
            event_type=TraceEventType.AGENT_START,
            name="agent_init",
            metadata={'agent_type': 'demo'}
        )
        
        collector.log_event(
            event_type=TraceEventType.LLM_START,
            name="llm_call_1",
            input_data={'prompt': 'What is AI?'},
            duration_ms=1250.5
        )
        
        collector.log_event(
            event_type=TraceEventType.LLM_END,
            name="llm_call_1",
            output_data={'response': 'AI is...'},
            duration_ms=1250.5
        )
        
        collector.end_trace(trace_id, "completed")
        
        trace = collector.get_trace(trace_id)
        print(f"\nTrace completed:")
        print(f"  Events: {len(trace.events)}")
        print(f"  Duration: {trace.duration_ms:.2f}ms")
        print(f"  Status: {trace.status}")
        
        # Example 2: Hierarchical Tracing
        print("\n" + "=" * 80)
        print("EXAMPLE 2: Hierarchical Tracing with Spans")
        print("=" * 80)
        
        collector.clear_traces()
        trace_id = collector.start_trace()
        
        # Parent span
        parent_span = collector.start_span("main_task", TraceEventType.AGENT_START)
        time.sleep(0.1)
        
        # Child span 1
        child_span1 = collector.start_span("subtask_1", TraceEventType.TOOL_START)
        time.sleep(0.05)
        collector.end_span(child_span1, {'result': 'success'})
        
        # Child span 2
        child_span2 = collector.start_span("subtask_2", TraceEventType.TOOL_START)
        time.sleep(0.03)
        collector.end_span(child_span2, {'result': 'success'})
        
        collector.end_span(parent_span, {'status': 'completed'})
        collector.end_trace(trace_id, "completed")
        
        trace = collector.get_trace(trace_id)
        print(f"\nHierarchical trace with {len(trace.events)} events:")
        for event in trace.events:
            indent = "  " if event.parent_id else ""
            duration = f" ({event.duration_ms:.1f}ms)" if event.duration_ms else ""
            print(f"{indent}{event.event_type.value}: {event.name}{duration}")
        
        # Example 3: Traced Agent Execution
        print("\n" + "=" * 80)
        print("EXAMPLE 3: Full Agent Execution with Tracing")
        print("=" * 80)
        
        collector.clear_traces()
        agent = TracedAgent(collector)
        
        print("\nExecuting query with tracing...")
        query = "What is machine learning?"
        response = agent.process_query(query, metadata={'priority': 'high'})
        
        print(f"\nResponse: {response[:100]}...")
        
        # Get the trace
        traces = collector.get_all_traces()
        if traces:
            trace = traces[0]
            print(f"\nTrace captured:")
            print(f"  Trace ID: {trace.trace_id[:8]}...")
            print(f"  Events: {len(trace.events)}")
            print(f"  Duration: {trace.duration_ms:.2f}ms")
            print(f"  Status: {trace.status}")
        
        # Example 4: Trace Analysis
        print("\n" + "=" * 80)
        print("EXAMPLE 4: Trace Analysis and Insights")
        print("=" * 80)
        
        analyzer = TraceAnalyzer()
        
        if traces:
            trace = traces[0]
            
            # Performance stats
            print("\nPerformance Statistics:")
            stats = analyzer.get_performance_stats(trace)
            for key, value in stats.items():
                if isinstance(value, float):
                    print(f"  {key}: {value:.2f}")
                else:
                    print(f"  {key}: {value}")
            
            # Event type stats
            print("\nEvent Type Distribution:")
            type_stats = analyzer.get_event_type_stats(trace)
            for event_type, count in sorted(type_stats.items()):
                print(f"  {event_type}: {count}")
            
            # Execution path
            print("\nExecution Path:")
            path = analyzer.get_execution_path(trace)
            for i, step in enumerate(path, 1):
                print(f"  {i}. {step}")
        
        # Example 5: Trace Visualization
        print("\n" + "=" * 80)
        print("EXAMPLE 5: Trace Visualization")
        print("=" * 80)
        
        if traces:
            visualization = analyzer.visualize_trace(traces[0])
            print("\n" + visualization)
        
        # Example 6: Error Tracing
        print("\n" + "=" * 80)
        print("EXAMPLE 6: Error Tracing and Debugging")
        print("=" * 80)
        
        collector.clear_traces()
        trace_id = collector.start_trace(metadata={'test': 'error_handling'})
        
        # Log successful operation
        collector.log_event(
            event_type=TraceEventType.AGENT_START,
            name="operation_start"
        )
        
        # Log error
        collector.log_event(
            event_type=TraceEventType.ERROR,
            name="operation_failed",
            error="Simulated error: Connection timeout",
            metadata={'error_code': 'TIMEOUT', 'retry_count': 3}
        )
        
        collector.end_trace(trace_id, "failed")
        
        trace = collector.get_trace(trace_id)
        errors = analyzer.get_error_summary(trace)
        
        print(f"\nTrace with errors:")
        print(f"  Status: {trace.status}")
        print(f"  Error count: {len(errors)}")
        
        if errors:
            print("\nError Details:")
            for error in errors:
                print(f"  Event: {error['name']}")
                print(f"  Type: {error['event_type']}")
                print(f"  Error: {error['error']}")
                print(f"  Time: {error['timestamp']}")
        
        # Example 7: Trace Export and Storage
        print("\n" + "=" * 80)
        print("EXAMPLE 7: Trace Export for Storage/Analysis")
        print("=" * 80)
        
        if traces:
            trace = traces[0]
            trace_dict = trace.to_dict()
            
            print("\nTrace exported to dictionary:")
            print(f"  Trace ID: {trace_dict['trace_id'][:8]}...")
            print(f"  Duration: {trace_dict['duration_ms']:.2f}ms")
            print(f"  Event count: {trace_dict['event_count']}")
            print(f"  Status: {trace_dict['status']}")
            
            # Simulate JSON export
            trace_json = json.dumps(trace_dict, indent=2, default=str)
            print(f"\nJSON size: {len(trace_json)} bytes")
            print(f"First 200 chars:\n{trace_json[:200]}...")
        
        # Summary
        print("\n" + "=" * 80)
        print("TRACE/LINEAGE TRACKING SUMMARY")
        print("=" * 80)
        print("""
    Trace Tracking Benefits:
    1. Complete Visibility: See every step of agent execution
    2. Debugging Power: Reproduce and understand issues
    3. Performance Insights: Identify bottlenecks
    4. Compliance: Document decision-making
    5. Reproducibility: Replay executions exactly
    6. Optimization: Data-driven improvements
    
    Key Components:
    1. Trace Events
       - Agent lifecycle (start, end)
       - LLM calls (input, output, tokens, cost)
       - Tool invocations (name, args, results)
       - Decision points (conditions, branches)
       - State changes (before, after)
       - Errors (type, message, context)
    
    2. Event Hierarchy
       - Parent-child relationships
       - Nested spans for complex operations
       - Call stack preservation
       - Context propagation
       - Timing at each level
    
    3. Trace Metadata
       - User/session information
       - Request context
       - Environment details
       - Custom attributes
       - Tags for filtering
    
    4. Analysis Capabilities
       - Performance profiling
       - Path visualization
       - Error pattern detection
       - Statistical analysis
       - Comparison tools
    
    What to Trace:
    1. Essential (Always)
       - All LLM calls
       - Tool invocations
       - Agent start/end
       - Errors and exceptions
       - Final results
    
    2. Important (Most cases)
       - Decision points
       - State changes
       - Performance metrics
       - Resource usage
       - Intermediate results
    
    3. Detailed (Development/Debug)
       - Full context
       - Internal calculations
       - Validation steps
       - Cache hits/misses
       - Detailed timing
    
    Trace Structure:
    ```
    Trace
    ├── trace_id (unique identifier)
    ├── start_time
    ├── end_time
    ├── status (running, completed, failed)
    ├── metadata (user, session, etc.)
    └── events []
        ├── Event 1
        │   ├── event_id
        │   ├── parent_id
        │   ├── event_type
        │   ├── timestamp
        │   ├── duration_ms
        │   ├── input_data
        │   ├── output_data
        │   └── metadata
        └── Event 2
            └── ...
    ```
    
    Best Practices:
    1. Trace Organization
       - Unique trace IDs (UUID v4)
       - Clear event names
       - Consistent structure
       - Rich metadata
       - Logical grouping
    
    2. Performance
       - Async trace writes
       - Batch uploads
       - Sampling in production
       - Compression for storage
       - Resource limits
    
    3. Privacy
       - PII redaction
       - Sensitive data filtering
       - Access controls
       - Encryption at rest/transit
       - Retention policies
    
    4. Storage
       - Hot: 7-30 days (fast access)
       - Warm: 31-90 days (slower)
       - Cold: 91-365 days (archive)
       - Automatic cleanup
       - Efficient formats
    
    Production Deployment:
    1. Sampling Strategy
       - Development: 100%
       - Staging: 20-50%
       - Production: 1-10% baseline
       - Errors: Always 100%
       - Important users: Higher rate
    
    2. Performance Impact
       - Target: < 5% overhead
       - Measure actual impact
       - Optimize hot paths
       - Circuit breaker
       - Degradation handling
    
    3. Integration
       - LangSmith for LangChain
       - OpenTelemetry standard
       - Custom backends
       - Log aggregation systems
       - APM tools integration
    
    4. Analysis Workflow
       - Real-time monitoring
       - Periodic analysis
       - Alert on patterns
       - Dashboard visualization
       - Export for deep analysis
    
    When to Use Tracing:
    ✓ All production AI systems
    ✓ Complex multi-step agents
    ✓ Debugging difficult issues
    ✓ Performance optimization
    ✓ Compliance requirements
    ✓ Quality assurance
    ✗ Simple single-shot calls (maybe)
    ✗ Privacy-critical scenarios (with redaction)
    
    Common Use Cases:
    1. Debugging
       - Why did agent fail?
       - What was the decision path?
       - What inputs caused issue?
       - How to reproduce?
    
    2. Optimization
       - Which step is slowest?
       - Where are bottlenecks?
       - Cost breakdown by component
       - Cache effectiveness
    
    3. Monitoring
       - Error rate trends
       - Performance degradation
       - Usage patterns
       - Resource consumption
    
    4. Compliance
       - Decision documentation
       - Audit trails
       - Regulatory requirements
       - Accountability
    
    Tools & Services:
    - LangSmith: Native LangChain tracing
    - OpenTelemetry: Standard observability
    - Jaeger: Distributed tracing
    - DataDog: APM and tracing
    - Custom: Roll your own
    
    ROI Analysis:
    - Debug time: 50-90% reduction
    - Issue resolution: 2-10x faster
    - Performance gains: 20-50%
    - Compliance: Critical for regulated industries
    - Cost: Low overhead, high value
    """)
        
        print("\n" + "=" * 80)
        print("Pattern 094 (Trace/Lineage Tracking) demonstration complete!")
        print("=" * 80)
    
    
    if __name__ == "__main__":
        demonstrate_trace_tracking()
    
        

    Agentic Design Pattern: Trace Tracking

    Pattern 094: Trace Tracking – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Trace Tracking pattern implementation.


    Pattern Description

    Pattern 094: Trace/Lineage Tracking

    Description: Trace and Lineage Tracking captures the complete execution path of an agent, including all decisions, actions, intermediate states, and data transformations. This pattern is essential for debugging complex agent behaviors, understanding how decisions were made, ensuring compliance, and optimizing performance. Comprehensive tracing enables reproducibility, auditing, and continuous improvement of AI systems.

    Trace tracking provides:
    - Complete execution history
    - Decision point visibility
    - Data lineage and provenance
    - Performance metrics per step
    - Error context and stack traces
    - Reproducibility information
    

    Components: 1. Trace Capture – Execution events (start, end, error) – Decision points and branches – Input/output at each step – Timestamps and durations – Resource consumption – Agent state snapshots

    2. Trace Storage
       - Structured trace format
       - Hierarchical relationships
       - Searchable metadata
       - Efficient retrieval
       - Long-term retention
    
    3. Trace Analysis
       - Path visualization
       - Performance analysis
       - Bottleneck identification
       - Error pattern detection
       - Comparison tools
    
    4. Trace Replay
       - Reproduce executions
       - Debug specific paths
       - Test alternative branches
       - Validate fixes
       - Training data generation
    

    Use Cases: 1. Debugging – Understand unexpected behavior – Reproduce production issues – Identify error causes – Validate fixes – Root cause analysis

    2. Performance Optimization
       - Identify bottlenecks
       - Measure step durations
       - Analyze resource usage
       - Compare implementations
       - Optimize hot paths
    
    3. Compliance & Auditing
       - Document decision making
       - Prove regulatory compliance
       - Audit trails for sensitive operations
       - Accountability tracking
       - Legal requirements
    
    4. Quality Assurance
       - Validate agent behavior
       - Ensure determinism
       - Track test coverage
       - Regression detection
       - Change impact analysis
    
    5. Agent Development
       - Understand agent flow
       - Optimize prompts
       - Refine decision logic
       - Test edge cases
       - Documentation generation
    

    LangChain Implementation: LangChain supports tracing through: – LangSmith for comprehensive tracing – Custom callback handlers – OpenTelemetry integration – Structured logging – Trace context propagation

    Key Features: 1. Comprehensive Capture – Every agent step traced – Full context preservation – Hierarchical relationships – Performance metrics – Error details

    2. Structured Format
       - Consistent trace structure
       - Searchable attributes
       - Machine-readable format
       - Human-readable display
       - Standard schemas
    
    3. Powerful Analysis
       - Path visualization
       - Query capabilities
       - Statistical analysis
       - Comparison tools
       - Pattern detection
    
    4. Production Ready
       - Low overhead (< 5% impact)
       - Efficient storage
       - Privacy controls
       - Sampling strategies
       - Scalable architecture
    

    Best Practices: 1. What to Trace – All LLM calls (input, output, model, tokens) – Tool invocations (name, args, results) – Decision points (conditions, chosen branch) – State changes (before, after) – Errors (type, message, stack) – Performance (start, end, duration)

    2. Trace Organization
       - Clear trace IDs (unique, sortable)
       - Parent-child relationships
       - Logical grouping (by session, user, task)
       - Consistent naming
       - Rich metadata
    
    3. Performance Considerations
       - Sampling in production (1-10%)
       - Async trace writes
       - Compression for storage
       - Automatic rotation
       - Resource limits
    
    4. Privacy & Security
       - PII redaction
       - Access controls
       - Encryption at rest
       - Retention policies
       - Compliance adherence
    

    Trade-offs: Advantages: – Complete execution visibility – Powerful debugging capabilities – Compliance documentation – Performance insights – Reproducible executions – Historical analysis

    Disadvantages:
    - Storage overhead
    - Performance impact (small)
    - Complexity in setup
    - Privacy concerns
    - Analysis tools needed
    - Maintenance overhead
    

    Production Considerations: 1. Sampling Strategy – 100% for development – 10-20% for staging – 1-5% for production (baseline) – 100% for errors – Adaptive based on load

    2. Storage Management
       - Hot storage: 7-30 days
       - Cold storage: 90-365 days
       - Archive: > 1 year
       - Compression for old traces
       - Automatic cleanup
    
    3. Performance Impact
       - Target: < 5% overhead
       - Async writes preferred
       - Batch uploads
       - Local buffering
       - Circuit breaker
    
    4. Privacy Compliance
       - Automatic PII detection
       - Redaction policies
       - Consent tracking
       - Data retention limits
       - Right to deletion
    
    5. Analysis Tools
       - Web UI for browsing
       - Query interface
       - Visualization tools
       - Alert integration
       - Export capabilities
    

    File Statistics

    • Total Lines: 1067
    • Classes: 6
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 094: Trace/Lineage Tracking
    
    Description:
        Trace and Lineage Tracking captures the complete execution path of an agent, including
        all decisions, actions, intermediate states, and data transformations. This pattern is
        essential for debugging complex agent behaviors, understanding how decisions were made,
        ensuring compliance, and optimizing performance. Comprehensive tracing enables
        reproducibility, auditing, and continuous improvement of AI systems.
    
        Trace tracking provides:
        - Complete execution history
        - Decision point visibility
        - Data lineage and provenance
        - Performance metrics per step
        - Error context and stack traces
        - Reproducibility information
    
    Components:
        1. Trace Capture
           - Execution events (start, end, error)
           - Decision points and branches
           - Input/output at each step
           - Timestamps and durations
           - Resource consumption
           - Agent state snapshots
    
        2. Trace Storage
           - Structured trace format
           - Hierarchical relationships
           - Searchable metadata
           - Efficient retrieval
           - Long-term retention
    
        3. Trace Analysis
           - Path visualization
           - Performance analysis
           - Bottleneck identification
           - Error pattern detection
           - Comparison tools
    
        4. Trace Replay
           - Reproduce executions
           - Debug specific paths
           - Test alternative branches
           - Validate fixes
           - Training data generation
    
    Use Cases:
        1. Debugging
           - Understand unexpected behavior
           - Reproduce production issues
           - Identify error causes
           - Validate fixes
           - Root cause analysis
    
        2. Performance Optimization
           - Identify bottlenecks
           - Measure step durations
           - Analyze resource usage
           - Compare implementations
           - Optimize hot paths
    
        3. Compliance & Auditing
           - Document decision making
           - Prove regulatory compliance
           - Audit trails for sensitive operations
           - Accountability tracking
           - Legal requirements
    
        4. Quality Assurance
           - Validate agent behavior
           - Ensure determinism
           - Track test coverage
           - Regression detection
           - Change impact analysis
    
        5. Agent Development
           - Understand agent flow
           - Optimize prompts
           - Refine decision logic
           - Test edge cases
           - Documentation generation
    
    LangChain Implementation:
        LangChain supports tracing through:
        - LangSmith for comprehensive tracing
        - Custom callback handlers
        - OpenTelemetry integration
        - Structured logging
        - Trace context propagation
    
    Key Features:
        1. Comprehensive Capture
           - Every agent step traced
           - Full context preservation
           - Hierarchical relationships
           - Performance metrics
           - Error details
    
        2. Structured Format
           - Consistent trace structure
           - Searchable attributes
           - Machine-readable format
           - Human-readable display
           - Standard schemas
    
        3. Powerful Analysis
           - Path visualization
           - Query capabilities
           - Statistical analysis
           - Comparison tools
           - Pattern detection
    
        4. Production Ready
           - Low overhead (< 5% impact)
           - Efficient storage
           - Privacy controls
           - Sampling strategies
           - Scalable architecture
    
    Best Practices:
        1. What to Trace
           - All LLM calls (input, output, model, tokens)
           - Tool invocations (name, args, results)
           - Decision points (conditions, chosen branch)
           - State changes (before, after)
           - Errors (type, message, stack)
           - Performance (start, end, duration)
    
        2. Trace Organization
           - Clear trace IDs (unique, sortable)
           - Parent-child relationships
           - Logical grouping (by session, user, task)
           - Consistent naming
           - Rich metadata
    
        3. Performance Considerations
           - Sampling in production (1-10%)
           - Async trace writes
           - Compression for storage
           - Automatic rotation
           - Resource limits
    
        4. Privacy & Security
           - PII redaction
           - Access controls
           - Encryption at rest
           - Retention policies
           - Compliance adherence
    
    Trade-offs:
        Advantages:
        - Complete execution visibility
        - Powerful debugging capabilities
        - Compliance documentation
        - Performance insights
        - Reproducible executions
        - Historical analysis
    
        Disadvantages:
        - Storage overhead
        - Performance impact (small)
        - Complexity in setup
        - Privacy concerns
        - Analysis tools needed
        - Maintenance overhead
    
    Production Considerations:
        1. Sampling Strategy
           - 100% for development
           - 10-20% for staging
           - 1-5% for production (baseline)
           - 100% for errors
           - Adaptive based on load
    
        2. Storage Management
           - Hot storage: 7-30 days
           - Cold storage: 90-365 days
           - Archive: > 1 year
           - Compression for old traces
           - Automatic cleanup
    
        3. Performance Impact
           - Target: < 5% overhead
           - Async writes preferred
           - Batch uploads
           - Local buffering
           - Circuit breaker
    
        4. Privacy Compliance
           - Automatic PII detection
           - Redaction policies
           - Consent tracking
           - Data retention limits
           - Right to deletion
    
        5. Analysis Tools
           - Web UI for browsing
           - Query interface
           - Visualization tools
           - Alert integration
           - Export capabilities
    """
    

    Purpose: This module implements the Trace Tracking pattern, providing a structured approach to agent design and implementation.


    Import Statements

    Standard Library Imports

    import dataclasses.asdict
    import dataclasses.dataclass
    import dataclasses.field
    import datetime.datetime
    import enum.Enum
    import json
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import time
    import typing.Any
    import typing.Dict
    import typing.List
    import typing.Optional
    

    Purpose: Core Python functionality: – dataclasses.asdict: Simplified class definitions with @dataclass – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – enum.Enum: Enumeration support – json: JSON serialization/deserialization – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – time: Time-related functions – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

    Purpose: LangChain framework components: – LLM Interface: Wrapper for OpenAI models – Chains: Composable building blocks for complex workflows

    Other Imports

    from dotenv.load_dotenv
    from uuid
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: TraceEventType

    Lines 223-233

    Inherits from: Enum

    Purpose:

    Types of trace events
    

    Class: TraceEvent

    Lines 237-258

    Purpose:

    Represents a single event in a trace
    

    Methods:

    to_dict(self)

    Lines 253-258

    Purpose:

    Convert to dictionary
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D624550>


    Class: Trace

    Lines 262-289

    Purpose:

    Complete trace of an agent execution
    

    Methods:

    duration_ms(self)

    Lines 272-276

    Decorators: property

    Purpose:

    Calculate total duration
    

    Parameters:

    Returns: float

    to_dict(self)

    Lines 278-289

    Purpose:

    Convert to dictionary
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D645990>


    Class: TraceCollector

    Lines 292-441

    Purpose:

    Collects and manages traces for agent executions.
    
    Captures events, maintains relationships, and provides query capabilities.
    

    Methods:

    __init__(self)

    Lines 299-303

    Purpose:

    Initialize trace collector
    

    Parameters:

    start_trace(self, metadata)

    Lines 305-323

    Purpose:

    Start a new trace.
    
    Args:
        metadata: Optional trace metadata
    
    Returns:
        Trace ID
    

    Parameters: – metadata: Metadata

    Returns: str

    end_trace(self, trace_id, status)

    Lines 325-336

    Purpose:

    End a trace.
    
    Args:
        trace_id: Trace identifier
        status: Final status (completed, failed)
    

    Parameters: – trace_id: Trace id – status: Status

    log_event(self, event_type, name, input_data, output_data, metadata, error, duration_ms)

    Lines 338-386

    Purpose:

    Log a trace event.
    
    Args:
        event_type: Type of event
        name: Event name
        input_data: Input data
        output_data: Output data
        metadata: Additional metadata
        error: Error message if applicable
        duration_ms: Event duration
    
    Returns:
        Event ID
    

    Parameters: – event_type: Event type – name: Name – input_data: Input data – output_data: Output data – metadata: Metadata – error: Error – duration_ms: Duration ms

    Returns: str

    start_span(self, name, event_type)

    Lines 388-405

    Purpose:

    Start a span (nested event).
    
    Args:
        name: Span name
        event_type: Event type
    
    Returns:
        Event ID
    

    Parameters: – name: Name – event_type: Event type

    Returns: str

    end_span(self, event_id, output_data)

    Lines 407-427

    Purpose:

    End a span.
    
    Args:
        event_id: Event ID to end
        output_data: Output data
    

    Parameters: – event_id: Event id – output_data: Output data

    get_trace(self, trace_id)

    Lines 429-431

    Purpose:

    Get trace by ID
    

    Parameters: – trace_id: Trace id

    Returns: <ast.Subscript object at 0x000001A06D7C9750>

    get_all_traces(self)

    Lines 433-435

    Purpose:

    Get all traces
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D7C85D0>

    clear_traces(self)

    Lines 437-441

    Purpose:

    Clear all traces
    

    Parameters:


    Class: TraceAnalyzer

    Lines 444-579

    Purpose:

    Analyzes traces to extract insights and metrics.
    
    Provides visualization, statistics, and pattern detection.
    

    Methods:

    __init__(self)

    Lines 451-453

    Purpose:

    Initialize analyzer
    

    Parameters:

    get_execution_path(self, trace)

    Lines 455-465

    Purpose:

    Get execution path as list of event names.
    
    Args:
        trace: Trace to analyze
    
    Returns:
        List of event names in order
    

    Parameters: – trace: Trace

    Returns: <ast.Subscript object at 0x000001A06D660DD0>

    get_performance_stats(self, trace)

    Lines 467-498

    Purpose:

    Calculate performance statistics.
    
    Args:
        trace: Trace to analyze
    
    Returns:
        Performance statistics
    

    Parameters: – trace: Trace

    Returns: <ast.Subscript object at 0x000001A06D888850>

    get_event_type_stats(self, trace)

    Lines 500-514

    Purpose:

    Count events by type.
    
    Args:
        trace: Trace to analyze
    
    Returns:
        Event counts by type
    

    Parameters: – trace: Trace

    Returns: <ast.Subscript object at 0x000001A06D88B6D0>

    get_error_summary(self, trace)

    Lines 516-536

    Purpose:

    Get summary of errors in trace.
    
    Args:
        trace: Trace to analyze
    
    Returns:
        List of error details
    

    Parameters: – trace: Trace

    Returns: <ast.Subscript object at 0x000001A06D75B110>

    visualize_trace(self, trace)

    Lines 538-579

    Purpose:

    Create text-based visualization of trace.
    
    Args:
        trace: Trace to visualize
    
    Returns:
        Formatted string visualization
    

    Parameters: – trace: Trace

    Returns: str


    Class: TracedAgent

    Lines 582-661

    Purpose:

    Example agent with comprehensive tracing.
    
    Demonstrates trace collection for all agent operations.
    

    Methods:

    __init__(self, collector)

    Lines 589-597

    Purpose:

    Initialize traced agent.
    
    Args:
        collector: Trace collector instance
    

    Parameters: – collector: Collector

    process_query(self, query, metadata)

    Lines 599-647

    Purpose:

    Process query with full tracing.
    
    Args:
        query: User query
        metadata: Optional metadata
    
    Returns:
        Response
    

    Parameters: – query: Input query or question – metadata: Metadata

    Returns: str

    _analyze_query(self, query)

    Lines 649-655

    Purpose:

    Analyze query type
    

    Parameters: – query: Input query or question

    Returns: str

    _call_llm(self, query)

    Lines 657-661

    Purpose:

    Call LLM
    

    Parameters: – query: Input query or question

    Returns: str


    Functions

    demonstrate_trace_tracking()

    Lines 664-1062

    Purpose:

    Demonstrate trace and lineage tracking
    

    Key Concepts Summary

    Pattern Overview

    The Trace Tracking pattern provides a structured approach to building AI agents with specific capabilities and behaviors. This implementation leverages LangChain’s composable components to create a robust and maintainable solution.

    Core Components

    1. Agent Class: Main implementation of the pattern logic
    2. Prompt Engineering: Carefully crafted prompts to guide LLM behavior
    3. Tool Integration: Optional tools for extended functionality
    4. Memory Management: State handling across interactions
    5. Output Parsing: Structured response processing

    When to Use

    ✅ Good for: – Scenarios requiring this specific pattern’s strengths – Applications needing structured agent behavior – Systems where explainability is important – Production environments requiring reliability

    ❌ Not ideal for: – Simple single-prompt tasks – Scenarios requiring different pattern characteristics – Resource-constrained environments (if computationally intensive)


    Best Practices Demonstrated

    1. Environment Configuration

    • ✅ Use .env files for API keys
    • ✅ Never hardcode credentials
    • ✅ Validate environment variables on startup

    2. Error Handling

    • ✅ Graceful degradation on failures
    • ✅ Informative error messages
    • ✅ Logging for debugging

    3. Prompt Engineering

    • ✅ Clear, specific instructions
    • ✅ Structured output formats
    • ✅ Few-shot examples when beneficial

    4. Code Organization

    • ✅ Separation of concerns (class-based design)
    • ✅ Type hints for clarity
    • ✅ Docstrings for documentation

    5. Performance Optimization

    • ✅ Appropriate temperature settings
    • ✅ Token budget management
    • ✅ Caching where applicable

    LangChain Components Used

    • ChatOpenAI: OpenAI model interface
    • Chains: Sequential component composition

    Why LangChain? – Composable abstractions for LLM applications – Rich ecosystem of integrations – Production-ready patterns – Active community and development


    Execution Flow

    1. Initialize agent with configuration
       ↓
    2. Load environment and dependencies
       ↓
    3. Prepare prompt with input data
       ↓
    4. Invoke LLM through LangChain
       ↓
    5. Parse and validate response
       ↓
    6. Return formatted result
    

    Key Decision Points: – Model selection (temperature, model type) – Prompt structure (few-shot vs zero-shot) – Error handling strategy – Output format validation


    Performance Considerations

    Computational Cost

    • LLM Calls: Primary cost factor (based on tokens)
    • Latency: Depends on model and input size
    • Memory: Minimal for most patterns

    Optimization Strategies

    1. Prompt Optimization: Minimize tokens while maintaining clarity
    2. Caching: Cache repeated queries/responses
    3. Model Selection: Use smaller models when appropriate
    4. Batch Processing: Group similar requests
    5. Streaming: For long responses, stream tokens

    Cost Estimates (Approximate)

    • GPT-3.5-turbo: ~$0.002 per request
    • GPT-4: ~$0.03 per request
    • GPT-4-turbo: ~$0.01 per request

    Actual costs vary based on input/output token counts


    Common Pitfalls to Avoid

    1. Prompt Design Issues

    ❌ Vague or ambiguous instructions
    ✅ Clear, specific, testable prompts

    2. Error Handling

    ❌ Ignoring API errors or rate limits
    ✅ Implement retry logic and graceful degradation

    3. Token Management

    ❌ Exceeding model token limits
    ✅ Truncate inputs, chunk processing

    4. Security

    ❌ Exposing API keys in code
    ✅ Use environment variables

    5. Testing

    ❌ Only testing happy paths
    ✅ Test edge cases, failures, malformed inputs

    6. Cost Management

    ❌ Unmonitored API usage
    ✅ Implement budgets, alerts, and monitoring


    Further Improvements

    Production Enhancements

    1. Monitoring: Add logging, metrics, tracing
    2. Validation: Input/output schema validation
    3. Rate Limiting: Implement request throttling
    4. Caching: Redis or in-memory cache for responses
    5. Async Support: Non-blocking operations for scalability

    Feature Extensions

    1. Multi-Model Support: Switch between providers
    2. Fallback Models: Graceful degradation strategy
    3. Custom Tools: Domain-specific function calling
    4. Advanced Memory: Vector stores for long-term memory
    5. Evaluation: Automated testing and benchmarking

    Code Quality

    1. Testing: Unit tests, integration tests
    2. Documentation: API docs, usage examples
    3. Type Safety: Stricter type hints
    4. Configuration: Externalized config files

    Related Patterns

    • Pattern 093
    • Pattern 095

    Pattern Families: – Reasoning Patterns (001-011) – Multi-Agent Patterns (012-019) – Tool Use Patterns (020-025) – Memory Patterns (026-031) – And many more…


    Summary

    The Trace Tracking pattern provides a powerful approach to building AI agents with specific capabilities. This implementation demonstrates:

    ✅ Key Strengths: – Clean, maintainable code structure – LangChain integration for composability – Type hints and documentation – Production-ready patterns

    🎯 Best Use Cases: – Applications requiring this pattern’s characteristics – Systems needing structured agent behavior – Production environments requiring reliability

    📚 Learning Resources: – LangChain Documentation: https://python.langchain.com/ – Pattern Documentation: See related patterns – OpenAI API Reference: https://platform.openai.com/docs/


    File: 094_trace_tracking.py
    Total Lines: 1067
    Pattern Category: AI Agent Design Pattern #094


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Ab Testing

    
        """
    Pattern 093: A/B Testing Pattern
    
    Description:
        A/B Testing (also called split testing) compares two or more variants of an agent,
        prompt, model, or configuration to determine which performs better. This pattern is
        essential for data-driven optimization of AI systems, enabling evidence-based decisions
        about changes rather than relying on intuition. A/B testing helps identify improvements,
        validate hypotheses, and continuously optimize agent performance in production.
    
        A/B testing enables:
        - Objective comparison of variants
        - Statistical significance validation
        - Risk mitigation through gradual rollout
        - Continuous improvement culture
        - Data-driven decision making
        - ROI measurement for changes
    
    Components:
        1. Variant Definition
           - Control (baseline/current version)
           - Treatment (new version/s)
           - Configuration differences
           - Hypothesis statement
           - Success metrics
    
        2. Traffic Splitting
           - Random assignment
           - Consistent user experience
           - Traffic allocation ratios
           - Segment-based splitting
           - Gradual rollout
    
        3. Metrics Collection
           - Primary metrics (key objectives)
           - Secondary metrics (side effects)
           - Guardrail metrics (safety)
           - Performance metrics (cost, latency)
           - User satisfaction metrics
    
        4. Statistical Analysis
           - Sample size calculation
           - Significance testing
           - Confidence intervals
           - Effect size measurement
           - Winner determination
    
    Use Cases:
        1. Prompt Optimization
           - Compare prompt variations
           - Test different instructions
           - Optimize few-shot examples
           - Refine system messages
           - Improve output formatting
    
        2. Model Selection
           - Compare different models (GPT-4 vs GPT-3.5)
           - Test model parameters (temperature, etc.)
           - Evaluate fine-tuned models
           - Assess cost/performance trade-offs
           - Validate model upgrades
    
        3. Feature Testing
           - New capabilities evaluation
           - Tool addition impact
           - Memory strategy comparison
           - Retrieval method testing
           - Chain architecture optimization
    
        4. Configuration Tuning
           - Response length optimization
           - Timeout adjustment
           - Retry logic tuning
           - Cache strategy comparison
           - Rate limit optimization
    
        5. User Experience
           - Response style testing
           - Persona variations
           - Output format preferences
           - Interaction flow optimization
           - Error message improvements
    
    LangChain Implementation:
        LangChain supports A/B testing through:
        - Multiple chain configurations
        - Custom callbacks for tracking
        - LangSmith for experiment tracking
        - Flexible chain composition
        - Variant switching logic
    
    Key Features:
        1. Experiment Management
           - Multiple variants support
           - Traffic allocation control
           - Consistent assignment
           - Experiment lifecycle
           - Result tracking
    
        2. Metrics Framework
           - Flexible metric definitions
           - Automatic collection
           - Real-time monitoring
           - Historical comparison
           - Statistical analysis
    
        3. Safety Mechanisms
           - Guardrail metrics
           - Auto-stop on degradation
           - Gradual rollout
           - Easy rollback
           - Error rate monitoring
    
        4. Analysis Tools
           - Statistical significance
           - Confidence intervals
           - Visualization support
           - Winner determination
           - ROI calculation
    
    Best Practices:
        1. Experiment Design
           - Clear hypothesis
           - Single variable change
           - Appropriate metrics
           - Sufficient sample size
           - Realistic duration
    
        2. Traffic Allocation
           - Start small (5-10% treatment)
           - Increase gradually
           - Monitor continuously
           - Keep control stable
           - Random assignment
    
        3. Metric Selection
           - Primary metric (main goal)
           - Secondary metrics (side effects)
           - Guardrails (safety limits)
           - Leading indicators
           - Lagging indicators
    
        4. Analysis Rigor
           - Wait for significance
           - Check for novelty effects
           - Consider practical significance
           - Review all metrics
           - Document decisions
    
    Trade-offs:
        Advantages:
        - Objective decision making
        - Risk reduction
        - Continuous optimization
        - Evidence-based improvements
        - Stakeholder confidence
        - Measurable impact
    
        Disadvantages:
        - Requires traffic volume
        - Time to statistical significance
        - Complexity overhead
        - May slow innovation
        - Requires discipline
        - Can be misinterpreted
    
    Production Considerations:
        1. Sample Size
           - Calculate minimum required
           - Consider baseline metrics
           - Factor in expected lift
           - Account for variance
           - Plan for duration
    
        2. Experiment Duration
           - Minimum: 1-2 weeks
           - Cover business cycles
           - Avoid holidays/events
           - Monitor stability
           - Check for time effects
    
        3. Monitoring
           - Real-time dashboards
           - Alert on anomalies
           - Track guardrails
           - Monitor costs
           - Review regularly
    
        4. Decision Criteria
           - Statistical significance (p  values
        
        def get_mean(self, metric_name: str) -> float:
            """Calculate mean for a metric"""
            values = self.metrics.get(metric_name, [])
            return sum(values) / len(values) if values else 0.0
        
        def get_std(self, metric_name: str) -> float:
            """Calculate standard deviation for a metric"""
            values = self.metrics.get(metric_name, [])
            if len(values)  Variant:
            """
            Select variant for a request.
            
            Args:
                user_id: Optional user ID for consistent assignment
                
            Returns:
                Selected variant
            """
            # Use user_id for consistent assignment if provided
            if user_id:
                # Simple hash-based assignment for consistency
                hash_value = hash(user_id) % 1000 / 1000.0
            else:
                hash_value = random.random()
            
            # Select variant based on traffic allocation
            cumulative = 0.0
            for variant in self.variants:
                if variant.status != VariantStatus.ACTIVE:
                    continue
                
                cumulative += variant.traffic_allocation
                if hash_value  VariantMetrics:
            """Get metrics for a variant"""
            return self.variant_metrics.get(variant_id)
        
        def check_guardrails(self) -> Dict[str, bool]:
            """
            Check if guardrail metrics are within thresholds.
            
            Returns:
                Dictionary of guardrail checks
            """
            results = {}
            
            for metric_name, metric in self.metrics.items():
                if not metric.is_guardrail:
                    continue
                
                for variant in self.variants:
                    if variant.is_control:
                        continue
                    
                    variant_metrics = self.variant_metrics[variant.id]
                    mean_value = variant_metrics.get_mean(metric_name)
                    
                    if metric.threshold is not None:
                        if metric.higher_is_better:
                            passed = mean_value >= metric.threshold
                        else:
                            passed = mean_value  float:
            """
            Calculate t-statistic for two samples.
            
            Returns:
                T-statistic
            """
            if n1 < 2 or n2  float:
            """
            Calculate p-value from t-statistic (simplified).
            
            This is a simplified approximation for demonstration.
            Production code should use scipy.stats.
            
            Returns:
                P-value
            """
            # Simplified: convert to z-score approximation
            # For large samples, t-distribution ≈ normal distribution
            if df  2.0 else 0.5
            else:
                # Normal approximation for large samples
                return 0.01 if abs(t_stat) > 2.58 else 0.05 if abs(t_stat) > 1.96 else 0.5
        
        def compare_variants(
            self,
            experiment: ABExperiment,
            control_id: str,
            treatment_id: str,
            metric_name: str
        ) -> ABTestResult:
            """
            Compare two variants on a metric.
            
            Args:
                experiment: A/B experiment
                control_id: Control variant ID
                treatment_id: Treatment variant ID
                metric_name: Metric to compare
                
            Returns:
                Test result
            """
            control_metrics = experiment.get_variant_metrics(control_id)
            treatment_metrics = experiment.get_variant_metrics(treatment_id)
            
            control_mean = control_metrics.get_mean(metric_name)
            control_std = control_metrics.get_std(metric_name)
            control_n = control_metrics.sample_size
            
            treatment_mean = treatment_metrics.get_mean(metric_name)
            treatment_std = treatment_metrics.get_std(metric_name)
            treatment_n = treatment_metrics.sample_size
            
            # Calculate lift
            if control_mean != 0:
                lift = ((treatment_mean - control_mean) / control_mean) * 100
            else:
                lift = 0.0
            
            # Calculate t-statistic
            t_stat = self.calculate_t_statistic(
                treatment_mean, treatment_std, treatment_n,
                control_mean, control_std, control_n
            )
            
            # Calculate p-value
            df = control_n + treatment_n - 2
            p_value = self.calculate_p_value(t_stat, df)
            
            # Determine significance
            is_significant = p_value  control_mean else control_id
                else:
                    winner = treatment_id if treatment_mean  0 else "↓"
                print(f"   {result.metric_name}: {direction} {abs(result.lift):.1f}%")
        
        print("\n" + "=" * 80)
        print("RECOMMENDATION:")
        print("=" * 80)
        
        if primary_result.is_significant and all_passed and primary_result.lift > 0:
            print("""
    ✓ DEPLOY TREATMENT
    
    Justification:
    - Primary metric shows significant improvement
    - All guardrails passed
    - No significant degradation in secondary metrics
    - Safe to rollout to 100% of traffic
    
    Next Steps:
    1. Increase treatment traffic to 100%
    2. Monitor for 1 week
    3. Validate sustained improvement
    4. Document learnings
    """)
        else:
            print("""
    ✗ KEEP CONTROL
    
    Justification:
    - Treatment did not show sufficient improvement
    - OR guardrail metrics were violated
    - OR improvement not statistically significant
    
    Next Steps:
    1. Analyze why treatment underperformed
    2. Iterate on design
    3. Run new experiment
    4. Consider alternative approaches
    """)
        
        # Example 7: Gradual Rollout Simulation
        print("\n" + "=" * 80)
        print("EXAMPLE 7: Gradual Rollout Strategy")
        print("=" * 80)
        
        print("\nRecommended rollout plan:\n")
        
        rollout_stages = [
            {"traffic": 5, "duration": "2 days", "action": "Initial validation"},
            {"traffic": 10, "duration": "3 days", "action": "Monitor stability"},
            {"traffic": 25, "duration": "1 week", "action": "Expand cautiously"},
            {"traffic": 50, "duration": "1 week", "action": "Half traffic"},
            {"traffic": 100, "duration": "Ongoing", "action": "Full rollout"}
        ]
        
        for stage in rollout_stages:
            print(f"Stage {stage['traffic']}%:")
            print(f"  Duration: {stage['duration']}")
            print(f"  Action: {stage['action']}")
            print(f"  If issues detected: Rollback to previous stage")
            print()
        
        # Summary
        print("\n" + "=" * 80)
        print("A/B TESTING SUMMARY")
        print("=" * 80)
        print("""
    A/B Testing Benefits:
    1. Objective Decision Making: Data over intuition
    2. Risk Mitigation: Gradual rollout limits exposure
    3. Continuous Improvement: Systematic optimization
    4. Evidence-Based: Statistical confidence
    5. Stakeholder Alignment: Clear success criteria
    6. ROI Measurement: Quantifiable impact
    
    Key Components:
    1. Variants
       - Control: Current/baseline version
       - Treatment: New version to test
       - Clear differences between variants
       - Consistent user experience
    
    2. Metrics
       - Primary: Main success metric
       - Secondary: Side effect monitoring
       - Guardrails: Safety boundaries
       - Leading & lagging indicators
    
    3. Traffic Splitting
       - Random assignment for unbiased results
       - Consistent per-user (same user = same variant)
       - Flexible allocation (50/50, 90/10, etc.)
       - Gradual rollout capability
    
    4. Statistical Analysis
       - Significance testing (p-value < 0.05)
       - Effect size (practical significance)
       - Confidence intervals
       - Sample size validation
    
    Best Practices:
    1. Experiment Design
       - Clear hypothesis statement
       - Single variable change
       - Measurable success criteria
       - Appropriate sample size
       - Realistic timeline
    
    2. Metric Selection
       - One primary metric (main goal)
       - Multiple secondary metrics
       - Guardrail metrics (boundaries)
       - Balance short/long term
       - Track user sentiment
    
    3. Running Experiments
       - Minimum 1-2 weeks duration
       - Wait for statistical significance
       - Monitor guardrails continuously
       - Check for novelty effects
       - Consider seasonality
    
    4. Making Decisions
       - Require statistical significance (p < 0.05)
       - Check practical significance (meaningful lift)
       - Validate all guardrails passed
       - Review all secondary metrics
       - Document reasoning
    
    5. Rollout Strategy
       - Start small (5-10% traffic)
       - Increase gradually
       - Monitor at each stage
       - Easy rollback mechanism
       - Full rollout only after validation
    
    Common Pitfalls to Avoid:
    1. Stopping too early (not enough samples)
    2. Multiple testing without correction
    3. Ignoring guardrail metrics
    4. Changing experiment mid-flight
    5. Cherry-picking metrics
    6. Insufficient traffic volume
    7. Not considering novelty effects
    8. Forgetting about opportunity cost
    
    When to Use A/B Testing:
    ✓ Production systems with traffic
    ✓ Uncertain about impact
    ✓ Multiple competing approaches
    ✓ Optimization opportunities
    ✓ Risk-averse changes
    ✓ Stakeholder buy-in needed
    ✗ Low traffic applications
    ✗ Obviously correct changes
    ✗ Emergency fixes
    ✗ Insufficient time
    
    Sample Size Guidelines:
    - Minimum 100 samples per variant
    - More for smaller expected differences
    - Calculator: n = (Z * σ / E)²
      where Z = z-score (1.96 for 95% confidence)
            σ = standard deviation
            E = margin of error
    
    Statistical Significance:
    - Standard: p-value < 0.05 (95% confidence)
    - Conservative: p-value < 0.01 (99% confidence)
    - Always report effect size, not just p-value
    - Consider practical significance
    
    Production Tips:
    - Integrate with feature flags
    - Real-time monitoring dashboards
    - Automated guardrail alerts
    - Easy rollback capability
    - Experiment registry/documentation
    - Post-experiment analysis
    - Share learnings across team
    - Iterate based on results
    """)
        
        print("\n" + "=" * 80)
        print("Pattern 093 (A/B Testing) demonstration complete!")
        print("=" * 80)
    
    
    if __name__ == "__main__":
        demonstrate_ab_testing()
    
        

    Agentic Design Pattern: Ab Testing

    Pattern 093: Ab Testing – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Ab Testing pattern implementation.


    Pattern Description

    Pattern 093: A/B Testing Pattern

    Description: A/B Testing (also called split testing) compares two or more variants of an agent, prompt, model, or configuration to determine which performs better. This pattern is essential for data-driven optimization of AI systems, enabling evidence-based decisions about changes rather than relying on intuition. A/B testing helps identify improvements, validate hypotheses, and continuously optimize agent performance in production.

    A/B testing enables:
    - Objective comparison of variants
    - Statistical significance validation
    - Risk mitigation through gradual rollout
    - Continuous improvement culture
    - Data-driven decision making
    - ROI measurement for changes
    

    Components: 1. Variant Definition – Control (baseline/current version) – Treatment (new version/s) – Configuration differences – Hypothesis statement – Success metrics

    2. Traffic Splitting
       - Random assignment
       - Consistent user experience
       - Traffic allocation ratios
       - Segment-based splitting
       - Gradual rollout
    
    3. Metrics Collection
       - Primary metrics (key objectives)
       - Secondary metrics (side effects)
       - Guardrail metrics (safety)
       - Performance metrics (cost, latency)
       - User satisfaction metrics
    
    4. Statistical Analysis
       - Sample size calculation
       - Significance testing
       - Confidence intervals
       - Effect size measurement
       - Winner determination
    

    Use Cases: 1. Prompt Optimization – Compare prompt variations – Test different instructions – Optimize few-shot examples – Refine system messages – Improve output formatting

    2. Model Selection
       - Compare different models (GPT-4 vs GPT-3.5)
       - Test model parameters (temperature, etc.)
       - Evaluate fine-tuned models
       - Assess cost/performance trade-offs
       - Validate model upgrades
    
    3. Feature Testing
       - New capabilities evaluation
       - Tool addition impact
       - Memory strategy comparison
       - Retrieval method testing
       - Chain architecture optimization
    
    4. Configuration Tuning
       - Response length optimization
       - Timeout adjustment
       - Retry logic tuning
       - Cache strategy comparison
       - Rate limit optimization
    
    5. User Experience
       - Response style testing
       - Persona variations
       - Output format preferences
       - Interaction flow optimization
       - Error message improvements
    

    LangChain Implementation: LangChain supports A/B testing through: – Multiple chain configurations – Custom callbacks for tracking – LangSmith for experiment tracking – Flexible chain composition – Variant switching logic

    Key Features: 1. Experiment Management – Multiple variants support – Traffic allocation control – Consistent assignment – Experiment lifecycle – Result tracking

    2. Metrics Framework
       - Flexible metric definitions
       - Automatic collection
       - Real-time monitoring
       - Historical comparison
       - Statistical analysis
    
    3. Safety Mechanisms
       - Guardrail metrics
       - Auto-stop on degradation
       - Gradual rollout
       - Easy rollback
       - Error rate monitoring
    
    4. Analysis Tools
       - Statistical significance
       - Confidence intervals
       - Visualization support
       - Winner determination
       - ROI calculation
    

    Best Practices: 1. Experiment Design – Clear hypothesis – Single variable change – Appropriate metrics – Sufficient sample size – Realistic duration

    2. Traffic Allocation
       - Start small (5-10% treatment)
       - Increase gradually
       - Monitor continuously
       - Keep control stable
       - Random assignment
    
    3. Metric Selection
       - Primary metric (main goal)
       - Secondary metrics (side effects)
       - Guardrails (safety limits)
       - Leading indicators
       - Lagging indicators
    
    4. Analysis Rigor
       - Wait for significance
       - Check for novelty effects
       - Consider practical significance
       - Review all metrics
       - Document decisions
    

    Trade-offs: Advantages: – Objective decision making – Risk reduction – Continuous optimization – Evidence-based improvements – Stakeholder confidence – Measurable impact

    Disadvantages:
    - Requires traffic volume
    - Time to statistical significance
    - Complexity overhead
    - May slow innovation
    - Requires discipline
    - Can be misinterpreted
    

    Production Considerations: 1. Sample Size – Calculate minimum required – Consider baseline metrics – Factor in expected lift – Account for variance – Plan for duration

    2. Experiment Duration
       - Minimum: 1-2 weeks
       - Cover business cycles
       - Avoid holidays/events
       - Monitor stability
       - Check for time effects
    
    3. Monitoring
       - Real-time dashboards
       - Alert on anomalies
       - Track guardrails
       - Monitor costs
       - Review regularly
    
    4. Decision Criteria
       - Statistical significance (p < 0.05)
       - Practical significance (effect size)
       - Cost implications
       - All metrics green
       - Business alignment
    
    5. Rollout Strategy
       - Start with 5-10%
       - Monitor for issues
       - Increase to 50%
       - Full rollout if successful
       - Easy rollback plan
    

    File Statistics

    • Total Lines: 969
    • Classes: 7
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 093: A/B Testing Pattern
    
    Description:
        A/B Testing (also called split testing) compares two or more variants of an agent,
        prompt, model, or configuration to determine which performs better. This pattern is
        essential for data-driven optimization of AI systems, enabling evidence-based decisions
        about changes rather than relying on intuition. A/B testing helps identify improvements,
        validate hypotheses, and continuously optimize agent performance in production.
    
        A/B testing enables:
        - Objective comparison of variants
        - Statistical significance validation
        - Risk mitigation through gradual rollout
        - Continuous improvement culture
        - Data-driven decision making
        - ROI measurement for changes
    
    Components:
        1. Variant Definition
           - Control (baseline/current version)
           - Treatment (new version/s)
           - Configuration differences
           - Hypothesis statement
           - Success metrics
    
        2. Traffic Splitting
           - Random assignment
           - Consistent user experience
           - Traffic allocation ratios
           - Segment-based splitting
           - Gradual rollout
    
        3. Metrics Collection
           - Primary metrics (key objectives)
           - Secondary metrics (side effects)
           - Guardrail metrics (safety)
           - Performance metrics (cost, latency)
           - User satisfaction metrics
    
        4. Statistical Analysis
           - Sample size calculation
           - Significance testing
           - Confidence intervals
           - Effect size measurement
           - Winner determination
    
    Use Cases:
        1. Prompt Optimization
           - Compare prompt variations
           - Test different instructions
           - Optimize few-shot examples
           - Refine system messages
           - Improve output formatting
    
        2. Model Selection
           - Compare different models (GPT-4 vs GPT-3.5)
           - Test model parameters (temperature, etc.)
           - Evaluate fine-tuned models
           - Assess cost/performance trade-offs
           - Validate model upgrades
    
        3. Feature Testing
           - New capabilities evaluation
           - Tool addition impact
           - Memory strategy comparison
           - Retrieval method testing
           - Chain architecture optimization
    
        4. Configuration Tuning
           - Response length optimization
           - Timeout adjustment
           - Retry logic tuning
           - Cache strategy comparison
           - Rate limit optimization
    
        5. User Experience
           - Response style testing
           - Persona variations
           - Output format preferences
           - Interaction flow optimization
           - Error message improvements
    
    LangChain Implementation:
        LangChain supports A/B testing through:
        - Multiple chain configurations
        - Custom callbacks for tracking
        - LangSmith for experiment tracking
        - Flexible chain composition
        - Variant switching logic
    
    Key Features:
        1. Experiment Management
           - Multiple variants support
           - Traffic allocation control
           - Consistent assignment
           - Experiment lifecycle
           - Result tracking
    
        2. Metrics Framework
           - Flexible metric definitions
           - Automatic collection
           - Real-time monitoring
           - Historical comparison
           - Statistical analysis
    
        3. Safety Mechanisms
           - Guardrail metrics
           - Auto-stop on degradation
           - Gradual rollout
           - Easy rollback
           - Error rate monitoring
    
        4. Analysis Tools
           - Statistical significance
           - Confidence intervals
           - Visualization support
           - Winner determination
           - ROI calculation
    
    Best Practices:
        1. Experiment Design
           - Clear hypothesis
           - Single variable change
           - Appropriate metrics
           - Sufficient sample size
           - Realistic duration
    
        2. Traffic Allocation
           - Start small (5-10% treatment)
           - Increase gradually
           - Monitor continuously
           - Keep control stable
           - Random assignment
    
        3. Metric Selection
           - Primary metric (main goal)
           - Secondary metrics (side effects)
           - Guardrails (safety limits)
           - Leading indicators
           - Lagging indicators
    
        4. Analysis Rigor
           - Wait for significance
           - Check for novelty effects
           - Consider practical significance
           - Review all metrics
           - Document decisions
    
    Trade-offs:
        Advantages:
        - Objective decision making
        - Risk reduction
        - Continuous optimization
        - Evidence-based improvements
        - Stakeholder confidence
        - Measurable impact
    
        Disadvantages:
        - Requires traffic volume
        - Time to statistical significance
        - Complexity overhead
        - May slow innovation
        - Requires discipline
        - Can be misinterpreted
    
    Production Considerations:
        1. Sample Size
           - Calculate minimum required
           - Consider baseline metrics
           - Factor in expected lift
           - Account for variance
           - Plan for duration
    
        2. Experiment Duration
           - Minimum: 1-2 weeks
           - Cover business cycles
           - Avoid holidays/events
           - Monitor stability
           - Check for time effects
    
        3. Monitoring
           - Real-time dashboards
           - Alert on anomalies
           - Track guardrails
           - Monitor costs
           - Review regularly
    
        4. Decision Criteria
           - Statistical significance (p < 0.05)
           - Practical significance (effect size)
           - Cost implications
           - All metrics green
           - Business alignment
    
        5. Rollout Strategy
           - Start with 5-10%
           - Monitor for issues
           - Increase to 50%
           - Full rollout if successful
           - Easy rollback plan
    """
    

    Purpose: This module implements the Ab Testing pattern, providing a structured approach to agent design and implementation.


    Import Statements

    Standard Library Imports

    import dataclasses.dataclass
    import dataclasses.field
    import datetime.datetime
    import enum.Enum
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import random
    import time
    import typing.Any
    import typing.Callable
    import typing.Dict
    import typing.List
    import typing.Optional
    

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – enum.Enum: Enumeration support – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – random: Random number generation – time: Time-related functions – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Callable: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

    Purpose: LangChain framework components: – LLM Interface: Wrapper for OpenAI models – Chains: Composable building blocks for complex workflows

    Other Imports

    from dotenv.load_dotenv
    from math
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: VariantStatus

    Lines 221-226

    Inherits from: Enum

    Purpose:

    Variant status in experiment
    

    Class: Variant

    Lines 230-238

    Purpose:

    Represents an experiment variant
    

    Class: Metric

    Lines 242-249

    Purpose:

    Represents a metric to track
    

    Class: VariantMetrics

    Lines 253-272

    Purpose:

    Metrics for a variant
    

    Methods:

    get_mean(self, metric_name)

    Lines 259-262

    Purpose:

    Calculate mean for a metric
    

    Parameters: – metric_name: Metric name

    Returns: float

    get_std(self, metric_name)

    Lines 264-272

    Purpose:

    Calculate standard deviation for a metric
    

    Parameters: – metric_name: Metric name

    Returns: float


    Class: ABTestResult

    Lines 276-288

    Purpose:

    Result of A/B test analysis
    

    Class: ABExperiment

    Lines 291-418

    Purpose:

    A/B test experiment manager.
    
    Manages variants, traffic allocation, and metrics collection.
    

    Methods:

    __init__(self, experiment_id, name, description, metrics)

    Lines 298-320

    Purpose:

    Initialize A/B experiment.
    
    Args:
        experiment_id: Unique experiment identifier
        name: Experiment name
        description: Experiment description
        metrics: Metrics to track
    

    Parameters: – experiment_id: Experiment id – name: Name – description: Description – metrics: Metrics

    add_variant(self, variant)

    Lines 322-334

    Purpose:

    Add a variant to the experiment.
    
    Args:
        variant: Variant to add
    

    Parameters: – variant: Variant

    select_variant(self, user_id)

    Lines 336-364

    Purpose:

    Select variant for a request.
    
    Args:
        user_id: Optional user ID for consistent assignment
    
    Returns:
        Selected variant
    

    Parameters: – user_id: User id

    Returns: Variant

    record_metric(self, variant_id, metric_name, value)

    Lines 366-384

    Purpose:

    Record a metric value for a variant.
    
    Args:
        variant_id: Variant identifier
        metric_name: Metric name
        value: Metric value
    

    Parameters: – variant_id: Variant id – metric_name: Metric name – value: Value

    get_variant_metrics(self, variant_id)

    Lines 386-388

    Purpose:

    Get metrics for a variant
    

    Parameters: – variant_id: Variant id

    Returns: VariantMetrics

    check_guardrails(self)

    Lines 390-418

    Purpose:

    Check if guardrail metrics are within thresholds.
    
    Returns:
        Dictionary of guardrail checks
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D7BA250>


    Class: ABTestAnalyzer

    Lines 421-555

    Purpose:

    Analyzes A/B test results and determines winners.
    
    Performs statistical tests and calculates significance.
    

    Methods:

    __init__(self, confidence_level)

    Lines 428-436

    Purpose:

    Initialize analyzer.
    
    Args:
        confidence_level: Confidence level for significance (default 95%)
    

    Parameters: – confidence_level: Confidence level

    calculate_t_statistic(self, mean1, std1, n1, mean2, std2, n2)

    Lines 438-463

    Purpose:

    Calculate t-statistic for two samples.
    
    Returns:
        T-statistic
    

    Parameters: – mean1: Mean1 – std1: Std1 – n1: N1 – mean2: Mean2 – std2: Std2 – n2: N2

    Returns: float

    calculate_p_value(self, t_stat, df)

    Lines 465-482

    Purpose:

    Calculate p-value from t-statistic (simplified).
    
    This is a simplified approximation for demonstration.
    Production code should use scipy.stats.
    
    Returns:
        P-value
    

    Parameters: – t_stat: T stat – df: Df

    Returns: float

    compare_variants(self, experiment, control_id, treatment_id, metric_name)

    Lines 484-555

    Purpose:

    Compare two variants on a metric.
    
    Args:
        experiment: A/B experiment
        control_id: Control variant ID
        treatment_id: Treatment variant ID
        metric_name: Metric to compare
    
    Returns:
        Test result
    

    Parameters: – experiment: Experiment – control_id: Control id – treatment_id: Treatment id – metric_name: Metric name

    Returns: ABTestResult


    Functions

    demonstrate_ab_testing()

    Lines 558-964

    Purpose:

    Demonstrate A/B testing pattern
    

    Key Concepts Summary

    Pattern Overview

    The Ab Testing pattern provides a structured approach to building AI agents with specific capabilities and behaviors. This implementation leverages LangChain’s composable components to create a robust and maintainable solution.

    Core Components

    1. Agent Class: Main implementation of the pattern logic
    2. Prompt Engineering: Carefully crafted prompts to guide LLM behavior
    3. Tool Integration: Optional tools for extended functionality
    4. Memory Management: State handling across interactions
    5. Output Parsing: Structured response processing

    When to Use

    ✅ Good for: – Scenarios requiring this specific pattern’s strengths – Applications needing structured agent behavior – Systems where explainability is important – Production environments requiring reliability

    ❌ Not ideal for: – Simple single-prompt tasks – Scenarios requiring different pattern characteristics – Resource-constrained environments (if computationally intensive)


    Best Practices Demonstrated

    1. Environment Configuration

    • ✅ Use .env files for API keys
    • ✅ Never hardcode credentials
    • ✅ Validate environment variables on startup

    2. Error Handling

    • ✅ Graceful degradation on failures
    • ✅ Informative error messages
    • ✅ Logging for debugging

    3. Prompt Engineering

    • ✅ Clear, specific instructions
    • ✅ Structured output formats
    • ✅ Few-shot examples when beneficial

    4. Code Organization

    • ✅ Separation of concerns (class-based design)
    • ✅ Type hints for clarity
    • ✅ Docstrings for documentation

    5. Performance Optimization

    • ✅ Appropriate temperature settings
    • ✅ Token budget management
    • ✅ Caching where applicable

    LangChain Components Used

    • ChatOpenAI: OpenAI model interface
    • Chains: Sequential component composition

    Why LangChain? – Composable abstractions for LLM applications – Rich ecosystem of integrations – Production-ready patterns – Active community and development


    Execution Flow

    1. Initialize agent with configuration
       ↓
    2. Load environment and dependencies
       ↓
    3. Prepare prompt with input data
       ↓
    4. Invoke LLM through LangChain
       ↓
    5. Parse and validate response
       ↓
    6. Return formatted result
    

    Key Decision Points: – Model selection (temperature, model type) – Prompt structure (few-shot vs zero-shot) – Error handling strategy – Output format validation


    Performance Considerations

    Computational Cost

    • LLM Calls: Primary cost factor (based on tokens)
    • Latency: Depends on model and input size
    • Memory: Minimal for most patterns

    Optimization Strategies

    1. Prompt Optimization: Minimize tokens while maintaining clarity
    2. Caching: Cache repeated queries/responses
    3. Model Selection: Use smaller models when appropriate
    4. Batch Processing: Group similar requests
    5. Streaming: For long responses, stream tokens

    Cost Estimates (Approximate)

    • GPT-3.5-turbo: ~$0.002 per request
    • GPT-4: ~$0.03 per request
    • GPT-4-turbo: ~$0.01 per request

    Actual costs vary based on input/output token counts


    Common Pitfalls to Avoid

    1. Prompt Design Issues

    ❌ Vague or ambiguous instructions
    ✅ Clear, specific, testable prompts

    2. Error Handling

    ❌ Ignoring API errors or rate limits
    ✅ Implement retry logic and graceful degradation

    3. Token Management

    ❌ Exceeding model token limits
    ✅ Truncate inputs, chunk processing

    4. Security

    ❌ Exposing API keys in code
    ✅ Use environment variables

    5. Testing

    ❌ Only testing happy paths
    ✅ Test edge cases, failures, malformed inputs

    6. Cost Management

    ❌ Unmonitored API usage
    ✅ Implement budgets, alerts, and monitoring


    Further Improvements

    Production Enhancements

    1. Monitoring: Add logging, metrics, tracing
    2. Validation: Input/output schema validation
    3. Rate Limiting: Implement request throttling
    4. Caching: Redis or in-memory cache for responses
    5. Async Support: Non-blocking operations for scalability

    Feature Extensions

    1. Multi-Model Support: Switch between providers
    2. Fallback Models: Graceful degradation strategy
    3. Custom Tools: Domain-specific function calling
    4. Advanced Memory: Vector stores for long-term memory
    5. Evaluation: Automated testing and benchmarking

    Code Quality

    1. Testing: Unit tests, integration tests
    2. Documentation: API docs, usage examples
    3. Type Safety: Stricter type hints
    4. Configuration: Externalized config files

    Related Patterns

    • Pattern 092
    • Pattern 094

    Pattern Families: – Reasoning Patterns (001-011) – Multi-Agent Patterns (012-019) – Tool Use Patterns (020-025) – Memory Patterns (026-031) – And many more…


    Summary

    The Ab Testing pattern provides a powerful approach to building AI agents with specific capabilities. This implementation demonstrates:

    ✅ Key Strengths: – Clean, maintainable code structure – LangChain integration for composability – Type hints and documentation – Production-ready patterns

    🎯 Best Use Cases: – Applications requiring this pattern’s characteristics – Systems needing structured agent behavior – Production environments requiring reliability

    📚 Learning Resources: – LangChain Documentation: https://python.langchain.com/ – Pattern Documentation: See related patterns – OpenAI API Reference: https://platform.openai.com/docs/


    File: 093_ab_testing.py
    Total Lines: 969
    Pattern Category: AI Agent Design Pattern #093


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Simulation Testing

    
        """
    Pattern 092: Simulation Testing
    
    Description:
        Simulation Testing creates controlled, reproducible environments to test agent behavior
        under various scenarios, conditions, and edge cases. Unlike production testing,
        simulations allow you to test dangerous, expensive, or rare scenarios safely and
        repeatedly. This pattern is essential for validating agent behavior before deployment
        and for stress testing system limits.
    
        Simulation testing enables:
        - Safe testing of dangerous scenarios
        - Reproducible test conditions
        - Stress and load testing
        - Edge case exploration
        - Multi-agent interaction testing
        - Cost-effective experimentation
    
    Components:
        1. Simulation Environment
           - Mock external services (APIs, databases)
           - Configurable conditions (delays, errors, loads)
           - State management and reset
           - Recording and replay capabilities
           - Deterministic execution
    
        2. Scenario Definition
           - Initial state setup
           - Event sequences
           - Expected outcomes
           - Success criteria
           - Failure conditions
    
        3. Test Harness
           - Scenario execution engine
           - Result validation
           - Metrics collection
           - Logging and tracing
           - Report generation
    
        4. Mock Components
           - LLM response mocking
           - Tool/API simulation
           - User interaction simulation
           - External system simulation
           - Network condition simulation
    
    Use Cases:
        1. Safety Testing
           - Dangerous scenarios without risk
           - Edge case handling
           - Failure mode analysis
           - Recovery testing
           - Adversarial inputs
    
        2. Performance Testing
           - Load testing (many concurrent requests)
           - Stress testing (beyond capacity)
           - Endurance testing (long duration)
           - Spike testing (sudden load changes)
           - Scalability validation
    
        3. Integration Testing
           - Multi-component interaction
           - External service integration
           - Data flow validation
           - Error propagation
           - State consistency
    
        4. Regression Testing
           - Validate fixes don't break features
           - Ensure consistent behavior
           - Track performance changes
           - Verify improvements
           - Document behavior changes
    
        5. Exploratory Testing
           - Discover edge cases
           - Test rare scenarios
           - Validate assumptions
           - Find unexpected behaviors
           - Stress system boundaries
    
    LangChain Implementation:
        LangChain supports simulation through:
        - Mock LLM implementations
        - Custom callbacks for control
        - Test chain compositions
        - Fake embeddings for testing
        - Memory simulation
    
    Key Features:
        1. Controllable Environment
           - Deterministic responses
           - Configurable delays
           - Error injection
           - State manipulation
           - Reproducible conditions
    
        2. Comprehensive Scenarios
           - Normal operations
           - Error conditions
           - Edge cases
           - Load patterns
           - Attack scenarios
    
        3. Detailed Observation
           - Action logging
           - State tracking
           - Performance metrics
           - Error recording
           - Decision paths
    
        4. Flexible Validation
           - Multiple success criteria
           - Partial success handling
           - Performance thresholds
           - State assertions
           - Behavior verification
    
    Best Practices:
        1. Scenario Design
           - Start simple, add complexity
           - Cover happy path first
           - Add error scenarios
           - Include edge cases
           - Test boundaries
    
        2. Mock Fidelity
           - Balance realism vs simplicity
           - Accurate error simulation
           - Realistic timing
           - State consistency
           - Clear documentation
    
        3. Test Independence
           - Each test isolated
           - Clean state between tests
           - No shared dependencies
           - Deterministic execution
           - Parallel-safe
    
        4. Performance
           - Fast execution ( str:
            """
            Mock invoke method.
            
            Args:
                input: Input string
                
            Returns:
                Mock response
            """
            self.call_count += 1
            self.call_history.append({
                "input": input,
                "timestamp": datetime.now()
            })
            
            # Find matching response
            response = self.responses.get(input, self.default_response)
            
            # Simulate delay
            if response.delay > 0:
                time.sleep(response.delay)
            
            # Simulate error
            if response.should_error:
                raise Exception(response.error_message or "Mock error")
            
            return response.content
        
        def add_response(self, input: str, response: MockResponse):
            """Add a response pattern"""
            self.responses[input] = response
        
        def reset(self):
            """Reset mock state"""
            self.call_count = 0
            self.call_history.clear()
    
    
    class MockTool:
        """
        Mock tool for testing agent tool use.
        
        Simulates external tools without actual execution.
        """
        
        def __init__(
            self,
            name: str,
            behavior: Callable[[str], str],
            should_fail: bool = False,
            failure_rate: float = 0.0
        ):
            """
            Initialize mock tool.
            
            Args:
                name: Tool name
                behavior: Function defining tool behavior
                should_fail: Whether tool should always fail
                failure_rate: Probability of failure (0-1)
            """
            self.name = name
            self.behavior = behavior
            self.should_fail = should_fail
            self.failure_rate = failure_rate
            self.call_count = 0
        
        def execute(self, input: str) -> str:
            """
            Execute tool with mock behavior.
            
            Args:
                input: Tool input
                
            Returns:
                Tool output
            """
            self.call_count += 1
            
            # Simulate failure
            if self.should_fail or random.random()  Any:
            """Get environment state"""
            return self.state.get(key)
        
        def log_event(self, event_type: str, details: Dict[str, Any]):
            """Log an event"""
            self.event_log.append({
                "timestamp": datetime.now(),
                "type": event_type,
                "details": details
            })
        
        def reset(self):
            """Reset environment to initial state"""
            self.mock_llm.reset()
            for tool in self.mock_tools.values():
                tool.call_count = 0
            self.state.clear()
            self.event_log.clear()
    
    
    class SimulationTester:
        """
        Executes and validates simulation tests.
        
        Runs scenarios in controlled environment and collects results.
        """
        
        def __init__(self, environment: SimulationEnvironment):
            """
            Initialize tester.
            
            Args:
                environment: Simulation environment
            """
            self.environment = environment
        
        def run_scenario(self, scenario: SimulationScenario) -> SimulationResult:
            """
            Run a simulation scenario.
            
            Args:
                scenario: Scenario to run
                
            Returns:
                Simulation result
            """
            # Reset environment
            self.environment.reset()
            
            # Setup initial state
            for key, value in scenario.initial_state.items():
                self.environment.set_state(key, value)
            
            start_time = time.time()
            observations = []
            errors = []
            events_executed = 0
            
            try:
                # Execute events
                for event in scenario.events:
                    event_type = event.get("type")
                    
                    if event_type == "llm_call":
                        input_text = event.get("input", "")
                        try:
                            response = self.environment.mock_llm.invoke(input_text)
                            observations.append(f"LLM called: {input_text[:50]}...")
                            observations.append(f"LLM response: {response[:50]}...")
                            events_executed += 1
                        except Exception as e:
                            errors.append(f"LLM error: {str(e)}")
                    
                    elif event_type == "tool_call":
                        tool_name = event.get("tool")
                        tool_input = event.get("input", "")
                        
                        if tool_name in self.environment.mock_tools:
                            try:
                                result = self.environment.mock_tools[tool_name].execute(tool_input)
                                observations.append(f"Tool {tool_name} called")
                                observations.append(f"Tool result: {result[:50]}...")
                                events_executed += 1
                            except Exception as e:
                                errors.append(f"Tool error: {str(e)}")
                    
                    elif event_type == "state_update":
                        key = event.get("key")
                        value = event.get("value")
                        self.environment.set_state(key, value)
                        observations.append(f"State updated: {key}={value}")
                        events_executed += 1
                    
                    # Log event
                    self.environment.log_event(event_type, event)
                
                # Evaluate success criteria
                success = scenario.success_criteria(self.environment.state)
                
                # Determine outcome
                if errors:
                    outcome = OutcomeType.ERROR
                elif success:
                    outcome = OutcomeType.SUCCESS
                elif events_executed  List[SimulationResult]:
            """
            Run multiple scenarios.
            
            Args:
                scenarios: List of scenarios
                
            Returns:
                List of results
            """
            results = []
            for scenario in scenarios:
                result = self.run_scenario(scenario)
                results.append(result)
            return results
    
    
    def demonstrate_simulation_testing():
        """Demonstrate simulation testing"""
        print("=" * 80)
        print("SIMULATION TESTING DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Setup Simulation Environment
        print("\n" + "=" * 80)
        print("EXAMPLE 1: Creating Simulation Environment")
        print("=" * 80)
        
        env = SimulationEnvironment()
        
        # Configure mock LLM responses
        env.setup_mock_llm({
            "What is 2+2?": MockResponse(content="4", delay=0.1),
            "What is the capital of France?": MockResponse(content="Paris", delay=0.1),
            "Error test": MockResponse(
                content="",
                should_error=True,
                error_message="Simulated LLM error"
            )
        })
        
        # Add mock tools
        calculator = MockTool(
            name="calculator",
            behavior=lambda x: str(eval(x)),  # Simple eval for demo
            failure_rate=0.0
        )
        env.add_mock_tool(calculator)
        
        search = MockTool(
            name="search",
            behavior=lambda x: f"Search results for: {x}",
            failure_rate=0.0
        )
        env.add_mock_tool(search)
        
        print("\nSimulation environment configured:")
        print(f"  Mock LLM: {len(env.mock_llm.responses)} responses")
        print(f"  Mock Tools: {len(env.mock_tools)} tools")
        print(f"    - calculator")
        print(f"    - search")
        
        # Example 2: Normal Operation Scenario
        print("\n" + "=" * 80)
        print("EXAMPLE 2: Normal Operation Scenario")
        print("=" * 80)
        
        normal_scenario = SimulationScenario(
            id="scenario_001",
            name="Normal Q&A Flow",
            description="Test normal question answering",
            scenario_type=ScenarioType.NORMAL,
            initial_state={"question_count": 0},
            events=[
                {"type": "llm_call", "input": "What is 2+2?"},
                {"type": "state_update", "key": "question_count", "value": 1},
                {"type": "llm_call", "input": "What is the capital of France?"},
                {"type": "state_update", "key": "question_count", "value": 2}
            ],
            success_criteria=lambda state: state.get("question_count", 0) == 2
        )
        
        tester = SimulationTester(env)
        result = tester.run_scenario(normal_scenario)
        
        print(f"\nScenario: {normal_scenario.name}")
        print(f"Type: {normal_scenario.scenario_type.value}")
        print(f"Outcome: {result.outcome.value}")
        print(f"Duration: {result.duration:.3f}s")
        print(f"Events executed: {result.events_executed}/{len(normal_scenario.events)}")
        print(f"Success criteria met: {result.success_criteria_met}")
        print(f"\nMetrics:")
        for key, value in result.metrics.items():
            print(f"  {key}: {value}")
        
        # Example 3: Error Handling Scenario
        print("\n" + "=" * 80)
        print("EXAMPLE 3: Error Handling Scenario")
        print("=" * 80)
        
        error_scenario = SimulationScenario(
            id="scenario_002",
            name="LLM Error Recovery",
            description="Test error handling when LLM fails",
            scenario_type=ScenarioType.ERROR,
            initial_state={"error_handled": False},
            events=[
                {"type": "llm_call", "input": "Error test"},
                {"type": "state_update", "key": "error_handled", "value": True}
            ],
            success_criteria=lambda state: state.get("error_handled", False)
        )
        
        error_result = tester.run_scenario(error_scenario)
        
        print(f"\nScenario: {error_scenario.name}")
        print(f"Outcome: {error_result.outcome.value}")
        print(f"Errors encountered: {len(error_result.errors)}")
        for error in error_result.errors:
            print(f"  - {error}")
        
        # Example 4: Tool Usage Scenario
        print("\n" + "=" * 80)
        print("EXAMPLE 4: Tool Usage Scenario")
        print("=" * 80)
        
        tool_scenario = SimulationScenario(
            id="scenario_003",
            name="Calculator Tool Usage",
            description="Test agent using calculator tool",
            scenario_type=ScenarioType.NORMAL,
            initial_state={"calculations_done": 0},
            events=[
                {"type": "tool_call", "tool": "calculator", "input": "10 + 5"},
                {"type": "state_update", "key": "calculations_done", "value": 1},
                {"type": "tool_call", "tool": "calculator", "input": "20 * 3"},
                {"type": "state_update", "key": "calculations_done", "value": 2}
            ],
            success_criteria=lambda state: state.get("calculations_done", 0) == 2
        )
        
        tool_result = tester.run_scenario(tool_scenario)
        
        print(f"\nScenario: {tool_scenario.name}")
        print(f"Outcome: {tool_result.outcome.value}")
        print(f"Tool calls: {tool_result.metrics['tool_calls']}")
        print(f"Success: {tool_result.success_criteria_met}")
        
        print("\nObservations:")
        for obs in tool_result.observations:
            print(f"  {obs}")
        
        # Example 5: Edge Case Scenario
        print("\n" + "=" * 80)
        print("EXAMPLE 5: Edge Case Scenario")
        print("=" * 80)
        
        edge_scenario = SimulationScenario(
            id="scenario_004",
            name="Empty Input Handling",
            description="Test handling of empty inputs",
            scenario_type=ScenarioType.EDGE_CASE,
            initial_state={"edge_case_handled": False},
            events=[
                {"type": "llm_call", "input": ""},
                {"type": "state_update", "key": "edge_case_handled", "value": True}
            ],
            success_criteria=lambda state: state.get("edge_case_handled", False)
        )
        
        edge_result = tester.run_scenario(edge_scenario)
        
        print(f"\nScenario: {edge_scenario.name}")
        print(f"Type: {edge_scenario.scenario_type.value}")
        print(f"Outcome: {edge_result.outcome.value}")
        print(f"Success: {edge_result.success_criteria_met}")
        
        # Example 6: Load Testing Simulation
        print("\n" + "=" * 80)
        print("EXAMPLE 6: Load Testing Simulation")
        print("=" * 80)
        
        print("\nSimulating high load scenario...")
        
        load_scenarios = []
        for i in range(10):
            scenario = SimulationScenario(
                id=f"load_test_{i}",
                name=f"Load Test {i}",
                description="Concurrent request simulation",
                scenario_type=ScenarioType.LOAD,
                initial_state={},
                events=[
                    {"type": "llm_call", "input": f"Query {i}"},
                ],
                success_criteria=lambda state: True
            )
            load_scenarios.append(scenario)
        
        start_time = time.time()
        load_results = tester.run_scenarios(load_scenarios)
        total_time = time.time() - start_time
        
        successful = sum(1 for r in load_results if r.outcome == OutcomeType.SUCCESS)
        
        print(f"\nLoad test completed:")
        print(f"  Total scenarios: {len(load_scenarios)}")
        print(f"  Successful: {successful}")
        print(f"  Failed: {len(load_scenarios) - successful}")
        print(f"  Total time: {total_time:.2f}s")
        print(f"  Avg time per scenario: {total_time/len(load_scenarios):.3f}s")
        
        # Example 7: Comprehensive Test Suite
        print("\n" + "=" * 80)
        print("EXAMPLE 7: Running Comprehensive Test Suite")
        print("=" * 80)
        
        test_suite = [normal_scenario, error_scenario, tool_scenario, edge_scenario]
        
        print(f"\nRunning {len(test_suite)} scenarios...\n")
        
        suite_results = tester.run_scenarios(test_suite)
        
        # Analyze results
        by_outcome = {}
        for result in suite_results:
            outcome = result.outcome.value
            by_outcome[outcome] = by_outcome.get(outcome, 0) + 1
        
        print("Test Suite Results:")
        print(f"  Total: {len(suite_results)}")
        for outcome, count in by_outcome.items():
            print(f"  {outcome.upper()}: {count}")
        
        success_rate = (by_outcome.get("success", 0) / len(suite_results)) * 100
        print(f"\nSuccess Rate: {success_rate:.1f}%")
        
        print("\nDetailed Results:")
        for result in suite_results:
            status = "✓" if result.outcome == OutcomeType.SUCCESS else "✗"
            print(f"  {status} {result.scenario_id}: {result.outcome.value}")
            print(f"     Duration: {result.duration:.3f}s")
            print(f"     Events: {result.events_executed}")
            if result.errors:
                print(f"     Errors: {len(result.errors)}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SIMULATION TESTING SUMMARY")
        print("=" * 80)
        print("""
    Simulation Testing Benefits:
    1. Safety: Test dangerous scenarios without risk
    2. Reproducibility: Consistent test conditions
    3. Speed: Fast execution without API calls
    4. Cost: No real API costs during testing
    5. Control: Complete control over conditions
    6. Coverage: Test rare and edge cases easily
    
    Simulation Components:
    1. Mock LLM
       - Pre-configured responses
       - Error simulation
       - Delay simulation
       - Call tracking
       - Deterministic behavior
    
    2. Mock Tools
       - Simulated tool behavior
       - Failure injection
       - Performance simulation
       - Call counting
       - State management
    
    3. Simulation Environment
       - State management
       - Event logging
       - Component coordination
       - Reset capabilities
       - Observation collection
    
    Scenario Types:
    1. Normal Operation
       - Happy path testing
       - Expected workflows
       - Common use cases
       - Standard behavior
       - Success criteria validation
    
    2. Error Handling
       - LLM failures
       - Tool failures
       - Network errors
       - Timeout scenarios
       - Recovery mechanisms
    
    3. Edge Cases
       - Boundary conditions
       - Empty inputs
       - Large inputs
       - Unusual patterns
       - Corner cases
    
    4. Load Testing
       - Concurrent requests
       - High volume
       - Resource limits
       - Performance degradation
       - Scalability validation
    
    5. Adversarial
       - Malicious inputs
       - Attack scenarios
       - Security testing
       - Abuse detection
       - Safety boundaries
    
    Best Practices:
    1. Scenario Design
       - Clear objectives
       - Measurable outcomes
       - Realistic conditions
       - Comprehensive coverage
       - Maintainable tests
    
    2. Mock Fidelity
       - Accurate behavior
       - Realistic timing
       - Error patterns
       - State consistency
       - Documentation
    
    3. Test Organization
       - Logical grouping
       - Clear naming
       - Independent tests
       - Reusable components
       - Version control
    
    4. Continuous Testing
       - CI/CD integration
       - Automated runs
       - Fast feedback
       - Regression detection
       - Quality gates
    
    When to Use Simulation:
    ✓ Development phase testing
    ✓ Dangerous scenario validation
    ✓ Rare case exploration
    ✓ Cost-sensitive testing
    ✓ Performance testing
    ✓ Edge case discovery
    ✗ Production validation (use real testing)
    ✗ API behavior verification (use integration tests)
    
    Limitations:
    - Mock/reality gap
    - May miss real-world issues
    - Requires maintenance
    - Setup complexity
    - False confidence risk
    
    Complementary Approaches:
    1. Simulation (Development)
       - Fast feedback
       - Safe exploration
       - Cost-effective
    
    2. Integration Tests (Pre-production)
       - Real API calls
       - Actual behavior
       - Environment validation
    
    3. Shadow Testing (Production)
       - Real traffic
       - No user impact
       - Behavior comparison
    
    4. A/B Testing (Production)
       - User feedback
       - Real performance
       - Gradual rollout
    
    Production Tips:
    - Use simulation for development
    - Combine with integration tests
    - Validate mocks against reality
    - Update scenarios regularly
    - Monitor real vs simulated gaps
    - Document assumptions
    - Review test coverage
    - Fast test execution (< 5s per scenario)
    - Parallel execution for large suites
    - Clear failure messages
    """)
        
        print("\n" + "=" * 80)
        print("Pattern 092 (Simulation Testing) demonstration complete!")
        print("=" * 80)
    
    
    if __name__ == "__main__":
        demonstrate_simulation_testing()
    
        

    Agentic Design Pattern: Simulation Testing

    Pattern 092: Simulation Testing – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Simulation Testing pattern implementation.


    Pattern Description

    Pattern 092: Simulation Testing

    Description: Simulation Testing creates controlled, reproducible environments to test agent behavior under various scenarios, conditions, and edge cases. Unlike production testing, simulations allow you to test dangerous, expensive, or rare scenarios safely and repeatedly. This pattern is essential for validating agent behavior before deployment and for stress testing system limits.

    Simulation testing enables:
    - Safe testing of dangerous scenarios
    - Reproducible test conditions
    - Stress and load testing
    - Edge case exploration
    - Multi-agent interaction testing
    - Cost-effective experimentation
    

    Components: 1. Simulation Environment – Mock external services (APIs, databases) – Configurable conditions (delays, errors, loads) – State management and reset – Recording and replay capabilities – Deterministic execution

    2. Scenario Definition
       - Initial state setup
       - Event sequences
       - Expected outcomes
       - Success criteria
       - Failure conditions
    
    3. Test Harness
       - Scenario execution engine
       - Result validation
       - Metrics collection
       - Logging and tracing
       - Report generation
    
    4. Mock Components
       - LLM response mocking
       - Tool/API simulation
       - User interaction simulation
       - External system simulation
       - Network condition simulation
    

    Use Cases: 1. Safety Testing – Dangerous scenarios without risk – Edge case handling – Failure mode analysis – Recovery testing – Adversarial inputs

    2. Performance Testing
       - Load testing (many concurrent requests)
       - Stress testing (beyond capacity)
       - Endurance testing (long duration)
       - Spike testing (sudden load changes)
       - Scalability validation
    
    3. Integration Testing
       - Multi-component interaction
       - External service integration
       - Data flow validation
       - Error propagation
       - State consistency
    
    4. Regression Testing
       - Validate fixes don't break features
       - Ensure consistent behavior
       - Track performance changes
       - Verify improvements
       - Document behavior changes
    
    5. Exploratory Testing
       - Discover edge cases
       - Test rare scenarios
       - Validate assumptions
       - Find unexpected behaviors
       - Stress system boundaries
    

    LangChain Implementation: LangChain supports simulation through: – Mock LLM implementations – Custom callbacks for control – Test chain compositions – Fake embeddings for testing – Memory simulation

    Key Features: 1. Controllable Environment – Deterministic responses – Configurable delays – Error injection – State manipulation – Reproducible conditions

    2. Comprehensive Scenarios
       - Normal operations
       - Error conditions
       - Edge cases
       - Load patterns
       - Attack scenarios
    
    3. Detailed Observation
       - Action logging
       - State tracking
       - Performance metrics
       - Error recording
       - Decision paths
    
    4. Flexible Validation
       - Multiple success criteria
       - Partial success handling
       - Performance thresholds
       - State assertions
       - Behavior verification
    

    Best Practices: 1. Scenario Design – Start simple, add complexity – Cover happy path first – Add error scenarios – Include edge cases – Test boundaries

    2. Mock Fidelity
       - Balance realism vs simplicity
       - Accurate error simulation
       - Realistic timing
       - State consistency
       - Clear documentation
    
    3. Test Independence
       - Each test isolated
       - Clean state between tests
       - No shared dependencies
       - Deterministic execution
       - Parallel-safe
    
    4. Performance
       - Fast execution (< 1s per test)
       - Efficient mocking
       - Minimal overhead
       - Parallel execution
       - Resource cleanup
    

    Trade-offs: Advantages: – Safe testing of dangerous scenarios – Reproducible conditions – Cost-effective (no real API calls) – Fast execution – Comprehensive coverage – Early issue detection

    Disadvantages:
    - Mock/reality gap
    - Maintenance overhead
    - Complexity in setup
    - May miss real-world issues
    - Requires good scenario design
    - Can give false confidence
    

    Production Considerations: 1. Test Coverage – Normal operation paths – Common error scenarios – Known edge cases – Performance limits – Security boundaries

    2. Realism Balance
       - Accurate enough for confidence
       - Simple enough to maintain
       - Fast enough for CI/CD
       - Flexible for experimentation
       - Close to production behavior
    
    3. Maintenance
       - Update with code changes
       - Sync with production behavior
       - Regular review
       - Refactor as needed
       - Document assumptions
    
    4. Integration with CI/CD
       - Automated test runs
       - Fast feedback loops
       - Quality gates
       - Regression prevention
       - Performance baselines
    
    5. Hybrid Approach
       - Simulation for development
       - Integration tests for validation
       - Shadow testing in production
       - A/B testing for changes
       - Monitoring in production
    

    File Statistics

    • Total Lines: 948
    • Classes: 9
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 092: Simulation Testing
    
    Description:
        Simulation Testing creates controlled, reproducible environments to test agent behavior
        under various scenarios, conditions, and edge cases. Unlike production testing,
        simulations allow you to test dangerous, expensive, or rare scenarios safely and
        repeatedly. This pattern is essential for validating agent behavior before deployment
        and for stress testing system limits.
    
        Simulation testing enables:
        - Safe testing of dangerous scenarios
        - Reproducible test conditions
        - Stress and load testing
        - Edge case exploration
        - Multi-agent interaction testing
        - Cost-effective experimentation
    
    Components:
        1. Simulation Environment
           - Mock external services (APIs, databases)
           - Configurable conditions (delays, errors, loads)
           - State management and reset
           - Recording and replay capabilities
           - Deterministic execution
    
        2. Scenario Definition
           - Initial state setup
           - Event sequences
           - Expected outcomes
           - Success criteria
           - Failure conditions
    
        3. Test Harness
           - Scenario execution engine
           - Result validation
           - Metrics collection
           - Logging and tracing
           - Report generation
    
        4. Mock Components
           - LLM response mocking
           - Tool/API simulation
           - User interaction simulation
           - External system simulation
           - Network condition simulation
    
    Use Cases:
        1. Safety Testing
           - Dangerous scenarios without risk
           - Edge case handling
           - Failure mode analysis
           - Recovery testing
           - Adversarial inputs
    
        2. Performance Testing
           - Load testing (many concurrent requests)
           - Stress testing (beyond capacity)
           - Endurance testing (long duration)
           - Spike testing (sudden load changes)
           - Scalability validation
    
        3. Integration Testing
           - Multi-component interaction
           - External service integration
           - Data flow validation
           - Error propagation
           - State consistency
    
        4. Regression Testing
           - Validate fixes don't break features
           - Ensure consistent behavior
           - Track performance changes
           - Verify improvements
           - Document behavior changes
    
        5. Exploratory Testing
           - Discover edge cases
           - Test rare scenarios
           - Validate assumptions
           - Find unexpected behaviors
           - Stress system boundaries
    
    LangChain Implementation:
        LangChain supports simulation through:
        - Mock LLM implementations
        - Custom callbacks for control
        - Test chain compositions
        - Fake embeddings for testing
        - Memory simulation
    
    Key Features:
        1. Controllable Environment
           - Deterministic responses
           - Configurable delays
           - Error injection
           - State manipulation
           - Reproducible conditions
    
        2. Comprehensive Scenarios
           - Normal operations
           - Error conditions
           - Edge cases
           - Load patterns
           - Attack scenarios
    
        3. Detailed Observation
           - Action logging
           - State tracking
           - Performance metrics
           - Error recording
           - Decision paths
    
        4. Flexible Validation
           - Multiple success criteria
           - Partial success handling
           - Performance thresholds
           - State assertions
           - Behavior verification
    
    Best Practices:
        1. Scenario Design
           - Start simple, add complexity
           - Cover happy path first
           - Add error scenarios
           - Include edge cases
           - Test boundaries
    
        2. Mock Fidelity
           - Balance realism vs simplicity
           - Accurate error simulation
           - Realistic timing
           - State consistency
           - Clear documentation
    
        3. Test Independence
           - Each test isolated
           - Clean state between tests
           - No shared dependencies
           - Deterministic execution
           - Parallel-safe
    
        4. Performance
           - Fast execution (< 1s per test)
           - Efficient mocking
           - Minimal overhead
           - Parallel execution
           - Resource cleanup
    
    Trade-offs:
        Advantages:
        - Safe testing of dangerous scenarios
        - Reproducible conditions
        - Cost-effective (no real API calls)
        - Fast execution
        - Comprehensive coverage
        - Early issue detection
    
        Disadvantages:
        - Mock/reality gap
        - Maintenance overhead
        - Complexity in setup
        - May miss real-world issues
        - Requires good scenario design
        - Can give false confidence
    
    Production Considerations:
        1. Test Coverage
           - Normal operation paths
           - Common error scenarios
           - Known edge cases
           - Performance limits
           - Security boundaries
    
        2. Realism Balance
           - Accurate enough for confidence
           - Simple enough to maintain
           - Fast enough for CI/CD
           - Flexible for experimentation
           - Close to production behavior
    
        3. Maintenance
           - Update with code changes
           - Sync with production behavior
           - Regular review
           - Refactor as needed
           - Document assumptions
    
        4. Integration with CI/CD
           - Automated test runs
           - Fast feedback loops
           - Quality gates
           - Regression prevention
           - Performance baselines
    
        5. Hybrid Approach
           - Simulation for development
           - Integration tests for validation
           - Shadow testing in production
           - A/B testing for changes
           - Monitoring in production
    """
    

    Purpose: This module implements the Simulation Testing pattern, providing a structured approach to agent design and implementation.


    Import Statements

    Standard Library Imports

    import dataclasses.dataclass
    import dataclasses.field
    import datetime.datetime
    import enum.Enum
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import random
    import time
    import typing.Any
    import typing.Callable
    import typing.Dict
    import typing.List
    import typing.Optional
    

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – enum.Enum: Enumeration support – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – random: Random number generation – time: Time-related functions – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Callable: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional)

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: ScenarioType

    Lines 219-225

    Inherits from: Enum

    Purpose:

    Types of test scenarios
    

    Class: OutcomeType

    Lines 228-233

    Inherits from: Enum

    Purpose:

    Test outcome types
    

    Class: MockResponse

    Lines 237-242

    Purpose:

    Represents a mock LLM response
    

    Class: SimulationScenario

    Lines 246-255

    Purpose:

    Defines a simulation test scenario
    

    Class: SimulationResult

    Lines 259-268

    Purpose:

    Result of a simulation test
    

    Class: MockLLM

    Lines 271-329

    Purpose:

    Mock LLM for testing that returns pre-configured responses.
    
    Allows testing without making actual API calls.
    

    Methods:

    __init__(self, responses)

    Lines 278-291

    Purpose:

    Initialize mock LLM.
    
    Args:
        responses: Dictionary mapping inputs to mock responses
    

    Parameters: – responses: Responses

    invoke(self, input)

    Lines 293-320

    Purpose:

    Mock invoke method.
    
    Args:
        input: Input string
    
    Returns:
        Mock response
    

    Parameters: – input: Input data

    Returns: str

    add_response(self, input, response)

    Lines 322-324

    Purpose:

    Add a response pattern
    

    Parameters: – input: Input data – response: Response

    reset(self)

    Lines 326-329

    Purpose:

    Reset mock state
    

    Parameters:


    Class: MockTool

    Lines 332-377

    Purpose:

    Mock tool for testing agent tool use.
    
    Simulates external tools without actual execution.
    

    Methods:

    __init__(self, name, behavior, should_fail, failure_rate)

    Lines 339-359

    Purpose:

    Initialize mock tool.
    
    Args:
        name: Tool name
        behavior: Function defining tool behavior
        should_fail: Whether tool should always fail
        failure_rate: Probability of failure (0-1)
    

    Parameters: – name: Name – behavior: Behavior – should_fail: Should fail – failure_rate: Failure rate

    execute(self, input)

    Lines 361-377

    Purpose:

    Execute tool with mock behavior.
    
    Args:
        input: Tool input
    
    Returns:
        Tool output
    

    Parameters: – input: Input data

    Returns: str


    Class: SimulationEnvironment

    Lines 380-424

    Purpose:

    Controlled environment for simulation testing.
    
    Provides mock components and state management.
    

    Methods:

    __init__(self)

    Lines 387-392

    Purpose:

    Initialize simulation environment
    

    Parameters:

    setup_mock_llm(self, responses)

    Lines 394-396

    Purpose:

    Configure mock LLM responses
    

    Parameters: – responses: Responses

    add_mock_tool(self, tool)

    Lines 398-400

    Purpose:

    Add a mock tool
    

    Parameters: – tool: Tool

    set_state(self, key, value)

    Lines 402-404

    Purpose:

    Set environment state
    

    Parameters: – key: Key – value: Value

    get_state(self, key)

    Lines 406-408

    Purpose:

    Get environment state
    

    Parameters: – key: Key

    Returns: Any

    log_event(self, event_type, details)

    Lines 410-416

    Purpose:

    Log an event
    

    Parameters: – event_type: Event type – details: Details

    reset(self)

    Lines 418-424

    Purpose:

    Reset environment to initial state
    

    Parameters:


    Class: SimulationTester

    Lines 427-558

    Purpose:

    Executes and validates simulation tests.
    
    Runs scenarios in controlled environment and collects results.
    

    Methods:

    __init__(self, environment)

    Lines 434-441

    Purpose:

    Initialize tester.
    
    Args:
        environment: Simulation environment
    

    Parameters: – environment: Environment

    run_scenario(self, scenario)

    Lines 443-539

    Purpose:

    Run a simulation scenario.
    
    Args:
        scenario: Scenario to run
    
    Returns:
        Simulation result
    

    Parameters: – scenario: Scenario

    Returns: SimulationResult

    run_scenarios(self, scenarios)

    Lines 541-558

    Purpose:

    Run multiple scenarios.
    
    Args:
        scenarios: List of scenarios
    
    Returns:
        List of results
    

    Parameters: – scenarios: Scenarios

    Returns: <ast.Subscript object at 0x000001A06D60A650>


    Functions

    demonstrate_simulation_testing()

    Lines 561-943

    Purpose:

    Demonstrate simulation testing
    

    Key Concepts Summary

    Pattern Overview

    The Simulation Testing pattern provides a structured approach to building AI agents with specific capabilities and behaviors. This implementation leverages LangChain’s composable components to create a robust and maintainable solution.

    Core Components

    1. Agent Class: Main implementation of the pattern logic
    2. Prompt Engineering: Carefully crafted prompts to guide LLM behavior
    3. Tool Integration: Optional tools for extended functionality
    4. Memory Management: State handling across interactions
    5. Output Parsing: Structured response processing

    When to Use

    ✅ Good for: – Scenarios requiring this specific pattern’s strengths – Applications needing structured agent behavior – Systems where explainability is important – Production environments requiring reliability

    ❌ Not ideal for: – Simple single-prompt tasks – Scenarios requiring different pattern characteristics – Resource-constrained environments (if computationally intensive)


    Best Practices Demonstrated

    1. Environment Configuration

    • ✅ Use .env files for API keys
    • ✅ Never hardcode credentials
    • ✅ Validate environment variables on startup

    2. Error Handling

    • ✅ Graceful degradation on failures
    • ✅ Informative error messages
    • ✅ Logging for debugging

    3. Prompt Engineering

    • ✅ Clear, specific instructions
    • ✅ Structured output formats
    • ✅ Few-shot examples when beneficial

    4. Code Organization

    • ✅ Separation of concerns (class-based design)
    • ✅ Type hints for clarity
    • ✅ Docstrings for documentation

    5. Performance Optimization

    • ✅ Appropriate temperature settings
    • ✅ Token budget management
    • ✅ Caching where applicable

    LangChain Components Used

    • Core LangChain framework components

    Why LangChain? – Composable abstractions for LLM applications – Rich ecosystem of integrations – Production-ready patterns – Active community and development


    Execution Flow

    1. Initialize agent with configuration
       ↓
    2. Load environment and dependencies
       ↓
    3. Prepare prompt with input data
       ↓
    4. Invoke LLM through LangChain
       ↓
    5. Parse and validate response
       ↓
    6. Return formatted result
    

    Key Decision Points: – Model selection (temperature, model type) – Prompt structure (few-shot vs zero-shot) – Error handling strategy – Output format validation


    Performance Considerations

    Computational Cost

    • LLM Calls: Primary cost factor (based on tokens)
    • Latency: Depends on model and input size
    • Memory: Minimal for most patterns

    Optimization Strategies

    1. Prompt Optimization: Minimize tokens while maintaining clarity
    2. Caching: Cache repeated queries/responses
    3. Model Selection: Use smaller models when appropriate
    4. Batch Processing: Group similar requests
    5. Streaming: For long responses, stream tokens

    Cost Estimates (Approximate)

    • GPT-3.5-turbo: ~$0.002 per request
    • GPT-4: ~$0.03 per request
    • GPT-4-turbo: ~$0.01 per request

    Actual costs vary based on input/output token counts


    Common Pitfalls to Avoid

    1. Prompt Design Issues

    ❌ Vague or ambiguous instructions
    ✅ Clear, specific, testable prompts

    2. Error Handling

    ❌ Ignoring API errors or rate limits
    ✅ Implement retry logic and graceful degradation

    3. Token Management

    ❌ Exceeding model token limits
    ✅ Truncate inputs, chunk processing

    4. Security

    ❌ Exposing API keys in code
    ✅ Use environment variables

    5. Testing

    ❌ Only testing happy paths
    ✅ Test edge cases, failures, malformed inputs

    6. Cost Management

    ❌ Unmonitored API usage
    ✅ Implement budgets, alerts, and monitoring


    Further Improvements

    Production Enhancements

    1. Monitoring: Add logging, metrics, tracing
    2. Validation: Input/output schema validation
    3. Rate Limiting: Implement request throttling
    4. Caching: Redis or in-memory cache for responses
    5. Async Support: Non-blocking operations for scalability

    Feature Extensions

    1. Multi-Model Support: Switch between providers
    2. Fallback Models: Graceful degradation strategy
    3. Custom Tools: Domain-specific function calling
    4. Advanced Memory: Vector stores for long-term memory
    5. Evaluation: Automated testing and benchmarking

    Code Quality

    1. Testing: Unit tests, integration tests
    2. Documentation: API docs, usage examples
    3. Type Safety: Stricter type hints
    4. Configuration: Externalized config files

    Related Patterns

    • Pattern 091
    • Pattern 093

    Pattern Families: – Reasoning Patterns (001-011) – Multi-Agent Patterns (012-019) – Tool Use Patterns (020-025) – Memory Patterns (026-031) – And many more…


    Summary

    The Simulation Testing pattern provides a powerful approach to building AI agents with specific capabilities. This implementation demonstrates:

    ✅ Key Strengths: – Clean, maintainable code structure – LangChain integration for composability – Type hints and documentation – Production-ready patterns

    🎯 Best Use Cases: – Applications requiring this pattern’s characteristics – Systems needing structured agent behavior – Production environments requiring reliability

    📚 Learning Resources: – LangChain Documentation: https://python.langchain.com/ – Pattern Documentation: See related patterns – OpenAI API Reference: https://platform.openai.com/docs/


    File: 092_simulation_testing.py
    Total Lines: 948
    Pattern Category: AI Agent Design Pattern #092


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Golden Dataset Testing

    
        """
    Pattern 091: Golden Dataset Testing
    
    Description:
        Golden Dataset Testing involves using curated, high-quality test datasets with known
        expected outputs to systematically evaluate agent performance. These "golden" examples
        serve as ground truth for validating agent behavior, detecting regressions, and
        measuring improvements across different versions or configurations. This pattern is
        essential for maintaining quality in production AI systems.
    
        Golden datasets provide:
        - Reproducible test cases for consistent evaluation
        - Regression detection across code changes
        - Performance benchmarking and comparison
        - Quality assurance for production deployments
        - Documentation of expected behavior
        - Training data for evaluation models
    
    Components:
        1. Golden Dataset Structure
           - Input examples (queries, prompts, contexts)
           - Expected outputs (answers, actions, decisions)
           - Metadata (difficulty, category, source)
           - Evaluation criteria (metrics, thresholds)
           - Test case descriptions
    
        2. Test Categories
           - Functional tests (correctness)
           - Edge cases (boundary conditions)
           - Error handling (invalid inputs)
           - Performance tests (speed, cost)
           - Regression tests (historical issues)
    
        3. Evaluation Metrics
           - Exact match accuracy
           - Semantic similarity
           - BLEU/ROUGE scores (text generation)
           - F1 score (classification)
           - Task-specific metrics
    
        4. Test Management
           - Dataset versioning
           - Test case prioritization
           - Result tracking and reporting
           - Historical comparison
           - Failure analysis
    
    Use Cases:
        1. Regression Testing
           - Detect performance degradation
           - Validate bug fixes
           - Ensure consistent behavior
           - Track improvements over time
           - Compare model versions
    
        2. Quality Assurance
           - Pre-deployment validation
           - Continuous integration testing
           - Release quality gates
           - Performance monitoring
           - Compliance verification
    
        3. Model Evaluation
           - Compare different models
           - Evaluate prompt variations
           - Test configuration changes
           - Measure improvements
           - Benchmark performance
    
        4. Development Workflow
           - TDD (Test-Driven Development)
           - Rapid iteration feedback
           - Feature validation
           - Debug assistance
           - Documentation
    
        5. Production Monitoring
           - Ongoing quality checks
           - Canary testing
           - Shadow mode validation
           - A/B test evaluation
           - Incident investigation
    
    LangChain Implementation:
        LangChain supports testing through:
        - Custom evaluation chains
        - Comparison evaluators
        - String distance metrics
        - LangSmith for test tracking
        - Dataset management utilities
    
    Key Features:
        1. Comprehensive Test Coverage
           - Multiple difficulty levels
           - Diverse test categories
           - Edge case coverage
           - Error scenarios
           - Performance tests
    
        2. Automated Evaluation
           - Exact match checking
           - Semantic similarity (embeddings)
           - Custom scoring functions
           - Multi-metric evaluation
           - Threshold-based pass/fail
    
        3. Result Analysis
           - Detailed failure reports
           - Performance statistics
           - Trend analysis
           - Comparison views
           - Root cause identification
    
        4. Dataset Management
           - Easy addition of test cases
           - Categorization and tagging
           - Version control
           - Import/export capabilities
           - Collaborative curation
    
    Best Practices:
        1. Dataset Quality
           - Curate carefully (quality over quantity)
           - Include diverse examples
           - Cover edge cases
           - Regular updates
           - Community validation
    
        2. Test Organization
           - Clear categorization
           - Descriptive naming
           - Priority levels
           - Dependency tracking
           - Logical grouping
    
        3. Evaluation Strategy
           - Multiple metrics
           - Appropriate thresholds
           - Context-aware scoring
           - Human validation for edge cases
           - Continuous calibration
    
        4. Maintenance
           - Regular review and updates
           - Remove obsolete tests
           - Add new failure cases
           - Update expected outputs
           - Document changes
    
    Trade-offs:
        Advantages:
        - Reproducible testing
        - Early issue detection
        - Objective quality measurement
        - Regression prevention
        - Development confidence
        - Clear success criteria
    
        Disadvantages:
        - Dataset curation effort
        - Maintenance overhead
        - May not cover all scenarios
        - Can become outdated
        - Evaluation complexity
        - False positive/negative risks
    
    Production Considerations:
        1. Dataset Coverage
           - Representative samples
           - Stratified by difficulty
           - Cover common patterns
           - Include rare cases
           - Balance across categories
    
        2. Evaluation Pipeline
           - Automated test runs
           - CI/CD integration
           - Fast feedback loops
           - Parallel execution
           - Result aggregation
    
        3. Metrics and Thresholds
           - Task-appropriate metrics
           - Calibrated thresholds
           - Multiple evaluation criteria
           - Weighted scoring
           - Context-specific rules
    
        4. Monitoring and Alerting
           - Track pass rates over time
           - Alert on regressions
           - Performance trends
           - Failure patterns
           - Quality dashboards
    
        5. Continuous Improvement
           - Add failing production cases
           - Update based on feedback
           - Refine evaluation criteria
           - Expand coverage
           - Community contributions
    """
    
    import os
    import json
    from typing import List, Dict, Any, Optional, Callable
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_openai import ChatOpenAI, OpenAIEmbeddings
    from langchain_core.output_parsers import StrOutputParser
    
    load_dotenv()
    
    
    class TestCategory(Enum):
        """Test case categories"""
        FUNCTIONAL = "functional"
        EDGE_CASE = "edge_case"
        ERROR_HANDLING = "error_handling"
        PERFORMANCE = "performance"
        REGRESSION = "regression"
    
    
    class Difficulty(Enum):
        """Test difficulty levels"""
        EASY = "easy"
        MEDIUM = "medium"
        HARD = "hard"
    
    
    @dataclass
    class GoldenTestCase:
        """Represents a single golden test case"""
        id: str
        input: str
        expected_output: str
        category: TestCategory
        difficulty: Difficulty
        description: str
        metadata: Dict[str, Any] = field(default_factory=dict)
        tags: List[str] = field(default_factory=list)
    
    
    @dataclass
    class TestResult:
        """Result of a single test case execution"""
        test_id: str
        passed: bool
        actual_output: str
        expected_output: str
        score: float
        execution_time: float
        error: Optional[str] = None
        details: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class TestSuiteResults:
        """Aggregated results for a test suite"""
        total_tests: int
        passed: int
        failed: int
        pass_rate: float
        average_score: float
        total_time: float
        results: List[TestResult]
        timestamp: datetime = field(default_factory=datetime.now)
        
        def summary(self) -> Dict[str, Any]:
            """Get summary statistics"""
            return {
                "total_tests": self.total_tests,
                "passed": self.passed,
                "failed": self.failed,
                "pass_rate": f"{self.pass_rate:.2f}%",
                "average_score": f"{self.average_score:.3f}",
                "total_time": f"{self.total_time:.2f}s",
                "timestamp": self.timestamp.isoformat()
            }
    
    
    class GoldenDataset:
        """
        Manages a golden dataset of test cases.
        
        Provides methods for adding, retrieving, and organizing test cases.
        """
        
        def __init__(self):
            """Initialize golden dataset"""
            self.test_cases: List[GoldenTestCase] = []
        
        def add_test_case(
            self,
            id: str,
            input: str,
            expected_output: str,
            category: TestCategory,
            difficulty: Difficulty,
            description: str,
            metadata: Optional[Dict[str, Any]] = None,
            tags: Optional[List[str]] = None
        ):
            """
            Add a test case to the dataset.
            
            Args:
                id: Unique test case identifier
                input: Test input
                expected_output: Expected output
                category: Test category
                difficulty: Difficulty level
                description: Test description
                metadata: Additional metadata
                tags: Test tags
            """
            test_case = GoldenTestCase(
                id=id,
                input=input,
                expected_output=expected_output,
                category=category,
                difficulty=difficulty,
                description=description,
                metadata=metadata or {},
                tags=tags or []
            )
            self.test_cases.append(test_case)
        
        def get_by_category(self, category: TestCategory) -> List[GoldenTestCase]:
            """Get test cases by category"""
            return [tc for tc in self.test_cases if tc.category == category]
        
        def get_by_difficulty(self, difficulty: Difficulty) -> List[GoldenTestCase]:
            """Get test cases by difficulty"""
            return [tc for tc in self.test_cases if tc.difficulty == difficulty]
        
        def get_by_tag(self, tag: str) -> List[GoldenTestCase]:
            """Get test cases by tag"""
            return [tc for tc in self.test_cases if tag in tc.tags]
        
        def get_all(self) -> List[GoldenTestCase]:
            """Get all test cases"""
            return self.test_cases
        
        def size(self) -> int:
            """Get dataset size"""
            return len(self.test_cases)
    
    
    class Evaluator:
        """
        Evaluates agent outputs against expected outputs.
        
        Supports multiple evaluation strategies and metrics.
        """
        
        def __init__(self):
            """Initialize evaluator"""
            self.embeddings = OpenAIEmbeddings()
        
        def exact_match(self, actual: str, expected: str) -> float:
            """
            Exact match evaluation.
            
            Returns:
                1.0 if exact match, 0.0 otherwise
            """
            return 1.0 if actual.strip().lower() == expected.strip().lower() else 0.0
        
        def contains_match(self, actual: str, expected: str) -> float:
            """
            Check if expected output is contained in actual output.
            
            Returns:
                1.0 if expected is in actual, 0.0 otherwise
            """
            return 1.0 if expected.lower() in actual.lower() else 0.0
        
        def semantic_similarity(self, actual: str, expected: str) -> float:
            """
            Compute semantic similarity using embeddings.
            
            Returns:
                Similarity score (0-1)
            """
            # Simple word overlap similarity (production would use actual embeddings)
            actual_words = set(actual.lower().split())
            expected_words = set(expected.lower().split())
            
            if not actual_words or not expected_words:
                return 0.0
            
            intersection = actual_words.intersection(expected_words)
            union = actual_words.union(expected_words)
            
            return len(intersection) / len(union)
        
        def fuzzy_match(self, actual: str, expected: str, threshold: float = 0.8) -> float:
            """
            Fuzzy string matching with threshold.
            
            Args:
                actual: Actual output
                expected: Expected output
                threshold: Similarity threshold
                
            Returns:
                1.0 if similarity >= threshold, else similarity score
            """
            similarity = self.semantic_similarity(actual, expected)
            return 1.0 if similarity >= threshold else similarity
        
        def evaluate(
            self,
            actual: str,
            expected: str,
            method: str = "semantic"
        ) -> float:
            """
            Evaluate using specified method.
            
            Args:
                actual: Actual output
                expected: Expected output
                method: Evaluation method (exact, contains, semantic, fuzzy)
                
            Returns:
                Score (0-1)
            """
            if method == "exact":
                return self.exact_match(actual, expected)
            elif method == "contains":
                return self.contains_match(actual, expected)
            elif method == "semantic":
                return self.semantic_similarity(actual, expected)
            elif method == "fuzzy":
                return self.fuzzy_match(actual, expected)
            else:
                return self.semantic_similarity(actual, expected)
    
    
    class GoldenDatasetTester:
        """
        Tests an agent using a golden dataset.
        
        Runs test cases, evaluates results, and generates reports.
        """
        
        def __init__(
            self,
            agent_fn: Callable[[str], str],
            evaluator: Evaluator,
            pass_threshold: float = 0.8
        ):
            """
            Initialize tester.
            
            Args:
                agent_fn: Function that takes input and returns output
                evaluator: Evaluator instance
                pass_threshold: Minimum score to pass
            """
            self.agent_fn = agent_fn
            self.evaluator = evaluator
            self.pass_threshold = pass_threshold
        
        def run_test_case(
            self,
            test_case: GoldenTestCase,
            eval_method: str = "semantic"
        ) -> TestResult:
            """
            Run a single test case.
            
            Args:
                test_case: Test case to run
                eval_method: Evaluation method
                
            Returns:
                Test result
            """
            import time
            
            try:
                start_time = time.time()
                actual_output = self.agent_fn(test_case.input)
                execution_time = time.time() - start_time
                
                score = self.evaluator.evaluate(
                    actual_output,
                    test_case.expected_output,
                    method=eval_method
                )
                
                passed = score >= self.pass_threshold
                
                return TestResult(
                    test_id=test_case.id,
                    passed=passed,
                    actual_output=actual_output,
                    expected_output=test_case.expected_output,
                    score=score,
                    execution_time=execution_time,
                    details={
                        "category": test_case.category.value,
                        "difficulty": test_case.difficulty.value,
                        "eval_method": eval_method
                    }
                )
                
            except Exception as e:
                return TestResult(
                    test_id=test_case.id,
                    passed=False,
                    actual_output="",
                    expected_output=test_case.expected_output,
                    score=0.0,
                    execution_time=0.0,
                    error=str(e)
                )
        
        def run_test_suite(
            self,
            dataset: GoldenDataset,
            eval_method: str = "semantic"
        ) -> TestSuiteResults:
            """
            Run entire test suite.
            
            Args:
                dataset: Golden dataset
                eval_method: Evaluation method
                
            Returns:
                Test suite results
            """
            results = []
            
            for test_case in dataset.get_all():
                result = self.run_test_case(test_case, eval_method)
                results.append(result)
            
            passed = sum(1 for r in results if r.passed)
            failed = len(results) - passed
            pass_rate = (passed / len(results) * 100) if results else 0.0
            average_score = sum(r.score for r in results) / len(results) if results else 0.0
            total_time = sum(r.execution_time for r in results)
            
            return TestSuiteResults(
                total_tests=len(results),
                passed=passed,
                failed=failed,
                pass_rate=pass_rate,
                average_score=average_score,
                total_time=total_time,
                results=results
            )
        
        def generate_report(self, results: TestSuiteResults) -> str:
            """
            Generate detailed test report.
            
            Args:
                results: Test suite results
                
            Returns:
                Formatted report
            """
            report = []
            report.append("=" * 80)
            report.append("GOLDEN DATASET TEST REPORT")
            report.append("=" * 80)
            report.append(f"Timestamp: {results.timestamp}")
            report.append(f"Total Tests: {results.total_tests}")
            report.append(f"Passed: {results.passed}")
            report.append(f"Failed: {results.failed}")
            report.append(f"Pass Rate: {results.pass_rate:.2f}%")
            report.append(f"Average Score: {results.average_score:.3f}")
            report.append(f"Total Time: {results.total_time:.2f}s")
            report.append("")
            
            if results.failed > 0:
                report.append("FAILED TESTS:")
                report.append("-" * 80)
                for result in results.results:
                    if not result.passed:
                        report.append(f"\nTest ID: {result.test_id}")
                        report.append(f"Score: {result.score:.3f}")
                        report.append(f"Expected: {result.expected_output[:100]}...")
                        report.append(f"Actual: {result.actual_output[:100]}...")
                        if result.error:
                            report.append(f"Error: {result.error}")
            
            report.append("\n" + "=" * 80)
            
            return "\n".join(report)
    
    
    def demonstrate_golden_dataset_testing():
        """Demonstrate golden dataset testing"""
        print("=" * 80)
        print("GOLDEN DATASET TESTING DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Create Golden Dataset
        print("\n" + "=" * 80)
        print("EXAMPLE 1: Creating a Golden Dataset")
        print("=" * 80)
        
        dataset = GoldenDataset()
        
        # Add test cases
        dataset.add_test_case(
            id="test_001",
            input="What is 2 + 2?",
            expected_output="4",
            category=TestCategory.FUNCTIONAL,
            difficulty=Difficulty.EASY,
            description="Basic arithmetic",
            tags=["math", "basic"]
        )
        
        dataset.add_test_case(
            id="test_002",
            input="What is the capital of France?",
            expected_output="Paris",
            category=TestCategory.FUNCTIONAL,
            difficulty=Difficulty.EASY,
            description="Geography question",
            tags=["geography", "factual"]
        )
        
        dataset.add_test_case(
            id="test_003",
            input="Explain quantum computing in simple terms",
            expected_output="Quantum computing uses quantum mechanics principles like superposition and entanglement",
            category=TestCategory.FUNCTIONAL,
            difficulty=Difficulty.HARD,
            description="Complex explanation",
            tags=["science", "explanation"]
        )
        
        dataset.add_test_case(
            id="test_004",
            input="",
            expected_output="I need a question to answer",
            category=TestCategory.EDGE_CASE,
            difficulty=Difficulty.MEDIUM,
            description="Empty input handling",
            tags=["edge_case", "validation"]
        )
        
        print(f"\nCreated golden dataset with {dataset.size()} test cases")
        print("\nTest cases by category:")
        for category in TestCategory:
            cases = dataset.get_by_category(category)
            print(f"  {category.value}: {len(cases)} tests")
        
        print("\nTest cases by difficulty:")
        for difficulty in Difficulty:
            cases = dataset.get_by_difficulty(difficulty)
            print(f"  {difficulty.value}: {len(cases)} tests")
        
        # Example 2: Simple Agent for Testing
        print("\n" + "=" * 80)
        print("EXAMPLE 2: Creating Test Agent")
        print("=" * 80)
        
        llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
        
        def simple_agent(input: str) -> str:
            """Simple agent for testing"""
            if not input.strip():
                return "I need a question to answer"
            
            prompt = ChatPromptTemplate.from_template(
                "Answer the following question concisely:\n\n{question}"
            )
            chain = prompt | llm | StrOutputParser()
            return chain.invoke({"question": input})
        
        print("\nAgent created: simple_agent")
        print("Agent behavior: Answers questions concisely")
        
        # Example 3: Evaluation Methods
        print("\n" + "=" * 80)
        print("EXAMPLE 3: Testing Different Evaluation Methods")
        print("=" * 80)
        
        evaluator = Evaluator()
        
        test_actual = "The capital of France is Paris"
        test_expected = "Paris"
        
        print(f"\nActual: '{test_actual}'")
        print(f"Expected: '{test_expected}'\n")
        
        methods = ["exact", "contains", "semantic", "fuzzy"]
        for method in methods:
            score = evaluator.evaluate(test_actual, test_expected, method=method)
            print(f"{method.upper()} match: {score:.3f}")
        
        # Example 4: Running Test Suite
        print("\n" + "=" * 80)
        print("EXAMPLE 4: Running Complete Test Suite")
        print("=" * 80)
        
        tester = GoldenDatasetTester(
            agent_fn=simple_agent,
            evaluator=evaluator,
            pass_threshold=0.7
        )
        
        print("\nRunning test suite...")
        print(f"Evaluation method: semantic similarity")
        print(f"Pass threshold: 0.7\n")
        
        results = tester.run_test_suite(dataset, eval_method="semantic")
        
        print(f"Results:")
        print(f"  Total: {results.total_tests}")
        print(f"  Passed: {results.passed}")
        print(f"  Failed: {results.failed}")
        print(f"  Pass Rate: {results.pass_rate:.2f}%")
        print(f"  Avg Score: {results.average_score:.3f}")
        print(f"  Total Time: {results.total_time:.2f}s")
        
        # Example 5: Detailed Results Analysis
        print("\n" + "=" * 80)
        print("EXAMPLE 5: Detailed Results Analysis")
        print("=" * 80)
        
        print("\nIndividual Test Results:\n")
        for result in results.results:
            status = "✓ PASS" if result.passed else "✗ FAIL"
            print(f"{status} - {result.test_id}")
            print(f"  Score: {result.score:.3f}")
            print(f"  Time: {result.execution_time:.3f}s")
            print(f"  Category: {result.details.get('category', 'N/A')}")
            print(f"  Difficulty: {result.details.get('difficulty', 'N/A')}")
            if not result.passed:
                print(f"  Expected: {result.expected_output[:50]}...")
                print(f"  Actual: {result.actual_output[:50]}...")
            print()
        
        # Example 6: Category-Based Analysis
        print("\n" + "=" * 80)
        print("EXAMPLE 6: Performance by Category")
        print("=" * 80)
        
        category_stats = {}
        for result in results.results:
            category = result.details.get('category', 'unknown')
            if category not in category_stats:
                category_stats[category] = {"total": 0, "passed": 0, "scores": []}
            
            category_stats[category]["total"] += 1
            if result.passed:
                category_stats[category]["passed"] += 1
            category_stats[category]["scores"].append(result.score)
        
        print("\nPerformance by Category:\n")
        for category, stats in category_stats.items():
            pass_rate = (stats["passed"] / stats["total"] * 100) if stats["total"] > 0 else 0
            avg_score = sum(stats["scores"]) / len(stats["scores"]) if stats["scores"] else 0
            
            print(f"{category.upper()}:")
            print(f"  Tests: {stats['total']}")
            print(f"  Passed: {stats['passed']}/{stats['total']}")
            print(f"  Pass Rate: {pass_rate:.2f}%")
            print(f"  Avg Score: {avg_score:.3f}")
            print()
        
        # Example 7: Regression Testing
        print("\n" + "=" * 80)
        print("EXAMPLE 7: Regression Testing Simulation")
        print("=" * 80)
        
        print("\nSimulating version comparison...")
        
        # Simulate baseline results
        baseline_pass_rate = 85.0
        current_pass_rate = results.pass_rate
        
        print(f"\nBaseline (v1.0): {baseline_pass_rate:.2f}% pass rate")
        print(f"Current (v2.0): {current_pass_rate:.2f}% pass rate")
        
        if current_pass_rate >= baseline_pass_rate:
            print("\n✓ No regression detected - Quality maintained or improved!")
        else:
            regression = baseline_pass_rate - current_pass_rate
            print(f"\n✗ REGRESSION DETECTED: {regression:.2f}% decrease in pass rate")
            print("   Action: Investigate failing tests before deployment")
        
        # Example 8: Test Report Generation
        print("\n" + "=" * 80)
        print("EXAMPLE 8: Generating Test Report")
        print("=" * 80)
        
        report = tester.generate_report(results)
        print("\n" + report)
        
        # Summary
        print("\n" + "=" * 80)
        print("GOLDEN DATASET TESTING SUMMARY")
        print("=" * 80)
        print("""
    Golden Dataset Testing Benefits:
    1. Reproducibility: Consistent test results across runs
    2. Regression Detection: Catch performance degradation early
    3. Quality Assurance: Objective measurement of agent quality
    4. Development Confidence: Safe to refactor and improve
    5. Documentation: Tests document expected behavior
    6. Benchmarking: Compare different configurations
    
    Dataset Best Practices:
    1. Quality Over Quantity
       - Carefully curate test cases
       - Focus on important scenarios
       - Include edge cases
       - Regular review and updates
    
    2. Organization
       - Clear categories (functional, edge case, etc.)
       - Difficulty levels (easy, medium, hard)
       - Descriptive tags
       - Comprehensive metadata
    
    3. Coverage
       - Common use cases
       - Edge cases and boundaries
       - Error scenarios
       - Performance critical paths
       - Regression cases from production
    
    4. Maintenance
       - Add failing production cases
       - Update expected outputs when needed
       - Remove obsolete tests
       - Version control dataset
       - Document changes
    
    Evaluation Methods:
    1. Exact Match
       - Strict equality check
       - Best for: Factual answers, specific outputs
       - Limitation: Too strict for natural language
    
    2. Contains Match
       - Check if expected is in actual
       - Best for: Key information presence
       - Limitation: Doesn't verify quality
    
    3. Semantic Similarity
       - Meaning-based comparison
       - Best for: Natural language responses
       - Limitation: Computationally expensive
    
    4. Fuzzy Match
       - Similarity with threshold
       - Best for: Near-match acceptance
       - Limitation: Threshold tuning needed
    
    Test Suite Structure:
    1. Core Functionality (60%)
       - Essential features
       - Common use cases
       - Critical paths
    
    2. Edge Cases (20%)
       - Boundary conditions
       - Unusual inputs
       - Corner cases
    
    3. Error Handling (10%)
       - Invalid inputs
       - System errors
       - Recovery scenarios
    
    4. Performance (10%)
       - Speed benchmarks
       - Resource usage
       - Scalability tests
    
    Continuous Integration:
    1. Automated Test Runs
       - Run on every commit
       - Pre-deployment validation
       - Scheduled regression tests
       - Performance monitoring
    
    2. Quality Gates
       - Minimum pass rate (e.g., 95%)
       - Maximum regression (e.g., 5%)
       - Performance thresholds
       - Coverage requirements
    
    3. Reporting
       - Dashboard visualization
       - Trend analysis
       - Failure notifications
       - Historical comparison
    
    When to Use Golden Dataset Testing:
    ✓ Production AI systems
    ✓ Iterative development
    ✓ Multiple team members
    ✓ Complex agent behaviors
    ✓ Compliance requirements
    ✓ Performance monitoring
    ✗ One-off scripts
    ✗ Exploratory prototypes (initially)
    
    Production Tips:
    - Start small, grow dataset organically
    - Add production failures to dataset
    - Regular dataset review sessions
    - Version control your datasets
    - Share datasets across team
    - Integrate with CI/CD pipeline
    - Monitor test execution time
    - Keep tests fast (< 5 min for suite)
    - Parallel execution for large datasets
    - Alert on regressions immediately
    """)
        
        print("\n" + "=" * 80)
        print("Pattern 091 (Golden Dataset Testing) demonstration complete!")
        print("=" * 80)
    
    
    if __name__ == "__main__":
        demonstrate_golden_dataset_testing()
    
        

    Agentic Design Pattern: Golden Dataset Testing

    Pattern 091: Golden Dataset Testing – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Golden Dataset Testing pattern implementation.


    Pattern Description

    Pattern 091: Golden Dataset Testing

    Description: Golden Dataset Testing involves using curated, high-quality test datasets with known expected outputs to systematically evaluate agent performance. These “golden” examples serve as ground truth for validating agent behavior, detecting regressions, and measuring improvements across different versions or configurations. This pattern is essential for maintaining quality in production AI systems.

    Golden datasets provide:
    - Reproducible test cases for consistent evaluation
    - Regression detection across code changes
    - Performance benchmarking and comparison
    - Quality assurance for production deployments
    - Documentation of expected behavior
    - Training data for evaluation models
    

    Components: 1. Golden Dataset Structure – Input examples (queries, prompts, contexts) – Expected outputs (answers, actions, decisions) – Metadata (difficulty, category, source) – Evaluation criteria (metrics, thresholds) – Test case descriptions

    2. Test Categories
       - Functional tests (correctness)
       - Edge cases (boundary conditions)
       - Error handling (invalid inputs)
       - Performance tests (speed, cost)
       - Regression tests (historical issues)
    
    3. Evaluation Metrics
       - Exact match accuracy
       - Semantic similarity
       - BLEU/ROUGE scores (text generation)
       - F1 score (classification)
       - Task-specific metrics
    
    4. Test Management
       - Dataset versioning
       - Test case prioritization
       - Result tracking and reporting
       - Historical comparison
       - Failure analysis
    

    Use Cases: 1. Regression Testing – Detect performance degradation – Validate bug fixes – Ensure consistent behavior – Track improvements over time – Compare model versions

    2. Quality Assurance
       - Pre-deployment validation
       - Continuous integration testing
       - Release quality gates
       - Performance monitoring
       - Compliance verification
    
    3. Model Evaluation
       - Compare different models
       - Evaluate prompt variations
       - Test configuration changes
       - Measure improvements
       - Benchmark performance
    
    4. Development Workflow
       - TDD (Test-Driven Development)
       - Rapid iteration feedback
       - Feature validation
       - Debug assistance
       - Documentation
    
    5. Production Monitoring
       - Ongoing quality checks
       - Canary testing
       - Shadow mode validation
       - A/B test evaluation
       - Incident investigation
    

    LangChain Implementation: LangChain supports testing through: – Custom evaluation chains – Comparison evaluators – String distance metrics – LangSmith for test tracking – Dataset management utilities

    Key Features: 1. Comprehensive Test Coverage – Multiple difficulty levels – Diverse test categories – Edge case coverage – Error scenarios – Performance tests

    2. Automated Evaluation
       - Exact match checking
       - Semantic similarity (embeddings)
       - Custom scoring functions
       - Multi-metric evaluation
       - Threshold-based pass/fail
    
    3. Result Analysis
       - Detailed failure reports
       - Performance statistics
       - Trend analysis
       - Comparison views
       - Root cause identification
    
    4. Dataset Management
       - Easy addition of test cases
       - Categorization and tagging
       - Version control
       - Import/export capabilities
       - Collaborative curation
    

    Best Practices: 1. Dataset Quality – Curate carefully (quality over quantity) – Include diverse examples – Cover edge cases – Regular updates – Community validation

    2. Test Organization
       - Clear categorization
       - Descriptive naming
       - Priority levels
       - Dependency tracking
       - Logical grouping
    
    3. Evaluation Strategy
       - Multiple metrics
       - Appropriate thresholds
       - Context-aware scoring
       - Human validation for edge cases
       - Continuous calibration
    
    4. Maintenance
       - Regular review and updates
       - Remove obsolete tests
       - Add new failure cases
       - Update expected outputs
       - Document changes
    

    Trade-offs: Advantages: – Reproducible testing – Early issue detection – Objective quality measurement – Regression prevention – Development confidence – Clear success criteria

    Disadvantages:
    - Dataset curation effort
    - Maintenance overhead
    - May not cover all scenarios
    - Can become outdated
    - Evaluation complexity
    - False positive/negative risks
    

    Production Considerations: 1. Dataset Coverage – Representative samples – Stratified by difficulty – Cover common patterns – Include rare cases – Balance across categories

    2. Evaluation Pipeline
       - Automated test runs
       - CI/CD integration
       - Fast feedback loops
       - Parallel execution
       - Result aggregation
    
    3. Metrics and Thresholds
       - Task-appropriate metrics
       - Calibrated thresholds
       - Multiple evaluation criteria
       - Weighted scoring
       - Context-specific rules
    
    4. Monitoring and Alerting
       - Track pass rates over time
       - Alert on regressions
       - Performance trends
       - Failure patterns
       - Quality dashboards
    
    5. Continuous Improvement
       - Add failing production cases
       - Update based on feedback
       - Refine evaluation criteria
       - Expand coverage
       - Community contributions
    

    File Statistics

    • Total Lines: 938
    • Classes: 8
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 091: Golden Dataset Testing
    
    Description:
        Golden Dataset Testing involves using curated, high-quality test datasets with known
        expected outputs to systematically evaluate agent performance. These "golden" examples
        serve as ground truth for validating agent behavior, detecting regressions, and
        measuring improvements across different versions or configurations. This pattern is
        essential for maintaining quality in production AI systems.
    
        Golden datasets provide:
        - Reproducible test cases for consistent evaluation
        - Regression detection across code changes
        - Performance benchmarking and comparison
        - Quality assurance for production deployments
        - Documentation of expected behavior
        - Training data for evaluation models
    
    Components:
        1. Golden Dataset Structure
           - Input examples (queries, prompts, contexts)
           - Expected outputs (answers, actions, decisions)
           - Metadata (difficulty, category, source)
           - Evaluation criteria (metrics, thresholds)
           - Test case descriptions
    
        2. Test Categories
           - Functional tests (correctness)
           - Edge cases (boundary conditions)
           - Error handling (invalid inputs)
           - Performance tests (speed, cost)
           - Regression tests (historical issues)
    
        3. Evaluation Metrics
           - Exact match accuracy
           - Semantic similarity
           - BLEU/ROUGE scores (text generation)
           - F1 score (classification)
           - Task-specific metrics
    
        4. Test Management
           - Dataset versioning
           - Test case prioritization
           - Result tracking and reporting
           - Historical comparison
           - Failure analysis
    
    Use Cases:
        1. Regression Testing
           - Detect performance degradation
           - Validate bug fixes
           - Ensure consistent behavior
           - Track improvements over time
           - Compare model versions
    
        2. Quality Assurance
           - Pre-deployment validation
           - Continuous integration testing
           - Release quality gates
           - Performance monitoring
           - Compliance verification
    
        3. Model Evaluation
           - Compare different models
           - Evaluate prompt variations
           - Test configuration changes
           - Measure improvements
           - Benchmark performance
    
        4. Development Workflow
           - TDD (Test-Driven Development)
           - Rapid iteration feedback
           - Feature validation
           - Debug assistance
           - Documentation
    
        5. Production Monitoring
           - Ongoing quality checks
           - Canary testing
           - Shadow mode validation
           - A/B test evaluation
           - Incident investigation
    
    LangChain Implementation:
        LangChain supports testing through:
        - Custom evaluation chains
        - Comparison evaluators
        - String distance metrics
        - LangSmith for test tracking
        - Dataset management utilities
    
    Key Features:
        1. Comprehensive Test Coverage
           - Multiple difficulty levels
           - Diverse test categories
           - Edge case coverage
           - Error scenarios
           - Performance tests
    
        2. Automated Evaluation
           - Exact match checking
           - Semantic similarity (embeddings)
           - Custom scoring functions
           - Multi-metric evaluation
           - Threshold-based pass/fail
    
        3. Result Analysis
           - Detailed failure reports
           - Performance statistics
           - Trend analysis
           - Comparison views
           - Root cause identification
    
        4. Dataset Management
           - Easy addition of test cases
           - Categorization and tagging
           - Version control
           - Import/export capabilities
           - Collaborative curation
    
    Best Practices:
        1. Dataset Quality
           - Curate carefully (quality over quantity)
           - Include diverse examples
           - Cover edge cases
           - Regular updates
           - Community validation
    
        2. Test Organization
           - Clear categorization
           - Descriptive naming
           - Priority levels
           - Dependency tracking
           - Logical grouping
    
        3. Evaluation Strategy
           - Multiple metrics
           - Appropriate thresholds
           - Context-aware scoring
           - Human validation for edge cases
           - Continuous calibration
    
        4. Maintenance
           - Regular review and updates
           - Remove obsolete tests
           - Add new failure cases
           - Update expected outputs
           - Document changes
    
    Trade-offs:
        Advantages:
        - Reproducible testing
        - Early issue detection
        - Objective quality measurement
        - Regression prevention
        - Development confidence
        - Clear success criteria
    
        Disadvantages:
        - Dataset curation effort
        - Maintenance overhead
        - May not cover all scenarios
        - Can become outdated
        - Evaluation complexity
        - False positive/negative risks
    
    Production Considerations:
        1. Dataset Coverage
           - Representative samples
           - Stratified by difficulty
           - Cover common patterns
           - Include rare cases
           - Balance across categories
    
        2. Evaluation Pipeline
           - Automated test runs
           - CI/CD integration
           - Fast feedback loops
           - Parallel execution
           - Result aggregation
    
        3. Metrics and Thresholds
           - Task-appropriate metrics
           - Calibrated thresholds
           - Multiple evaluation criteria
           - Weighted scoring
           - Context-specific rules
    
        4. Monitoring and Alerting
           - Track pass rates over time
           - Alert on regressions
           - Performance trends
           - Failure patterns
           - Quality dashboards
    
        5. Continuous Improvement
           - Add failing production cases
           - Update based on feedback
           - Refine evaluation criteria
           - Expand coverage
           - Community contributions
    """
    

    Purpose: This module implements the Golden Dataset Testing pattern, providing a structured approach to agent design and implementation.


    Import Statements

    Standard Library Imports

    import dataclasses.dataclass
    import dataclasses.field
    import datetime.datetime
    import enum.Enum
    import json
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import time
    import typing.Any
    import typing.Callable
    import typing.Dict
    import typing.List
    import typing.Optional
    

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – enum.Enum: Enumeration support – json: JSON serialization/deserialization – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – time: Time-related functions – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Callable: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain_openai.ChatOpenAI
    from langchain_openai.OpenAIEmbeddings
    

    Purpose: LangChain framework components: – LLM Interface: Wrapper for OpenAI models – Chains: Composable building blocks for complex workflows

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: TestCategory

    Lines 219-225

    Inherits from: Enum

    Purpose:

    Test case categories
    

    Class: Difficulty

    Lines 228-232

    Inherits from: Enum

    Purpose:

    Test difficulty levels
    

    Class: GoldenTestCase

    Lines 236-245

    Purpose:

    Represents a single golden test case
    

    Class: TestResult

    Lines 249-258

    Purpose:

    Result of a single test case execution
    

    Class: TestSuiteResults

    Lines 262-283

    Purpose:

    Aggregated results for a test suite
    

    Methods:

    summary(self)

    Lines 273-283

    Purpose:

    Get summary statistics
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D855410>


    Class: GoldenDataset

    Lines 286-351

    Purpose:

    Manages a golden dataset of test cases.
    
    Provides methods for adding, retrieving, and organizing test cases.
    

    Methods:

    __init__(self)

    Lines 293-295

    Purpose:

    Initialize golden dataset
    

    Parameters:

    add_test_case(self, id, input, expected_output, category, difficulty, description, metadata, tags)

    Lines 297-331

    Purpose:

    Add a test case to the dataset.
    
    Args:
        id: Unique test case identifier
        input: Test input
        expected_output: Expected output
        category: Test category
        difficulty: Difficulty level
        description: Test description
        metadata: Additional metadata
        tags: Test tags
    

    Parameters: – id: Id – input: Input data – expected_output: Expected output – category: Category – difficulty: Difficulty – description: Description – metadata: Metadata – tags: Tags

    get_by_category(self, category)

    Lines 333-335

    Purpose:

    Get test cases by category
    

    Parameters: – category: Category

    Returns: <ast.Subscript object at 0x000001A06E094450>

    get_by_difficulty(self, difficulty)

    Lines 337-339

    Purpose:

    Get test cases by difficulty
    

    Parameters: – difficulty: Difficulty

    Returns: <ast.Subscript object at 0x000001A06D65BE50>

    get_by_tag(self, tag)

    Lines 341-343

    Purpose:

    Get test cases by tag
    

    Parameters: – tag: Tag

    Returns: <ast.Subscript object at 0x000001A06D65A850>

    get_all(self)

    Lines 345-347

    Purpose:

    Get all test cases
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D65B2D0>

    size(self)

    Lines 349-351

    Purpose:

    Get dataset size
    

    Parameters:

    Returns: int


    Class: Evaluator

    Lines 354-443

    Purpose:

    Evaluates agent outputs against expected outputs.
    
    Supports multiple evaluation strategies and metrics.
    

    Methods:

    __init__(self)

    Lines 361-363

    Purpose:

    Initialize evaluator
    

    Parameters:

    exact_match(self, actual, expected)

    Lines 365-372

    Purpose:

    Exact match evaluation.
    
    Returns:
        1.0 if exact match, 0.0 otherwise
    

    Parameters: – actual: Actual – expected: Expected

    Returns: float

    contains_match(self, actual, expected)

    Lines 374-381

    Purpose:

    Check if expected output is contained in actual output.
    
    Returns:
        1.0 if expected is in actual, 0.0 otherwise
    

    Parameters: – actual: Actual – expected: Expected

    Returns: float

    semantic_similarity(self, actual, expected)

    Lines 383-400

    Purpose:

    Compute semantic similarity using embeddings.
    
    Returns:
        Similarity score (0-1)
    

    Parameters: – actual: Actual – expected: Expected

    Returns: float

    fuzzy_match(self, actual, expected, threshold)

    Lines 402-415

    Purpose:

    Fuzzy string matching with threshold.
    
    Args:
        actual: Actual output
        expected: Expected output
        threshold: Similarity threshold
    
    Returns:
        1.0 if similarity >= threshold, else similarity score
    

    Parameters: – actual: Actual – expected: Expected – threshold: Threshold

    Returns: float

    evaluate(self, actual, expected, method)

    Lines 417-443

    Purpose:

    Evaluate using specified method.
    
    Args:
        actual: Actual output
        expected: Expected output
        method: Evaluation method (exact, contains, semantic, fuzzy)
    
    Returns:
        Score (0-1)
    

    Parameters: – actual: Actual – expected: Expected – method: Method

    Returns: float


    Class: GoldenDatasetTester

    Lines 446-600

    Purpose:

    Tests an agent using a golden dataset.
    
    Runs test cases, evaluates results, and generates reports.
    

    Methods:

    __init__(self, agent_fn, evaluator, pass_threshold)

    Lines 453-469

    Purpose:

    Initialize tester.
    
    Args:
        agent_fn: Function that takes input and returns output
        evaluator: Evaluator instance
        pass_threshold: Minimum score to pass
    

    Parameters: – agent_fn: Agent fn – evaluator: Evaluator – pass_threshold: Pass threshold

    run_test_case(self, test_case, eval_method)

    Lines 471-524

    Purpose:

    Run a single test case.
    
    Args:
        test_case: Test case to run
        eval_method: Evaluation method
    
    Returns:
        Test result
    

    Parameters: – test_case: Test case – eval_method: Eval method

    Returns: TestResult

    run_test_suite(self, dataset, eval_method)

    Lines 526-561

    Purpose:

    Run entire test suite.
    
    Args:
        dataset: Golden dataset
        eval_method: Evaluation method
    
    Returns:
        Test suite results
    

    Parameters: – dataset: Dataset – eval_method: Eval method

    Returns: TestSuiteResults

    generate_report(self, results)

    Lines 563-600

    Purpose:

    Generate detailed test report.
    
    Args:
        results: Test suite results
    
    Returns:
        Formatted report
    

    Parameters: – results: Results

    Returns: str


    Functions

    demonstrate_golden_dataset_testing()

    Lines 603-933

    Purpose:

    Demonstrate golden dataset testing
    

    Key Concepts Summary

    Pattern Overview

    The Golden Dataset Testing pattern provides a structured approach to building AI agents with specific capabilities and behaviors. This implementation leverages LangChain’s composable components to create a robust and maintainable solution.

    Core Components

    1. Agent Class: Main implementation of the pattern logic
    2. Prompt Engineering: Carefully crafted prompts to guide LLM behavior
    3. Tool Integration: Optional tools for extended functionality
    4. Memory Management: State handling across interactions
    5. Output Parsing: Structured response processing

    When to Use

    ✅ Good for: – Scenarios requiring this specific pattern’s strengths – Applications needing structured agent behavior – Systems where explainability is important – Production environments requiring reliability

    ❌ Not ideal for: – Simple single-prompt tasks – Scenarios requiring different pattern characteristics – Resource-constrained environments (if computationally intensive)


    Best Practices Demonstrated

    1. Environment Configuration

    • ✅ Use .env files for API keys
    • ✅ Never hardcode credentials
    • ✅ Validate environment variables on startup

    2. Error Handling

    • ✅ Graceful degradation on failures
    • ✅ Informative error messages
    • ✅ Logging for debugging

    3. Prompt Engineering

    • ✅ Clear, specific instructions
    • ✅ Structured output formats
    • ✅ Few-shot examples when beneficial

    4. Code Organization

    • ✅ Separation of concerns (class-based design)
    • ✅ Type hints for clarity
    • ✅ Docstrings for documentation

    5. Performance Optimization

    • ✅ Appropriate temperature settings
    • ✅ Token budget management
    • ✅ Caching where applicable

    LangChain Components Used

    • ChatOpenAI: OpenAI model interface
    • Chains: Sequential component composition

    Why LangChain? – Composable abstractions for LLM applications – Rich ecosystem of integrations – Production-ready patterns – Active community and development


    Execution Flow

    1. Initialize agent with configuration
       ↓
    2. Load environment and dependencies
       ↓
    3. Prepare prompt with input data
       ↓
    4. Invoke LLM through LangChain
       ↓
    5. Parse and validate response
       ↓
    6. Return formatted result
    

    Key Decision Points: – Model selection (temperature, model type) – Prompt structure (few-shot vs zero-shot) – Error handling strategy – Output format validation


    Performance Considerations

    Computational Cost

    • LLM Calls: Primary cost factor (based on tokens)
    • Latency: Depends on model and input size
    • Memory: Minimal for most patterns

    Optimization Strategies

    1. Prompt Optimization: Minimize tokens while maintaining clarity
    2. Caching: Cache repeated queries/responses
    3. Model Selection: Use smaller models when appropriate
    4. Batch Processing: Group similar requests
    5. Streaming: For long responses, stream tokens

    Cost Estimates (Approximate)

    • GPT-3.5-turbo: ~$0.002 per request
    • GPT-4: ~$0.03 per request
    • GPT-4-turbo: ~$0.01 per request

    Actual costs vary based on input/output token counts


    Common Pitfalls to Avoid

    1. Prompt Design Issues

    ❌ Vague or ambiguous instructions
    ✅ Clear, specific, testable prompts

    2. Error Handling

    ❌ Ignoring API errors or rate limits
    ✅ Implement retry logic and graceful degradation

    3. Token Management

    ❌ Exceeding model token limits
    ✅ Truncate inputs, chunk processing

    4. Security

    ❌ Exposing API keys in code
    ✅ Use environment variables

    5. Testing

    ❌ Only testing happy paths
    ✅ Test edge cases, failures, malformed inputs

    6. Cost Management

    ❌ Unmonitored API usage
    ✅ Implement budgets, alerts, and monitoring


    Further Improvements

    Production Enhancements

    1. Monitoring: Add logging, metrics, tracing
    2. Validation: Input/output schema validation
    3. Rate Limiting: Implement request throttling
    4. Caching: Redis or in-memory cache for responses
    5. Async Support: Non-blocking operations for scalability

    Feature Extensions

    1. Multi-Model Support: Switch between providers
    2. Fallback Models: Graceful degradation strategy
    3. Custom Tools: Domain-specific function calling
    4. Advanced Memory: Vector stores for long-term memory
    5. Evaluation: Automated testing and benchmarking

    Code Quality

    1. Testing: Unit tests, integration tests
    2. Documentation: API docs, usage examples
    3. Type Safety: Stricter type hints
    4. Configuration: Externalized config files

    Related Patterns

    • Pattern 090
    • Pattern 092

    Pattern Families: – Reasoning Patterns (001-011) – Multi-Agent Patterns (012-019) – Tool Use Patterns (020-025) – Memory Patterns (026-031) – And many more…


    Summary

    The Golden Dataset Testing pattern provides a powerful approach to building AI agents with specific capabilities. This implementation demonstrates:

    ✅ Key Strengths: – Clean, maintainable code structure – LangChain integration for composability – Type hints and documentation – Production-ready patterns

    🎯 Best Use Cases: – Applications requiring this pattern’s characteristics – Systems needing structured agent behavior – Production environments requiring reliability

    📚 Learning Resources: – LangChain Documentation: https://python.langchain.com/ – Pattern Documentation: See related patterns – OpenAI API Reference: https://platform.openai.com/docs/


    File: 091_golden_dataset_testing.py
    Total Lines: 938
    Pattern Category: AI Agent Design Pattern #091


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Load Balancing

    
        """
    Pattern 090: Load Balancing Pattern
    
    Description:
        The Load Balancing Pattern distributes requests across multiple LLM endpoints or
        instances to achieve high availability, improved performance, and fault tolerance.
        In production LLM applications, load balancing is critical for handling high traffic
        volumes, preventing single points of failure, and ensuring consistent service
        quality even when individual endpoints fail or become overloaded.
    
        Load balancing strategies enable:
        - High availability through redundancy
        - Improved performance via parallel processing
        - Cost optimization through provider selection
        - Rate limit management across multiple accounts
        - Geographic distribution for lower latency
        - Failover and disaster recovery
    
    Components:
        1. Load Balancing Strategies
           - Round Robin: Distribute equally across endpoints
           - Weighted Round Robin: Priority-based distribution
           - Least Loaded: Send to endpoint with lowest load
           - Random: Random endpoint selection
           - Geographic: Route based on location
           - Cost-Based: Choose cheapest available endpoint
    
        2. Health Checking
           - Endpoint availability monitoring
           - Response time tracking
           - Error rate monitoring
           - Periodic health probes
           - Automatic endpoint removal/recovery
    
        3. Request Management
           - Request queuing and buffering
           - Retry logic with backoff
           - Circuit breaker pattern
           - Request timeout handling
           - Priority-based routing
    
        4. Failover Mechanisms
           - Automatic endpoint switching
           - Degraded mode operation
           - Fallback to alternative providers
           - Request replay on failure
           - State preservation
    
    Use Cases:
        1. High-Traffic Applications
           - Chatbots serving thousands of users
           - API services with SLA requirements
           - Real-time processing pipelines
           - Multi-tenant platforms
           - Peak load handling
    
        2. Multi-Provider Deployments
           - OpenAI + Anthropic + Google
           - Primary and backup providers
           - Cost optimization strategies
           - Feature-specific routing
           - Geographic compliance
    
        3. Rate Limit Management
           - Distribute across multiple API keys
           - Prevent rate limit exhaustion
           - Smooth request distribution
           - Burst handling
           - Cost control
    
        4. Disaster Recovery
           - Automatic failover
           - Multi-region deployment
           - Zero-downtime updates
           - Service degradation handling
           - Business continuity
    
        5. Performance Optimization
           - Parallel request processing
           - Load-based routing
           - Latency optimization
           - Resource utilization
           - Capacity planning
    
    LangChain Implementation:
        LangChain doesn't have built-in load balancing, but you can implement
        it using:
        - Custom LLM wrappers
        - Callback handlers for monitoring
        - Retry logic with different models
        - Custom routing logic
        - Health check mechanisms
    
    Key Features:
        1. Endpoint Management
           - Dynamic endpoint registration
           - Health status tracking
           - Automatic recovery
           - Configuration updates
           - Version management
    
        2. Intelligent Routing
           - Strategy selection
           - Real-time decision making
           - Performance-based routing
           - Cost optimization
           - SLA enforcement
    
        3. Fault Tolerance
           - Automatic retry with exponential backoff
           - Circuit breaker to prevent cascading failures
           - Graceful degradation
           - Error handling and recovery
           - Request preservation
    
        4. Monitoring
           - Request distribution metrics
           - Endpoint health status
           - Response time tracking
           - Error rate monitoring
           - Cost tracking per endpoint
    
    Best Practices:
        1. Health Monitoring
           - Regular health checks (every 30-60s)
           - Multiple health metrics (latency, errors, availability)
           - Automatic endpoint recovery
           - Alert on degraded performance
           - Log all health transitions
    
        2. Load Distribution
           - Consider endpoint capacity
           - Account for different model speeds
           - Balance cost vs performance
           - Avoid overloading any single endpoint
           - Reserve capacity for spikes
    
        3. Failover Strategy
           - Fast failure detection (< 5s)
           - Quick failover ( float:
            """Calculate success rate"""
            if self.total_requests == 0:
                return 0.0
            return (self.successful_requests / self.total_requests) * 100
        
        @property
        def average_response_time(self) -> float:
            """Calculate average response time"""
            if self.successful_requests == 0:
                return 0.0
            return self.total_response_time / self.successful_requests
    
    
    @dataclass
    class LoadBalancerStats:
        """Overall load balancer statistics"""
        total_requests: int = 0
        successful_requests: int = 0
        failed_requests: int = 0
        failovers: int = 0
        total_response_time: float = 0.0
        endpoint_metrics: Dict[str, EndpointMetrics] = field(default_factory=dict)
    
    
    class LLMEndpoint:
        """
        Represents a single LLM endpoint with health tracking.
        """
        
        def __init__(self, config: EndpointConfig):
            """
            Initialize endpoint.
            
            Args:
                config: Endpoint configuration
            """
            self.config = config
            self.metrics = EndpointMetrics()
            self.llm: Optional[ChatOpenAI] = None
            self.last_health_check: Optional[datetime] = None
            self.request_times: deque = deque(maxlen=100)  # Track recent request times
            
            self._initialize_llm()
        
        def _initialize_llm(self):
            """Initialize the LLM client"""
            try:
                # Use provided API key or default from environment
                api_key = self.config.api_key or os.getenv("OPENAI_API_KEY")
                
                self.llm = ChatOpenAI(
                    model=self.config.model,
                    openai_api_key=api_key,
                    timeout=self.config.timeout,
                    temperature=0.7
                )
                self.metrics.status = EndpointStatus.HEALTHY
            except Exception as e:
                print(f"Error initializing endpoint {self.config.id}: {e}")
                self.metrics.status = EndpointStatus.UNHEALTHY
                self.metrics.last_error = str(e)
                self.metrics.last_error_time = datetime.now()
        
        def is_available(self) -> bool:
            """Check if endpoint is available for requests"""
            return (
                self.metrics.status in [EndpointStatus.HEALTHY, EndpointStatus.DEGRADED]
                and self.llm is not None
                and self.metrics.active_requests  EndpointStatus:
            """
            Perform health check on endpoint.
            
            Returns:
                Current health status
            """
            try:
                if self.llm is None:
                    self._initialize_llm()
                
                # Simple health check - try a minimal request
                start_time = time.time()
                self.llm.invoke("test")
                response_time = time.time() - start_time
                
                # Update status based on response time and error rate
                if response_time  95:
                    self.metrics.status = EndpointStatus.HEALTHY
                elif response_time  80:
                    self.metrics.status = EndpointStatus.DEGRADED
                else:
                    self.metrics.status = EndpointStatus.UNHEALTHY
                
                self.last_health_check = datetime.now()
                return self.metrics.status
                
            except Exception as e:
                self.metrics.status = EndpointStatus.UNHEALTHY
                self.metrics.last_error = str(e)
                self.metrics.last_error_time = datetime.now()
                return EndpointStatus.UNHEALTHY
        
        def generate(self, prompt: str) -> Dict[str, Any]:
            """
            Generate response using this endpoint.
            
            Args:
                prompt: Input prompt
                
            Returns:
                Response dictionary
            """
            self.metrics.total_requests += 1
            self.metrics.active_requests += 1
            
            try:
                start_time = time.time()
                response = self.llm.invoke(prompt)
                response_time = time.time() - start_time
                
                self.metrics.successful_requests += 1
                self.metrics.total_response_time += response_time
                self.metrics.active_requests -= 1
                self.request_times.append(response_time)
                
                return {
                    "response": response.content,
                    "endpoint_id": self.config.id,
                    "response_time": response_time,
                    "success": True
                }
                
            except Exception as e:
                self.metrics.failed_requests += 1
                self.metrics.active_requests -= 1
                self.metrics.last_error = str(e)
                self.metrics.last_error_time = datetime.now()
                
                # Mark as unhealthy after repeated failures
                if self.metrics.success_rate  Optional[LLMEndpoint]:
            """Select endpoint using round-robin strategy"""
            attempts = 0
            while attempts  Optional[LLMEndpoint]:
            """Select endpoint using weighted round-robin strategy"""
            # Create weighted list
            weighted_endpoints = []
            for endpoint in self.endpoints:
                if endpoint.is_available():
                    weighted_endpoints.extend([endpoint] * endpoint.config.weight)
            
            if not weighted_endpoints:
                return None
            
            # Round-robin through weighted list
            endpoint = weighted_endpoints[self.current_endpoint_index % len(weighted_endpoints)]
            self.current_endpoint_index += 1
            return endpoint
        
        def _select_endpoint_least_loaded(self) -> Optional[LLMEndpoint]:
            """Select endpoint with least active requests"""
            available = [e for e in self.endpoints if e.is_available()]
            if not available:
                return None
            
            return min(available, key=lambda e: e.metrics.active_requests)
        
        def _select_endpoint_random(self) -> Optional[LLMEndpoint]:
            """Select random available endpoint"""
            available = [e for e in self.endpoints if e.is_available()]
            if not available:
                return None
            
            return random.choice(available)
        
        def _select_endpoint_fastest(self) -> Optional[LLMEndpoint]:
            """Select endpoint with fastest average response time"""
            available = [e for e in self.endpoints if e.is_available()]
            if not available:
                return None
            
            # Sort by average response time (endpoints with no history get high priority)
            return min(
                available,
                key=lambda e: e.metrics.average_response_time if e.metrics.successful_requests > 0 else 0.0
            )
        
        def _select_endpoint(self) -> Optional[LLMEndpoint]:
            """
            Select endpoint based on strategy.
            
            Returns:
                Selected endpoint or None if none available
            """
            if self.strategy == LoadBalancingStrategy.ROUND_ROBIN:
                return self._select_endpoint_round_robin()
            elif self.strategy == LoadBalancingStrategy.WEIGHTED_ROUND_ROBIN:
                return self._select_endpoint_weighted()
            elif self.strategy == LoadBalancingStrategy.LEAST_LOADED:
                return self._select_endpoint_least_loaded()
            elif self.strategy == LoadBalancingStrategy.RANDOM:
                return self._select_endpoint_random()
            elif self.strategy == LoadBalancingStrategy.FASTEST_RESPONSE:
                return self._select_endpoint_fastest()
            else:
                return self._select_endpoint_round_robin()
        
        def _perform_health_checks(self):
            """Perform health checks on all endpoints if needed"""
            now = datetime.now()
            if (now - self.last_health_check).seconds  Dict[str, Any]:
            """
            Generate response with load balancing and failover.
            
            Args:
                prompt: Input prompt
                max_retries: Maximum retry attempts
                
            Returns:
                Response dictionary
            """
            self.stats.total_requests += 1
            
            # Perform health checks if needed
            self._perform_health_checks()
            
            attempts = 0
            last_error = None
            
            while attempts  Dict[str, Any]:
            """Get load balancer statistics"""
            return {
                "total_requests": self.stats.total_requests,
                "successful_requests": self.stats.successful_requests,
                "failed_requests": self.stats.failed_requests,
                "failovers": self.stats.failovers,
                "success_rate": (
                    (self.stats.successful_requests / self.stats.total_requests * 100)
                    if self.stats.total_requests > 0 else 0.0
                ),
                "average_response_time": (
                    self.stats.total_response_time / self.stats.successful_requests
                    if self.stats.successful_requests > 0 else 0.0
                ),
                "endpoints": [
                    {
                        "id": e.config.id,
                        "model": e.config.model,
                        "status": e.metrics.status.value,
                        "total_requests": e.metrics.total_requests,
                        "success_rate": f"{e.metrics.success_rate:.2f}%",
                        "avg_response_time": f"{e.metrics.average_response_time:.3f}s",
                        "active_requests": e.metrics.active_requests
                    }
                    for e in self.endpoints
                ]
            }
    
    
    def demonstrate_load_balancing():
        """Demonstrate load balancing patterns"""
        print("=" * 80)
        print("LOAD BALANCING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic Round-Robin Load Balancing
        print("\n" + "=" * 80)
        print("EXAMPLE 1: Round-Robin Load Balancing")
        print("=" * 80)
        
        # Configure multiple endpoints (simulating multiple API keys or models)
        endpoints = [
            EndpointConfig(id="endpoint-1", model="gpt-3.5-turbo", weight=1),
            EndpointConfig(id="endpoint-2", model="gpt-3.5-turbo", weight=1),
            EndpointConfig(id="endpoint-3", model="gpt-3.5-turbo", weight=1),
        ]
        
        lb = LoadBalancer(endpoints, strategy=LoadBalancingStrategy.ROUND_ROBIN)
        
        print("\nConfigured 3 endpoints with round-robin strategy\n")
        print("Sending 6 requests to observe distribution:\n")
        
        queries = [
            "What is 2+2?",
            "What is Python?",
            "What is AI?",
            "What is ML?",
            "What is DL?",
            "What is NLP?"
        ]
        
        for i, query in enumerate(queries, 1):
            print(f"Request {i}: {query}")
            result = lb.generate(query)
            
            if result["success"]:
                print(f"  ✓ Endpoint: {result['endpoint_id']}")
                print(f"  Response time: {result['response_time']:.3f}s")
                print(f"  Response: {result['response'][:60]}...")
            else:
                print(f"  ✗ Error: {result['error']}")
            print()
        
        stats = lb.get_stats()
        print("Load Distribution:")
        for endpoint in stats["endpoints"]:
            print(f"  {endpoint['id']}: {endpoint['total_requests']} requests")
        
        # Example 2: Weighted Load Balancing
        print("\n" + "=" * 80)
        print("EXAMPLE 2: Weighted Load Balancing")
        print("=" * 80)
        
        weighted_endpoints = [
            EndpointConfig(id="premium-endpoint", model="gpt-4", weight=3),  # Gets more traffic
            EndpointConfig(id="standard-endpoint", model="gpt-3.5-turbo", weight=1),
        ]
        
        weighted_lb = LoadBalancer(
            weighted_endpoints,
            strategy=LoadBalancingStrategy.WEIGHTED_ROUND_ROBIN
        )
        
        print("\nConfigured 2 endpoints:")
        print("  - Premium (GPT-4): weight=3")
        print("  - Standard (GPT-3.5): weight=1")
        print("\nSending 8 requests:\n")
        
        for i in range(8):
            result = weighted_lb.generate(f"Request {i+1}")
            if result["success"]:
                print(f"Request {i+1} → {result['endpoint_id']}")
        
        print("\nExpected distribution: ~6 to premium, ~2 to standard")
        stats = weighted_lb.get_stats()
        for endpoint in stats["endpoints"]:
            print(f"  {endpoint['id']}: {endpoint['total_requests']} requests")
        
        # Example 3: Least-Loaded Strategy
        print("\n" + "=" * 80)
        print("EXAMPLE 3: Least-Loaded Strategy")
        print("=" * 80)
        
        ll_endpoints = [
            EndpointConfig(id="endpoint-A", model="gpt-3.5-turbo"),
            EndpointConfig(id="endpoint-B", model="gpt-3.5-turbo"),
        ]
        
        ll_lb = LoadBalancer(ll_endpoints, strategy=LoadBalancingStrategy.LEAST_LOADED)
        
        print("\nLeast-loaded strategy routes to endpoint with fewest active requests")
        print("This optimizes for load distribution in real-time\n")
        
        for i in range(4):
            result = ll_lb.generate(f"Query {i+1}")
            if result["success"]:
                print(f"Query {i+1} → {result['endpoint_id']}")
        
        # Example 4: Failover Handling
        print("\n" + "=" * 80)
        print("EXAMPLE 4: Automatic Failover")
        print("=" * 80)
        
        print("\nSimulating endpoint failure scenario:")
        print("  - 3 endpoints configured")
        print("  - Mark one as unhealthy")
        print("  - Load balancer automatically routes around it\n")
        
        failover_endpoints = [
            EndpointConfig(id="healthy-1", model="gpt-3.5-turbo"),
            EndpointConfig(id="unhealthy", model="gpt-3.5-turbo"),
            EndpointConfig(id="healthy-2", model="gpt-3.5-turbo"),
        ]
        
        fo_lb = LoadBalancer(failover_endpoints, strategy=LoadBalancingStrategy.ROUND_ROBIN)
        
        # Simulate endpoint failure
        fo_lb.endpoints[1].metrics.status = EndpointStatus.UNHEALTHY
        print("Marked 'unhealthy' endpoint as UNHEALTHY\n")
        
        print("Sending 4 requests:\n")
        for i in range(4):
            result = fo_lb.generate(f"Query {i+1}")
            if result["success"]:
                print(f"Query {i+1} → {result['endpoint_id']} ✓")
        
        print("\nOnly healthy endpoints received requests!")
        
        # Example 5: Performance Comparison
        print("\n" + "=" * 80)
        print("EXAMPLE 5: Strategy Performance Comparison")
        print("=" * 80)
        
        strategies = [
            LoadBalancingStrategy.ROUND_ROBIN,
            LoadBalancingStrategy.RANDOM,
            LoadBalancingStrategy.LEAST_LOADED,
        ]
        
        print("\nComparing different load balancing strategies:\n")
        
        for strategy in strategies:
            test_endpoints = [
                EndpointConfig(id=f"ep-{i}", model="gpt-3.5-turbo")
                for i in range(3)
            ]
            
            test_lb = LoadBalancer(test_endpoints, strategy=strategy)
            
            # Send some requests
            for i in range(6):
                test_lb.generate(f"Test query {i}")
            
            stats = test_lb.get_stats()
            print(f"{strategy.value.upper()}:")
            print(f"  Success Rate: {stats['success_rate']:.2f}%")
            print(f"  Total Requests: {stats['total_requests']}")
            for ep in stats["endpoints"]:
                print(f"    {ep['id']}: {ep['total_requests']} requests")
            print()
        
        # Example 6: Monitoring and Statistics
        print("\n" + "=" * 80)
        print("EXAMPLE 6: Comprehensive Statistics")
        print("=" * 80)
        
        monitor_endpoints = [
            EndpointConfig(id="prod-1", model="gpt-3.5-turbo"),
            EndpointConfig(id="prod-2", model="gpt-3.5-turbo"),
        ]
        
        monitor_lb = LoadBalancer(monitor_endpoints, strategy=LoadBalancingStrategy.ROUND_ROBIN)
        
        print("\nSimulating production workload...\n")
        
        # Simulate various scenarios
        for i in range(10):
            result = monitor_lb.generate(f"Production query {i+1}")
        
        stats = monitor_lb.get_stats()
        
        print("Load Balancer Statistics:")
        print(f"  Total Requests: {stats['total_requests']}")
        print(f"  Successful: {stats['successful_requests']}")
        print(f"  Failed: {stats['failed_requests']}")
        print(f"  Success Rate: {stats['success_rate']:.2f}%")
        print(f"  Failovers: {stats['failovers']}")
        print(f"  Avg Response Time: {stats['average_response_time']:.3f}s")
        
        print("\nPer-Endpoint Metrics:")
        for ep in stats["endpoints"]:
            print(f"\n  {ep['id']} ({ep['model']}):")
            print(f"    Status: {ep['status']}")
            print(f"    Requests: {ep['total_requests']}")
            print(f"    Success Rate: {ep['success_rate']}")
            print(f"    Avg Response Time: {ep['avg_response_time']}")
            print(f"    Active Requests: {ep['active_requests']}")
        
        # Summary
        print("\n" + "=" * 80)
        print("LOAD BALANCING PATTERN SUMMARY")
        print("=" * 80)
        print("""
    Load Balancing Benefits:
    1. High Availability: 99.9%+ uptime through redundancy
    2. Performance: Better throughput via parallel processing
    3. Fault Tolerance: Automatic failover on endpoint failure
    4. Scalability: Easy to add more endpoints
    5. Cost Optimization: Route based on cost/performance
    6. Rate Limit Management: Distribute across API keys
    
    Load Balancing Strategies:
    1. Round Robin
       - Equal distribution across endpoints
       - Simple and predictable
       - Good for homogeneous endpoints
       - Use when: All endpoints have similar capacity
    
    2. Weighted Round Robin
       - Priority-based distribution
       - Higher weight = more requests
       - Good for heterogeneous endpoints
       - Use when: Different endpoint capacities
    
    3. Least Loaded
       - Route to least busy endpoint
       - Optimizes for current load
       - Dynamic adjustment
       - Use when: Variable request durations
    
    4. Random
       - Random endpoint selection
       - Stateless and simple
       - Good load distribution over time
       - Use when: Simplicity preferred
    
    5. Fastest Response
       - Route to historically fastest endpoint
       - Performance-optimized
       - Adaptive to endpoint characteristics
       - Use when: Latency is critical
    
    Health Checking:
    1. Active Health Checks
       - Regular test requests
       - Verify endpoint availability
       - Monitor response times
       - Interval: 30-60 seconds
    
    2. Passive Health Monitoring
       - Track request success/failure
       - Monitor error rates
       - Calculate response times
       - Continuous monitoring
    
    3. Status Levels
       - HEALTHY:  95% success
       - DEGRADED:  80% success
       - UNHEALTHY: Slow or 1000 req/hr)
    ✓ SLA requirements (>99% uptime)
    ✓ Rate limit concerns
    ✓ Multi-provider deployments
    ✓ Geographic distribution needed
    ✗ Low-traffic applications
    ✗ Single endpoint sufficient
    ✗ Cost constraints (added complexity)
    
    ROI Analysis:
    - Uptime improvement: 99% → 99.9% (10x fewer outages)
    - Performance: 2-3x throughput capacity
    - User satisfaction: Reduced error rates
    - Cost: +20% complexity, but worth it for scale
    - Break-even: ~500 requests/hour
    """)
        
        print("\n" + "=" * 80)
        print("Pattern 090 (Load Balancing) demonstration complete!")
        print("=" * 80)
    
    
    if __name__ == "__main__":
        demonstrate_load_balancing()
    
        

    Agentic Design Pattern: Load Balancing

    Pattern 090: Load Balancing – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Load Balancing pattern implementation.


    Pattern Description

    Pattern 090: Load Balancing Pattern

    Description: The Load Balancing Pattern distributes requests across multiple LLM endpoints or instances to achieve high availability, improved performance, and fault tolerance. In production LLM applications, load balancing is critical for handling high traffic volumes, preventing single points of failure, and ensuring consistent service quality even when individual endpoints fail or become overloaded.

    Load balancing strategies enable:
    - High availability through redundancy
    - Improved performance via parallel processing
    - Cost optimization through provider selection
    - Rate limit management across multiple accounts
    - Geographic distribution for lower latency
    - Failover and disaster recovery
    

    Components: 1. Load Balancing Strategies – Round Robin: Distribute equally across endpoints – Weighted Round Robin: Priority-based distribution – Least Loaded: Send to endpoint with lowest load – Random: Random endpoint selection – Geographic: Route based on location – Cost-Based: Choose cheapest available endpoint

    2. Health Checking
       - Endpoint availability monitoring
       - Response time tracking
       - Error rate monitoring
       - Periodic health probes
       - Automatic endpoint removal/recovery
    
    3. Request Management
       - Request queuing and buffering
       - Retry logic with backoff
       - Circuit breaker pattern
       - Request timeout handling
       - Priority-based routing
    
    4. Failover Mechanisms
       - Automatic endpoint switching
       - Degraded mode operation
       - Fallback to alternative providers
       - Request replay on failure
       - State preservation
    

    Use Cases: 1. High-Traffic Applications – Chatbots serving thousands of users – API services with SLA requirements – Real-time processing pipelines – Multi-tenant platforms – Peak load handling

    2. Multi-Provider Deployments
       - OpenAI + Anthropic + Google
       - Primary and backup providers
       - Cost optimization strategies
       - Feature-specific routing
       - Geographic compliance
    
    3. Rate Limit Management
       - Distribute across multiple API keys
       - Prevent rate limit exhaustion
       - Smooth request distribution
       - Burst handling
       - Cost control
    
    4. Disaster Recovery
       - Automatic failover
       - Multi-region deployment
       - Zero-downtime updates
       - Service degradation handling
       - Business continuity
    
    5. Performance Optimization
       - Parallel request processing
       - Load-based routing
       - Latency optimization
       - Resource utilization
       - Capacity planning
    

    LangChain Implementation: LangChain doesn’t have built-in load balancing, but you can implement it using: – Custom LLM wrappers – Callback handlers for monitoring – Retry logic with different models – Custom routing logic – Health check mechanisms

    Key Features: 1. Endpoint Management – Dynamic endpoint registration – Health status tracking – Automatic recovery – Configuration updates – Version management

    2. Intelligent Routing
       - Strategy selection
       - Real-time decision making
       - Performance-based routing
       - Cost optimization
       - SLA enforcement
    
    3. Fault Tolerance
       - Automatic retry with exponential backoff
       - Circuit breaker to prevent cascading failures
       - Graceful degradation
       - Error handling and recovery
       - Request preservation
    
    4. Monitoring
       - Request distribution metrics
       - Endpoint health status
       - Response time tracking
       - Error rate monitoring
       - Cost tracking per endpoint
    

    Best Practices: 1. Health Monitoring – Regular health checks (every 30-60s) – Multiple health metrics (latency, errors, availability) – Automatic endpoint recovery – Alert on degraded performance – Log all health transitions

    2. Load Distribution
       - Consider endpoint capacity
       - Account for different model speeds
       - Balance cost vs performance
       - Avoid overloading any single endpoint
       - Reserve capacity for spikes
    
    3. Failover Strategy
       - Fast failure detection (< 5s)
       - Quick failover (< 1s)
       - Preserve request context
       - Transparent to users
       - Log all failovers
    
    4. Request Management
       - Set appropriate timeouts
       - Implement retry with backoff
       - Queue management for spikes
       - Priority handling
       - Request deduplication
    

    Trade-offs: Advantages: – High availability (99.9%+ uptime) – Better performance through parallelization – Fault tolerance and resilience – Flexible scaling – Cost optimization opportunities – Geographic distribution

    Disadvantages:
    - Increased complexity
    - Additional monitoring requirements
    - Potential consistency issues
    - Higher operational costs
    - Configuration management overhead
    - Testing complexity
    

    Production Considerations: 1. Endpoint Configuration – Multiple providers (OpenAI, Anthropic, etc.) – Multiple API keys per provider – Geographic distribution – Capacity planning – Cost monitoring

    2. Monitoring and Alerting
       - Real-time health dashboards
       - Performance metrics
       - Error tracking
       - Cost tracking
       - SLA compliance monitoring
    
    3. Security
       - Secure credential management
       - Rate limiting per client
       - Access control
       - Audit logging
       - Encryption in transit
    
    4. Scaling
       - Horizontal scaling capability
       - Auto-scaling based on load
       - Capacity planning
       - Performance testing
       - Load testing
    
    5. Disaster Recovery
       - Multi-region deployment
       - Backup endpoints
       - Failover procedures
       - Data consistency
       - Recovery time objectives (RTO)
    

    File Statistics

    • Total Lines: 988
    • Classes: 7
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 090: Load Balancing Pattern
    
    Description:
        The Load Balancing Pattern distributes requests across multiple LLM endpoints or
        instances to achieve high availability, improved performance, and fault tolerance.
        In production LLM applications, load balancing is critical for handling high traffic
        volumes, preventing single points of failure, and ensuring consistent service
        quality even when individual endpoints fail or become overloaded.
    
        Load balancing strategies enable:
        - High availability through redundancy
        - Improved performance via parallel processing
        - Cost optimization through provider selection
        - Rate limit management across multiple accounts
        - Geographic distribution for lower latency
        - Failover and disaster recovery
    
    Components:
        1. Load Balancing Strategies
           - Round Robin: Distribute equally across endpoints
           - Weighted Round Robin: Priority-based distribution
           - Least Loaded: Send to endpoint with lowest load
           - Random: Random endpoint selection
           - Geographic: Route based on location
           - Cost-Based: Choose cheapest available endpoint
    
        2. Health Checking
           - Endpoint availability monitoring
           - Response time tracking
           - Error rate monitoring
           - Periodic health probes
           - Automatic endpoint removal/recovery
    
        3. Request Management
           - Request queuing and buffering
           - Retry logic with backoff
           - Circuit breaker pattern
           - Request timeout handling
           - Priority-based routing
    
        4. Failover Mechanisms
           - Automatic endpoint switching
           - Degraded mode operation
           - Fallback to alternative providers
           - Request replay on failure
           - State preservation
    
    Use Cases:
        1. High-Traffic Applications
           - Chatbots serving thousands of users
           - API services with SLA requirements
           - Real-time processing pipelines
           - Multi-tenant platforms
           - Peak load handling
    
        2. Multi-Provider Deployments
           - OpenAI + Anthropic + Google
           - Primary and backup providers
           - Cost optimization strategies
           - Feature-specific routing
           - Geographic compliance
    
        3. Rate Limit Management
           - Distribute across multiple API keys
           - Prevent rate limit exhaustion
           - Smooth request distribution
           - Burst handling
           - Cost control
    
        4. Disaster Recovery
           - Automatic failover
           - Multi-region deployment
           - Zero-downtime updates
           - Service degradation handling
           - Business continuity
    
        5. Performance Optimization
           - Parallel request processing
           - Load-based routing
           - Latency optimization
           - Resource utilization
           - Capacity planning
    
    LangChain Implementation:
        LangChain doesn't have built-in load balancing, but you can implement
        it using:
        - Custom LLM wrappers
        - Callback handlers for monitoring
        - Retry logic with different models
        - Custom routing logic
        - Health check mechanisms
    
    Key Features:
        1. Endpoint Management
           - Dynamic endpoint registration
           - Health status tracking
           - Automatic recovery
           - Configuration updates
           - Version management
    
        2. Intelligent Routing
           - Strategy selection
           - Real-time decision making
           - Performance-based routing
           - Cost optimization
           - SLA enforcement
    
        3. Fault Tolerance
           - Automatic retry with exponential backoff
           - Circuit breaker to prevent cascading failures
           - Graceful degradation
           - Error handling and recovery
           - Request preservation
    
        4. Monitoring
           - Request distribution metrics
           - Endpoint health status
           - Response time tracking
           - Error rate monitoring
           - Cost tracking per endpoint
    
    Best Practices:
        1. Health Monitoring
           - Regular health checks (every 30-60s)
           - Multiple health metrics (latency, errors, availability)
           - Automatic endpoint recovery
           - Alert on degraded performance
           - Log all health transitions
    
        2. Load Distribution
           - Consider endpoint capacity
           - Account for different model speeds
           - Balance cost vs performance
           - Avoid overloading any single endpoint
           - Reserve capacity for spikes
    
        3. Failover Strategy
           - Fast failure detection (< 5s)
           - Quick failover (< 1s)
           - Preserve request context
           - Transparent to users
           - Log all failovers
    
        4. Request Management
           - Set appropriate timeouts
           - Implement retry with backoff
           - Queue management for spikes
           - Priority handling
           - Request deduplication
    
    Trade-offs:
        Advantages:
        - High availability (99.9%+ uptime)
        - Better performance through parallelization
        - Fault tolerance and resilience
        - Flexible scaling
        - Cost optimization opportunities
        - Geographic distribution
    
        Disadvantages:
        - Increased complexity
        - Additional monitoring requirements
        - Potential consistency issues
        - Higher operational costs
        - Configuration management overhead
        - Testing complexity
    
    Production Considerations:
        1. Endpoint Configuration
           - Multiple providers (OpenAI, Anthropic, etc.)
           - Multiple API keys per provider
           - Geographic distribution
           - Capacity planning
           - Cost monitoring
    
        2. Monitoring and Alerting
           - Real-time health dashboards
           - Performance metrics
           - Error tracking
           - Cost tracking
           - SLA compliance monitoring
    
        3. Security
           - Secure credential management
           - Rate limiting per client
           - Access control
           - Audit logging
           - Encryption in transit
    
        4. Scaling
           - Horizontal scaling capability
           - Auto-scaling based on load
           - Capacity planning
           - Performance testing
           - Load testing
    
        5. Disaster Recovery
           - Multi-region deployment
           - Backup endpoints
           - Failover procedures
           - Data consistency
           - Recovery time objectives (RTO)
    """
    

    Purpose: This module implements the Load Balancing pattern, providing a structured approach to agent design and implementation.


    Import Statements

    Standard Library Imports

    import collections.deque
    import dataclasses.dataclass
    import dataclasses.field
    import datetime.datetime
    import datetime.timedelta
    import enum.Enum
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import random
    import time
    import typing.Any
    import typing.Callable
    import typing.Dict
    import typing.List
    import typing.Optional
    

    Purpose: Core Python functionality: – collections.deque: Specialized container datatypes – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – datetime.timedelta: Date and time handling – enum.Enum: Enumeration support – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – random: Random number generation – time: Time-related functions – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Callable: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

    Purpose: LangChain framework components: – LLM Interface: Wrapper for OpenAI models – Chains: Composable building blocks for complex workflows

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: LoadBalancingStrategy

    Lines 222-228

    Inherits from: Enum

    Purpose:

    Load balancing strategies
    

    Class: EndpointStatus

    Lines 231-236

    Inherits from: Enum

    Purpose:

    Endpoint health status
    

    Class: EndpointConfig

    Lines 240-248

    Purpose:

    Configuration for an LLM endpoint
    

    Class: EndpointMetrics

    Lines 252-275

    Purpose:

    Metrics for an endpoint
    

    Methods:

    success_rate(self)

    Lines 264-268

    Decorators: property

    Purpose:

    Calculate success rate
    

    Parameters:

    Returns: float

    average_response_time(self)

    Lines 271-275

    Decorators: property

    Purpose:

    Calculate average response time
    

    Parameters:

    Returns: float


    Class: LoadBalancerStats

    Lines 279-286

    Purpose:

    Overall load balancer statistics
    

    Class: LLMEndpoint

    Lines 289-414

    Purpose:

    Represents a single LLM endpoint with health tracking.
    

    Methods:

    __init__(self, config)

    Lines 294-307

    Purpose:

    Initialize endpoint.
    
    Args:
        config: Endpoint configuration
    

    Parameters: – config: Config

    _initialize_llm(self)

    Lines 309-326

    Purpose:

    Initialize the LLM client
    

    Parameters:

    is_available(self)

    Lines 328-334

    Purpose:

    Check if endpoint is available for requests
    

    Parameters:

    Returns: bool

    check_health(self)

    Lines 336-367

    Purpose:

    Perform health check on endpoint.
    
    Returns:
        Current health status
    

    Parameters:

    Returns: EndpointStatus

    generate(self, prompt)

    Lines 369-414

    Purpose:

    Generate response using this endpoint.
    
    Args:
        prompt: Input prompt
    
    Returns:
        Response dictionary
    

    Parameters: – prompt: Prompt template or text

    Returns: <ast.Subscript object at 0x000001A06D7523D0>


    Class: LoadBalancer

    Lines 417-622

    Purpose:

    Load balancer for distributing requests across multiple LLM endpoints.
    
    Supports multiple load balancing strategies and automatic failover.
    

    Methods:

    __init__(self, endpoints, strategy, health_check_interval)

    Lines 424-449

    Purpose:

    Initialize load balancer.
    
    Args:
        endpoints: List of endpoint configurations
        strategy: Load balancing strategy
        health_check_interval: Seconds between health checks
    

    Parameters: – endpoints: Endpoints – strategy: Strategy – health_check_interval: Health check interval

    _select_endpoint_round_robin(self)

    Lines 451-463

    Purpose:

    Select endpoint using round-robin strategy
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D8881D0>

    _select_endpoint_weighted(self)

    Lines 465-479

    Purpose:

    Select endpoint using weighted round-robin strategy
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D88B410>

    _select_endpoint_least_loaded(self)

    Lines 481-487

    Purpose:

    Select endpoint with least active requests
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D75B250>

    _select_endpoint_random(self)

    Lines 489-495

    Purpose:

    Select random available endpoint
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D75A790>

    _select_endpoint_fastest(self)

    Lines 497-507

    Purpose:

    Select endpoint with fastest average response time
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D80CF90>

    _select_endpoint(self)

    Lines 509-527

    Purpose:

    Select endpoint based on strategy.
    
    Returns:
        Selected endpoint or None if none available
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D157C90>

    _perform_health_checks(self)

    Lines 529-538

    Purpose:

    Perform health checks on all endpoints if needed
    

    Parameters:

    generate(self, prompt, max_retries)

    Lines 540-593

    Purpose:

    Generate response with load balancing and failover.
    
    Args:
        prompt: Input prompt
        max_retries: Maximum retry attempts
    
    Returns:
        Response dictionary
    

    Parameters: – prompt: Prompt template or text – max_retries: Max retries

    Returns: <ast.Subscript object at 0x000001A06D83E910>

    get_stats(self)

    Lines 595-622

    Purpose:

    Get load balancer statistics
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D850690>


    Functions

    demonstrate_load_balancing()

    Lines 625-983

    Purpose:

    Demonstrate load balancing patterns
    

    Key Concepts Summary

    Pattern Overview

    The Load Balancing pattern provides a structured approach to building AI agents with specific capabilities and behaviors. This implementation leverages LangChain’s composable components to create a robust and maintainable solution.

    Core Components

    1. Agent Class: Main implementation of the pattern logic
    2. Prompt Engineering: Carefully crafted prompts to guide LLM behavior
    3. Tool Integration: Optional tools for extended functionality
    4. Memory Management: State handling across interactions
    5. Output Parsing: Structured response processing

    When to Use

    ✅ Good for: – Scenarios requiring this specific pattern’s strengths – Applications needing structured agent behavior – Systems where explainability is important – Production environments requiring reliability

    ❌ Not ideal for: – Simple single-prompt tasks – Scenarios requiring different pattern characteristics – Resource-constrained environments (if computationally intensive)


    Best Practices Demonstrated

    1. Environment Configuration

    • ✅ Use .env files for API keys
    • ✅ Never hardcode credentials
    • ✅ Validate environment variables on startup

    2. Error Handling

    • ✅ Graceful degradation on failures
    • ✅ Informative error messages
    • ✅ Logging for debugging

    3. Prompt Engineering

    • ✅ Clear, specific instructions
    • ✅ Structured output formats
    • ✅ Few-shot examples when beneficial

    4. Code Organization

    • ✅ Separation of concerns (class-based design)
    • ✅ Type hints for clarity
    • ✅ Docstrings for documentation

    5. Performance Optimization

    • ✅ Appropriate temperature settings
    • ✅ Token budget management
    • ✅ Caching where applicable

    LangChain Components Used

    • ChatOpenAI: OpenAI model interface
    • Chains: Sequential component composition

    Why LangChain? – Composable abstractions for LLM applications – Rich ecosystem of integrations – Production-ready patterns – Active community and development


    Execution Flow

    1. Initialize agent with configuration
       ↓
    2. Load environment and dependencies
       ↓
    3. Prepare prompt with input data
       ↓
    4. Invoke LLM through LangChain
       ↓
    5. Parse and validate response
       ↓
    6. Return formatted result
    

    Key Decision Points: – Model selection (temperature, model type) – Prompt structure (few-shot vs zero-shot) – Error handling strategy – Output format validation


    Performance Considerations

    Computational Cost

    • LLM Calls: Primary cost factor (based on tokens)
    • Latency: Depends on model and input size
    • Memory: Minimal for most patterns

    Optimization Strategies

    1. Prompt Optimization: Minimize tokens while maintaining clarity
    2. Caching: Cache repeated queries/responses
    3. Model Selection: Use smaller models when appropriate
    4. Batch Processing: Group similar requests
    5. Streaming: For long responses, stream tokens

    Cost Estimates (Approximate)

    • GPT-3.5-turbo: ~$0.002 per request
    • GPT-4: ~$0.03 per request
    • GPT-4-turbo: ~$0.01 per request

    Actual costs vary based on input/output token counts


    Common Pitfalls to Avoid

    1. Prompt Design Issues

    ❌ Vague or ambiguous instructions
    ✅ Clear, specific, testable prompts

    2. Error Handling

    ❌ Ignoring API errors or rate limits
    ✅ Implement retry logic and graceful degradation

    3. Token Management

    ❌ Exceeding model token limits
    ✅ Truncate inputs, chunk processing

    4. Security

    ❌ Exposing API keys in code
    ✅ Use environment variables

    5. Testing

    ❌ Only testing happy paths
    ✅ Test edge cases, failures, malformed inputs

    6. Cost Management

    ❌ Unmonitored API usage
    ✅ Implement budgets, alerts, and monitoring


    Further Improvements

    Production Enhancements

    1. Monitoring: Add logging, metrics, tracing
    2. Validation: Input/output schema validation
    3. Rate Limiting: Implement request throttling
    4. Caching: Redis or in-memory cache for responses
    5. Async Support: Non-blocking operations for scalability

    Feature Extensions

    1. Multi-Model Support: Switch between providers
    2. Fallback Models: Graceful degradation strategy
    3. Custom Tools: Domain-specific function calling
    4. Advanced Memory: Vector stores for long-term memory
    5. Evaluation: Automated testing and benchmarking

    Code Quality

    1. Testing: Unit tests, integration tests
    2. Documentation: API docs, usage examples
    3. Type Safety: Stricter type hints
    4. Configuration: Externalized config files

    Related Patterns

    • Pattern 089
    • Pattern 091

    Pattern Families: – Reasoning Patterns (001-011) – Multi-Agent Patterns (012-019) – Tool Use Patterns (020-025) – Memory Patterns (026-031) – And many more…


    Summary

    The Load Balancing pattern provides a powerful approach to building AI agents with specific capabilities. This implementation demonstrates:

    ✅ Key Strengths: – Clean, maintainable code structure – LangChain integration for composability – Type hints and documentation – Production-ready patterns

    🎯 Best Use Cases: – Applications requiring this pattern’s characteristics – Systems needing structured agent behavior – Production environments requiring reliability

    📚 Learning Resources: – LangChain Documentation: https://python.langchain.com/ – Pattern Documentation: See related patterns – OpenAI API Reference: https://platform.openai.com/docs/


    File: 090_load_balancing.py
    Total Lines: 988
    Pattern Category: AI Agent Design Pattern #090


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Caching Patterns

    
        """
    Pattern 089: Caching Patterns
    
    Description:
        The Caching Patterns involve storing and reusing previously computed results to
        improve performance, reduce latency, and minimize costs. In the context of LLM
        applications, caching is critical for optimizing response times and reducing API
        calls for repeated or similar queries. Effective caching strategies can
        dramatically improve user experience while significantly cutting operational costs.
    
        Caching in LLM applications targets:
        - Complete responses (exact match caching)
        - Embeddings (vector representations)
        - Prompt templates (reusable structures)
        - Intermediate results (multi-step workflows)
        - API responses (rate limit optimization)
    
    Components:
        1. Cache Types
           - In-memory cache (fast, volatile)
           - Disk cache (persistent, slower)
           - Distributed cache (Redis, Memcached)
           - Database cache (structured, queryable)
           - CDN cache (geographic distribution)
    
        2. Cache Keys
           - Exact match (full prompt hash)
           - Semantic match (embedding similarity)
           - Parametric keys (template + parameters)
           - Hierarchical keys (nested cache structures)
           - Time-based keys (temporal validity)
    
        3. Cache Policies
           - TTL (Time To Live) - expire after time
           - LRU (Least Recently Used) - evict old entries
           - LFU (Least Frequently Used) - evict unpopular
           - Size-based - limit total cache size
           - Custom policies - application-specific
    
        4. Invalidation Strategies
           - Time-based expiration
           - Event-triggered invalidation
           - Manual invalidation
           - Cascade invalidation (dependencies)
           - Version-based invalidation
    
    Use Cases:
        1. Repeated Queries
           - FAQ responses
           - Common questions
           - Popular content
           - Standard procedures
           - Frequently accessed data
    
        2. Expensive Operations
           - Complex reasoning chains
           - Multi-step workflows
           - Large context processing
           - Batch operations
           - Resource-intensive computations
    
        3. Rate Limit Optimization
           - Reduce API calls
           - Stay within quotas
           - Handle traffic spikes
           - Cost optimization
           - Performance consistency
    
        4. Embeddings and Vectors
           - Document embeddings
           - Query embeddings
           - Semantic search optimization
           - Similarity computations
           - Vector database queries
    
        5. Multi-User Applications
           - Shared responses
           - Common workflows
           - Popular queries
           - Template responses
           - Collaborative features
    
    LangChain Implementation:
        LangChain supports caching through:
        - InMemoryCache for simple caching
        - SQLiteCache for persistent caching
        - Custom cache implementations
        - Semantic similarity caching
        - Embedding caching
    
    Key Features:
        1. Multiple Cache Layers
           - L1: In-memory (fastest)
           - L2: Local disk (persistent)
           - L3: Distributed cache (shared)
           - L4: Database (long-term)
    
        2. Intelligent Matching
           - Exact match for identical queries
           - Semantic match for similar queries
           - Fuzzy match with thresholds
           - Parametric templates
    
        3. Performance Optimization
           - Fast lookup ( 70%)
           - Measure latency improvements
           - Calculate cost savings
           - Identify cache misses patterns
    
        4. Handle Cache Misses
           - Graceful fallback to computation
           - Transparent to users
           - Background cache warming
           - Predictive pre-caching
    
    Trade-offs:
        Advantages:
        - Dramatically improved response times
        - Significant cost reduction
        - Better user experience
        - Reduced API rate limiting
        - Lower resource consumption
        - Improved scalability
    
        Disadvantages:
        - Stale data risk
        - Memory/storage overhead
        - Cache consistency challenges
        - Complexity in invalidation
        - Initial cache warming time
        - Debugging complexity
    
    Production Considerations:
        1. Cache Size Management
           - Monitor memory usage
           - Implement eviction policies
           - Set size limits
           - Regular cleanup
           - Compression strategies
    
        2. Cache Consistency
           - Version tracking
           - Update propagation
           - Invalidation timing
           - Distributed consistency
           - Race condition handling
    
        3. Security
           - Access control for cached data
           - Encryption for sensitive content
           - Cache poisoning prevention
           - User isolation in multi-tenant
           - Audit trails
    
        4. Monitoring
           - Hit/miss rates
           - Cache size trends
           - Eviction frequency
           - Cost savings
           - Performance impact
    
        5. Disaster Recovery
           - Cache rebuild procedures
           - Backup strategies
           - Failover mechanisms
           - Data loss handling
           - Recovery time objectives
    """
    
    import os
    import time
    import hashlib
    import json
    from typing import List, Dict, Any, Optional, Callable
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime, timedelta
    from collections import OrderedDict
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI, OpenAIEmbeddings
    from langchain_core.caches import InMemoryCache
    from langchain.globals import set_llm_cache
    
    load_dotenv()
    
    
    class CachePolicy(Enum):
        """Cache eviction policies"""
        LRU = "lru"  # Least Recently Used
        LFU = "lfu"  # Least Frequently Used
        FIFO = "fifo"  # First In First Out
        TTL = "ttl"  # Time To Live
    
    
    @dataclass
    class CacheEntry:
        """Represents a cached entry"""
        key: str
        value: Any
        created_at: datetime
        last_accessed: datetime
        access_count: int = 0
        ttl: Optional[int] = None  # TTL in seconds
        
        def is_expired(self) -> bool:
            """Check if entry is expired"""
            if self.ttl is None:
                return False
            age = (datetime.now() - self.created_at).total_seconds()
            return age > self.ttl
        
        def touch(self):
            """Update access information"""
            self.last_accessed = datetime.now()
            self.access_count += 1
    
    
    @dataclass
    class CacheStats:
        """Cache performance statistics"""
        hits: int = 0
        misses: int = 0
        evictions: int = 0
        total_requests: int = 0
        
        @property
        def hit_rate(self) -> float:
            """Calculate cache hit rate"""
            if self.total_requests == 0:
                return 0.0
            return (self.hits / self.total_requests) * 100
        
        @property
        def miss_rate(self) -> float:
            """Calculate cache miss rate"""
            return 100.0 - self.hit_rate
    
    
    class SimpleCache:
        """
        Simple in-memory cache with multiple eviction policies.
        
        This cache supports:
        1. Multiple eviction policies (LRU, LFU, FIFO, TTL)
        2. Size limits
        3. TTL per entry
        4. Performance statistics
        """
        
        def __init__(
            self,
            max_size: int = 1000,
            policy: CachePolicy = CachePolicy.LRU,
            default_ttl: Optional[int] = None
        ):
            """
            Initialize cache.
            
            Args:
                max_size: Maximum number of entries
                policy: Eviction policy
                default_ttl: Default TTL in seconds
            """
            self.max_size = max_size
            self.policy = policy
            self.default_ttl = default_ttl
            self.cache: OrderedDict[str, CacheEntry] = OrderedDict()
            self.stats = CacheStats()
        
        def _make_key(self, key: Any) -> str:
            """Create cache key from input"""
            if isinstance(key, str):
                return hashlib.md5(key.encode()).hexdigest()
            return hashlib.md5(json.dumps(key, sort_keys=True).encode()).hexdigest()
        
        def _evict(self):
            """Evict entries based on policy"""
            if len(self.cache)  Optional[Any]:
            """
            Get value from cache.
            
            Args:
                key: Cache key
                
            Returns:
                Cached value or None
            """
            self.stats.total_requests += 1
            cache_key = self._make_key(key)
            
            # Remove expired entries
            self._remove_expired()
            
            if cache_key in self.cache:
                entry = self.cache[cache_key]
                
                if entry.is_expired():
                    del self.cache[cache_key]
                    self.stats.misses += 1
                    return None
                
                entry.touch()
                
                # Move to end for LRU
                if self.policy == CachePolicy.LRU:
                    self.cache.move_to_end(cache_key)
                
                self.stats.hits += 1
                return entry.value
            
            self.stats.misses += 1
            return None
        
        def set(self, key: Any, value: Any, ttl: Optional[int] = None):
            """
            Set value in cache.
            
            Args:
                key: Cache key
                value: Value to cache
                ttl: Optional TTL in seconds
            """
            cache_key = self._make_key(key)
            
            # Evict if necessary
            if cache_key not in self.cache:
                self._evict()
            
            entry = CacheEntry(
                key=cache_key,
                value=value,
                created_at=datetime.now(),
                last_accessed=datetime.now(),
                ttl=ttl or self.default_ttl
            )
            
            self.cache[cache_key] = entry
            
            # Move to end for LRU
            if self.policy == CachePolicy.LRU:
                self.cache.move_to_end(cache_key)
        
        def clear(self):
            """Clear all cache entries"""
            self.cache.clear()
            self.stats = CacheStats()
        
        def size(self) -> int:
            """Get current cache size"""
            return len(self.cache)
        
        def get_stats(self) -> Dict[str, Any]:
            """Get cache statistics"""
            return {
                "hits": self.stats.hits,
                "misses": self.stats.misses,
                "evictions": self.stats.evictions,
                "total_requests": self.stats.total_requests,
                "hit_rate": f"{self.stats.hit_rate:.2f}%",
                "miss_rate": f"{self.stats.miss_rate:.2f}%",
                "current_size": len(self.cache),
                "max_size": self.max_size
            }
    
    
    class SemanticCache:
        """
        Semantic cache using embeddings for similarity matching.
        
        This cache finds similar queries using vector similarity,
        allowing cache hits for semantically similar but not identical queries.
        """
        
        def __init__(
            self,
            similarity_threshold: float = 0.95,
            max_size: int = 500
        ):
            """
            Initialize semantic cache.
            
            Args:
                similarity_threshold: Minimum similarity for cache hit
                max_size: Maximum cache entries
            """
            self.similarity_threshold = similarity_threshold
            self.max_size = max_size
            self.embeddings = OpenAIEmbeddings()
            self.cache: List[Dict[str, Any]] = []
            self.stats = CacheStats()
        
        def _compute_similarity(self, query1: str, query2: str) -> float:
            """
            Compute semantic similarity between queries.
            
            Args:
                query1: First query
                query2: Second query
                
            Returns:
                Similarity score (0-1)
            """
            # Simple implementation - in production use precomputed embeddings
            # This is for demonstration only
            query1_lower = query1.lower()
            query2_lower = query2.lower()
            
            # Simple word overlap similarity
            words1 = set(query1_lower.split())
            words2 = set(query2_lower.split())
            
            if not words1 or not words2:
                return 0.0
            
            intersection = words1.intersection(words2)
            union = words1.union(words2)
            
            return len(intersection) / len(union)
        
        def get(self, query: str) -> Optional[Any]:
            """
            Get value for semantically similar query.
            
            Args:
                query: Query string
                
            Returns:
                Cached value or None
            """
            self.stats.total_requests += 1
            
            # Find most similar cached query
            best_match = None
            best_similarity = 0.0
            
            for entry in self.cache:
                similarity = self._compute_similarity(query, entry["query"])
                if similarity > best_similarity:
                    best_similarity = similarity
                    best_match = entry
            
            if best_match and best_similarity >= self.similarity_threshold:
                self.stats.hits += 1
                # Update access time
                best_match["last_accessed"] = datetime.now()
                return best_match["value"]
            
            self.stats.misses += 1
            return None
        
        def set(self, query: str, value: Any):
            """
            Cache value for query.
            
            Args:
                query: Query string
                value: Value to cache
            """
            # Evict oldest if at capacity
            if len(self.cache) >= self.max_size:
                self.cache.pop(0)
                self.stats.evictions += 1
            
            self.cache.append({
                "query": query,
                "value": value,
                "created_at": datetime.now(),
                "last_accessed": datetime.now()
            })
        
        def clear(self):
            """Clear cache"""
            self.cache.clear()
            self.stats = CacheStats()
        
        def get_stats(self) -> Dict[str, Any]:
            """Get cache statistics"""
            return {
                "hits": self.stats.hits,
                "misses": self.stats.misses,
                "evictions": self.stats.evictions,
                "total_requests": self.stats.total_requests,
                "hit_rate": f"{self.stats.hit_rate:.2f}%",
                "current_size": len(self.cache),
                "max_size": self.max_size,
                "similarity_threshold": self.similarity_threshold
            }
    
    
    class CachedLLMAgent:
        """
        LLM agent with caching support.
        
        This agent automatically caches responses and retrieves them
        for repeated or similar queries.
        """
        
        def __init__(
            self,
            cache: SimpleCache,
            model: str = "gpt-3.5-turbo"
        ):
            """
            Initialize cached agent.
            
            Args:
                cache: Cache instance to use
                model: LLM model name
            """
            self.cache = cache
            self.llm = ChatOpenAI(model=model, temperature=0.7)
        
        def generate(self, prompt: str) -> Dict[str, Any]:
            """
            Generate response with caching.
            
            Args:
                prompt: Input prompt
                
            Returns:
                Dictionary with response and cache info
            """
            # Check cache first
            cached_response = self.cache.get(prompt)
            
            if cached_response is not None:
                return {
                    "response": cached_response,
                    "from_cache": True,
                    "cache_stats": self.cache.get_stats()
                }
            
            # Generate new response
            start_time = time.time()
            response = self.llm.invoke(prompt)
            generation_time = time.time() - start_time
            
            # Cache the response
            self.cache.set(prompt, response.content)
            
            return {
                "response": response.content,
                "from_cache": False,
                "generation_time": generation_time,
                "cache_stats": self.cache.get_stats()
            }
    
    
    def demonstrate_caching_patterns():
        """Demonstrate caching patterns"""
        print("=" * 80)
        print("CACHING PATTERNS DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic LRU Cache
        print("\n" + "=" * 80)
        print("EXAMPLE 1: Basic LRU Cache")
        print("=" * 80)
        
        cache = SimpleCache(max_size=5, policy=CachePolicy.LRU)
        agent = CachedLLMAgent(cache)
        
        print("\nMaking requests (first time - cache misses):\n")
        
        queries = [
            "What is Python?",
            "What is JavaScript?",
            "What is Python?",  # Repeat
            "What is Java?",
            "What is Python?",  # Repeat again
        ]
        
        for i, query in enumerate(queries, 1):
            print(f"Request {i}: {query}")
            result = agent.generate(query)
            
            cache_status = "HIT ✓" if result["from_cache"] else "MISS ✗"
            print(f"  Status: {cache_status}")
            
            if not result["from_cache"]:
                print(f"  Generation time: {result['generation_time']:.2f}s")
            print(f"  Response: {result['response'][:80]}...")
            print()
        
        stats = cache.get_stats()
        print("Cache Statistics:")
        print(f"  Hit Rate: {stats['hit_rate']}")
        print(f"  Total Requests: {stats['total_requests']}")
        print(f"  Hits: {stats['hits']}")
        print(f"  Misses: {stats['misses']}")
        
        # Example 2: TTL-Based Caching
        print("\n" + "=" * 80)
        print("EXAMPLE 2: TTL-Based Caching (Time Expiration)")
        print("=" * 80)
        
        ttl_cache = SimpleCache(max_size=100, default_ttl=5)  # 5 second TTL
        
        print("\nCaching value with 5-second TTL...")
        ttl_cache.set("test_key", "test_value", ttl=5)
        
        print("Immediate retrieval:")
        value = ttl_cache.get("test_key")
        print(f"  Result: {'HIT' if value else 'MISS'} - {value}")
        
        print("\nWaiting 6 seconds for expiration...")
        time.sleep(6)
        
        print("Retrieval after TTL:")
        value = ttl_cache.get("test_key")
        print(f"  Result: {'HIT' if value else 'MISS'} - {value}")
        
        # Example 3: Cache Eviction Policies
        print("\n" + "=" * 80)
        print("EXAMPLE 3: Cache Eviction Policies Comparison")
        print("=" * 80)
        
        policies = [CachePolicy.LRU, CachePolicy.LFU, CachePolicy.FIFO]
        
        for policy in policies:
            print(f"\n{policy.value.upper()} Policy:")
            test_cache = SimpleCache(max_size=3, policy=policy)
            
            # Add 5 items to cache of size 3
            for i in range(5):
                key = f"item_{i}"
                test_cache.set(key, f"value_{i}")
                print(f"  Added {key}, cache size: {test_cache.size()}")
            
            stats = test_cache.get_stats()
            print(f"  Evictions: {stats['evictions']}")
        
        # Example 4: Semantic Cache
        print("\n" + "=" * 80)
        print("EXAMPLE 4: Semantic Cache (Similar Query Matching)")
        print("=" * 80)
        
        semantic_cache = SemanticCache(similarity_threshold=0.7, max_size=100)
        
        # Cache a response
        original_query = "What is machine learning?"
        original_response = "Machine learning is a subset of AI that enables systems to learn from data."
        semantic_cache.set(original_query, original_response)
        
        print(f"\nCached query: '{original_query}'")
        print(f"Response: {original_response}\n")
        
        # Try similar queries
        similar_queries = [
            "What is machine learning?",  # Exact match
            "What's machine learning?",   # Slight variation
            "Tell me about machine learning",  # Similar
            "Explain artificial intelligence",  # Different topic
        ]
        
        for query in similar_queries:
            result = semantic_cache.get(query)
            status = "HIT ✓" if result else "MISS ✗"
            print(f"Query: '{query}'")
            print(f"  Status: {status}")
            if result:
                print(f"  Retrieved: {result}")
            print()
        
        # Example 5: Multi-Layer Cache
        print("\n" + "=" * 80)
        print("EXAMPLE 5: Multi-Layer Cache Strategy")
        print("=" * 80)
        
        # L1: Small, fast, in-memory cache
        l1_cache = SimpleCache(max_size=10, policy=CachePolicy.LRU)
        
        # L2: Larger, still in-memory
        l2_cache = SimpleCache(max_size=100, policy=CachePolicy.LRU)
        
        print("\nMulti-layer cache simulation:")
        print("  L1: Small (10 items), fast")
        print("  L2: Large (100 items), moderate\n")
        
        def multi_layer_get(key: str) -> Optional[str]:
            """Get from multi-layer cache"""
            # Try L1 first
            value = l1_cache.get(key)
            if value:
                print(f"  L1 HIT for '{key}'")
                return value
            
            # Try L2
            value = l2_cache.get(key)
            if value:
                print(f"  L2 HIT for '{key}'")
                # Promote to L1
                l1_cache.set(key, value)
                return value
            
            print(f"  MISS for '{key}'")
            return None
        
        # Simulate access pattern
        l1_cache.set("frequently_accessed", "hot_data")
        l2_cache.set("occasionally_accessed", "warm_data")
        
        print("Access patterns:")
        multi_layer_get("frequently_accessed")
        multi_layer_get("occasionally_accessed")
        multi_layer_get("never_cached")
        
        # Example 6: Cache Performance Metrics
        print("\n" + "=" * 80)
        print("EXAMPLE 6: Cache Performance Analysis")
        print("=" * 80)
        
        perf_cache = SimpleCache(max_size=50, policy=CachePolicy.LRU)
        perf_agent = CachedLLMAgent(perf_cache)
        
        print("\nSimulating workload with repeated queries...\n")
        
        workload = [
            "What is AI?",
            "What is ML?",
            "What is AI?",  # Repeat
            "What is DL?",
            "What is AI?",  # Repeat
            "What is ML?",  # Repeat
            "What is NLP?",
            "What is AI?",  # Repeat
        ]
        
        total_time_with_cache = 0
        total_time_without_cache = 0
        
        for query in workload:
            result = perf_agent.generate(query)
            
            if result["from_cache"]:
                total_time_with_cache += 0.001  # Assume 1ms for cache hit
            else:
                gen_time = result.get("generation_time", 1.0)
                total_time_with_cache += gen_time
                total_time_without_cache += gen_time
        
        # Estimate time without cache (all queries would need generation)
        total_time_without_cache = len(workload) * 1.0  # Assume 1s per generation
        
        print(f"Performance Analysis:")
        print(f"  Total Queries: {len(workload)}")
        print(f"  Time WITH cache: {total_time_with_cache:.2f}s")
        print(f"  Time WITHOUT cache (estimated): {total_time_without_cache:.2f}s")
        print(f"  Speedup: {total_time_without_cache/total_time_with_cache:.1f}x")
        print(f"  Time Saved: {total_time_without_cache - total_time_with_cache:.2f}s")
        
        final_stats = perf_cache.get_stats()
        print(f"\nFinal Cache Statistics:")
        print(f"  Hit Rate: {final_stats['hit_rate']}")
        print(f"  Hits: {final_stats['hits']}")
        print(f"  Misses: {final_stats['misses']}")
        
        # Summary
        print("\n" + "=" * 80)
        print("CACHING PATTERNS SUMMARY")
        print("=" * 80)
        print("""
    Caching Pattern Benefits:
    1. Performance: Dramatically faster response times (10-1000x)
    2. Cost Reduction: Fewer API calls = lower costs
    3. Scalability: Handle more requests with same resources
    4. Reliability: Reduce dependency on external services
    5. User Experience: Instant responses for cached queries
    
    Cache Types Demonstrated:
    1. Simple Cache: Basic in-memory caching with eviction policies
    2. TTL Cache: Time-based expiration for freshness
    3. Semantic Cache: Similarity-based matching for related queries
    4. Multi-Layer Cache: Hierarchical caching strategy
    5. Performance-Optimized: Focus on hit rates and speedup
    
    Eviction Policies:
    1. LRU (Least Recently Used): Remove oldest accessed items
    2. LFU (Least Frequently Used): Remove least popular items
    3. FIFO (First In First Out): Remove oldest items
    4. TTL (Time To Live): Expire after time period
    
    Best Practices:
    1. Cache Strategy Selection:
       - Exact match: Simple, fast, for identical queries
       - Semantic: Better coverage, for similar queries
       - TTL: For time-sensitive data
       - Multi-layer: For diverse access patterns
    
    2. TTL Configuration:
       - Short (minutes): Dynamic, frequently changing data
       - Medium (hours): Semi-static content
       - Long (days): Stable, rarely changing data
       - No TTL: Static reference data
    
    3. Size Management:
       - Monitor memory usage
       - Set appropriate limits
       - Implement eviction policies
       - Regular cleanup
    
    4. Performance Monitoring:
       - Target >70% hit rate for good performance
       - Track cache size trends
       - Monitor eviction frequency
       - Measure latency improvements
    
    When to Use Caching:
    ✓ Repeated identical queries
    ✓ Similar semantic queries
    ✓ Expensive computations
    ✓ Rate-limited APIs
    ✓ Stable, unchanging data
    ✗ Real-time data requirements
    ✗ Highly personalized responses
    ✗ Security-sensitive information
    ✗ Rapidly changing data
    
    Production Considerations:
    - Distributed caching (Redis) for multi-instance deployments
    - Cache warming strategies for cold starts
    - Monitoring and alerting on hit rates
    - Regular cache analysis and optimization
    - Security: encrypt sensitive cached data
    - Compliance: respect data retention policies
    - Backup and recovery procedures
    - Cache versioning for updates
    
    Cost-Benefit Analysis:
    - Typical API call: $0.001-0.01 per request
    - Cache hit cost: ~$0.000001 per request
    - Potential savings: 90-99% cost reduction
    - Performance gain: 10-1000x faster responses
    - ROI: Immediate for high-volume applications
    """)
        
        print("\n" + "=" * 80)
        print("Pattern 089 (Caching Patterns) demonstration complete!")
        print("=" * 80)
    
    
    if __name__ == "__main__":
        demonstrate_caching_patterns()
    
        

    Agentic Design Pattern: Caching Patterns

    Pattern 089: Caching Patterns – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Caching Patterns pattern implementation.


    Pattern Description

    Pattern 089: Caching Patterns

    Description: The Caching Patterns involve storing and reusing previously computed results to improve performance, reduce latency, and minimize costs. In the context of LLM applications, caching is critical for optimizing response times and reducing API calls for repeated or similar queries. Effective caching strategies can dramatically improve user experience while significantly cutting operational costs.

    Caching in LLM applications targets:
    - Complete responses (exact match caching)
    - Embeddings (vector representations)
    - Prompt templates (reusable structures)
    - Intermediate results (multi-step workflows)
    - API responses (rate limit optimization)
    

    Components: 1. Cache Types – In-memory cache (fast, volatile) – Disk cache (persistent, slower) – Distributed cache (Redis, Memcached) – Database cache (structured, queryable) – CDN cache (geographic distribution)

    2. Cache Keys
       - Exact match (full prompt hash)
       - Semantic match (embedding similarity)
       - Parametric keys (template + parameters)
       - Hierarchical keys (nested cache structures)
       - Time-based keys (temporal validity)
    
    3. Cache Policies
       - TTL (Time To Live) - expire after time
       - LRU (Least Recently Used) - evict old entries
       - LFU (Least Frequently Used) - evict unpopular
       - Size-based - limit total cache size
       - Custom policies - application-specific
    
    4. Invalidation Strategies
       - Time-based expiration
       - Event-triggered invalidation
       - Manual invalidation
       - Cascade invalidation (dependencies)
       - Version-based invalidation
    

    Use Cases: 1. Repeated Queries – FAQ responses – Common questions – Popular content – Standard procedures – Frequently accessed data

    2. Expensive Operations
       - Complex reasoning chains
       - Multi-step workflows
       - Large context processing
       - Batch operations
       - Resource-intensive computations
    
    3. Rate Limit Optimization
       - Reduce API calls
       - Stay within quotas
       - Handle traffic spikes
       - Cost optimization
       - Performance consistency
    
    4. Embeddings and Vectors
       - Document embeddings
       - Query embeddings
       - Semantic search optimization
       - Similarity computations
       - Vector database queries
    
    5. Multi-User Applications
       - Shared responses
       - Common workflows
       - Popular queries
       - Template responses
       - Collaborative features
    

    LangChain Implementation: LangChain supports caching through: – InMemoryCache for simple caching – SQLiteCache for persistent caching – Custom cache implementations – Semantic similarity caching – Embedding caching

    Key Features: 1. Multiple Cache Layers – L1: In-memory (fastest) – L2: Local disk (persistent) – L3: Distributed cache (shared) – L4: Database (long-term)

    2. Intelligent Matching
       - Exact match for identical queries
       - Semantic match for similar queries
       - Fuzzy match with thresholds
       - Parametric templates
    
    3. Performance Optimization
       - Fast lookup (< 1ms for in-memory)
       - Efficient serialization
       - Compression for large entries
       - Lazy loading
    
    4. Observability
       - Hit rate monitoring
       - Miss analysis
       - Performance metrics
       - Cost savings tracking
    

    Best Practices: 1. Cache Appropriately – Cache stable, reusable results – Don’t cache rapidly changing data – Consider data sensitivity – Balance freshness vs performance

    2. Set Proper TTLs
       - Shorter for dynamic content
       - Longer for stable content
       - Consider update frequency
       - Account for user expectations
    
    3. Monitor Performance
       - Track hit rates (target > 70%)
       - Measure latency improvements
       - Calculate cost savings
       - Identify cache misses patterns
    
    4. Handle Cache Misses
       - Graceful fallback to computation
       - Transparent to users
       - Background cache warming
       - Predictive pre-caching
    

    Trade-offs: Advantages: – Dramatically improved response times – Significant cost reduction – Better user experience – Reduced API rate limiting – Lower resource consumption – Improved scalability

    Disadvantages:
    - Stale data risk
    - Memory/storage overhead
    - Cache consistency challenges
    - Complexity in invalidation
    - Initial cache warming time
    - Debugging complexity
    

    Production Considerations: 1. Cache Size Management – Monitor memory usage – Implement eviction policies – Set size limits – Regular cleanup – Compression strategies

    2. Cache Consistency
       - Version tracking
       - Update propagation
       - Invalidation timing
       - Distributed consistency
       - Race condition handling
    
    3. Security
       - Access control for cached data
       - Encryption for sensitive content
       - Cache poisoning prevention
       - User isolation in multi-tenant
       - Audit trails
    
    4. Monitoring
       - Hit/miss rates
       - Cache size trends
       - Eviction frequency
       - Cost savings
       - Performance impact
    
    5. Disaster Recovery
       - Cache rebuild procedures
       - Backup strategies
       - Failover mechanisms
       - Data loss handling
       - Recovery time objectives
    

    File Statistics

    • Total Lines: 897
    • Classes: 6
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 089: Caching Patterns
    
    Description:
        The Caching Patterns involve storing and reusing previously computed results to
        improve performance, reduce latency, and minimize costs. In the context of LLM
        applications, caching is critical for optimizing response times and reducing API
        calls for repeated or similar queries. Effective caching strategies can
        dramatically improve user experience while significantly cutting operational costs.
    
        Caching in LLM applications targets:
        - Complete responses (exact match caching)
        - Embeddings (vector representations)
        - Prompt templates (reusable structures)
        - Intermediate results (multi-step workflows)
        - API responses (rate limit optimization)
    
    Components:
        1. Cache Types
           - In-memory cache (fast, volatile)
           - Disk cache (persistent, slower)
           - Distributed cache (Redis, Memcached)
           - Database cache (structured, queryable)
           - CDN cache (geographic distribution)
    
        2. Cache Keys
           - Exact match (full prompt hash)
           - Semantic match (embedding similarity)
           - Parametric keys (template + parameters)
           - Hierarchical keys (nested cache structures)
           - Time-based keys (temporal validity)
    
        3. Cache Policies
           - TTL (Time To Live) - expire after time
           - LRU (Least Recently Used) - evict old entries
           - LFU (Least Frequently Used) - evict unpopular
           - Size-based - limit total cache size
           - Custom policies - application-specific
    
        4. Invalidation Strategies
           - Time-based expiration
           - Event-triggered invalidation
           - Manual invalidation
           - Cascade invalidation (dependencies)
           - Version-based invalidation
    
    Use Cases:
        1. Repeated Queries
           - FAQ responses
           - Common questions
           - Popular content
           - Standard procedures
           - Frequently accessed data
    
        2. Expensive Operations
           - Complex reasoning chains
           - Multi-step workflows
           - Large context processing
           - Batch operations
           - Resource-intensive computations
    
        3. Rate Limit Optimization
           - Reduce API calls
           - Stay within quotas
           - Handle traffic spikes
           - Cost optimization
           - Performance consistency
    
        4. Embeddings and Vectors
           - Document embeddings
           - Query embeddings
           - Semantic search optimization
           - Similarity computations
           - Vector database queries
    
        5. Multi-User Applications
           - Shared responses
           - Common workflows
           - Popular queries
           - Template responses
           - Collaborative features
    
    LangChain Implementation:
        LangChain supports caching through:
        - InMemoryCache for simple caching
        - SQLiteCache for persistent caching
        - Custom cache implementations
        - Semantic similarity caching
        - Embedding caching
    
    Key Features:
        1. Multiple Cache Layers
           - L1: In-memory (fastest)
           - L2: Local disk (persistent)
           - L3: Distributed cache (shared)
           - L4: Database (long-term)
    
        2. Intelligent Matching
           - Exact match for identical queries
           - Semantic match for similar queries
           - Fuzzy match with thresholds
           - Parametric templates
    
        3. Performance Optimization
           - Fast lookup (< 1ms for in-memory)
           - Efficient serialization
           - Compression for large entries
           - Lazy loading
    
        4. Observability
           - Hit rate monitoring
           - Miss analysis
           - Performance metrics
           - Cost savings tracking
    
    Best Practices:
        1. Cache Appropriately
           - Cache stable, reusable results
           - Don't cache rapidly changing data
           - Consider data sensitivity
           - Balance freshness vs performance
    
        2. Set Proper TTLs
           - Shorter for dynamic content
           - Longer for stable content
           - Consider update frequency
           - Account for user expectations
    
        3. Monitor Performance
           - Track hit rates (target > 70%)
           - Measure latency improvements
           - Calculate cost savings
           - Identify cache misses patterns
    
        4. Handle Cache Misses
           - Graceful fallback to computation
           - Transparent to users
           - Background cache warming
           - Predictive pre-caching
    
    Trade-offs:
        Advantages:
        - Dramatically improved response times
        - Significant cost reduction
        - Better user experience
        - Reduced API rate limiting
        - Lower resource consumption
        - Improved scalability
    
        Disadvantages:
        - Stale data risk
        - Memory/storage overhead
        - Cache consistency challenges
        - Complexity in invalidation
        - Initial cache warming time
        - Debugging complexity
    
    Production Considerations:
        1. Cache Size Management
           - Monitor memory usage
           - Implement eviction policies
           - Set size limits
           - Regular cleanup
           - Compression strategies
    
        2. Cache Consistency
           - Version tracking
           - Update propagation
           - Invalidation timing
           - Distributed consistency
           - Race condition handling
    
        3. Security
           - Access control for cached data
           - Encryption for sensitive content
           - Cache poisoning prevention
           - User isolation in multi-tenant
           - Audit trails
    
        4. Monitoring
           - Hit/miss rates
           - Cache size trends
           - Eviction frequency
           - Cost savings
           - Performance impact
    
        5. Disaster Recovery
           - Cache rebuild procedures
           - Backup strategies
           - Failover mechanisms
           - Data loss handling
           - Recovery time objectives
    """
    

    Purpose: This module implements the Caching Patterns pattern, providing a structured approach to agent design and implementation.


    Import Statements

    Standard Library Imports

    import collections.OrderedDict
    import dataclasses.dataclass
    import dataclasses.field
    import datetime.datetime
    import datetime.timedelta
    import enum.Enum
    import json
    import langchain_core.caches.InMemoryCache
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import time
    import typing.Any
    import typing.Callable
    import typing.Dict
    import typing.List
    import typing.Optional
    

    Purpose: Core Python functionality: – collections.OrderedDict: Regular expression operations – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – datetime.timedelta: Date and time handling – enum.Enum: Enumeration support – json: JSON serialization/deserialization – langchain_core.caches.InMemoryCache: Regular expression operations – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – time: Time-related functions – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Callable: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain.globals.set_llm_cache
    from langchain_openai.ChatOpenAI
    from langchain_openai.OpenAIEmbeddings
    

    Purpose: LangChain framework components: – LLM Interface: Wrapper for OpenAI models – Chains: Composable building blocks for complex workflows

    Other Imports

    from dotenv.load_dotenv
    from hashlib
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: CachePolicy

    Lines 215-220

    Inherits from: Enum

    Purpose:

    Cache eviction policies
    

    Class: CacheEntry

    Lines 224-243

    Purpose:

    Represents a cached entry
    

    Methods:

    is_expired(self)

    Lines 233-238

    Purpose:

    Check if entry is expired
    

    Parameters:

    Returns: bool

    touch(self)

    Lines 240-243

    Purpose:

    Update access information
    

    Parameters:


    Class: CacheStats

    Lines 247-264

    Purpose:

    Cache performance statistics
    

    Methods:

    hit_rate(self)

    Lines 255-259

    Decorators: property

    Purpose:

    Calculate cache hit rate
    

    Parameters:

    Returns: float

    miss_rate(self)

    Lines 262-264

    Decorators: property

    Purpose:

    Calculate cache miss rate
    

    Parameters:

    Returns: float


    Class: SimpleCache

    Lines 267-416

    Purpose:

    Simple in-memory cache with multiple eviction policies.
    
    This cache supports:
    1. Multiple eviction policies (LRU, LFU, FIFO, TTL)
    2. Size limits
    3. TTL per entry
    4. Performance statistics
    

    Methods:

    __init__(self, max_size, policy, default_ttl)

    Lines 278-296

    Purpose:

    Initialize cache.
    
    Args:
        max_size: Maximum number of entries
        policy: Eviction policy
        default_ttl: Default TTL in seconds
    

    Parameters: – max_size: Max size – policy: Policy – default_ttl: Default ttl

    _make_key(self, key)

    Lines 298-302

    Purpose:

    Create cache key from input
    

    Parameters: – key: Key

    Returns: str

    _evict(self)

    Lines 304-320

    Purpose:

    Evict entries based on policy
    

    Parameters:

    _remove_expired(self)

    Lines 322-329

    Purpose:

    Remove expired entries
    

    Parameters:

    get(self, key)

    Lines 331-365

    Purpose:

    Get value from cache.
    
    Args:
        key: Cache key
    
    Returns:
        Cached value or None
    

    Parameters: – key: Key

    Returns: <ast.Subscript object at 0x000001A06D62AED0>

    set(self, key, value, ttl)

    Lines 367-394

    Purpose:

    Set value in cache.
    
    Args:
        key: Cache key
        value: Value to cache
        ttl: Optional TTL in seconds
    

    Parameters: – key: Key – value: Value – ttl: Ttl

    clear(self)

    Lines 396-399

    Purpose:

    Clear all cache entries
    

    Parameters:

    size(self)

    Lines 401-403

    Purpose:

    Get current cache size
    

    Parameters:

    Returns: int

    get_stats(self)

    Lines 405-416

    Purpose:

    Get cache statistics
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D74A3D0>


    Class: SemanticCache

    Lines 419-540

    Purpose:

    Semantic cache using embeddings for similarity matching.
    
    This cache finds similar queries using vector similarity,
    allowing cache hits for semantically similar but not identical queries.
    

    Methods:

    __init__(self, similarity_threshold, max_size)

    Lines 427-443

    Purpose:

    Initialize semantic cache.
    
    Args:
        similarity_threshold: Minimum similarity for cache hit
        max_size: Maximum cache entries
    

    Parameters: – similarity_threshold: Similarity threshold – max_size: Max size

    _compute_similarity(self, query1, query2)

    Lines 445-471

    Purpose:

    Compute semantic similarity between queries.
    
    Args:
        query1: First query
        query2: Second query
    
    Returns:
        Similarity score (0-1)
    

    Parameters: – query1: Input query or question – query2: Input query or question

    Returns: float

    get(self, query)

    Lines 473-502

    Purpose:

    Get value for semantically similar query.
    
    Args:
        query: Query string
    
    Returns:
        Cached value or None
    

    Parameters: – query: Input query or question

    Returns: <ast.Subscript object at 0x000001A06D6A1F50>

    set(self, query, value)

    Lines 504-522

    Purpose:

    Cache value for query.
    
    Args:
        query: Query string
        value: Value to cache
    

    Parameters: – query: Input query or question – value: Value

    clear(self)

    Lines 524-527

    Purpose:

    Clear cache
    

    Parameters:

    get_stats(self)

    Lines 529-540

    Purpose:

    Get cache statistics
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D7FC110>


    Class: CachedLLMAgent

    Lines 543-599

    Purpose:

    LLM agent with caching support.
    
    This agent automatically caches responses and retrieves them
    for repeated or similar queries.
    

    Methods:

    __init__(self, cache, model)

    Lines 551-564

    Purpose:

    Initialize cached agent.
    
    Args:
        cache: Cache instance to use
        model: LLM model name
    

    Parameters: – cache: Cache – model: Language model instance

    generate(self, prompt)

    Lines 566-599

    Purpose:

    Generate response with caching.
    
    Args:
        prompt: Input prompt
    
    Returns:
        Dictionary with response and cache info
    

    Parameters: – prompt: Prompt template or text

    Returns: <ast.Subscript object at 0x000001A06D7C1A90>


    Functions

    demonstrate_caching_patterns()

    Lines 602-892

    Purpose:

    Demonstrate caching patterns
    

    Key Concepts Summary

    Pattern Overview

    The Caching Patterns pattern provides a structured approach to building AI agents with specific capabilities and behaviors. This implementation leverages LangChain’s composable components to create a robust and maintainable solution.

    Core Components

    1. Agent Class: Main implementation of the pattern logic
    2. Prompt Engineering: Carefully crafted prompts to guide LLM behavior
    3. Tool Integration: Optional tools for extended functionality
    4. Memory Management: State handling across interactions
    5. Output Parsing: Structured response processing

    When to Use

    ✅ Good for: – Scenarios requiring this specific pattern’s strengths – Applications needing structured agent behavior – Systems where explainability is important – Production environments requiring reliability

    ❌ Not ideal for: – Simple single-prompt tasks – Scenarios requiring different pattern characteristics – Resource-constrained environments (if computationally intensive)


    Best Practices Demonstrated

    1. Environment Configuration

    • ✅ Use .env files for API keys
    • ✅ Never hardcode credentials
    • ✅ Validate environment variables on startup

    2. Error Handling

    • ✅ Graceful degradation on failures
    • ✅ Informative error messages
    • ✅ Logging for debugging

    3. Prompt Engineering

    • ✅ Clear, specific instructions
    • ✅ Structured output formats
    • ✅ Few-shot examples when beneficial

    4. Code Organization

    • ✅ Separation of concerns (class-based design)
    • ✅ Type hints for clarity
    • ✅ Docstrings for documentation

    5. Performance Optimization

    • ✅ Appropriate temperature settings
    • ✅ Token budget management
    • ✅ Caching where applicable

    LangChain Components Used

    • ChatOpenAI: OpenAI model interface
    • Chains: Sequential component composition

    Why LangChain? – Composable abstractions for LLM applications – Rich ecosystem of integrations – Production-ready patterns – Active community and development


    Execution Flow

    1. Initialize agent with configuration
       ↓
    2. Load environment and dependencies
       ↓
    3. Prepare prompt with input data
       ↓
    4. Invoke LLM through LangChain
       ↓
    5. Parse and validate response
       ↓
    6. Return formatted result
    

    Key Decision Points: – Model selection (temperature, model type) – Prompt structure (few-shot vs zero-shot) – Error handling strategy – Output format validation


    Performance Considerations

    Computational Cost

    • LLM Calls: Primary cost factor (based on tokens)
    • Latency: Depends on model and input size
    • Memory: Minimal for most patterns

    Optimization Strategies

    1. Prompt Optimization: Minimize tokens while maintaining clarity
    2. Caching: Cache repeated queries/responses
    3. Model Selection: Use smaller models when appropriate
    4. Batch Processing: Group similar requests
    5. Streaming: For long responses, stream tokens

    Cost Estimates (Approximate)

    • GPT-3.5-turbo: ~$0.002 per request
    • GPT-4: ~$0.03 per request
    • GPT-4-turbo: ~$0.01 per request

    Actual costs vary based on input/output token counts


    Common Pitfalls to Avoid

    1. Prompt Design Issues

    ❌ Vague or ambiguous instructions
    ✅ Clear, specific, testable prompts

    2. Error Handling

    ❌ Ignoring API errors or rate limits
    ✅ Implement retry logic and graceful degradation

    3. Token Management

    ❌ Exceeding model token limits
    ✅ Truncate inputs, chunk processing

    4. Security

    ❌ Exposing API keys in code
    ✅ Use environment variables

    5. Testing

    ❌ Only testing happy paths
    ✅ Test edge cases, failures, malformed inputs

    6. Cost Management

    ❌ Unmonitored API usage
    ✅ Implement budgets, alerts, and monitoring


    Further Improvements

    Production Enhancements

    1. Monitoring: Add logging, metrics, tracing
    2. Validation: Input/output schema validation
    3. Rate Limiting: Implement request throttling
    4. Caching: Redis or in-memory cache for responses
    5. Async Support: Non-blocking operations for scalability

    Feature Extensions

    1. Multi-Model Support: Switch between providers
    2. Fallback Models: Graceful degradation strategy
    3. Custom Tools: Domain-specific function calling
    4. Advanced Memory: Vector stores for long-term memory
    5. Evaluation: Automated testing and benchmarking

    Code Quality

    1. Testing: Unit tests, integration tests
    2. Documentation: API docs, usage examples
    3. Type Safety: Stricter type hints
    4. Configuration: Externalized config files

    Related Patterns

    • Pattern 088
    • Pattern 090

    Pattern Families: – Reasoning Patterns (001-011) – Multi-Agent Patterns (012-019) – Tool Use Patterns (020-025) – Memory Patterns (026-031) – And many more…


    Summary

    The Caching Patterns pattern provides a powerful approach to building AI agents with specific capabilities. This implementation demonstrates:

    ✅ Key Strengths: – Clean, maintainable code structure – LangChain integration for composability – Type hints and documentation – Production-ready patterns

    🎯 Best Use Cases: – Applications requiring this pattern’s characteristics – Systems needing structured agent behavior – Production environments requiring reliability

    📚 Learning Resources: – LangChain Documentation: https://python.langchain.com/ – Pattern Documentation: See related patterns – OpenAI API Reference: https://platform.openai.com/docs/


    File: 089_caching_patterns.py
    Total Lines: 897
    Pattern Category: AI Agent Design Pattern #089


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Token Budget

    
        """
    Pattern 088: Token Budget Management
    
    Description:
        The Token Budget Management pattern monitors and controls token usage across LLM
        interactions to optimize costs, prevent budget overruns, and ensure efficient
        resource utilization. This pattern is critical for production deployments where
        token costs can escalate quickly, especially with high-volume applications or
        complex multi-step workflows.
    
        Token budget management involves:
        - Real-time token usage tracking
        - Cost estimation and forecasting
        - Budget enforcement and alerts
        - Optimization strategies for token efficiency
        - Usage analytics and reporting
    
    Components:
        1. Token Counting
           - Input token counting (prompt tokens)
           - Output token counting (completion tokens)
           - Total token tracking per request
           - Cumulative usage tracking
           - Model-specific token limits
    
        2. Cost Calculation
           - Model pricing tables (per-token costs)
           - Request cost estimation
           - Cumulative cost tracking
           - Budget vs actual comparison
           - Cost forecasting
    
        3. Budget Enforcement
           - Hard limits (stop when exceeded)
           - Soft limits (warnings when approaching)
           - Rate-based budgets (tokens per hour/day)
           - User-specific quotas
           - Priority-based allocation
    
        4. Optimization Strategies
           - Prompt compression
           - Response length limiting
           - Model selection (cost vs quality)
           - Caching frequently used responses
           - Batching requests
    
    Use Cases:
        1. Cost Control
           - Prevent unexpected charges
           - Stay within allocated budgets
           - Cost attribution per user/project
           - Predictable monthly spending
           - Resource optimization
    
        2. Multi-Tenant Applications
           - Per-user token quotas
           - Fair resource allocation
           - Usage-based billing
           - Tier-based limits (free/premium)
           - Abuse prevention
    
        3. Development and Testing
           - Sandbox environments with limits
           - Cost-effective experimentation
           - Testing budget allocation
           - Development vs production budgets
           - CI/CD pipeline limits
    
        4. High-Volume Applications
           - Aggregate usage tracking
           - Peak usage management
           - Resource planning
           - Scaling decisions
           - Cost optimization at scale
    
        5. Enterprise Deployments
           - Department-level budgets
           - Project-based allocation
           - Chargeback mechanisms
           - Compliance and auditing
           - Executive reporting
    
    LangChain Implementation:
        LangChain supports token budget management through:
        - Callback handlers for token tracking
        - get_openai_callback for cost monitoring
        - Custom token counters
        - Usage analytics integration
        - Budget enforcement wrappers
    
    Key Features:
        1. Real-Time Tracking
           - Immediate token counting
           - Running total updates
           - Cost calculation on-the-fly
           - Budget threshold monitoring
           - Alert triggering
    
        2. Multi-Model Support
           - Different pricing for different models
           - Model-specific token limits
           - Automatic model selection
           - Cost-aware routing
           - Fallback to cheaper models
    
        3. Granular Control
           - Request-level limits
           - Session-level budgets
           - User-level quotas
           - Project-level allocations
           - Global organization limits
    
        4. Analytics and Reporting
           - Usage trends over time
           - Cost breakdown by model
           - Per-user consumption
           - Peak usage identification
           - Cost forecasting
    
    Best Practices:
        1. Set Appropriate Budgets
           - Based on historical data
           - Include buffer for peaks
           - Align with business value
           - Regular review and adjustment
           - Clear escalation procedures
    
        2. Implement Progressive Enforcement
           - Soft warnings at 70-80%
           - Stricter controls at 90%
           - Hard stops at 100%
           - Grace period for critical operations
           - Override mechanisms for emergencies
    
        3. Optimize Token Usage
           - Compress verbose prompts
           - Limit response lengths
           - Use cheaper models when sufficient
           - Cache common queries
           - Batch similar requests
    
        4. Monitor and Alert
           - Real-time dashboards
           - Automated alerts
           - Anomaly detection
           - Trend analysis
           - Regular reporting
    
    Trade-offs:
        Advantages:
        - Predictable costs
        - Prevents budget overruns
        - Enables cost optimization
        - Fair resource allocation
        - Usage visibility and control
        - Informed scaling decisions
    
        Disadvantages:
        - Implementation complexity
        - Tracking overhead (minimal)
        - May limit functionality at budget
        - Requires ongoing management
        - False positives in alerting
        - User friction from limits
    
    Production Considerations:
        1. Accuracy
           - Precise token counting
           - Up-to-date pricing information
           - Correct model identification
           - Cache hit accounting
           - Network overhead handling
    
        2. Performance
           - Minimal tracking overhead
           - Efficient storage of usage data
           - Fast budget checks
           - Optimized queries for analytics
           - Scalable architecture
    
        3. User Experience
           - Clear budget status visibility
           - Helpful error messages
           - Grace period handling
           - Budget increase requests
           - Usage optimization tips
    
        4. Maintenance
           - Regular pricing updates
           - Budget policy reviews
           - Alert threshold tuning
           - Anomaly investigation
           - Capacity planning
    
        5. Security
           - Access control for budget data
           - Secure token counting
           - Audit trail for usage
           - Fraud detection
           - Quota manipulation prevention
    """
    
    import os
    import time
    from typing import List, Dict, Any, Optional, Callable
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime, timedelta
    from collections import defaultdict
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    from langchain.callbacks import get_openai_callback
    
    load_dotenv()
    
    
    class BudgetStatus(Enum):
        """Budget status levels"""
        NORMAL = "normal"
        WARNING = "warning"
        CRITICAL = "critical"
        EXCEEDED = "exceeded"
    
    
    class ModelTier(Enum):
        """Model cost tiers"""
        CHEAP = "cheap"
        STANDARD = "standard"
        PREMIUM = "premium"
    
    
    @dataclass
    class ModelPricing:
        """Pricing information for a model"""
        model_name: str
        input_cost_per_1k: float  # Cost per 1000 input tokens
        output_cost_per_1k: float  # Cost per 1000 output tokens
        tier: ModelTier
    
    
    @dataclass
    class TokenUsage:
        """Token usage for a single request"""
        prompt_tokens: int
        completion_tokens: int
        total_tokens: int
        cost: float
        model: str
        timestamp: datetime = field(default_factory=datetime.now)
    
    
    @dataclass
    class BudgetConfig:
        """Configuration for token budget management"""
        total_budget: float  # Total budget in USD
        warning_threshold: float = 0.8  # Warning at 80%
        critical_threshold: float = 0.9  # Critical at 90%
        enforce_hard_limit: bool = True
        default_model: str = "gpt-3.5-turbo"
        allow_model_downgrade: bool = True
    
    
    class TokenBudgetManager:
        """
        Manages token budgets and costs across LLM interactions.
        
        This manager:
        1. Tracks token usage in real-time
        2. Calculates costs based on model pricing
        3. Enforces budget limits
        4. Provides usage analytics
        """
        
        # Model pricing (as of 2025)
        MODEL_PRICING = {
            "gpt-3.5-turbo": ModelPricing("gpt-3.5-turbo", 0.0005, 0.0015, ModelTier.CHEAP),
            "gpt-4": ModelPricing("gpt-4", 0.03, 0.06, ModelTier.PREMIUM),
            "gpt-4-turbo": ModelPricing("gpt-4-turbo", 0.01, 0.03, ModelTier.STANDARD),
        }
        
        def __init__(self, config: BudgetConfig):
            """
            Initialize token budget manager.
            
            Args:
                config: Budget configuration
            """
            self.config = config
            self.total_spent = 0.0
            self.usage_history: List[TokenUsage] = []
            self.requests_count = 0
            self.model_usage: Dict[str, int] = defaultdict(int)
            self.alert_callbacks: List[Callable] = []
        
        def add_alert_callback(self, callback: Callable[[BudgetStatus, Dict], None]):
            """Add callback for budget alerts"""
            self.alert_callbacks.append(callback)
        
        def _trigger_alerts(self, status: BudgetStatus, details: Dict[str, Any]):
            """Trigger all registered alert callbacks"""
            for callback in self.alert_callbacks:
                try:
                    callback(status, details)
                except Exception as e:
                    print(f"Alert callback error: {e}")
        
        def get_status(self) -> BudgetStatus:
            """Get current budget status"""
            usage_ratio = self.total_spent / self.config.total_budget
            
            if usage_ratio >= 1.0:
                return BudgetStatus.EXCEEDED
            elif usage_ratio >= self.config.critical_threshold:
                return BudgetStatus.CRITICAL
            elif usage_ratio >= self.config.warning_threshold:
                return BudgetStatus.WARNING
            else:
                return BudgetStatus.NORMAL
        
        def can_proceed(self, estimated_cost: float = 0.0) -> tuple[bool, str]:
            """
            Check if request can proceed within budget.
            
            Args:
                estimated_cost: Estimated cost of request
                
            Returns:
                Tuple of (can_proceed, reason)
            """
            status = self.get_status()
            
            if status == BudgetStatus.EXCEEDED:
                if self.config.enforce_hard_limit:
                    return False, "Budget exceeded. Request denied."
                else:
                    return True, "Budget exceeded but enforcement disabled."
            
            projected_cost = self.total_spent + estimated_cost
            if projected_cost > self.config.total_budget:
                if self.config.enforce_hard_limit:
                    return False, f"Request would exceed budget. Projected: ${projected_cost:.4f}"
                
            if status == BudgetStatus.CRITICAL:
                return True, "Budget critical. Proceed with caution."
            
            return True, "OK"
        
        def calculate_cost(
            self,
            prompt_tokens: int,
            completion_tokens: int,
            model: str
        ) -> float:
            """
            Calculate cost for token usage.
            
            Args:
                prompt_tokens: Number of prompt tokens
                completion_tokens: Number of completion tokens
                model: Model name
                
            Returns:
                Cost in USD
            """
            pricing = self.MODEL_PRICING.get(model)
            if not pricing:
                # Default to gpt-3.5-turbo pricing
                pricing = self.MODEL_PRICING["gpt-3.5-turbo"]
            
            input_cost = (prompt_tokens / 1000) * pricing.input_cost_per_1k
            output_cost = (completion_tokens / 1000) * pricing.output_cost_per_1k
            
            return input_cost + output_cost
        
        def record_usage(
            self,
            prompt_tokens: int,
            completion_tokens: int,
            model: str
        ) -> TokenUsage:
            """
            Record token usage and update budget.
            
            Args:
                prompt_tokens: Number of prompt tokens
                completion_tokens: Number of completion tokens
                model: Model name
                
            Returns:
                TokenUsage object
            """
            total_tokens = prompt_tokens + completion_tokens
            cost = self.calculate_cost(prompt_tokens, completion_tokens, model)
            
            usage = TokenUsage(
                prompt_tokens=prompt_tokens,
                completion_tokens=completion_tokens,
                total_tokens=total_tokens,
                cost=cost,
                model=model
            )
            
            # Update tracking
            self.total_spent += cost
            self.usage_history.append(usage)
            self.requests_count += 1
            self.model_usage[model] += total_tokens
            
            # Check status and trigger alerts if needed
            status = self.get_status()
            if status in [BudgetStatus.WARNING, BudgetStatus.CRITICAL, BudgetStatus.EXCEEDED]:
                self._trigger_alerts(status, {
                    "total_spent": self.total_spent,
                    "budget": self.config.total_budget,
                    "usage_ratio": self.total_spent / self.config.total_budget,
                    "latest_usage": usage
                })
            
            return usage
        
        def get_remaining_budget(self) -> float:
            """Get remaining budget"""
            return max(0, self.config.total_budget - self.total_spent)
        
        def get_usage_summary(self) -> Dict[str, Any]:
            """Get comprehensive usage summary"""
            status = self.get_status()
            
            return {
                "total_budget": self.config.total_budget,
                "total_spent": self.total_spent,
                "remaining": self.get_remaining_budget(),
                "usage_percentage": (self.total_spent / self.config.total_budget) * 100,
                "status": status.value,
                "total_requests": self.requests_count,
                "total_tokens": sum(u.total_tokens for u in self.usage_history),
                "model_breakdown": dict(self.model_usage),
                "average_cost_per_request": self.total_spent / max(1, self.requests_count)
            }
        
        def suggest_cheaper_model(self, current_model: str) -> Optional[str]:
            """Suggest a cheaper model alternative"""
            if not self.config.allow_model_downgrade:
                return None
            
            current_pricing = self.MODEL_PRICING.get(current_model)
            if not current_pricing:
                return None
            
            # Find cheaper alternatives
            cheaper_models = [
                model for model, pricing in self.MODEL_PRICING.items()
                if pricing.tier.value  Dict[str, Any]:
            """
            Generate response with budget tracking.
            
            Args:
                prompt: Input prompt
                max_tokens: Maximum tokens for response
                
            Returns:
                Dictionary with response and usage info
            """
            # Check budget before proceeding
            can_proceed, reason = self.budget_manager.can_proceed()
            if not can_proceed:
                return {
                    "success": False,
                    "error": reason,
                    "response": None,
                    "usage": None
                }
            
            # Generate with token tracking
            with get_openai_callback() as cb:
                try:
                    response = self.llm.invoke(prompt)
                    
                    # Record usage
                    usage = self.budget_manager.record_usage(
                        prompt_tokens=cb.prompt_tokens,
                        completion_tokens=cb.completion_tokens,
                        model=self.model
                    )
                    
                    return {
                        "success": True,
                        "response": response.content,
                        "usage": {
                            "prompt_tokens": usage.prompt_tokens,
                            "completion_tokens": usage.completion_tokens,
                            "total_tokens": usage.total_tokens,
                            "cost": usage.cost
                        },
                        "budget_status": self.budget_manager.get_status().value
                    }
                    
                except Exception as e:
                    return {
                        "success": False,
                        "error": str(e),
                        "response": None,
                        "usage": None
                    }
        
        def generate_with_limit(
            self,
            prompt: str,
            max_cost: float
        ) -> Dict[str, Any]:
            """
            Generate response with per-request cost limit.
            
            Args:
                prompt: Input prompt
                max_cost: Maximum cost for this request
                
            Returns:
                Dictionary with response and usage info
            """
            # Estimate and check
            can_proceed, reason = self.budget_manager.can_proceed(max_cost)
            if not can_proceed:
                return {
                    "success": False,
                    "error": reason,
                    "response": None
                }
            
            return self.generate(prompt)
        
        def batch_generate(
            self,
            prompts: List[str],
            max_total_cost: Optional[float] = None
        ) -> List[Dict[str, Any]]:
            """
            Generate responses for multiple prompts with budget tracking.
            
            Args:
                prompts: List of prompts
                max_total_cost: Optional cost limit for batch
                
            Returns:
                List of results
            """
            results = []
            batch_cost = 0.0
            
            for prompt in prompts:
                # Check batch cost limit
                if max_total_cost and batch_cost >= max_total_cost:
                    results.append({
                        "success": False,
                        "error": "Batch cost limit reached",
                        "response": None
                    })
                    continue
                
                result = self.generate(prompt)
                results.append(result)
                
                if result["success"] and result["usage"]:
                    batch_cost += result["usage"]["cost"]
            
            return results
    
    
    def demonstrate_token_budget_management():
        """Demonstrate token budget management patterns"""
        print("=" * 80)
        print("TOKEN BUDGET MANAGEMENT PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic Budget Tracking
        print("\n" + "=" * 80)
        print("EXAMPLE 1: Basic Budget Tracking")
        print("=" * 80)
        
        config = BudgetConfig(
            total_budget=1.00,  # $1.00 budget
            warning_threshold=0.7,
            critical_threshold=0.9,
            enforce_hard_limit=False  # Allow going over for demo
        )
        
        budget_manager = TokenBudgetManager(config)
        agent = BudgetAwareLLMAgent(budget_manager)
        
        print(f"\nInitial Budget: ${config.total_budget}")
        print("\nGenerating responses...\n")
        
        prompts = [
            "What is machine learning?",
            "Explain neural networks briefly.",
            "What is deep learning?"
        ]
        
        for i, prompt in enumerate(prompts, 1):
            print(f"Request {i}: {prompt}")
            result = agent.generate(prompt)
            
            if result["success"]:
                print(f"Response: {result['response'][:100]}...")
                print(f"Tokens: {result['usage']['total_tokens']}")
                print(f"Cost: ${result['usage']['cost']:.6f}")
                print(f"Status: {result['budget_status']}")
            else:
                print(f"Error: {result['error']}")
            print()
        
        # Show summary
        summary = budget_manager.get_usage_summary()
        print("\nBudget Summary:")
        print(f"  Total Spent: ${summary['total_spent']:.6f}")
        print(f"  Remaining: ${summary['remaining']:.6f}")
        print(f"  Usage: {summary['usage_percentage']:.1f}%")
        print(f"  Status: {summary['status']}")
        print(f"  Total Requests: {summary['total_requests']}")
        print(f"  Total Tokens: {summary['total_tokens']}")
        print(f"  Avg Cost/Request: ${summary['average_cost_per_request']:.6f}")
        
        # Example 2: Budget Alerts
        print("\n" + "=" * 80)
        print("EXAMPLE 2: Budget Alerts and Thresholds")
        print("=" * 80)
        
        def budget_alert_handler(status: BudgetStatus, details: Dict):
            """Custom alert handler"""
            print(f"\n🚨 BUDGET ALERT: {status.value.upper()}")
            print(f"   Spent: ${details['total_spent']:.6f}")
            print(f"   Budget: ${details['budget']:.6f}")
            print(f"   Usage: {details['usage_ratio']*100:.1f}%")
        
        config2 = BudgetConfig(
            total_budget=0.10,  # Small budget to trigger alerts
            warning_threshold=0.5,
            critical_threshold=0.8,
            enforce_hard_limit=False
        )
        
        budget_manager2 = TokenBudgetManager(config2)
        budget_manager2.add_alert_callback(budget_alert_handler)
        agent2 = BudgetAwareLLMAgent(budget_manager2)
        
        print(f"\nBudget: ${config2.total_budget}")
        print("Generating responses to trigger alerts...\n")
        
        test_prompts = [
            "Explain quantum computing in detail.",
            "What are the applications of AI?",
            "Describe blockchain technology."
        ]
        
        for prompt in test_prompts:
            result = agent2.generate(prompt)
            if result["success"]:
                print(f"✓ Generated response (${result['usage']['cost']:.6f})")
        
        # Example 3: Cost Estimation and Limits
        print("\n" + "=" * 80)
        print("EXAMPLE 3: Per-Request Cost Limits")
        print("=" * 80)
        
        config3 = BudgetConfig(total_budget=0.50, enforce_hard_limit=True)
        budget_manager3 = TokenBudgetManager(config3)
        agent3 = BudgetAwareLLMAgent(budget_manager3)
        
        print("\nTrying request with cost limit...\n")
        
        result = agent3.generate_with_limit(
            "Write a short poem about AI",
            max_cost=0.01
        )
        
        if result["success"]:
            print(f"Response: {result['response']}")
            print(f"Cost: ${result['usage']['cost']:.6f}")
        else:
            print(f"Request blocked: {result['error']}")
        
        # Example 4: Model Cost Comparison
        print("\n" + "=" * 80)
        print("EXAMPLE 4: Model Cost Comparison")
        print("=" * 80)
        
        print("\nModel Pricing:")
        for model_name, pricing in TokenBudgetManager.MODEL_PRICING.items():
            print(f"\n{model_name} ({pricing.tier.value}):")
            print(f"  Input: ${pricing.input_cost_per_1k}/1K tokens")
            print(f"  Output: ${pricing.output_cost_per_1k}/1K tokens")
        
        # Calculate example costs
        print("\n\nExample cost for 1000 input + 500 output tokens:")
        for model_name in TokenBudgetManager.MODEL_PRICING.keys():
            cost = budget_manager3.calculate_cost(1000, 500, model_name)
            print(f"  {model_name}: ${cost:.6f}")
        
        # Example 5: Batch Processing with Budget
        print("\n" + "=" * 80)
        print("EXAMPLE 5: Batch Processing with Budget Control")
        print("=" * 80)
        
        config5 = BudgetConfig(total_budget=0.50)
        budget_manager5 = TokenBudgetManager(config5)
        agent5 = BudgetAwareLLMAgent(budget_manager5, model="gpt-3.5-turbo")
        
        batch_prompts = [
            "What is Python?",
            "What is JavaScript?",
            "What is Java?",
            "What is C++?",
            "What is Ruby?"
        ]
        
        print(f"\nProcessing {len(batch_prompts)} prompts with batch cost limit...")
        print(f"Batch cost limit: $0.05\n")
        
        results = agent5.batch_generate(batch_prompts, max_total_cost=0.05)
        
        successful = sum(1 for r in results if r["success"])
        total_cost = sum(r["usage"]["cost"] for r in results if r["success"] and r["usage"])
        
        print(f"\nBatch Results:")
        print(f"  Successful: {successful}/{len(batch_prompts)}")
        print(f"  Total Cost: ${total_cost:.6f}")
        print(f"  Average Cost: ${total_cost/max(1, successful):.6f}")
        
        # Summary
        print("\n" + "=" * 80)
        print("TOKEN BUDGET MANAGEMENT SUMMARY")
        print("=" * 80)
        print("""
    Token Budget Management Benefits:
    1. Cost Control: Prevent unexpected charges and budget overruns
    2. Visibility: Real-time tracking of token usage and costs
    3. Optimization: Identify opportunities to reduce token consumption
    4. Accountability: Per-user or per-project cost attribution
    5. Planning: Forecast future costs based on usage trends
    6. Alerts: Proactive notifications when approaching limits
    
    Key Components Demonstrated:
    1. Real-Time Tracking: Automatic token and cost monitoring
    2. Budget Thresholds: Warning, critical, and exceeded states
    3. Cost Calculation: Model-specific pricing and estimates
    4. Enforcement: Hard and soft budget limits
    5. Alerts: Custom callback handlers for notifications
    6. Analytics: Usage summaries and breakdowns
    
    Budget Management Strategies:
    1. Progressive Thresholds:
       - Normal:  100% of budget
    
    2. Cost Optimization:
       - Use cheaper models when quality allows
       - Compress verbose prompts
       - Limit response lengths
       - Cache common queries
       - Batch similar requests
    
    3. Monitoring:
       - Real-time dashboards
       - Usage trend analysis
       - Anomaly detection
       - Regular reporting
       - Forecasting
    
    Best Practices:
    1. Set realistic budgets based on historical data
    2. Implement progressive enforcement (soft → hard limits)
    3. Monitor usage patterns and trends
    4. Optimize prompts to reduce token consumption
    5. Use alerts to catch issues early
    6. Regular budget reviews and adjustments
    7. Cost attribution by user/project/feature
    8. Document budget policies clearly
    
    Model Selection for Cost:
    - gpt-3.5-turbo: Cheapest, good for simple tasks
    - gpt-4-turbo: Balanced cost/quality
    - gpt-4: Premium, best quality but expensive
    
    Production Considerations:
    - Accurate token counting (model-specific)
    - Up-to-date pricing information
    - Efficient usage data storage
    - Scalable tracking architecture
    - User-friendly budget notifications
    - Grace periods for critical operations
    - Emergency override procedures
    - Regular auditing and reconciliation
    
    Common Use Cases:
    - SaaS applications with freemium tiers
    - Multi-tenant platforms
    - Development/testing environments
    - High-volume production systems
    - Enterprise cost centers
    - Research project budgets
    - Educational platforms with quotas
    """)
        
        print("\n" + "=" * 80)
        print("Pattern 088 (Token Budget Management) demonstration complete!")
        print("=" * 80)
    
    
    if __name__ == "__main__":
        demonstrate_token_budget_management()
    
        

    Agentic Design Pattern: Token Budget

    Pattern 088: Token Budget – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Token Budget pattern implementation.


    Pattern Description

    Pattern 088: Token Budget Management

    Description: The Token Budget Management pattern monitors and controls token usage across LLM interactions to optimize costs, prevent budget overruns, and ensure efficient resource utilization. This pattern is critical for production deployments where token costs can escalate quickly, especially with high-volume applications or complex multi-step workflows.

    Token budget management involves:
    - Real-time token usage tracking
    - Cost estimation and forecasting
    - Budget enforcement and alerts
    - Optimization strategies for token efficiency
    - Usage analytics and reporting
    

    Components: 1. Token Counting – Input token counting (prompt tokens) – Output token counting (completion tokens) – Total token tracking per request – Cumulative usage tracking – Model-specific token limits

    2. Cost Calculation
       - Model pricing tables (per-token costs)
       - Request cost estimation
       - Cumulative cost tracking
       - Budget vs actual comparison
       - Cost forecasting
    
    3. Budget Enforcement
       - Hard limits (stop when exceeded)
       - Soft limits (warnings when approaching)
       - Rate-based budgets (tokens per hour/day)
       - User-specific quotas
       - Priority-based allocation
    
    4. Optimization Strategies
       - Prompt compression
       - Response length limiting
       - Model selection (cost vs quality)
       - Caching frequently used responses
       - Batching requests
    

    Use Cases: 1. Cost Control – Prevent unexpected charges – Stay within allocated budgets – Cost attribution per user/project – Predictable monthly spending – Resource optimization

    2. Multi-Tenant Applications
       - Per-user token quotas
       - Fair resource allocation
       - Usage-based billing
       - Tier-based limits (free/premium)
       - Abuse prevention
    
    3. Development and Testing
       - Sandbox environments with limits
       - Cost-effective experimentation
       - Testing budget allocation
       - Development vs production budgets
       - CI/CD pipeline limits
    
    4. High-Volume Applications
       - Aggregate usage tracking
       - Peak usage management
       - Resource planning
       - Scaling decisions
       - Cost optimization at scale
    
    5. Enterprise Deployments
       - Department-level budgets
       - Project-based allocation
       - Chargeback mechanisms
       - Compliance and auditing
       - Executive reporting
    

    LangChain Implementation: LangChain supports token budget management through: – Callback handlers for token tracking – get_openai_callback for cost monitoring – Custom token counters – Usage analytics integration – Budget enforcement wrappers

    Key Features: 1. Real-Time Tracking – Immediate token counting – Running total updates – Cost calculation on-the-fly – Budget threshold monitoring – Alert triggering

    2. Multi-Model Support
       - Different pricing for different models
       - Model-specific token limits
       - Automatic model selection
       - Cost-aware routing
       - Fallback to cheaper models
    
    3. Granular Control
       - Request-level limits
       - Session-level budgets
       - User-level quotas
       - Project-level allocations
       - Global organization limits
    
    4. Analytics and Reporting
       - Usage trends over time
       - Cost breakdown by model
       - Per-user consumption
       - Peak usage identification
       - Cost forecasting
    

    Best Practices: 1. Set Appropriate Budgets – Based on historical data – Include buffer for peaks – Align with business value – Regular review and adjustment – Clear escalation procedures

    2. Implement Progressive Enforcement
       - Soft warnings at 70-80%
       - Stricter controls at 90%
       - Hard stops at 100%
       - Grace period for critical operations
       - Override mechanisms for emergencies
    
    3. Optimize Token Usage
       - Compress verbose prompts
       - Limit response lengths
       - Use cheaper models when sufficient
       - Cache common queries
       - Batch similar requests
    
    4. Monitor and Alert
       - Real-time dashboards
       - Automated alerts
       - Anomaly detection
       - Trend analysis
       - Regular reporting
    

    Trade-offs: Advantages: – Predictable costs – Prevents budget overruns – Enables cost optimization – Fair resource allocation – Usage visibility and control – Informed scaling decisions

    Disadvantages:
    - Implementation complexity
    - Tracking overhead (minimal)
    - May limit functionality at budget
    - Requires ongoing management
    - False positives in alerting
    - User friction from limits
    

    Production Considerations: 1. Accuracy – Precise token counting – Up-to-date pricing information – Correct model identification – Cache hit accounting – Network overhead handling

    2. Performance
       - Minimal tracking overhead
       - Efficient storage of usage data
       - Fast budget checks
       - Optimized queries for analytics
       - Scalable architecture
    
    3. User Experience
       - Clear budget status visibility
       - Helpful error messages
       - Grace period handling
       - Budget increase requests
       - Usage optimization tips
    
    4. Maintenance
       - Regular pricing updates
       - Budget policy reviews
       - Alert threshold tuning
       - Anomaly investigation
       - Capacity planning
    
    5. Security
       - Access control for budget data
       - Secure token counting
       - Audit trail for usage
       - Fraud detection
       - Quota manipulation prevention
    

    File Statistics

    • Total Lines: 863
    • Classes: 7
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 088: Token Budget Management
    
    Description:
        The Token Budget Management pattern monitors and controls token usage across LLM
        interactions to optimize costs, prevent budget overruns, and ensure efficient
        resource utilization. This pattern is critical for production deployments where
        token costs can escalate quickly, especially with high-volume applications or
        complex multi-step workflows.
    
        Token budget management involves:
        - Real-time token usage tracking
        - Cost estimation and forecasting
        - Budget enforcement and alerts
        - Optimization strategies for token efficiency
        - Usage analytics and reporting
    
    Components:
        1. Token Counting
           - Input token counting (prompt tokens)
           - Output token counting (completion tokens)
           - Total token tracking per request
           - Cumulative usage tracking
           - Model-specific token limits
    
        2. Cost Calculation
           - Model pricing tables (per-token costs)
           - Request cost estimation
           - Cumulative cost tracking
           - Budget vs actual comparison
           - Cost forecasting
    
        3. Budget Enforcement
           - Hard limits (stop when exceeded)
           - Soft limits (warnings when approaching)
           - Rate-based budgets (tokens per hour/day)
           - User-specific quotas
           - Priority-based allocation
    
        4. Optimization Strategies
           - Prompt compression
           - Response length limiting
           - Model selection (cost vs quality)
           - Caching frequently used responses
           - Batching requests
    
    Use Cases:
        1. Cost Control
           - Prevent unexpected charges
           - Stay within allocated budgets
           - Cost attribution per user/project
           - Predictable monthly spending
           - Resource optimization
    
        2. Multi-Tenant Applications
           - Per-user token quotas
           - Fair resource allocation
           - Usage-based billing
           - Tier-based limits (free/premium)
           - Abuse prevention
    
        3. Development and Testing
           - Sandbox environments with limits
           - Cost-effective experimentation
           - Testing budget allocation
           - Development vs production budgets
           - CI/CD pipeline limits
    
        4. High-Volume Applications
           - Aggregate usage tracking
           - Peak usage management
           - Resource planning
           - Scaling decisions
           - Cost optimization at scale
    
        5. Enterprise Deployments
           - Department-level budgets
           - Project-based allocation
           - Chargeback mechanisms
           - Compliance and auditing
           - Executive reporting
    
    LangChain Implementation:
        LangChain supports token budget management through:
        - Callback handlers for token tracking
        - get_openai_callback for cost monitoring
        - Custom token counters
        - Usage analytics integration
        - Budget enforcement wrappers
    
    Key Features:
        1. Real-Time Tracking
           - Immediate token counting
           - Running total updates
           - Cost calculation on-the-fly
           - Budget threshold monitoring
           - Alert triggering
    
        2. Multi-Model Support
           - Different pricing for different models
           - Model-specific token limits
           - Automatic model selection
           - Cost-aware routing
           - Fallback to cheaper models
    
        3. Granular Control
           - Request-level limits
           - Session-level budgets
           - User-level quotas
           - Project-level allocations
           - Global organization limits
    
        4. Analytics and Reporting
           - Usage trends over time
           - Cost breakdown by model
           - Per-user consumption
           - Peak usage identification
           - Cost forecasting
    
    Best Practices:
        1. Set Appropriate Budgets
           - Based on historical data
           - Include buffer for peaks
           - Align with business value
           - Regular review and adjustment
           - Clear escalation procedures
    
        2. Implement Progressive Enforcement
           - Soft warnings at 70-80%
           - Stricter controls at 90%
           - Hard stops at 100%
           - Grace period for critical operations
           - Override mechanisms for emergencies
    
        3. Optimize Token Usage
           - Compress verbose prompts
           - Limit response lengths
           - Use cheaper models when sufficient
           - Cache common queries
           - Batch similar requests
    
        4. Monitor and Alert
           - Real-time dashboards
           - Automated alerts
           - Anomaly detection
           - Trend analysis
           - Regular reporting
    
    Trade-offs:
        Advantages:
        - Predictable costs
        - Prevents budget overruns
        - Enables cost optimization
        - Fair resource allocation
        - Usage visibility and control
        - Informed scaling decisions
    
        Disadvantages:
        - Implementation complexity
        - Tracking overhead (minimal)
        - May limit functionality at budget
        - Requires ongoing management
        - False positives in alerting
        - User friction from limits
    
    Production Considerations:
        1. Accuracy
           - Precise token counting
           - Up-to-date pricing information
           - Correct model identification
           - Cache hit accounting
           - Network overhead handling
    
        2. Performance
           - Minimal tracking overhead
           - Efficient storage of usage data
           - Fast budget checks
           - Optimized queries for analytics
           - Scalable architecture
    
        3. User Experience
           - Clear budget status visibility
           - Helpful error messages
           - Grace period handling
           - Budget increase requests
           - Usage optimization tips
    
        4. Maintenance
           - Regular pricing updates
           - Budget policy reviews
           - Alert threshold tuning
           - Anomaly investigation
           - Capacity planning
    
        5. Security
           - Access control for budget data
           - Secure token counting
           - Audit trail for usage
           - Fraud detection
           - Quota manipulation prevention
    """
    

    Purpose: This module implements the Token Budget pattern, providing a structured approach to agent design and implementation.


    Import Statements

    Standard Library Imports

    import collections.defaultdict
    import dataclasses.dataclass
    import dataclasses.field
    import datetime.datetime
    import datetime.timedelta
    import enum.Enum
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import time
    import typing.Any
    import typing.Callable
    import typing.Dict
    import typing.List
    import typing.Optional
    

    Purpose: Core Python functionality: – collections.defaultdict: Specialized container datatypes – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – datetime.timedelta: Date and time handling – enum.Enum: Enumeration support – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – time: Time-related functions – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Callable: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain.callbacks.get_openai_callback
    from langchain_openai.ChatOpenAI
    

    Purpose: LangChain framework components: – LLM Interface: Wrapper for OpenAI models – Chains: Composable building blocks for complex workflows

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: BudgetStatus

    Lines 220-225

    Inherits from: Enum

    Purpose:

    Budget status levels
    

    Class: ModelTier

    Lines 228-232

    Inherits from: Enum

    Purpose:

    Model cost tiers
    

    Class: ModelPricing

    Lines 236-241

    Purpose:

    Pricing information for a model
    

    Class: TokenUsage

    Lines 245-252

    Purpose:

    Token usage for a single request
    

    Class: BudgetConfig

    Lines 256-263

    Purpose:

    Configuration for token budget management
    

    Class: TokenBudgetManager

    Lines 266-469

    Purpose:

    Manages token budgets and costs across LLM interactions.
    
    This manager:
    1. Tracks token usage in real-time
    2. Calculates costs based on model pricing
    3. Enforces budget limits
    4. Provides usage analytics
    

    Methods:

    __init__(self, config)

    Lines 284-296

    Purpose:

    Initialize token budget manager.
    
    Args:
        config: Budget configuration
    

    Parameters: – config: Config

    add_alert_callback(self, callback)

    Lines 298-300

    Purpose:

    Add callback for budget alerts
    

    Parameters: – callback: Callback

    _trigger_alerts(self, status, details)

    Lines 302-308

    Purpose:

    Trigger all registered alert callbacks
    

    Parameters: – status: Status – details: Details

    get_status(self)

    Lines 310-321

    Purpose:

    Get current budget status
    

    Parameters:

    Returns: BudgetStatus

    can_proceed(self, estimated_cost)

    Lines 323-349

    Purpose:

    Check if request can proceed within budget.
    
    Args:
        estimated_cost: Estimated cost of request
    
    Returns:
        Tuple of (can_proceed, reason)
    

    Parameters: – estimated_cost: Estimated cost

    Returns: <ast.Subscript object at 0x000001A06D71B2D0>

    calculate_cost(self, prompt_tokens, completion_tokens, model)

    Lines 351-376

    Purpose:

    Calculate cost for token usage.
    
    Args:
        prompt_tokens: Number of prompt tokens
        completion_tokens: Number of completion tokens
        model: Model name
    
    Returns:
        Cost in USD
    

    Parameters: – prompt_tokens: Prompt template or text – completion_tokens: Completion tokens – model: Language model instance

    Returns: float

    record_usage(self, prompt_tokens, completion_tokens, model)

    Lines 378-422

    Purpose:

    Record token usage and update budget.
    
    Args:
        prompt_tokens: Number of prompt tokens
        completion_tokens: Number of completion tokens
        model: Model name
    
    Returns:
        TokenUsage object
    

    Parameters: – prompt_tokens: Prompt template or text – completion_tokens: Completion tokens – model: Language model instance

    Returns: TokenUsage

    get_remaining_budget(self)

    Lines 424-426

    Purpose:

    Get remaining budget
    

    Parameters:

    Returns: float

    get_usage_summary(self)

    Lines 428-442

    Purpose:

    Get comprehensive usage summary
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D857210>

    suggest_cheaper_model(self, current_model)

    Lines 444-459

    Purpose:

    Suggest a cheaper model alternative
    

    Parameters: – current_model: Language model instance

    Returns: <ast.Subscript object at 0x000001A06D8505D0>

    reset_budget(self, new_budget)

    Lines 461-469

    Purpose:

    Reset budget and usage tracking
    

    Parameters: – new_budget: New budget


    Class: BudgetAwareLLMAgent

    Lines 472-616

    Purpose:

    LLM agent with token budget awareness.
    
    This agent:
    1. Tracks token usage automatically
    2. Enforces budget limits
    3. Optimizes token usage
    4. Provides cost visibility
    

    Methods:

    __init__(self, budget_manager, model)

    Lines 483-497

    Purpose:

    Initialize budget-aware agent.
    
    Args:
        budget_manager: Token budget manager
        model: Model to use (defaults to config default)
    

    Parameters: – budget_manager: Budget manager – model: Language model instance

    generate(self, prompt, max_tokens)

    Lines 499-554

    Purpose:

    Generate response with budget tracking.
    
    Args:
        prompt: Input prompt
        max_tokens: Maximum tokens for response
    
    Returns:
        Dictionary with response and usage info
    

    Parameters: – prompt: Prompt template or text – max_tokens: Maximum tokens to generate

    Returns: <ast.Subscript object at 0x000001A06D83EF90>

    generate_with_limit(self, prompt, max_cost)

    Lines 556-580

    Purpose:

    Generate response with per-request cost limit.
    
    Args:
        prompt: Input prompt
        max_cost: Maximum cost for this request
    
    Returns:
        Dictionary with response and usage info
    

    Parameters: – prompt: Prompt template or text – max_cost: Max cost

    Returns: <ast.Subscript object at 0x000001A06D88B2D0>

    batch_generate(self, prompts, max_total_cost)

    Lines 582-616

    Purpose:

    Generate responses for multiple prompts with budget tracking.
    
    Args:
        prompts: List of prompts
        max_total_cost: Optional cost limit for batch
    
    Returns:
        List of results
    

    Parameters: – prompts: Prompt template or text – max_total_cost: Max total cost

    Returns: <ast.Subscript object at 0x000001A06D625290>


    Functions

    demonstrate_token_budget_management()

    Lines 619-858

    Purpose:

    Demonstrate token budget management patterns
    

    Key Concepts Summary

    Pattern Overview

    The Token Budget pattern provides a structured approach to building AI agents with specific capabilities and behaviors. This implementation leverages LangChain’s composable components to create a robust and maintainable solution.

    Core Components

    1. Agent Class: Main implementation of the pattern logic
    2. Prompt Engineering: Carefully crafted prompts to guide LLM behavior
    3. Tool Integration: Optional tools for extended functionality
    4. Memory Management: State handling across interactions
    5. Output Parsing: Structured response processing

    When to Use

    ✅ Good for: – Scenarios requiring this specific pattern’s strengths – Applications needing structured agent behavior – Systems where explainability is important – Production environments requiring reliability

    ❌ Not ideal for: – Simple single-prompt tasks – Scenarios requiring different pattern characteristics – Resource-constrained environments (if computationally intensive)


    Best Practices Demonstrated

    1. Environment Configuration

    • ✅ Use .env files for API keys
    • ✅ Never hardcode credentials
    • ✅ Validate environment variables on startup

    2. Error Handling

    • ✅ Graceful degradation on failures
    • ✅ Informative error messages
    • ✅ Logging for debugging

    3. Prompt Engineering

    • ✅ Clear, specific instructions
    • ✅ Structured output formats
    • ✅ Few-shot examples when beneficial

    4. Code Organization

    • ✅ Separation of concerns (class-based design)
    • ✅ Type hints for clarity
    • ✅ Docstrings for documentation

    5. Performance Optimization

    • ✅ Appropriate temperature settings
    • ✅ Token budget management
    • ✅ Caching where applicable

    LangChain Components Used

    • ChatOpenAI: OpenAI model interface
    • Chains: Sequential component composition

    Why LangChain? – Composable abstractions for LLM applications – Rich ecosystem of integrations – Production-ready patterns – Active community and development


    Execution Flow

    1. Initialize agent with configuration
       ↓
    2. Load environment and dependencies
       ↓
    3. Prepare prompt with input data
       ↓
    4. Invoke LLM through LangChain
       ↓
    5. Parse and validate response
       ↓
    6. Return formatted result
    

    Key Decision Points: – Model selection (temperature, model type) – Prompt structure (few-shot vs zero-shot) – Error handling strategy – Output format validation


    Performance Considerations

    Computational Cost

    • LLM Calls: Primary cost factor (based on tokens)
    • Latency: Depends on model and input size
    • Memory: Minimal for most patterns

    Optimization Strategies

    1. Prompt Optimization: Minimize tokens while maintaining clarity
    2. Caching: Cache repeated queries/responses
    3. Model Selection: Use smaller models when appropriate
    4. Batch Processing: Group similar requests
    5. Streaming: For long responses, stream tokens

    Cost Estimates (Approximate)

    • GPT-3.5-turbo: ~$0.002 per request
    • GPT-4: ~$0.03 per request
    • GPT-4-turbo: ~$0.01 per request

    Actual costs vary based on input/output token counts


    Common Pitfalls to Avoid

    1. Prompt Design Issues

    ❌ Vague or ambiguous instructions
    ✅ Clear, specific, testable prompts

    2. Error Handling

    ❌ Ignoring API errors or rate limits
    ✅ Implement retry logic and graceful degradation

    3. Token Management

    ❌ Exceeding model token limits
    ✅ Truncate inputs, chunk processing

    4. Security

    ❌ Exposing API keys in code
    ✅ Use environment variables

    5. Testing

    ❌ Only testing happy paths
    ✅ Test edge cases, failures, malformed inputs

    6. Cost Management

    ❌ Unmonitored API usage
    ✅ Implement budgets, alerts, and monitoring


    Further Improvements

    Production Enhancements

    1. Monitoring: Add logging, metrics, tracing
    2. Validation: Input/output schema validation
    3. Rate Limiting: Implement request throttling
    4. Caching: Redis or in-memory cache for responses
    5. Async Support: Non-blocking operations for scalability

    Feature Extensions

    1. Multi-Model Support: Switch between providers
    2. Fallback Models: Graceful degradation strategy
    3. Custom Tools: Domain-specific function calling
    4. Advanced Memory: Vector stores for long-term memory
    5. Evaluation: Automated testing and benchmarking

    Code Quality

    1. Testing: Unit tests, integration tests
    2. Documentation: API docs, usage examples
    3. Type Safety: Stricter type hints
    4. Configuration: Externalized config files

    Related Patterns

    • Pattern 087
    • Pattern 089

    Pattern Families: – Reasoning Patterns (001-011) – Multi-Agent Patterns (012-019) – Tool Use Patterns (020-025) – Memory Patterns (026-031) – And many more…


    Summary

    The Token Budget pattern provides a powerful approach to building AI agents with specific capabilities. This implementation demonstrates:

    ✅ Key Strengths: – Clean, maintainable code structure – LangChain integration for composability – Type hints and documentation – Production-ready patterns

    🎯 Best Use Cases: – Applications requiring this pattern’s characteristics – Systems needing structured agent behavior – Production environments requiring reliability

    📚 Learning Resources: – LangChain Documentation: https://python.langchain.com/ – Pattern Documentation: See related patterns – OpenAI API Reference: https://platform.openai.com/docs/


    File: 088_token_budget.py
    Total Lines: 863
    Pattern Category: AI Agent Design Pattern #088


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Constraint Specification

    
        """
    Pattern 087: Constraint Specification Pattern
    
    Description:
        The Constraint Specification pattern explicitly defines boundaries, rules, and
        limitations that the LLM must adhere to when generating responses. This pattern
        is crucial for controlling LLM behavior, ensuring compliance with requirements,
        and preventing undesired outputs. Constraints can be content-based, format-based,
        style-based, or domain-specific, and they guide the model toward appropriate
        responses while filtering out inappropriate ones.
    
        Constraint specification is essential for:
        - Safety and compliance requirements
        - Brand voice and style consistency
        - Content appropriateness filtering
        - Domain-specific requirements
        - Length and complexity control
        - Factual accuracy boundaries
    
    Components:
        1. Constraint Types
           - Content constraints (what to include/exclude)
           - Format constraints (structure and presentation)
           - Style constraints (tone, formality, vocabulary)
           - Length constraints (word/character limits)
           - Domain constraints (topic boundaries)
           - Safety constraints (harmful content prevention)
    
        2. Constraint Expression
           - Explicit rules ("must", "must not")
           - Positive constraints (requirements)
           - Negative constraints (prohibitions)
           - Conditional constraints (if-then rules)
           - Priority levels (strict vs flexible)
    
        3. Enforcement Strategies
           - Pre-generation (prompt engineering)
           - Post-generation (validation and filtering)
           - Iterative refinement (generate-validate-refine)
           - Guardrails (safety layers)
           - Fallback handling (when constraints violated)
    
        4. Validation Methods
           - Rule-based checking
           - Pattern matching
           - Content analysis
           - Length verification
           - Format validation
           - Safety scoring
    
    Use Cases:
        1. Content Safety
           - Prevent harmful content generation
           - Filter inappropriate language
           - Block sensitive topics
           - Ensure age-appropriate content
           - Compliance with platform policies
    
        2. Brand Consistency
           - Maintain brand voice
           - Use approved terminology
           - Follow style guidelines
           - Consistent tone across content
           - Corporate communication standards
    
        3. Domain Expertise
           - Stay within knowledge boundaries
           - Acknowledge uncertainty
           - Avoid medical/legal claims
           - Respect professional standards
           - Domain-specific terminology
    
        4. Format and Structure
           - Length limitations (tweets, abstracts)
           - Structural requirements (sections, headings)
           - Citation requirements
           - Formatting rules (markdown, HTML)
           - Template compliance
    
        5. Legal and Compliance
           - GDPR compliance (no PII without consent)
           - HIPAA compliance (no PHI exposure)
           - Copyright respect
           - Regulatory requirements
           - Terms of service adherence
    
    LangChain Implementation:
        LangChain supports constraint specification through:
        - System messages with explicit rules
        - Constitutional AI principles
        - Output validators and filters
        - Custom guardrails
        - Retry mechanisms with corrections
    
    Key Features:
        1. Multi-Level Constraints
           - Hard constraints (must be satisfied)
           - Soft constraints (preferred but flexible)
           - Conditional constraints (context-dependent)
           - Hierarchical constraints (priority ordering)
    
        2. Constraint Composition
           - Multiple constraints simultaneously
           - Constraint conflicts resolution
           - Priority-based enforcement
           - Constraint inheritance
    
        3. Validation and Feedback
           - Immediate validation
           - Detailed violation reporting
           - Correction suggestions
           - Iterative improvement
    
        4. Dynamic Constraints
           - Context-aware constraints
           - User-specific rules
           - Time-based constraints
           - Adaptive boundaries
    
    Best Practices:
        1. Clarity and Specificity
           - State constraints explicitly
           - Provide examples of violations
           - Clarify edge cases
           - Use concrete language
    
        2. Prioritization
           - Identify critical constraints
           - Allow flexibility where possible
           - Balance constraints with quality
           - Avoid over-constraining
    
        3. Testing
           - Test constraint enforcement
           - Try boundary cases
           - Verify violation detection
           - Check false positives
    
        4. Documentation
           - Document all constraints
           - Explain rationale
           - Provide violation examples
           - Maintain constraint catalog
    
    Trade-offs:
        Advantages:
        - Better control over outputs
        - Improved safety and compliance
        - Consistent brand voice
        - Reduced inappropriate content
        - Clear expectations
        - Easier validation
    
        Disadvantages:
        - May limit creativity
        - Can reduce output quality
        - Increases complexity
        - Requires careful design
        - May need frequent updates
        - Potential over-constraint
    
    Production Considerations:
        1. Constraint Management
           - Centralized constraint definitions
           - Version control for constraints
           - Easy updates and modifications
           - Constraint testing framework
           - Documentation and examples
    
        2. Performance Impact
           - Validation overhead
           - Retry costs for violations
           - Caching validated outputs
           - Efficient constraint checking
           - Batch validation
    
        3. User Experience
           - Clear violation messages
           - Suggestion for corrections
           - Graceful degradation
           - Transparency about constraints
           - User constraint customization
    
        4. Monitoring
           - Track constraint violations
           - Analyze violation patterns
           - Measure enforcement effectiveness
           - Identify constraint conflicts
           - User feedback collection
    
        5. Evolution
           - Regular constraint reviews
           - Adapt to new requirements
           - Learn from violations
           - Community feedback
           - A/B test constraint changes
    """
    
    import os
    import re
    from typing import List, Dict, Any, Optional, Callable
    from dataclasses import dataclass
    from enum import Enum
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class ConstraintType(Enum):
        """Types of constraints"""
        CONTENT = "content"
        LENGTH = "length"
        STYLE = "style"
        FORMAT = "format"
        SAFETY = "safety"
        DOMAIN = "domain"
    
    
    class ConstraintSeverity(Enum):
        """Severity levels for constraints"""
        STRICT = "strict"  # Must be satisfied
        MODERATE = "moderate"  # Should be satisfied
        FLEXIBLE = "flexible"  # Nice to have
    
    
    @dataclass
    class Constraint:
        """Represents a single constraint"""
        name: str
        description: str
        constraint_type: ConstraintType
        severity: ConstraintSeverity
        validation_func: Optional[Callable[[str], bool]] = None
        violation_message: str = "Constraint violated"
    
    
    @dataclass
    class ConstraintConfig:
        """Configuration for constraint-based generation"""
        max_retries: int = 3
        temperature: float = 0.5
        model_name: str = "gpt-3.5-turbo"
        fail_on_violation: bool = False
    
    
    class ConstraintSpecificationAgent:
        """
        Agent that enforces constraints on LLM outputs.
        
        This agent demonstrates:
        1. Defining multiple types of constraints
        2. Enforcing constraints during generation
        3. Validating outputs against constraints
        4. Handling constraint violations
        """
        
        def __init__(self, config: Optional[ConstraintConfig] = None):
            """
            Initialize constraint specification agent.
            
            Args:
                config: Configuration for constraint handling
            """
            self.config = config or ConstraintConfig()
            self.llm = ChatOpenAI(
                model=self.config.model_name,
                temperature=self.config.temperature
            )
            self.constraints: List[Constraint] = []
        
        def add_constraint(self, constraint: Constraint):
            """Add a constraint to the agent"""
            self.constraints.append(constraint)
        
        def clear_constraints(self):
            """Remove all constraints"""
            self.constraints = []
        
        def _build_constraint_instructions(self) -> str:
            """Build constraint instructions for prompt"""
            if not self.constraints:
                return ""
            
            instructions = ["You must follow these constraints:"]
            
            for i, constraint in enumerate(self.constraints, 1):
                severity_label = {
                    ConstraintSeverity.STRICT: "MUST",
                    ConstraintSeverity.MODERATE: "SHOULD",
                    ConstraintSeverity.FLEXIBLE: "IDEALLY"
                }[constraint.severity]
                
                instructions.append(f"{i}. {severity_label}: {constraint.description}")
            
            return "\n".join(instructions)
        
        def _validate_constraints(self, text: str) -> Dict[str, Any]:
            """
            Validate text against all constraints.
            
            Args:
                text: Text to validate
                
            Returns:
                Validation results
            """
            violations = []
            
            for constraint in self.constraints:
                if constraint.validation_func:
                    if not constraint.validation_func(text):
                        violations.append({
                            "constraint": constraint.name,
                            "severity": constraint.severity.value,
                            "message": constraint.violation_message
                        })
            
            return {
                "valid": len(violations) == 0,
                "violations": violations,
                "num_violations": len(violations)
            }
        
        def generate_with_constraints(
            self,
            prompt: str,
            task_description: str = "Generate a response"
        ) -> Dict[str, Any]:
            """
            Generate text while enforcing constraints.
            
            Args:
                prompt: User prompt
                task_description: Description of task
                
            Returns:
                Generation result with validation info
            """
            constraint_instructions = self._build_constraint_instructions()
            
            template = ChatPromptTemplate.from_messages([
                ("system", """{task_description}
    
    {constraints}
    
    Ensure your response satisfies all constraints."""),
                ("human", "{prompt}")
            ])
            
            chain = template | self.llm | StrOutputParser()
            
            for attempt in range(self.config.max_retries):
                # Generate
                result = chain.invoke({
                    "task_description": task_description,
                    "constraints": constraint_instructions,
                    "prompt": prompt
                })
                
                # Validate
                validation = self._validate_constraints(result)
                
                if validation["valid"]:
                    return {
                        "success": True,
                        "text": result,
                        "attempts": attempt + 1,
                        "validation": validation
                    }
                
                # If not last attempt, provide feedback for retry
                if attempt  str:
            """
            Generate text with length constraints.
            
            Args:
                prompt: User prompt
                max_words: Maximum word count
                min_words: Optional minimum word count
                
            Returns:
                Generated text
            """
            self.clear_constraints()
            
            # Add length constraint
            def validate_length(text: str) -> bool:
                word_count = len(text.split())
                if min_words and word_count  max_words:
                    return False
                return True
            
            length_desc = f"between {min_words} and {max_words} words" if min_words else f"at most {max_words} words"
            
            self.add_constraint(Constraint(
                name="length",
                description=f"Response must be {length_desc}",
                constraint_type=ConstraintType.LENGTH,
                severity=ConstraintSeverity.STRICT,
                validation_func=validate_length,
                violation_message=f"Length constraint violated (required: {length_desc})"
            ))
            
            result = self.generate_with_constraints(prompt)
            return result["text"] if result["success"] else result.get("error", "Generation failed")
        
        def generate_with_style_constraint(
            self,
            prompt: str,
            style: str = "professional"
        ) -> str:
            """
            Generate text with style constraints.
            
            Args:
                prompt: User prompt
                style: Desired style (professional, casual, formal)
                
            Returns:
                Generated text
            """
            self.clear_constraints()
            
            style_guidelines = {
                "professional": "Use professional language, avoid slang, maintain objective tone",
                "casual": "Use conversational language, contractions are fine, friendly tone",
                "formal": "Use formal language, no contractions, academic or business tone",
                "humorous": "Use humor and wit, light-hearted tone, creative wordplay"
            }
            
            guideline = style_guidelines.get(style.lower(), style_guidelines["professional"])
            
            self.add_constraint(Constraint(
                name="style",
                description=guideline,
                constraint_type=ConstraintType.STYLE,
                severity=ConstraintSeverity.MODERATE
            ))
            
            result = self.generate_with_constraints(
                prompt,
                task_description=f"Generate a {style} response"
            )
            
            return result["text"] if result["success"] else result.get("text", "")
        
        def generate_with_content_constraints(
            self,
            prompt: str,
            must_include: Optional[List[str]] = None,
            must_exclude: Optional[List[str]] = None
        ) -> str:
            """
            Generate text with content inclusion/exclusion constraints.
            
            Args:
                prompt: User prompt
                must_include: Terms that must appear
                must_exclude: Terms that must not appear
                
            Returns:
                Generated text
            """
            self.clear_constraints()
            
            # Inclusion constraint
            if must_include:
                def validate_inclusion(text: str) -> bool:
                    text_lower = text.lower()
                    return all(term.lower() in text_lower for term in must_include)
                
                self.add_constraint(Constraint(
                    name="content_inclusion",
                    description=f"Must include these terms: {', '.join(must_include)}",
                    constraint_type=ConstraintType.CONTENT,
                    severity=ConstraintSeverity.STRICT,
                    validation_func=validate_inclusion,
                    violation_message=f"Required terms missing: {', '.join(must_include)}"
                ))
            
            # Exclusion constraint
            if must_exclude:
                def validate_exclusion(text: str) -> bool:
                    text_lower = text.lower()
                    return not any(term.lower() in text_lower for term in must_exclude)
                
                self.add_constraint(Constraint(
                    name="content_exclusion",
                    description=f"Must NOT include these terms: {', '.join(must_exclude)}",
                    constraint_type=ConstraintType.CONTENT,
                    severity=ConstraintSeverity.STRICT,
                    validation_func=validate_exclusion,
                    violation_message=f"Prohibited terms found: {', '.join(must_exclude)}"
                ))
            
            result = self.generate_with_constraints(prompt)
            return result["text"] if result["success"] else result.get("text", "")
        
        def generate_with_format_constraint(
            self,
            prompt: str,
            format_type: str = "bullet_points"
        ) -> str:
            """
            Generate text with format constraints.
            
            Args:
                prompt: User prompt
                format_type: Format type (bullet_points, numbered_list, paragraphs)
                
            Returns:
                Generated text
            """
            self.clear_constraints()
            
            format_descriptions = {
                "bullet_points": "Use bullet points (- or •) for each item. No numbered lists.",
                "numbered_list": "Use numbered list format (1. 2. 3. etc.). No bullet points.",
                "paragraphs": "Write in paragraph form with clear topic sentences. No lists.",
                "single_sentence": "Provide answer as a single, concise sentence."
            }
            
            description = format_descriptions.get(format_type, format_descriptions["bullet_points"])
            
            self.add_constraint(Constraint(
                name="format",
                description=description,
                constraint_type=ConstraintType.FORMAT,
                severity=ConstraintSeverity.STRICT
            ))
            
            result = self.generate_with_constraints(
                prompt,
                task_description=f"Generate response in {format_type} format"
            )
            
            return result["text"] if result["success"] else result.get("text", "")
        
        def generate_with_safety_constraints(
            self,
            prompt: str
        ) -> str:
            """
            Generate text with safety constraints.
            
            Args:
                prompt: User prompt
                
            Returns:
                Generated text
            """
            self.clear_constraints()
            
            safety_rules = [
                "No harmful, hateful, or violent content",
                "No personal attacks or discrimination",
                "No illegal activities or advice",
                "No explicit or inappropriate content",
                "Respectful and inclusive language"
            ]
            
            for rule in safety_rules:
                self.add_constraint(Constraint(
                    name=f"safety_{safety_rules.index(rule)}",
                    description=rule,
                    constraint_type=ConstraintType.SAFETY,
                    severity=ConstraintSeverity.STRICT
                ))
            
            result = self.generate_with_constraints(
                prompt,
                task_description="Generate a safe and appropriate response"
            )
            
            return result["text"] if result["success"] else "Unable to generate safe content for this prompt."
    
    
    def demonstrate_constraint_specification():
        """Demonstrate constraint specification patterns"""
        print("=" * 80)
        print("CONSTRAINT SPECIFICATION PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = ConstraintSpecificationAgent()
        
        # Example 1: Length Constraints
        print("\n" + "=" * 80)
        print("EXAMPLE 1: Length Constraints")
        print("=" * 80)
        
        prompt = "Explain artificial intelligence"
        print(f"\nPrompt: {prompt}")
        print("\nConstraint: Maximum 50 words\n")
        
        short_response = agent.generate_with_length_constraint(prompt, max_words=50)
        print(f"Response ({len(short_response.split())} words):")
        print(short_response)
        
        print("\nConstraint: 80-120 words\n")
        medium_response = agent.generate_with_length_constraint(prompt, max_words=120, min_words=80)
        print(f"Response ({len(medium_response.split())} words):")
        print(medium_response)
        
        # Example 2: Style Constraints
        print("\n" + "=" * 80)
        print("EXAMPLE 2: Style Constraints")
        print("=" * 80)
        
        prompt = "Describe the benefits of exercise"
        print(f"\nPrompt: {prompt}\n")
        
        styles = ["professional", "casual", "formal"]
        for style in styles:
            print(f"\n{style.upper()} STYLE:")
            print("-" * 40)
            response = agent.generate_with_style_constraint(prompt, style=style)
            print(response)
        
        # Example 3: Content Inclusion/Exclusion
        print("\n" + "=" * 80)
        print("EXAMPLE 3: Content Constraints (Must Include/Exclude)")
        print("=" * 80)
        
        prompt = "Write about healthy eating habits"
        must_include = ["vegetables", "protein", "hydration"]
        must_exclude = ["diet pills", "supplements"]
        
        print(f"\nPrompt: {prompt}")
        print(f"Must include: {must_include}")
        print(f"Must exclude: {must_exclude}\n")
        
        response = agent.generate_with_content_constraints(
            prompt,
            must_include=must_include,
            must_exclude=must_exclude
        )
        print("Response:")
        print(response)
        
        # Example 4: Format Constraints
        print("\n" + "=" * 80)
        print("EXAMPLE 4: Format Constraints")
        print("=" * 80)
        
        prompt = "List the steps to start a business"
        print(f"\nPrompt: {prompt}\n")
        
        formats = ["bullet_points", "numbered_list", "paragraphs"]
        for fmt in formats:
            print(f"\n{fmt.upper().replace('_', ' ')} FORMAT:")
            print("-" * 40)
            response = agent.generate_with_format_constraint(prompt, format_type=fmt)
            print(response)
        
        # Example 5: Multiple Constraints
        print("\n" + "=" * 80)
        print("EXAMPLE 5: Multiple Simultaneous Constraints")
        print("=" * 80)
        
        agent.clear_constraints()
        
        # Add multiple constraints
        agent.add_constraint(Constraint(
            name="length",
            description="Response must be 60-80 words",
            constraint_type=ConstraintType.LENGTH,
            severity=ConstraintSeverity.STRICT,
            validation_func=lambda t: 60 <= len(t.split()) <= 80,
            violation_message="Length must be 60-80 words"
        ))
        
        agent.add_constraint(Constraint(
            name="style",
            description="Use professional, objective tone",
            constraint_type=ConstraintType.STYLE,
            severity=ConstraintSeverity.MODERATE
        ))
        
        agent.add_constraint(Constraint(
            name="content",
            description="Must mention 'innovation' and 'technology'",
            constraint_type=ConstraintType.CONTENT,
            severity=ConstraintSeverity.STRICT,
            validation_func=lambda t: "innovation" in t.lower() and "technology" in t.lower(),
            violation_message="Must include 'innovation' and 'technology'"
        ))
        
        prompt = "Describe the future of work"
        print(f"\nPrompt: {prompt}")
        print("\nConstraints:")
        print("1. Length: 60-80 words")
        print("2. Style: Professional, objective")
        print("3. Content: Must mention 'innovation' and 'technology'\n")
        
        result = agent.generate_with_constraints(prompt)
        print(f"Success: {result['success']}")
        print(f"Attempts: {result['attempts']}")
        print(f"\nResponse ({len(result['text'].split())} words):")
        print(result['text'])
        
        # Example 6: Safety Constraints
        print("\n" + "=" * 80)
        print("EXAMPLE 6: Safety Constraints")
        print("=" * 80)
        
        safe_prompt = "Give advice on managing workplace conflicts"
        print(f"\nPrompt: {safe_prompt}")
        print("\nApplying safety constraints...\n")
        
        response = agent.generate_with_safety_constraints(safe_prompt)
        print("Response:")
        print(response)
        
        # Summary
        print("\n" + "=" * 80)
        print("CONSTRAINT SPECIFICATION SUMMARY")
        print("=" * 80)
        print("""
    Constraint Specification Pattern Benefits:
    1. Control: Precise control over LLM outputs
    2. Compliance: Ensures regulatory and policy adherence
    3. Consistency: Maintains brand voice and standards
    4. Safety: Prevents inappropriate content
    5. Quality: Enforces quality standards
    6. Integration: Easier to integrate with systems
    
    Constraint Types Demonstrated:
    1. Length Constraints: Word/character limits
    2. Style Constraints: Tone, formality, voice
    3. Content Constraints: Required/prohibited terms
    4. Format Constraints: Structure and presentation
    5. Safety Constraints: Harmful content prevention
    6. Multiple Constraints: Combined requirements
    
    Constraint Severity Levels:
    - STRICT: Must be satisfied (violations cause failure)
    - MODERATE: Should be satisfied (violations logged)
    - FLEXIBLE: Nice to have (soft preferences)
    
    Enforcement Strategies:
    1. Pre-generation: Include constraints in prompt
    2. Validation: Check outputs against rules
    3. Iteration: Retry with feedback on violations
    4. Fallback: Alternative approach when constraints can't be met
    
    Best Practices:
    1. Be explicit and specific with constraints
    2. Prioritize constraints (strict vs flexible)
    3. Provide examples of acceptable outputs
    4. Test constraint enforcement thoroughly
    5. Handle violations gracefully
    6. Monitor constraint effectiveness
    7. Update constraints based on feedback
    
    Common Use Cases:
    - Content moderation (safety)
    - Brand consistency (style)
    - Legal compliance (domain rules)
    - Format standardization (structure)
    - Length requirements (summaries, tweets)
    - Quality control (standards)
    
    Production Considerations:
    - Balance constraints with creativity
    - Allow appropriate flexibility
    - Provide clear violation messages
    - Implement retry mechanisms
    - Monitor constraint violation rates
    - A/B test constraint effectiveness
    - Document all constraints clearly
    - Version control constraint rules
    
    Constraint Design Tips:
    1. Start with essential constraints only
    2. Add constraints incrementally
    3. Test against diverse inputs
    4. Avoid over-constraining
    5. Make constraints measurable
    6. Provide violation feedback
    7. Allow for constraint evolution
    
    When to Use Constraints:
    - Safety-critical applications
    - Regulated industries
    - Brand-sensitive content
    - System integration requirements
    - Quality assurance needs
    - Compliance requirements
    
    Trade-offs:
    ✅ Better control and safety
    ✅ Consistent outputs
    ✅ Easier validation
    ❌ May limit creativity
    ❌ Requires careful design
    ❌ Can be overly restrictive
    """)
        
        print("\n" + "=" * 80)
        print("Pattern 087 (Constraint Specification) demonstration complete!")
        print("=" * 80)
    
    
    if __name__ == "__main__":
        demonstrate_constraint_specification()
    
        

    Agentic Design Pattern: Constraint Specification

    Pattern 087: Constraint Specification – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Constraint Specification pattern implementation.


    Pattern Description

    Pattern 087: Constraint Specification Pattern

    Description: The Constraint Specification pattern explicitly defines boundaries, rules, and limitations that the LLM must adhere to when generating responses. This pattern is crucial for controlling LLM behavior, ensuring compliance with requirements, and preventing undesired outputs. Constraints can be content-based, format-based, style-based, or domain-specific, and they guide the model toward appropriate responses while filtering out inappropriate ones.

    Constraint specification is essential for:
    - Safety and compliance requirements
    - Brand voice and style consistency
    - Content appropriateness filtering
    - Domain-specific requirements
    - Length and complexity control
    - Factual accuracy boundaries
    

    Components: 1. Constraint Types – Content constraints (what to include/exclude) – Format constraints (structure and presentation) – Style constraints (tone, formality, vocabulary) – Length constraints (word/character limits) – Domain constraints (topic boundaries) – Safety constraints (harmful content prevention)

    2. Constraint Expression
       - Explicit rules ("must", "must not")
       - Positive constraints (requirements)
       - Negative constraints (prohibitions)
       - Conditional constraints (if-then rules)
       - Priority levels (strict vs flexible)
    
    3. Enforcement Strategies
       - Pre-generation (prompt engineering)
       - Post-generation (validation and filtering)
       - Iterative refinement (generate-validate-refine)
       - Guardrails (safety layers)
       - Fallback handling (when constraints violated)
    
    4. Validation Methods
       - Rule-based checking
       - Pattern matching
       - Content analysis
       - Length verification
       - Format validation
       - Safety scoring
    

    Use Cases: 1. Content Safety – Prevent harmful content generation – Filter inappropriate language – Block sensitive topics – Ensure age-appropriate content – Compliance with platform policies

    2. Brand Consistency
       - Maintain brand voice
       - Use approved terminology
       - Follow style guidelines
       - Consistent tone across content
       - Corporate communication standards
    
    3. Domain Expertise
       - Stay within knowledge boundaries
       - Acknowledge uncertainty
       - Avoid medical/legal claims
       - Respect professional standards
       - Domain-specific terminology
    
    4. Format and Structure
       - Length limitations (tweets, abstracts)
       - Structural requirements (sections, headings)
       - Citation requirements
       - Formatting rules (markdown, HTML)
       - Template compliance
    
    5. Legal and Compliance
       - GDPR compliance (no PII without consent)
       - HIPAA compliance (no PHI exposure)
       - Copyright respect
       - Regulatory requirements
       - Terms of service adherence
    

    LangChain Implementation: LangChain supports constraint specification through: – System messages with explicit rules – Constitutional AI principles – Output validators and filters – Custom guardrails – Retry mechanisms with corrections

    Key Features: 1. Multi-Level Constraints – Hard constraints (must be satisfied) – Soft constraints (preferred but flexible) – Conditional constraints (context-dependent) – Hierarchical constraints (priority ordering)

    2. Constraint Composition
       - Multiple constraints simultaneously
       - Constraint conflicts resolution
       - Priority-based enforcement
       - Constraint inheritance
    
    3. Validation and Feedback
       - Immediate validation
       - Detailed violation reporting
       - Correction suggestions
       - Iterative improvement
    
    4. Dynamic Constraints
       - Context-aware constraints
       - User-specific rules
       - Time-based constraints
       - Adaptive boundaries
    

    Best Practices: 1. Clarity and Specificity – State constraints explicitly – Provide examples of violations – Clarify edge cases – Use concrete language

    2. Prioritization
       - Identify critical constraints
       - Allow flexibility where possible
       - Balance constraints with quality
       - Avoid over-constraining
    
    3. Testing
       - Test constraint enforcement
       - Try boundary cases
       - Verify violation detection
       - Check false positives
    
    4. Documentation
       - Document all constraints
       - Explain rationale
       - Provide violation examples
       - Maintain constraint catalog
    

    Trade-offs: Advantages: – Better control over outputs – Improved safety and compliance – Consistent brand voice – Reduced inappropriate content – Clear expectations – Easier validation

    Disadvantages:
    - May limit creativity
    - Can reduce output quality
    - Increases complexity
    - Requires careful design
    - May need frequent updates
    - Potential over-constraint
    

    Production Considerations: 1. Constraint Management – Centralized constraint definitions – Version control for constraints – Easy updates and modifications – Constraint testing framework – Documentation and examples

    2. Performance Impact
       - Validation overhead
       - Retry costs for violations
       - Caching validated outputs
       - Efficient constraint checking
       - Batch validation
    
    3. User Experience
       - Clear violation messages
       - Suggestion for corrections
       - Graceful degradation
       - Transparency about constraints
       - User constraint customization
    
    4. Monitoring
       - Track constraint violations
       - Analyze violation patterns
       - Measure enforcement effectiveness
       - Identify constraint conflicts
       - User feedback collection
    
    5. Evolution
       - Regular constraint reviews
       - Adapt to new requirements
       - Learn from violations
       - Community feedback
       - A/B test constraint changes
    

    File Statistics

    • Total Lines: 835
    • Classes: 5
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 087: Constraint Specification Pattern
    
    Description:
        The Constraint Specification pattern explicitly defines boundaries, rules, and
        limitations that the LLM must adhere to when generating responses. This pattern
        is crucial for controlling LLM behavior, ensuring compliance with requirements,
        and preventing undesired outputs. Constraints can be content-based, format-based,
        style-based, or domain-specific, and they guide the model toward appropriate
        responses while filtering out inappropriate ones.
    
        Constraint specification is essential for:
        - Safety and compliance requirements
        - Brand voice and style consistency
        - Content appropriateness filtering
        - Domain-specific requirements
        - Length and complexity control
        - Factual accuracy boundaries
    
    Components:
        1. Constraint Types
           - Content constraints (what to include/exclude)
           - Format constraints (structure and presentation)
           - Style constraints (tone, formality, vocabulary)
           - Length constraints (word/character limits)
           - Domain constraints (topic boundaries)
           - Safety constraints (harmful content prevention)
    
        2. Constraint Expression
           - Explicit rules ("must", "must not")
           - Positive constraints (requirements)
           - Negative constraints (prohibitions)
           - Conditional constraints (if-then rules)
           - Priority levels (strict vs flexible)
    
        3. Enforcement Strategies
           - Pre-generation (prompt engineering)
           - Post-generation (validation and filtering)
           - Iterative refinement (generate-validate-refine)
           - Guardrails (safety layers)
           - Fallback handling (when constraints violated)
    
        4. Validation Methods
           - Rule-based checking
           - Pattern matching
           - Content analysis
           - Length verification
           - Format validation
           - Safety scoring
    
    Use Cases:
        1. Content Safety
           - Prevent harmful content generation
           - Filter inappropriate language
           - Block sensitive topics
           - Ensure age-appropriate content
           - Compliance with platform policies
    
        2. Brand Consistency
           - Maintain brand voice
           - Use approved terminology
           - Follow style guidelines
           - Consistent tone across content
           - Corporate communication standards
    
        3. Domain Expertise
           - Stay within knowledge boundaries
           - Acknowledge uncertainty
           - Avoid medical/legal claims
           - Respect professional standards
           - Domain-specific terminology
    
        4. Format and Structure
           - Length limitations (tweets, abstracts)
           - Structural requirements (sections, headings)
           - Citation requirements
           - Formatting rules (markdown, HTML)
           - Template compliance
    
        5. Legal and Compliance
           - GDPR compliance (no PII without consent)
           - HIPAA compliance (no PHI exposure)
           - Copyright respect
           - Regulatory requirements
           - Terms of service adherence
    
    LangChain Implementation:
        LangChain supports constraint specification through:
        - System messages with explicit rules
        - Constitutional AI principles
        - Output validators and filters
        - Custom guardrails
        - Retry mechanisms with corrections
    
    Key Features:
        1. Multi-Level Constraints
           - Hard constraints (must be satisfied)
           - Soft constraints (preferred but flexible)
           - Conditional constraints (context-dependent)
           - Hierarchical constraints (priority ordering)
    
        2. Constraint Composition
           - Multiple constraints simultaneously
           - Constraint conflicts resolution
           - Priority-based enforcement
           - Constraint inheritance
    
        3. Validation and Feedback
           - Immediate validation
           - Detailed violation reporting
           - Correction suggestions
           - Iterative improvement
    
        4. Dynamic Constraints
           - Context-aware constraints
           - User-specific rules
           - Time-based constraints
           - Adaptive boundaries
    
    Best Practices:
        1. Clarity and Specificity
           - State constraints explicitly
           - Provide examples of violations
           - Clarify edge cases
           - Use concrete language
    
        2. Prioritization
           - Identify critical constraints
           - Allow flexibility where possible
           - Balance constraints with quality
           - Avoid over-constraining
    
        3. Testing
           - Test constraint enforcement
           - Try boundary cases
           - Verify violation detection
           - Check false positives
    
        4. Documentation
           - Document all constraints
           - Explain rationale
           - Provide violation examples
           - Maintain constraint catalog
    
    Trade-offs:
        Advantages:
        - Better control over outputs
        - Improved safety and compliance
        - Consistent brand voice
        - Reduced inappropriate content
        - Clear expectations
        - Easier validation
    
        Disadvantages:
        - May limit creativity
        - Can reduce output quality
        - Increases complexity
        - Requires careful design
        - May need frequent updates
        - Potential over-constraint
    
    Production Considerations:
        1. Constraint Management
           - Centralized constraint definitions
           - Version control for constraints
           - Easy updates and modifications
           - Constraint testing framework
           - Documentation and examples
    
        2. Performance Impact
           - Validation overhead
           - Retry costs for violations
           - Caching validated outputs
           - Efficient constraint checking
           - Batch validation
    
        3. User Experience
           - Clear violation messages
           - Suggestion for corrections
           - Graceful degradation
           - Transparency about constraints
           - User constraint customization
    
        4. Monitoring
           - Track constraint violations
           - Analyze violation patterns
           - Measure enforcement effectiveness
           - Identify constraint conflicts
           - User feedback collection
    
        5. Evolution
           - Regular constraint reviews
           - Adapt to new requirements
           - Learn from violations
           - Community feedback
           - A/B test constraint changes
    """
    

    Purpose: This module implements the Constraint Specification pattern, providing a structured approach to agent design and implementation.


    Import Statements

    Standard Library Imports

    import dataclasses.dataclass
    import enum.Enum
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import re
    import typing.Any
    import typing.Callable
    import typing.Dict
    import typing.List
    import typing.Optional
    

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – enum.Enum: Enumeration support – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – re: Regular expression operations – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Callable: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

    Purpose: LangChain framework components: – LLM Interface: Wrapper for OpenAI models – Chains: Composable building blocks for complex workflows

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: ConstraintType

    Lines 213-220

    Inherits from: Enum

    Purpose:

    Types of constraints
    

    Class: ConstraintSeverity

    Lines 223-227

    Inherits from: Enum

    Purpose:

    Severity levels for constraints
    

    Class: Constraint

    Lines 231-238

    Purpose:

    Represents a single constraint
    

    Class: ConstraintConfig

    Lines 242-247

    Purpose:

    Configuration for constraint-based generation
    

    Class: ConstraintSpecificationAgent

    Lines 250-604

    Purpose:

    Agent that enforces constraints on LLM outputs.
    
    This agent demonstrates:
    1. Defining multiple types of constraints
    2. Enforcing constraints during generation
    3. Validating outputs against constraints
    4. Handling constraint violations
    

    Methods:

    __init__(self, config)

    Lines 261-273

    Purpose:

    Initialize constraint specification agent.
    
    Args:
        config: Configuration for constraint handling
    

    Parameters: – config: Config

    add_constraint(self, constraint)

    Lines 275-277

    Purpose:

    Add a constraint to the agent
    

    Parameters: – constraint: Constraint

    clear_constraints(self)

    Lines 279-281

    Purpose:

    Remove all constraints
    

    Parameters:

    _build_constraint_instructions(self)

    Lines 283-299

    Purpose:

    Build constraint instructions for prompt
    

    Parameters:

    Returns: str

    _validate_constraints(self, text)

    Lines 301-326

    Purpose:

    Validate text against all constraints.
    
    Args:
        text: Text to validate
    
    Returns:
        Validation results
    

    Parameters: – text: Text

    Returns: <ast.Subscript object at 0x000001A06D7F4B90>

    generate_with_constraints(self, prompt, task_description)

    Lines 328-392

    Purpose:

    Generate text while enforcing constraints.
    
    Args:
        prompt: User prompt
        task_description: Description of task
    
    Returns:
        Generation result with validation info
    

    Parameters: – prompt: Prompt template or text – task_description: Task description

    Returns: <ast.Subscript object at 0x000001A06D849590>

    generate_with_length_constraint(self, prompt, max_words, min_words)

    Lines 394-434

    Purpose:

    Generate text with length constraints.
    
    Args:
        prompt: User prompt
        max_words: Maximum word count
        min_words: Optional minimum word count
    
    Returns:
        Generated text
    

    Parameters: – prompt: Prompt template or text – max_words: Max words – min_words: Min words

    Returns: str

    generate_with_style_constraint(self, prompt, style)

    Lines 436-474

    Purpose:

    Generate text with style constraints.
    
    Args:
        prompt: User prompt
        style: Desired style (professional, casual, formal)
    
    Returns:
        Generated text
    

    Parameters: – prompt: Prompt template or text – style: Style

    Returns: str

    generate_with_content_constraints(self, prompt, must_include, must_exclude)

    Lines 476-526

    Purpose:

    Generate text with content inclusion/exclusion constraints.
    
    Args:
        prompt: User prompt
        must_include: Terms that must appear
        must_exclude: Terms that must not appear
    
    Returns:
        Generated text
    

    Parameters: – prompt: Prompt template or text – must_include: Must include – must_exclude: Must exclude

    Returns: str

    generate_with_format_constraint(self, prompt, format_type)

    Lines 528-566

    Purpose:

    Generate text with format constraints.
    
    Args:
        prompt: User prompt
        format_type: Format type (bullet_points, numbered_list, paragraphs)
    
    Returns:
        Generated text
    

    Parameters: – prompt: Prompt template or text – format_type: Format type

    Returns: str

    generate_with_safety_constraints(self, prompt)

    Lines 568-604

    Purpose:

    Generate text with safety constraints.
    
    Args:
        prompt: User prompt
    
    Returns:
        Generated text
    

    Parameters: – prompt: Prompt template or text

    Returns: str


    Functions

    demonstrate_constraint_specification()

    Lines 607-830

    Purpose:

    Demonstrate constraint specification patterns
    

    Key Concepts Summary

    Pattern Overview

    The Constraint Specification pattern provides a structured approach to building AI agents with specific capabilities and behaviors. This implementation leverages LangChain’s composable components to create a robust and maintainable solution.

    Core Components

    1. Agent Class: Main implementation of the pattern logic
    2. Prompt Engineering: Carefully crafted prompts to guide LLM behavior
    3. Tool Integration: Optional tools for extended functionality
    4. Memory Management: State handling across interactions
    5. Output Parsing: Structured response processing

    When to Use

    ✅ Good for: – Scenarios requiring this specific pattern’s strengths – Applications needing structured agent behavior – Systems where explainability is important – Production environments requiring reliability

    ❌ Not ideal for: – Simple single-prompt tasks – Scenarios requiring different pattern characteristics – Resource-constrained environments (if computationally intensive)


    Best Practices Demonstrated

    1. Environment Configuration

    • ✅ Use .env files for API keys
    • ✅ Never hardcode credentials
    • ✅ Validate environment variables on startup

    2. Error Handling

    • ✅ Graceful degradation on failures
    • ✅ Informative error messages
    • ✅ Logging for debugging

    3. Prompt Engineering

    • ✅ Clear, specific instructions
    • ✅ Structured output formats
    • ✅ Few-shot examples when beneficial

    4. Code Organization

    • ✅ Separation of concerns (class-based design)
    • ✅ Type hints for clarity
    • ✅ Docstrings for documentation

    5. Performance Optimization

    • ✅ Appropriate temperature settings
    • ✅ Token budget management
    • ✅ Caching where applicable

    LangChain Components Used

    • ChatOpenAI: OpenAI model interface
    • Chains: Sequential component composition

    Why LangChain? – Composable abstractions for LLM applications – Rich ecosystem of integrations – Production-ready patterns – Active community and development


    Execution Flow

    1. Initialize agent with configuration
       ↓
    2. Load environment and dependencies
       ↓
    3. Prepare prompt with input data
       ↓
    4. Invoke LLM through LangChain
       ↓
    5. Parse and validate response
       ↓
    6. Return formatted result
    

    Key Decision Points: – Model selection (temperature, model type) – Prompt structure (few-shot vs zero-shot) – Error handling strategy – Output format validation


    Performance Considerations

    Computational Cost

    • LLM Calls: Primary cost factor (based on tokens)
    • Latency: Depends on model and input size
    • Memory: Minimal for most patterns

    Optimization Strategies

    1. Prompt Optimization: Minimize tokens while maintaining clarity
    2. Caching: Cache repeated queries/responses
    3. Model Selection: Use smaller models when appropriate
    4. Batch Processing: Group similar requests
    5. Streaming: For long responses, stream tokens

    Cost Estimates (Approximate)

    • GPT-3.5-turbo: ~$0.002 per request
    • GPT-4: ~$0.03 per request
    • GPT-4-turbo: ~$0.01 per request

    Actual costs vary based on input/output token counts


    Common Pitfalls to Avoid

    1. Prompt Design Issues

    ❌ Vague or ambiguous instructions
    ✅ Clear, specific, testable prompts

    2. Error Handling

    ❌ Ignoring API errors or rate limits
    ✅ Implement retry logic and graceful degradation

    3. Token Management

    ❌ Exceeding model token limits
    ✅ Truncate inputs, chunk processing

    4. Security

    ❌ Exposing API keys in code
    ✅ Use environment variables

    5. Testing

    ❌ Only testing happy paths
    ✅ Test edge cases, failures, malformed inputs

    6. Cost Management

    ❌ Unmonitored API usage
    ✅ Implement budgets, alerts, and monitoring


    Further Improvements

    Production Enhancements

    1. Monitoring: Add logging, metrics, tracing
    2. Validation: Input/output schema validation
    3. Rate Limiting: Implement request throttling
    4. Caching: Redis or in-memory cache for responses
    5. Async Support: Non-blocking operations for scalability

    Feature Extensions

    1. Multi-Model Support: Switch between providers
    2. Fallback Models: Graceful degradation strategy
    3. Custom Tools: Domain-specific function calling
    4. Advanced Memory: Vector stores for long-term memory
    5. Evaluation: Automated testing and benchmarking

    Code Quality

    1. Testing: Unit tests, integration tests
    2. Documentation: API docs, usage examples
    3. Type Safety: Stricter type hints
    4. Configuration: Externalized config files

    Related Patterns

    • Pattern 086
    • Pattern 088

    Pattern Families: – Reasoning Patterns (001-011) – Multi-Agent Patterns (012-019) – Tool Use Patterns (020-025) – Memory Patterns (026-031) – And many more…


    Summary

    The Constraint Specification pattern provides a powerful approach to building AI agents with specific capabilities. This implementation demonstrates:

    ✅ Key Strengths: – Clean, maintainable code structure – LangChain integration for composability – Type hints and documentation – Production-ready patterns

    🎯 Best Use Cases: – Applications requiring this pattern’s characteristics – Systems needing structured agent behavior – Production environments requiring reliability

    📚 Learning Resources: – LangChain Documentation: https://python.langchain.com/ – Pattern Documentation: See related patterns – OpenAI API Reference: https://platform.openai.com/docs/


    File: 087_constraint_specification.py
    Total Lines: 835
    Pattern Category: AI Agent Design Pattern #087


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Output Format

    
        """
    Pattern 086: Output Format Specification Pattern
    
    Description:
        The Output Format Specification pattern explicitly defines the structure, format,
        and schema of the LLM's response. This pattern is crucial for integrating LLM
        outputs with downstream systems, APIs, and applications that require consistent,
        parseable, and structured data. By specifying the exact format upfront, this
        pattern eliminates ambiguity and ensures reliable programmatic processing.
    
        Format specification is essential for:
        - API integration and data exchange
        - Structured data extraction
        - Multi-step workflows with defined interfaces
        - Database storage and retrieval
        - User interface rendering
        - Automated testing and validation
    
    Components:
        1. Format Type
           - JSON (JavaScript Object Notation)
           - XML (Extensible Markup Language)
           - YAML (YAML Ain't Markup Language)
           - CSV (Comma-Separated Values)
           - Markdown (formatted text)
           - Custom formats (domain-specific)
    
        2. Schema Definition
           - Field names and types
           - Required vs optional fields
           - Value constraints and ranges
           - Nested structures
           - Array/list specifications
    
        3. Format Enforcement
           - Clear examples in prompts
           - Schema validation
           - Error handling for format violations
           - Retry mechanisms
           - Fallback strategies
    
        4. Parsing and Validation
           - Automated parsing
           - Schema compliance checking
           - Type validation
           - Constraint verification
           - Error reporting
    
    Use Cases:
        1. Data Extraction
           - Entity extraction from text → JSON
           - Information retrieval → structured records
           - Document parsing → database entries
           - Web scraping → CSV data
           - Form filling → key-value pairs
    
        2. API Integration
           - API request generation → JSON payloads
           - Response formatting → standardized structures
           - Webhook payloads → event schemas
           - Configuration files → YAML/JSON
           - Data exchange → XML documents
    
        3. Report Generation
           - Business reports → Markdown tables
           - Data summaries → formatted text
           - Analytics → JSON metrics
           - Documentation → structured sections
           - Dashboards → visualization specs
    
        4. Code Generation
           - Function signatures → specific syntax
           - Configuration files → language-specific format
           - Test cases → structured test specs
           - API documentation → OpenAPI/Swagger
           - Database schemas → SQL/DDL
    
        5. Content Creation
           - Blog posts → Markdown with frontmatter
           - Product descriptions → structured fields
           - Email templates → HTML with placeholders
           - Social media → platform-specific formats
           - Translations → parallel text structures
    
    LangChain Implementation:
        LangChain provides excellent format control through:
        - StructuredOutputParser for schema-based parsing
        - JsonOutputParser for JSON responses
        - PydanticOutputParser for type-safe outputs
        - Custom output parsers for specific formats
        - Output fixing parser for auto-correction
    
    Key Features:
        1. Type Safety
           - Pydantic models for validation
           - Automatic type checking
           - Type coercion when possible
           - Clear error messages
    
        2. Schema Validation
           - JSON Schema compliance
           - Custom validation rules
           - Nested object validation
           - Array element validation
    
        3. Error Recovery
           - Automatic retry with corrections
           - Format fixing parsers
           - Fallback to alternative formats
           - Graceful degradation
    
        4. Multiple Format Support
           - JSON (most common)
           - XML for legacy systems
           - CSV for tabular data
           - Markdown for documents
           - Custom formats as needed
    
    Best Practices:
        1. Clear Format Specification
           - Provide explicit format examples
           - Use schema definitions when possible
           - Show complete structure (not partial)
           - Include edge cases in examples
    
        2. Validation Strategy
           - Validate immediately after generation
           - Provide clear error messages
           - Implement retry logic
           - Log format violations
    
        3. Error Handling
           - Graceful fallbacks
           - User-friendly error messages
           - Automatic format correction when possible
           - Manual intervention for critical failures
    
        4. Documentation
           - Document expected formats
           - Provide format examples
           - Explain field meanings
           - Note format versions
    
    Trade-offs:
        Advantages:
        - Reliable programmatic processing
        - Easy integration with systems
        - Reduced parsing errors
        - Type safety and validation
        - Better error detection
        - Automated testing possible
    
        Disadvantages:
        - More rigid (less flexibility)
        - Format violations require handling
        - Increases prompt complexity
        - May constrain natural language quality
        - Requires careful schema design
        - Parsing overhead
    
    Production Considerations:
        1. Schema Evolution
           - Version format schemas
           - Backward compatibility
           - Migration strategies
           - Deprecation policies
    
        2. Performance
           - Parsing efficiency
           - Validation overhead
           - Caching strategies
           - Batch processing
    
        3. Reliability
           - Retry logic for format errors
           - Format fixing mechanisms
           - Fallback formats
           - Monitoring and alerting
    
        4. Maintenance
           - Schema documentation
           - Format validation tests
           - Breaking change detection
           - Client SDK generation
    
        5. Security
           - Input sanitization
           - Injection prevention
           - Size limits
           - Type validation
    """
    
    import os
    import json
    from typing import List, Dict, Any, Optional
    from dataclasses import dataclass
    from enum import Enum
    from dotenv import load_dotenv
    from pydantic import BaseModel, Field
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import (
        StrOutputParser,
        JsonOutputParser,
        PydanticOutputParser
    )
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class OutputFormat(Enum):
        """Supported output formats"""
        JSON = "json"
        XML = "xml"
        MARKDOWN = "markdown"
        CSV = "csv"
        YAML = "yaml"
    
    
    # Pydantic models for structured outputs
    class Person(BaseModel):
        """Person entity"""
        name: str = Field(description="Full name of the person")
        age: Optional[int] = Field(description="Age in years", ge=0, le=150)
        email: Optional[str] = Field(description="Email address")
        occupation: Optional[str] = Field(description="Job title or occupation")
    
    
    class Product(BaseModel):
        """Product information"""
        name: str = Field(description="Product name")
        category: str = Field(description="Product category")
        price: float = Field(description="Price in USD", gt=0)
        in_stock: bool = Field(description="Availability status")
        rating: Optional[float] = Field(description="Average rating", ge=0, le=5)
    
    
    class Article(BaseModel):
        """Article/content structure"""
        title: str = Field(description="Article title")
        author: str = Field(description="Author name")
        summary: str = Field(description="Brief summary")
        tags: List[str] = Field(description="Content tags")
        word_count: int = Field(description="Approximate word count", gt=0)
    
    
    class SentimentAnalysis(BaseModel):
        """Sentiment analysis result"""
        text: str = Field(description="Analyzed text")
        sentiment: str = Field(description="Sentiment: positive, negative, or neutral")
        confidence: float = Field(description="Confidence score", ge=0, le=1)
        keywords: List[str] = Field(description="Key terms identified")
    
    
    @dataclass
    class FormatConfig:
        """Configuration for format specification"""
        format_type: OutputFormat = OutputFormat.JSON
        strict_mode: bool = True
        temperature: float = 0.1  # Lower for more consistent formatting
        model_name: str = "gpt-3.5-turbo"
    
    
    class OutputFormatAgent:
        """
        Agent that enforces specific output formats.
        
        This agent demonstrates:
        1. JSON format specification
        2. Pydantic-based type-safe outputs
        3. Multiple format support
        4. Schema validation
        """
        
        def __init__(self, config: Optional[FormatConfig] = None):
            """
            Initialize output format agent.
            
            Args:
                config: Configuration for format handling
            """
            self.config = config or FormatConfig()
            self.llm = ChatOpenAI(
                model=self.config.model_name,
                temperature=self.config.temperature
            )
        
        def extract_as_json(
            self,
            text: str,
            schema_description: str
        ) -> Dict[str, Any]:
            """
            Extract information in JSON format.
            
            Args:
                text: Text to extract from
                schema_description: Description of desired JSON structure
                
            Returns:
                Extracted data as dictionary
            """
            json_parser = JsonOutputParser()
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """Extract information from the text in JSON format.
    
    Format requirements:
    - Valid JSON only
    - Follow the schema described
    - Use appropriate data types
    - Include all requested fields
    
    {format_instructions}"""),
                ("human", "Extract from this text: {text}\n\nSchema: {schema}")
            ])
            
            chain = prompt | self.llm | json_parser
            
            try:
                result = chain.invoke({
                    "text": text,
                    "schema": schema_description,
                    "format_instructions": json_parser.get_format_instructions()
                })
                return result
            except Exception as e:
                return {"error": str(e), "raw_text": text}
        
        def extract_person_info(self, text: str) -> Person:
            """
            Extract person information with type safety.
            
            Args:
                text: Text containing person information
                
            Returns:
                Person object with validated fields
            """
            parser = PydanticOutputParser(pydantic_object=Person)
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """Extract person information from the text.
    
    {format_instructions}
    
    Provide valid data for all available fields."""),
                ("human", "{text}")
            ])
            
            chain = prompt | self.llm | parser
            
            result = chain.invoke({
                "text": text,
                "format_instructions": parser.get_format_instructions()
            })
            
            return result
        
        def extract_product_info(self, text: str) -> Product:
            """
            Extract product information with validation.
            
            Args:
                text: Text containing product information
                
            Returns:
                Product object with validated fields
            """
            parser = PydanticOutputParser(pydantic_object=Product)
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """Extract product information from the text.
    
    {format_instructions}
    
    Ensure all data types are correct and constraints are met."""),
                ("human", "{text}")
            ])
            
            chain = prompt | self.llm | parser
            
            result = chain.invoke({
                "text": text,
                "format_instructions": parser.get_format_instructions()
            })
            
            return result
        
        def analyze_sentiment_structured(self, text: str) -> SentimentAnalysis:
            """
            Perform sentiment analysis with structured output.
            
            Args:
                text: Text to analyze
                
            Returns:
                SentimentAnalysis object with validated fields
            """
            parser = PydanticOutputParser(pydantic_object=SentimentAnalysis)
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """Analyze the sentiment of the text and provide results in the specified format.
    
    {format_instructions}
    
    - Sentiment must be: positive, negative, or neutral
    - Confidence must be between 0 and 1
    - Extract 3-5 relevant keywords"""),
                ("human", "{text}")
            ])
            
            chain = prompt | self.llm | parser
            
            result = chain.invoke({
                "text": text,
                "format_instructions": parser.get_format_instructions()
            })
            
            return result
        
        def generate_markdown_report(
            self,
            topic: str,
            sections: List[str]
        ) -> str:
            """
            Generate report in markdown format.
            
            Args:
                topic: Report topic
                sections: List of section titles
                
            Returns:
                Markdown formatted report
            """
            sections_list = "\n".join([f"- {s}" for s in sections])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """Generate a report in Markdown format.
    
    Format requirements:
    - Use # for main title
    - Use ## for section headers
    - Use **bold** for emphasis
    - Use bullet points (- ) for lists
    - Use code blocks (```) for code
    - Maintain consistent formatting"""),
                ("human", """Create a report on: {topic}
    
    Include these sections:
    {sections}""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            result = chain.invoke({
                "topic": topic,
                "sections": sections_list
            })
            
            return result
        
        def generate_csv_format(
            self,
            data_description: str,
            num_rows: int = 5
        ) -> str:
            """
            Generate data in CSV format.
            
            Args:
                data_description: Description of data to generate
                num_rows: Number of data rows
                
            Returns:
                CSV formatted data
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """Generate data in CSV format.
    
    Format requirements:
    - First row: column headers
    - Subsequent rows: data values
    - Use commas as separators
    - Quote strings containing commas
    - No extra whitespace
    - Valid CSV syntax"""),
                ("human", """Generate {num_rows} rows of CSV data for: {description}""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            result = chain.invoke({
                "description": data_description,
                "num_rows": num_rows
            })
            
            return result
        
        def generate_xml_format(self, data_description: str) -> str:
            """
            Generate data in XML format.
            
            Args:
                data_description: Description of data structure
                
            Returns:
                XML formatted data
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """Generate data in XML format.
    
    Format requirements:
    - Proper XML declaration
    - Well-formed tags (open and close)
    - Proper nesting
    - Attribute syntax when appropriate
    - Valid XML structure"""),
                ("human", "Generate XML for: {description}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            result = chain.invoke({"description": data_description})
            
            return result
        
        def compare_formats(self, data: str) -> Dict[str, str]:
            """
            Generate same data in multiple formats for comparison.
            
            Args:
                data: Data description
                
            Returns:
                Dictionary with data in different formats
            """
            formats = {}
            
            # JSON format
            json_result = self.extract_as_json(
                data,
                "Extract as JSON with appropriate fields"
            )
            formats["json"] = json.dumps(json_result, indent=2)
            
            # XML format
            formats["xml"] = self.generate_xml_format(data)
            
            # CSV format (if tabular)
            formats["csv"] = self.generate_csv_format(data, num_rows=3)
            
            return formats
    
    
    def demonstrate_output_format():
        """Demonstrate output format specification patterns"""
        print("=" * 80)
        print("OUTPUT FORMAT SPECIFICATION PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = OutputFormatAgent()
        
        # Example 1: JSON Extraction
        print("\n" + "=" * 80)
        print("EXAMPLE 1: JSON Format Extraction")
        print("=" * 80)
        
        text = "Apple Inc. released the iPhone 15 Pro in September 2023 priced at $999."
        schema = "Extract product name, company, release date, and price"
        print(f"\nText: {text}")
        print(f"Schema: {schema}\n")
        
        json_result = agent.extract_as_json(text, schema)
        print("JSON Output:")
        print(json.dumps(json_result, indent=2))
        
        # Example 2: Type-Safe Person Extraction
        print("\n" + "=" * 80)
        print("EXAMPLE 2: Type-Safe Person Information (Pydantic)")
        print("=" * 80)
        
        person_text = "Dr. Sarah Johnson is a 45-year-old cardiologist at Mayo Clinic. Her email is sarah.j@mayo.edu"
        print(f"\nText: {person_text}\n")
        
        person = agent.extract_person_info(person_text)
        print("Extracted Person (Type-Safe):")
        print(f"  Name: {person.name}")
        print(f"  Age: {person.age}")
        print(f"  Occupation: {person.occupation}")
        print(f"  Email: {person.email}")
        print(f"\nObject type: {type(person)}")
        
        # Example 3: Product Information
        print("\n" + "=" * 80)
        print("EXAMPLE 3: Structured Product Information")
        print("=" * 80)
        
        product_text = "The UltraBook Pro laptop in the Electronics category costs $1,299. Currently in stock with 4.5 star rating."
        print(f"\nText: {product_text}\n")
        
        product = agent.extract_product_info(product_text)
        print("Extracted Product:")
        print(f"  Name: {product.name}")
        print(f"  Category: {product.category}")
        print(f"  Price: ${product.price}")
        print(f"  In Stock: {product.in_stock}")
        print(f"  Rating: {product.rating}")
        
        # Example 4: Sentiment Analysis with Structure
        print("\n" + "=" * 80)
        print("EXAMPLE 4: Structured Sentiment Analysis")
        print("=" * 80)
        
        review = "This product is absolutely amazing! Best purchase I've made this year. Highly recommended!"
        print(f"\nReview: {review}\n")
        
        sentiment = agent.analyze_sentiment_structured(review)
        print("Sentiment Analysis Result:")
        print(f"  Sentiment: {sentiment.sentiment}")
        print(f"  Confidence: {sentiment.confidence:.2f}")
        print(f"  Keywords: {', '.join(sentiment.keywords)}")
        
        # Example 5: Markdown Report
        print("\n" + "=" * 80)
        print("EXAMPLE 5: Markdown Formatted Report")
        print("=" * 80)
        
        topic = "Quarterly Sales Performance"
        sections = ["Executive Summary", "Key Metrics", "Regional Analysis", "Recommendations"]
        print(f"\nTopic: {topic}")
        print(f"Sections: {sections}\n")
        
        markdown_report = agent.generate_markdown_report(topic, sections)
        print("Markdown Output:")
        print(markdown_report)
        
        # Example 6: CSV Format
        print("\n" + "=" * 80)
        print("EXAMPLE 6: CSV Format Generation")
        print("=" * 80)
        
        data_desc = "Employee records with name, department, salary, and hire date"
        print(f"\nData: {data_desc}\n")
        
        csv_data = agent.generate_csv_format(data_desc, num_rows=5)
        print("CSV Output:")
        print(csv_data)
        
        # Example 7: XML Format
        print("\n" + "=" * 80)
        print("EXAMPLE 7: XML Format Generation")
        print("=" * 80)
        
        xml_desc = "Book catalog with title, author, ISBN, and publication year"
        print(f"\nData: {xml_desc}\n")
        
        xml_data = agent.generate_xml_format(xml_desc)
        print("XML Output:")
        print(xml_data)
        
        # Summary
        print("\n" + "=" * 80)
        print("OUTPUT FORMAT SPECIFICATION SUMMARY")
        print("=" * 80)
        print("""
    Output Format Pattern Benefits:
    1. Programmatic Processing: Easy to parse and use in code
    2. Type Safety: Validation ensures correct data types
    3. Integration Ready: Works seamlessly with APIs and systems
    4. Error Detection: Schema validation catches issues early
    5. Documentation: Format serves as documentation
    6. Testing: Enables automated testing
    
    Key Formats Demonstrated:
    1. JSON: Most common, flexible, widely supported
    2. Pydantic Models: Type-safe Python objects with validation
    3. Markdown: Human-readable formatted documents
    4. CSV: Tabular data for spreadsheets
    5. XML: Structured data for legacy systems
    
    Format Specification Techniques:
    1. Explicit Examples: Show exact format in prompt
    2. Schema Definition: Use JSON Schema or Pydantic
    3. Format Instructions: Parser-generated instructions
    4. Validation: Automatic type and constraint checking
    5. Error Recovery: Retry and fix malformed outputs
    
    Best Practices:
    1. Choose appropriate format for use case
       - JSON: APIs, web apps, general data
       - CSV: Tabular data, spreadsheets
       - XML: Legacy systems, complex hierarchies
       - Markdown: Documentation, reports
       
    2. Use type safety when possible (Pydantic)
       - Automatic validation
       - IDE support
       - Clear error messages
       
    3. Provide clear format instructions
       - Show complete examples
       - Specify all fields
       - Define data types and constraints
       
    4. Handle format errors gracefully
       - Retry with corrections
       - Fallback formats
       - Clear error messages
       
    5. Validate outputs
       - Schema compliance
       - Type checking
       - Constraint verification
    
    Production Considerations:
    - Lower temperature (0.1-0.3) for consistent formatting
    - Implement retry logic for format violations
    - Cache format instructions to save tokens
    - Version schemas for evolution
    - Monitor parsing success rates
    - Log format errors for debugging
    - Test edge cases thoroughly
    
    Format Selection Guide:
    - JSON: Default choice for most applications
    - Pydantic: When type safety is critical
    - CSV: For tabular data and spreadsheet integration
    - XML: For legacy system compatibility
    - Markdown: For human-readable documents
    - YAML: For configuration files
    
    Common Pitfalls:
    - Insufficient format examples in prompts
    - No validation of generated outputs
    - Overly complex schemas
    - Missing error handling
    - Temperature too high (inconsistent outputs)
    - Not handling optional fields properly
    """)
        
        print("\n" + "=" * 80)
        print("Pattern 086 (Output Format Specification) demonstration complete!")
        print("=" * 80)
    
    
    if __name__ == "__main__":
        demonstrate_output_format()
    
        

    Agentic Design Pattern: Output Format

    Pattern 086: Output Format – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Output Format pattern implementation.


    Pattern Description

    Pattern 086: Output Format Specification Pattern

    Description: The Output Format Specification pattern explicitly defines the structure, format, and schema of the LLM’s response. This pattern is crucial for integrating LLM outputs with downstream systems, APIs, and applications that require consistent, parseable, and structured data. By specifying the exact format upfront, this pattern eliminates ambiguity and ensures reliable programmatic processing.

    Format specification is essential for:
    - API integration and data exchange
    - Structured data extraction
    - Multi-step workflows with defined interfaces
    - Database storage and retrieval
    - User interface rendering
    - Automated testing and validation
    

    Components: 1. Format Type – JSON (JavaScript Object Notation) – XML (Extensible Markup Language) – YAML (YAML Ain’t Markup Language) – CSV (Comma-Separated Values) – Markdown (formatted text) – Custom formats (domain-specific)

    2. Schema Definition
       - Field names and types
       - Required vs optional fields
       - Value constraints and ranges
       - Nested structures
       - Array/list specifications
    
    3. Format Enforcement
       - Clear examples in prompts
       - Schema validation
       - Error handling for format violations
       - Retry mechanisms
       - Fallback strategies
    
    4. Parsing and Validation
       - Automated parsing
       - Schema compliance checking
       - Type validation
       - Constraint verification
       - Error reporting
    

    Use Cases: 1. Data Extraction – Entity extraction from text → JSON – Information retrieval → structured records – Document parsing → database entries – Web scraping → CSV data – Form filling → key-value pairs

    2. API Integration
       - API request generation → JSON payloads
       - Response formatting → standardized structures
       - Webhook payloads → event schemas
       - Configuration files → YAML/JSON
       - Data exchange → XML documents
    
    3. Report Generation
       - Business reports → Markdown tables
       - Data summaries → formatted text
       - Analytics → JSON metrics
       - Documentation → structured sections
       - Dashboards → visualization specs
    
    4. Code Generation
       - Function signatures → specific syntax
       - Configuration files → language-specific format
       - Test cases → structured test specs
       - API documentation → OpenAPI/Swagger
       - Database schemas → SQL/DDL
    
    5. Content Creation
       - Blog posts → Markdown with frontmatter
       - Product descriptions → structured fields
       - Email templates → HTML with placeholders
       - Social media → platform-specific formats
       - Translations → parallel text structures
    

    LangChain Implementation: LangChain provides excellent format control through: – StructuredOutputParser for schema-based parsing – JsonOutputParser for JSON responses – PydanticOutputParser for type-safe outputs – Custom output parsers for specific formats – Output fixing parser for auto-correction

    Key Features: 1. Type Safety – Pydantic models for validation – Automatic type checking – Type coercion when possible – Clear error messages

    2. Schema Validation
       - JSON Schema compliance
       - Custom validation rules
       - Nested object validation
       - Array element validation
    
    3. Error Recovery
       - Automatic retry with corrections
       - Format fixing parsers
       - Fallback to alternative formats
       - Graceful degradation
    
    4. Multiple Format Support
       - JSON (most common)
       - XML for legacy systems
       - CSV for tabular data
       - Markdown for documents
       - Custom formats as needed
    

    Best Practices: 1. Clear Format Specification – Provide explicit format examples – Use schema definitions when possible – Show complete structure (not partial) – Include edge cases in examples

    2. Validation Strategy
       - Validate immediately after generation
       - Provide clear error messages
       - Implement retry logic
       - Log format violations
    
    3. Error Handling
       - Graceful fallbacks
       - User-friendly error messages
       - Automatic format correction when possible
       - Manual intervention for critical failures
    
    4. Documentation
       - Document expected formats
       - Provide format examples
       - Explain field meanings
       - Note format versions
    

    Trade-offs: Advantages: – Reliable programmatic processing – Easy integration with systems – Reduced parsing errors – Type safety and validation – Better error detection – Automated testing possible

    Disadvantages:
    - More rigid (less flexibility)
    - Format violations require handling
    - Increases prompt complexity
    - May constrain natural language quality
    - Requires careful schema design
    - Parsing overhead
    

    Production Considerations: 1. Schema Evolution – Version format schemas – Backward compatibility – Migration strategies – Deprecation policies

    2. Performance
       - Parsing efficiency
       - Validation overhead
       - Caching strategies
       - Batch processing
    
    3. Reliability
       - Retry logic for format errors
       - Format fixing mechanisms
       - Fallback formats
       - Monitoring and alerting
    
    4. Maintenance
       - Schema documentation
       - Format validation tests
       - Breaking change detection
       - Client SDK generation
    
    5. Security
       - Input sanitization
       - Injection prevention
       - Size limits
       - Type validation
    

    File Statistics

    • Total Lines: 751
    • Classes: 7
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 086: Output Format Specification Pattern
    
    Description:
        The Output Format Specification pattern explicitly defines the structure, format,
        and schema of the LLM's response. This pattern is crucial for integrating LLM
        outputs with downstream systems, APIs, and applications that require consistent,
        parseable, and structured data. By specifying the exact format upfront, this
        pattern eliminates ambiguity and ensures reliable programmatic processing.
    
        Format specification is essential for:
        - API integration and data exchange
        - Structured data extraction
        - Multi-step workflows with defined interfaces
        - Database storage and retrieval
        - User interface rendering
        - Automated testing and validation
    
    Components:
        1. Format Type
           - JSON (JavaScript Object Notation)
           - XML (Extensible Markup Language)
           - YAML (YAML Ain't Markup Language)
           - CSV (Comma-Separated Values)
           - Markdown (formatted text)
           - Custom formats (domain-specific)
    
        2. Schema Definition
           - Field names and types
           - Required vs optional fields
           - Value constraints and ranges
           - Nested structures
           - Array/list specifications
    
        3. Format Enforcement
           - Clear examples in prompts
           - Schema validation
           - Error handling for format violations
           - Retry mechanisms
           - Fallback strategies
    
        4. Parsing and Validation
           - Automated parsing
           - Schema compliance checking
           - Type validation
           - Constraint verification
           - Error reporting
    
    Use Cases:
        1. Data Extraction
           - Entity extraction from text → JSON
           - Information retrieval → structured records
           - Document parsing → database entries
           - Web scraping → CSV data
           - Form filling → key-value pairs
    
        2. API Integration
           - API request generation → JSON payloads
           - Response formatting → standardized structures
           - Webhook payloads → event schemas
           - Configuration files → YAML/JSON
           - Data exchange → XML documents
    
        3. Report Generation
           - Business reports → Markdown tables
           - Data summaries → formatted text
           - Analytics → JSON metrics
           - Documentation → structured sections
           - Dashboards → visualization specs
    
        4. Code Generation
           - Function signatures → specific syntax
           - Configuration files → language-specific format
           - Test cases → structured test specs
           - API documentation → OpenAPI/Swagger
           - Database schemas → SQL/DDL
    
        5. Content Creation
           - Blog posts → Markdown with frontmatter
           - Product descriptions → structured fields
           - Email templates → HTML with placeholders
           - Social media → platform-specific formats
           - Translations → parallel text structures
    
    LangChain Implementation:
        LangChain provides excellent format control through:
        - StructuredOutputParser for schema-based parsing
        - JsonOutputParser for JSON responses
        - PydanticOutputParser for type-safe outputs
        - Custom output parsers for specific formats
        - Output fixing parser for auto-correction
    
    Key Features:
        1. Type Safety
           - Pydantic models for validation
           - Automatic type checking
           - Type coercion when possible
           - Clear error messages
    
        2. Schema Validation
           - JSON Schema compliance
           - Custom validation rules
           - Nested object validation
           - Array element validation
    
        3. Error Recovery
           - Automatic retry with corrections
           - Format fixing parsers
           - Fallback to alternative formats
           - Graceful degradation
    
        4. Multiple Format Support
           - JSON (most common)
           - XML for legacy systems
           - CSV for tabular data
           - Markdown for documents
           - Custom formats as needed
    
    Best Practices:
        1. Clear Format Specification
           - Provide explicit format examples
           - Use schema definitions when possible
           - Show complete structure (not partial)
           - Include edge cases in examples
    
        2. Validation Strategy
           - Validate immediately after generation
           - Provide clear error messages
           - Implement retry logic
           - Log format violations
    
        3. Error Handling
           - Graceful fallbacks
           - User-friendly error messages
           - Automatic format correction when possible
           - Manual intervention for critical failures
    
        4. Documentation
           - Document expected formats
           - Provide format examples
           - Explain field meanings
           - Note format versions
    
    Trade-offs:
        Advantages:
        - Reliable programmatic processing
        - Easy integration with systems
        - Reduced parsing errors
        - Type safety and validation
        - Better error detection
        - Automated testing possible
    
        Disadvantages:
        - More rigid (less flexibility)
        - Format violations require handling
        - Increases prompt complexity
        - May constrain natural language quality
        - Requires careful schema design
        - Parsing overhead
    
    Production Considerations:
        1. Schema Evolution
           - Version format schemas
           - Backward compatibility
           - Migration strategies
           - Deprecation policies
    
        2. Performance
           - Parsing efficiency
           - Validation overhead
           - Caching strategies
           - Batch processing
    
        3. Reliability
           - Retry logic for format errors
           - Format fixing mechanisms
           - Fallback formats
           - Monitoring and alerting
    
        4. Maintenance
           - Schema documentation
           - Format validation tests
           - Breaking change detection
           - Client SDK generation
    
        5. Security
           - Input sanitization
           - Injection prevention
           - Size limits
           - Type validation
    """
    

    Purpose: This module implements the Output Format pattern, providing a structured approach to agent design and implementation.


    Import Statements

    Standard Library Imports

    import dataclasses.dataclass
    import enum.Enum
    import json
    import langchain_core.output_parsers.JsonOutputParser
    import langchain_core.output_parsers.PydanticOutputParser
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import typing.Any
    import typing.Dict
    import typing.List
    import typing.Optional
    

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – enum.Enum: Enumeration support – json: JSON serialization/deserialization – langchain_core.output_parsers.JsonOutputParser: Regular expression operations – langchain_core.output_parsers.PydanticOutputParser: Regular expression operations – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

    Purpose: LangChain framework components: – LLM Interface: Wrapper for OpenAI models – Chains: Composable building blocks for complex workflows

    Other Imports

    from dotenv.load_dotenv
    from pydantic.BaseModel
    from pydantic.Field
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: OutputFormat

    Lines 212-218

    Inherits from: Enum

    Purpose:

    Supported output formats
    

    Class: Person

    Lines 222-227

    Inherits from: BaseModel

    Purpose:

    Person entity
    

    Class: Product

    Lines 230-236

    Inherits from: BaseModel

    Purpose:

    Product information
    

    Class: Article

    Lines 239-245

    Inherits from: BaseModel

    Purpose:

    Article/content structure
    

    Class: SentimentAnalysis

    Lines 248-253

    Inherits from: BaseModel

    Purpose:

    Sentiment analysis result
    

    Class: FormatConfig

    Lines 257-262

    Purpose:

    Configuration for format specification
    

    Class: OutputFormatAgent

    Lines 265-555

    Purpose:

    Agent that enforces specific output formats.
    
    This agent demonstrates:
    1. JSON format specification
    2. Pydantic-based type-safe outputs
    3. Multiple format support
    4. Schema validation
    

    Methods:

    __init__(self, config)

    Lines 276-287

    Purpose:

    Initialize output format agent.
    
    Args:
        config: Configuration for format handling
    

    Parameters: – config: Config

    extract_as_json(self, text, schema_description)

    Lines 289-329

    Purpose:

    Extract information in JSON format.
    
    Args:
        text: Text to extract from
        schema_description: Description of desired JSON structure
    
    Returns:
        Extracted data as dictionary
    

    Parameters: – text: Text – schema_description: Schema description

    Returns: <ast.Subscript object at 0x000001A06D63E910>

    extract_person_info(self, text)

    Lines 331-359

    Purpose:

    Extract person information with type safety.
    
    Args:
        text: Text containing person information
    
    Returns:
        Person object with validated fields
    

    Parameters: – text: Text

    Returns: Person

    extract_product_info(self, text)

    Lines 361-389

    Purpose:

    Extract product information with validation.
    
    Args:
        text: Text containing product information
    
    Returns:
        Product object with validated fields
    

    Parameters: – text: Text

    Returns: Product

    analyze_sentiment_structured(self, text)

    Lines 391-421

    Purpose:

    Perform sentiment analysis with structured output.
    
    Args:
        text: Text to analyze
    
    Returns:
        SentimentAnalysis object with validated fields
    

    Parameters: – text: Text

    Returns: SentimentAnalysis

    generate_markdown_report(self, topic, sections)

    Lines 423-463

    Purpose:

    Generate report in markdown format.
    
    Args:
        topic: Report topic
        sections: List of section titles
    
    Returns:
        Markdown formatted report
    

    Parameters: – topic: Topic – sections: Sections

    Returns: str

    generate_csv_format(self, data_description, num_rows)

    Lines 465-500

    Purpose:

    Generate data in CSV format.
    
    Args:
        data_description: Description of data to generate
        num_rows: Number of data rows
    
    Returns:
        CSV formatted data
    

    Parameters: – data_description: Data description – num_rows: Num rows

    Returns: str

    generate_xml_format(self, data_description)

    Lines 502-528

    Purpose:

    Generate data in XML format.
    
    Args:
        data_description: Description of data structure
    
    Returns:
        XML formatted data
    

    Parameters: – data_description: Data description

    Returns: str

    compare_formats(self, data)

    Lines 530-555

    Purpose:

    Generate same data in multiple formats for comparison.
    
    Args:
        data: Data description
    
    Returns:
        Dictionary with data in different formats
    

    Parameters: – data: Data

    Returns: <ast.Subscript object at 0x000001A06D83C910>


    Functions

    demonstrate_output_format()

    Lines 558-746

    Purpose:

    Demonstrate output format specification patterns
    

    Key Concepts Summary

    Pattern Overview

    The Output Format pattern provides a structured approach to building AI agents with specific capabilities and behaviors. This implementation leverages LangChain’s composable components to create a robust and maintainable solution.

    Core Components

    1. Agent Class: Main implementation of the pattern logic
    2. Prompt Engineering: Carefully crafted prompts to guide LLM behavior
    3. Tool Integration: Optional tools for extended functionality
    4. Memory Management: State handling across interactions
    5. Output Parsing: Structured response processing

    When to Use

    ✅ Good for: – Scenarios requiring this specific pattern’s strengths – Applications needing structured agent behavior – Systems where explainability is important – Production environments requiring reliability

    ❌ Not ideal for: – Simple single-prompt tasks – Scenarios requiring different pattern characteristics – Resource-constrained environments (if computationally intensive)


    Best Practices Demonstrated

    1. Environment Configuration

    • ✅ Use .env files for API keys
    • ✅ Never hardcode credentials
    • ✅ Validate environment variables on startup

    2. Error Handling

    • ✅ Graceful degradation on failures
    • ✅ Informative error messages
    • ✅ Logging for debugging

    3. Prompt Engineering

    • ✅ Clear, specific instructions
    • ✅ Structured output formats
    • ✅ Few-shot examples when beneficial

    4. Code Organization

    • ✅ Separation of concerns (class-based design)
    • ✅ Type hints for clarity
    • ✅ Docstrings for documentation

    5. Performance Optimization

    • ✅ Appropriate temperature settings
    • ✅ Token budget management
    • ✅ Caching where applicable

    LangChain Components Used

    • ChatOpenAI: OpenAI model interface
    • Chains: Sequential component composition

    Why LangChain? – Composable abstractions for LLM applications – Rich ecosystem of integrations – Production-ready patterns – Active community and development


    Execution Flow

    1. Initialize agent with configuration
       ↓
    2. Load environment and dependencies
       ↓
    3. Prepare prompt with input data
       ↓
    4. Invoke LLM through LangChain
       ↓
    5. Parse and validate response
       ↓
    6. Return formatted result
    

    Key Decision Points: – Model selection (temperature, model type) – Prompt structure (few-shot vs zero-shot) – Error handling strategy – Output format validation


    Performance Considerations

    Computational Cost

    • LLM Calls: Primary cost factor (based on tokens)
    • Latency: Depends on model and input size
    • Memory: Minimal for most patterns

    Optimization Strategies

    1. Prompt Optimization: Minimize tokens while maintaining clarity
    2. Caching: Cache repeated queries/responses
    3. Model Selection: Use smaller models when appropriate
    4. Batch Processing: Group similar requests
    5. Streaming: For long responses, stream tokens

    Cost Estimates (Approximate)

    • GPT-3.5-turbo: ~$0.002 per request
    • GPT-4: ~$0.03 per request
    • GPT-4-turbo: ~$0.01 per request

    Actual costs vary based on input/output token counts


    Common Pitfalls to Avoid

    1. Prompt Design Issues

    ❌ Vague or ambiguous instructions
    ✅ Clear, specific, testable prompts

    2. Error Handling

    ❌ Ignoring API errors or rate limits
    ✅ Implement retry logic and graceful degradation

    3. Token Management

    ❌ Exceeding model token limits
    ✅ Truncate inputs, chunk processing

    4. Security

    ❌ Exposing API keys in code
    ✅ Use environment variables

    5. Testing

    ❌ Only testing happy paths
    ✅ Test edge cases, failures, malformed inputs

    6. Cost Management

    ❌ Unmonitored API usage
    ✅ Implement budgets, alerts, and monitoring


    Further Improvements

    Production Enhancements

    1. Monitoring: Add logging, metrics, tracing
    2. Validation: Input/output schema validation
    3. Rate Limiting: Implement request throttling
    4. Caching: Redis or in-memory cache for responses
    5. Async Support: Non-blocking operations for scalability

    Feature Extensions

    1. Multi-Model Support: Switch between providers
    2. Fallback Models: Graceful degradation strategy
    3. Custom Tools: Domain-specific function calling
    4. Advanced Memory: Vector stores for long-term memory
    5. Evaluation: Automated testing and benchmarking

    Code Quality

    1. Testing: Unit tests, integration tests
    2. Documentation: API docs, usage examples
    3. Type Safety: Stricter type hints
    4. Configuration: Externalized config files

    Related Patterns

    • Pattern 085
    • Pattern 087

    Pattern Families: – Reasoning Patterns (001-011) – Multi-Agent Patterns (012-019) – Tool Use Patterns (020-025) – Memory Patterns (026-031) – And many more…


    Summary

    The Output Format pattern provides a powerful approach to building AI agents with specific capabilities. This implementation demonstrates:

    ✅ Key Strengths: – Clean, maintainable code structure – LangChain integration for composability – Type hints and documentation – Production-ready patterns

    🎯 Best Use Cases: – Applications requiring this pattern’s characteristics – Systems needing structured agent behavior – Production environments requiring reliability

    📚 Learning Resources: – LangChain Documentation: https://python.langchain.com/ – Pattern Documentation: See related patterns – OpenAI API Reference: https://platform.openai.com/docs/


    File: 086_output_format.py
    Total Lines: 751
    Pattern Category: AI Agent Design Pattern #086


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Step By Step

    
        """
    Pattern 085: Step-by-Step Instructions Pattern
    
    Description:
        The Step-by-Step Instructions pattern structures prompts to guide the LLM through
        a process in discrete, ordered steps. This pattern is particularly effective for
        complex tasks that require systematic execution, clear sequencing, and validation
        at each stage. By breaking down tasks into explicit steps, this pattern ensures
        thorough coverage, reduces errors, and provides clear reasoning traces.
    
        This pattern is essential for:
        - Complex multi-stage workflows
        - Tasks requiring specific ordering
        - Processes with validation checkpoints
        - Teaching and tutoring applications
        - Troubleshooting and debugging
        - Recipe and instruction generation
    
    Components:
        1. Step Definition
           - Numbered sequence (1, 2, 3...)
           - Clear action verbs (calculate, analyze, verify)
           - Specific objectives per step
           - Expected outcomes
    
        2. Step Organization
           - Main steps (high-level stages)
           - Sub-steps (detailed actions)
           - Conditional branches (if-then logic)
           - Iteration loops (repeat until)
    
        3. Validation Checkpoints
           - Completion criteria per step
           - Quality checks
           - Error detection
           - Progress verification
    
        4. Progress Tracking
           - Current step indication
           - Completed steps summary
           - Remaining steps preview
           - Overall progress percentage
    
    Use Cases:
        1. Problem-Solving Workflows
           - Mathematical problem solving
           - Debugging code issues
           - Troubleshooting technical problems
           - Root cause analysis
           - Decision-making frameworks
    
        2. Instructional Content
           - Tutorial generation
           - How-to guides
           - Recipe creation
           - Assembly instructions
           - Learning modules
    
        3. Analysis Tasks
           - Data analysis workflows
           - Research methodology
           - Document review processes
           - Competitive analysis
           - Risk assessment
    
        4. Creative Processes
           - Story development (plot, characters, scenes)
           - Content creation workflow
           - Design thinking process
           - Brainstorming structure
           - Iterative refinement
    
        5. Compliance and Safety
           - Safety protocols
           - Audit procedures
           - Quality assurance checks
           - Regulatory compliance steps
           - Standard operating procedures
    
    LangChain Implementation:
        LangChain enables step-by-step patterns through:
        - Structured prompt templates
        - Chain sequencing (LCEL)
        - State management between steps
        - Conditional routing
        - Progress tracking utilities
    
    Key Features:
        1. Explicit Step Numbering
           - Clear sequential order
           - Easy progress tracking
           - Reference-able steps
           - Skippable sections
    
        2. Hierarchical Structure
           - Main steps and sub-steps
           - Nested levels (steps within steps)
           - Grouped related actions
           - Flexible depth
    
        3. Validation Integration
           - Check results at each step
           - Verify assumptions
           - Catch errors early
           - Confirm understanding
    
        4. Adaptive Sequencing
           - Conditional next steps
           - Skip irrelevant steps
           - Loop for iteration
           - Branch based on results
    
    Best Practices:
        1. Step Clarity
           - Use clear, action-oriented language
           - One main action per step
           - Avoid ambiguity
           - Provide examples when helpful
    
        2. Appropriate Granularity
           - Not too high-level (vague)
           - Not too detailed (overwhelming)
           - Balance based on complexity
           - Adjust for target audience
    
        3. Logical Ordering
           - Dependencies respected
           - Natural flow
           - Build on previous steps
           - Group related steps
    
        4. Completeness
           - Cover all necessary steps
           - Don't skip crucial details
           - Include error handling
           - Provide alternative paths
    
    Trade-offs:
        Advantages:
        - Reduced cognitive load (one step at a time)
        - Fewer errors (systematic approach)
        - Better traceability (clear reasoning path)
        - Easier debugging (identify problem step)
        - Improved learning (explicit process)
        - Higher consistency (standardized approach)
    
        Disadvantages:
        - Can be verbose (more tokens)
        - May feel rigid (less flexibility)
        - Slower for simple tasks (overhead)
        - Requires careful design (step definition)
        - May over-constrain creativity
        - Context window consumption
    
    Production Considerations:
        1. Step Optimization
           - Balance detail with brevity
           - Remove redundant steps
           - Combine simple steps
           - Expand complex steps
    
        2. Error Handling
           - Define failure conditions
           - Provide recovery steps
           - Include validation checks
           - Offer troubleshooting guidance
    
        3. User Experience
           - Show progress indicators
           - Allow step skipping (when safe)
           - Provide step summaries
           - Enable backtracking
    
        4. Performance
           - Cache intermediate results
           - Parallelize independent steps
           - Optimize step execution
           - Monitor step durations
    
        5. Maintenance
           - Version control for procedures
           - Regular step reviews
           - Update based on feedback
           - Test step completeness
    """
    
    import os
    from typing import List, Dict, Any, Optional
    from dataclasses import dataclass, field
    from enum import Enum
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class StepStatus(Enum):
        """Status of a step"""
        PENDING = "pending"
        IN_PROGRESS = "in_progress"
        COMPLETED = "completed"
        FAILED = "failed"
        SKIPPED = "skipped"
    
    
    @dataclass
    class Step:
        """Represents a single step in a process"""
        number: int
        title: str
        description: str
        action: str
        validation: Optional[str] = None
        status: StepStatus = StepStatus.PENDING
        result: Optional[str] = None
        sub_steps: List['Step'] = field(default_factory=list)
    
    
    @dataclass
    class StepByStepConfig:
        """Configuration for step-by-step instructions"""
        include_validation: bool = True
        include_examples: bool = False
        include_reasoning: bool = True
        temperature: float = 0.3
        model_name: str = "gpt-3.5-turbo"
    
    
    class StepByStepAgent:
        """
        Agent that structures tasks into clear step-by-step instructions.
        
        This agent demonstrates:
        1. Generating step-by-step processes
        2. Following structured procedures
        3. Progress tracking and validation
        4. Hierarchical step organization
        """
        
        def __init__(self, config: Optional[StepByStepConfig] = None):
            """
            Initialize step-by-step agent.
            
            Args:
                config: Configuration for step-by-step processing
            """
            self.config = config or StepByStepConfig()
            self.llm = ChatOpenAI(
                model=self.config.model_name,
                temperature=self.config.temperature
            )
        
        def generate_steps(
            self,
            task: str,
            num_steps: Optional[int] = None
        ) -> str:
            """
            Generate step-by-step instructions for a task.
            
            Args:
                task: Task to create steps for
                num_steps: Optional target number of steps
                
            Returns:
                Step-by-step instructions
            """
            num_steps_instruction = f" in approximately {num_steps} steps" if num_steps else ""
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert at creating clear, actionable step-by-step instructions.
    
    Your instructions should:
    1. Break down the task into discrete, manageable steps
    2. Use numbered steps (1, 2, 3, etc.)
    3. Start each step with an action verb
    4. Be specific and actionable
    5. Include validation checks where appropriate
    6. Maintain logical ordering"""),
                ("human", "Create step-by-step instructions for: {task}{num_steps_instruction}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            result = chain.invoke({
                "task": task,
                "num_steps_instruction": num_steps_instruction
            })
            
            return result
        
        def solve_problem_step_by_step(
            self,
            problem: str
        ) -> str:
            """
            Solve a problem using step-by-step reasoning.
            
            Args:
                problem: Problem to solve
                
            Returns:
                Step-by-step solution
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a problem solver who thinks step-by-step.
    
    For each problem:
    1. Break it down into logical steps
    2. Show your work for each step
    3. Validate results as you go
    4. State the final answer clearly
    
    Use this format:
    Step 1: [First action]
    [Work/reasoning]
    
    Step 2: [Second action]
    [Work/reasoning]
    
    ...
    
    Final Answer: [Conclusion]"""),
                ("human", "Solve this problem step-by-step: {problem}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            result = chain.invoke({"problem": problem})
            
            return result
        
        def debug_code_step_by_step(
            self,
            code: str,
            error: str
        ) -> str:
            """
            Debug code using systematic step-by-step approach.
            
            Args:
                code: Code with error
                error: Error message
                
            Returns:
                Step-by-step debugging process
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a debugging expert who follows a systematic step-by-step process.
    
    Debugging steps:
    1. Understand the error message
    2. Locate the error in the code
    3. Identify the root cause
    4. Propose a fix
    5. Explain why the fix works
    6. Provide the corrected code
    
    Follow these steps explicitly in your response."""),
                ("human", """Debug this code step-by-step:
    
    Code:
    {code}
    
    Error:
    {error}""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            result = chain.invoke({"code": code, "error": error})
            
            return result
        
        def create_tutorial_step_by_step(
            self,
            topic: str,
            audience: str = "beginners"
        ) -> str:
            """
            Create a tutorial with step-by-step instructions.
            
            Args:
                topic: Topic to teach
                audience: Target audience level
                
            Returns:
                Step-by-step tutorial
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert teacher creating step-by-step tutorials.
    
    Your tutorials should:
    1. Start with an overview
    2. List prerequisites
    3. Break content into numbered steps
    4. Include examples in each step
    5. Provide checkpoints to verify understanding
    6. End with a summary and next steps
    
    Adapt complexity to the audience level."""),
                ("human", "Create a step-by-step tutorial on '{topic}' for {audience}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            result = chain.invoke({"topic": topic, "audience": audience})
            
            return result
        
        def analyze_data_step_by_step(
            self,
            data_description: str,
            analysis_goal: str
        ) -> str:
            """
            Perform data analysis using step-by-step methodology.
            
            Args:
                data_description: Description of the data
                analysis_goal: Goal of the analysis
                
            Returns:
                Step-by-step analysis process
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a data analyst who follows a structured step-by-step methodology.
    
    Standard data analysis steps:
    1. Understand the data and goal
    2. Data cleaning and preparation
    3. Exploratory analysis
    4. Apply appropriate techniques
    5. Interpret results
    6. Draw conclusions
    7. Recommend actions
    
    Follow these steps in your analysis."""),
                ("human", """Analyze this data step-by-step:
    
    Data: {data_description}
    Goal: {analysis_goal}""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            result = chain.invoke({
                "data_description": data_description,
                "analysis_goal": analysis_goal
            })
            
            return result
        
        def decision_framework_step_by_step(
            self,
            decision: str,
            context: str
        ) -> str:
            """
            Apply decision-making framework step-by-step.
            
            Args:
                decision: Decision to make
                context: Context and constraints
                
            Returns:
                Step-by-step decision analysis
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a decision analyst using a structured step-by-step framework.
    
    Decision-making steps:
    1. Define the decision clearly
    2. Identify objectives and criteria
    3. Generate alternatives
    4. Evaluate each alternative against criteria
    5. Assess risks and uncertainties
    6. Make recommendation
    7. Plan implementation
    
    Follow these steps systematically."""),
                ("human", """Help me make this decision step-by-step:
    
    Decision: {decision}
    Context: {context}""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            result = chain.invoke({"decision": decision, "context": context})
            
            return result
        
        def troubleshoot_step_by_step(
            self,
            problem_description: str,
            system: str = "general"
        ) -> str:
            """
            Troubleshoot a problem using systematic steps.
            
            Args:
                problem_description: Description of the problem
                system: System or domain
                
            Returns:
                Step-by-step troubleshooting guide
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a troubleshooting expert who uses a systematic step-by-step approach.
    
    Troubleshooting methodology:
    1. Gather information about the problem
    2. Identify symptoms
    3. Form hypotheses about causes
    4. Test hypotheses systematically
    5. Isolate the root cause
    6. Implement solution
    7. Verify resolution
    8. Document for future reference
    
    Follow these steps in your troubleshooting process."""),
                ("human", """Troubleshoot this problem step-by-step:
    
    Problem: {problem_description}
    System: {system}""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            result = chain.invoke({
                "problem_description": problem_description,
                "system": system
            })
            
            return result
    
    
    def demonstrate_step_by_step():
        """Demonstrate step-by-step instruction patterns"""
        print("=" * 80)
        print("STEP-BY-STEP INSTRUCTIONS PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = StepByStepAgent()
        
        # Example 1: Generate Instructions for Task
        print("\n" + "=" * 80)
        print("EXAMPLE 1: Generate Step-by-Step Instructions")
        print("=" * 80)
        
        task = "Plan and execute a successful product launch"
        print(f"\nTask: {task}\n")
        instructions = agent.generate_steps(task, num_steps=7)
        print(instructions)
        
        # Example 2: Math Problem Solving
        print("\n" + "=" * 80)
        print("EXAMPLE 2: Step-by-Step Problem Solving")
        print("=" * 80)
        
        problem = "If a train travels 120 miles in 2 hours, then slows down and travels 80 miles in 2 hours, what is its average speed for the entire journey?"
        print(f"\nProblem: {problem}\n")
        solution = agent.solve_problem_step_by_step(problem)
        print(solution)
        
        # Example 3: Code Debugging
        print("\n" + "=" * 80)
        print("EXAMPLE 3: Step-by-Step Code Debugging")
        print("=" * 80)
        
        buggy_code = """
    def calculate_average(numbers):
        total = 0
        for num in numbers:
            total += num
        return total / len(numbers)
    
    result = calculate_average([])
    print(result)
    """
        error = "ZeroDivisionError: division by zero"
        
        print(f"Code:\n{buggy_code}")
        print(f"\nError: {error}\n")
        debug_process = agent.debug_code_step_by_step(buggy_code, error)
        print(debug_process)
        
        # Example 4: Tutorial Creation
        print("\n" + "=" * 80)
        print("EXAMPLE 4: Step-by-Step Tutorial")
        print("=" * 80)
        
        topic = "Setting up a Python virtual environment"
        audience = "beginners"
        print(f"\nTopic: {topic}")
        print(f"Audience: {audience}\n")
        tutorial = agent.create_tutorial_step_by_step(topic, audience)
        print(tutorial)
        
        # Example 5: Data Analysis
        print("\n" + "=" * 80)
        print("EXAMPLE 5: Step-by-Step Data Analysis")
        print("=" * 80)
        
        data_desc = "Sales data for last quarter showing revenue, units sold, and customer segments"
        goal = "Identify which customer segment has the highest growth potential"
        print(f"\nData: {data_desc}")
        print(f"Goal: {goal}\n")
        analysis = agent.analyze_data_step_by_step(data_desc, goal)
        print(analysis)
        
        # Example 6: Decision Making
        print("\n" + "=" * 80)
        print("EXAMPLE 6: Step-by-Step Decision Framework")
        print("=" * 80)
        
        decision = "Should we expand to a new market?"
        context = "B2B software company with stable domestic revenue, considering Asian market expansion"
        print(f"\nDecision: {decision}")
        print(f"Context: {context}\n")
        decision_analysis = agent.decision_framework_step_by_step(decision, context)
        print(decision_analysis)
        
        # Example 7: Troubleshooting
        print("\n" + "=" * 80)
        print("EXAMPLE 7: Step-by-Step Troubleshooting")
        print("=" * 80)
        
        problem = "Website is loading very slowly for users"
        system = "web application"
        print(f"\nProblem: {problem}")
        print(f"System: {system}\n")
        troubleshooting = agent.troubleshoot_step_by_step(problem, system)
        print(troubleshooting)
        
        # Summary
        print("\n" + "=" * 80)
        print("STEP-BY-STEP INSTRUCTIONS SUMMARY")
        print("=" * 80)
        print("""
    Step-by-Step Pattern Benefits:
    1. Reduced Complexity: Breaks down complex tasks into manageable parts
    2. Clear Progress: Easy to track where you are in the process
    3. Fewer Errors: Systematic approach catches mistakes early
    4. Better Learning: Explicit steps aid understanding
    5. Reproducibility: Others can follow the same process
    6. Debugging: Easy to identify where things went wrong
    
    Key Applications Demonstrated:
    1. Task Planning: Creating structured workflows
    2. Problem Solving: Mathematical and logical problems
    3. Code Debugging: Systematic error identification
    4. Tutorial Creation: Teaching complex topics
    5. Data Analysis: Structured analytical methodology
    6. Decision Making: Rational choice frameworks
    7. Troubleshooting: Systematic problem diagnosis
    
    Step-by-Step Structure Elements:
    - Numbered sequence (clear ordering)
    - Action verbs (calculate, analyze, verify)
    - Validation checkpoints (confirm results)
    - Progress indicators (current step)
    - Sub-steps (hierarchical detail)
    - Conditional branches (if-then logic)
    
    Best Practices:
    1. Use clear, action-oriented language
    2. Maintain logical dependencies between steps
    3. Include validation checks at key points
    4. Keep steps at appropriate granularity
    5. Number steps clearly (1, 2, 3...)
    6. Start steps with action verbs
    7. Provide examples within steps when helpful
    8. Include error handling and alternatives
    
    When to Use Step-by-Step:
    - Complex multi-stage tasks
    - Teaching and learning scenarios
    - Troubleshooting and debugging
    - Compliance and safety procedures
    - Quality assurance processes
    - Standardized workflows
    - Problem-solving that requires systematic approach
    
    Advantages Over Free-Form:
    - More thorough coverage
    - Easier to follow and verify
    - Better for complex tasks
    - Reduces cognitive load
    - Improves consistency
    - Facilitates collaboration
    
    Trade-offs:
    - More verbose (uses more tokens)
    - Can feel rigid for simple tasks
    - Requires careful step design
    - May over-structure creative tasks
    - Slower for trivial operations
    
    Production Tips:
    - Cache common step sequences
    - Allow step skipping when safe
    - Provide progress indicators
    - Enable backtracking to previous steps
    - Version control procedural steps
    - Gather feedback on step clarity
    - Optimize step granularity based on usage
    """)
        
        print("\n" + "=" * 80)
        print("Pattern 085 (Step-by-Step Instructions) demonstration complete!")
        print("=" * 80)
    
    
    if __name__ == "__main__":
        demonstrate_step_by_step()
    
        

    Agentic Design Pattern: Step By Step

    Pattern 085: Step By Step – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Step By Step pattern implementation.


    Pattern Description

    Pattern 085: Step-by-Step Instructions Pattern

    Description: The Step-by-Step Instructions pattern structures prompts to guide the LLM through a process in discrete, ordered steps. This pattern is particularly effective for complex tasks that require systematic execution, clear sequencing, and validation at each stage. By breaking down tasks into explicit steps, this pattern ensures thorough coverage, reduces errors, and provides clear reasoning traces.

    This pattern is essential for:
    - Complex multi-stage workflows
    - Tasks requiring specific ordering
    - Processes with validation checkpoints
    - Teaching and tutoring applications
    - Troubleshooting and debugging
    - Recipe and instruction generation
    

    Components: 1. Step Definition – Numbered sequence (1, 2, 3…) – Clear action verbs (calculate, analyze, verify) – Specific objectives per step – Expected outcomes

    2. Step Organization
       - Main steps (high-level stages)
       - Sub-steps (detailed actions)
       - Conditional branches (if-then logic)
       - Iteration loops (repeat until)
    
    3. Validation Checkpoints
       - Completion criteria per step
       - Quality checks
       - Error detection
       - Progress verification
    
    4. Progress Tracking
       - Current step indication
       - Completed steps summary
       - Remaining steps preview
       - Overall progress percentage
    

    Use Cases: 1. Problem-Solving Workflows – Mathematical problem solving – Debugging code issues – Troubleshooting technical problems – Root cause analysis – Decision-making frameworks

    2. Instructional Content
       - Tutorial generation
       - How-to guides
       - Recipe creation
       - Assembly instructions
       - Learning modules
    
    3. Analysis Tasks
       - Data analysis workflows
       - Research methodology
       - Document review processes
       - Competitive analysis
       - Risk assessment
    
    4. Creative Processes
       - Story development (plot, characters, scenes)
       - Content creation workflow
       - Design thinking process
       - Brainstorming structure
       - Iterative refinement
    
    5. Compliance and Safety
       - Safety protocols
       - Audit procedures
       - Quality assurance checks
       - Regulatory compliance steps
       - Standard operating procedures
    

    LangChain Implementation: LangChain enables step-by-step patterns through: – Structured prompt templates – Chain sequencing (LCEL) – State management between steps – Conditional routing – Progress tracking utilities

    Key Features: 1. Explicit Step Numbering – Clear sequential order – Easy progress tracking – Reference-able steps – Skippable sections

    2. Hierarchical Structure
       - Main steps and sub-steps
       - Nested levels (steps within steps)
       - Grouped related actions
       - Flexible depth
    
    3. Validation Integration
       - Check results at each step
       - Verify assumptions
       - Catch errors early
       - Confirm understanding
    
    4. Adaptive Sequencing
       - Conditional next steps
       - Skip irrelevant steps
       - Loop for iteration
       - Branch based on results
    

    Best Practices: 1. Step Clarity – Use clear, action-oriented language – One main action per step – Avoid ambiguity – Provide examples when helpful

    2. Appropriate Granularity
       - Not too high-level (vague)
       - Not too detailed (overwhelming)
       - Balance based on complexity
       - Adjust for target audience
    
    3. Logical Ordering
       - Dependencies respected
       - Natural flow
       - Build on previous steps
       - Group related steps
    
    4. Completeness
       - Cover all necessary steps
       - Don't skip crucial details
       - Include error handling
       - Provide alternative paths
    

    Trade-offs: Advantages: – Reduced cognitive load (one step at a time) – Fewer errors (systematic approach) – Better traceability (clear reasoning path) – Easier debugging (identify problem step) – Improved learning (explicit process) – Higher consistency (standardized approach)

    Disadvantages:
    - Can be verbose (more tokens)
    - May feel rigid (less flexibility)
    - Slower for simple tasks (overhead)
    - Requires careful design (step definition)
    - May over-constrain creativity
    - Context window consumption
    

    Production Considerations: 1. Step Optimization – Balance detail with brevity – Remove redundant steps – Combine simple steps – Expand complex steps

    2. Error Handling
       - Define failure conditions
       - Provide recovery steps
       - Include validation checks
       - Offer troubleshooting guidance
    
    3. User Experience
       - Show progress indicators
       - Allow step skipping (when safe)
       - Provide step summaries
       - Enable backtracking
    
    4. Performance
       - Cache intermediate results
       - Parallelize independent steps
       - Optimize step execution
       - Monitor step durations
    
    5. Maintenance
       - Version control for procedures
       - Regular step reviews
       - Update based on feedback
       - Test step completeness
    

    File Statistics

    • Total Lines: 715
    • Classes: 4
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 085: Step-by-Step Instructions Pattern
    
    Description:
        The Step-by-Step Instructions pattern structures prompts to guide the LLM through
        a process in discrete, ordered steps. This pattern is particularly effective for
        complex tasks that require systematic execution, clear sequencing, and validation
        at each stage. By breaking down tasks into explicit steps, this pattern ensures
        thorough coverage, reduces errors, and provides clear reasoning traces.
    
        This pattern is essential for:
        - Complex multi-stage workflows
        - Tasks requiring specific ordering
        - Processes with validation checkpoints
        - Teaching and tutoring applications
        - Troubleshooting and debugging
        - Recipe and instruction generation
    
    Components:
        1. Step Definition
           - Numbered sequence (1, 2, 3...)
           - Clear action verbs (calculate, analyze, verify)
           - Specific objectives per step
           - Expected outcomes
    
        2. Step Organization
           - Main steps (high-level stages)
           - Sub-steps (detailed actions)
           - Conditional branches (if-then logic)
           - Iteration loops (repeat until)
    
        3. Validation Checkpoints
           - Completion criteria per step
           - Quality checks
           - Error detection
           - Progress verification
    
        4. Progress Tracking
           - Current step indication
           - Completed steps summary
           - Remaining steps preview
           - Overall progress percentage
    
    Use Cases:
        1. Problem-Solving Workflows
           - Mathematical problem solving
           - Debugging code issues
           - Troubleshooting technical problems
           - Root cause analysis
           - Decision-making frameworks
    
        2. Instructional Content
           - Tutorial generation
           - How-to guides
           - Recipe creation
           - Assembly instructions
           - Learning modules
    
        3. Analysis Tasks
           - Data analysis workflows
           - Research methodology
           - Document review processes
           - Competitive analysis
           - Risk assessment
    
        4. Creative Processes
           - Story development (plot, characters, scenes)
           - Content creation workflow
           - Design thinking process
           - Brainstorming structure
           - Iterative refinement
    
        5. Compliance and Safety
           - Safety protocols
           - Audit procedures
           - Quality assurance checks
           - Regulatory compliance steps
           - Standard operating procedures
    
    LangChain Implementation:
        LangChain enables step-by-step patterns through:
        - Structured prompt templates
        - Chain sequencing (LCEL)
        - State management between steps
        - Conditional routing
        - Progress tracking utilities
    
    Key Features:
        1. Explicit Step Numbering
           - Clear sequential order
           - Easy progress tracking
           - Reference-able steps
           - Skippable sections
    
        2. Hierarchical Structure
           - Main steps and sub-steps
           - Nested levels (steps within steps)
           - Grouped related actions
           - Flexible depth
    
        3. Validation Integration
           - Check results at each step
           - Verify assumptions
           - Catch errors early
           - Confirm understanding
    
        4. Adaptive Sequencing
           - Conditional next steps
           - Skip irrelevant steps
           - Loop for iteration
           - Branch based on results
    
    Best Practices:
        1. Step Clarity
           - Use clear, action-oriented language
           - One main action per step
           - Avoid ambiguity
           - Provide examples when helpful
    
        2. Appropriate Granularity
           - Not too high-level (vague)
           - Not too detailed (overwhelming)
           - Balance based on complexity
           - Adjust for target audience
    
        3. Logical Ordering
           - Dependencies respected
           - Natural flow
           - Build on previous steps
           - Group related steps
    
        4. Completeness
           - Cover all necessary steps
           - Don't skip crucial details
           - Include error handling
           - Provide alternative paths
    
    Trade-offs:
        Advantages:
        - Reduced cognitive load (one step at a time)
        - Fewer errors (systematic approach)
        - Better traceability (clear reasoning path)
        - Easier debugging (identify problem step)
        - Improved learning (explicit process)
        - Higher consistency (standardized approach)
    
        Disadvantages:
        - Can be verbose (more tokens)
        - May feel rigid (less flexibility)
        - Slower for simple tasks (overhead)
        - Requires careful design (step definition)
        - May over-constrain creativity
        - Context window consumption
    
    Production Considerations:
        1. Step Optimization
           - Balance detail with brevity
           - Remove redundant steps
           - Combine simple steps
           - Expand complex steps
    
        2. Error Handling
           - Define failure conditions
           - Provide recovery steps
           - Include validation checks
           - Offer troubleshooting guidance
    
        3. User Experience
           - Show progress indicators
           - Allow step skipping (when safe)
           - Provide step summaries
           - Enable backtracking
    
        4. Performance
           - Cache intermediate results
           - Parallelize independent steps
           - Optimize step execution
           - Monitor step durations
    
        5. Maintenance
           - Version control for procedures
           - Regular step reviews
           - Update based on feedback
           - Test step completeness
    """
    

    Purpose: This module implements the Step By Step pattern, providing a structured approach to agent design and implementation.


    Import Statements

    Standard Library Imports

    import dataclasses.dataclass
    import dataclasses.field
    import enum.Enum
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import typing.Any
    import typing.Dict
    import typing.List
    import typing.Optional
    

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – enum.Enum: Enumeration support – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

    Purpose: LangChain framework components: – LLM Interface: Wrapper for OpenAI models – Chains: Composable building blocks for complex workflows

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: StepStatus

    Lines 200-206

    Inherits from: Enum

    Purpose:

    Status of a step
    

    Class: Step

    Lines 210-219

    Purpose:

    Represents a single step in a process
    

    Class: StepByStepConfig

    Lines 223-229

    Purpose:

    Configuration for step-by-step instructions
    

    Class: StepByStepAgent

    Lines 232-533

    Purpose:

    Agent that structures tasks into clear step-by-step instructions.
    
    This agent demonstrates:
    1. Generating step-by-step processes
    2. Following structured procedures
    3. Progress tracking and validation
    4. Hierarchical step organization
    

    Methods:

    __init__(self, config)

    Lines 243-254

    Purpose:

    Initialize step-by-step agent.
    
    Args:
        config: Configuration for step-by-step processing
    

    Parameters: – config: Config

    generate_steps(self, task, num_steps)

    Lines 256-293

    Purpose:

    Generate step-by-step instructions for a task.
    
    Args:
        task: Task to create steps for
        num_steps: Optional target number of steps
    
    Returns:
        Step-by-step instructions
    

    Parameters: – task: Task – num_steps: Num steps

    Returns: str

    solve_problem_step_by_step(self, problem)

    Lines 295-333

    Purpose:

    Solve a problem using step-by-step reasoning.
    
    Args:
        problem: Problem to solve
    
    Returns:
        Step-by-step solution
    

    Parameters: – problem: Problem

    Returns: str

    debug_code_step_by_step(self, code, error)

    Lines 335-374

    Purpose:

    Debug code using systematic step-by-step approach.
    
    Args:
        code: Code with error
        error: Error message
    
    Returns:
        Step-by-step debugging process
    

    Parameters: – code: Code – error: Error

    Returns: str

    create_tutorial_step_by_step(self, topic, audience)

    Lines 376-409

    Purpose:

    Create a tutorial with step-by-step instructions.
    
    Args:
        topic: Topic to teach
        audience: Target audience level
    
    Returns:
        Step-by-step tutorial
    

    Parameters: – topic: Topic – audience: Audience

    Returns: str

    analyze_data_step_by_step(self, data_description, analysis_goal)

    Lines 411-451

    Purpose:

    Perform data analysis using step-by-step methodology.
    
    Args:
        data_description: Description of the data
        analysis_goal: Goal of the analysis
    
    Returns:
        Step-by-step analysis process
    

    Parameters: – data_description: Data description – analysis_goal: Analysis goal

    Returns: str

    decision_framework_step_by_step(self, decision, context)

    Lines 453-490

    Purpose:

    Apply decision-making framework step-by-step.
    
    Args:
        decision: Decision to make
        context: Context and constraints
    
    Returns:
        Step-by-step decision analysis
    

    Parameters: – decision: Decision – context: Context

    Returns: str

    troubleshoot_step_by_step(self, problem_description, system)

    Lines 492-533

    Purpose:

    Troubleshoot a problem using systematic steps.
    
    Args:
        problem_description: Description of the problem
        system: System or domain
    
    Returns:
        Step-by-step troubleshooting guide
    

    Parameters: – problem_description: Problem description – system: System

    Returns: str


    Functions

    demonstrate_step_by_step()

    Lines 536-710

    Purpose:

    Demonstrate step-by-step instruction patterns
    

    Key Concepts Summary

    Pattern Overview

    The Step By Step pattern provides a structured approach to building AI agents with specific capabilities and behaviors. This implementation leverages LangChain’s composable components to create a robust and maintainable solution.

    Core Components

    1. Agent Class: Main implementation of the pattern logic
    2. Prompt Engineering: Carefully crafted prompts to guide LLM behavior
    3. Tool Integration: Optional tools for extended functionality
    4. Memory Management: State handling across interactions
    5. Output Parsing: Structured response processing

    When to Use

    ✅ Good for: – Scenarios requiring this specific pattern’s strengths – Applications needing structured agent behavior – Systems where explainability is important – Production environments requiring reliability

    ❌ Not ideal for: – Simple single-prompt tasks – Scenarios requiring different pattern characteristics – Resource-constrained environments (if computationally intensive)


    Best Practices Demonstrated

    1. Environment Configuration

    • ✅ Use .env files for API keys
    • ✅ Never hardcode credentials
    • ✅ Validate environment variables on startup

    2. Error Handling

    • ✅ Graceful degradation on failures
    • ✅ Informative error messages
    • ✅ Logging for debugging

    3. Prompt Engineering

    • ✅ Clear, specific instructions
    • ✅ Structured output formats
    • ✅ Few-shot examples when beneficial

    4. Code Organization

    • ✅ Separation of concerns (class-based design)
    • ✅ Type hints for clarity
    • ✅ Docstrings for documentation

    5. Performance Optimization

    • ✅ Appropriate temperature settings
    • ✅ Token budget management
    • ✅ Caching where applicable

    LangChain Components Used

    • ChatOpenAI: OpenAI model interface
    • Chains: Sequential component composition

    Why LangChain? – Composable abstractions for LLM applications – Rich ecosystem of integrations – Production-ready patterns – Active community and development


    Execution Flow

    1. Initialize agent with configuration
       ↓
    2. Load environment and dependencies
       ↓
    3. Prepare prompt with input data
       ↓
    4. Invoke LLM through LangChain
       ↓
    5. Parse and validate response
       ↓
    6. Return formatted result
    

    Key Decision Points: – Model selection (temperature, model type) – Prompt structure (few-shot vs zero-shot) – Error handling strategy – Output format validation


    Performance Considerations

    Computational Cost

    • LLM Calls: Primary cost factor (based on tokens)
    • Latency: Depends on model and input size
    • Memory: Minimal for most patterns

    Optimization Strategies

    1. Prompt Optimization: Minimize tokens while maintaining clarity
    2. Caching: Cache repeated queries/responses
    3. Model Selection: Use smaller models when appropriate
    4. Batch Processing: Group similar requests
    5. Streaming: For long responses, stream tokens

    Cost Estimates (Approximate)

    • GPT-3.5-turbo: ~$0.002 per request
    • GPT-4: ~$0.03 per request
    • GPT-4-turbo: ~$0.01 per request

    Actual costs vary based on input/output token counts


    Common Pitfalls to Avoid

    1. Prompt Design Issues

    ❌ Vague or ambiguous instructions
    ✅ Clear, specific, testable prompts

    2. Error Handling

    ❌ Ignoring API errors or rate limits
    ✅ Implement retry logic and graceful degradation

    3. Token Management

    ❌ Exceeding model token limits
    ✅ Truncate inputs, chunk processing

    4. Security

    ❌ Exposing API keys in code
    ✅ Use environment variables

    5. Testing

    ❌ Only testing happy paths
    ✅ Test edge cases, failures, malformed inputs

    6. Cost Management

    ❌ Unmonitored API usage
    ✅ Implement budgets, alerts, and monitoring


    Further Improvements

    Production Enhancements

    1. Monitoring: Add logging, metrics, tracing
    2. Validation: Input/output schema validation
    3. Rate Limiting: Implement request throttling
    4. Caching: Redis or in-memory cache for responses
    5. Async Support: Non-blocking operations for scalability

    Feature Extensions

    1. Multi-Model Support: Switch between providers
    2. Fallback Models: Graceful degradation strategy
    3. Custom Tools: Domain-specific function calling
    4. Advanced Memory: Vector stores for long-term memory
    5. Evaluation: Automated testing and benchmarking

    Code Quality

    1. Testing: Unit tests, integration tests
    2. Documentation: API docs, usage examples
    3. Type Safety: Stricter type hints
    4. Configuration: Externalized config files

    Related Patterns

    • Pattern 084
    • Pattern 086

    Pattern Families: – Reasoning Patterns (001-011) – Multi-Agent Patterns (012-019) – Tool Use Patterns (020-025) – Memory Patterns (026-031) – And many more…


    Summary

    The Step By Step pattern provides a powerful approach to building AI agents with specific capabilities. This implementation demonstrates:

    ✅ Key Strengths: – Clean, maintainable code structure – LangChain integration for composability – Type hints and documentation – Production-ready patterns

    🎯 Best Use Cases: – Applications requiring this pattern’s characteristics – Systems needing structured agent behavior – Production environments requiring reliability

    📚 Learning Resources: – LangChain Documentation: https://python.langchain.com/ – Pattern Documentation: See related patterns – OpenAI API Reference: https://platform.openai.com/docs/


    File: 085_step_by_step.py
    Total Lines: 715
    Pattern Category: AI Agent Design Pattern #085


    Generated from actual implementation code
    For educational and reference purposes