Tag: llm

  • Agentic Design Pattern: Error Classification

    
        """
    Pattern 148: Error Classification & Routing
    
    Description:
        Implements intelligent error classification and routing to appropriate
        handlers based on error type, severity, and context. This pattern categorizes
        errors and routes them to specialized recovery strategies, enabling more
        sophisticated error handling than simple try-catch blocks.
    
    Components:
        - Error Classifier: Categorizes errors by type and severity
        - Error Router: Routes errors to appropriate handlers
        - Handler Registry: Maps error types to handlers
        - Context Analyzer: Extracts error context
        - Recovery Strategies: Specialized recovery for each error type
        - Escalation Manager: Escalates unhandled errors
    
    Use Cases:
        - Complex agent error handling
        - Multi-service application error management
        - Production system fault tolerance
        - Intelligent error recovery
        - Error analytics and monitoring
    
    Benefits:
        - Targeted error recovery
        - Better error handling granularity
        - Reduced error resolution time
        - Improved system reliability
        - Better debugging and monitoring
    
    Trade-offs:
        - Increased complexity
        - Requires comprehensive error taxonomy
        - Handler maintenance overhead
        - Potential routing errors
    
    LangChain Implementation:
        Uses error classification, pattern matching, and routing to specialized
        handlers for intelligent error management.
    """
    
    import os
    import re
    import traceback
    from typing import Any, Callable, Optional, List, Dict, Type
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    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
    
    load_dotenv()
    
    
    class ErrorCategory(Enum):
        """High-level error categories"""
        TRANSIENT = "transient"  # Temporary, retryable
        PERMANENT = "permanent"  # Persistent, not retryable
        RESOURCE = "resource"  # Resource-related (memory, disk, etc.)
        RATE_LIMIT = "rate_limit"  # API rate limiting
        AUTHENTICATION = "authentication"  # Auth/permission errors
        VALIDATION = "validation"  # Input validation errors
        TIMEOUT = "timeout"  # Timeout errors
        NETWORK = "network"  # Network connectivity
        BUSINESS_LOGIC = "business_logic"  # Business rule violations
        UNKNOWN = "unknown"  # Unclassified
    
    
    class ErrorSeverity(Enum):
        """Error severity levels"""
        LOW = "low"  # Minor, can be ignored
        MEDIUM = "medium"  # Important, should be handled
        HIGH = "high"  # Critical, requires immediate attention
        CRITICAL = "critical"  # System-level, may need escalation
    
    
    @dataclass
    class ErrorContext:
        """Context information about an error"""
        exception: Exception
        exception_type: str
        message: str
        traceback_str: str
        timestamp: datetime
        operation: Optional[str] = None
        user_id: Optional[str] = None
        request_id: Optional[str] = None
        additional_data: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class ClassifiedError:
        """Classified error with category and severity"""
        context: ErrorContext
        category: ErrorCategory
        severity: ErrorSeverity
        retryable: bool
        suggested_action: str
        confidence: float = 1.0
    
    
    @dataclass
    class ErrorStats:
        """Statistics about error handling"""
        total_errors: int = 0
        by_category: Dict[ErrorCategory, int] = field(default_factory=lambda: defaultdict(int))
        by_severity: Dict[ErrorSeverity, int] = field(default_factory=lambda: defaultdict(int))
        by_handler: Dict[str, int] = field(default_factory=lambda: defaultdict(int))
        successful_recoveries: int = 0
        failed_recoveries: int = 0
        escalations: int = 0
    
    
    class ErrorClassificationRouter:
        """
        Agent that classifies and routes errors to appropriate handlers.
        
        Analyzes errors, categorizes them, and routes to specialized recovery
        strategies for intelligent error management.
        """
        
        def __init__(self, model: str = "gpt-3.5-turbo"):
            """
            Initialize the error classification router.
            
            Args:
                model: LLM model to use
            """
            self.llm = ChatOpenAI(model=model, temperature=0)
            
            self.handlers: Dict[ErrorCategory, Callable] = {}
            self.classification_rules: List[Dict[str, Any]] = []
            self.stats = ErrorStats()
            
            # Set up default classification rules
            self._setup_default_rules()
        
        def _setup_default_rules(self):
            """Set up default error classification rules."""
            self.classification_rules = [
                {
                    "patterns": ["rate limit", "429", "too many requests"],
                    "category": ErrorCategory.RATE_LIMIT,
                    "severity": ErrorSeverity.MEDIUM,
                    "retryable": True,
                    "action": "Retry with exponential backoff"
                },
                {
                    "patterns": ["timeout", "timed out", "deadline exceeded"],
                    "category": ErrorCategory.TIMEOUT,
                    "severity": ErrorSeverity.MEDIUM,
                    "retryable": True,
                    "action": "Retry with increased timeout"
                },
                {
                    "patterns": ["connection", "network", "unreachable", "503"],
                    "category": ErrorCategory.NETWORK,
                    "severity": ErrorSeverity.MEDIUM,
                    "retryable": True,
                    "action": "Check network and retry"
                },
                {
                    "patterns": ["authentication", "unauthorized", "401", "403"],
                    "category": ErrorCategory.AUTHENTICATION,
                    "severity": ErrorSeverity.HIGH,
                    "retryable": False,
                    "action": "Verify credentials"
                },
                {
                    "patterns": ["validation", "invalid", "bad request", "400"],
                    "category": ErrorCategory.VALIDATION,
                    "severity": ErrorSeverity.MEDIUM,
                    "retryable": False,
                    "action": "Fix input and retry"
                },
                {
                    "patterns": ["out of memory", "memory error", "allocation failed"],
                    "category": ErrorCategory.RESOURCE,
                    "severity": ErrorSeverity.HIGH,
                    "retryable": False,
                    "action": "Reduce resource usage or scale up"
                },
                {
                    "patterns": ["disk full", "no space left", "quota exceeded"],
                    "category": ErrorCategory.RESOURCE,
                    "severity": ErrorSeverity.HIGH,
                    "retryable": False,
                    "action": "Free up space or increase quota"
                }
            ]
        
        def classify_error(self, error: Exception, **context) -> ClassifiedError:
            """
            Classify an error based on type and message.
            
            Args:
                error: Exception to classify
                **context: Additional context
                
            Returns:
                Classified error with category and severity
            """
            self.stats.total_errors += 1
            
            # Create error context
            error_context = ErrorContext(
                exception=error,
                exception_type=type(error).__name__,
                message=str(error),
                traceback_str=traceback.format_exc(),
                timestamp=datetime.now(),
                operation=context.get("operation"),
                user_id=context.get("user_id"),
                request_id=context.get("request_id"),
                additional_data=context
            )
            
            # Try rule-based classification first
            classified = self._classify_by_rules(error_context)
            
            if classified.category == ErrorCategory.UNKNOWN:
                # Fall back to LLM-based classification
                classified = self._classify_by_llm(error_context)
            
            # Update stats
            self.stats.by_category[classified.category] += 1
            self.stats.by_severity[classified.severity] += 1
            
            return classified
        
        def _classify_by_rules(self, context: ErrorContext) -> ClassifiedError:
            """Classify error using rule-based matching."""
            error_str = f"{context.exception_type} {context.message}".lower()
            
            for rule in self.classification_rules:
                if any(pattern in error_str for pattern in rule["patterns"]):
                    return ClassifiedError(
                        context=context,
                        category=rule["category"],
                        severity=rule["severity"],
                        retryable=rule["retryable"],
                        suggested_action=rule["action"],
                        confidence=1.0
                    )
            
            # No rule matched
            return ClassifiedError(
                context=context,
                category=ErrorCategory.UNKNOWN,
                severity=ErrorSeverity.MEDIUM,
                retryable=False,
                suggested_action="Investigate error",
                confidence=0.0
            )
        
        def _classify_by_llm(self, context: ErrorContext) -> ClassifiedError:
            """Classify error using LLM."""
            prompt = ChatPromptTemplate.from_messages([
                ("system", "You are an expert in error classification. "
                          "Classify the error into one of these categories:\n"
                          "- transient: Temporary, can be retried\n"
                          "- permanent: Persistent, cannot be retried\n"
                          "- resource: Resource-related (memory, disk)\n"
                          "- rate_limit: API rate limiting\n"
                          "- authentication: Auth/permission errors\n"
                          "- validation: Input validation errors\n"
                          "- timeout: Timeout errors\n"
                          "- network: Network connectivity\n"
                          "- business_logic: Business rule violations\n\n"
                          "Also determine severity (low/medium/high/critical) "
                          "and if it's retryable (yes/no).\n\n"
                          "Respond in format: category|severity|retryable|action"),
                ("user", "Error Type: {error_type}\n"
                        "Message: {message}\n"
                        "Context: {operation}\n\n"
                        "Classify this error:")
            ])
            
            try:
                chain = prompt | self.llm | StrOutputParser()
                result = chain.invoke({
                    "error_type": context.exception_type,
                    "message": context.message,
                    "operation": context.operation or "unknown"
                })
                
                # Parse result
                parts = result.split("|")
                if len(parts) >= 4:
                    category = ErrorCategory(parts[0].strip().lower())
                    severity = ErrorSeverity(parts[1].strip().lower())
                    retryable = "yes" in parts[2].lower()
                    action = parts[3].strip()
                    
                    return ClassifiedError(
                        context=context,
                        category=category,
                        severity=severity,
                        retryable=retryable,
                        suggested_action=action,
                        confidence=0.8
                    )
                    
            except Exception as e:
                print(f"LLM classification failed: {e}")
            
            # Fallback
            return ClassifiedError(
                context=context,
                category=ErrorCategory.UNKNOWN,
                severity=ErrorSeverity.MEDIUM,
                retryable=False,
                suggested_action="Manual investigation required",
                confidence=0.5
            )
        
        def register_handler(
            self,
            category: ErrorCategory,
            handler: Callable[[ClassifiedError], Any]
        ):
            """
            Register a handler for an error category.
            
            Args:
                category: Error category
                handler: Handler function
            """
            self.handlers[category] = handler
        
        def route_error(self, classified_error: ClassifiedError) -> Any:
            """
            Route error to appropriate handler.
            
            Args:
                classified_error: Classified error
                
            Returns:
                Handler result
            """
            category = classified_error.category
            
            if category in self.handlers:
                handler = self.handlers[category]
                handler_name = handler.__name__
                
                print(f"\n🔀 Routing to handler: {handler_name}")
                self.stats.by_handler[handler_name] += 1
                
                try:
                    result = handler(classified_error)
                    self.stats.successful_recoveries += 1
                    return result
                except Exception as e:
                    print(f"❌ Handler failed: {e}")
                    self.stats.failed_recoveries += 1
                    return self._default_handler(classified_error)
            else:
                print(f"⚠️  No handler for category: {category.value}")
                return self._default_handler(classified_error)
        
        def _default_handler(self, classified_error: ClassifiedError) -> Dict[str, Any]:
            """Default handler for unhandled errors."""
            if classified_error.severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]:
                print(f"🚨 Escalating {classified_error.severity.value} severity error")
                self.stats.escalations += 1
            
            return {
                "handled": False,
                "category": classified_error.category.value,
                "severity": classified_error.severity.value,
                "action": classified_error.suggested_action,
                "escalated": classified_error.severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]
            }
        
        def handle_error(self, error: Exception, **context) -> Any:
            """
            Complete error handling: classify and route.
            
            Args:
                error: Exception to handle
                **context: Additional context
                
            Returns:
                Handler result
            """
            # Classify error
            classified = self.classify_error(error, **context)
            
            print(f"\n📋 Error Classification:")
            print(f"   Category: {classified.category.value}")
            print(f"   Severity: {classified.severity.value}")
            print(f"   Retryable: {classified.retryable}")
            print(f"   Confidence: {classified.confidence:.1%}")
            print(f"   Suggested Action: {classified.suggested_action}")
            
            # Route to handler
            result = self.route_error(classified)
            
            return result
        
        def get_stats_report(self) -> str:
            """Generate error statistics report."""
            report = []
            report.append("\n" + "="*60)
            report.append("ERROR CLASSIFICATION & ROUTING STATISTICS")
            report.append("="*60)
            
            report.append(f"\n📊 Overall Statistics:")
            report.append(f"   Total Errors: {self.stats.total_errors}")
            report.append(f"   Successful Recoveries: {self.stats.successful_recoveries}")
            report.append(f"   Failed Recoveries: {self.stats.failed_recoveries}")
            report.append(f"   Escalations: {self.stats.escalations}")
            
            if self.stats.total_errors > 0:
                recovery_rate = (
                    self.stats.successful_recoveries / self.stats.total_errors
                )
                report.append(f"   Recovery Rate: {recovery_rate:.1%}")
            
            report.append(f"\n📂 By Category:")
            for category, count in sorted(
                self.stats.by_category.items(),
                key=lambda x: x[1],
                reverse=True
            ):
                report.append(f"   {category.value}: {count}")
            
            report.append(f"\n⚠️  By Severity:")
            for severity, count in sorted(
                self.stats.by_severity.items(),
                key=lambda x: x[1],
                reverse=True
            ):
                report.append(f"   {severity.value}: {count}")
            
            if self.stats.by_handler:
                report.append(f"\n🔧 By Handler:")
                for handler, count in sorted(
                    self.stats.by_handler.items(),
                    key=lambda x: x[1],
                    reverse=True
                ):
                    report.append(f"   {handler}: {count}")
            
            report.append("\n" + "="*60)
            
            return "\n".join(report)
    
    
    def demonstrate_error_classification_routing():
        """Demonstrate the Error Classification & Routing pattern."""
        print("="*60)
        print("ERROR CLASSIFICATION & ROUTING PATTERN DEMONSTRATION")
        print("="*60)
        
        router = ErrorClassificationRouter()
        
        # Example 1: Register handlers for different error types
        print("\n" + "="*60)
        print("Example 1: Register Specialized Error Handlers")
        print("="*60)
        
        def handle_rate_limit(error: ClassifiedError) -> Dict:
            """Handler for rate limit errors."""
            print("   💤 Handling rate limit: waiting before retry...")
            return {"action": "retry_after_delay", "delay": 60}
        
        def handle_authentication(error: ClassifiedError) -> Dict:
            """Handler for authentication errors."""
            print("   🔐 Handling auth error: refreshing credentials...")
            return {"action": "refresh_credentials"}
        
        def handle_validation(error: ClassifiedError) -> Dict:
            """Handler for validation errors."""
            print("   ✏️  Handling validation error: correcting input...")
            return {"action": "fix_input", "message": error.context.message}
        
        def handle_network(error: ClassifiedError) -> Dict:
            """Handler for network errors."""
            print("   🌐 Handling network error: checking connection...")
            return {"action": "check_network_and_retry"}
        
        # Register handlers
        router.register_handler(ErrorCategory.RATE_LIMIT, handle_rate_limit)
        router.register_handler(ErrorCategory.AUTHENTICATION, handle_authentication)
        router.register_handler(ErrorCategory.VALIDATION, handle_validation)
        router.register_handler(ErrorCategory.NETWORK, handle_network)
        
        print("✅ Registered 4 specialized handlers")
        
        # Example 2: Classify and route various errors
        print("\n" + "="*60)
        print("Example 2: Classify and Route Various Errors")
        print("="*60)
        
        test_errors = [
            (Exception("Rate limit exceeded (429)"), "api_call"),
            (Exception("Connection timeout after 30s"), "database_query"),
            (Exception("Invalid API key provided"), "authentication"),
            (Exception("Invalid input: email format incorrect"), "user_registration"),
            (Exception("Network unreachable"), "external_service"),
        ]
        
        for error, operation in test_errors:
            print(f"\n{'='*60}")
            print(f"Error: {error}")
            print(f"Operation: {operation}")
            
            result = router.handle_error(error, operation=operation)
            print(f"Result: {result}")
        
        # Example 3: LLM-based classification for unknown errors
        print("\n" + "="*60)
        print("Example 3: LLM-Based Classification")
        print("="*60)
        
        unknown_error = Exception(
            "Failed to process payment: insufficient funds in account"
        )
        
        print(f"\nClassifying unknown error: {unknown_error}")
        classified = router.classify_error(
            unknown_error,
            operation="payment_processing"
        )
        
        print(f"\n📋 LLM Classification:")
        print(f"   Category: {classified.category.value}")
        print(f"   Severity: {classified.severity.value}")
        print(f"   Confidence: {classified.confidence:.1%}")
        print(f"   Suggested Action: {classified.suggested_action}")
        
        # Example 4: Error severity-based routing
        print("\n" + "="*60)
        print("Example 4: Severity-Based Escalation")
        print("="*60)
        
        severity_errors = [
            (Exception("Minor warning: cache miss"), ErrorSeverity.LOW),
            (Exception("API timeout on retry"), ErrorSeverity.MEDIUM),
            (Exception("Database connection pool exhausted"), ErrorSeverity.HIGH),
            (Exception("Critical system failure: all services down"), ErrorSeverity.CRITICAL),
        ]
        
        print("\nProcessing errors by severity:")
        for error, expected_severity in severity_errors:
            classified = router.classify_error(error)
            severity = classified.severity
            
            status = "🔴" if severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL] else "🟡"
            print(f"\n{status} {severity.value.upper()}: {error}")
            
            if severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]:
                print(f"   ⚠️  Escalating to on-call team")
        
        # Example 5: Context-aware classification
        print("\n" + "="*60)
        print("Example 5: Context-Aware Error Classification")
        print("="*60)
        
        context_error = Exception("Operation failed")
        
        contexts = [
            {"operation": "payment_processing", "user_id": "user123", "amount": 1000},
            {"operation": "cache_refresh", "service": "recommendations"},
            {"operation": "user_login", "user_id": "admin", "ip": "192.168.1.1"}
        ]
        
        print("\nSame error in different contexts:")
        for ctx in contexts:
            print(f"\nContext: {ctx['operation']}")
            classified = router.classify_error(context_error, **ctx)
            print(f"   Severity: {classified.severity.value}")
            print(f"   Action: {classified.suggested_action}")
        
        # Example 6: Error pattern analysis
        print("\n" + "="*60)
        print("Example 6: Error Pattern Analysis")
        print("="*60)
        
        # Simulate multiple errors
        simulated_errors = [
            Exception("Rate limit exceeded"),
            Exception("Rate limit exceeded"),
            Exception("Rate limit exceeded"),
            Exception("Connection timeout"),
            Exception("Connection timeout"),
            Exception("Invalid credentials"),
        ]
        
        print("\nProcessing batch of errors:")
        for i, error in enumerate(simulated_errors, 1):
            router.handle_error(error, request_id=f"req-{i}")
        
        # Generate statistics
        stats_report = router.get_stats_report()
        print(stats_report)
        
        # Identify patterns
        print("\n💡 Pattern Analysis:")
        if router.stats.by_category[ErrorCategory.RATE_LIMIT] >= 3:
            print("   ⚠️  High rate of rate limit errors detected")
            print("   💡 Recommendation: Implement request throttling")
        
        if router.stats.by_category[ErrorCategory.NETWORK] >= 2:
            print("   ⚠️  Multiple network errors detected")
            print("   💡 Recommendation: Check service health")
        
        # Summary
        print("\n" + "="*60)
        print("SUMMARY")
        print("="*60)
        print("""
    The Error Classification & Routing pattern demonstrates:
    
    1. Intelligent Classification: Categorizes errors by type and severity
    2. Specialized Handlers: Routes errors to appropriate recovery strategies
    3. Rule-Based + LLM: Combines rule matching with AI classification
    4. Context Awareness: Considers operation context in classification
    5. Escalation Logic: Escalates severe errors automatically
    
    Key Benefits:
    - Targeted error recovery (80-95% automation typical)
    - Reduced mean time to recovery (MTTR)
    - Better error visibility and monitoring
    - Intelligent escalation of critical issues
    - Comprehensive error analytics
    
    Error Categories:
    - Transient: Temporary, retry with backoff
    - Permanent: Persistent, manual intervention
    - Rate Limit: Backoff and respect limits
    - Authentication: Refresh credentials
    - Validation: Fix input data
    - Resource: Scale or optimize
    - Network: Check connectivity
    - Timeout: Increase limits or optimize
    
    Handler Strategies:
    - Retry: For transient errors
    - Refresh: For authentication issues
    - Validate: For input errors
    - Scale: For resource errors
    - Alert: For critical errors
    - Log: For analysis and debugging
    
    Best Practices:
    - Maintain comprehensive error taxonomy
    - Use rule-based classification first (fast)
    - Fall back to LLM for unknown errors
    - Log all errors with full context
    - Monitor error patterns over time
    - Update handlers based on learnings
    - Test handlers thoroughly
    - Document recovery procedures
    
    Severity Escalation:
    - Low: Log only
    - Medium: Automated recovery
    - High: Alert and automated recovery
    - Critical: Immediate escalation to on-call
    
    Use Cases:
    - Production system error handling
    - Multi-service applications
    - API gateway error management
    - Microservices fault tolerance
    - Distributed system coordination
        """)
    
    
    if __name__ == "__main__":
        demonstrate_error_classification_routing()
    
        

    Agentic Design Pattern: Error Classification

    Pattern 148: Error Classification – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Error Classification pattern implementation.


    Pattern Description

    Pattern 148: Error Classification & Routing

    Description: Implements intelligent error classification and routing to appropriate handlers based on error type, severity, and context. This pattern categorizes errors and routes them to specialized recovery strategies, enabling more sophisticated error handling than simple try-catch blocks.

    Components: – Error Classifier: Categorizes errors by type and severity – Error Router: Routes errors to appropriate handlers – Handler Registry: Maps error types to handlers – Context Analyzer: Extracts error context – Recovery Strategies: Specialized recovery for each error type – Escalation Manager: Escalates unhandled errors

    Use Cases: – Complex agent error handling – Multi-service application error management – Production system fault tolerance – Intelligent error recovery – Error analytics and monitoring

    Benefits: – Targeted error recovery – Better error handling granularity – Reduced error resolution time – Improved system reliability – Better debugging and monitoring

    Trade-offs: – Increased complexity – Requires comprehensive error taxonomy – Handler maintenance overhead – Potential routing errors

    LangChain Implementation: Uses error classification, pattern matching, and routing to specialized handlers for intelligent error management.


    File Statistics

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

    Module Documentation

    """
    Pattern 148: Error Classification & Routing
    
    Description:
        Implements intelligent error classification and routing to appropriate
        handlers based on error type, severity, and context. This pattern categorizes
        errors and routes them to specialized recovery strategies, enabling more
        sophisticated error handling than simple try-catch blocks.
    
    Components:
        - Error Classifier: Categorizes errors by type and severity
        - Error Router: Routes errors to appropriate handlers
        - Handler Registry: Maps error types to handlers
        - Context Analyzer: Extracts error context
        - Recovery Strategies: Specialized recovery for each error type
        - Escalation Manager: Escalates unhandled errors
    
    Use Cases:
        - Complex agent error handling
        - Multi-service application error management
        - Production system fault tolerance
        - Intelligent error recovery
        - Error analytics and monitoring
    
    Benefits:
        - Targeted error recovery
        - Better error handling granularity
        - Reduced error resolution time
        - Improved system reliability
        - Better debugging and monitoring
    
    Trade-offs:
        - Increased complexity
        - Requires comprehensive error taxonomy
        - Handler maintenance overhead
        - Potential routing errors
    
    LangChain Implementation:
        Uses error classification, pattern matching, and routing to specialized
        handlers for intelligent error management.
    """
    

    Purpose: This module implements the Error Classification 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 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
    import typing.Type
    

    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 – 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) – typing.Type: 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 traceback
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: ErrorCategory

    Lines 60-71

    Inherits from: Enum

    Purpose:

    High-level error categories
    

    Class: ErrorSeverity

    Lines 74-79

    Inherits from: Enum

    Purpose:

    Error severity levels
    

    Class: ErrorContext

    Lines 83-93

    Purpose:

    Context information about an error
    

    Class: ClassifiedError

    Lines 97-104

    Purpose:

    Classified error with category and severity
    

    Class: ErrorStats

    Lines 108-116

    Purpose:

    Statistics about error handling
    

    Class: ErrorClassificationRouter

    Lines 119-453

    Purpose:

    Agent that classifies and routes errors to appropriate handlers.
    
    Analyzes errors, categorizes them, and routes to specialized recovery
    strategies for intelligent error management.
    

    Methods:

    __init__(self, model)

    Lines 127-141

    Purpose:

    Initialize the error classification router.
    
    Args:
        model: LLM model to use
    

    Parameters: – model: Language model instance

    _setup_default_rules(self)

    Lines 143-195

    Purpose:

    Set up default error classification rules.
    

    Parameters:

    classify_error(self, error)

    Lines 197-234

    Purpose:

    Classify an error based on type and message.
    
    Args:
        error: Exception to classify
        **context: Additional context
    
    Returns:
        Classified error with category and severity
    

    Parameters: – error: Error

    Returns: ClassifiedError

    _classify_by_rules(self, context)

    Lines 236-259

    Purpose:

    Classify error using rule-based matching.
    

    Parameters: – context: Context

    Returns: ClassifiedError

    _classify_by_llm(self, context)

    Lines 261-320

    Purpose:

    Classify error using LLM.
    

    Parameters: – context: Context

    Returns: ClassifiedError

    register_handler(self, category, handler)

    Lines 322-334

    Purpose:

    Register a handler for an error category.
    
    Args:
        category: Error category
        handler: Handler function
    

    Parameters: – category: Category – handler: Handler

    route_error(self, classified_error)

    Lines 336-365

    Purpose:

    Route error to appropriate handler.
    
    Args:
        classified_error: Classified error
    
    Returns:
        Handler result
    

    Parameters: – classified_error: Classified error

    Returns: Any

    _default_handler(self, classified_error)

    Lines 367-379

    Purpose:

    Default handler for unhandled errors.
    

    Parameters: – classified_error: Classified error

    Returns: <ast.Subscript object at 0x000001A06D7CEBD0>

    handle_error(self, error)

    Lines 381-405

    Purpose:

    Complete error handling: classify and route.
    
    Args:
        error: Exception to handle
        **context: Additional context
    
    Returns:
        Handler result
    

    Parameters: – error: Error

    Returns: Any

    get_stats_report(self)

    Lines 407-453

    Purpose:

    Generate error statistics report.
    

    Parameters:

    Returns: str


    Functions

    demonstrate_error_classification_routing()

    Lines 456-675

    Purpose:

    Demonstrate the Error Classification & Routing pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Error Classification 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 147
    • Pattern 149

    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 Error Classification 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: 148_error_classification.py
    Total Lines: 680
    Pattern Category: AI Agent Design Pattern #148


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Compensating Actions

    
        """
    Pattern 147: Compensating Actions
    
    Description:
        Implements compensating actions for rolling back completed operations when
        a workflow fails. This pattern maintains consistency by undoing successful
        steps in reverse order, similar to database transaction rollback but for
        complex multi-step agent workflows.
    
    Components:
        - Action Logger: Records completed actions
        - Compensation Registry: Maps actions to compensating actions
        - Rollback Coordinator: Orchestrates compensation execution
        - State Tracker: Maintains workflow state
        - Failure Handler: Detects and triggers compensation
        - Recovery Monitor: Tracks compensation success
    
    Use Cases:
        - Multi-step agent workflows with side effects
        - API orchestration with multiple services
        - Data pipeline error recovery
        - Transaction-like behavior for agent operations
        - Distributed system coordination
    
    Benefits:
        - Maintains system consistency
        - Automatic error recovery
        - Clear rollback semantics
        - Reduced manual cleanup
        - Better fault tolerance
    
    Trade-offs:
        - Complexity overhead
        - Not all actions are reversible
        - Potential partial rollback states
        - Resource consumption during compensation
    
    LangChain Implementation:
        Uses action tracking, compensation functions, and rollback orchestration
        to implement saga-like patterns for agent workflows.
    """
    
    import os
    import time
    from typing import Any, Callable, Optional, List, Dict, Tuple
    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_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class ActionStatus(Enum):
        """Status of an action"""
        PENDING = "pending"
        EXECUTING = "executing"
        COMPLETED = "completed"
        FAILED = "failed"
        COMPENSATING = "compensating"
        COMPENSATED = "compensated"
        COMPENSATION_FAILED = "compensation_failed"
    
    
    @dataclass
    class Action:
        """Represents a workflow action"""
        action_id: str
        name: str
        execute_fn: Callable
        compensate_fn: Optional[Callable] = None
        args: Tuple = field(default_factory=tuple)
        kwargs: Dict = field(default_factory=dict)
        status: ActionStatus = ActionStatus.PENDING
        result: Any = None
        error: Optional[str] = None
        executed_at: Optional[datetime] = None
        compensated_at: Optional[datetime] = None
        idempotent: bool = False
    
    
    @dataclass
    class WorkflowExecution:
        """Tracks workflow execution"""
        workflow_id: str
        actions: List[Action]
        current_index: int = 0
        completed_actions: List[Action] = field(default_factory=list)
        failed_action: Optional[Action] = None
        compensation_results: List[Tuple[Action, bool]] = field(default_factory=list)
        start_time: datetime = field(default_factory=datetime.now)
        end_time: Optional[datetime] = None
        success: bool = False
    
    
    class CompensatingActionsAgent:
        """
        Agent that implements compensating actions for workflow rollback.
        
        Executes multi-step workflows and automatically compensates completed
        actions when failures occur, maintaining system consistency.
        """
        
        def __init__(self, model: str = "gpt-3.5-turbo"):
            """
            Initialize the compensating actions agent.
            
            Args:
                model: LLM model to use
            """
            self.llm = ChatOpenAI(model=model, temperature=0.7)
            
            self.workflows: Dict[str, WorkflowExecution] = {}
            self.compensation_registry: Dict[str, Callable] = {}
        
        def register_compensation(
            self,
            action_name: str,
            compensate_fn: Callable
        ):
            """
            Register a compensation function for an action type.
            
            Args:
                action_name: Name of the action
                compensate_fn: Function to compensate the action
            """
            self.compensation_registry[action_name] = compensate_fn
        
        def execute_workflow(
            self,
            workflow_id: str,
            actions: List[Action]
        ) -> WorkflowExecution:
            """
            Execute a workflow with compensating action support.
            
            Args:
                workflow_id: Unique workflow identifier
                actions: List of actions to execute
                
            Returns:
                Workflow execution result
            """
            execution = WorkflowExecution(
                workflow_id=workflow_id,
                actions=actions
            )
            
            self.workflows[workflow_id] = execution
            
            print(f"\n🚀 Starting workflow: {workflow_id}")
            print(f"   Total actions: {len(actions)}")
            
            try:
                # Execute actions sequentially
                for i, action in enumerate(actions):
                    execution.current_index = i
                    
                    print(f"\n📍 Executing action {i+1}/{len(actions)}: {action.name}")
                    
                    action.status = ActionStatus.EXECUTING
                    
                    try:
                        # Execute the action
                        start_time = time.time()
                        result = action.execute_fn(*action.args, **action.kwargs)
                        duration = time.time() - start_time
                        
                        action.result = result
                        action.status = ActionStatus.COMPLETED
                        action.executed_at = datetime.now()
                        
                        execution.completed_actions.append(action)
                        
                        print(f"   ✅ Completed in {duration:.2f}s")
                        if result:
                            print(f"   Result: {str(result)[:100]}")
                        
                    except Exception as e:
                        # Action failed - trigger compensation
                        action.status = ActionStatus.FAILED
                        action.error = str(e)
                        execution.failed_action = action
                        
                        print(f"   ❌ Failed: {e}")
                        print(f"\n🔄 Triggering compensation for {len(execution.completed_actions)} completed actions")
                        
                        self._compensate_actions(execution)
                        
                        execution.end_time = datetime.now()
                        execution.success = False
                        
                        return execution
                
                # All actions completed successfully
                execution.end_time = datetime.now()
                execution.success = True
                
                print(f"\n✅ Workflow completed successfully!")
                
                return execution
                
            except Exception as e:
                print(f"\n❌ Workflow execution error: {e}")
                execution.success = False
                execution.end_time = datetime.now()
                return execution
        
        def _compensate_actions(self, execution: WorkflowExecution):
            """
            Compensate completed actions in reverse order.
            
            Args:
                execution: Workflow execution to compensate
            """
            # Compensate in reverse order
            for action in reversed(execution.completed_actions):
                print(f"\n   🔙 Compensating: {action.name}")
                
                action.status = ActionStatus.COMPENSATING
                
                # Get compensation function
                compensate_fn = action.compensate_fn
                if not compensate_fn and action.name in self.compensation_registry:
                    compensate_fn = self.compensation_registry[action.name]
                
                if not compensate_fn:
                    print(f"      ⚠️  No compensation function available")
                    execution.compensation_results.append((action, False))
                    action.status = ActionStatus.COMPENSATION_FAILED
                    continue
                
                try:
                    # Execute compensation
                    compensate_fn(action.result)
                    action.status = ActionStatus.COMPENSATED
                    action.compensated_at = datetime.now()
                    execution.compensation_results.append((action, True))
                    
                    print(f"      ✅ Compensated successfully")
                    
                except Exception as e:
                    print(f"      ❌ Compensation failed: {e}")
                    action.status = ActionStatus.COMPENSATION_FAILED
                    execution.compensation_results.append((action, False))
        
        def get_workflow_status(self, workflow_id: str) -> Optional[WorkflowExecution]:
            """Get status of a workflow execution."""
            return self.workflows.get(workflow_id)
        
        def generate_compensation_report(
            self,
            workflow_id: str
        ) -> str:
            """
            Generate a detailed compensation report.
            
            Args:
                workflow_id: Workflow to report on
                
            Returns:
                Formatted report
            """
            execution = self.workflows.get(workflow_id)
            if not execution:
                return f"Workflow {workflow_id} not found"
            
            report = []
            report.append("\n" + "="*60)
            report.append(f"WORKFLOW COMPENSATION REPORT: {workflow_id}")
            report.append("="*60)
            
            report.append(f"\n📊 Execution Summary:")
            report.append(f"   Total Actions: {len(execution.actions)}")
            report.append(f"   Completed: {len(execution.completed_actions)}")
            report.append(f"   Status: {'✅ SUCCESS' if execution.success else '❌ FAILED'}")
            
            if execution.failed_action:
                report.append(f"\n⚠️  Failed Action:")
                report.append(f"   Name: {execution.failed_action.name}")
                report.append(f"   Error: {execution.failed_action.error}")
                report.append(f"   Position: {execution.current_index + 1}/{len(execution.actions)}")
            
            if execution.compensation_results:
                report.append(f"\n🔄 Compensation Results:")
                successful = sum(1 for _, success in execution.compensation_results if success)
                report.append(f"   Total Compensations: {len(execution.compensation_results)}")
                report.append(f"   Successful: {successful}")
                report.append(f"   Failed: {len(execution.compensation_results) - successful}")
                
                report.append(f"\n   Details:")
                for action, success in execution.compensation_results:
                    status = "✅" if success else "❌"
                    report.append(f"      {status} {action.name}")
            
            if execution.end_time:
                duration = (execution.end_time - execution.start_time).total_seconds()
                report.append(f"\n⏱️  Timing:")
                report.append(f"   Duration: {duration:.2f}s")
            
            report.append("\n" + "="*60)
            
            return "\n".join(report)
    
    
    def demonstrate_compensating_actions():
        """Demonstrate the Compensating Actions pattern."""
        print("="*60)
        print("COMPENSATING ACTIONS PATTERN DEMONSTRATION")
        print("="*60)
        
        agent = CompensatingActionsAgent()
        
        # Example 1: Simple workflow with compensation
        print("\n" + "="*60)
        print("Example 1: Simple Workflow with Successful Compensation")
        print("="*60)
        
        # Simulated state
        state = {
            "user_created": False,
            "email_sent": False,
            "database_updated": False
        }
        
        def create_user(user_id: str) -> str:
            """Create a user account."""
            print(f"      Creating user: {user_id}")
            state["user_created"] = True
            return f"User {user_id} created"
        
        def compensate_create_user(result: str):
            """Delete the created user."""
            print(f"      Deleting user from result: {result}")
            state["user_created"] = False
        
        def send_email(email: str) -> str:
            """Send welcome email."""
            print(f"      Sending email to: {email}")
            state["email_sent"] = True
            return f"Email sent to {email}"
        
        def compensate_send_email(result: str):
            """Send cancellation email."""
            print(f"      Sending cancellation email")
            state["email_sent"] = False
        
        def update_database(data: dict) -> str:
            """Update database - THIS WILL FAIL."""
            print(f"      Updating database with: {data}")
            raise Exception("Database connection failed")
        
        # Create workflow
        actions = [
            Action(
                action_id="1",
                name="create_user",
                execute_fn=create_user,
                compensate_fn=compensate_create_user,
                args=("user123",)
            ),
            Action(
                action_id="2",
                name="send_email",
                execute_fn=send_email,
                compensate_fn=compensate_send_email,
                args=("user@example.com",)
            ),
            Action(
                action_id="3",
                name="update_database",
                execute_fn=update_database,
                args=({"user": "user123"},)
            )
        ]
        
        execution = agent.execute_workflow("workflow-001", actions)
        
        print(f"\nFinal State:")
        print(f"   User Created: {state['user_created']}")
        print(f"   Email Sent: {state['email_sent']}")
        print(f"   Database Updated: {state['database_updated']}")
        
        report = agent.generate_compensation_report("workflow-001")
        print(report)
        
        # Example 2: Multi-step API workflow
        print("\n" + "="*60)
        print("Example 2: Multi-Step API Workflow")
        print("="*60)
        
        api_state = {
            "payment_id": None,
            "inventory_reserved": False,
            "shipping_scheduled": False
        }
        
        def process_payment(amount: float) -> str:
            """Process payment."""
            payment_id = f"PAY-{int(time.time())}"
            api_state["payment_id"] = payment_id
            print(f"      Payment processed: {payment_id} for ${amount}")
            return payment_id
        
        def compensate_payment(payment_id: str):
            """Refund payment."""
            print(f"      Refunding payment: {payment_id}")
            api_state["payment_id"] = None
        
        def reserve_inventory(item_id: str, quantity: int) -> str:
            """Reserve inventory."""
            api_state["inventory_reserved"] = True
            print(f"      Reserved {quantity} units of {item_id}")
            return f"Reserved {quantity} units"
        
        def compensate_inventory(result: str):
            """Release inventory reservation."""
            print(f"      Releasing inventory reservation")
            api_state["inventory_reserved"] = False
        
        def schedule_shipping(address: str) -> str:
            """Schedule shipping - WILL FAIL."""
            print(f"      Scheduling shipping to: {address}")
            raise Exception("Shipping service unavailable")
        
        actions = [
            Action(
                action_id="1",
                name="process_payment",
                execute_fn=process_payment,
                compensate_fn=compensate_payment,
                args=(99.99,)
            ),
            Action(
                action_id="2",
                name="reserve_inventory",
                execute_fn=reserve_inventory,
                compensate_fn=compensate_inventory,
                args=("ITEM-001", 2)
            ),
            Action(
                action_id="3",
                name="schedule_shipping",
                execute_fn=schedule_shipping,
                args=("123 Main St",)
            )
        ]
        
        execution = agent.execute_workflow("workflow-002", actions)
        
        print(f"\nFinal API State:")
        print(f"   Payment ID: {api_state['payment_id']}")
        print(f"   Inventory Reserved: {api_state['inventory_reserved']}")
        print(f"   Shipping Scheduled: {api_state['shipping_scheduled']}")
        
        # Example 3: Successful workflow (no compensation needed)
        print("\n" + "="*60)
        print("Example 3: Successful Workflow (No Compensation)")
        print("="*60)
        
        success_state = {"steps": []}
        
        def step1() -> str:
            success_state["steps"].append("step1")
            return "Step 1 completed"
        
        def step2() -> str:
            success_state["steps"].append("step2")
            return "Step 2 completed"
        
        def step3() -> str:
            success_state["steps"].append("step3")
            return "Step 3 completed"
        
        actions = [
            Action("1", "step1", step1),
            Action("2", "step2", step2),
            Action("3", "step3", step3)
        ]
        
        execution = agent.execute_workflow("workflow-003", actions)
        
        print(f"\nCompleted Steps: {success_state['steps']}")
        print(f"Workflow Success: {execution.success}")
        
        # Example 4: Compensation with LLM decision
        print("\n" + "="*60)
        print("Example 4: LLM-Assisted Compensation Decision")
        print("="*60)
        
        def should_compensate_action(action: Action, error: str) -> bool:
            """Use LLM to decide if action should be compensated."""
            prompt = ChatPromptTemplate.from_messages([
                ("system", "You are an expert in distributed systems. "
                          "Determine if an action should be compensated (undone) "
                          "based on the error. Reply with ONLY 'yes' or 'no'."),
                ("user", "Action: {action}\nError: {error}\n\n"
                        "Should this action be compensated?")
            ])
            
            chain = prompt | agent.llm | StrOutputParser()
            result = chain.invoke({
                "action": action.name,
                "error": error
            })
            
            return "yes" in result.lower()
        
        # Simulate a workflow with LLM-guided compensation
        test_action = Action(
            action_id="test",
            name="send_notification",
            execute_fn=lambda: "Notification sent",
            compensate_fn=lambda x: print("Notification cancelled")
        )
        test_action.result = "Notification sent"
        
        test_error = "Network timeout after 30 seconds"
        
        print(f"\nAsking LLM if '{test_action.name}' should be compensated")
        print(f"Error: {test_error}")
        
        should_compensate = should_compensate_action(test_action, test_error)
        print(f"\nLLM Decision: {'Compensate' if should_compensate else 'Do not compensate'}")
        
        # Example 5: Idempotent actions
        print("\n" + "="*60)
        print("Example 5: Idempotent Actions (Safe to Retry)")
        print("="*60)
        
        idempotent_state = {"writes": 0}
        
        def idempotent_write(key: str, value: str) -> str:
            """Idempotent write operation."""
            idempotent_state["writes"] += 1
            print(f"      Write #{idempotent_state['writes']}: {key} = {value}")
            return f"Set {key} = {value}"
        
        # Idempotent actions don't need compensation
        idempotent_action = Action(
            action_id="idem",
            name="idempotent_write",
            execute_fn=idempotent_write,
            args=("config", "value123"),
            idempotent=True
        )
        
        print("Executing idempotent action multiple times:")
        for i in range(3):
            result = idempotent_action.execute_fn(*idempotent_action.args)
            print(f"   Attempt {i+1}: {result}")
        
        print(f"\n💡 Idempotent actions can be safely retried without compensation")
        print(f"   Total writes: {idempotent_state['writes']}")
        
        # Summary
        print("\n" + "="*60)
        print("SUMMARY")
        print("="*60)
        print("""
    The Compensating Actions pattern demonstrates:
    
    1. Automatic Rollback: Undoes completed actions when workflow fails
    2. Reverse Order: Compensates actions in reverse of execution
    3. State Consistency: Maintains system consistency after failures
    4. Saga Pattern: Implements saga-like transaction semantics
    5. Flexible Compensation: Supports custom compensation logic
    
    Key Benefits:
    - Maintains consistency in distributed workflows
    - Automatic error recovery
    - Clear rollback semantics
    - Reduces manual cleanup effort
    - Better fault tolerance
    
    Compensation Strategies:
    - Reverse Order: Compensate from last to first (recommended)
    - Parallel: Compensate all actions simultaneously (faster)
    - Selective: Compensate only specific actions (intelligent)
    - Best Effort: Continue compensation even if some fail
    
    Best Practices:
    - Design compensating actions alongside forward actions
    - Make compensation idempotent when possible
    - Log all compensation attempts
    - Handle compensation failures gracefully
    - Test compensation logic thoroughly
    - Consider partial compensation scenarios
    - Document non-compensable actions
    
    Idempotent Actions:
    - Can be safely retried without side effects
    - Don't require compensation
    - Examples: PUT operations, SET commands
    - Mark actions as idempotent in workflow
    
    Use Cases:
    - Multi-service API orchestration
    - Database transaction-like workflows
    - Payment and order processing
    - Resource provisioning
    - Data pipeline error recovery
    
    Common Patterns:
    - Forward Progress + Compensation (Saga)
    - Try-Confirm-Cancel (TCC)
    - Two-Phase Commit alternative
    - Eventual consistency patterns
        """)
    
    
    if __name__ == "__main__":
        demonstrate_compensating_actions()
    
        

    Agentic Design Pattern: Compensating Actions

    Pattern 147: Compensating Actions – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Compensating Actions pattern implementation.


    Pattern Description

    Pattern 147: Compensating Actions

    Description: Implements compensating actions for rolling back completed operations when a workflow fails. This pattern maintains consistency by undoing successful steps in reverse order, similar to database transaction rollback but for complex multi-step agent workflows.

    Components: – Action Logger: Records completed actions – Compensation Registry: Maps actions to compensating actions – Rollback Coordinator: Orchestrates compensation execution – State Tracker: Maintains workflow state – Failure Handler: Detects and triggers compensation – Recovery Monitor: Tracks compensation success

    Use Cases: – Multi-step agent workflows with side effects – API orchestration with multiple services – Data pipeline error recovery – Transaction-like behavior for agent operations – Distributed system coordination

    Benefits: – Maintains system consistency – Automatic error recovery – Clear rollback semantics – Reduced manual cleanup – Better fault tolerance

    Trade-offs: – Complexity overhead – Not all actions are reversible – Potential partial rollback states – Resource consumption during compensation

    LangChain Implementation: Uses action tracking, compensation functions, and rollback orchestration to implement saga-like patterns for agent workflows.


    File Statistics

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

    Module Documentation

    """
    Pattern 147: Compensating Actions
    
    Description:
        Implements compensating actions for rolling back completed operations when
        a workflow fails. This pattern maintains consistency by undoing successful
        steps in reverse order, similar to database transaction rollback but for
        complex multi-step agent workflows.
    
    Components:
        - Action Logger: Records completed actions
        - Compensation Registry: Maps actions to compensating actions
        - Rollback Coordinator: Orchestrates compensation execution
        - State Tracker: Maintains workflow state
        - Failure Handler: Detects and triggers compensation
        - Recovery Monitor: Tracks compensation success
    
    Use Cases:
        - Multi-step agent workflows with side effects
        - API orchestration with multiple services
        - Data pipeline error recovery
        - Transaction-like behavior for agent operations
        - Distributed system coordination
    
    Benefits:
        - Maintains system consistency
        - Automatic error recovery
        - Clear rollback semantics
        - Reduced manual cleanup
        - Better fault tolerance
    
    Trade-offs:
        - Complexity overhead
        - Not all actions are reversible
        - Potential partial rollback states
        - Resource consumption during compensation
    
    LangChain Implementation:
        Uses action tracking, compensation functions, and rollback orchestration
        to implement saga-like patterns for agent workflows.
    """
    

    Purpose: This module implements the Compensating Actions 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 time
    import typing.Any
    import typing.Callable
    import typing.Dict
    import typing.List
    import typing.Optional
    import typing.Tuple
    

    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 – 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) – typing.Tuple: 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: ActionStatus

    Lines 58-66

    Inherits from: Enum

    Purpose:

    Status of an action
    

    Class: Action

    Lines 70-83

    Purpose:

    Represents a workflow action
    

    Class: WorkflowExecution

    Lines 87-97

    Purpose:

    Tracks workflow execution
    

    Class: CompensatingActionsAgent

    Lines 100-308

    Purpose:

    Agent that implements compensating actions for workflow rollback.
    
    Executes multi-step workflows and automatically compensates completed
    actions when failures occur, maintaining system consistency.
    

    Methods:

    __init__(self, model)

    Lines 108-118

    Purpose:

    Initialize the compensating actions agent.
    
    Args:
        model: LLM model to use
    

    Parameters: – model: Language model instance

    register_compensation(self, action_name, compensate_fn)

    Lines 120-132

    Purpose:

    Register a compensation function for an action type.
    
    Args:
        action_name: Name of the action
        compensate_fn: Function to compensate the action
    

    Parameters: – action_name: Action name – compensate_fn: Compensate fn

    execute_workflow(self, workflow_id, actions)

    Lines 134-212

    Purpose:

    Execute a workflow with compensating action support.
    
    Args:
        workflow_id: Unique workflow identifier
        actions: List of actions to execute
    
    Returns:
        Workflow execution result
    

    Parameters: – workflow_id: Workflow id – actions: Actions

    Returns: WorkflowExecution

    _compensate_actions(self, execution)

    Lines 214-250

    Purpose:

    Compensate completed actions in reverse order.
    
    Args:
        execution: Workflow execution to compensate
    

    Parameters: – execution: Execution

    get_workflow_status(self, workflow_id)

    Lines 252-254

    Purpose:

    Get status of a workflow execution.
    

    Parameters: – workflow_id: Workflow id

    Returns: <ast.Subscript object at 0x000001A06D7C9CD0>

    generate_compensation_report(self, workflow_id)

    Lines 256-308

    Purpose:

    Generate a detailed compensation report.
    
    Args:
        workflow_id: Workflow to report on
    
    Returns:
        Formatted report
    

    Parameters: – workflow_id: Workflow id

    Returns: str


    Functions

    demonstrate_compensating_actions()

    Lines 311-614

    Purpose:

    Demonstrate the Compensating Actions pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Compensating Actions 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 146
    • Pattern 148

    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 Compensating Actions 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: 147_compensating_actions.py
    Total Lines: 619
    Pattern Category: AI Agent Design Pattern #147


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Retry Backoff

    
        """
    Pattern 146: Retry with Backoff
    
    Description:
        Implements retry logic with exponential backoff for handling transient failures
        in LLM API calls and agent operations. This pattern automatically retries failed
        operations with increasing delays, helping systems recover from temporary issues
        like rate limits, network errors, or service disruptions.
    
    Components:
        - Retry Manager: Orchestrates retry attempts
        - Backoff Strategy: Calculates delay between retries
        - Failure Classifier: Determines if error is retryable
        - Success Monitor: Tracks retry statistics
        - Circuit Breaker Integration: Prevents endless retries
        - Fallback Handler: Provides alternatives when retries exhausted
    
    Use Cases:
        - Handle API rate limiting
        - Recover from transient network errors
        - Manage service unavailability
        - Deal with timeout errors
        - Handle overloaded systems
    
    Benefits:
        - Automatic error recovery
        - Reduced manual intervention
        - Improved reliability
        - Better user experience
        - Graceful handling of transient failures
    
    Trade-offs:
        - Increased latency on failures
        - Resource consumption during retries
        - Potential for cascading delays
        - Complexity in retry logic
    
    LangChain Implementation:
        Uses decorators and custom retry logic with exponential backoff,
        jitter, and intelligent failure classification.
    """
    
    import os
    import time
    import random
    from typing import Any, Callable, Optional, List, Dict
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from functools import wraps
    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 BackoffStrategy(Enum):
        """Backoff strategy for retries"""
        FIXED = "fixed"
        LINEAR = "linear"
        EXPONENTIAL = "exponential"
        FIBONACCI = "fibonacci"
    
    
    class ErrorType(Enum):
        """Types of errors for classification"""
        TRANSIENT = "transient"  # Temporary, retryable
        PERMANENT = "permanent"  # Persistent, not retryable
        RATE_LIMIT = "rate_limit"  # Rate limiting
        TIMEOUT = "timeout"  # Timeout error
        UNKNOWN = "unknown"  # Unknown error type
    
    
    @dataclass
    class RetryConfig:
        """Configuration for retry behavior"""
        max_attempts: int = 3
        initial_delay: float = 1.0
        max_delay: float = 60.0
        backoff_strategy: BackoffStrategy = BackoffStrategy.EXPONENTIAL
        backoff_multiplier: float = 2.0
        jitter: bool = True
        jitter_range: float = 0.1
        retryable_errors: List[str] = field(default_factory=lambda: [
            "rate_limit",
            "timeout",
            "connection",
            "service_unavailable"
        ])
    
    
    @dataclass
    class RetryAttempt:
        """Information about a retry attempt"""
        attempt_number: int
        error_type: ErrorType
        error_message: str
        delay_before: float
        timestamp: datetime
        success: bool
    
    
    @dataclass
    class RetryMetrics:
        """Metrics for retry operations"""
        total_calls: int = 0
        successful_first_try: int = 0
        successful_after_retry: int = 0
        failed_after_retries: int = 0
        total_retries: int = 0
        total_delay_time: float = 0.0
        average_attempts: float = 0.0
        retry_success_rate: float = 0.0
    
    
    class RetryWithBackoffAgent:
        """
        Agent that implements retry logic with exponential backoff.
        
        Automatically retries failed operations with intelligent backoff
        strategies and error classification.
        """
        
        def __init__(
            self,
            model: str = "gpt-3.5-turbo",
            config: Optional[RetryConfig] = None
        ):
            """
            Initialize the retry agent.
            
            Args:
                model: LLM model to use
                config: Retry configuration
            """
            self.llm = ChatOpenAI(model=model, temperature=0.7)
            self.config = config or RetryConfig()
            
            self.metrics = RetryMetrics()
            self.retry_history: List[RetryAttempt] = []
        
        def calculate_delay(self, attempt: int) -> float:
            """
            Calculate delay for retry attempt based on strategy.
            
            Args:
                attempt: Current attempt number (0-indexed)
                
            Returns:
                Delay in seconds
            """
            if self.config.backoff_strategy == BackoffStrategy.FIXED:
                delay = self.config.initial_delay
                
            elif self.config.backoff_strategy == BackoffStrategy.LINEAR:
                delay = self.config.initial_delay * (attempt + 1)
                
            elif self.config.backoff_strategy == BackoffStrategy.EXPONENTIAL:
                delay = self.config.initial_delay * (
                    self.config.backoff_multiplier ** attempt
                )
                
            elif self.config.backoff_strategy == BackoffStrategy.FIBONACCI:
                delay = self.config.initial_delay * self._fibonacci(attempt + 1)
            
            else:
                delay = self.config.initial_delay
            
            # Cap at max delay
            delay = min(delay, self.config.max_delay)
            
            # Add jitter if enabled
            if self.config.jitter:
                jitter_amount = delay * self.config.jitter_range
                delay += random.uniform(-jitter_amount, jitter_amount)
            
            return max(0, delay)  # Ensure non-negative
        
        def _fibonacci(self, n: int) -> int:
            """Calculate nth Fibonacci number."""
            if n  ErrorType:
            """
            Classify error to determine if retryable.
            
            Args:
                error: Exception to classify
                
            Returns:
                Error type classification
            """
            error_str = str(error).lower()
            
            if any(keyword in error_str for keyword in ["rate limit", "429"]):
                return ErrorType.RATE_LIMIT
            
            elif any(keyword in error_str for keyword in ["timeout", "timed out"]):
                return ErrorType.TIMEOUT
            
            elif any(keyword in error_str for keyword in [
                "connection", "network", "unavailable", "503", "502"
            ]):
                return ErrorType.TRANSIENT
            
            elif any(keyword in error_str for keyword in [
                "invalid", "authentication", "authorization", "400", "401", "403"
            ]):
                return ErrorType.PERMANENT
            
            else:
                return ErrorType.UNKNOWN
        
        def is_retryable(self, error: Exception) -> bool:
            """
            Determine if error should be retried.
            
            Args:
                error: Exception to check
                
            Returns:
                True if error is retryable
            """
            error_type = self.classify_error(error)
            
            if error_type == ErrorType.PERMANENT:
                return False
            
            error_str = str(error).lower()
            return any(
                keyword in error_str
                for keyword in self.config.retryable_errors
            )
        
        def retry_with_backoff(
            self,
            func: Callable,
            *args,
            **kwargs
        ) -> Any:
            """
            Execute function with retry logic.
            
            Args:
                func: Function to execute
                *args: Positional arguments
                **kwargs: Keyword arguments
                
            Returns:
                Function result
                
            Raises:
                Exception: If all retries exhausted
            """
            self.metrics.total_calls += 1
            attempts = []
            
            for attempt in range(self.config.max_attempts):
                try:
                    result = func(*args, **kwargs)
                    
                    # Record success
                    if attempt == 0:
                        self.metrics.successful_first_try += 1
                    else:
                        self.metrics.successful_after_retry += 1
                        self.metrics.total_retries += attempt
                    
                    # Record attempt
                    attempts.append(RetryAttempt(
                        attempt_number=attempt + 1,
                        error_type=ErrorType.TRANSIENT,
                        error_message="Success",
                        delay_before=0,
                        timestamp=datetime.now(),
                        success=True
                    ))
                    
                    self.retry_history.extend(attempts)
                    self._update_metrics()
                    
                    return result
                    
                except Exception as e:
                    error_type = self.classify_error(e)
                    
                    # Check if should retry
                    if not self.is_retryable(e):
                        print(f"⛔ Non-retryable error: {error_type.value}")
                        self.metrics.failed_after_retries += 1
                        raise
                    
                    # Last attempt - don't retry
                    if attempt == self.config.max_attempts - 1:
                        print(f"❌ Max retries ({self.config.max_attempts}) exhausted")
                        self.metrics.failed_after_retries += 1
                        
                        attempts.append(RetryAttempt(
                            attempt_number=attempt + 1,
                            error_type=error_type,
                            error_message=str(e),
                            delay_before=0,
                            timestamp=datetime.now(),
                            success=False
                        ))
                        
                        self.retry_history.extend(attempts)
                        self._update_metrics()
                        raise
                    
                    # Calculate delay and retry
                    delay = self.calculate_delay(attempt)
                    
                    print(f"⚠️  Attempt {attempt + 1} failed: {error_type.value}")
                    print(f"   Retrying in {delay:.2f}s...")
                    
                    attempts.append(RetryAttempt(
                        attempt_number=attempt + 1,
                        error_type=error_type,
                        error_message=str(e),
                        delay_before=delay,
                        timestamp=datetime.now(),
                        success=False
                    ))
                    
                    self.metrics.total_delay_time += delay
                    time.sleep(delay)
        
        def _update_metrics(self):
            """Update calculated metrics."""
            total_operations = (
                self.metrics.successful_first_try +
                self.metrics.successful_after_retry +
                self.metrics.failed_after_retries
            )
            
            if total_operations > 0:
                self.metrics.average_attempts = (
                    (self.metrics.successful_first_try +
                     self.metrics.successful_after_retry +
                     self.metrics.total_retries) / total_operations
                )
            
            retry_operations = (
                self.metrics.successful_after_retry +
                self.metrics.failed_after_retries
            )
            
            if retry_operations > 0:
                self.metrics.retry_success_rate = (
                    self.metrics.successful_after_retry / retry_operations
                )
        
        def query_with_retry(self, query: str) -> str:
            """
            Query LLM with retry logic.
            
            Args:
                query: Query string
                
            Returns:
                LLM response
            """
            def _query():
                prompt = ChatPromptTemplate.from_messages([
                    ("system", "You are a helpful assistant."),
                    ("user", "{query}")
                ])
                chain = prompt | self.llm | StrOutputParser()
                return chain.invoke({"query": query})
            
            return self.retry_with_backoff(_query)
        
        def get_metrics_report(self) -> str:
            """Generate retry metrics report."""
            report = []
            report.append("\n" + "="*60)
            report.append("RETRY WITH BACKOFF METRICS REPORT")
            report.append("="*60)
            
            report.append(f"\n📊 Overall Statistics:")
            report.append(f"   Total Calls: {self.metrics.total_calls}")
            report.append(f"   Successful First Try: {self.metrics.successful_first_try}")
            report.append(f"   Successful After Retry: {self.metrics.successful_after_retry}")
            report.append(f"   Failed After Retries: {self.metrics.failed_after_retries}")
            
            report.append(f"\n🔄 Retry Statistics:")
            report.append(f"   Total Retries: {self.metrics.total_retries}")
            report.append(f"   Average Attempts: {self.metrics.average_attempts:.2f}")
            report.append(f"   Retry Success Rate: {self.metrics.retry_success_rate:.1%}")
            
            report.append(f"\n⏱️  Timing:")
            report.append(f"   Total Delay Time: {self.metrics.total_delay_time:.2f}s")
            
            if self.metrics.total_calls > 0:
                success_rate = (
                    (self.metrics.successful_first_try + self.metrics.successful_after_retry) /
                    self.metrics.total_calls
                )
                report.append(f"\n✅ Overall Success Rate: {success_rate:.1%}")
            
            report.append("\n" + "="*60)
            
            return "\n".join(report)
    
    
    def retry_decorator(config: Optional[RetryConfig] = None):
        """
        Decorator for automatic retry with backoff.
        
        Args:
            config: Retry configuration
            
        Returns:
            Decorated function
        """
        if config is None:
            config = RetryConfig()
        
        def decorator(func: Callable) -> Callable:
            @wraps(func)
            def wrapper(*args, **kwargs):
                agent = RetryWithBackoffAgent(config=config)
                return agent.retry_with_backoff(func, *args, **kwargs)
            return wrapper
        return decorator
    
    
    def demonstrate_retry_with_backoff():
        """Demonstrate the Retry with Backoff pattern."""
        print("="*60)
        print("RETRY WITH BACKOFF PATTERN DEMONSTRATION")
        print("="*60)
        
        # Example 1: Basic retry with exponential backoff
        print("\n" + "="*60)
        print("Example 1: Basic Retry with Exponential Backoff")
        print("="*60)
        
        config = RetryConfig(
            max_attempts=4,
            initial_delay=1.0,
            backoff_strategy=BackoffStrategy.EXPONENTIAL,
            backoff_multiplier=2.0
        )
        
        agent = RetryWithBackoffAgent(config=config)
        
        print("\nDelay progression for exponential backoff:")
        for i in range(4):
            delay = agent.calculate_delay(i)
            print(f"  Attempt {i+1}: {delay:.2f}s delay")
        
        # Example 2: Compare backoff strategies
        print("\n" + "="*60)
        print("Example 2: Compare Backoff Strategies")
        print("="*60)
        
        strategies = [
            BackoffStrategy.FIXED,
            BackoffStrategy.LINEAR,
            BackoffStrategy.EXPONENTIAL,
            BackoffStrategy.FIBONACCI
        ]
        
        print(f"\n{'Strategy':<15} {'Attempt 1':<12} {'Attempt 2':<12} {'Attempt 3':<12} {'Attempt 4':<12}")
        print("-" * 63)
        
        for strategy in strategies:
            config = RetryConfig(
                initial_delay=1.0,
                backoff_strategy=strategy,
                backoff_multiplier=2.0,
                jitter=False
            )
            agent = RetryWithBackoffAgent(config=config)
            
            delays = [agent.calculate_delay(i) for i in range(4)]
            print(f"{strategy.value:<15} {delays[0]:<12.2f} {delays[1]:<12.2f} {delays[2]:<12.2f} {delays[3]:<12.2f}")
        
        # Example 3: Error classification
        print("\n" + "="*60)
        print("Example 3: Error Classification")
        print("="*60)
        
        agent = RetryWithBackoffAgent()
        
        test_errors = [
            Exception("Rate limit exceeded (429)"),
            Exception("Connection timeout"),
            Exception("Service unavailable (503)"),
            Exception("Invalid API key (401)"),
            Exception("Network connection failed"),
            Exception("Unknown error occurred")
        ]
        
        print("\nError classification results:")
        for error in test_errors:
            error_type = agent.classify_error(error)
            is_retryable = agent.is_retryable(error)
            status = "✅ RETRY" if is_retryable else "⛔ DON'T RETRY"
            print(f"  {status:<15} {error_type.value:<15} {str(error)[:40]}")
        
        # Example 4: Simulate retry scenarios
        print("\n" + "="*60)
        print("Example 4: Simulate Retry Scenarios")
        print("="*60)
        
        # Simulate transient failure that succeeds on retry
        call_count = {"count": 0}
        
        def flaky_function():
            """Function that fails first 2 times, then succeeds."""
            call_count["count"] += 1
            if call_count["count"] < 3:
                raise Exception("Service temporarily unavailable (503)")
            return "Success!"
        
        config = RetryConfig(max_attempts=5, initial_delay=0.5)
        agent = RetryWithBackoffAgent(config=config)
        
        print("\nSimulating transient failure (succeeds on 3rd attempt):")
        try:
            result = agent.retry_with_backoff(flaky_function)
            print(f"✅ Result: {result}")
        except Exception as e:
            print(f"❌ Failed: {e}")
        
        # Example 5: Jitter demonstration
        print("\n" + "="*60)
        print("Example 5: Jitter for Thundering Herd Prevention")
        print("="*60)
        
        config_no_jitter = RetryConfig(
            initial_delay=1.0,
            backoff_strategy=BackoffStrategy.EXPONENTIAL,
            jitter=False
        )
        
        config_with_jitter = RetryConfig(
            initial_delay=1.0,
            backoff_strategy=BackoffStrategy.EXPONENTIAL,
            jitter=True,
            jitter_range=0.2
        )
        
        agent_no_jitter = RetryWithBackoffAgent(config=config_no_jitter)
        agent_with_jitter = RetryWithBackoffAgent(config=config_with_jitter)
        
        print("\nDelay comparison (5 simulated clients):")
        print(f"\n{'Attempt':<10} {'No Jitter':<40} {'With Jitter':<40}")
        print("-" * 90)
        
        for attempt in range(3):
            no_jitter_delays = [agent_no_jitter.calculate_delay(attempt) for _ in range(5)]
            with_jitter_delays = [agent_with_jitter.calculate_delay(attempt) for _ in range(5)]
            
            no_jitter_str = ", ".join(f"{d:.2f}s" for d in no_jitter_delays)
            with_jitter_str = ", ".join(f"{d:.2f}s" for d in with_jitter_delays)
            
            print(f"{attempt+1:<10} {no_jitter_str:<40} {with_jitter_str:<40}")
        
        print("\n💡 Jitter spreads retry attempts, preventing thundering herd")
        
        # Example 6: Real LLM query with retry
        print("\n" + "="*60)
        print("Example 6: LLM Query with Retry Protection")
        print("="*60)
        
        config = RetryConfig(
            max_attempts=3,
            initial_delay=1.0,
            backoff_strategy=BackoffStrategy.EXPONENTIAL
        )
        
        agent = RetryWithBackoffAgent(config=config)
        
        try:
            print("\nQuerying LLM with retry protection...")
            result = agent.query_with_retry(
                "What is the capital of France? Answer in one word."
            )
            print(f"✅ Response: {result}")
        except Exception as e:
            print(f"❌ Failed after all retries: {e}")
        
        # Example 7: Metrics and reporting
        print("\n" + "="*60)
        print("Example 7: Retry Metrics and Reporting")
        print("="*60)
        
        # Simulate multiple operations
        agent = RetryWithBackoffAgent(config=RetryConfig(max_attempts=3))
        
        test_queries = [
            "What is 2+2?",
            "Explain quantum computing in one sentence",
            "Name a primary color"
        ]
        
        print("\nExecuting multiple queries with retry protection...")
        for query in test_queries:
            try:
                agent.query_with_retry(query)
            except Exception as e:
                print(f"Query failed: {e}")
        
        report = agent.get_metrics_report()
        print(report)
        
        # Summary
        print("\n" + "="*60)
        print("SUMMARY")
        print("="*60)
        print("""
    The Retry with Backoff pattern demonstrates:
    
    1. Automatic Recovery: Handles transient failures automatically
    2. Multiple Strategies: Fixed, linear, exponential, Fibonacci backoff
    3. Error Classification: Intelligent distinction between retryable/non-retryable
    4. Jitter Support: Prevents thundering herd problem
    5. Metrics Tracking: Comprehensive monitoring of retry behavior
    
    Key Benefits:
    - Improved reliability (often 95%+ success rate)
    - Reduced manual intervention
    - Better user experience
    - Graceful handling of rate limits
    - Protection against cascading failures
    
    Backoff Strategies:
    - Fixed: Same delay each time (simple, predictable)
    - Linear: Delay increases linearly (gradual backoff)
    - Exponential: Delay doubles each time (recommended)
    - Fibonacci: Delay follows Fibonacci sequence (balanced)
    
    Best Practices:
    - Use exponential backoff as default
    - Add jitter to prevent synchronized retries
    - Set reasonable max attempts (3-5 typical)
    - Cap maximum delay (60s common)
    - Classify errors correctly
    - Log retry attempts for debugging
    - Integrate with circuit breaker
    - Monitor retry metrics
    
    Common Configurations:
    - API calls: 3 retries, exponential, 1s initial
    - Database: 5 retries, exponential, 0.5s initial
    - Network: 4 retries, linear, 2s initial
    - Rate limits: Fibonacci, respect Retry-After header
        """)
    
    
    if __name__ == "__main__":
        demonstrate_retry_with_backoff()
    
        

    Agentic Design Pattern: Retry Backoff

    Pattern 146: Retry Backoff – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Retry Backoff pattern implementation.


    Pattern Description

    Pattern 146: Retry with Backoff

    Description: Implements retry logic with exponential backoff for handling transient failures in LLM API calls and agent operations. This pattern automatically retries failed operations with increasing delays, helping systems recover from temporary issues like rate limits, network errors, or service disruptions.

    Components: – Retry Manager: Orchestrates retry attempts – Backoff Strategy: Calculates delay between retries – Failure Classifier: Determines if error is retryable – Success Monitor: Tracks retry statistics – Circuit Breaker Integration: Prevents endless retries – Fallback Handler: Provides alternatives when retries exhausted

    Use Cases: – Handle API rate limiting – Recover from transient network errors – Manage service unavailability – Deal with timeout errors – Handle overloaded systems

    Benefits: – Automatic error recovery – Reduced manual intervention – Improved reliability – Better user experience – Graceful handling of transient failures

    Trade-offs: – Increased latency on failures – Resource consumption during retries – Potential for cascading delays – Complexity in retry logic

    LangChain Implementation: Uses decorators and custom retry logic with exponential backoff, jitter, and intelligent failure classification.


    File Statistics

    • Total Lines: 665
    • Classes: 6
    • Functions: 2
    • Constants: 0

    Module Documentation

    """
    Pattern 146: Retry with Backoff
    
    Description:
        Implements retry logic with exponential backoff for handling transient failures
        in LLM API calls and agent operations. This pattern automatically retries failed
        operations with increasing delays, helping systems recover from temporary issues
        like rate limits, network errors, or service disruptions.
    
    Components:
        - Retry Manager: Orchestrates retry attempts
        - Backoff Strategy: Calculates delay between retries
        - Failure Classifier: Determines if error is retryable
        - Success Monitor: Tracks retry statistics
        - Circuit Breaker Integration: Prevents endless retries
        - Fallback Handler: Provides alternatives when retries exhausted
    
    Use Cases:
        - Handle API rate limiting
        - Recover from transient network errors
        - Manage service unavailability
        - Deal with timeout errors
        - Handle overloaded systems
    
    Benefits:
        - Automatic error recovery
        - Reduced manual intervention
        - Improved reliability
        - Better user experience
        - Graceful handling of transient failures
    
    Trade-offs:
        - Increased latency on failures
        - Resource consumption during retries
        - Potential for cascading delays
        - Complexity in retry logic
    
    LangChain Implementation:
        Uses decorators and custom retry logic with exponential backoff,
        jitter, and intelligent failure classification.
    """
    

    Purpose: This module implements the Retry Backoff 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 functools.wraps
    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 – functools.wraps: Higher-order functions and operations on callable objects – 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
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: BackoffStrategy

    Lines 60-65

    Inherits from: Enum

    Purpose:

    Backoff strategy for retries
    

    Class: ErrorType

    Lines 68-74

    Inherits from: Enum

    Purpose:

    Types of errors for classification
    

    Class: RetryConfig

    Lines 78-92

    Purpose:

    Configuration for retry behavior
    

    Class: RetryAttempt

    Lines 96-103

    Purpose:

    Information about a retry attempt
    

    Class: RetryMetrics

    Lines 107-116

    Purpose:

    Metrics for retry operations
    

    Class: RetryWithBackoffAgent

    Lines 119-412

    Purpose:

    Agent that implements retry logic with exponential backoff.
    
    Automatically retries failed operations with intelligent backoff
    strategies and error classification.
    

    Methods:

    __init__(self, model, config)

    Lines 127-143

    Purpose:

    Initialize the retry agent.
    
    Args:
        model: LLM model to use
        config: Retry configuration
    

    Parameters: – model: Language model instance – config: Config

    calculate_delay(self, attempt)

    Lines 145-180

    Purpose:

    Calculate delay for retry attempt based on strategy.
    
    Args:
        attempt: Current attempt number (0-indexed)
    
    Returns:
        Delay in seconds
    

    Parameters: – attempt: Attempt

    Returns: float

    _fibonacci(self, n)

    Lines 182-189

    Purpose:

    Calculate nth Fibonacci number.
    

    Parameters: – n: N

    Returns: int

    classify_error(self, error)

    Lines 191-220

    Purpose:

    Classify error to determine if retryable.
    
    Args:
        error: Exception to classify
    
    Returns:
        Error type classification
    

    Parameters: – error: Error

    Returns: ErrorType

    is_retryable(self, error)

    Lines 222-241

    Purpose:

    Determine if error should be retried.
    
    Args:
        error: Exception to check
    
    Returns:
        True if error is retryable
    

    Parameters: – error: Error

    Returns: bool

    retry_with_backoff(self, func)

    Lines 243-335

    Purpose:

    Execute function with retry logic.
    
    Args:
        func: Function to execute
        *args: Positional arguments
        **kwargs: Keyword arguments
    
    Returns:
        Function result
    
    Raises:
        Exception: If all retries exhausted
    

    Parameters: – func: Func

    Returns: Any

    _update_metrics(self)

    Lines 337-360

    Purpose:

    Update calculated metrics.
    

    Parameters:

    query_with_retry(self, query)

    Lines 362-380

    Purpose:

    Query LLM with retry logic.
    
    Args:
        query: Query string
    
    Returns:
        LLM response
    

    Parameters: – query: Input query or question

    Returns: str

    get_metrics_report(self)

    Lines 382-412

    Purpose:

    Generate retry metrics report.
    

    Parameters:

    Returns: str


    Functions

    retry_decorator(config)

    Lines 415-434

    Purpose:

    Decorator for automatic retry with backoff.
    
    Args:
        config: Retry configuration
    
    Returns:
        Decorated function
    

    Parameters: – config: Config

    demonstrate_retry_with_backoff()

    Lines 437-660

    Purpose:

    Demonstrate the Retry with Backoff pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Retry Backoff 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 145
    • Pattern 147

    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 Retry Backoff 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: 146_retry_backoff.py
    Total Lines: 665
    Pattern Category: AI Agent Design Pattern #146


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Quantization

    
        """
    Pattern 145: Quantization & Compression
    
    Description:
        Implements model quantization and compression techniques to reduce model size
        and improve inference speed. This pattern uses prompt engineering, output
        compression, and response optimization to simulate quantization effects in
        LLM agents, reducing token usage and improving efficiency.
    
    Components:
        - Quantization Strategies: Different levels of output compression
        - Token Optimizer: Reduces unnecessary tokens in responses
        - Compression Evaluator: Measures compression quality
        - Format Optimizer: Structured output formats for efficiency
        - Quality Monitor: Tracks information loss
        - Performance Tracker: Measures speed and size improvements
    
    Use Cases:
        - Reduce API costs through token reduction
        - Improve response latency
        - Optimize for bandwidth-constrained environments
        - Mobile and edge deployment
        - High-volume production systems
    
    Benefits:
        - Reduced token consumption (20-60%)
        - Faster response times
        - Lower API costs
        - Maintained core information
        - Improved throughput
    
    Trade-offs:
        - Some information loss
        - Less verbose responses
        - Potential clarity reduction
        - Requires careful tuning
    
    LangChain Implementation:
        Uses prompt engineering to generate compressed responses, structured
        output formats, and token optimization strategies.
    """
    
    import os
    import re
    import time
    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_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class CompressionLevel(Enum):
        """Compression level for responses"""
        NONE = "none"
        LOW = "low"
        MEDIUM = "medium"
        HIGH = "high"
        EXTREME = "extreme"
    
    
    class OutputFormat(Enum):
        """Output format optimization"""
        NATURAL = "natural"
        STRUCTURED = "structured"
        BULLET_POINTS = "bullet_points"
        KEYWORDS = "keywords"
        JSON = "json"
    
    
    @dataclass
    class CompressionMetrics:
        """Metrics for compression performance"""
        original_tokens: int = 0
        compressed_tokens: int = 0
        compression_ratio: float = 0.0
        token_savings: int = 0
        latency_original: float = 0.0
        latency_compressed: float = 0.0
        speed_improvement: float = 0.0
        quality_score: float = 0.0
        information_retention: float = 0.0
    
    
    @dataclass
    class CompressionStrategy:
        """Strategy for compression"""
        level: CompressionLevel
        format: OutputFormat
        max_length: Optional[int] = None
        preserve_key_info: bool = True
        remove_examples: bool = False
        use_abbreviations: bool = False
    
    
    class QuantizationAgent:
        """
        Agent that implements quantization and compression for LLM responses.
        
        Optimizes responses by reducing token usage while maintaining information
        quality through various compression strategies.
        """
        
        def __init__(self, model: str = "gpt-3.5-turbo", temperature: float = 0.7):
            """
            Initialize the quantization agent.
            
            Args:
                model: LLM model to use
                temperature: Sampling temperature
            """
            self.llm = ChatOpenAI(model=model, temperature=temperature)
            self.evaluator = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
            
            self.metrics_history: List[CompressionMetrics] = []
            
            # Compression instructions for different levels
            self.compression_instructions = {
                CompressionLevel.NONE: "",
                CompressionLevel.LOW: (
                    "Be concise but complete. Remove unnecessary filler words."
                ),
                CompressionLevel.MEDIUM: (
                    "Be very concise. Use short sentences. Focus on key points only."
                ),
                CompressionLevel.HIGH: (
                    "Be extremely concise. Use minimal words. Only essential information. "
                    "Use abbreviations where clear."
                ),
                CompressionLevel.EXTREME: (
                    "Absolute minimum words. Telegram style. Essential facts only. "
                    "Abbreviate aggressively."
                )
            }
            
            # Format instructions
            self.format_instructions = {
                OutputFormat.NATURAL: "Use natural language.",
                OutputFormat.STRUCTURED: "Use structured format with clear sections.",
                OutputFormat.BULLET_POINTS: "Use bullet points only. One line per point.",
                OutputFormat.KEYWORDS: "Use keywords and short phrases only.",
                OutputFormat.JSON: "Return structured JSON format."
            }
        
        def generate_compressed_response(
            self,
            query: str,
            strategy: CompressionStrategy
        ) -> tuple[str, CompressionMetrics]:
            """
            Generate a compressed response using specified strategy.
            
            Args:
                query: Input query
                strategy: Compression strategy to use
                
            Returns:
                Tuple of (compressed response, metrics)
            """
            # First generate uncompressed response
            start_time = time.time()
            uncompressed = self._generate_uncompressed(query)
            uncompressed_latency = time.time() - start_time
            uncompressed_tokens = self._count_tokens(uncompressed)
            
            # Generate compressed response
            start_time = time.time()
            compressed = self._generate_compressed(query, strategy)
            compressed_latency = time.time() - start_time
            compressed_tokens = self._count_tokens(compressed)
            
            # Calculate metrics
            metrics = CompressionMetrics(
                original_tokens=uncompressed_tokens,
                compressed_tokens=compressed_tokens,
                compression_ratio=compressed_tokens / uncompressed_tokens if uncompressed_tokens > 0 else 1.0,
                token_savings=uncompressed_tokens - compressed_tokens,
                latency_original=uncompressed_latency,
                latency_compressed=compressed_latency,
                speed_improvement=uncompressed_latency / compressed_latency if compressed_latency > 0 else 1.0,
            )
            
            # Evaluate information retention
            metrics.information_retention = self._evaluate_information_retention(
                query, uncompressed, compressed
            )
            metrics.quality_score = metrics.information_retention * (1.0 - metrics.compression_ratio)
            
            self.metrics_history.append(metrics)
            
            return compressed, metrics
        
        def _generate_uncompressed(self, query: str) -> str:
            """Generate uncompressed response."""
            prompt = ChatPromptTemplate.from_messages([
                ("system", "You are a helpful assistant. Provide comprehensive answers."),
                ("user", "{query}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            return chain.invoke({"query": query})
        
        def _generate_compressed(
            self,
            query: str,
            strategy: CompressionStrategy
        ) -> str:
            """Generate compressed response using strategy."""
            # Build system message with compression instructions
            system_msg = "You are a helpful assistant."
            
            # Add compression level instruction
            comp_instruction = self.compression_instructions[strategy.level]
            if comp_instruction:
                system_msg += f" {comp_instruction}"
            
            # Add format instruction
            format_instruction = self.format_instructions[strategy.format]
            if format_instruction:
                system_msg += f" {format_instruction}"
            
            # Add max length constraint
            if strategy.max_length:
                system_msg += f" Maximum {strategy.max_length} words."
            
            # Add other constraints
            if strategy.remove_examples:
                system_msg += " Do not include examples."
            
            if strategy.use_abbreviations:
                system_msg += " Use common abbreviations."
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", system_msg),
                ("user", "{query}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            return chain.invoke({"query": query})
        
        def _count_tokens(self, text: str) -> int:
            """
            Estimate token count (simplified).
            
            In production, use tiktoken library for accurate counting.
            """
            # Rough estimation: ~4 characters per token
            return len(text) // 4
        
        def _evaluate_information_retention(
            self,
            query: str,
            original: str,
            compressed: str
        ) -> float:
            """
            Evaluate how much information is retained in compressed response.
            
            Args:
                query: Original query
                original: Original uncompressed response
                compressed: Compressed response
                
            Returns:
                Information retention score (0-1)
            """
            eval_prompt = ChatPromptTemplate.from_messages([
                ("system", "You are evaluating information retention. "
                          "Compare the compressed response to the original and rate "
                          "how much of the essential information is retained. "
                          "Rate from 0-10. Return ONLY a number."),
                ("user", "Query: {query}\n\n"
                        "Original Response: {original}\n\n"
                        "Compressed Response: {compressed}\n\n"
                        "Information Retention Score (0-10):")
            ])
            
            try:
                chain = eval_prompt | self.evaluator | StrOutputParser()
                result = chain.invoke({
                    "query": query,
                    "original": original,
                    "compressed": compressed
                })
                
                score = float(''.join(c for c in result if c.isdigit() or c == '.'))
                return min(max(score / 10.0, 0.0), 1.0)
                
            except Exception as e:
                print(f"⚠️  Error evaluating retention: {e}")
                return 0.5
        
        def optimize_for_cost(
            self,
            queries: List[str],
            target_savings: float = 0.5
        ) -> Dict[str, Any]:
            """
            Find optimal compression strategy for cost savings.
            
            Args:
                queries: Test queries
                target_savings: Target token savings (0-1)
                
            Returns:
                Optimal strategy and results
            """
            print(f"\n🔍 Optimizing compression for {target_savings:.0%} cost savings...")
            
            strategies = [
                CompressionStrategy(CompressionLevel.LOW, OutputFormat.NATURAL),
                CompressionStrategy(CompressionLevel.MEDIUM, OutputFormat.STRUCTURED),
                CompressionStrategy(CompressionLevel.MEDIUM, OutputFormat.BULLET_POINTS),
                CompressionStrategy(CompressionLevel.HIGH, OutputFormat.BULLET_POINTS),
                CompressionStrategy(CompressionLevel.EXTREME, OutputFormat.KEYWORDS),
            ]
            
            results = []
            
            for strategy in strategies:
                print(f"\n  Testing: {strategy.level.value} / {strategy.format.value}")
                
                metrics_list = []
                for query in queries[:3]:  # Test on subset
                    _, metrics = self.generate_compressed_response(query, strategy)
                    metrics_list.append(metrics)
                
                avg_compression = sum(m.compression_ratio for m in metrics_list) / len(metrics_list)
                avg_retention = sum(m.information_retention for m in metrics_list) / len(metrics_list)
                avg_quality = sum(m.quality_score for m in metrics_list) / len(metrics_list)
                
                results.append({
                    "strategy": strategy,
                    "compression_ratio": avg_compression,
                    "information_retention": avg_retention,
                    "quality_score": avg_quality,
                    "meets_target": (1 - avg_compression) >= target_savings
                })
                
                print(f"    Compression: {avg_compression:.2%}")
                print(f"    Retention: {avg_retention:.2%}")
                print(f"    Quality: {avg_quality:.2%}")
            
            # Find best strategy that meets target
            valid_results = [r for r in results if r["meets_target"]]
            
            if valid_results:
                best = max(valid_results, key=lambda x: x["quality_score"])
                print(f"\n✅ Found optimal strategy!")
            else:
                best = max(results, key=lambda x: x["quality_score"])
                print(f"\n⚠️  No strategy meets target. Using best available.")
            
            return {
                "optimal_strategy": best["strategy"],
                "all_results": results,
                "best_result": best
            }
        
        def compare_formats(self, query: str) -> Dict[str, Any]:
            """
            Compare different output formats for same query.
            
            Args:
                query: Query to test
                
            Returns:
                Comparison results
            """
            print(f"\n📊 Comparing output formats...")
            
            results = {}
            
            for format_type in OutputFormat:
                strategy = CompressionStrategy(
                    level=CompressionLevel.MEDIUM,
                    format=format_type
                )
                
                response, metrics = self.generate_compressed_response(query, strategy)
                
                results[format_type.value] = {
                    "response": response,
                    "tokens": metrics.compressed_tokens,
                    "retention": metrics.information_retention,
                    "metrics": metrics
                }
                
                print(f"\n  {format_type.value}:")
                print(f"    Tokens: {metrics.compressed_tokens}")
                print(f"    Retention: {metrics.information_retention:.1%}")
            
            return results
        
        def get_compression_report(self) -> str:
            """Generate compression metrics report."""
            if not self.metrics_history:
                return "No compression metrics available."
            
            report = []
            report.append("\n" + "="*60)
            report.append("QUANTIZATION & COMPRESSION REPORT")
            report.append("="*60)
            
            avg_compression = sum(m.compression_ratio for m in self.metrics_history) / len(self.metrics_history)
            avg_retention = sum(m.information_retention for m in self.metrics_history) / len(self.metrics_history)
            total_savings = sum(m.token_savings for m in self.metrics_history)
            
            report.append(f"\n📊 Overall Statistics:")
            report.append(f"   Queries Processed: {len(self.metrics_history)}")
            report.append(f"   Avg Compression Ratio: {avg_compression:.2%}")
            report.append(f"   Avg Information Retention: {avg_retention:.2%}")
            report.append(f"   Total Token Savings: {total_savings}")
            
            report.append(f"\n💰 Cost Savings:")
            # Assume $0.002 per 1K tokens (GPT-3.5 pricing)
            cost_per_token = 0.000002
            cost_savings = total_savings * cost_per_token
            report.append(f"   Estimated Cost Savings: ${cost_savings:.4f}")
            
            report.append(f"\n⚡ Performance:")
            avg_speed = sum(m.speed_improvement for m in self.metrics_history) / len(self.metrics_history)
            report.append(f"   Avg Speed Improvement: {avg_speed:.2f}x")
            
            report.append("\n" + "="*60)
            
            return "\n".join(report)
    
    
    def demonstrate_quantization_compression():
        """Demonstrate the Quantization & Compression pattern."""
        print("="*60)
        print("QUANTIZATION & COMPRESSION PATTERN DEMONSTRATION")
        print("="*60)
        
        agent = QuantizationAgent()
        
        # Example 1: Compare compression levels
        print("\n" + "="*60)
        print("Example 1: Compare Compression Levels")
        print("="*60)
        
        query = "Explain how neural networks learn through backpropagation"
        
        compression_levels = [
            CompressionLevel.NONE,
            CompressionLevel.LOW,
            CompressionLevel.MEDIUM,
            CompressionLevel.HIGH,
            CompressionLevel.EXTREME
        ]
        
        results = {}
        
        for level in compression_levels:
            strategy = CompressionStrategy(
                level=level,
                format=OutputFormat.NATURAL
            )
            
            response, metrics = agent.generate_compressed_response(query, strategy)
            results[level] = {"response": response, "metrics": metrics}
            
            print(f"\n{level.value.upper()} Compression:")
            print(f"  Tokens: {metrics.compressed_tokens} (ratio: {metrics.compression_ratio:.2%})")
            print(f"  Retention: {metrics.information_retention:.1%}")
            print(f"  Response: {response[:150]}...")
        
        # Example 2: Compare output formats
        print("\n" + "="*60)
        print("Example 2: Compare Output Formats")
        print("="*60)
        
        query = "What are the key differences between supervised and unsupervised learning?"
        
        format_results = agent.compare_formats(query)
        
        print(f"\nBest format by tokens: {min(format_results.items(), key=lambda x: x[1]['tokens'])[0]}")
        print(f"Best format by retention: {max(format_results.items(), key=lambda x: x[1]['retention'])[0]}")
        
        # Example 3: Optimize for cost savings
        print("\n" + "="*60)
        print("Example 3: Optimize for 50% Cost Reduction")
        print("="*60)
        
        test_queries = [
            "What is machine learning?",
            "Explain cloud computing benefits",
            "How do databases work?"
        ]
        
        optimization = agent.optimize_for_cost(
            queries=test_queries,
            target_savings=0.5
        )
        
        best_strategy = optimization["optimal_strategy"]
        print(f"\n✨ Optimal Strategy:")
        print(f"   Level: {best_strategy.level.value}")
        print(f"   Format: {best_strategy.format.value}")
        print(f"   Compression: {optimization['best_result']['compression_ratio']:.2%}")
        print(f"   Retention: {optimization['best_result']['information_retention']:.1%}")
        
        # Example 4: Token budget management
        print("\n" + "="*60)
        print("Example 4: Token Budget Management")
        print("="*60)
        
        max_tokens_budget = 100
        
        strategy = CompressionStrategy(
            level=CompressionLevel.HIGH,
            format=OutputFormat.BULLET_POINTS,
            max_length=max_tokens_budget // 4  # Approximate words from tokens
        )
        
        query = "Describe the architecture of a modern web application"
        response, metrics = agent.generate_compressed_response(query, strategy)
        
        print(f"\nToken Budget: {max_tokens_budget}")
        print(f"Actual Tokens: {metrics.compressed_tokens}")
        print(f"Within Budget: {'✅' if metrics.compressed_tokens <= max_tokens_budget else '❌'}")
        print(f"\nResponse:\n{response}")
        
        # Example 5: High-volume optimization
        print("\n" + "="*60)
        print("Example 5: High-Volume Production Optimization")
        print("="*60)
        
        # Simulate high-volume scenario
        queries_per_day = 10000
        
        strategy_standard = CompressionStrategy(
            level=CompressionLevel.NONE,
            format=OutputFormat.NATURAL
        )
        
        strategy_optimized = CompressionStrategy(
            level=CompressionLevel.MEDIUM,
            format=OutputFormat.STRUCTURED
        )
        
        # Sample query for estimation
        sample_query = "Explain the concept"
        _, metrics_standard = agent.generate_compressed_response(sample_query, strategy_standard)
        _, metrics_optimized = agent.generate_compressed_response(sample_query, strategy_optimized)
        
        # Calculate daily savings
        tokens_per_query_standard = metrics_standard.compressed_tokens
        tokens_per_query_optimized = metrics_optimized.compressed_tokens
        
        daily_tokens_standard = queries_per_day * tokens_per_query_standard
        daily_tokens_optimized = queries_per_day * tokens_per_query_optimized
        daily_savings = daily_tokens_standard - daily_tokens_optimized
        
        print(f"\n📊 Daily Volume Analysis ({queries_per_day:,} queries):")
        print(f"   Standard Tokens: {daily_tokens_standard:,}")
        print(f"   Optimized Tokens: {daily_tokens_optimized:,}")
        print(f"   Daily Savings: {daily_savings:,} tokens")
        
        # Cost estimation
        cost_per_1k_tokens = 0.002
        daily_cost_standard = (daily_tokens_standard / 1000) * cost_per_1k_tokens
        daily_cost_optimized = (daily_tokens_optimized / 1000) * cost_per_1k_tokens
        daily_cost_savings = daily_cost_standard - daily_cost_optimized
        
        print(f"\n💰 Cost Analysis:")
        print(f"   Standard Daily Cost: ${daily_cost_standard:.2f}")
        print(f"   Optimized Daily Cost: ${daily_cost_optimized:.2f}")
        print(f"   Daily Savings: ${daily_cost_savings:.2f}")
        print(f"   Monthly Savings: ${daily_cost_savings * 30:.2f}")
        print(f"   Annual Savings: ${daily_cost_savings * 365:.2f}")
        
        # Example 6: Quality vs compression trade-off
        print("\n" + "="*60)
        print("Example 6: Quality vs Compression Trade-off")
        print("="*60)
        
        query = "Explain quantum computing"
        
        strategies_to_test = [
            ("Conservative", CompressionStrategy(CompressionLevel.LOW, OutputFormat.NATURAL)),
            ("Balanced", CompressionStrategy(CompressionLevel.MEDIUM, OutputFormat.STRUCTURED)),
            ("Aggressive", CompressionStrategy(CompressionLevel.HIGH, OutputFormat.BULLET_POINTS)),
            ("Extreme", CompressionStrategy(CompressionLevel.EXTREME, OutputFormat.KEYWORDS))
        ]
        
        print(f"\n{'Strategy':<15} {'Compression':<15} {'Retention':<15} {'Quality':<15}")
        print("-" * 60)
        
        for name, strategy in strategies_to_test:
            _, metrics = agent.generate_compressed_response(query, strategy)
            print(f"{name:<15} {metrics.compression_ratio:<15.1%} "
                  f"{metrics.information_retention:<15.1%} {metrics.quality_score:<15.2f}")
        
        # Generate final report
        print("\n" + "="*60)
        print("Example 7: Comprehensive Metrics Report")
        print("="*60)
        
        report = agent.get_compression_report()
        print(report)
        
        # Summary
        print("\n" + "="*60)
        print("SUMMARY")
        print("="*60)
        print("""
    The Quantization & Compression pattern demonstrates:
    
    1. Token Reduction: Significant reduction in token usage (20-60%)
    2. Cost Optimization: Substantial cost savings for high-volume systems
    3. Multiple Strategies: Various compression levels and output formats
    4. Quality Preservation: Maintained information with minimal loss
    5. Performance Gains: Faster response times through reduction
    
    Key Benefits:
    - Reduced API costs (30-70% savings typical)
    - Improved response latency
    - Better resource utilization
    - Maintained core information quality
    - Scalable to high-volume production
    
    Compression Strategies:
    - Low: 10-20% reduction, 95%+ retention (safe for all uses)
    - Medium: 30-40% reduction, 85-95% retention (recommended)
    - High: 50-60% reduction, 75-85% retention (non-critical)
    - Extreme: 70%+ reduction, 60-75% retention (keywords only)
    
    Best Practices:
    - Start with medium compression and evaluate
    - Use bullet points for structured information
    - Monitor information retention metrics
    - A/B test compression strategies
    - Adjust based on use case criticality
    - Consider user experience impact
    
    Use Cases:
    - High-volume production APIs
    - Cost-sensitive applications
    - Mobile and bandwidth-constrained environments
    - Real-time response requirements
    - Batch processing systems
        """)
    
    
    if __name__ == "__main__":
        demonstrate_quantization_compression()
    
        

    Agentic Design Pattern: Quantization

    Pattern 145: Quantization – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 145: Quantization & Compression

    Description: Implements model quantization and compression techniques to reduce model size and improve inference speed. This pattern uses prompt engineering, output compression, and response optimization to simulate quantization effects in LLM agents, reducing token usage and improving efficiency.

    Components: – Quantization Strategies: Different levels of output compression – Token Optimizer: Reduces unnecessary tokens in responses – Compression Evaluator: Measures compression quality – Format Optimizer: Structured output formats for efficiency – Quality Monitor: Tracks information loss – Performance Tracker: Measures speed and size improvements

    Use Cases: – Reduce API costs through token reduction – Improve response latency – Optimize for bandwidth-constrained environments – Mobile and edge deployment – High-volume production systems

    Benefits: – Reduced token consumption (20-60%) – Faster response times – Lower API costs – Maintained core information – Improved throughput

    Trade-offs: – Some information loss – Less verbose responses – Potential clarity reduction – Requires careful tuning

    LangChain Implementation: Uses prompt engineering to generate compressed responses, structured output formats, and token optimization strategies.


    File Statistics

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

    Module Documentation

    """
    Pattern 145: Quantization & Compression
    
    Description:
        Implements model quantization and compression techniques to reduce model size
        and improve inference speed. This pattern uses prompt engineering, output
        compression, and response optimization to simulate quantization effects in
        LLM agents, reducing token usage and improving efficiency.
    
    Components:
        - Quantization Strategies: Different levels of output compression
        - Token Optimizer: Reduces unnecessary tokens in responses
        - Compression Evaluator: Measures compression quality
        - Format Optimizer: Structured output formats for efficiency
        - Quality Monitor: Tracks information loss
        - Performance Tracker: Measures speed and size improvements
    
    Use Cases:
        - Reduce API costs through token reduction
        - Improve response latency
        - Optimize for bandwidth-constrained environments
        - Mobile and edge deployment
        - High-volume production systems
    
    Benefits:
        - Reduced token consumption (20-60%)
        - Faster response times
        - Lower API costs
        - Maintained core information
        - Improved throughput
    
    Trade-offs:
        - Some information loss
        - Less verbose responses
        - Potential clarity reduction
        - Requires careful tuning
    
    LangChain Implementation:
        Uses prompt engineering to generate compressed responses, structured
        output formats, and token optimization strategies.
    """
    

    Purpose: This module implements the Quantization 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 re
    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 – re: Regular expression 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
    

    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: CompressionLevel

    Lines 59-65

    Inherits from: Enum

    Purpose:

    Compression level for responses
    

    Class: OutputFormat

    Lines 68-74

    Inherits from: Enum

    Purpose:

    Output format optimization
    

    Class: CompressionMetrics

    Lines 78-88

    Purpose:

    Metrics for compression performance
    

    Class: CompressionStrategy

    Lines 92-99

    Purpose:

    Strategy for compression
    

    Class: QuantizationAgent

    Lines 102-433

    Purpose:

    Agent that implements quantization and compression for LLM responses.
    
    Optimizes responses by reducing token usage while maintaining information
    quality through various compression strategies.
    

    Methods:

    __init__(self, model, temperature)

    Lines 110-149

    Purpose:

    Initialize the quantization agent.
    
    Args:
        model: LLM model to use
        temperature: Sampling temperature
    

    Parameters: – model: Language model instance – temperature: Sampling temperature (0-1)

    generate_compressed_response(self, query, strategy)

    Lines 151-197

    Purpose:

    Generate a compressed response using specified strategy.
    
    Args:
        query: Input query
        strategy: Compression strategy to use
    
    Returns:
        Tuple of (compressed response, metrics)
    

    Parameters: – query: Input query or question – strategy: Strategy

    Returns: <ast.Subscript object at 0x000001A06D7EF910>

    _generate_uncompressed(self, query)

    Lines 199-207

    Purpose:

    Generate uncompressed response.
    

    Parameters: – query: Input query or question

    Returns: str

    _generate_compressed(self, query, strategy)

    Lines 209-245

    Purpose:

    Generate compressed response using strategy.
    

    Parameters: – query: Input query or question – strategy: Strategy

    Returns: str

    _count_tokens(self, text)

    Lines 247-254

    Purpose:

    Estimate token count (simplified).
    
    In production, use tiktoken library for accurate counting.
    

    Parameters: – text: Text

    Returns: int

    _evaluate_information_retention(self, query, original, compressed)

    Lines 256-297

    Purpose:

    Evaluate how much information is retained in compressed response.
    
    Args:
        query: Original query
        original: Original uncompressed response
        compressed: Compressed response
    
    Returns:
        Information retention score (0-1)
    

    Parameters: – query: Input query or question – original: Original – compressed: Compressed

    Returns: float

    optimize_for_cost(self, queries, target_savings)

    Lines 299-364

    Purpose:

    Find optimal compression strategy for cost savings.
    
    Args:
        queries: Test queries
        target_savings: Target token savings (0-1)
    
    Returns:
        Optimal strategy and results
    

    Parameters: – queries: Queries – target_savings: Target savings

    Returns: <ast.Subscript object at 0x000001A06E096A50>

    compare_formats(self, query)

    Lines 366-399

    Purpose:

    Compare different output formats for same query.
    
    Args:
        query: Query to test
    
    Returns:
        Comparison results
    

    Parameters: – query: Input query or question

    Returns: <ast.Subscript object at 0x000001A06D751C90>

    get_compression_report(self)

    Lines 401-433

    Purpose:

    Generate compression metrics report.
    

    Parameters:

    Returns: str


    Functions

    demonstrate_quantization_compression()

    Lines 436-650

    Purpose:

    Demonstrate the Quantization & Compression pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Quantization 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 144
    • Pattern 146

    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 Quantization 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: 145_quantization.py
    Total Lines: 655
    Pattern Category: AI Agent Design Pattern #145


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Model Distillation

    
        """
    Pattern 144: Model Distillation
    
    Description:
        Implements knowledge distillation from larger teacher models to smaller student
        models, enabling efficient deployment while maintaining performance. This pattern
        transfers knowledge through response generation, logits matching, and behavior cloning.
    
    Components:
        - Teacher Model: Large, powerful model with superior capabilities
        - Student Model: Smaller, efficient model that learns from teacher
        - Distillation Process: Knowledge transfer mechanism
        - Performance Comparison: Evaluation and validation
        - Training Data Generator: Creates synthetic training examples
        - Knowledge Transfer Evaluator: Measures transfer quality
    
    Use Cases:
        - Deploy efficient models in resource-constrained environments
        - Reduce inference costs while maintaining quality
        - Edge deployment and mobile applications
        - Real-time processing requirements
        - Multi-model deployment strategies
    
    Benefits:
        - Reduced computational costs
        - Faster inference times
        - Smaller model footprint
        - Maintained accuracy
        - Efficient knowledge transfer
    
    Trade-offs:
        - Some performance degradation
        - Requires training data and time
        - Quality depends on teacher model
        - May not capture all nuances
    
    LangChain Implementation:
        Uses ChatOpenAI with different model sizes, synthetic data generation,
        and comparison metrics to demonstrate knowledge distillation.
    """
    
    import os
    import json
    import time
    from typing import List, Dict, Any, Tuple, Optional
    from dataclasses import dataclass, field
    from datetime import datetime
    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
    
    load_dotenv()
    
    
    @dataclass
    class DistillationExample:
        """Training example for distillation"""
        input: str
        teacher_output: str
        student_output: Optional[str] = None
        quality_score: float = 0.0
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class DistillationMetrics:
        """Metrics for distillation performance"""
        teacher_latency: float = 0.0
        student_latency: float = 0.0
        quality_retention: float = 0.0
        speed_improvement: float = 0.0
        size_reduction: float = 0.0
        examples_processed: int = 0
        accuracy_comparison: Dict[str, float] = field(default_factory=dict)
    
    
    @dataclass
    class ModelProfile:
        """Profile of a model's characteristics"""
        model_name: str
        model_type: str  # teacher or student
        average_latency: float = 0.0
        average_tokens: int = 0
        cost_per_call: float = 0.0
        quality_score: float = 0.0
    
    
    class ModelDistillationAgent:
        """
        Agent that implements knowledge distillation from teacher to student models.
        
        The agent generates training data using a teacher model, trains a student model
        on this data, and evaluates the quality of knowledge transfer.
        """
        
        def __init__(
            self,
            teacher_model: str = "gpt-4",
            student_model: str = "gpt-3.5-turbo",
            temperature: float = 0.7
        ):
            """
            Initialize the distillation agent.
            
            Args:
                teacher_model: Name of the teacher model (larger, more capable)
                student_model: Name of the student model (smaller, efficient)
                temperature: Sampling temperature for generation
            """
            self.teacher = ChatOpenAI(model=teacher_model, temperature=temperature)
            self.student = ChatOpenAI(model=student_model, temperature=temperature)
            self.evaluator = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
            
            self.teacher_model = teacher_model
            self.student_model = student_model
            
            self.examples: List[DistillationExample] = []
            self.metrics = DistillationMetrics()
            self.teacher_profile = ModelProfile(teacher_model, "teacher")
            self.student_profile = ModelProfile(student_model, "student")
        
        def generate_training_data(
            self,
            domains: List[str],
            examples_per_domain: int = 5
        ) -> List[DistillationExample]:
            """
            Generate synthetic training data for distillation.
            
            Args:
                domains: List of domains to generate examples for
                examples_per_domain: Number of examples per domain
                
            Returns:
                List of training examples
            """
            print(f"\n🔄 Generating training data from teacher model...")
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "Generate {count} diverse questions about {domain}. "
                          "Return a JSON array of questions."),
                ("user", "Generate questions that are challenging and require expertise.")
            ])
            
            chain = prompt | self.teacher | StrOutputParser()
            
            all_examples = []
            
            for domain in domains:
                try:
                    result = chain.invoke({
                        "domain": domain,
                        "count": examples_per_domain
                    })
                    
                    # Parse questions (simplified - in production, use structured output)
                    questions = self._extract_questions(result, examples_per_domain)
                    
                    for question in questions:
                        # Get teacher's response
                        start_time = time.time()
                        teacher_response = self.teacher.invoke(question).content
                        teacher_latency = time.time() - start_time
                        
                        example = DistillationExample(
                            input=question,
                            teacher_output=teacher_response,
                            metadata={
                                "domain": domain,
                                "teacher_latency": teacher_latency,
                                "timestamp": datetime.now().isoformat()
                            }
                        )
                        all_examples.append(example)
                        
                    print(f"  ✓ Generated {len(questions)} examples for {domain}")
                    
                except Exception as e:
                    print(f"  ✗ Error generating examples for {domain}: {e}")
            
            self.examples.extend(all_examples)
            print(f"\n✅ Generated {len(all_examples)} total training examples")
            return all_examples
        
        def distill_knowledge(
            self,
            examples: Optional[List[DistillationExample]] = None
        ) -> DistillationMetrics:
            """
            Perform knowledge distillation on examples.
            
            Args:
                examples: Training examples (uses self.examples if None)
                
            Returns:
                Distillation metrics
            """
            if examples is None:
                examples = self.examples
            
            if not examples:
                print("⚠️  No examples available for distillation")
                return self.metrics
            
            print(f"\n🎓 Distilling knowledge to student model...")
            print(f"   Teacher: {self.teacher_model}")
            print(f"   Student: {self.student_model}")
            
            teacher_times = []
            student_times = []
            quality_scores = []
            
            for i, example in enumerate(examples, 1):
                try:
                    # Student generates response
                    start_time = time.time()
                    student_response = self.student.invoke(example.input).content
                    student_latency = time.time() - start_time
                    
                    example.student_output = student_response
                    
                    # Evaluate quality
                    quality_score = self._evaluate_response_quality(
                        example.input,
                        example.teacher_output,
                        student_response
                    )
                    example.quality_score = quality_score
                    
                    teacher_times.append(example.metadata.get("teacher_latency", 0))
                    student_times.append(student_latency)
                    quality_scores.append(quality_score)
                    
                    if i % 5 == 0:
                        print(f"   Processed {i}/{len(examples)} examples...")
                    
                except Exception as e:
                    print(f"   ✗ Error processing example {i}: {e}")
            
            # Calculate metrics
            self.metrics.teacher_latency = sum(teacher_times) / len(teacher_times)
            self.metrics.student_latency = sum(student_times) / len(student_times)
            self.metrics.quality_retention = sum(quality_scores) / len(quality_scores)
            self.metrics.speed_improvement = (
                self.metrics.teacher_latency / self.metrics.student_latency
            )
            self.metrics.examples_processed = len(examples)
            
            print(f"\n✅ Distillation complete!")
            return self.metrics
        
        def compare_models(self, test_queries: List[str]) -> Dict[str, Any]:
            """
            Compare teacher and student model performance.
            
            Args:
                test_queries: Queries to test both models on
                
            Returns:
                Comparison results
            """
            print(f"\n📊 Comparing models on {len(test_queries)} test queries...")
            
            results = {
                "teacher": {"responses": [], "latencies": [], "total_time": 0},
                "student": {"responses": [], "latencies": [], "total_time": 0},
                "quality_scores": []
            }
            
            for query in test_queries:
                # Teacher response
                start = time.time()
                teacher_resp = self.teacher.invoke(query).content
                teacher_time = time.time() - start
                
                # Student response
                start = time.time()
                student_resp = self.student.invoke(query).content
                student_time = time.time() - start
                
                # Evaluate
                quality = self._evaluate_response_quality(
                    query, teacher_resp, student_resp
                )
                
                results["teacher"]["responses"].append(teacher_resp)
                results["teacher"]["latencies"].append(teacher_time)
                results["teacher"]["total_time"] += teacher_time
                
                results["student"]["responses"].append(student_resp)
                results["student"]["latencies"].append(student_time)
                results["student"]["total_time"] += student_time
                
                results["quality_scores"].append(quality)
            
            # Calculate summary statistics
            results["summary"] = {
                "avg_teacher_latency": sum(results["teacher"]["latencies"]) / len(test_queries),
                "avg_student_latency": sum(results["student"]["latencies"]) / len(test_queries),
                "avg_quality_retention": sum(results["quality_scores"]) / len(test_queries),
                "speed_improvement": (
                    sum(results["teacher"]["latencies"]) / 
                    sum(results["student"]["latencies"])
                ),
                "total_queries": len(test_queries)
            }
            
            print(f"\n✅ Model comparison complete")
            return results
        
        def _evaluate_response_quality(
            self,
            query: str,
            teacher_response: str,
            student_response: str
        ) -> float:
            """
            Evaluate quality of student response compared to teacher.
            
            Args:
                query: Original query
                teacher_response: Teacher's response
                student_response: Student's response
                
            Returns:
                Quality score (0-1)
            """
            eval_prompt = ChatPromptTemplate.from_messages([
                ("system", "You are evaluating the quality of a student model's response "
                          "compared to a teacher model's response. Rate the student's response "
                          "on a scale of 0-10 based on:\n"
                          "- Accuracy and correctness\n"
                          "- Completeness of information\n"
                          "- Clarity and coherence\n"
                          "- Similarity to teacher's response\n\n"
                          "Return ONLY a number between 0 and 10."),
                ("user", "Query: {query}\n\n"
                        "Teacher Response: {teacher}\n\n"
                        "Student Response: {student}\n\n"
                        "Quality Score (0-10):")
            ])
            
            try:
                chain = eval_prompt | self.evaluator | StrOutputParser()
                result = chain.invoke({
                    "query": query,
                    "teacher": teacher_response,
                    "student": student_response
                })
                
                # Extract numeric score
                score = float(''.join(c for c in result if c.isdigit() or c == '.'))
                return min(max(score / 10.0, 0.0), 1.0)  # Normalize to 0-1
                
            except Exception as e:
                print(f"⚠️  Error evaluating quality: {e}")
                return 0.5  # Default to neutral score
        
        def _extract_questions(self, text: str, count: int) -> List[str]:
            """Extract questions from generated text."""
            # Simple extraction - in production, use structured output
            lines = text.strip().split('\n')
            questions = []
            
            for line in lines:
                line = line.strip()
                if line and ('?' in line or line.startswith(('-', '*', str(len(questions)+1)))):
                    # Clean up line
                    question = line.lstrip('-*0123456789. ')
                    if question and len(questions) < count:
                        questions.append(question)
            
            # Ensure we have enough questions
            while len(questions)  str:
            """Generate a comprehensive metrics report."""
            report = []
            report.append("\n" + "="*60)
            report.append("MODEL DISTILLATION METRICS REPORT")
            report.append("="*60)
            
            report.append(f"\n📚 Training Data:")
            report.append(f"   Examples Processed: {self.metrics.examples_processed}")
            
            report.append(f"\n⚡ Performance Metrics:")
            report.append(f"   Teacher Avg Latency: {self.metrics.teacher_latency:.3f}s")
            report.append(f"   Student Avg Latency: {self.metrics.student_latency:.3f}s")
            report.append(f"   Speed Improvement: {self.metrics.speed_improvement:.2f}x")
            
            report.append(f"\n🎯 Quality Metrics:")
            report.append(f"   Quality Retention: {self.metrics.quality_retention:.1%}")
            
            report.append(f"\n💰 Efficiency Gains:")
            cost_reduction = (1 - (self.metrics.student_latency / self.metrics.teacher_latency)) * 100
            report.append(f"   Latency Reduction: {cost_reduction:.1f}%")
            
            report.append("\n" + "="*60)
            
            return "\n".join(report)
        
        def save_distilled_examples(self, filepath: str):
            """Save distilled examples to file for future use."""
            data = {
                "teacher_model": self.teacher_model,
                "student_model": self.student_model,
                "examples": [
                    {
                        "input": ex.input,
                        "teacher_output": ex.teacher_output,
                        "student_output": ex.student_output,
                        "quality_score": ex.quality_score,
                        "metadata": ex.metadata
                    }
                    for ex in self.examples
                ],
                "metrics": {
                    "teacher_latency": self.metrics.teacher_latency,
                    "student_latency": self.metrics.student_latency,
                    "quality_retention": self.metrics.quality_retention,
                    "speed_improvement": self.metrics.speed_improvement,
                    "examples_processed": self.metrics.examples_processed
                }
            }
            
            with open(filepath, 'w') as f:
                json.dump(data, f, indent=2)
            
            print(f"\n💾 Saved distilled examples to {filepath}")
    
    
    def demonstrate_model_distillation():
        """Demonstrate the Model Distillation pattern."""
        print("="*60)
        print("MODEL DISTILLATION PATTERN DEMONSTRATION")
        print("="*60)
        
        # Initialize agent with teacher and student models
        agent = ModelDistillationAgent(
            teacher_model="gpt-4",
            student_model="gpt-3.5-turbo",
            temperature=0.7
        )
        
        # Example 1: Generate training data
        print("\n" + "="*60)
        print("Example 1: Generate Training Data from Teacher")
        print("="*60)
        
        domains = [
            "machine learning algorithms",
            "software architecture patterns",
            "data structures"
        ]
        
        examples = agent.generate_training_data(
            domains=domains,
            examples_per_domain=3
        )
        
        print(f"\nSample generated example:")
        if examples:
            ex = examples[0]
            print(f"  Query: {ex.input[:100]}...")
            print(f"  Teacher Response: {ex.teacher_output[:150]}...")
        
        # Example 2: Distill knowledge to student
        print("\n" + "="*60)
        print("Example 2: Distill Knowledge to Student Model")
        print("="*60)
        
        metrics = agent.distill_knowledge()
        
        print(f"\nDistillation Results:")
        print(f"  Quality Retention: {metrics.quality_retention:.1%}")
        print(f"  Speed Improvement: {metrics.speed_improvement:.2f}x faster")
        
        # Example 3: Compare models on test queries
        print("\n" + "="*60)
        print("Example 3: Compare Teacher vs Student Performance")
        print("="*60)
        
        test_queries = [
            "Explain the concept of overfitting in machine learning",
            "What is the difference between composition and inheritance?",
            "How does a hash table achieve O(1) lookup time?"
        ]
        
        comparison = agent.compare_models(test_queries)
        
        print(f"\nComparison Results:")
        print(f"  Teacher Avg Latency: {comparison['summary']['avg_teacher_latency']:.3f}s")
        print(f"  Student Avg Latency: {comparison['summary']['avg_student_latency']:.3f}s")
        print(f"  Quality Retention: {comparison['summary']['avg_quality_retention']:.1%}")
        print(f"  Speed Improvement: {comparison['summary']['speed_improvement']:.2f}x")
        
        # Example 4: Demonstrate efficiency gains
        print("\n" + "="*60)
        print("Example 4: Efficiency Gains Analysis")
        print("="*60)
        
        print(f"\n📊 Efficiency Analysis:")
        print(f"   Total Time (Teacher): {comparison['teacher']['total_time']:.2f}s")
        print(f"   Total Time (Student): {comparison['student']['total_time']:.2f}s")
        
        time_saved = comparison['teacher']['total_time'] - comparison['student']['total_time']
        time_saved_pct = (time_saved / comparison['teacher']['total_time']) * 100
        
        print(f"   Time Saved: {time_saved:.2f}s ({time_saved_pct:.1f}%)")
        
        # Estimate cost savings (simplified)
        # Assuming GPT-4 is 10x more expensive than GPT-3.5
        cost_ratio = 10
        queries_per_month = 10000
        
        teacher_cost = queries_per_month * cost_ratio
        student_cost = queries_per_month * 1
        cost_savings = teacher_cost - student_cost
        
        print(f"\n💰 Estimated Monthly Cost Savings (10K queries):")
        print(f"   Teacher Cost (relative): {teacher_cost} units")
        print(f"   Student Cost (relative): {student_cost} units")
        print(f"   Savings: {cost_savings} units ({(cost_savings/teacher_cost)*100:.1f}%)")
        
        # Example 5: Quality vs Speed trade-off
        print("\n" + "="*60)
        print("Example 5: Quality vs Speed Trade-off Analysis")
        print("="*60)
        
        print(f"\n🎯 Trade-off Analysis:")
        print(f"   Quality Loss: {(1 - comparison['summary']['avg_quality_retention']) * 100:.1f}%")
        print(f"   Speed Gain: {(comparison['summary']['speed_improvement'] - 1) * 100:.1f}%")
        
        # Calculate trade-off ratio
        quality_loss = (1 - comparison['summary']['avg_quality_retention'])
        speed_gain = (comparison['summary']['speed_improvement'] - 1)
        
        if quality_loss > 0:
            tradeoff_ratio = speed_gain / quality_loss
            print(f"   Trade-off Ratio: {tradeoff_ratio:.2f}x speed gain per 1% quality loss")
        
        print(f"\n💡 Recommendation:")
        if comparison['summary']['avg_quality_retention'] >= 0.85:
            print("   ✅ Student model provides excellent quality retention")
            print("   ✅ Recommended for production deployment")
        elif comparison['summary']['avg_quality_retention'] >= 0.70:
            print("   ⚠️  Student model provides acceptable quality")
            print("   ⚠️  Consider for non-critical use cases")
        else:
            print("   ⛔ Student model quality too low")
            print("   ⛔ Additional distillation or different approach needed")
        
        # Example 6: Generate full metrics report
        print("\n" + "="*60)
        print("Example 6: Comprehensive Metrics Report")
        print("="*60)
        
        report = agent.get_metrics_report()
        print(report)
        
        # Summary
        print("\n" + "="*60)
        print("SUMMARY")
        print("="*60)
        print("""
    The Model Distillation pattern demonstrates:
    
    1. Knowledge Transfer: Large teacher models transfer knowledge to smaller students
    2. Efficiency Gains: Significant speed improvements with minimal quality loss
    3. Cost Reduction: Lower inference costs for production deployment
    4. Quality Metrics: Systematic evaluation of distillation quality
    5. Trade-off Analysis: Clear understanding of quality vs efficiency
    
    Key Benefits:
    - Faster inference times (typically 2-5x improvement)
    - Reduced computational costs (often 90%+ savings)
    - Maintained accuracy (usually 80-95% retention)
    - Suitable for resource-constrained environments
    - Enables real-time applications
    
    Best Practices:
    - Use diverse training data from teacher model
    - Evaluate quality systematically on held-out test set
    - Monitor quality retention thresholds
    - Consider ensemble of student models for critical applications
    - Continuously update student model as teacher improves
        """)
    
    
    if __name__ == "__main__":
        demonstrate_model_distillation()
    
        

    Agentic Design Pattern: Model Distillation

    Pattern 144: Model Distillation – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Model Distillation pattern implementation.


    Pattern Description

    Pattern 144: Model Distillation

    Description: Implements knowledge distillation from larger teacher models to smaller student models, enabling efficient deployment while maintaining performance. This pattern transfers knowledge through response generation, logits matching, and behavior cloning.

    Components: – Teacher Model: Large, powerful model with superior capabilities – Student Model: Smaller, efficient model that learns from teacher – Distillation Process: Knowledge transfer mechanism – Performance Comparison: Evaluation and validation – Training Data Generator: Creates synthetic training examples – Knowledge Transfer Evaluator: Measures transfer quality

    Use Cases: – Deploy efficient models in resource-constrained environments – Reduce inference costs while maintaining quality – Edge deployment and mobile applications – Real-time processing requirements – Multi-model deployment strategies

    Benefits: – Reduced computational costs – Faster inference times – Smaller model footprint – Maintained accuracy – Efficient knowledge transfer

    Trade-offs: – Some performance degradation – Requires training data and time – Quality depends on teacher model – May not capture all nuances

    LangChain Implementation: Uses ChatOpenAI with different model sizes, synthetic data generation, and comparison metrics to demonstrate knowledge distillation.


    File Statistics

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

    Module Documentation

    """
    Pattern 144: Model Distillation
    
    Description:
        Implements knowledge distillation from larger teacher models to smaller student
        models, enabling efficient deployment while maintaining performance. This pattern
        transfers knowledge through response generation, logits matching, and behavior cloning.
    
    Components:
        - Teacher Model: Large, powerful model with superior capabilities
        - Student Model: Smaller, efficient model that learns from teacher
        - Distillation Process: Knowledge transfer mechanism
        - Performance Comparison: Evaluation and validation
        - Training Data Generator: Creates synthetic training examples
        - Knowledge Transfer Evaluator: Measures transfer quality
    
    Use Cases:
        - Deploy efficient models in resource-constrained environments
        - Reduce inference costs while maintaining quality
        - Edge deployment and mobile applications
        - Real-time processing requirements
        - Multi-model deployment strategies
    
    Benefits:
        - Reduced computational costs
        - Faster inference times
        - Smaller model footprint
        - Maintained accuracy
        - Efficient knowledge transfer
    
    Trade-offs:
        - Some performance degradation
        - Requires training data and time
        - Quality depends on teacher model
        - May not capture all nuances
    
    LangChain Implementation:
        Uses ChatOpenAI with different model sizes, synthetic data generation,
        and comparison metrics to demonstrate knowledge distillation.
    """
    

    Purpose: This module implements the Model Distillation 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 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
    import typing.Tuple
    

    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 – 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) – typing.Tuple: 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: DistillationExample

    Lines 59-65

    Purpose:

    Training example for distillation
    

    Class: DistillationMetrics

    Lines 69-77

    Purpose:

    Metrics for distillation performance
    

    Class: ModelProfile

    Lines 81-88

    Purpose:

    Profile of a model's characteristics
    

    Class: ModelDistillationAgent

    Lines 91-435

    Purpose:

    Agent that implements knowledge distillation from teacher to student models.
    
    The agent generates training data using a teacher model, trains a student model
    on this data, and evaluates the quality of knowledge transfer.
    

    Methods:

    __init__(self, teacher_model, student_model, temperature)

    Lines 99-123

    Purpose:

    Initialize the distillation agent.
    
    Args:
        teacher_model: Name of the teacher model (larger, more capable)
        student_model: Name of the student model (smaller, efficient)
        temperature: Sampling temperature for generation
    

    Parameters: – teacher_model: Language model instance – student_model: Language model instance – temperature: Sampling temperature (0-1)

    generate_training_data(self, domains, examples_per_domain)

    Lines 125-186

    Purpose:

    Generate synthetic training data for distillation.
    
    Args:
        domains: List of domains to generate examples for
        examples_per_domain: Number of examples per domain
    
    Returns:
        List of training examples
    

    Parameters: – domains: Domains – examples_per_domain: Examples per domain

    Returns: <ast.Subscript object at 0x000001A06D649110>

    distill_knowledge(self, examples)

    Lines 188-253

    Purpose:

    Perform knowledge distillation on examples.
    
    Args:
        examples: Training examples (uses self.examples if None)
    
    Returns:
        Distillation metrics
    

    Parameters: – examples: Examples

    Returns: DistillationMetrics

    compare_models(self, test_queries)

    Lines 255-312

    Purpose:

    Compare teacher and student model performance.
    
    Args:
        test_queries: Queries to test both models on
    
    Returns:
        Comparison results
    

    Parameters: – test_queries: Test queries

    Returns: <ast.Subscript object at 0x000001A06D65AED0>

    _evaluate_response_quality(self, query, teacher_response, student_response)

    Lines 314-360

    Purpose:

    Evaluate quality of student response compared to teacher.
    
    Args:
        query: Original query
        teacher_response: Teacher's response
        student_response: Student's response
    
    Returns:
        Quality score (0-1)
    

    Parameters: – query: Input query or question – teacher_response: Teacher response – student_response: Student response

    Returns: float

    _extract_questions(self, text, count)

    Lines 362-380

    Purpose:

    Extract questions from generated text.
    

    Parameters: – text: Text – count: Count

    Returns: <ast.Subscript object at 0x000001A06D6F7150>

    get_metrics_report(self)

    Lines 382-406

    Purpose:

    Generate a comprehensive metrics report.
    

    Parameters:

    Returns: str

    save_distilled_examples(self, filepath)

    Lines 408-435

    Purpose:

    Save distilled examples to file for future use.
    

    Parameters: – filepath: Filepath


    Functions

    demonstrate_model_distillation()

    Lines 438-593

    Purpose:

    Demonstrate the Model Distillation pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Model Distillation 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 143
    • Pattern 145

    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 Model Distillation 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: 144_model_distillation.py
    Total Lines: 598
    Pattern Category: AI Agent Design Pattern #144


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Result Memoization

    
        """
    Pattern 143: Result Memoization
    
    Description:
        The Result Memoization pattern caches computation results to avoid redundant
        processing, significantly improving performance for repeated operations. When
        an agent encounters the same input or query it has processed before, it returns
        the cached result immediately instead of recomputing. This pattern is essential
        for production AI systems where identical or similar queries occur frequently.
    
        Memoization provides dramatic performance improvements for deterministic operations,
        expensive computations, and frequently accessed data. It trades memory for speed,
        storing results with appropriate cache keys, expiration policies, and invalidation
        strategies. The pattern supports various caching strategies including LRU, TTL,
        and semantic similarity-based caching.
    
        This implementation provides comprehensive memoization capabilities including
        function-level caching, LLM response caching, semantic similarity caching,
        cache warming, invalidation strategies, and performance metrics. It handles
        both simple key-value caching and advanced semantic caching for AI operations.
    
    Components:
        - Function Memoization: Cache function results by arguments
        - LLM Response Caching: Cache model outputs for queries
        - Semantic Caching: Cache based on semantic similarity
        - Cache Warming: Preload frequently accessed results
        - Invalidation Strategy: Remove stale cache entries
        - Performance Metrics: Track cache effectiveness
    
    Use Cases:
        - Repeated queries in chatbots
        - Expensive computations
        - API call reduction
        - Database query caching
        - Embedding generation
        - Translation caching
        - Recommendation systems
        - Data processing pipelines
    
    LangChain Implementation:
        This implementation uses:
        - Python functools.lru_cache for function memoization
        - Custom cache implementations with TTL
        - Semantic similarity-based caching
        - LangChain cache integration
        - Hash-based cache keys
        - Cache metrics and monitoring
    
    Benefits:
        - Dramatically reduces latency
        - Lowers computational costs
        - Reduces API calls
        - Improves scalability
        - Provides consistent responses
        - Enables offline operation
        - Reduces load on services
    
    Trade-offs:
        - Memory overhead for cache storage
        - Stale data if not invalidated properly
        - Cache consistency challenges
        - Cold start performance
        - Complex invalidation logic
        - Potential memory leaks
    
    Production Considerations:
        - Set appropriate cache sizes
        - Implement TTL for freshness
        - Monitor cache hit rates
        - Use distributed caching for scale
        - Handle cache misses gracefully
        - Implement cache warming
        - Monitor memory usage
        - Use appropriate invalidation
        - Consider cache coherence
        - Profile cache effectiveness
        - Implement cache preloading
        - Handle cache failures
    """
    
    import os
    import time
    import hashlib
    import json
    from typing import List, Dict, Any, Optional, Callable, Tuple
    from datetime import datetime, timedelta
    from functools import wraps, lru_cache
    from collections import OrderedDict
    from dataclasses import dataclass, field
    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()
    
    
    @dataclass
    class CacheEntry:
        """Cache entry with metadata."""
        key: str
        value: Any
        created_at: datetime
        last_accessed: datetime
        access_count: int
        ttl_seconds: Optional[float]
        
        def is_expired(self) -> bool:
            """Check if entry is expired."""
            if self.ttl_seconds is None:
                return False
            age = (datetime.now() - self.created_at).total_seconds()
            return age > self.ttl_seconds
        
        def access(self):
            """Record access."""
            self.last_accessed = datetime.now()
            self.access_count += 1
    
    
    class LRUCache:
        """LRU (Least Recently Used) Cache implementation."""
        
        def __init__(self, max_size: int = 100, ttl_seconds: Optional[float] = None):
            """Initialize LRU cache."""
            self.max_size = max_size
            self.ttl_seconds = ttl_seconds
            self.cache: OrderedDict[str, CacheEntry] = OrderedDict()
            self.hits = 0
            self.misses = 0
        
        def get(self, key: str) -> Optional[Any]:
            """Get value from cache."""
            if key not in self.cache:
                self.misses += 1
                return None
            
            entry = self.cache[key]
            
            # Check if expired
            if entry.is_expired():
                del self.cache[key]
                self.misses += 1
                return None
            
            # Move to end (most recently used)
            self.cache.move_to_end(key)
            entry.access()
            self.hits += 1
            return entry.value
        
        def set(self, key: str, value: Any):
            """Set value in cache."""
            # Remove if exists
            if key in self.cache:
                del self.cache[key]
            
            # Create entry
            entry = CacheEntry(
                key=key,
                value=value,
                created_at=datetime.now(),
                last_accessed=datetime.now(),
                access_count=0,
                ttl_seconds=self.ttl_seconds
            )
            
            self.cache[key] = entry
            
            # Evict oldest if over size
            if len(self.cache) > self.max_size:
                self.cache.popitem(last=False)
        
        def clear(self):
            """Clear cache."""
            self.cache.clear()
            self.hits = 0
            self.misses = 0
        
        def get_stats(self) -> Dict[str, Any]:
            """Get cache statistics."""
            total = self.hits + self.misses
            hit_rate = self.hits / total if total > 0 else 0
            return {
                "size": len(self.cache),
                "max_size": self.max_size,
                "hits": self.hits,
                "misses": self.misses,
                "hit_rate": f"{hit_rate:.2%}",
                "total_requests": total
            }
    
    
    class SemanticCache:
        """Cache based on semantic similarity of inputs."""
        
        def __init__(self, similarity_threshold: float = 0.9, max_size: int = 100):
            """Initialize semantic cache."""
            self.similarity_threshold = similarity_threshold
            self.max_size = max_size
            self.entries: List[Tuple[str, Any, str]] = []  # (key, value, original_text)
            self.hits = 0
            self.misses = 0
        
        def _compute_similarity(self, text1: str, text2: str) -> float:
            """Compute simple similarity (in production, use embeddings)."""
            # Simple word overlap similarity
            words1 = set(text1.lower().split())
            words2 = set(text2.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 from cache based on semantic similarity."""
            for key, value, original_text in self.entries:
                similarity = self._compute_similarity(query, original_text)
                if similarity >= self.similarity_threshold:
                    self.hits += 1
                    return value
            
            self.misses += 1
            return None
        
        def set(self, query: str, value: Any):
            """Set value in cache."""
            key = hashlib.md5(query.encode()).hexdigest()
            self.entries.append((key, value, query))
            
            # Evict oldest if over size
            if len(self.entries) > self.max_size:
                self.entries.pop(0)
        
        def get_stats(self) -> Dict[str, Any]:
            """Get cache statistics."""
            total = self.hits + self.misses
            hit_rate = self.hits / total if total > 0 else 0
            return {
                "size": len(self.entries),
                "max_size": self.max_size,
                "hits": self.hits,
                "misses": self.misses,
                "hit_rate": f"{hit_rate:.2%}",
                "similarity_threshold": self.similarity_threshold
            }
    
    
    def memoize(cache: Optional[LRUCache] = None, ttl_seconds: Optional[float] = None):
        """Decorator for function memoization."""
        if cache is None:
            cache = LRUCache(ttl_seconds=ttl_seconds)
        
        def decorator(func: Callable) -> Callable:
            @wraps(func)
            def wrapper(*args, **kwargs):
                # Create cache key from arguments
                key_data = {
                    "func": func.__name__,
                    "args": args,
                    "kwargs": kwargs
                }
                key = hashlib.md5(
                    json.dumps(key_data, sort_keys=True, default=str).encode()
                ).hexdigest()
                
                # Check cache
                cached_result = cache.get(key)
                if cached_result is not None:
                    return cached_result
                
                # Compute and cache
                result = func(*args, **kwargs)
                cache.set(key, result)
                return result
            
            wrapper.cache = cache
            return wrapper
        
        return decorator
    
    
    class MemoizationAgent:
        """
        Agent that uses memoization for performance optimization.
        
        This agent caches results to avoid redundant computation,
        dramatically improving performance for repeated operations.
        """
        
        def __init__(self, temperature: float = 0.3):
            """Initialize the memoization agent."""
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            
            # Different caches for different purposes
            self.llm_cache = LRUCache(max_size=100, ttl_seconds=3600)
            self.computation_cache = LRUCache(max_size=50, ttl_seconds=None)
            self.semantic_cache = SemanticCache(similarity_threshold=0.85)
            
            # Create LLM chain (we'll cache its results)
            self.chain = (
                ChatPromptTemplate.from_template("{query}")
                | self.llm
                | StrOutputParser()
            )
        
        def query_llm_cached(self, query: str, use_semantic: bool = False) -> str:
            """Query LLM with caching."""
            # Create cache key
            cache_key = hashlib.md5(query.encode()).hexdigest()
            
            # Try semantic cache first if enabled
            if use_semantic:
                result = self.semantic_cache.get(query)
                if result:
                    return f"[SEMANTIC CACHE HIT] {result}"
            
            # Try exact cache
            result = self.llm_cache.get(cache_key)
            if result:
                return f"[CACHE HIT] {result}"
            
            # Cache miss - query LLM
            result = self.chain.invoke({"query": query})
            
            # Store in caches
            self.llm_cache.set(cache_key, result)
            if use_semantic:
                self.semantic_cache.set(query, result)
            
            return f"[CACHE MISS] {result}"
        
        @memoize(ttl_seconds=300)
        def expensive_computation(self, value: int) -> int:
            """Simulate expensive computation with memoization."""
            time.sleep(0.5)  # Simulate expensive work
            return value ** 2
        
        def cache_warm(self, common_queries: List[str]):
            """Warm up cache with common queries."""
            print(f"Warming cache with {len(common_queries)} common queries...")
            for query in common_queries:
                result = self.chain.invoke({"query": query})
                cache_key = hashlib.md5(query.encode()).hexdigest()
                self.llm_cache.set(cache_key, result)
                self.semantic_cache.set(query, result)
            print("Cache warming complete")
        
        def invalidate_cache(self, pattern: Optional[str] = None):
            """Invalidate cache entries."""
            if pattern is None:
                # Clear all caches
                self.llm_cache.clear()
                self.computation_cache.clear()
                print("All caches cleared")
            else:
                print(f"Pattern-based invalidation not implemented for: {pattern}")
        
        def get_all_stats(self) -> Dict[str, Any]:
            """Get statistics for all caches."""
            return {
                "llm_cache": self.llm_cache.get_stats(),
                "computation_cache": self.computation_cache.get_stats(),
                "semantic_cache": self.semantic_cache.get_stats()
            }
    
    
    def demonstrate_memoization():
        """Demonstrate the result memoization pattern."""
        print("=" * 80)
        print("Result Memoization Pattern Demonstration")
        print("=" * 80)
        
        agent = MemoizationAgent()
        
        # Demonstration 1: Basic Function Memoization
        print("\n" + "=" * 80)
        print("Demonstration 1: Basic Function Memoization")
        print("=" * 80)
        
        print("\nFirst call (cache miss, will take 0.5s):")
        start = time.time()
        result1 = agent.expensive_computation(10)
        elapsed1 = time.time() - start
        print(f"Result: {result1}, Time: {elapsed1:.3f}s")
        
        print("\nSecond call with same argument (cache hit, instant):")
        start = time.time()
        result2 = agent.expensive_computation(10)
        elapsed2 = time.time() - start
        print(f"Result: {result2}, Time: {elapsed2:.3f}s")
        
        print(f"\nSpeedup: {elapsed1/elapsed2:.1f}x faster")
        
        # Demonstration 2: LLM Response Caching
        print("\n" + "=" * 80)
        print("Demonstration 2: LLM Response Caching")
        print("=" * 80)
        
        query = "What is 2+2?"
        
        print(f"\nQuery: {query}")
        print("First call (cache miss):")
        start = time.time()
        response1 = agent.query_llm_cached(query)
        elapsed1 = time.time() - start
        print(f"Response: {response1[:100]}...")
        print(f"Time: {elapsed1:.3f}s")
        
        print("\nSecond call (cache hit):")
        start = time.time()
        response2 = agent.query_llm_cached(query)
        elapsed2 = time.time() - start
        print(f"Response: {response2[:100]}...")
        print(f"Time: {elapsed2:.3f}s")
        
        # Demonstration 3: Semantic Caching
        print("\n" + "=" * 80)
        print("Demonstration 3: Semantic Caching")
        print("=" * 80)
        
        query1 = "What is the capital of France?"
        query2 = "Tell me the capital city of France"  # Similar semantic meaning
        
        print(f"\nFirst query: {query1}")
        response1 = agent.query_llm_cached(query1, use_semantic=True)
        print(f"Response: {response1[:80]}...")
        
        print(f"\nSimilar query: {query2}")
        response2 = agent.query_llm_cached(query2, use_semantic=True)
        print(f"Response: {response2[:80]}...")
        
        if "[SEMANTIC CACHE HIT]" in response2:
            print("\n✓ Semantic cache successfully matched similar query!")
        
        # Demonstration 4: Cache Warming
        print("\n" + "=" * 80)
        print("Demonstration 4: Cache Warming")
        print("=" * 80)
        
        common_queries = [
            "What is the weather?",
            "Tell me a joke",
            "Hello, how are you?"
        ]
        
        agent.cache_warm(common_queries)
        
        print("\nQuerying warmed cache:")
        for query in common_queries[:2]:
            response = agent.query_llm_cached(query)
            print(f"  {query}: {response[:60]}...")
        
        # Demonstration 5: TTL Expiration
        print("\n" + "=" * 80)
        print("Demonstration 5: TTL (Time-To-Live) Expiration")
        print("=" * 80)
        
        # Create cache with short TTL
        short_ttl_cache = LRUCache(max_size=10, ttl_seconds=2)
        
        @memoize(cache=short_ttl_cache)
        def short_lived_computation(x):
            time.sleep(0.1)
            return x * 2
        
        print("\nComputation with 2-second TTL:")
        print("First call:")
        result1 = short_lived_computation(5)
        print(f"Result: {result1}")
        
        print("\nImmediate second call (within TTL):")
        result2 = short_lived_computation(5)
        print(f"Result: {result2} (cached)")
        
        print("\nWaiting 2.5 seconds for TTL expiration...")
        time.sleep(2.5)
        
        print("Third call (after TTL expiration):")
        result3 = short_lived_computation(5)
        print(f"Result: {result3} (recomputed)")
        
        # Demonstration 6: LRU Eviction
        print("\n" + "=" * 80)
        print("Demonstration 6: LRU Cache Eviction")
        print("=" * 80)
        
        small_cache = LRUCache(max_size=3)
        
        print("\nCache with max size 3:")
        print("Adding 5 items...")
        for i in range(5):
            small_cache.set(f"key_{i}", f"value_{i}")
            print(f"  Added key_{i}, cache size: {len(small_cache.cache)}")
        
        print(f"\nFinal cache size: {len(small_cache.cache)} (oldest evicted)")
        print(f"Remaining keys: {list(small_cache.cache.keys())}")
        
        # Demonstration 7: Cache Invalidation
        print("\n" + "=" * 80)
        print("Demonstration 7: Cache Invalidation")
        print("=" * 80)
        
        print("\nQuerying LLM to populate cache...")
        agent.query_llm_cached("Test query 1")
        agent.query_llm_cached("Test query 2")
        
        stats_before = agent.get_all_stats()
        print(f"LLM cache size before: {stats_before['llm_cache']['size']}")
        
        print("\nInvalidating all caches...")
        agent.invalidate_cache()
        
        stats_after = agent.get_all_stats()
        print(f"LLM cache size after: {stats_after['llm_cache']['size']}")
        
        # Demonstration 8: Cache Statistics
        print("\n" + "=" * 80)
        print("Demonstration 8: Cache Performance Statistics")
        print("=" * 80)
        
        # Generate some cache activity
        for i in range(10):
            agent.query_llm_cached(f"Query {i % 3}")  # Repeat some queries
        
        stats = agent.get_all_stats()
        
        print("\nLLM Cache Statistics:")
        for key, value in stats['llm_cache'].items():
            print(f"  {key}: {value}")
        
        print("\nSemantic Cache Statistics:")
        for key, value in stats['semantic_cache'].items():
            print(f"  {key}: {value}")
        
        # Summary
        print("\n" + "=" * 80)
        print("Summary: Result Memoization Pattern")
        print("=" * 80)
        print("""
    The Result Memoization pattern dramatically improves performance through caching:
    
    Key Features Demonstrated:
    1. Function Memoization - Cache expensive computations
    2. LLM Response Caching - Avoid redundant API calls
    3. Semantic Caching - Match similar queries
    4. Cache Warming - Preload common queries
    5. TTL Expiration - Auto-invalidate stale data
    6. LRU Eviction - Manage cache size
    7. Cache Invalidation - Clear stale entries
    8. Performance Metrics - Track cache effectiveness
    
    Benefits:
    - Dramatically reduces latency
    - Lowers computational costs
    - Reduces API calls
    - Improves scalability
    - Provides consistent responses
    - Reduces load on services
    
    Best Practices:
    - Set appropriate cache sizes
    - Implement TTL for freshness
    - Monitor cache hit rates
    - Use distributed caching for scale
    - Handle cache misses gracefully
    - Implement cache warming
    - Monitor memory usage
    - Use appropriate invalidation
    - Consider cache coherence
    - Profile cache effectiveness
    
    Common Use Cases:
    - Repeated queries in chatbots
    - Expensive computations
    - API call reduction
    - Database query caching
    - Embedding generation
    - Translation caching
    - Recommendation systems
    - Data processing pipelines
    
    Performance Considerations:
    - Memory overhead for cache storage
    - Balance cache size vs. hit rate
    - Implement appropriate eviction policies
    - Monitor for stale data
    - Consider distributed caching
    - Profile actual performance gains
    
    This pattern is essential for production AI systems requiring high performance
    and cost optimization through intelligent caching.
    """)
    
    
    if __name__ == "__main__":
        demonstrate_memoization()
    
        

    Agentic Design Pattern: Result Memoization

    Pattern 143: Result Memoization – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Result Memoization pattern implementation.


    Pattern Description

    Pattern 143: Result Memoization

    Description: The Result Memoization pattern caches computation results to avoid redundant processing, significantly improving performance for repeated operations. When an agent encounters the same input or query it has processed before, it returns the cached result immediately instead of recomputing. This pattern is essential for production AI systems where identical or similar queries occur frequently.

    Memoization provides dramatic performance improvements for deterministic operations,
    expensive computations, and frequently accessed data. It trades memory for speed,
    storing results with appropriate cache keys, expiration policies, and invalidation
    strategies. The pattern supports various caching strategies including LRU, TTL,
    and semantic similarity-based caching.
    
    This implementation provides comprehensive memoization capabilities including
    function-level caching, LLM response caching, semantic similarity caching,
    cache warming, invalidation strategies, and performance metrics. It handles
    both simple key-value caching and advanced semantic caching for AI operations.
    

    Components: – Function Memoization: Cache function results by arguments – LLM Response Caching: Cache model outputs for queries – Semantic Caching: Cache based on semantic similarity – Cache Warming: Preload frequently accessed results – Invalidation Strategy: Remove stale cache entries – Performance Metrics: Track cache effectiveness

    Use Cases: – Repeated queries in chatbots – Expensive computations – API call reduction – Database query caching – Embedding generation – Translation caching – Recommendation systems – Data processing pipelines

    LangChain Implementation: This implementation uses: – Python functools.lru_cache for function memoization – Custom cache implementations with TTL – Semantic similarity-based caching – LangChain cache integration – Hash-based cache keys – Cache metrics and monitoring

    Benefits: – Dramatically reduces latency – Lowers computational costs – Reduces API calls – Improves scalability – Provides consistent responses – Enables offline operation – Reduces load on services

    Trade-offs: – Memory overhead for cache storage – Stale data if not invalidated properly – Cache consistency challenges – Cold start performance – Complex invalidation logic – Potential memory leaks

    Production Considerations: – Set appropriate cache sizes – Implement TTL for freshness – Monitor cache hit rates – Use distributed caching for scale – Handle cache misses gracefully – Implement cache warming – Monitor memory usage – Use appropriate invalidation – Consider cache coherence – Profile cache effectiveness – Implement cache preloading – Handle cache failures


    File Statistics

    • Total Lines: 603
    • Classes: 4
    • Functions: 2
    • Constants: 0

    Module Documentation

    """
    Pattern 143: Result Memoization
    
    Description:
        The Result Memoization pattern caches computation results to avoid redundant
        processing, significantly improving performance for repeated operations. When
        an agent encounters the same input or query it has processed before, it returns
        the cached result immediately instead of recomputing. This pattern is essential
        for production AI systems where identical or similar queries occur frequently.
    
        Memoization provides dramatic performance improvements for deterministic operations,
        expensive computations, and frequently accessed data. It trades memory for speed,
        storing results with appropriate cache keys, expiration policies, and invalidation
        strategies. The pattern supports various caching strategies including LRU, TTL,
        and semantic similarity-based caching.
    
        This implementation provides comprehensive memoization capabilities including
        function-level caching, LLM response caching, semantic similarity caching,
        cache warming, invalidation strategies, and performance metrics. It handles
        both simple key-value caching and advanced semantic caching for AI operations.
    
    Components:
        - Function Memoization: Cache function results by arguments
        - LLM Response Caching: Cache model outputs for queries
        - Semantic Caching: Cache based on semantic similarity
        - Cache Warming: Preload frequently accessed results
        - Invalidation Strategy: Remove stale cache entries
        - Performance Metrics: Track cache effectiveness
    
    Use Cases:
        - Repeated queries in chatbots
        - Expensive computations
        - API call reduction
        - Database query caching
        - Embedding generation
        - Translation caching
        - Recommendation systems
        - Data processing pipelines
    
    LangChain Implementation:
        This implementation uses:
        - Python functools.lru_cache for function memoization
        - Custom cache implementations with TTL
        - Semantic similarity-based caching
        - LangChain cache integration
        - Hash-based cache keys
        - Cache metrics and monitoring
    
    Benefits:
        - Dramatically reduces latency
        - Lowers computational costs
        - Reduces API calls
        - Improves scalability
        - Provides consistent responses
        - Enables offline operation
        - Reduces load on services
    
    Trade-offs:
        - Memory overhead for cache storage
        - Stale data if not invalidated properly
        - Cache consistency challenges
        - Cold start performance
        - Complex invalidation logic
        - Potential memory leaks
    
    Production Considerations:
        - Set appropriate cache sizes
        - Implement TTL for freshness
        - Monitor cache hit rates
        - Use distributed caching for scale
        - Handle cache misses gracefully
        - Implement cache warming
        - Monitor memory usage
        - Use appropriate invalidation
        - Consider cache coherence
        - Profile cache effectiveness
        - Implement cache preloading
        - Handle cache failures
    """
    

    Purpose: This module implements the Result Memoization 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 functools.lru_cache
    import functools.wraps
    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
    import typing.Tuple
    

    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 – functools.lru_cache: Higher-order functions and operations on callable objects – functools.wraps: Higher-order functions and operations on callable objects – 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) – typing.Tuple: 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 hashlib
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: CacheEntry

    Lines 100-119

    Purpose:

    Cache entry with metadata.
    

    Methods:

    is_expired(self)

    Lines 109-114

    Purpose:

    Check if entry is expired.
    

    Parameters:

    Returns: bool

    access(self)

    Lines 116-119

    Purpose:

    Record access.
    

    Parameters:


    Class: LRUCache

    Lines 122-192

    Purpose:

    LRU (Least Recently Used) Cache implementation.
    

    Methods:

    __init__(self, max_size, ttl_seconds)

    Lines 125-131

    Purpose:

    Initialize LRU cache.
    

    Parameters: – max_size: Max size – ttl_seconds: Ttl seconds

    get(self, key)

    Lines 133-151

    Purpose:

    Get value from cache.
    

    Parameters: – key: Key

    Returns: <ast.Subscript object at 0x000001A06D811350>

    set(self, key, value)

    Lines 153-173

    Purpose:

    Set value in cache.
    

    Parameters: – key: Key – value: Value

    clear(self)

    Lines 175-179

    Purpose:

    Clear cache.
    

    Parameters:

    get_stats(self)

    Lines 181-192

    Purpose:

    Get cache statistics.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D164C10>


    Class: SemanticCache

    Lines 195-251

    Purpose:

    Cache based on semantic similarity of inputs.
    

    Methods:

    __init__(self, similarity_threshold, max_size)

    Lines 198-204

    Purpose:

    Initialize semantic cache.
    

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

    _compute_similarity(self, text1, text2)

    Lines 206-218

    Purpose:

    Compute simple similarity (in production, use embeddings).
    

    Parameters: – text1: Text1 – text2: Text2

    Returns: float

    get(self, query)

    Lines 220-229

    Purpose:

    Get value from cache based on semantic similarity.
    

    Parameters: – query: Input query or question

    Returns: <ast.Subscript object at 0x000001A06D669D10>

    set(self, query, value)

    Lines 231-238

    Purpose:

    Set value in cache.
    

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

    get_stats(self)

    Lines 240-251

    Purpose:

    Get cache statistics.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D859510>


    Class: MemoizationAgent

    Lines 288-370

    Purpose:

    Agent that uses memoization for performance optimization.
    
    This agent caches results to avoid redundant computation,
    dramatically improving performance for repeated operations.
    

    Methods:

    __init__(self, temperature)

    Lines 296-310

    Purpose:

    Initialize the memoization agent.
    

    Parameters: – temperature: Sampling temperature (0-1)

    query_llm_cached(self, query, use_semantic)

    Lines 312-336

    Purpose:

    Query LLM with caching.
    

    Parameters: – query: Input query or question – use_semantic: Use semantic

    Returns: str

    expensive_computation(self, value)

    Lines 339-342

    Decorators:

    Purpose:

    Simulate expensive computation with memoization.
    

    Parameters: – value: Value

    Returns: int

    cache_warm(self, common_queries)

    Lines 344-352

    Purpose:

    Warm up cache with common queries.
    

    Parameters: – common_queries: Common queries

    invalidate_cache(self, pattern)

    Lines 354-362

    Purpose:

    Invalidate cache entries.
    

    Parameters: – pattern: Pattern

    get_all_stats(self)

    Lines 364-370

    Purpose:

    Get statistics for all caches.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D605C10>


    Functions

    memoize(cache, ttl_seconds)

    Lines 254-285

    Purpose:

    Decorator for function memoization.
    

    Parameters: – cache: Cache – ttl_seconds: Ttl seconds

    demonstrate_memoization()

    Lines 373-598

    Purpose:

    Demonstrate the result memoization pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Result Memoization 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 142
    • Pattern 144

    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 Result Memoization 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: 143_result_memoization.py
    Total Lines: 603
    Pattern Category: AI Agent Design Pattern #143


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Speculative Execution

    
        """
    Pattern 142: Speculative Execution
    
    Description:
        The Speculative Execution pattern optimizes performance by predicting and executing
        likely operations before they're actually needed, enabling parallel processing and
        reduced latency. The agent anticipates probable next steps and begins computation
        proactively, discarding results if predictions prove incorrect. This pattern is
        particularly valuable for operations with high latency or when multiple potential
        paths exist.
    
        Speculative execution leverages parallelism to compute multiple branches
        simultaneously, betting that at least one will be correct. When predictions succeed,
        the agent gains significant performance improvements by having results ready
        immediately. When wrong, the cost is the wasted computation, which must be balanced
        against the benefits.
    
        This implementation provides speculative execution capabilities including predictive
        computation, parallel branch execution, result management, confidence-based
        speculation, and waste tracking. It supports both simple lookahead and complex
        multi-path speculation with cancellation mechanisms.
    
    Components:
        - Predictive Computation: Anticipate likely operations
        - Parallel Execution: Run multiple possibilities simultaneously
        - Result Management: Handle multiple speculative results
        - Confidence-Based: Speculate based on prediction confidence
        - Waste Tracking: Monitor efficiency of speculation
        - Cancellation: Stop incorrect speculations early
    
    Use Cases:
        - High-latency operations (API calls, database queries)
        - Branching workflows with predictable paths
        - User interaction prediction
        - Data prefetching
        - Multi-modal processing
        - Recommendation systems
        - Search optimization
        - Decision trees
    
    LangChain Implementation:
        This implementation uses:
        - Parallel chain execution with asyncio
        - Predictive path selection with LLM
        - Speculative result caching
        - Confidence-based speculation triggers
        - Waste tracking and efficiency metrics
        - Result selection and fallback
    
    Benefits:
        - Reduces perceived latency
        - Improves responsiveness
        - Enables parallel processing
        - Optimizes resource utilization
        - Handles uncertainty gracefully
        - Provides fallback options
        - Accelerates common paths
    
    Trade-offs:
        - Wastes resources on incorrect predictions
        - Increases complexity
        - May overload systems
        - Requires careful tuning
        - Can mask performance issues
        - Needs good prediction models
        - Risk of cascade failures
    
    Production Considerations:
        - Monitor speculation accuracy
        - Set resource limits
        - Implement cancellation
        - Track waste metrics
        - Use confidence thresholds
        - Limit speculation depth
        - Handle failures gracefully
        - Provide fallback paths
        - Consider cost implications
        - Test prediction models
        - Monitor system load
        - Balance parallelism
    """
    
    import os
    import time
    import asyncio
    from typing import List, Dict, Any, Optional, Callable, Tuple
    from datetime import datetime
    from enum import Enum
    from dataclasses import dataclass, field
    from concurrent.futures import ThreadPoolExecutor, Future
    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 SpeculationStatus(Enum):
        """Status of speculative execution."""
        PENDING = "pending"
        RUNNING = "running"
        COMPLETED = "completed"
        CANCELLED = "cancelled"
        FAILED = "failed"
        USED = "used"
        WASTED = "wasted"
    
    
    @dataclass
    class SpeculativeTask:
        """Represents a speculative task."""
        task_id: str
        path_id: str
        computation: Callable[[], Any]
        confidence: float
        status: SpeculationStatus = SpeculationStatus.PENDING
        result: Optional[Any] = None
        start_time: Optional[datetime] = None
        end_time: Optional[datetime] = None
        execution_time: Optional[float] = None
        
        def execute(self) -> Any:
            """Execute the speculative task."""
            self.status = SpeculationStatus.RUNNING
            self.start_time = datetime.now()
            try:
                self.result = self.computation()
                self.status = SpeculationStatus.COMPLETED
            except Exception as e:
                self.status = SpeculationStatus.FAILED
                self.result = None
            finally:
                self.end_time = datetime.now()
                if self.start_time and self.end_time:
                    self.execution_time = (self.end_time - self.start_time).total_seconds()
            return self.result
    
    
    @dataclass
    class SpeculationMetrics:
        """Metrics for speculative execution."""
        total_speculations: int = 0
        successful: int = 0
        wasted: int = 0
        cancelled: int = 0
        failed: int = 0
        total_time_saved: float = 0.0
        total_time_wasted: float = 0.0
        
        @property
        def success_rate(self) -> float:
            """Calculate speculation success rate."""
            total = self.successful + self.wasted
            return self.successful / total if total > 0 else 0.0
        
        @property
        def efficiency(self) -> float:
            """Calculate efficiency (time saved vs wasted)."""
            total = self.total_time_saved + self.total_time_wasted
            return self.total_time_saved / total if total > 0 else 0.0
    
    
    class SpeculativeExecutionAgent:
        """
        Agent that uses speculative execution for performance optimization.
        
        This agent anticipates likely operations and executes them proactively,
        reducing latency when predictions are correct.
        """
        
        def __init__(self, temperature: float = 0.3, max_workers: int = 4):
            """Initialize the speculative execution agent."""
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.max_workers = max_workers
            self.executor = ThreadPoolExecutor(max_workers=max_workers)
            self.active_tasks: Dict[str, SpeculativeTask] = {}
            self.metrics = SpeculationMetrics()
            
            # Speculation thresholds
            self.min_confidence = 0.3  # Minimum confidence to speculate
            self.max_parallel = 3  # Maximum parallel speculations
            
            # Create path prediction chain
            prediction_prompt = ChatPromptTemplate.from_template(
                """You are an execution path predictor. Predict the most likely next steps.
    
    Current Context: {context}
    Current State: {state}
    Available Paths: {available_paths}
    
    Based on the context and patterns, predict which paths are most likely to be taken.
    
    Respond in this format:
    PATH_1: path_name (confidence: 0-1)
    PATH_2: path_name (confidence: 0-1)
    PATH_3: path_name (confidence: 0-1)
    REASONING: explanation"""
            )
            self.prediction_chain = prediction_prompt | self.llm | StrOutputParser()
        
        def predict_paths(
            self,
            context: Dict[str, Any],
            available_paths: List[str]
        ) -> List[Tuple[str, float]]:
            """
            Predict likely execution paths with confidence scores.
            
            Returns:
                List of (path_name, confidence) tuples
            """
            try:
                result = self.prediction_chain.invoke({
                    "context": str(context),
                    "state": context.get("current_state", "unknown"),
                    "available_paths": ", ".join(available_paths)
                })
                
                # Parse predictions
                predictions = []
                for line in result.split('\n'):
                    if line.startswith("PATH_"):
                        parts = line.split(":")
                        if len(parts) >= 2:
                            path_part = parts[1].strip()
                            # Extract path name and confidence
                            if "(confidence:" in path_part:
                                path_name = path_part.split("(confidence:")[0].strip()
                                confidence_str = path_part.split("(confidence:")[1].split(")")[0].strip()
                                try:
                                    confidence = float(confidence_str)
                                    predictions.append((path_name, confidence))
                                except ValueError:
                                    pass
                
                return predictions[:self.max_parallel]
                
            except Exception as e:
                print(f"Error predicting paths: {e}")
                # Default: equal confidence for all paths
                return [(path, 1.0 / len(available_paths)) for path in available_paths[:self.max_parallel]]
        
        def speculate(
            self,
            paths: Dict[str, Callable[[], Any]],
            context: Dict[str, Any]
        ) -> Dict[str, Future]:
            """
            Execute multiple paths speculatively in parallel.
            
            Args:
                paths: Dictionary of path_name -> computation function
                context: Context for path prediction
                
            Returns:
                Dictionary of path_name -> Future
            """
            # Predict likely paths
            predictions = self.predict_paths(context, list(paths.keys()))
            
            # Filter by confidence threshold
            valid_predictions = [
                (path, conf) for path, conf in predictions
                if conf >= self.min_confidence and path in paths
            ]
            
            # Submit speculative tasks
            futures = {}
            for path_name, confidence in valid_predictions:
                task = SpeculativeTask(
                    task_id=f"spec_{len(self.active_tasks)}_{time.time()}",
                    path_id=path_name,
                    computation=paths[path_name],
                    confidence=confidence
                )
                self.active_tasks[task.task_id] = task
                futures[path_name] = self.executor.submit(task.execute)
                self.metrics.total_speculations += 1
            
            return futures
        
        def select_result(
            self,
            futures: Dict[str, Future],
            actual_path: str,
            timeout: float = 10.0
        ) -> Any:
            """
            Select the result from the actual path taken.
            
            Args:
                futures: Dictionary of path_name -> Future
                actual_path: The path that was actually taken
                timeout: Maximum time to wait for result
                
            Returns:
                Result from the actual path
            """
            if actual_path not in futures:
                raise ValueError(f"Actual path {actual_path} not in futures")
            
            # Wait for the actual path result
            actual_future = futures[actual_path]
            try:
                result = actual_future.result(timeout=timeout)
                
                # Update metrics for actual path
                actual_task = self._find_task_by_path(actual_path)
                if actual_task:
                    actual_task.status = SpeculationStatus.USED
                    self.metrics.successful += 1
                    if actual_task.execution_time:
                        # Time saved is the time it would have taken if we waited
                        self.metrics.total_time_saved += actual_task.execution_time
                
                # Cancel and mark other paths as wasted
                for path_name, future in futures.items():
                    if path_name != actual_path:
                        future.cancel()
                        task = self._find_task_by_path(path_name)
                        if task:
                            if task.status == SpeculationStatus.COMPLETED:
                                task.status = SpeculationStatus.WASTED
                                self.metrics.wasted += 1
                                if task.execution_time:
                                    self.metrics.total_time_wasted += task.execution_time
                            else:
                                task.status = SpeculationStatus.CANCELLED
                                self.metrics.cancelled += 1
                
                return result
                
            except Exception as e:
                self.metrics.failed += 1
                raise e
        
        def _find_task_by_path(self, path_name: str) -> Optional[SpeculativeTask]:
            """Find task by path name."""
            for task in self.active_tasks.values():
                if task.path_id == path_name:
                    return task
            return None
        
        def speculative_search(
            self,
            query: str,
            search_strategies: Dict[str, Callable[[str], Any]]
        ) -> Any:
            """
            Speculatively execute multiple search strategies.
            
            Args:
                query: Search query
                search_strategies: Dictionary of strategy_name -> search function
                
            Returns:
                Best search result
            """
            context = {"query": query, "query_type": "search"}
            
            # Create speculation tasks
            tasks = {
                name: lambda name=name: strategy(query)
                for name, strategy in search_strategies.items()
            }
            
            # Execute speculatively
            futures = self.speculate(tasks, context)
            
            # For search, we take the first one that completes
            completed_results = {}
            for name, future in futures.items():
                try:
                    result = future.result(timeout=5.0)
                    completed_results[name] = result
                except Exception:
                    pass
            
            # Return first completed result
            if completed_results:
                first_strategy = list(completed_results.keys())[0]
                return self.select_result(futures, first_strategy, timeout=0.1)
            
            return None
        
        def prefetch_data(
            self,
            likely_requests: List[str],
            fetch_function: Callable[[str], Any],
            confidence_scores: Optional[List[float]] = None
        ) -> Dict[str, Future]:
            """
            Prefetch data for likely requests.
            
            Args:
                likely_requests: List of likely data requests
                fetch_function: Function to fetch data
                confidence_scores: Optional confidence for each request
                
            Returns:
                Dictionary of request -> Future
            """
            if confidence_scores is None:
                confidence_scores = [1.0 / len(likely_requests)] * len(likely_requests)
            
            futures = {}
            for request, confidence in zip(likely_requests, confidence_scores):
                if confidence >= self.min_confidence:
                    task = SpeculativeTask(
                        task_id=f"prefetch_{len(self.active_tasks)}_{time.time()}",
                        path_id=request,
                        computation=lambda req=request: fetch_function(req),
                        confidence=confidence
                    )
                    self.active_tasks[task.task_id] = task
                    futures[request] = self.executor.submit(task.execute)
                    self.metrics.total_speculations += 1
            
            return futures
        
        def get_metrics(self) -> Dict[str, Any]:
            """Get speculation metrics."""
            return {
                "total_speculations": self.metrics.total_speculations,
                "successful": self.metrics.successful,
                "wasted": self.metrics.wasted,
                "cancelled": self.metrics.cancelled,
                "failed": self.metrics.failed,
                "success_rate": f"{self.metrics.success_rate:.2%}",
                "efficiency": f"{self.metrics.efficiency:.2%}",
                "time_saved": f"{self.metrics.total_time_saved:.2f}s",
                "time_wasted": f"{self.metrics.total_time_wasted:.2f}s",
                "net_benefit": f"{self.metrics.total_time_saved - self.metrics.total_time_wasted:.2f}s"
            }
        
        def shutdown(self):
            """Shutdown the executor."""
            self.executor.shutdown(wait=True)
    
    
    def demonstrate_speculative_execution():
        """Demonstrate the speculative execution pattern."""
        print("=" * 80)
        print("Speculative Execution Pattern Demonstration")
        print("=" * 80)
        
        agent = SpeculativeExecutionAgent(max_workers=4)
        
        try:
            # Demonstration 1: Basic Speculative Execution
            print("\n" + "=" * 80)
            print("Demonstration 1: Basic Speculative Execution")
            print("=" * 80)
            
            def path_a():
                time.sleep(0.5)
                return "Result from Path A"
            
            def path_b():
                time.sleep(0.5)
                return "Result from Path B"
            
            def path_c():
                time.sleep(0.5)
                return "Result from Path C"
            
            paths = {
                "path_a": path_a,
                "path_b": path_b,
                "path_c": path_c
            }
            
            context = {"current_state": "initial", "user_preference": "fast"}
            
            print("\nStarting speculative execution of 3 paths...")
            start = time.time()
            futures = agent.speculate(paths, context)
            print(f"Speculated paths: {list(futures.keys())}")
            
            # Simulate that path_b was the actual path taken
            actual_path = "path_b"
            print(f"\nActual path taken: {actual_path}")
            
            result = agent.select_result(futures, actual_path)
            elapsed = time.time() - start
            
            print(f"Result: {result}")
            print(f"Total time: {elapsed:.2f}s (with speculation)")
            print("Without speculation, would have taken 0.5s after decision")
            
            # Demonstration 2: Confidence-Based Speculation
            print("\n" + "=" * 80)
            print("Demonstration 2: Confidence-Based Speculation")
            print("=" * 80)
            
            def operation_high_conf():
                time.sleep(0.3)
                return "High confidence result"
            
            def operation_low_conf():
                time.sleep(0.3)
                return "Low confidence result"
            
            # Manually set predictions with different confidences
            print("\nPredictions:")
            print("  Path 1 (high confidence): 0.8")
            print("  Path 2 (low confidence): 0.2")
            
            paths = {
                "high_conf": operation_high_conf,
                "low_conf": operation_low_conf
            }
            
            # Only high confidence should be speculated
            agent.min_confidence = 0.5
            futures = agent.speculate(paths, {"confidence_test": True})
            
            print(f"\nSpeculated paths (min confidence 0.5): {list(futures.keys())}")
            
            # Demonstration 3: Prefetching
            print("\n" + "=" * 80)
            print("Demonstration 3: Data Prefetching")
            print("=" * 80)
            
            def fetch_data(item_id: str) -> str:
                """Simulate data fetching."""
                time.sleep(0.2)
                return f"Data for {item_id}"
            
            likely_requests = ["item_1", "item_2", "item_3"]
            confidence_scores = [0.7, 0.5, 0.3]
            
            print(f"Likely requests: {likely_requests}")
            print(f"Confidence scores: {confidence_scores}")
            print(f"Prefetching data speculatively...")
            
            prefetch_futures = agent.prefetch_data(
                likely_requests,
                fetch_data,
                confidence_scores
            )
            
            print(f"Prefetched: {list(prefetch_futures.keys())}")
            
            # User actually requests item_2
            actual_request = "item_2"
            print(f"\nUser requests: {actual_request}")
            
            if actual_request in prefetch_futures:
                result = agent.select_result(prefetch_futures, actual_request, timeout=1.0)
                print(f"Result (from prefetch): {result}")
            
            # Demonstration 4: Multi-Path Processing
            print("\n" + "=" * 80)
            print("Demonstration 4: Multi-Path Processing")
            print("=" * 80)
            
            def process_with_method_a(data: str) -> str:
                time.sleep(0.4)
                return f"Processed {data} with method A"
            
            def process_with_method_b(data: str) -> str:
                time.sleep(0.4)
                return f"Processed {data} with method B"
            
            def process_with_method_c(data: str) -> str:
                time.sleep(0.4)
                return f"Processed {data} with method C"
            
            data = "sample_data"
            methods = {
                "method_a": lambda: process_with_method_a(data),
                "method_b": lambda: process_with_method_b(data),
                "method_c": lambda: process_with_method_c(data)
            }
            
            context = {"data_type": "text", "processing_mode": "fast"}
            
            print(f"Processing {data} with multiple methods speculatively...")
            futures = agent.speculate(methods, context)
            print(f"Speculated methods: {list(futures.keys())}")
            
            # Decision: use method_a
            chosen_method = list(futures.keys())[0] if futures else "method_a"
            print(f"\nChosen method: {chosen_method}")
            
            result = agent.select_result(futures, chosen_method)
            print(f"Result: {result}")
            
            # Demonstration 5: Early Cancellation
            print("\n" + "=" * 80)
            print("Demonstration 5: Early Cancellation")
            print("=" * 80)
            
            def slow_operation_1():
                time.sleep(2.0)
                return "Slow result 1"
            
            def slow_operation_2():
                time.sleep(2.0)
                return "Slow result 2"
            
            def fast_operation():
                time.sleep(0.1)
                return "Fast result"
            
            paths = {
                "slow_1": slow_operation_1,
                "slow_2": slow_operation_2,
                "fast": fast_operation
            }
            
            print("Starting speculation with slow and fast paths...")
            futures = agent.speculate(paths, {"cancel_test": True})
            
            # Quickly decide on fast path
            time.sleep(0.2)
            actual_path = "fast"
            print(f"\nQuick decision: use {actual_path}")
            print("Cancelling slow speculations...")
            
            result = agent.select_result(futures, actual_path, timeout=0.5)
            print(f"Result: {result}")
            print("Slow operations were cancelled early")
            
            # Demonstration 6: Speculation Metrics
            print("\n" + "=" * 80)
            print("Demonstration 6: Speculation Metrics")
            print("=" * 80)
            
            metrics = agent.get_metrics()
            print("\nSpeculation Performance:")
            print(f"  Total Speculations: {metrics['total_speculations']}")
            print(f"  Successful: {metrics['successful']}")
            print(f"  Wasted: {metrics['wasted']}")
            print(f"  Cancelled: {metrics['cancelled']}")
            print(f"  Failed: {metrics['failed']}")
            print(f"  Success Rate: {metrics['success_rate']}")
            print(f"  Efficiency: {metrics['efficiency']}")
            print(f"  Time Saved: {metrics['time_saved']}")
            print(f"  Time Wasted: {metrics['time_wasted']}")
            print(f"  Net Benefit: {metrics['net_benefit']}")
            
            # Summary
            print("\n" + "=" * 80)
            print("Summary: Speculative Execution Pattern")
            print("=" * 80)
            print("""
    The Speculative Execution pattern optimizes performance through prediction:
    
    Key Features Demonstrated:
    1. Basic Speculation - Execute multiple paths in parallel
    2. Confidence-Based - Only speculate on likely paths
    3. Data Prefetching - Preload likely-needed data
    4. Multi-Path Processing - Try multiple approaches simultaneously
    5. Early Cancellation - Stop unused speculations quickly
    6. Metrics Tracking - Monitor speculation effectiveness
    
    Benefits:
    - Reduces perceived latency
    - Improves responsiveness
    - Enables parallel processing
    - Optimizes resource utilization
    - Provides fallback options
    - Accelerates common paths
    
    Best Practices:
    - Monitor speculation accuracy
    - Set resource limits
    - Implement cancellation
    - Track waste metrics
    - Use confidence thresholds
    - Limit speculation depth
    - Handle failures gracefully
    - Provide fallback paths
    - Consider cost implications
    - Test prediction models
    - Monitor system load
    - Balance parallelism
    
    Common Use Cases:
    - High-latency operations
    - Branching workflows
    - User interaction prediction
    - Data prefetching
    - Multi-modal processing
    - Recommendation systems
    - Search optimization
    - Decision trees
    
    Performance Considerations:
    - Speculation wastes resources on wrong predictions
    - Best for high-latency operations
    - Requires good prediction models
    - Monitor success rate and efficiency
    - Balance speculation vs. waste
    - Consider system capacity
    
    This pattern is essential for responsive AI agents that need to minimize
    latency by anticipating likely operations.
    """)
        
        finally:
            # Cleanup
            agent.shutdown()
    
    
    if __name__ == "__main__":
        demonstrate_speculative_execution()
    
        

    Agentic Design Pattern: Speculative Execution

    Pattern 142: Speculative Execution – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Speculative Execution pattern implementation.


    Pattern Description

    Pattern 142: Speculative Execution

    Description: The Speculative Execution pattern optimizes performance by predicting and executing likely operations before they’re actually needed, enabling parallel processing and reduced latency. The agent anticipates probable next steps and begins computation proactively, discarding results if predictions prove incorrect. This pattern is particularly valuable for operations with high latency or when multiple potential paths exist.

    Speculative execution leverages parallelism to compute multiple branches
    simultaneously, betting that at least one will be correct. When predictions succeed,
    the agent gains significant performance improvements by having results ready
    immediately. When wrong, the cost is the wasted computation, which must be balanced
    against the benefits.
    
    This implementation provides speculative execution capabilities including predictive
    computation, parallel branch execution, result management, confidence-based
    speculation, and waste tracking. It supports both simple lookahead and complex
    multi-path speculation with cancellation mechanisms.
    

    Components: – Predictive Computation: Anticipate likely operations – Parallel Execution: Run multiple possibilities simultaneously – Result Management: Handle multiple speculative results – Confidence-Based: Speculate based on prediction confidence – Waste Tracking: Monitor efficiency of speculation – Cancellation: Stop incorrect speculations early

    Use Cases: – High-latency operations (API calls, database queries) – Branching workflows with predictable paths – User interaction prediction – Data prefetching – Multi-modal processing – Recommendation systems – Search optimization – Decision trees

    LangChain Implementation: This implementation uses: – Parallel chain execution with asyncio – Predictive path selection with LLM – Speculative result caching – Confidence-based speculation triggers – Waste tracking and efficiency metrics – Result selection and fallback

    Benefits: – Reduces perceived latency – Improves responsiveness – Enables parallel processing – Optimizes resource utilization – Handles uncertainty gracefully – Provides fallback options – Accelerates common paths

    Trade-offs: – Wastes resources on incorrect predictions – Increases complexity – May overload systems – Requires careful tuning – Can mask performance issues – Needs good prediction models – Risk of cascade failures

    Production Considerations: – Monitor speculation accuracy – Set resource limits – Implement cancellation – Track waste metrics – Use confidence thresholds – Limit speculation depth – Handle failures gracefully – Provide fallback paths – Consider cost implications – Test prediction models – Monitor system load – Balance parallelism


    File Statistics

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

    Module Documentation

    """
    Pattern 142: Speculative Execution
    
    Description:
        The Speculative Execution pattern optimizes performance by predicting and executing
        likely operations before they're actually needed, enabling parallel processing and
        reduced latency. The agent anticipates probable next steps and begins computation
        proactively, discarding results if predictions prove incorrect. This pattern is
        particularly valuable for operations with high latency or when multiple potential
        paths exist.
    
        Speculative execution leverages parallelism to compute multiple branches
        simultaneously, betting that at least one will be correct. When predictions succeed,
        the agent gains significant performance improvements by having results ready
        immediately. When wrong, the cost is the wasted computation, which must be balanced
        against the benefits.
    
        This implementation provides speculative execution capabilities including predictive
        computation, parallel branch execution, result management, confidence-based
        speculation, and waste tracking. It supports both simple lookahead and complex
        multi-path speculation with cancellation mechanisms.
    
    Components:
        - Predictive Computation: Anticipate likely operations
        - Parallel Execution: Run multiple possibilities simultaneously
        - Result Management: Handle multiple speculative results
        - Confidence-Based: Speculate based on prediction confidence
        - Waste Tracking: Monitor efficiency of speculation
        - Cancellation: Stop incorrect speculations early
    
    Use Cases:
        - High-latency operations (API calls, database queries)
        - Branching workflows with predictable paths
        - User interaction prediction
        - Data prefetching
        - Multi-modal processing
        - Recommendation systems
        - Search optimization
        - Decision trees
    
    LangChain Implementation:
        This implementation uses:
        - Parallel chain execution with asyncio
        - Predictive path selection with LLM
        - Speculative result caching
        - Confidence-based speculation triggers
        - Waste tracking and efficiency metrics
        - Result selection and fallback
    
    Benefits:
        - Reduces perceived latency
        - Improves responsiveness
        - Enables parallel processing
        - Optimizes resource utilization
        - Handles uncertainty gracefully
        - Provides fallback options
        - Accelerates common paths
    
    Trade-offs:
        - Wastes resources on incorrect predictions
        - Increases complexity
        - May overload systems
        - Requires careful tuning
        - Can mask performance issues
        - Needs good prediction models
        - Risk of cascade failures
    
    Production Considerations:
        - Monitor speculation accuracy
        - Set resource limits
        - Implement cancellation
        - Track waste metrics
        - Use confidence thresholds
        - Limit speculation depth
        - Handle failures gracefully
        - Provide fallback paths
        - Consider cost implications
        - Test prediction models
        - Monitor system load
        - Balance parallelism
    """
    

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


    Import Statements

    Standard Library Imports

    import asyncio
    import concurrent.futures.Future
    import concurrent.futures.ThreadPoolExecutor
    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 time
    import typing.Any
    import typing.Callable
    import typing.Dict
    import typing.List
    import typing.Optional
    import typing.Tuple
    

    Purpose: Core Python functionality: – asyncio: Asynchronous I/O operations – concurrent.futures.Future: Regular expression operations – concurrent.futures.ThreadPoolExecutor: Regular expression operations – 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 – 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) – typing.Tuple: 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: SpeculationStatus

    Lines 100-108

    Inherits from: Enum

    Purpose:

    Status of speculative execution.
    

    Class: SpeculativeTask

    Lines 112-138

    Purpose:

    Represents a speculative task.
    

    Methods:

    execute(self)

    Lines 124-138

    Purpose:

    Execute the speculative task.
    

    Parameters:

    Returns: Any


    Class: SpeculationMetrics

    Lines 142-162

    Purpose:

    Metrics for speculative execution.
    

    Methods:

    success_rate(self)

    Lines 153-156

    Decorators: property

    Purpose:

    Calculate speculation success rate.
    

    Parameters:

    Returns: float

    efficiency(self)

    Lines 159-162

    Decorators: property

    Purpose:

    Calculate efficiency (time saved vs wasted).
    

    Parameters:

    Returns: float


    Class: SpeculativeExecutionAgent

    Lines 165-440

    Purpose:

    Agent that uses speculative execution for performance optimization.
    
    This agent anticipates likely operations and executes them proactively,
    reducing latency when predictions are correct.
    

    Methods:

    __init__(self, temperature, max_workers)

    Lines 173-201

    Purpose:

    Initialize the speculative execution agent.
    

    Parameters: – temperature: Sampling temperature (0-1) – max_workers: Max workers

    predict_paths(self, context, available_paths)

    Lines 203-243

    Purpose:

    Predict likely execution paths with confidence scores.
    
    Returns:
        List of (path_name, confidence) tuples
    

    Parameters: – context: Context – available_paths: Available paths

    Returns: <ast.Subscript object at 0x000001A06D7F76D0>

    speculate(self, paths, context)

    Lines 245-282

    Purpose:

    Execute multiple paths speculatively in parallel.
    
    Args:
        paths: Dictionary of path_name -> computation function
        context: Context for path prediction
    
    Returns:
        Dictionary of path_name -> Future
    

    Parameters: – paths: Paths – context: Context

    Returns: <ast.Subscript object at 0x000001A06D83E910>

    select_result(self, futures, actual_path, timeout)

    Lines 284-337

    Purpose:

    Select the result from the actual path taken.
    
    Args:
        futures: Dictionary of path_name -> Future
        actual_path: The path that was actually taken
        timeout: Maximum time to wait for result
    
    Returns:
        Result from the actual path
    

    Parameters: – futures: Futures – actual_path: Actual path – timeout: Timeout

    Returns: Any

    _find_task_by_path(self, path_name)

    Lines 339-344

    Purpose:

    Find task by path name.
    

    Parameters: – path_name: Path name

    Returns: <ast.Subscript object at 0x000001A06D68E790>

    speculative_search(self, query, search_strategies)

    Lines 346-386

    Purpose:

    Speculatively execute multiple search strategies.
    
    Args:
        query: Search query
        search_strategies: Dictionary of strategy_name -> search function
    
    Returns:
        Best search result
    

    Parameters: – query: Input query or question – search_strategies: Search strategies

    Returns: Any

    prefetch_data(self, likely_requests, fetch_function, confidence_scores)

    Lines 388-421

    Purpose:

    Prefetch data for likely requests.
    
    Args:
        likely_requests: List of likely data requests
        fetch_function: Function to fetch data
        confidence_scores: Optional confidence for each request
    
    Returns:
        Dictionary of request -> Future
    

    Parameters: – likely_requests: Likely requests – fetch_function: Fetch function – confidence_scores: Confidence scores

    Returns: <ast.Subscript object at 0x000001A06D6F4710>

    get_metrics(self)

    Lines 423-436

    Purpose:

    Get speculation metrics.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D62A2D0>

    shutdown(self)

    Lines 438-440

    Purpose:

    Shutdown the executor.
    

    Parameters:


    Functions

    demonstrate_speculative_execution()

    Lines 443-707

    Purpose:

    Demonstrate the speculative execution pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Speculative Execution 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 141
    • Pattern 143

    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 Speculative Execution 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: 142_speculative_execution.py
    Total Lines: 712
    Pattern Category: AI Agent Design Pattern #142


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Lazy Evaluation

    
        """
    Pattern 141: Lazy Evaluation
    
    Description:
        The Lazy Evaluation pattern defers computation until results are actually needed,
        optimizing resource usage by avoiding unnecessary work. Instead of eagerly computing
        all results upfront, lazy evaluation creates computation plans that execute only
        when values are requested. This pattern is particularly valuable for expensive
        operations, large datasets, and scenarios where not all computed values may be used.
    
        Lazy evaluation improves performance by reducing wasted computation, lowering
        memory usage, enabling infinite sequences, and allowing for optimization opportunities
        through computation graph analysis. It's especially useful in AI agents that might
        explore multiple paths but only need results from successful branches, or when
        processing large datasets where early termination is possible.
    
        This implementation provides lazy evaluation capabilities for LangChain agents,
        including deferred chain execution, lazy data loading, conditional computation,
        result streaming, and computation graph optimization. It supports both simple
        delayed execution and complex multi-stage lazy pipelines.
    
    Components:
        - Deferred Execution: Delay computation until needed
        - Lazy Data Loading: Load data on-demand
        - Conditional Computation: Skip unnecessary branches
        - Result Streaming: Process results incrementally
        - Graph Optimization: Optimize computation plans
        - Caching Integration: Cache lazy results
    
    Use Cases:
        - Large dataset processing
        - Multi-path exploration
        - Conditional workflows
        - Resource-constrained environments
        - Expensive API calls
        - Database query optimization
        - Streaming data processing
        - Early termination scenarios
    
    LangChain Implementation:
        This implementation uses:
        - Python generators for lazy iteration
        - Deferred chain execution with LCEL
        - Lazy data loading with document loaders
        - Conditional branching in chains
        - Streaming for incremental results
        - Custom lazy wrappers for expensive operations
    
    Benefits:
        - Reduces unnecessary computation
        - Lowers memory usage
        - Enables early termination
        - Improves responsiveness
        - Supports infinite sequences
        - Allows optimization opportunities
        - Reduces API costs
    
    Trade-offs:
        - Can make debugging harder
        - May cause unexpected delays
        - Complexity in error handling
        - Potential memory leaks if not careful
        - Harder to reason about execution flow
        - May hide performance issues
    
    Production Considerations:
        - Profile to identify expensive operations
        - Use for truly expensive computations
        - Combine with caching
        - Handle errors in lazy contexts
        - Set timeouts for lazy operations
        - Monitor actual execution patterns
        - Document lazy behavior clearly
        - Test edge cases thoroughly
        - Consider memory management
        - Balance with eager evaluation
        - Use generators appropriately
        - Profile actual vs potential savings
    """
    
    import os
    import time
    from typing import List, Dict, Any, Optional, Iterator, Callable, Generator
    from datetime import datetime
    from dataclasses import dataclass
    from functools import wraps
    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()
    
    
    @dataclass
    class LazyResult:
        """Wrapper for lazy computation result."""
        computation: Callable[[], Any]
        _cached_value: Optional[Any] = None
        _computed: bool = False
        _computation_time: Optional[float] = None
        
        def compute(self) -> Any:
            """Execute the lazy computation."""
            if not self._computed:
                start = time.time()
                self._cached_value = self.computation()
                self._computation_time = time.time() - start
                self._computed = True
            return self._cached_value
        
        @property
        def value(self) -> Any:
            """Get the computed value."""
            return self.compute()
        
        @property
        def is_computed(self) -> bool:
            """Check if value has been computed."""
            return self._computed
        
        @property
        def computation_time(self) -> Optional[float]:
            """Get computation time in seconds."""
            return self._computation_time
    
    
    def lazy(func: Callable) -> Callable:
        """Decorator to make a function lazy."""
        @wraps(func)
        def wrapper(*args, **kwargs):
            def computation():
                return func(*args, **kwargs)
            return LazyResult(computation=computation)
        return wrapper
    
    
    class LazyChain:
        """Lazy evaluation wrapper for LangChain chains."""
        
        def __init__(self, chain):
            """Initialize lazy chain."""
            self.chain = chain
            self._result = None
            self._computed = False
        
        def compute(self, input_data: Dict[str, Any]) -> Any:
            """Execute the chain."""
            if not self._computed:
                self._result = self.chain.invoke(input_data)
                self._computed = True
            return self._result
        
        def stream(self, input_data: Dict[str, Any]) -> Iterator[Any]:
            """Stream results lazily."""
            for chunk in self.chain.stream(input_data):
                yield chunk
    
    
    class LazyDataLoader:
        """Lazy data loader that loads data on demand."""
        
        def __init__(self, data_source: str):
            """Initialize lazy data loader."""
            self.data_source = data_source
            self._data = None
            self._loaded = False
            self._load_time = None
        
        def load(self) -> List[Any]:
            """Load data (simulated)."""
            if not self._loaded:
                start = time.time()
                # Simulate expensive data loading
                time.sleep(0.1)
                self._data = [f"Item {i} from {self.data_source}" for i in range(100)]
                self._load_time = time.time() - start
                self._loaded = True
            return self._data
        
        def iter_lazy(self, chunk_size: int = 10) -> Generator[List[Any], None, None]:
            """Iterate over data in chunks."""
            data = self.load()
            for i in range(0, len(data), chunk_size):
                yield data[i:i + chunk_size]
        
        @property
        def is_loaded(self) -> bool:
            """Check if data is loaded."""
            return self._loaded
    
    
    class LazyEvaluationAgent:
        """
        Agent that uses lazy evaluation for efficient processing.
        
        This agent defers expensive computations until results are needed,
        optimizing resource usage and enabling early termination.
        """
        
        def __init__(self, temperature: float = 0.3):
            """Initialize the lazy evaluation agent."""
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.computation_stats = {
                "deferred": 0,
                "executed": 0,
                "saved": 0
            }
            
            # Create lazy chains
            self.analysis_chain = LazyChain(
                ChatPromptTemplate.from_template(
                    "Analyze this text: {text}\n\nProvide a detailed analysis."
                ) | self.llm | StrOutputParser()
            )
        
        @lazy
        def expensive_computation(self, value: int) -> int:
            """Simulate expensive computation."""
            time.sleep(0.5)  # Simulate work
            return value ** 2
        
        def lazy_filter(
            self,
            items: List[Any],
            condition: Callable[[Any], bool]
        ) -> Generator[Any, None, None]:
            """Lazy filter that yields items matching condition."""
            for item in items:
                if condition(item):
                    yield item
        
        def lazy_map(
            self,
            items: List[Any],
            transform: Callable[[Any], Any]
        ) -> Generator[Any, None, None]:
            """Lazy map that transforms items on demand."""
            for item in items:
                yield transform(item)
        
        def conditional_computation(
            self,
            condition: bool,
            true_computation: Callable[[], Any],
            false_computation: Callable[[], Any]
        ) -> LazyResult:
            """Conditionally defer computation."""
            if condition:
                return LazyResult(computation=true_computation)
            else:
                return LazyResult(computation=false_computation)
        
        def lazy_pipeline(
            self,
            data: List[Any],
            operations: List[Callable[[Any], Any]],
            early_exit: Optional[Callable[[Any], bool]] = None
        ) -> Generator[Any, None, None]:
            """
            Execute a lazy pipeline with early exit capability.
            
            Args:
                data: Input data
                operations: List of operations to apply
                early_exit: Optional function to check if we should stop early
            """
            for item in data:
                result = item
                
                # Apply operations lazily
                for op in operations:
                    result = op(result)
                    
                    # Check early exit condition
                    if early_exit and early_exit(result):
                        yield result
                        return  # Early termination
                
                yield result
        
        def parallel_lazy_evaluation(
            self,
            tasks: List[Callable[[], Any]],
            max_results: Optional[int] = None
        ) -> Generator[Any, None, None]:
            """
            Evaluate multiple lazy tasks, stopping when enough results obtained.
            
            Args:
                tasks: List of lazy computations
                max_results: Maximum number of results to compute
            """
            count = 0
            for task in tasks:
                if max_results and count >= max_results:
                    break
                
                result = LazyResult(computation=task)
                yield result
                count += 1
        
        def lazy_llm_batch(
            self,
            prompts: List[str],
            process_until: Optional[Callable[[str], bool]] = None
        ) -> Generator[str, None, None]:
            """
            Process prompts lazily, with optional early termination.
            
            Args:
                prompts: List of prompts to process
                process_until: Optional function to check if we should stop
            """
            for prompt in prompts:
                result = self.llm.invoke(prompt).content
                yield result
                
                if process_until and process_until(result):
                    break  # Early termination
        
        def get_stats(self) -> Dict[str, Any]:
            """Get computation statistics."""
            total = self.computation_stats["executed"] + self.computation_stats["saved"]
            return {
                "deferred": self.computation_stats["deferred"],
                "executed": self.computation_stats["executed"],
                "saved": self.computation_stats["saved"],
                "efficiency": (
                    self.computation_stats["saved"] / total * 100 
                    if total > 0 else 0
                )
            }
    
    
    def demonstrate_lazy_evaluation():
        """Demonstrate the lazy evaluation pattern."""
        print("=" * 80)
        print("Lazy Evaluation Pattern Demonstration")
        print("=" * 80)
        
        agent = LazyEvaluationAgent()
        
        # Demonstration 1: Basic Lazy Computation
        print("\n" + "=" * 80)
        print("Demonstration 1: Basic Lazy Computation")
        print("=" * 80)
        
        print("\nCreating lazy computation...")
        lazy_result = agent.expensive_computation(10)
        print(f"Computation created (not executed yet)")
        print(f"Is computed: {lazy_result.is_computed}")
        
        print("\nAccessing value (triggers computation)...")
        start = time.time()
        value = lazy_result.value
        elapsed = time.time() - start
        print(f"Result: {value}")
        print(f"Time: {elapsed:.3f}s")
        print(f"Is computed: {lazy_result.is_computed}")
        
        print("\nAccessing value again (uses cached)...")
        start = time.time()
        value2 = lazy_result.value
        elapsed = time.time() - start
        print(f"Result: {value2}")
        print(f"Time: {elapsed:.3f}s (cached)")
        
        # Demonstration 2: Lazy Filter
        print("\n" + "=" * 80)
        print("Demonstration 2: Lazy Filter (Early Termination)")
        print("=" * 80)
        
        numbers = list(range(1, 101))
        print(f"Processing {len(numbers)} numbers...")
        
        # Eager evaluation
        start = time.time()
        eager_result = [x for x in numbers if x % 7 == 0][:5]  # Get first 5
        eager_time = time.time() - start
        print(f"\nEager evaluation: {eager_result}")
        print(f"Time: {eager_time:.4f}s (processed all {len(numbers)} items)")
        
        # Lazy evaluation with early termination
        start = time.time()
        lazy_filtered = agent.lazy_filter(numbers, lambda x: x % 7 == 0)
        lazy_result = []
        for item in lazy_filtered:
            lazy_result.append(item)
            if len(lazy_result) >= 5:
                break  # Early termination
        lazy_time = time.time() - start
        print(f"\nLazy evaluation: {lazy_result}")
        print(f"Time: {lazy_time:.4f}s (stopped after finding 5)")
        
        # Demonstration 3: Lazy Data Loading
        print("\n" + "=" * 80)
        print("Demonstration 3: Lazy Data Loading")
        print("=" * 80)
        
        loader = LazyDataLoader("database")
        print(f"Data loader created")
        print(f"Is loaded: {loader.is_loaded}")
        
        print("\nProcessing first chunk only...")
        for chunk in loader.iter_lazy(chunk_size=10):
            print(f"Processing chunk of {len(chunk)} items")
            # Only process first chunk
            break
        
        print(f"Is loaded: {loader.is_loaded} (only loaded what was needed)")
        
        # Demonstration 4: Conditional Lazy Computation
        print("\n" + "=" * 80)
        print("Demonstration 4: Conditional Lazy Computation")
        print("=" * 80)
        
        def expensive_path_a():
            time.sleep(0.3)
            return "Result from expensive path A"
        
        def expensive_path_b():
            time.sleep(0.3)
            return "Result from expensive path B"
        
        condition = True
        print(f"Condition: {condition}")
        print("Creating conditional lazy computation...")
        
        result = agent.conditional_computation(
            condition,
            expensive_path_a,
            expensive_path_b
        )
        
        print(f"Computation deferred, is computed: {result.is_computed}")
        print("\nNow computing...")
        value = result.value
        print(f"Result: {value}")
        print(f"Computation time: {result.computation_time:.3f}s")
        
        # Demonstration 5: Lazy Pipeline with Early Exit
        print("\n" + "=" * 80)
        print("Demonstration 5: Lazy Pipeline with Early Exit")
        print("=" * 80)
        
        data = list(range(1, 21))
        
        operations = [
            lambda x: x * 2,
            lambda x: x + 10,
            lambda x: x ** 2
        ]
        
        early_exit = lambda x: x > 1000
        
        print(f"Processing {len(data)} items through pipeline...")
        print("Early exit when result > 1000")
        
        results = list(agent.lazy_pipeline(data, operations, early_exit))
        print(f"\nProcessed {len(results)} items before early exit")
        print(f"Results: {results}")
        
        # Demonstration 6: Lazy Map Transformation
        print("\n" + "=" * 80)
        print("Demonstration 6: Lazy Map (Process Only What's Needed)")
        print("=" * 80)
        
        items = ["apple", "banana", "cherry", "date", "elderberry"]
        
        def expensive_transform(item: str) -> str:
            """Simulate expensive transformation."""
            time.sleep(0.1)
            return item.upper()
        
        print(f"Items to transform: {items}")
        print("Transforming lazily, taking only first 3...")
        
        start = time.time()
        transformed = agent.lazy_map(items, expensive_transform)
        result = []
        for item in transformed:
            result.append(item)
            if len(result) >= 3:
                break
        elapsed = time.time() - start
        
        print(f"Transformed: {result}")
        print(f"Time: {elapsed:.2f}s (only transformed 3 items, not all {len(items)})")
        
        # Demonstration 7: Parallel Lazy Tasks with Limit
        print("\n" + "=" * 80)
        print("Demonstration 7: Parallel Lazy Tasks (Limited Execution)")
        print("=" * 80)
        
        tasks = [
            lambda: f"Task 1 result",
            lambda: f"Task 2 result",
            lambda: f"Task 3 result",
            lambda: f"Task 4 result",
            lambda: f"Task 5 result",
        ]
        
        print(f"Created {len(tasks)} tasks")
        print("Executing only first 2 tasks...")
        
        lazy_results = agent.parallel_lazy_evaluation(tasks, max_results=2)
        executed = []
        for lazy_result in lazy_results:
            executed.append(lazy_result.value)
        
        print(f"Executed: {len(executed)} tasks")
        print(f"Results: {executed}")
        print(f"Saved: {len(tasks) - len(executed)} task executions")
        
        # Demonstration 8: Lazy LLM Batch Processing
        print("\n" + "=" * 80)
        print("Demonstration 8: Lazy LLM Batch (Early Termination)")
        print("=" * 80)
        
        prompts = [
            "Count to 3",
            "Count to 5",
            "Count to 10",
            "Count to 20"
        ]
        
        print(f"Created {len(prompts)} prompts")
        print("Processing until we get a result with '10' in it...")
        
        processed = 0
        found_result = None
        
        for result in agent.lazy_llm_batch(
            prompts,
            process_until=lambda r: "10" in r
        ):
            processed += 1
            if "10" in result:
                found_result = result
                break
        
        print(f"\nProcessed: {processed}/{len(prompts)} prompts")
        print(f"Found result: {found_result[:50]}...")
        print(f"Saved: {len(prompts) - processed} LLM calls")
        
        # Summary
        print("\n" + "=" * 80)
        print("Summary: Lazy Evaluation Pattern")
        print("=" * 80)
        print("""
    The Lazy Evaluation pattern optimizes resource usage by deferring computation:
    
    Key Features Demonstrated:
    1. Basic Lazy Computation - Defer expensive operations until needed
    2. Lazy Filter - Filter with early termination
    3. Lazy Data Loading - Load data on-demand
    4. Conditional Computation - Only compute chosen branch
    5. Lazy Pipeline - Process with early exit
    6. Lazy Map - Transform only what's needed
    7. Limited Execution - Execute only required tasks
    8. LLM Batch Processing - Stop when condition met
    
    Benefits:
    - Reduces unnecessary computation
    - Lowers memory usage
    - Enables early termination
    - Improves responsiveness
    - Reduces API costs
    - Supports infinite sequences
    - Allows optimization
    
    Best Practices:
    - Use for expensive operations
    - Combine with caching
    - Handle errors properly
    - Set timeouts
    - Monitor execution patterns
    - Document lazy behavior
    - Test edge cases
    - Balance with eager evaluation
    - Use generators appropriately
    - Profile actual savings
    
    Common Use Cases:
    - Large dataset processing
    - Multi-path exploration
    - Conditional workflows
    - Resource-constrained environments
    - Expensive API calls
    - Database queries
    - Streaming data
    - Early termination scenarios
    
    This pattern is essential for efficient AI agents that need to optimize
    resource usage and avoid unnecessary computation.
    """)
    
    
    if __name__ == "__main__":
        demonstrate_lazy_evaluation()
    
        

    Agentic Design Pattern: Lazy Evaluation

    Pattern 141: Lazy Evaluation – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Lazy Evaluation pattern implementation.


    Pattern Description

    Pattern 141: Lazy Evaluation

    Description: The Lazy Evaluation pattern defers computation until results are actually needed, optimizing resource usage by avoiding unnecessary work. Instead of eagerly computing all results upfront, lazy evaluation creates computation plans that execute only when values are requested. This pattern is particularly valuable for expensive operations, large datasets, and scenarios where not all computed values may be used.

    Lazy evaluation improves performance by reducing wasted computation, lowering
    memory usage, enabling infinite sequences, and allowing for optimization opportunities
    through computation graph analysis. It's especially useful in AI agents that might
    explore multiple paths but only need results from successful branches, or when
    processing large datasets where early termination is possible.
    
    This implementation provides lazy evaluation capabilities for LangChain agents,
    including deferred chain execution, lazy data loading, conditional computation,
    result streaming, and computation graph optimization. It supports both simple
    delayed execution and complex multi-stage lazy pipelines.
    

    Components: – Deferred Execution: Delay computation until needed – Lazy Data Loading: Load data on-demand – Conditional Computation: Skip unnecessary branches – Result Streaming: Process results incrementally – Graph Optimization: Optimize computation plans – Caching Integration: Cache lazy results

    Use Cases: – Large dataset processing – Multi-path exploration – Conditional workflows – Resource-constrained environments – Expensive API calls – Database query optimization – Streaming data processing – Early termination scenarios

    LangChain Implementation: This implementation uses: – Python generators for lazy iteration – Deferred chain execution with LCEL – Lazy data loading with document loaders – Conditional branching in chains – Streaming for incremental results – Custom lazy wrappers for expensive operations

    Benefits: – Reduces unnecessary computation – Lowers memory usage – Enables early termination – Improves responsiveness – Supports infinite sequences – Allows optimization opportunities – Reduces API costs

    Trade-offs: – Can make debugging harder – May cause unexpected delays – Complexity in error handling – Potential memory leaks if not careful – Harder to reason about execution flow – May hide performance issues

    Production Considerations: – Profile to identify expensive operations – Use for truly expensive computations – Combine with caching – Handle errors in lazy contexts – Set timeouts for lazy operations – Monitor actual execution patterns – Document lazy behavior clearly – Test edge cases thoroughly – Consider memory management – Balance with eager evaluation – Use generators appropriately – Profile actual vs potential savings


    File Statistics

    • Total Lines: 603
    • Classes: 4
    • Functions: 2
    • Constants: 0

    Module Documentation

    """
    Pattern 141: Lazy Evaluation
    
    Description:
        The Lazy Evaluation pattern defers computation until results are actually needed,
        optimizing resource usage by avoiding unnecessary work. Instead of eagerly computing
        all results upfront, lazy evaluation creates computation plans that execute only
        when values are requested. This pattern is particularly valuable for expensive
        operations, large datasets, and scenarios where not all computed values may be used.
    
        Lazy evaluation improves performance by reducing wasted computation, lowering
        memory usage, enabling infinite sequences, and allowing for optimization opportunities
        through computation graph analysis. It's especially useful in AI agents that might
        explore multiple paths but only need results from successful branches, or when
        processing large datasets where early termination is possible.
    
        This implementation provides lazy evaluation capabilities for LangChain agents,
        including deferred chain execution, lazy data loading, conditional computation,
        result streaming, and computation graph optimization. It supports both simple
        delayed execution and complex multi-stage lazy pipelines.
    
    Components:
        - Deferred Execution: Delay computation until needed
        - Lazy Data Loading: Load data on-demand
        - Conditional Computation: Skip unnecessary branches
        - Result Streaming: Process results incrementally
        - Graph Optimization: Optimize computation plans
        - Caching Integration: Cache lazy results
    
    Use Cases:
        - Large dataset processing
        - Multi-path exploration
        - Conditional workflows
        - Resource-constrained environments
        - Expensive API calls
        - Database query optimization
        - Streaming data processing
        - Early termination scenarios
    
    LangChain Implementation:
        This implementation uses:
        - Python generators for lazy iteration
        - Deferred chain execution with LCEL
        - Lazy data loading with document loaders
        - Conditional branching in chains
        - Streaming for incremental results
        - Custom lazy wrappers for expensive operations
    
    Benefits:
        - Reduces unnecessary computation
        - Lowers memory usage
        - Enables early termination
        - Improves responsiveness
        - Supports infinite sequences
        - Allows optimization opportunities
        - Reduces API costs
    
    Trade-offs:
        - Can make debugging harder
        - May cause unexpected delays
        - Complexity in error handling
        - Potential memory leaks if not careful
        - Harder to reason about execution flow
        - May hide performance issues
    
    Production Considerations:
        - Profile to identify expensive operations
        - Use for truly expensive computations
        - Combine with caching
        - Handle errors in lazy contexts
        - Set timeouts for lazy operations
        - Monitor actual execution patterns
        - Document lazy behavior clearly
        - Test edge cases thoroughly
        - Consider memory management
        - Balance with eager evaluation
        - Use generators appropriately
        - Profile actual vs potential savings
    """
    

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


    Import Statements

    Standard Library Imports

    import dataclasses.dataclass
    import datetime.datetime
    import functools.wraps
    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.Generator
    import typing.Iterator
    import typing.List
    import typing.Optional
    

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – functools.wraps: Higher-order functions and operations on callable objects – 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.Generator: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Iterator: 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: LazyResult

    Lines 97-126

    Purpose:

    Wrapper for lazy computation result.
    

    Methods:

    compute(self)

    Lines 104-111

    Purpose:

    Execute the lazy computation.
    

    Parameters:

    Returns: Any

    value(self)

    Lines 114-116

    Decorators: property

    Purpose:

    Get the computed value.
    

    Parameters:

    Returns: Any

    is_computed(self)

    Lines 119-121

    Decorators: property

    Purpose:

    Check if value has been computed.
    

    Parameters:

    Returns: bool

    computation_time(self)

    Lines 124-126

    Decorators: property

    Purpose:

    Get computation time in seconds.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D169C10>


    Class: LazyChain

    Lines 139-158

    Purpose:

    Lazy evaluation wrapper for LangChain chains.
    

    Methods:

    __init__(self, chain)

    Lines 142-146

    Purpose:

    Initialize lazy chain.
    

    Parameters: – chain: Chain

    compute(self, input_data)

    Lines 148-153

    Purpose:

    Execute the chain.
    

    Parameters: – input_data: Input data

    Returns: Any

    stream(self, input_data)

    Lines 155-158

    Purpose:

    Stream results lazily.
    

    Parameters: – input_data: Input data

    Returns: <ast.Subscript object at 0x000001A06D7B1150>


    Class: LazyDataLoader

    Lines 161-191

    Purpose:

    Lazy data loader that loads data on demand.
    

    Methods:

    __init__(self, data_source)

    Lines 164-169

    Purpose:

    Initialize lazy data loader.
    

    Parameters: – data_source: Data source

    load(self)

    Lines 171-180

    Purpose:

    Load data (simulated).
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D72DDD0>

    iter_lazy(self, chunk_size)

    Lines 182-186

    Purpose:

    Iterate over data in chunks.
    

    Parameters: – chunk_size: Chunk size

    Returns: <ast.Subscript object at 0x000001A06D72ED90>

    is_loaded(self)

    Lines 189-191

    Decorators: property

    Purpose:

    Check if data is loaded.
    

    Parameters:

    Returns: bool


    Class: LazyEvaluationAgent

    Lines 194-334

    Purpose:

    Agent that uses lazy evaluation for efficient processing.
    
    This agent defers expensive computations until results are needed,
    optimizing resource usage and enabling early termination.
    

    Methods:

    __init__(self, temperature)

    Lines 202-216

    Purpose:

    Initialize the lazy evaluation agent.
    

    Parameters: – temperature: Sampling temperature (0-1)

    expensive_computation(self, value)

    Lines 219-222

    Decorators: lazy

    Purpose:

    Simulate expensive computation.
    

    Parameters: – value: Value

    Returns: int

    lazy_filter(self, items, condition)

    Lines 224-232

    Purpose:

    Lazy filter that yields items matching condition.
    

    Parameters: – items: Items – condition: Condition

    Returns: <ast.Subscript object at 0x000001A06D88E250>

    lazy_map(self, items, transform)

    Lines 234-241

    Purpose:

    Lazy map that transforms items on demand.
    

    Parameters: – items: Items – transform: Transform

    Returns: <ast.Subscript object at 0x000001A06D622A50>

    conditional_computation(self, condition, true_computation, false_computation)

    Lines 243-253

    Purpose:

    Conditionally defer computation.
    

    Parameters: – condition: Condition – true_computation: True computation – false_computation: False computation

    Returns: LazyResult

    lazy_pipeline(self, data, operations, early_exit)

    Lines 255-281

    Purpose:

    Execute a lazy pipeline with early exit capability.
    
    Args:
        data: Input data
        operations: List of operations to apply
        early_exit: Optional function to check if we should stop early
    

    Parameters: – data: Data – operations: Operations – early_exit: Early exit

    Returns: <ast.Subscript object at 0x000001A06D77C0D0>

    parallel_lazy_evaluation(self, tasks, max_results)

    Lines 283-302

    Purpose:

    Evaluate multiple lazy tasks, stopping when enough results obtained.
    
    Args:
        tasks: List of lazy computations
        max_results: Maximum number of results to compute
    

    Parameters: – tasks: Tasks – max_results: Max results

    Returns: <ast.Subscript object at 0x000001A06D865010>

    lazy_llm_batch(self, prompts, process_until)

    Lines 304-321

    Purpose:

    Process prompts lazily, with optional early termination.
    
    Args:
        prompts: List of prompts to process
        process_until: Optional function to check if we should stop
    

    Parameters: – prompts: Prompt template or text – process_until: Process until

    Returns: <ast.Subscript object at 0x000001A06D864E50>

    get_stats(self)

    Lines 323-334

    Purpose:

    Get computation statistics.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D811510>


    Functions

    lazy(func)

    Lines 129-136

    Purpose:

    Decorator to make a function lazy.
    

    Parameters: – func: Func

    Returns: Callable

    demonstrate_lazy_evaluation()

    Lines 337-598

    Purpose:

    Demonstrate the lazy evaluation pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Lazy Evaluation 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 140
    • Pattern 142

    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 Lazy Evaluation 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: 141_lazy_evaluation.py
    Total Lines: 603
    Pattern Category: AI Agent Design Pattern #141


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Escalation

    
        """
    Pattern 140: Escalation Pattern
    
    Description:
        The Escalation Pattern implements mechanisms for identifying situations that
        require human intervention or higher-level decision-making and routing them
        appropriately through an escalation hierarchy. This pattern recognizes when an
        agent encounters scenarios beyond its capabilities, authority, or confidence
        level and escalates to appropriate handlers based on severity and context.
    
        Escalation is critical for AI safety and reliability, ensuring that complex,
        high-stakes, or ambiguous situations receive appropriate human oversight. The
        pattern supports multiple escalation levels, severity assessment, automatic
        triggering based on rules or confidence thresholds, and routing to appropriate
        stakeholders. It balances agent autonomy with human control.
    
        This implementation provides a comprehensive escalation system with severity
        assessment, trigger conditions, escalation routing, stakeholder management,
        escalation tracking, and resolution workflows. It supports both rule-based
        and confidence-based escalation with customizable escalation paths.
    
    Components:
        - Severity Assessment: Evaluate situation severity
        - Trigger Detection: Identify when to escalate
        - Escalation Routing: Route to appropriate handler
        - Stakeholder Management: Manage escalation recipients
        - Escalation Tracking: Monitor escalation status
        - Resolution Workflow: Handle escalation lifecycle
    
    Use Cases:
        - High-stakes decisions (medical, financial, legal)
        - Low-confidence predictions
        - Policy violations
        - Security incidents
        - Customer complaints
        - System errors
        - Ethical dilemmas
        - Edge cases
    
    LangChain Implementation:
        This implementation uses:
        - LLM for severity assessment and decision complexity analysis
        - Rule-based escalation triggers
        - Confidence-threshold escalation
        - Hierarchical escalation routing
        - Escalation event tracking
        - Integration with human-in-the-loop systems
    
    Benefits:
        - Ensures human oversight for critical decisions
        - Prevents autonomous errors in high-stakes scenarios
        - Provides safety net for uncertain situations
        - Enables graduated response based on severity
        - Supports compliance requirements
        - Facilitates learning from escalations
        - Maintains audit trail
    
    Trade-offs:
        - Reduces full autonomy
        - Introduces latency for escalated cases
        - Requires human availability
        - May create bottlenecks
        - Needs clear escalation criteria
        - Can interrupt workflow
    
    Production Considerations:
        - Define clear escalation criteria
        - Set appropriate confidence thresholds
        - Implement escalation routing rules
        - Ensure stakeholder availability
        - Track escalation metrics
        - Provide escalation status updates
        - Support escalation history
        - Enable escalation feedback
        - Monitor escalation patterns
        - Reduce false escalations
        - Support emergency escalations
        - Document escalation procedures
    """
    
    import os
    from typing import List, Dict, Any, Optional
    from datetime import datetime, timedelta
    from enum import Enum
    from dataclasses import dataclass, field
    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 EscalationLevel(Enum):
        """Escalation severity levels."""
        CRITICAL = "critical"
        HIGH = "high"
        MEDIUM = "medium"
        LOW = "low"
    
    
    class EscalationStatus(Enum):
        """Status of escalation."""
        PENDING = "pending"
        IN_PROGRESS = "in_progress"
        RESOLVED = "resolved"
        CANCELLED = "cancelled"
    
    
    class EscalationTrigger(Enum):
        """Reasons for escalation."""
        LOW_CONFIDENCE = "low_confidence"
        POLICY_VIOLATION = "policy_violation"
        HIGH_RISK = "high_risk"
        SECURITY_INCIDENT = "security_incident"
        ERROR = "error"
        AMBIGUOUS = "ambiguous"
        SENSITIVE_DATA = "sensitive_data"
        MANUAL_REQUEST = "manual_request"
        THRESHOLD_EXCEEDED = "threshold_exceeded"
    
    
    @dataclass
    class Stakeholder:
        """Escalation stakeholder."""
        stakeholder_id: str
        name: str
        role: str
        email: str
        escalation_levels: List[EscalationLevel]
        availability: bool = True
        priority: int = 0
    
    
    @dataclass
    class EscalationCase:
        """Escalation case record."""
        case_id: str
        level: EscalationLevel
        trigger: EscalationTrigger
        title: str
        description: str
        context: Dict[str, Any]
        agent_id: str
        confidence_score: Optional[float]
        created_at: datetime
        assigned_to: Optional[str] = None
        status: EscalationStatus = EscalationStatus.PENDING
        resolution: Optional[str] = None
        resolved_at: Optional[datetime] = None
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class EscalationRule:
        """Rule for triggering escalation."""
        rule_id: str
        name: str
        condition: str  # e.g., "confidence  None:
            """Add escalation stakeholder."""
            self.stakeholders[stakeholder.stakeholder_id] = stakeholder
            print(f"Added stakeholder: {stakeholder.name} ({stakeholder.role})")
        
        def add_rule(self, rule: EscalationRule) -> None:
            """Add escalation rule."""
            self.rules[rule.rule_id] = rule
            print(f"Added escalation rule: {rule.name}")
        
        def should_escalate(
            self,
            context: Dict[str, Any],
            confidence: Optional[float] = None
        ) -> tuple[bool, Optional[EscalationTrigger], Optional[EscalationLevel]]:
            """
            Determine if situation should be escalated.
            
            Returns:
                Tuple of (should_escalate, trigger, level)
            """
            # Check confidence threshold
            if confidence is not None and confidence  bool:
            """Evaluate if rule condition is met."""
            condition = rule.condition.lower()
            
            # Simple condition evaluation
            if "sensitive_data" in condition:
                return context.get("has_sensitive_data", False)
            
            if "high_risk" in condition:
                risk_score = context.get("risk_score", 0)
                return risk_score > self.risk_threshold
            
            if "error" in condition:
                return context.get("error_occurred", False)
            
            return False
        
        def create_escalation(
            self,
            title: str,
            description: str,
            context: Dict[str, Any],
            trigger: EscalationTrigger,
            level: Optional[EscalationLevel] = None,
            confidence: Optional[float] = None
        ) -> EscalationCase:
            """Create an escalation case."""
            # Assess severity if not provided
            if level is None:
                level = self._assess_severity(title, description, context, trigger)
            
            case = EscalationCase(
                case_id=f"esc_{len(self.cases) + 1}_{datetime.now().timestamp()}",
                level=level,
                trigger=trigger,
                title=title,
                description=description,
                context=context,
                agent_id=self.agent_id,
                confidence_score=confidence,
                created_at=datetime.now()
            )
            
            # Route to stakeholder
            stakeholder = self._route_escalation(case)
            if stakeholder:
                case.assigned_to = stakeholder.stakeholder_id
            
            self.cases.append(case)
            return case
        
        def _assess_severity(
            self,
            title: str,
            description: str,
            context: Dict[str, Any],
            trigger: EscalationTrigger
        ) -> EscalationLevel:
            """Assess severity using LLM."""
            try:
                result = self.severity_chain.invoke({
                    "title": title,
                    "description": description,
                    "context": str(context),
                    "trigger": trigger.value
                })
                
                # Parse severity
                for line in result.split('\n'):
                    if line.startswith("SEVERITY:"):
                        severity = line.replace("SEVERITY:", "").strip().lower()
                        if severity == "critical":
                            return EscalationLevel.CRITICAL
                        elif severity == "high":
                            return EscalationLevel.HIGH
                        elif severity == "medium":
                            return EscalationLevel.MEDIUM
                        else:
                            return EscalationLevel.LOW
                
                return EscalationLevel.MEDIUM
                
            except Exception as e:
                print(f"Error assessing severity: {e}")
                return EscalationLevel.MEDIUM
        
        def _route_escalation(
            self,
            case: EscalationCase
        ) -> Optional[Stakeholder]:
            """Route escalation to appropriate stakeholder."""
            # Find available stakeholders for this level
            candidates = [
                s for s in self.stakeholders.values()
                if case.level in s.escalation_levels and s.availability
            ]
            
            if not candidates:
                return None
            
            # Sort by priority and return highest
            candidates.sort(key=lambda s: s.priority, reverse=True)
            return candidates[0]
        
        def update_case_status(
            self,
            case_id: str,
            status: EscalationStatus,
            resolution: Optional[str] = None
        ) -> bool:
            """Update escalation case status."""
            case = next((c for c in self.cases if c.case_id == case_id), None)
            if not case:
                return False
            
            case.status = status
            if resolution:
                case.resolution = resolution
            
            if status == EscalationStatus.RESOLVED:
                case.resolved_at = datetime.now()
            
            return True
        
        def get_pending_escalations(
            self,
            level: Optional[EscalationLevel] = None,
            stakeholder_id: Optional[str] = None
        ) -> List[EscalationCase]:
            """Get pending escalation cases."""
            pending = [c for c in self.cases if c.status == EscalationStatus.PENDING]
            
            if level:
                pending = [c for c in pending if c.level == level]
            
            if stakeholder_id:
                pending = [c for c in pending if c.assigned_to == stakeholder_id]
            
            return pending
        
        def get_escalation_metrics(
            self,
            days: int = 7
        ) -> Dict[str, Any]:
            """Get escalation metrics."""
            cutoff = datetime.now() - timedelta(days=days)
            recent_cases = [c for c in self.cases if c.created_at >= cutoff]
            
            # Resolution time for resolved cases
            resolved = [c for c in recent_cases if c.status == EscalationStatus.RESOLVED]
            avg_resolution_time = None
            if resolved:
                times = [
                    (c.resolved_at - c.created_at).total_seconds() / 3600
                    for c in resolved if c.resolved_at
                ]
                avg_resolution_time = sum(times) / len(times) if times else None
            
            return {
                "period_days": days,
                "total_escalations": len(recent_cases),
                "pending": len([c for c in recent_cases if c.status == EscalationStatus.PENDING]),
                "in_progress": len([c for c in recent_cases if c.status == EscalationStatus.IN_PROGRESS]),
                "resolved": len(resolved),
                "by_level": {
                    level.value: len([c for c in recent_cases if c.level == level])
                    for level in EscalationLevel
                },
                "by_trigger": {
                    trigger.value: len([c for c in recent_cases if c.trigger == trigger])
                    for trigger in EscalationTrigger
                },
                "avg_resolution_hours": avg_resolution_time
            }
    
    
    def demonstrate_escalation():
        """Demonstrate the escalation pattern."""
        print("=" * 80)
        print("Escalation Pattern Demonstration")
        print("=" * 80)
        
        agent = EscalationAgent(agent_id="agent_001")
        
        # Demonstration 1: Add Stakeholders
        print("\n" + "=" * 80)
        print("Demonstration 1: Configure Escalation Stakeholders")
        print("=" * 80)
        
        support = Stakeholder(
            stakeholder_id="stake_001",
            name="Support Team",
            role="support",
            email="support@company.com",
            escalation_levels=[EscalationLevel.LOW, EscalationLevel.MEDIUM],
            priority=10
        )
        
        manager = Stakeholder(
            stakeholder_id="stake_002",
            name="Engineering Manager",
            role="manager",
            email="manager@company.com",
            escalation_levels=[EscalationLevel.MEDIUM, EscalationLevel.HIGH],
            priority=20
        )
        
        director = Stakeholder(
            stakeholder_id="stake_003",
            name="Director",
            role="director",
            email="director@company.com",
            escalation_levels=[EscalationLevel.HIGH, EscalationLevel.CRITICAL],
            priority=30
        )
        
        agent.add_stakeholder(support)
        agent.add_stakeholder(manager)
        agent.add_stakeholder(director)
        
        # Demonstration 2: Add Escalation Rules
        print("\n" + "=" * 80)
        print("Demonstration 2: Define Escalation Rules")
        print("=" * 80)
        
        sensitive_rule = EscalationRule(
            rule_id="rule_001",
            name="Sensitive Data Rule",
            condition="contains sensitive_data",
            level=EscalationLevel.HIGH,
            trigger=EscalationTrigger.SENSITIVE_DATA
        )
        
        risk_rule = EscalationRule(
            rule_id="rule_002",
            name="High Risk Rule",
            condition="high_risk",
            level=EscalationLevel.HIGH,
            trigger=EscalationTrigger.HIGH_RISK
        )
        
        agent.add_rule(sensitive_rule)
        agent.add_rule(risk_rule)
        
        # Demonstration 3: Low Confidence Escalation
        print("\n" + "=" * 80)
        print("Demonstration 3: Low Confidence Escalation")
        print("=" * 80)
        
        should_escalate, trigger, level = agent.should_escalate(
            context={"task": "medical_diagnosis"},
            confidence=0.45
        )
        
        print(f"Should escalate: {should_escalate}")
        print(f"Trigger: {trigger.value if trigger else None}")
        print(f"Level: {level.value if level else None}")
        
        if should_escalate:
            case1 = agent.create_escalation(
                title="Low Confidence Medical Diagnosis",
                description="Agent confidence below threshold for medical diagnosis",
                context={"task": "medical_diagnosis", "symptoms": "fever, cough"},
                trigger=trigger,
                level=level,
                confidence=0.45
            )
            print(f"\nCreated escalation case: {case1.case_id}")
            print(f"Assigned to: {case1.assigned_to}")
            print(f"Status: {case1.status.value}")
        
        # Demonstration 4: Security Incident Escalation
        print("\n" + "=" * 80)
        print("Demonstration 4: Security Incident Escalation")
        print("=" * 80)
        
        case2 = agent.create_escalation(
            title="Unauthorized Access Attempt",
            description="Multiple failed login attempts detected",
            context={
                "security_incident": True,
                "source_ip": "192.168.1.100",
                "attempts": 10
            },
            trigger=EscalationTrigger.SECURITY_INCIDENT,
            level=EscalationLevel.CRITICAL
        )
        
        print(f"Created critical escalation: {case2.case_id}")
        print(f"Level: {case2.level.value}")
        print(f"Trigger: {case2.trigger.value}")
        print(f"Assigned to: {case2.assigned_to}")
        
        # Demonstration 5: Rule-Based Escalation
        print("\n" + "=" * 80)
        print("Demonstration 5: Rule-Based Escalation")
        print("=" * 80)
        
        should_escalate, trigger, level = agent.should_escalate(
            context={
                "has_sensitive_data": True,
                "data_type": "PII"
            }
        )
        
        if should_escalate:
            case3 = agent.create_escalation(
                title="Sensitive Data Access",
                description="Request to access personally identifiable information",
                context={"has_sensitive_data": True, "data_type": "PII"},
                trigger=trigger,
                level=level
            )
            print(f"Created escalation: {case3.case_id}")
            print(f"Trigger: {trigger.value}")
            print(f"Level: {level.value}")
        
        # Demonstration 6: Manual Escalation Request
        print("\n" + "=" * 80)
        print("Demonstration 6: Manual Escalation Request")
        print("=" * 80)
        
        case4 = agent.create_escalation(
            title="Complex Customer Complaint",
            description="Customer requesting refund for premium service",
            context={
                "escalate": True,
                "customer_id": "cust_12345",
                "amount": 5000
            },
            trigger=EscalationTrigger.MANUAL_REQUEST
        )
        
        print(f"Manual escalation: {case4.case_id}")
        print(f"Level: {case4.level.value}")
        print(f"Assigned to: {case4.assigned_to}")
        
        # Demonstration 7: Case Resolution
        print("\n" + "=" * 80)
        print("Demonstration 7: Escalation Resolution")
        print("=" * 80)
        
        print(f"\nResolving case: {case3.case_id}")
        agent.update_case_status(
            case_id=case3.case_id,
            status=EscalationStatus.RESOLVED,
            resolution="Access granted with additional monitoring"
        )
        
        resolved_case = next(c for c in agent.cases if c.case_id == case3.case_id)
        print(f"Status: {resolved_case.status.value}")
        print(f"Resolution: {resolved_case.resolution}")
        
        # Demonstration 8: Pending Escalations
        print("\n" + "=" * 80)
        print("Demonstration 8: Pending Escalations")
        print("=" * 80)
        
        pending = agent.get_pending_escalations()
        print(f"\nTotal pending escalations: {len(pending)}")
        
        for case in pending:
            print(f"\n  Case: {case.case_id}")
            print(f"    Title: {case.title}")
            print(f"    Level: {case.level.value}")
            print(f"    Assigned to: {case.assigned_to}")
            print(f"    Age: {(datetime.now() - case.created_at).seconds // 60} minutes")
        
        # Demonstration 9: Critical Escalations Only
        print("\n" + "=" * 80)
        print("Demonstration 9: Critical Escalations")
        print("=" * 80)
        
        critical = agent.get_pending_escalations(level=EscalationLevel.CRITICAL)
        print(f"\nCritical pending escalations: {len(critical)}")
        for case in critical:
            print(f"  - {case.title}")
        
        # Demonstration 10: Escalation Metrics
        print("\n" + "=" * 80)
        print("Demonstration 10: Escalation Metrics")
        print("=" * 80)
        
        metrics = agent.get_escalation_metrics(days=7)
        print(f"\nEscalation Metrics (last {metrics['period_days']} days):")
        print(f"  Total Escalations: {metrics['total_escalations']}")
        print(f"  Pending: {metrics['pending']}")
        print(f"  In Progress: {metrics['in_progress']}")
        print(f"  Resolved: {metrics['resolved']}")
        print(f"\n  By Level:")
        for level, count in metrics['by_level'].items():
            if count > 0:
                print(f"    {level}: {count}")
        print(f"\n  By Trigger:")
        for trigger, count in metrics['by_trigger'].items():
            if count > 0:
                print(f"    {trigger}: {count}")
        
        if metrics['avg_resolution_hours']:
            print(f"\n  Avg Resolution Time: {metrics['avg_resolution_hours']:.1f} hours")
        
        # Summary
        print("\n" + "=" * 80)
        print("Summary: Escalation Pattern")
        print("=" * 80)
        print("""
    The Escalation Pattern ensures appropriate human oversight for critical situations:
    
    Key Features Demonstrated:
    1. Stakeholder Management - Configure escalation recipients by level
    2. Escalation Rules - Define automatic escalation triggers
    3. Confidence-Based Escalation - Escalate low-confidence decisions
    4. Security Escalation - Handle security incidents
    5. Rule-Based Escalation - Trigger based on context rules
    6. Manual Escalation - Support explicit escalation requests
    7. Case Resolution - Track escalation lifecycle
    8. Pending Cases - Monitor unresolved escalations
    9. Level Filtering - Focus on critical cases
    10. Metrics Tracking - Monitor escalation patterns
    
    Benefits:
    - Ensures human oversight for critical decisions
    - Prevents autonomous errors
    - Provides safety net for uncertainty
    - Graduated response by severity
    - Supports compliance
    - Facilitates learning
    - Maintains audit trail
    
    Best Practices:
    - Define clear escalation criteria
    - Set appropriate thresholds
    - Implement routing rules
    - Ensure stakeholder availability
    - Track metrics
    - Provide status updates
    - Support escalation history
    - Enable feedback loops
    - Monitor patterns
    - Reduce false escalations
    - Support emergency escalations
    - Document procedures
    
    This pattern is essential for safe AI systems requiring human oversight,
    especially in high-stakes domains like healthcare, finance, and security.
    """)
    
    
    if __name__ == "__main__":
        demonstrate_escalation()
    
        

    Agentic Design Pattern: Escalation

    Pattern 140: Escalation – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 140: Escalation Pattern

    Description: The Escalation Pattern implements mechanisms for identifying situations that require human intervention or higher-level decision-making and routing them appropriately through an escalation hierarchy. This pattern recognizes when an agent encounters scenarios beyond its capabilities, authority, or confidence level and escalates to appropriate handlers based on severity and context.

    Escalation is critical for AI safety and reliability, ensuring that complex,
    high-stakes, or ambiguous situations receive appropriate human oversight. The
    pattern supports multiple escalation levels, severity assessment, automatic
    triggering based on rules or confidence thresholds, and routing to appropriate
    stakeholders. It balances agent autonomy with human control.
    
    This implementation provides a comprehensive escalation system with severity
    assessment, trigger conditions, escalation routing, stakeholder management,
    escalation tracking, and resolution workflows. It supports both rule-based
    and confidence-based escalation with customizable escalation paths.
    

    Components: – Severity Assessment: Evaluate situation severity – Trigger Detection: Identify when to escalate – Escalation Routing: Route to appropriate handler – Stakeholder Management: Manage escalation recipients – Escalation Tracking: Monitor escalation status – Resolution Workflow: Handle escalation lifecycle

    Use Cases: – High-stakes decisions (medical, financial, legal) – Low-confidence predictions – Policy violations – Security incidents – Customer complaints – System errors – Ethical dilemmas – Edge cases

    LangChain Implementation: This implementation uses: – LLM for severity assessment and decision complexity analysis – Rule-based escalation triggers – Confidence-threshold escalation – Hierarchical escalation routing – Escalation event tracking – Integration with human-in-the-loop systems

    Benefits: – Ensures human oversight for critical decisions – Prevents autonomous errors in high-stakes scenarios – Provides safety net for uncertain situations – Enables graduated response based on severity – Supports compliance requirements – Facilitates learning from escalations – Maintains audit trail

    Trade-offs: – Reduces full autonomy – Introduces latency for escalated cases – Requires human availability – May create bottlenecks – Needs clear escalation criteria – Can interrupt workflow

    Production Considerations: – Define clear escalation criteria – Set appropriate confidence thresholds – Implement escalation routing rules – Ensure stakeholder availability – Track escalation metrics – Provide escalation status updates – Support escalation history – Enable escalation feedback – Monitor escalation patterns – Reduce false escalations – Support emergency escalations – Document escalation procedures


    File Statistics

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

    Module Documentation

    """
    Pattern 140: Escalation Pattern
    
    Description:
        The Escalation Pattern implements mechanisms for identifying situations that
        require human intervention or higher-level decision-making and routing them
        appropriately through an escalation hierarchy. This pattern recognizes when an
        agent encounters scenarios beyond its capabilities, authority, or confidence
        level and escalates to appropriate handlers based on severity and context.
    
        Escalation is critical for AI safety and reliability, ensuring that complex,
        high-stakes, or ambiguous situations receive appropriate human oversight. The
        pattern supports multiple escalation levels, severity assessment, automatic
        triggering based on rules or confidence thresholds, and routing to appropriate
        stakeholders. It balances agent autonomy with human control.
    
        This implementation provides a comprehensive escalation system with severity
        assessment, trigger conditions, escalation routing, stakeholder management,
        escalation tracking, and resolution workflows. It supports both rule-based
        and confidence-based escalation with customizable escalation paths.
    
    Components:
        - Severity Assessment: Evaluate situation severity
        - Trigger Detection: Identify when to escalate
        - Escalation Routing: Route to appropriate handler
        - Stakeholder Management: Manage escalation recipients
        - Escalation Tracking: Monitor escalation status
        - Resolution Workflow: Handle escalation lifecycle
    
    Use Cases:
        - High-stakes decisions (medical, financial, legal)
        - Low-confidence predictions
        - Policy violations
        - Security incidents
        - Customer complaints
        - System errors
        - Ethical dilemmas
        - Edge cases
    
    LangChain Implementation:
        This implementation uses:
        - LLM for severity assessment and decision complexity analysis
        - Rule-based escalation triggers
        - Confidence-threshold escalation
        - Hierarchical escalation routing
        - Escalation event tracking
        - Integration with human-in-the-loop systems
    
    Benefits:
        - Ensures human oversight for critical decisions
        - Prevents autonomous errors in high-stakes scenarios
        - Provides safety net for uncertain situations
        - Enables graduated response based on severity
        - Supports compliance requirements
        - Facilitates learning from escalations
        - Maintains audit trail
    
    Trade-offs:
        - Reduces full autonomy
        - Introduces latency for escalated cases
        - Requires human availability
        - May create bottlenecks
        - Needs clear escalation criteria
        - Can interrupt workflow
    
    Production Considerations:
        - Define clear escalation criteria
        - Set appropriate confidence thresholds
        - Implement escalation routing rules
        - Ensure stakeholder availability
        - Track escalation metrics
        - Provide escalation status updates
        - Support escalation history
        - Enable escalation feedback
        - Monitor escalation patterns
        - Reduce false escalations
        - Support emergency escalations
        - Document escalation procedures
    """
    

    Purpose: This module implements the Escalation 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 datetime.timedelta
    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 – 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 – 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: EscalationLevel

    Lines 95-100

    Inherits from: Enum

    Purpose:

    Escalation severity levels.
    

    Class: EscalationStatus

    Lines 103-108

    Inherits from: Enum

    Purpose:

    Status of escalation.
    

    Class: EscalationTrigger

    Lines 111-121

    Inherits from: Enum

    Purpose:

    Reasons for escalation.
    

    Class: Stakeholder

    Lines 125-133

    Purpose:

    Escalation stakeholder.
    

    Class: EscalationCase

    Lines 137-152

    Purpose:

    Escalation case record.
    

    Class: EscalationRule

    Lines 156-163

    Purpose:

    Rule for triggering escalation.
    

    Class: EscalationAgent

    Lines 166-432

    Purpose:

    Agent that manages escalation of decisions and situations.
    
    This agent identifies when to escalate, assesses severity,
    routes to appropriate stakeholders, and tracks resolution.
    

    Methods:

    __init__(self, agent_id, temperature)

    Lines 174-208

    Purpose:

    Initialize the escalation agent.
    

    Parameters: – agent_id: Agent id – temperature: Sampling temperature (0-1)

    add_stakeholder(self, stakeholder)

    Lines 210-213

    Purpose:

    Add escalation stakeholder.
    

    Parameters: – stakeholder: Stakeholder

    Returns: None

    add_rule(self, rule)

    Lines 215-218

    Purpose:

    Add escalation rule.
    

    Parameters: – rule: Rule

    Returns: None

    should_escalate(self, context, confidence)

    Lines 220-253

    Purpose:

    Determine if situation should be escalated.
    
    Returns:
        Tuple of (should_escalate, trigger, level)
    

    Parameters: – context: Context – confidence: Confidence

    Returns: <ast.Subscript object at 0x000001A06D649A90>

    _evaluate_rule(self, rule, context)

    Lines 255-274

    Purpose:

    Evaluate if rule condition is met.
    

    Parameters: – rule: Rule – context: Context

    Returns: bool

    create_escalation(self, title, description, context, trigger, level, confidence)

    Lines 276-308

    Purpose:

    Create an escalation case.
    

    Parameters: – title: Title – description: Description – context: Context – trigger: Trigger – level: Level – confidence: Confidence

    Returns: EscalationCase

    _assess_severity(self, title, description, context, trigger)

    Lines 310-343

    Purpose:

    Assess severity using LLM.
    

    Parameters: – title: Title – description: Description – context: Context – trigger: Trigger

    Returns: EscalationLevel

    _route_escalation(self, case)

    Lines 345-361

    Purpose:

    Route escalation to appropriate stakeholder.
    

    Parameters: – case: Case

    Returns: <ast.Subscript object at 0x000001A06D8890D0>

    update_case_status(self, case_id, status, resolution)

    Lines 363-381

    Purpose:

    Update escalation case status.
    

    Parameters: – case_id: Case id – status: Status – resolution: Resolution

    Returns: bool

    get_pending_escalations(self, level, stakeholder_id)

    Lines 383-397

    Purpose:

    Get pending escalation cases.
    

    Parameters: – level: Level – stakeholder_id: Stakeholder id

    Returns: <ast.Subscript object at 0x000001A06D824C90>

    get_escalation_metrics(self, days)

    Lines 399-432

    Purpose:

    Get escalation metrics.
    

    Parameters: – days: Days

    Returns: <ast.Subscript object at 0x000001A06D683110>


    Functions

    demonstrate_escalation()

    Lines 435-704

    Purpose:

    Demonstrate the escalation pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Escalation 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 139
    • Pattern 141

    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 Escalation 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: 140_escalation.py
    Total Lines: 709
    Pattern Category: AI Agent Design Pattern #140


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Permission Authorization

    
        """
    Pattern 139: Permission & Authorization
    
    Description:
        The Permission & Authorization pattern implements fine-grained access control
        for agent systems, determining who can perform what actions on which resources.
        This pattern provides role-based access control (RBAC), attribute-based access
        control (ABAC), and resource-level permissions to ensure that agents and users
        only access what they're authorized to access.
    
        Authorization is fundamental to secure AI systems, especially in multi-user
        environments or when handling sensitive data. It enforces the principle of
        least privilege, prevents unauthorized access, and ensures compliance with
        security policies. The pattern supports hierarchical roles, permission
        inheritance, dynamic permissions, and context-aware authorization.
    
        This implementation provides a comprehensive authorization system with role
        management, permission assignment, access control checks, resource-level
        permissions, and permission delegation. It supports both static and dynamic
        authorization decisions based on user attributes, resource properties, and
        environmental context.
    
    Components:
        - Role Management: Define and manage user roles
        - Permission Assignment: Assign permissions to roles and users
        - Access Control: Check if action is authorized
        - Resource Protection: Control access to specific resources
        - Permission Delegation: Temporary permission grants
        - Context-Aware Authorization: Consider context in decisions
    
    Use Cases:
        - Multi-user AI systems
        - Enterprise agent platforms
        - Data access control
        - API authorization
        - Healthcare systems (HIPAA compliance)
        - Financial systems
        - Government systems
        - Multi-tenant platforms
    
    LangChain Implementation:
        This implementation uses:
        - Custom RBAC and ABAC systems
        - Permission hierarchy and inheritance
        - Resource-level access control
        - LLM for complex authorization decisions
        - Context-aware permission evaluation
        - Audit logging integration
    
    Benefits:
        - Enforces least privilege principle
        - Prevents unauthorized access
        - Supports compliance requirements
        - Enables fine-grained control
        - Facilitates security auditing
        - Supports multi-tenancy
        - Enables delegation
    
    Trade-offs:
        - Adds complexity to system
        - Requires permission management
        - May impact performance
        - Needs careful role design
        - Can be difficult to debug
        - Requires ongoing maintenance
    
    Production Considerations:
        - Design clear role hierarchy
        - Implement efficient permission checks
        - Cache authorization decisions
        - Log all authorization events
        - Support emergency access
        - Implement permission review process
        - Handle permission conflicts
        - Support temporal permissions
        - Provide audit capabilities
        - Test permission combinations
        - Document permission model
        - Monitor unauthorized attempts
    """
    
    import os
    from typing import List, Dict, Any, Optional, Set
    from datetime import datetime, timedelta
    from enum import Enum
    from dataclasses import dataclass, field
    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 ActionType(Enum):
        """Types of actions."""
        READ = "read"
        WRITE = "write"
        UPDATE = "update"
        DELETE = "delete"
        EXECUTE = "execute"
        ADMIN = "admin"
        EXPORT = "export"
        SHARE = "share"
    
    
    class ResourceType(Enum):
        """Types of resources."""
        DATA = "data"
        MODEL = "model"
        AGENT = "agent"
        CONFIG = "config"
        USER = "user"
        API = "api"
    
    
    @dataclass
    class Permission:
        """Permission definition."""
        permission_id: str
        name: str
        description: str
        resource_type: ResourceType
        action: ActionType
        conditions: Dict[str, Any] = field(default_factory=dict)
        
        def __hash__(self):
            return hash(self.permission_id)
    
    
    @dataclass
    class Role:
        """Role definition."""
        role_id: str
        name: str
        description: str
        permissions: Set[Permission] = field(default_factory=set)
        parent_roles: Set[str] = field(default_factory=set)
        created_at: datetime = field(default_factory=datetime.now)
        
        def add_permission(self, permission: Permission):
            """Add permission to role."""
            self.permissions.add(permission)
        
        def remove_permission(self, permission: Permission):
            """Remove permission from role."""
            self.permissions.discard(permission)
        
        def has_permission(self, permission: Permission) -> bool:
            """Check if role has permission."""
            return permission in self.permissions
    
    
    @dataclass
    class User:
        """User definition."""
        user_id: str
        username: str
        roles: Set[str] = field(default_factory=set)
        direct_permissions: Set[Permission] = field(default_factory=set)
        attributes: Dict[str, Any] = field(default_factory=dict)
        created_at: datetime = field(default_factory=datetime.now)
        
        def add_role(self, role_id: str):
            """Add role to user."""
            self.roles.add(role_id)
        
        def remove_role(self, role_id: str):
            """Remove role from user."""
            self.roles.discard(role_id)
        
        def add_permission(self, permission: Permission):
            """Add direct permission to user."""
            self.direct_permissions.add(permission)
    
    
    @dataclass
    class Resource:
        """Resource definition."""
        resource_id: str
        resource_type: ResourceType
        owner_id: str
        attributes: Dict[str, Any] = field(default_factory=dict)
        created_at: datetime = field(default_factory=datetime.now)
    
    
    @dataclass
    class AuthorizationRequest:
        """Authorization request."""
        user_id: str
        action: ActionType
        resource: Resource
        context: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class AuthorizationResult:
        """Authorization result."""
        authorized: bool
        reason: str
        matched_permissions: List[Permission]
        timestamp: datetime = field(default_factory=datetime.now)
    
    
    class PermissionAuthorizationAgent:
        """
        Agent that provides permission and authorization management.
        
        This agent manages roles, permissions, and authorization decisions
        using RBAC and ABAC approaches.
        """
        
        def __init__(self, temperature: float = 0.3):
            """Initialize the authorization agent."""
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.roles: Dict[str, Role] = {}
            self.users: Dict[str, User] = {}
            self.resources: Dict[str, Resource] = {}
            self.permissions: Dict[str, Permission] = {}
            
            # Create authorization chain for complex decisions
            auth_prompt = ChatPromptTemplate.from_template(
                """You are an authorization expert. Determine if the user should be authorized for this action.
    
    User: {user_id}
    User Attributes: {user_attributes}
    Action: {action}
    Resource Type: {resource_type}
    Resource Attributes: {resource_attributes}
    Context: {context}
    
    Consider:
    1. User's roles and permissions
    2. Resource sensitivity
    3. Context (time, location, etc.)
    4. Business rules and policies
    
    Respond in this format:
    AUTHORIZED: yes/no
    REASON: explanation
    RISK_LEVEL: low/medium/high
    CONDITIONS: any conditions for authorization"""
            )
            self.auth_chain = auth_prompt | self.llm | StrOutputParser()
        
        def create_permission(
            self,
            permission_id: str,
            name: str,
            description: str,
            resource_type: ResourceType,
            action: ActionType,
            conditions: Optional[Dict[str, Any]] = None
        ) -> Permission:
            """Create a new permission."""
            permission = Permission(
                permission_id=permission_id,
                name=name,
                description=description,
                resource_type=resource_type,
                action=action,
                conditions=conditions or {}
            )
            self.permissions[permission_id] = permission
            return permission
        
        def create_role(
            self,
            role_id: str,
            name: str,
            description: str,
            parent_roles: Optional[Set[str]] = None
        ) -> Role:
            """Create a new role."""
            role = Role(
                role_id=role_id,
                name=name,
                description=description,
                parent_roles=parent_roles or set()
            )
            self.roles[role_id] = role
            return role
        
        def create_user(
            self,
            user_id: str,
            username: str,
            roles: Optional[Set[str]] = None,
            attributes: Optional[Dict[str, Any]] = None
        ) -> User:
            """Create a new user."""
            user = User(
                user_id=user_id,
                username=username,
                roles=roles or set(),
                attributes=attributes or {}
            )
            self.users[user_id] = user
            return user
        
        def create_resource(
            self,
            resource_id: str,
            resource_type: ResourceType,
            owner_id: str,
            attributes: Optional[Dict[str, Any]] = None
        ) -> Resource:
            """Create a new resource."""
            resource = Resource(
                resource_id=resource_id,
                resource_type=resource_type,
                owner_id=owner_id,
                attributes=attributes or {}
            )
            self.resources[resource_id] = resource
            return resource
        
        def assign_permission_to_role(
            self,
            role_id: str,
            permission_id: str
        ) -> bool:
            """Assign permission to role."""
            if role_id not in self.roles or permission_id not in self.permissions:
                return False
            
            role = self.roles[role_id]
            permission = self.permissions[permission_id]
            role.add_permission(permission)
            return True
        
        def assign_role_to_user(
            self,
            user_id: str,
            role_id: str
        ) -> bool:
            """Assign role to user."""
            if user_id not in self.users or role_id not in self.roles:
                return False
            
            user = self.users[user_id]
            user.add_role(role_id)
            return True
        
        def get_user_permissions(self, user_id: str) -> Set[Permission]:
            """Get all permissions for a user (from roles and direct)."""
            if user_id not in self.users:
                return set()
            
            user = self.users[user_id]
            permissions = user.direct_permissions.copy()
            
            # Add permissions from roles
            for role_id in user.roles:
                if role_id in self.roles:
                    role = self.roles[role_id]
                    permissions.update(role.permissions)
                    
                    # Add permissions from parent roles (inheritance)
                    for parent_id in role.parent_roles:
                        if parent_id in self.roles:
                            permissions.update(self.roles[parent_id].permissions)
            
            return permissions
        
        def check_authorization(
            self,
            request: AuthorizationRequest
        ) -> AuthorizationResult:
            """
            Check if user is authorized for action on resource.
            
            This performs both static (RBAC) and dynamic (ABAC) checks.
            """
            if request.user_id not in self.users:
                return AuthorizationResult(
                    authorized=False,
                    reason="User not found",
                    matched_permissions=[]
                )
            
            user = self.users[request.user_id]
            user_permissions = self.get_user_permissions(request.user_id)
            
            # Check if user is owner
            if request.resource.owner_id == request.user_id:
                return AuthorizationResult(
                    authorized=True,
                    reason="User is resource owner",
                    matched_permissions=[]
                )
            
            # Check static permissions (RBAC)
            matched = []
            for permission in user_permissions:
                if (permission.resource_type == request.resource.resource_type and
                    permission.action == request.action):
                    
                    # Check conditions
                    if self._check_conditions(permission.conditions, request.context):
                        matched.append(permission)
            
            if matched:
                return AuthorizationResult(
                    authorized=True,
                    reason=f"Authorized via permissions: {', '.join(p.name for p in matched)}",
                    matched_permissions=matched
                )
            
            # If no static match, use LLM for dynamic decision (ABAC)
            return self._dynamic_authorization(user, request)
        
        def _check_conditions(
            self,
            conditions: Dict[str, Any],
            context: Dict[str, Any]
        ) -> bool:
            """Check if conditions are met."""
            if not conditions:
                return True
            
            for key, value in conditions.items():
                if key not in context or context[key] != value:
                    return False
            
            return True
        
        def _dynamic_authorization(
            self,
            user: User,
            request: AuthorizationRequest
        ) -> AuthorizationResult:
            """Use LLM for dynamic authorization decision."""
            try:
                result = self.auth_chain.invoke({
                    "user_id": user.user_id,
                    "user_attributes": str(user.attributes),
                    "action": request.action.value,
                    "resource_type": request.resource.resource_type.value,
                    "resource_attributes": str(request.resource.attributes),
                    "context": str(request.context)
                })
                
                # Parse result
                authorized = "yes" in result.lower().split("AUTHORIZED:")[1].split("\n")[0]
                reason_line = [l for l in result.split("\n") if l.startswith("REASON:")]
                reason = reason_line[0].replace("REASON:", "").strip() if reason_line else "Dynamic authorization"
                
                return AuthorizationResult(
                    authorized=authorized,
                    reason=reason,
                    matched_permissions=[]
                )
                
            except Exception as e:
                return AuthorizationResult(
                    authorized=False,
                    reason=f"Authorization check failed: {e}",
                    matched_permissions=[]
                )
        
        def get_authorization_summary(self) -> Dict[str, Any]:
            """Get summary of authorization system."""
            return {
                "total_users": len(self.users),
                "total_roles": len(self.roles),
                "total_permissions": len(self.permissions),
                "total_resources": len(self.resources),
                "users_by_role": {
                    role_id: len([u for u in self.users.values() if role_id in u.roles])
                    for role_id in self.roles.keys()
                }
            }
    
    
    def demonstrate_permission_authorization():
        """Demonstrate the permission and authorization pattern."""
        print("=" * 80)
        print("Permission & Authorization Pattern Demonstration")
        print("=" * 80)
        
        agent = PermissionAuthorizationAgent()
        
        # Demonstration 1: Create Permissions
        print("\n" + "=" * 80)
        print("Demonstration 1: Create Permissions")
        print("=" * 80)
        
        read_data = agent.create_permission(
            permission_id="perm_read_data",
            name="Read Data",
            description="Permission to read data resources",
            resource_type=ResourceType.DATA,
            action=ActionType.READ
        )
        
        write_data = agent.create_permission(
            permission_id="perm_write_data",
            name="Write Data",
            description="Permission to write data resources",
            resource_type=ResourceType.DATA,
            action=ActionType.WRITE
        )
        
        admin_all = agent.create_permission(
            permission_id="perm_admin",
            name="Admin All",
            description="Full administrative access",
            resource_type=ResourceType.DATA,
            action=ActionType.ADMIN
        )
        
        print(f"Created permissions:")
        print(f"  - {read_data.name}: {read_data.description}")
        print(f"  - {write_data.name}: {write_data.description}")
        print(f"  - {admin_all.name}: {admin_all.description}")
        
        # Demonstration 2: Create Roles
        print("\n" + "=" * 80)
        print("Demonstration 2: Create Roles with Hierarchy")
        print("=" * 80)
        
        viewer_role = agent.create_role(
            role_id="role_viewer",
            name="Viewer",
            description="Can view data"
        )
        agent.assign_permission_to_role("role_viewer", "perm_read_data")
        
        editor_role = agent.create_role(
            role_id="role_editor",
            name="Editor",
            description="Can view and edit data",
            parent_roles={"role_viewer"}
        )
        agent.assign_permission_to_role("role_editor", "perm_write_data")
        
        admin_role = agent.create_role(
            role_id="role_admin",
            name="Admin",
            description="Full system access",
            parent_roles={"role_editor"}
        )
        agent.assign_permission_to_role("role_admin", "perm_admin")
        
        print(f"Created role hierarchy:")
        print(f"  Admin -> Editor -> Viewer")
        print(f"  - Viewer: {len(viewer_role.permissions)} permissions")
        print(f"  - Editor: {len(editor_role.permissions)} permissions (+ inherited)")
        print(f"  - Admin: {len(admin_role.permissions)} permissions (+ inherited)")
        
        # Demonstration 3: Create Users
        print("\n" + "=" * 80)
        print("Demonstration 3: Create Users with Roles")
        print("=" * 80)
        
        user1 = agent.create_user(
            user_id="user_001",
            username="alice",
            roles={"role_viewer"},
            attributes={"department": "sales", "level": "junior"}
        )
        
        user2 = agent.create_user(
            user_id="user_002",
            username="bob",
            roles={"role_editor"},
            attributes={"department": "engineering", "level": "senior"}
        )
        
        user3 = agent.create_user(
            user_id="user_003",
            username="charlie",
            roles={"role_admin"},
            attributes={"department": "it", "level": "manager"}
        )
        
        print(f"Created users:")
        print(f"  - {user1.username}: {user1.roles}")
        print(f"  - {user2.username}: {user2.roles}")
        print(f"  - {user3.username}: {user3.roles}")
        
        # Demonstration 4: Create Resources
        print("\n" + "=" * 80)
        print("Demonstration 4: Create Protected Resources")
        print("=" * 80)
        
        resource1 = agent.create_resource(
            resource_id="res_001",
            resource_type=ResourceType.DATA,
            owner_id="user_003",
            attributes={"classification": "public", "department": "sales"}
        )
        
        resource2 = agent.create_resource(
            resource_id="res_002",
            resource_type=ResourceType.DATA,
            owner_id="user_003",
            attributes={"classification": "confidential", "department": "finance"}
        )
        
        print(f"Created resources:")
        print(f"  - {resource1.resource_id}: {resource1.attributes['classification']}")
        print(f"  - {resource2.resource_id}: {resource2.attributes['classification']}")
        
        # Demonstration 5: Authorization Checks
        print("\n" + "=" * 80)
        print("Demonstration 5: Authorization Checks")
        print("=" * 80)
        
        # Alice (viewer) tries to read
        request1 = AuthorizationRequest(
            user_id="user_001",
            action=ActionType.READ,
            resource=resource1,
            context={}
        )
        result1 = agent.check_authorization(request1)
        print(f"\nAlice (viewer) reads public data:")
        print(f"  Authorized: {result1.authorized}")
        print(f"  Reason: {result1.reason}")
        
        # Alice tries to write
        request2 = AuthorizationRequest(
            user_id="user_001",
            action=ActionType.WRITE,
            resource=resource1,
            context={}
        )
        result2 = agent.check_authorization(request2)
        print(f"\nAlice (viewer) writes data:")
        print(f"  Authorized: {result2.authorized}")
        print(f"  Reason: {result2.reason}")
        
        # Bob (editor) tries to write
        request3 = AuthorizationRequest(
            user_id="user_002",
            action=ActionType.WRITE,
            resource=resource1,
            context={}
        )
        result3 = agent.check_authorization(request3)
        print(f"\nBob (editor) writes data:")
        print(f"  Authorized: {result3.authorized}")
        print(f"  Reason: {result3.reason}")
        
        # Demonstration 6: Owner Access
        print("\n" + "=" * 80)
        print("Demonstration 6: Resource Owner Access")
        print("=" * 80)
        
        # Charlie is owner
        request4 = AuthorizationRequest(
            user_id="user_003",
            action=ActionType.DELETE,
            resource=resource1,
            context={}
        )
        result4 = agent.check_authorization(request4)
        print(f"\nCharlie (owner) deletes resource:")
        print(f"  Authorized: {result4.authorized}")
        print(f"  Reason: {result4.reason}")
        
        # Demonstration 7: User Permissions Summary
        print("\n" + "=" * 80)
        print("Demonstration 7: User Permissions Summary")
        print("=" * 80)
        
        for user_id in ["user_001", "user_002", "user_003"]:
            user = agent.users[user_id]
            permissions = agent.get_user_permissions(user_id)
            print(f"\n{user.username} permissions:")
            print(f"  Roles: {', '.join(user.roles)}")
            print(f"  Total Permissions: {len(permissions)}")
            for perm in permissions:
                print(f"    - {perm.name}: {perm.action.value} {perm.resource_type.value}")
        
        # Demonstration 8: System Summary
        print("\n" + "=" * 80)
        print("Demonstration 8: Authorization System Summary")
        print("=" * 80)
        
        summary = agent.get_authorization_summary()
        print(f"\nAuthorization System:")
        print(f"  Users: {summary['total_users']}")
        print(f"  Roles: {summary['total_roles']}")
        print(f"  Permissions: {summary['total_permissions']}")
        print(f"  Resources: {summary['total_resources']}")
        print(f"\n  Users by Role:")
        for role, count in summary['users_by_role'].items():
            if count > 0:
                print(f"    {role}: {count}")
        
        # Summary
        print("\n" + "=" * 80)
        print("Summary: Permission & Authorization Pattern")
        print("=" * 80)
        print("""
    The Permission & Authorization pattern provides fine-grained access control:
    
    Key Features Demonstrated:
    1. Permission Management - Create and manage permissions
    2. Role-Based Access Control - RBAC with role hierarchy
    3. User Management - Assign roles and permissions to users
    4. Resource Protection - Control access to specific resources
    5. Authorization Checks - Verify user access rights
    6. Owner Access - Automatic authorization for resource owners
    7. Permission Inheritance - Inherit permissions from parent roles
    8. System Management - Track and manage authorization state
    
    Benefits:
    - Enforces least privilege
    - Prevents unauthorized access
    - Supports compliance
    - Enables fine-grained control
    - Facilitates auditing
    - Supports multi-tenancy
    - Enables role delegation
    
    Best Practices:
    - Design clear role hierarchy
    - Use principle of least privilege
    - Cache authorization decisions
    - Log authorization events
    - Support emergency access
    - Review permissions regularly
    - Handle permission conflicts
    - Test permission combinations
    - Document permission model
    - Monitor unauthorized attempts
    - Support temporal permissions
    - Provide audit capabilities
    
    This pattern is essential for secure multi-user AI systems requiring
    access control, compliance, and security enforcement.
    """)
    
    
    if __name__ == "__main__":
        demonstrate_permission_authorization()
    
        

    Agentic Design Pattern: Permission Authorization

    Pattern 139: Permission Authorization – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Permission Authorization pattern implementation.


    Pattern Description

    Pattern 139: Permission & Authorization

    Description: The Permission & Authorization pattern implements fine-grained access control for agent systems, determining who can perform what actions on which resources. This pattern provides role-based access control (RBAC), attribute-based access control (ABAC), and resource-level permissions to ensure that agents and users only access what they’re authorized to access.

    Authorization is fundamental to secure AI systems, especially in multi-user
    environments or when handling sensitive data. It enforces the principle of
    least privilege, prevents unauthorized access, and ensures compliance with
    security policies. The pattern supports hierarchical roles, permission
    inheritance, dynamic permissions, and context-aware authorization.
    
    This implementation provides a comprehensive authorization system with role
    management, permission assignment, access control checks, resource-level
    permissions, and permission delegation. It supports both static and dynamic
    authorization decisions based on user attributes, resource properties, and
    environmental context.
    

    Components: – Role Management: Define and manage user roles – Permission Assignment: Assign permissions to roles and users – Access Control: Check if action is authorized – Resource Protection: Control access to specific resources – Permission Delegation: Temporary permission grants – Context-Aware Authorization: Consider context in decisions

    Use Cases: – Multi-user AI systems – Enterprise agent platforms – Data access control – API authorization – Healthcare systems (HIPAA compliance) – Financial systems – Government systems – Multi-tenant platforms

    LangChain Implementation: This implementation uses: – Custom RBAC and ABAC systems – Permission hierarchy and inheritance – Resource-level access control – LLM for complex authorization decisions – Context-aware permission evaluation – Audit logging integration

    Benefits: – Enforces least privilege principle – Prevents unauthorized access – Supports compliance requirements – Enables fine-grained control – Facilitates security auditing – Supports multi-tenancy – Enables delegation

    Trade-offs: – Adds complexity to system – Requires permission management – May impact performance – Needs careful role design – Can be difficult to debug – Requires ongoing maintenance

    Production Considerations: – Design clear role hierarchy – Implement efficient permission checks – Cache authorization decisions – Log all authorization events – Support emergency access – Implement permission review process – Handle permission conflicts – Support temporal permissions – Provide audit capabilities – Test permission combinations – Document permission model – Monitor unauthorized attempts


    File Statistics

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

    Module Documentation

    """
    Pattern 139: Permission & Authorization
    
    Description:
        The Permission & Authorization pattern implements fine-grained access control
        for agent systems, determining who can perform what actions on which resources.
        This pattern provides role-based access control (RBAC), attribute-based access
        control (ABAC), and resource-level permissions to ensure that agents and users
        only access what they're authorized to access.
    
        Authorization is fundamental to secure AI systems, especially in multi-user
        environments or when handling sensitive data. It enforces the principle of
        least privilege, prevents unauthorized access, and ensures compliance with
        security policies. The pattern supports hierarchical roles, permission
        inheritance, dynamic permissions, and context-aware authorization.
    
        This implementation provides a comprehensive authorization system with role
        management, permission assignment, access control checks, resource-level
        permissions, and permission delegation. It supports both static and dynamic
        authorization decisions based on user attributes, resource properties, and
        environmental context.
    
    Components:
        - Role Management: Define and manage user roles
        - Permission Assignment: Assign permissions to roles and users
        - Access Control: Check if action is authorized
        - Resource Protection: Control access to specific resources
        - Permission Delegation: Temporary permission grants
        - Context-Aware Authorization: Consider context in decisions
    
    Use Cases:
        - Multi-user AI systems
        - Enterprise agent platforms
        - Data access control
        - API authorization
        - Healthcare systems (HIPAA compliance)
        - Financial systems
        - Government systems
        - Multi-tenant platforms
    
    LangChain Implementation:
        This implementation uses:
        - Custom RBAC and ABAC systems
        - Permission hierarchy and inheritance
        - Resource-level access control
        - LLM for complex authorization decisions
        - Context-aware permission evaluation
        - Audit logging integration
    
    Benefits:
        - Enforces least privilege principle
        - Prevents unauthorized access
        - Supports compliance requirements
        - Enables fine-grained control
        - Facilitates security auditing
        - Supports multi-tenancy
        - Enables delegation
    
    Trade-offs:
        - Adds complexity to system
        - Requires permission management
        - May impact performance
        - Needs careful role design
        - Can be difficult to debug
        - Requires ongoing maintenance
    
    Production Considerations:
        - Design clear role hierarchy
        - Implement efficient permission checks
        - Cache authorization decisions
        - Log all authorization events
        - Support emergency access
        - Implement permission review process
        - Handle permission conflicts
        - Support temporal permissions
        - Provide audit capabilities
        - Test permission combinations
        - Document permission model
        - Monitor unauthorized attempts
    """
    

    Purpose: This module implements the Permission Authorization 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 datetime.timedelta
    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
    import typing.Set
    

    Purpose: Core Python functionality: – 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 – 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) – typing.Set: 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: ActionType

    Lines 96-105

    Inherits from: Enum

    Purpose:

    Types of actions.
    

    Class: ResourceType

    Lines 108-115

    Inherits from: Enum

    Purpose:

    Types of resources.
    

    Class: Permission

    Lines 119-129

    Purpose:

    Permission definition.
    

    Methods:

    __hash__(self)

    Lines 128-129

    Purpose: Hash operation.

    Parameters:


    Class: Role

    Lines 133-152

    Purpose:

    Role definition.
    

    Methods:

    add_permission(self, permission)

    Lines 142-144

    Purpose:

    Add permission to role.
    

    Parameters: – permission: Permission

    remove_permission(self, permission)

    Lines 146-148

    Purpose:

    Remove permission from role.
    

    Parameters: – permission: Permission

    has_permission(self, permission)

    Lines 150-152

    Purpose:

    Check if role has permission.
    

    Parameters: – permission: Permission

    Returns: bool


    Class: User

    Lines 156-175

    Purpose:

    User definition.
    

    Methods:

    add_role(self, role_id)

    Lines 165-167

    Purpose:

    Add role to user.
    

    Parameters: – role_id: Role id

    remove_role(self, role_id)

    Lines 169-171

    Purpose:

    Remove role from user.
    

    Parameters: – role_id: Role id

    add_permission(self, permission)

    Lines 173-175

    Purpose:

    Add direct permission to user.
    

    Parameters: – permission: Permission


    Class: Resource

    Lines 179-185

    Purpose:

    Resource definition.
    

    Class: AuthorizationRequest

    Lines 189-194

    Purpose:

    Authorization request.
    

    Class: AuthorizationResult

    Lines 198-203

    Purpose:

    Authorization result.
    

    Class: PermissionAuthorizationAgent

    Lines 206-474

    Purpose:

    Agent that provides permission and authorization management.
    
    This agent manages roles, permissions, and authorization decisions
    using RBAC and ABAC approaches.
    

    Methods:

    __init__(self, temperature)

    Lines 214-245

    Purpose:

    Initialize the authorization agent.
    

    Parameters: – temperature: Sampling temperature (0-1)

    create_permission(self, permission_id, name, description, resource_type, action, conditions)

    Lines 247-266

    Purpose:

    Create a new permission.
    

    Parameters: – permission_id: Permission id – name: Name – description: Description – resource_type: Resource type – action: Action – conditions: Conditions

    Returns: Permission

    create_role(self, role_id, name, description, parent_roles)

    Lines 268-283

    Purpose:

    Create a new role.
    

    Parameters: – role_id: Role id – name: Name – description: Description – parent_roles: Parent roles

    Returns: Role

    create_user(self, user_id, username, roles, attributes)

    Lines 285-300

    Purpose:

    Create a new user.
    

    Parameters: – user_id: User id – username: Username – roles: Roles – attributes: Attributes

    Returns: User

    create_resource(self, resource_id, resource_type, owner_id, attributes)

    Lines 302-317

    Purpose:

    Create a new resource.
    

    Parameters: – resource_id: Resource id – resource_type: Resource type – owner_id: Owner id – attributes: Attributes

    Returns: Resource

    assign_permission_to_role(self, role_id, permission_id)

    Lines 319-331

    Purpose:

    Assign permission to role.
    

    Parameters: – role_id: Role id – permission_id: Permission id

    Returns: bool

    assign_role_to_user(self, user_id, role_id)

    Lines 333-344

    Purpose:

    Assign role to user.
    

    Parameters: – user_id: User id – role_id: Role id

    Returns: bool

    get_user_permissions(self, user_id)

    Lines 346-365

    Purpose:

    Get all permissions for a user (from roles and direct).
    

    Parameters: – user_id: User id

    Returns: <ast.Subscript object at 0x000001A06D70EE50>

    check_authorization(self, request)

    Lines 367-412

    Purpose:

    Check if user is authorized for action on resource.
    
    This performs both static (RBAC) and dynamic (ABAC) checks.
    

    Parameters: – request: Request

    Returns: AuthorizationResult

    _check_conditions(self, conditions, context)

    Lines 414-427

    Purpose:

    Check if conditions are met.
    

    Parameters: – conditions: Conditions – context: Context

    Returns: bool

    _dynamic_authorization(self, user, request)

    Lines 429-461

    Purpose:

    Use LLM for dynamic authorization decision.
    

    Parameters: – user: User – request: Request

    Returns: AuthorizationResult

    get_authorization_summary(self)

    Lines 463-474

    Purpose:

    Get summary of authorization system.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D7EDE90>


    Functions

    demonstrate_permission_authorization()

    Lines 477-737

    Purpose:

    Demonstrate the permission and authorization pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Permission Authorization 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 138
    • Pattern 140

    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 Permission Authorization 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: 139_permission_authorization.py
    Total Lines: 742
    Pattern Category: AI Agent Design Pattern #139


    Generated from actual implementation code
    For educational and reference purposes