Tag: artificial-intelligence

  • Agentic Design Pattern: Foundation Model Orchestration

    
        """
    Pattern 165: Foundation Model Orchestration
    
    Description:
        The Foundation Model Orchestration pattern coordinates multiple foundation models
        (LLMs with different capabilities, sizes, and specializations) to solve complex tasks
        optimally. The orchestrator routes tasks to the most appropriate model based on
        requirements, capabilities, cost, and performance trade-offs.
    
    Components:
        1. Model Registry: Catalog of available models and their capabilities
        2. Task Analyzer: Analyzes task requirements and complexity
        3. Model Selector: Chooses optimal model for each task
        4. Result Aggregator: Combines results from multiple models
        5. Performance Monitor: Tracks model performance and costs
        6. Fallback Manager: Handles model failures
    
    Use Cases:
        - Cost-optimized AI pipelines
        - Quality-performance trade-offs
        - Specialized task routing
        - Multi-model ensemble systems
        - Adaptive model selection
        - Resource optimization
    
    Benefits:
        - Optimal cost-performance balance
        - Better quality through specialization
        - Fault tolerance with fallbacks
        - Flexible model upgrades
        - Resource efficiency
    
    Trade-offs:
        - Orchestration complexity
        - Latency overhead
        - Model compatibility issues
        - Cost tracking complexity
        - Requires multiple API keys
    
    LangChain Implementation:
        Uses LangChain's model abstraction to coordinate multiple models. Implements
        intelligent routing logic based on task characteristics and model capabilities.
    """
    
    import os
    import time
    from typing import List, Dict, Any, Optional, Callable
    from dataclasses import dataclass, field
    from enum import Enum
    from collections import defaultdict
    from dotenv import load_dotenv
    
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    
    load_dotenv()
    
    
    class ModelCapability(Enum):
        """Model capability types"""
        REASONING = "reasoning"
        CODING = "coding"
        CREATIVE = "creative"
        ANALYSIS = "analysis"
        SUMMARIZATION = "summarization"
        TRANSLATION = "translation"
        FAST_RESPONSE = "fast_response"
        LONG_CONTEXT = "long_context"
    
    
    class ModelTier(Enum):
        """Model tier for cost/performance trade-off"""
        NANO = "nano"  # Fastest, cheapest, basic
        SMALL = "small"  # Fast, cheap, good quality
        MEDIUM = "medium"  # Balanced
        LARGE = "large"  # High quality, slower, expensive
        PREMIUM = "premium"  # Best quality, slowest, most expensive
    
    
    @dataclass
    class ModelProfile:
        """Profile of a foundation model"""
        name: str
        model_id: str
        tier: ModelTier
        capabilities: List[ModelCapability]
        max_tokens: int
        cost_per_1k_tokens: float
        avg_latency_ms: float
        quality_score: float = 0.8  # 0-1 scale
        available: bool = True
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class Task:
        """A task to be processed"""
        id: str
        content: str
        required_capabilities: List[ModelCapability]
        priority: str = "medium"  # low, medium, high
        max_cost: Optional[float] = None
        max_latency: Optional[float] = None
        min_quality: float = 0.7
    
    
    @dataclass
    class TaskResult:
        """Result of task execution"""
        task_id: str
        model_used: str
        result: str
        execution_time: float
        estimated_cost: float
        quality_estimate: float
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    class FoundationModelOrchestrator:
        """Orchestrates multiple foundation models for optimal task execution"""
        
        def __init__(self):
            """Initialize the orchestrator with model registry"""
            self.models: Dict[str, ModelProfile] = {}
            self.llm_instances: Dict[str, ChatOpenAI] = {}
            self.performance_stats: Dict[str, Dict[str, Any]] = defaultdict(lambda: {
                "tasks_completed": 0,
                "total_cost": 0.0,
                "total_latency": 0.0,
                "failures": 0
            })
            
            # Register models
            self._register_default_models()
            
            # Task routing prompt
            self.routing_prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a task analysis expert. Analyze the task and determine
                which capabilities are most important for successful completion."""),
                ("user", """Task: {task}
    
    Analyze this task and identify the key capabilities needed (choose from: reasoning,
    coding, creative, analysis, summarization, translation, fast_response, long_context).
    
    Respond with just the capability names, comma-separated.""")
            ])
        
        def _register_default_models(self):
            """Register default model profiles"""
            # Nano tier - fast and cheap
            self.register_model(ModelProfile(
                name="GPT-3.5 Turbo",
                model_id="gpt-3.5-turbo",
                tier=ModelTier.SMALL,
                capabilities=[
                    ModelCapability.FAST_RESPONSE,
                    ModelCapability.SUMMARIZATION,
                    ModelCapability.ANALYSIS
                ],
                max_tokens=4096,
                cost_per_1k_tokens=0.002,
                avg_latency_ms=800,
                quality_score=0.75
            ))
            
            # Medium tier - balanced
            self.register_model(ModelProfile(
                name="GPT-4 Turbo",
                model_id="gpt-4-turbo-preview",
                tier=ModelTier.LARGE,
                capabilities=[
                    ModelCapability.REASONING,
                    ModelCapability.CODING,
                    ModelCapability.CREATIVE,
                    ModelCapability.ANALYSIS,
                    ModelCapability.LONG_CONTEXT
                ],
                max_tokens=128000,
                cost_per_1k_tokens=0.01,
                avg_latency_ms=2000,
                quality_score=0.95
            ))
            
            # Specialized models (simulated with different configs)
            self.register_model(ModelProfile(
                name="Code Specialist",
                model_id="gpt-3.5-turbo",  # Using same model but different profile
                tier=ModelTier.MEDIUM,
                capabilities=[
                    ModelCapability.CODING,
                    ModelCapability.REASONING
                ],
                max_tokens=4096,
                cost_per_1k_tokens=0.003,
                avg_latency_ms=1000,
                quality_score=0.85
            ))
        
        def register_model(self, profile: ModelProfile):
            """Register a model in the orchestrator"""
            self.models[profile.name] = profile
            # Create LLM instance
            try:
                self.llm_instances[profile.name] = ChatOpenAI(
                    model=profile.model_id,
                    temperature=0.7,
                    max_tokens=1000
                )
            except Exception as e:
                print(f"Warning: Could not create LLM instance for {profile.name}: {e}")
                profile.available = False
        
        def execute_task(self, task: Task, use_fallback: bool = True) -> TaskResult:
            """Execute a task using the most appropriate model"""
            # Select best model for task
            selected_model = self._select_model(task)
            
            if not selected_model:
                raise ValueError("No suitable model found for task")
            
            # Execute task
            start_time = time.time()
            
            try:
                result = self._execute_on_model(task, selected_model)
                execution_time = time.time() - start_time
                
                # Calculate cost
                estimated_tokens = len(task.content.split()) * 1.3  # Rough estimate
                estimated_cost = (estimated_tokens / 1000) * selected_model.cost_per_1k_tokens
                
                # Update stats
                self._update_stats(selected_model.name, execution_time, estimated_cost, success=True)
                
                return TaskResult(
                    task_id=task.id,
                    model_used=selected_model.name,
                    result=result,
                    execution_time=execution_time,
                    estimated_cost=estimated_cost,
                    quality_estimate=selected_model.quality_score
                )
                
            except Exception as e:
                self._update_stats(selected_model.name, 0, 0, success=False)
                
                if use_fallback:
                    # Try fallback model
                    fallback_model = self._select_fallback_model(task, selected_model)
                    if fallback_model:
                        print(f"Primary model failed, trying fallback: {fallback_model.name}")
                        task_copy = Task(
                            id=task.id,
                            content=task.content,
                            required_capabilities=task.required_capabilities,
                            priority=task.priority
                        )
                        return self.execute_task(task_copy, use_fallback=False)
                
                raise RuntimeError(f"Task execution failed: {e}")
        
        def execute_batch(self, tasks: List[Task], parallel: bool = False) -> List[TaskResult]:
            """Execute multiple tasks"""
            results = []
            
            for task in tasks:
                try:
                    result = self.execute_task(task)
                    results.append(result)
                except Exception as e:
                    print(f"Task {task.id} failed: {e}")
            
            return results
        
        def execute_with_ensemble(self, task: Task, num_models: int = 3) -> Dict[str, Any]:
            """Execute task on multiple models and aggregate results"""
            # Select top N models
            candidates = self._rank_models_for_task(task)[:num_models]
            
            results = []
            for model_profile in candidates:
                try:
                    result = self._execute_on_model(task, model_profile)
                    results.append({
                        "model": model_profile.name,
                        "result": result,
                        "quality": model_profile.quality_score
                    })
                except Exception as e:
                    print(f"Model {model_profile.name} failed: {e}")
            
            if not results:
                raise RuntimeError("All models failed")
            
            # Aggregate results (simple voting or quality-weighted selection)
            best_result = max(results, key=lambda x: x["quality"])
            
            return {
                "aggregated_result": best_result["result"],
                "all_results": results,
                "models_used": [r["model"] for r in results]
            }
        
        def _select_model(self, task: Task) -> Optional[ModelProfile]:
            """Select the best model for a task"""
            candidates = self._rank_models_for_task(task)
            
            if not candidates:
                return None
            
            # Filter by constraints
            for model in candidates:
                # Check cost constraint
                if task.max_cost:
                    estimated_tokens = len(task.content.split()) * 1.3
                    estimated_cost = (estimated_tokens / 1000) * model.cost_per_1k_tokens
                    if estimated_cost > task.max_cost:
                        continue
                
                # Check latency constraint
                if task.max_latency and model.avg_latency_ms > task.max_latency:
                    continue
                
                # Check quality constraint
                if model.quality_score  List[ModelProfile]:
            """Rank models by suitability for task"""
            scores = []
            
            for model in self.models.values():
                if not model.available:
                    continue
                
                score = 0.0
                
                # Capability match
                matching_caps = sum(
                    1 for cap in task.required_capabilities 
                    if cap in model.capabilities
                )
                if task.required_capabilities:
                    capability_score = matching_caps / len(task.required_capabilities)
                else:
                    capability_score = 0.5
                
                score += capability_score * 40  # 40% weight
                
                # Quality score
                score += model.quality_score * 30  # 30% weight
                
                # Cost efficiency (lower is better)
                cost_score = 1.0 - min(model.cost_per_1k_tokens / 0.05, 1.0)
                score += cost_score * 15  # 15% weight
                
                # Speed (lower latency is better)
                speed_score = 1.0 - min(model.avg_latency_ms / 5000, 1.0)
                score += speed_score * 15  # 15% weight
                
                # Priority adjustment
                if task.priority == "high":
                    score += model.quality_score * 10
                elif task.priority == "low":
                    score += cost_score * 10
                
                scores.append((score, model))
            
            # Sort by score descending
            scores.sort(key=lambda x: x[0], reverse=True)
            
            return [model for _, model in scores]
        
        def _select_fallback_model(self, task: Task, failed_model: ModelProfile) -> Optional[ModelProfile]:
            """Select a fallback model after primary fails"""
            candidates = self._rank_models_for_task(task)
            
            # Return first available model that's not the failed one
            for model in candidates:
                if model.name != failed_model.name and model.available:
                    return model
            
            return None
        
        def _execute_on_model(self, task: Task, model: ModelProfile) -> str:
            """Execute task on specific model"""
            llm = self.llm_instances.get(model.name)
            
            if not llm:
                raise RuntimeError(f"LLM instance not available for {model.name}")
            
            # Create execution prompt
            prompt = ChatPromptTemplate.from_messages([
                ("system", f"You are processing this task using {model.name}."),
                ("user", "{task_content}")
            ])
            
            chain = prompt | llm | StrOutputParser()
            result = chain.invoke({"task_content": task.content})
            
            return result
        
        def _update_stats(self, model_name: str, execution_time: float, 
                         cost: float, success: bool):
            """Update performance statistics"""
            stats = self.performance_stats[model_name]
            
            if success:
                stats["tasks_completed"] += 1
                stats["total_cost"] += cost
                stats["total_latency"] += execution_time
            else:
                stats["failures"] += 1
        
        def get_performance_report(self) -> Dict[str, Any]:
            """Get performance report for all models"""
            report = {}
            
            for model_name, stats in self.performance_stats.items():
                total_tasks = stats["tasks_completed"] + stats["failures"]
                
                if total_tasks > 0:
                    report[model_name] = {
                        "tasks_completed": stats["tasks_completed"],
                        "failures": stats["failures"],
                        "success_rate": stats["tasks_completed"] / total_tasks,
                        "total_cost": stats["total_cost"],
                        "avg_latency": stats["total_latency"] / stats["tasks_completed"] 
                                      if stats["tasks_completed"] > 0 else 0,
                        "cost_per_task": stats["total_cost"] / stats["tasks_completed"]
                                        if stats["tasks_completed"] > 0 else 0
                    }
            
            return report
    
    
    def demonstrate_foundation_model_orchestration():
        """Demonstrate foundation model orchestration"""
        print("=" * 80)
        print("FOUNDATION MODEL ORCHESTRATION PATTERN DEMONSTRATION")
        print("=" * 80)
        
        orchestrator = FoundationModelOrchestrator()
        
        # Example 1: Simple task routing
        print("\n" + "=" * 80)
        print("Example 1: Simple Task Routing")
        print("=" * 80)
        
        tasks = [
            Task(
                id="task1",
                content="Summarize this article in 2 sentences: AI has transformed modern technology...",
                required_capabilities=[ModelCapability.SUMMARIZATION, ModelCapability.FAST_RESPONSE],
                priority="medium"
            ),
            Task(
                id="task2",
                content="Write a Python function to calculate fibonacci numbers recursively",
                required_capabilities=[ModelCapability.CODING],
                priority="high"
            ),
            Task(
                id="task3",
                content="Write a creative short story about a robot learning to paint",
                required_capabilities=[ModelCapability.CREATIVE],
                priority="low"
            )
        ]
        
        print("\nExecuting tasks with automatic model selection:")
        for task in tasks:
            try:
                result = orchestrator.execute_task(task)
                print(f"\nTask: {task.id}")
                print(f"Required capabilities: {[c.value for c in task.required_capabilities]}")
                print(f"Selected model: {result.model_used}")
                print(f"Execution time: {result.execution_time:.3f}s")
                print(f"Estimated cost: ${result.estimated_cost:.6f}")
                print(f"Quality estimate: {result.quality_estimate:.2f}")
                print(f"Result preview: {result.result[:100]}...")
            except Exception as e:
                print(f"\nTask {task.id} failed: {e}")
        
        # Example 2: Cost-constrained execution
        print("\n" + "=" * 80)
        print("Example 2: Cost-Constrained Execution")
        print("=" * 80)
        
        budget_task = Task(
            id="budget_task",
            content="Analyze the sentiment of this review: The product was okay but overpriced.",
            required_capabilities=[ModelCapability.ANALYSIS],
            priority="medium",
            max_cost=0.001  # Very tight budget
        )
        
        try:
            result = orchestrator.execute_task(budget_task)
            print(f"\nTask with max cost ${budget_task.max_cost:.6f}:")
            print(f"Selected model: {result.model_used}")
            print(f"Actual cost: ${result.estimated_cost:.6f}")
            print(f"Result: {result.result[:150]}...")
        except Exception as e:
            print(f"Task failed: {e}")
        
        # Example 3: Quality-focused execution
        print("\n" + "=" * 80)
        print("Example 3: Quality-Focused Execution")
        print("=" * 80)
        
        quality_task = Task(
            id="quality_task",
            content="Solve this logic puzzle: If all A are B, and some B are C, what can we conclude?",
            required_capabilities=[ModelCapability.REASONING],
            priority="high",
            min_quality=0.9  # High quality requirement
        )
        
        try:
            result = orchestrator.execute_task(quality_task)
            print(f"\nTask with min quality {quality_task.min_quality}:")
            print(f"Selected model: {result.model_used}")
            print(f"Quality estimate: {result.quality_estimate}")
            print(f"Result: {result.result[:200]}...")
        except Exception as e:
            print(f"Task failed: {e}")
        
        # Example 4: Ensemble execution
        print("\n" + "=" * 80)
        print("Example 4: Ensemble Execution")
        print("=" * 80)
        
        ensemble_task = Task(
            id="ensemble_task",
            content="What are the three most important factors in climate change?",
            required_capabilities=[ModelCapability.ANALYSIS, ModelCapability.REASONING],
            priority="high"
        )
        
        try:
            ensemble_result = orchestrator.execute_with_ensemble(ensemble_task, num_models=2)
            print(f"\nTask executed with ensemble approach:")
            print(f"Models used: {', '.join(ensemble_result['models_used'])}")
            print(f"\nAggregated result:")
            print(ensemble_result['aggregated_result'][:200] + "...")
            
            print(f"\nAll model results:")
            for i, r in enumerate(ensemble_result['all_results'], 1):
                print(f"\n  Model {i}: {r['model']} (quality: {r['quality']})")
                print(f"  Result: {r['result'][:100]}...")
        except Exception as e:
            print(f"Ensemble execution failed: {e}")
        
        # Example 5: Batch processing
        print("\n" + "=" * 80)
        print("Example 5: Batch Processing")
        print("=" * 80)
        
        batch_tasks = [
            Task(f"batch_{i}", f"Process item {i}: analyze and summarize", 
                 [ModelCapability.ANALYSIS], "medium")
            for i in range(3)
        ]
        
        print(f"\nProcessing batch of {len(batch_tasks)} tasks:")
        batch_results = orchestrator.execute_batch(batch_tasks)
        
        print(f"Completed: {len(batch_results)}/{len(batch_tasks)} tasks")
        for result in batch_results:
            print(f"  {result.task_id}: {result.model_used} in {result.execution_time:.3f}s")
        
        # Example 6: Performance reporting
        print("\n" + "=" * 80)
        print("Example 6: Performance Reporting")
        print("=" * 80)
        
        report = orchestrator.get_performance_report()
        
        print("\nPerformance Report:")
        print("-" * 60)
        for model_name, stats in report.items():
            print(f"\n{model_name}:")
            print(f"  Tasks completed: {stats['tasks_completed']}")
            print(f"  Failures: {stats['failures']}")
            print(f"  Success rate: {stats['success_rate']:.1%}")
            print(f"  Total cost: ${stats['total_cost']:.6f}")
            print(f"  Avg latency: {stats['avg_latency']:.3f}s")
            print(f"  Cost per task: ${stats['cost_per_task']:.6f}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        print("""
    The Foundation Model Orchestration pattern enables:
    ✓ Intelligent routing of tasks to optimal models
    ✓ Cost-performance trade-off optimization
    ✓ Quality-focused model selection
    ✓ Ensemble execution for critical tasks
    ✓ Fallback handling for reliability
    ✓ Performance monitoring and optimization
    ✓ Batch processing with automatic routing
    
    This pattern is valuable for:
    - Production AI systems with cost constraints
    - Multi-model architectures
    - Quality-performance optimization
    - Resource-efficient AI pipelines
    - Fault-tolerant AI systems
    - Adaptive model selection
        """)
    
    
    if __name__ == "__main__":
        demonstrate_foundation_model_orchestration()
    
        

    Agentic Design Pattern: Foundation Model Orchestration

    Pattern 165: Foundation Model Orchestration – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 165: Foundation Model Orchestration

    Description: The Foundation Model Orchestration pattern coordinates multiple foundation models (LLMs with different capabilities, sizes, and specializations) to solve complex tasks optimally. The orchestrator routes tasks to the most appropriate model based on requirements, capabilities, cost, and performance trade-offs.

    Components: 1. Model Registry: Catalog of available models and their capabilities 2. Task Analyzer: Analyzes task requirements and complexity 3. Model Selector: Chooses optimal model for each task 4. Result Aggregator: Combines results from multiple models 5. Performance Monitor: Tracks model performance and costs 6. Fallback Manager: Handles model failures

    Use Cases: – Cost-optimized AI pipelines – Quality-performance trade-offs – Specialized task routing – Multi-model ensemble systems – Adaptive model selection – Resource optimization

    Benefits: – Optimal cost-performance balance – Better quality through specialization – Fault tolerance with fallbacks – Flexible model upgrades – Resource efficiency

    Trade-offs: – Orchestration complexity – Latency overhead – Model compatibility issues – Cost tracking complexity – Requires multiple API keys

    LangChain Implementation: Uses LangChain’s model abstraction to coordinate multiple models. Implements intelligent routing logic based on task characteristics and model capabilities.


    File Statistics

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

    Module Documentation

    """
    Pattern 165: Foundation Model Orchestration
    
    Description:
        The Foundation Model Orchestration pattern coordinates multiple foundation models
        (LLMs with different capabilities, sizes, and specializations) to solve complex tasks
        optimally. The orchestrator routes tasks to the most appropriate model based on
        requirements, capabilities, cost, and performance trade-offs.
    
    Components:
        1. Model Registry: Catalog of available models and their capabilities
        2. Task Analyzer: Analyzes task requirements and complexity
        3. Model Selector: Chooses optimal model for each task
        4. Result Aggregator: Combines results from multiple models
        5. Performance Monitor: Tracks model performance and costs
        6. Fallback Manager: Handles model failures
    
    Use Cases:
        - Cost-optimized AI pipelines
        - Quality-performance trade-offs
        - Specialized task routing
        - Multi-model ensemble systems
        - Adaptive model selection
        - Resource optimization
    
    Benefits:
        - Optimal cost-performance balance
        - Better quality through specialization
        - Fault tolerance with fallbacks
        - Flexible model upgrades
        - Resource efficiency
    
    Trade-offs:
        - Orchestration complexity
        - Latency overhead
        - Model compatibility issues
        - Cost tracking complexity
        - Requires multiple API keys
    
    LangChain Implementation:
        Uses LangChain's model abstraction to coordinate multiple models. Implements
        intelligent routing logic based on task characteristics and model capabilities.
    """
    

    Purpose: This module implements the Foundation Model Orchestration 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 enum.Enum
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import time
    import typing.Any
    import typing.Callable
    import typing.Dict
    import typing.List
    import typing.Optional
    

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

    LangChain Imports

    from langchain_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: ModelCapability

    Lines 60-69

    Inherits from: Enum

    Purpose:

    Model capability types
    

    Class: ModelTier

    Lines 72-78

    Inherits from: Enum

    Purpose:

    Model tier for cost/performance trade-off
    

    Class: ModelProfile

    Lines 82-93

    Purpose:

    Profile of a foundation model
    

    Class: Task

    Lines 97-105

    Purpose:

    A task to be processed
    

    Class: TaskResult

    Lines 109-117

    Purpose:

    Result of task execution
    

    Class: FoundationModelOrchestrator

    Lines 120-444

    Purpose:

    Orchestrates multiple foundation models for optimal task execution
    

    Methods:

    __init__(self)

    Lines 123-147

    Purpose:

    Initialize the orchestrator with model registry
    

    Parameters:

    _register_default_models(self)

    Lines 149-198

    Purpose:

    Register default model profiles
    

    Parameters:

    register_model(self, profile)

    Lines 200-212

    Purpose:

    Register a model in the orchestrator
    

    Parameters: – profile: Profile

    execute_task(self, task, use_fallback)

    Lines 214-261

    Purpose:

    Execute a task using the most appropriate model
    

    Parameters: – task: Task – use_fallback: Use fallback

    Returns: TaskResult

    execute_batch(self, tasks, parallel)

    Lines 263-274

    Purpose:

    Execute multiple tasks
    

    Parameters: – tasks: Tasks – parallel: Parallel

    Returns: <ast.Subscript object at 0x000001A06D6210D0>

    execute_with_ensemble(self, task, num_models)

    Lines 276-303

    Purpose:

    Execute task on multiple models and aggregate results
    

    Parameters: – task: Task – num_models: Language model instance

    Returns: <ast.Subscript object at 0x000001A06E0ABE50>

    _select_model(self, task)

    Lines 305-336

    Purpose:

    Select the best model for a task
    

    Parameters: – task: Task

    Returns: <ast.Subscript object at 0x000001A06D6744D0>

    _rank_models_for_task(self, task)

    Lines 338-382

    Purpose:

    Rank models by suitability for task
    

    Parameters: – task: Task

    Returns: <ast.Subscript object at 0x000001A06D7C90D0>

    _select_fallback_model(self, task, failed_model)

    Lines 384-393

    Purpose:

    Select a fallback model after primary fails
    

    Parameters: – task: Task – failed_model: Language model instance

    Returns: <ast.Subscript object at 0x000001A06D7C9090>

    _execute_on_model(self, task, model)

    Lines 395-411

    Purpose:

    Execute task on specific model
    

    Parameters: – task: Task – model: Language model instance

    Returns: str

    _update_stats(self, model_name, execution_time, cost, success)

    Lines 413-423

    Purpose:

    Update performance statistics
    

    Parameters: – model_name: Language model instance – execution_time: Execution time – cost: Cost – success: Success

    get_performance_report(self)

    Lines 425-444

    Purpose:

    Get performance report for all models
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D889090>


    Functions

    demonstrate_foundation_model_orchestration()

    Lines 447-622

    Purpose:

    Demonstrate foundation model orchestration
    

    Key Concepts Summary

    Pattern Overview

    The Foundation Model Orchestration 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 164
    • Pattern 166

    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 Foundation Model Orchestration 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: 165_foundation_model_orchestration.py
    Total Lines: 627
    Pattern Category: AI Agent Design Pattern #165


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Temporal Reasoning

    
        """
    Pattern 164: Temporal Reasoning
    
    Description:
        The Temporal Reasoning pattern enables agents to understand and reason about time-based
        relationships, sequences, durations, and temporal logic. This pattern helps agents solve
        problems involving timelines, schedules, causality, and temporal constraints.
    
    Components:
        1. Timeline Manager: Maintains ordered sequences of events
        2. Temporal Relation Analyzer: Identifies before/after/during/overlapping relationships
        3. Duration Calculator: Computes time intervals and durations
        4. Temporal Constraint Checker: Validates temporal constraints
        5. Sequence Analyzer: Analyzes event sequences and patterns
        6. Temporal Query Engine: Answers time-based questions
    
    Use Cases:
        - Schedule planning and optimization
        - Event sequence analysis
        - Causal reasoning
        - Historical timeline construction
        - Project timeline management
        - Deadline tracking
        - Temporal pattern detection
    
    Benefits:
        - Enables time-aware reasoning
        - Supports scheduling and planning
        - Detects temporal patterns
        - Validates temporal constraints
        - Better understanding of causality
    
    Trade-offs:
        - Complex time representation
        - Timezone handling challenges
        - Requires precise temporal data
        - May need calendar logic
        - Limited by data accuracy
    
    LangChain Implementation:
        Uses LLM for temporal relationship interpretation with structured datetime
        management and interval calculations. Combines symbolic temporal logic with
        natural language understanding for temporal queries.
    """
    
    import os
    from typing import List, Dict, Any, Optional, Tuple
    from dataclasses import dataclass, field
    from datetime import datetime, timedelta
    from enum import Enum
    from dotenv import load_dotenv
    
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    
    load_dotenv()
    
    
    class TemporalRelation(Enum):
        """Types of temporal relationships (Allen's interval algebra)"""
        BEFORE = "before"
        AFTER = "after"
        DURING = "during"
        CONTAINS = "contains"
        OVERLAPS = "overlaps"
        OVERLAPPED_BY = "overlapped_by"
        MEETS = "meets"
        MET_BY = "met_by"
        STARTS = "starts"
        STARTED_BY = "started_by"
        FINISHES = "finishes"
        FINISHED_BY = "finished_by"
        EQUALS = "equals"
    
    
    class TimeUnit(Enum):
        """Units of time measurement"""
        SECOND = "second"
        MINUTE = "minute"
        HOUR = "hour"
        DAY = "day"
        WEEK = "week"
        MONTH = "month"
        YEAR = "year"
    
    
    @dataclass
    class TemporalEvent:
        """Represents an event with temporal properties"""
        name: str
        start_time: datetime
        end_time: Optional[datetime] = None
        description: str = ""
        metadata: Dict[str, Any] = field(default_factory=dict)
        
        @property
        def duration(self) -> Optional[timedelta]:
            """Calculate duration of the event"""
            if self.end_time:
                return self.end_time - self.start_time
            return None
        
        @property
        def is_point_event(self) -> bool:
            """Check if this is a point event (no duration)"""
            return self.end_time is None or self.start_time == self.end_time
    
    
    @dataclass
    class Timeline:
        """Represents a timeline with multiple events"""
        name: str
        events: List[TemporalEvent] = field(default_factory=list)
        
        def add_event(self, event: TemporalEvent):
            """Add an event to the timeline"""
            self.events.append(event)
            self.events.sort(key=lambda e: e.start_time)
        
        def get_events_in_range(self, start: datetime, end: datetime) -> List[TemporalEvent]:
            """Get all events within a time range"""
            return [e for e in self.events 
                    if e.start_time = start)]
    
    
    @dataclass
    class TemporalQuery:
        """A temporal reasoning query"""
        question: str
        events: List[TemporalEvent]
        context: str = ""
    
    
    @dataclass
    class TemporalAnalysis:
        """Result of temporal reasoning"""
        answer: str
        relationships: List[str]
        timeline: str
        sequences: List[List[str]]
        reasoning: str
    
    
    class TemporalReasoningAgent:
        """Agent that performs temporal reasoning tasks"""
        
        def __init__(self, model_name: str = "gpt-3.5-turbo"):
            """Initialize the temporal reasoning agent"""
            self.llm = ChatOpenAI(model=model_name, temperature=0)
            
            # Prompts for temporal reasoning
            self.temporal_prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a temporal reasoning expert. Analyze events, timelines,
                and temporal relationships. Consider durations, sequences, and causal relationships.
                Be precise about time-based logic."""),
                ("user", """Events and their times:
    {event_descriptions}
    
    Question: {question}
    
    Provide a clear answer based on temporal analysis.""")
            ])
            
            self.sequence_prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert at analyzing event sequences and patterns.
                Identify patterns, detect anomalies, and reason about causality."""),
                ("user", """Event sequence:
    {sequence}
    
    Analysis task: {task}
    
    Describe patterns, relationships, and insights.""")
            ])
            
            self.temporal_chain = self.temporal_prompt | self.llm | StrOutputParser()
            self.sequence_chain = self.sequence_prompt | self.llm | StrOutputParser()
        
        def analyze_temporal_query(self, query: TemporalQuery) -> TemporalAnalysis:
            """Analyze a temporal query and return comprehensive analysis"""
            # Identify temporal relationships
            relationships = self._identify_temporal_relationships(query.events)
            
            # Create timeline visualization
            timeline_viz = self._create_timeline_visualization(query.events)
            
            # Identify sequences
            sequences = self._identify_sequences(query.events)
            
            # Create event descriptions
            event_descriptions = self._create_event_descriptions(query.events)
            
            # Get LLM reasoning
            reasoning = self.temporal_chain.invoke({
                "event_descriptions": event_descriptions,
                "question": query.question
            })
            
            return TemporalAnalysis(
                answer=reasoning,
                relationships=relationships,
                timeline=timeline_viz,
                sequences=sequences,
                reasoning=reasoning
            )
        
        def check_temporal_relation(self, event1: TemporalEvent, 
                                   event2: TemporalEvent,
                                   relation: TemporalRelation) -> bool:
            """Check if a specific temporal relationship holds between two events"""
            e1_start = event1.start_time
            e1_end = event1.end_time or event1.start_time
            e2_start = event2.start_time
            e2_end = event2.end_time or event2.start_time
            
            if relation == TemporalRelation.BEFORE:
                return e1_end  e2_end
            elif relation == TemporalRelation.DURING:
                return e1_start > e2_start and e1_end < e2_end
            elif relation == TemporalRelation.CONTAINS:
                return e1_start  e2_end
            elif relation == TemporalRelation.OVERLAPS:
                return e1_start  e2_start and e1_end < e2_end
            elif relation == TemporalRelation.OVERLAPPED_BY:
                return e2_start  e1_start and e2_end < e1_end
            elif relation == TemporalRelation.MEETS:
                return e1_end == e2_start
            elif relation == TemporalRelation.MET_BY:
                return e2_end == e1_start
            elif relation == TemporalRelation.STARTS:
                return e1_start == e2_start and e1_end  e2_end
            elif relation == TemporalRelation.FINISHES:
                return e1_start > e2_start and e1_end == e2_end
            elif relation == TemporalRelation.FINISHED_BY:
                return e1_start  List[Tuple[str, str]]:
            """Find all pairs of overlapping events"""
            overlaps = []
            
            for i, event1 in enumerate(events):
                for event2 in events[i+1:]:
                    if self._events_overlap(event1, event2):
                        overlaps.append((event1.name, event2.name))
            
            return overlaps
        
        def find_gaps(self, events: List[TemporalEvent]) -> List[Tuple[datetime, datetime]]:
            """Find time gaps between consecutive events"""
            if not events:
                return []
            
            sorted_events = sorted(events, key=lambda e: e.start_time)
            gaps = []
            
            for i in range(len(sorted_events) - 1):
                current_end = sorted_events[i].end_time or sorted_events[i].start_time
                next_start = sorted_events[i + 1].start_time
                
                if current_end  timedelta:
            """Calculate total duration of events"""
            if not events:
                return timedelta(0)
            
            if not merge_overlaps:
                # Simple sum of all durations
                total = timedelta(0)
                for event in events:
                    if event.duration:
                        total += event.duration
                return total
            else:
                # Merge overlapping intervals first
                intervals = []
                for event in events:
                    start = event.start_time
                    end = event.end_time or event.start_time
                    intervals.append((start, end))
                
                # Sort and merge
                intervals.sort()
                merged = []
                current_start, current_end = intervals[0]
                
                for start, end in intervals[1:]:
                    if start  Dict[str, Any]:
            """Detect patterns in temporal data"""
            if not events:
                return {"patterns": []}
            
            sorted_events = sorted(events, key=lambda e: e.start_time)
            
            patterns = {
                "total_events": len(events),
                "time_span": {
                    "start": sorted_events[0].start_time,
                    "end": sorted_events[-1].end_time or sorted_events[-1].start_time
                },
                "regular_intervals": self._detect_regular_intervals(sorted_events),
                "clusters": self._detect_time_clusters(sorted_events),
                "gaps": len(self.find_gaps(events)),
                "overlaps": len(self.find_overlapping_events(events))
            }
            
            return patterns
        
        def _events_overlap(self, event1: TemporalEvent, event2: TemporalEvent) -> bool:
            """Check if two events overlap in time"""
            e1_start = event1.start_time
            e1_end = event1.end_time or event1.start_time
            e2_start = event2.start_time
            e2_end = event2.end_time or event2.start_time
            
            return not (e1_end < e2_start or e2_end  List[str]:
            """Identify temporal relationships between events"""
            relationships = []
            
            for i, event1 in enumerate(events):
                for event2 in events[i+1:]:
                    # Check key relationships
                    if self.check_temporal_relation(event1, event2, TemporalRelation.BEFORE):
                        relationships.append(f"{event1.name} before {event2.name}")
                    elif self.check_temporal_relation(event1, event2, TemporalRelation.DURING):
                        relationships.append(f"{event1.name} during {event2.name}")
                    elif self.check_temporal_relation(event1, event2, TemporalRelation.OVERLAPS):
                        relationships.append(f"{event1.name} overlaps {event2.name}")
                    elif self.check_temporal_relation(event1, event2, TemporalRelation.MEETS):
                        relationships.append(f"{event1.name} meets {event2.name}")
            
            return relationships
        
        def _identify_sequences(self, events: List[TemporalEvent]) -> List[List[str]]:
            """Identify sequences of consecutive events"""
            if not events:
                return []
            
            sorted_events = sorted(events, key=lambda e: e.start_time)
            sequences = []
            current_sequence = [sorted_events[0].name]
            
            for i in range(1, len(sorted_events)):
                prev_end = sorted_events[i-1].end_time or sorted_events[i-1].start_time
                curr_start = sorted_events[i].start_time
                
                # Check if events are consecutive (within 1 hour)
                if (curr_start - prev_end)  1:
                        sequences.append(current_sequence)
                    current_sequence = [sorted_events[i].name]
            
            if len(current_sequence) > 1:
                sequences.append(current_sequence)
            
            return sequences
        
        def _detect_regular_intervals(self, sorted_events: List[TemporalEvent]) -> Optional[timedelta]:
            """Detect if events occur at regular intervals"""
            if len(sorted_events) < 3:
                return None
            
            intervals = []
            for i in range(1, len(sorted_events)):
                interval = sorted_events[i].start_time - sorted_events[i-1].start_time
                intervals.append(interval)
            
            # Check if intervals are similar (within 10% variance)
            avg_interval = sum(intervals, timedelta(0)) / len(intervals)
            variance = sum((i - avg_interval).total_seconds()**2 for i in intervals) / len(intervals)
            
            if variance  int:
            """Detect clusters of events happening close together"""
            if len(sorted_events)  timedelta(hours=2):  # New cluster if gap > 2 hours
                    clusters += 1
            
            return clusters
        
        def _create_event_descriptions(self, events: List[TemporalEvent]) -> str:
            """Create textual descriptions of events"""
            descriptions = []
            for event in sorted(events, key=lambda e: e.start_time):
                desc = f"{event.name}: {event.start_time.strftime('%Y-%m-%d %H:%M')}"
                if event.end_time:
                    desc += f" to {event.end_time.strftime('%Y-%m-%d %H:%M')}"
                    desc += f" (duration: {event.duration})"
                if event.description:
                    desc += f" - {event.description}"
                descriptions.append(desc)
            return "\n".join(descriptions)
        
        def _create_timeline_visualization(self, events: List[TemporalEvent]) -> str:
            """Create ASCII timeline visualization"""
            if not events:
                return "No events to visualize"
            
            sorted_events = sorted(events, key=lambda e: e.start_time)
            
            lines = ["Timeline:", "=" * 60]
            
            for event in sorted_events:
                time_str = event.start_time.strftime('%Y-%m-%d %H:%M')
                if event.end_time:
                    duration_str = f" [{event.duration}]"
                else:
                    duration_str = " [instant]"
                
                lines.append(f"  {time_str} | {event.name}{duration_str}")
            
            return "\n".join(lines)
    
    
    def demonstrate_temporal_reasoning():
        """Demonstrate temporal reasoning capabilities"""
        print("=" * 80)
        print("TEMPORAL REASONING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = TemporalReasoningAgent()
        
        # Example 1: Project timeline analysis
        print("\n" + "=" * 80)
        print("Example 1: Project Timeline Analysis")
        print("=" * 80)
        
        base_time = datetime(2024, 1, 1, 9, 0)
        
        project_events = [
            TemporalEvent("Planning", base_time, base_time + timedelta(days=7), "Initial planning phase"),
            TemporalEvent("Design", base_time + timedelta(days=7), base_time + timedelta(days=14), "Design phase"),
            TemporalEvent("Development", base_time + timedelta(days=14), base_time + timedelta(days=45), "Development phase"),
            TemporalEvent("Testing", base_time + timedelta(days=40), base_time + timedelta(days=52), "Testing phase overlaps with dev"),
            TemporalEvent("Deployment", base_time + timedelta(days=52), base_time + timedelta(days=53), "Final deployment")
        ]
        
        query = TemporalQuery(
            question="What is the timeline of the project and are there any overlapping phases?",
            events=project_events,
            context="Software project timeline"
        )
        
        analysis = agent.analyze_temporal_query(query)
        
        print(f"\nQuery: {query.question}")
        print(f"\n{analysis.timeline}")
        print(f"\nTemporal Relationships:")
        for rel in analysis.relationships[:5]:
            print(f"  - {rel}")
        print(f"\nAnalysis:\n{analysis.answer}")
        
        # Example 2: Overlapping events detection
        print("\n" + "=" * 80)
        print("Example 2: Overlapping Events Detection")
        print("=" * 80)
        
        meeting_events = [
            TemporalEvent("Team Meeting", datetime(2024, 1, 15, 10, 0), datetime(2024, 1, 15, 11, 0)),
            TemporalEvent("Client Call", datetime(2024, 1, 15, 10, 30), datetime(2024, 1, 15, 11, 30)),
            TemporalEvent("Lunch Break", datetime(2024, 1, 15, 12, 0), datetime(2024, 1, 15, 13, 0)),
            TemporalEvent("Code Review", datetime(2024, 1, 15, 14, 0), datetime(2024, 1, 15, 15, 0))
        ]
        
        overlaps = agent.find_overlapping_events(meeting_events)
        
        print("\nSchedule for January 15, 2024:")
        for event in sorted(meeting_events, key=lambda e: e.start_time):
            print(f"  {event.start_time.strftime('%H:%M')}-{event.end_time.strftime('%H:%M')}: {event.name}")
        
        if overlaps:
            print(f"\nConflicts detected:")
            for event1, event2 in overlaps:
                print(f"  ⚠ {event1} overlaps with {event2}")
        else:
            print("\n✓ No scheduling conflicts detected")
        
        # Example 3: Temporal relationship checking
        print("\n" + "=" * 80)
        print("Example 3: Temporal Relationship Checking")
        print("=" * 80)
        
        event_a = TemporalEvent("Event A", datetime(2024, 1, 1, 10, 0), datetime(2024, 1, 1, 12, 0))
        event_b = TemporalEvent("Event B", datetime(2024, 1, 1, 11, 0), datetime(2024, 1, 1, 11, 30))
        event_c = TemporalEvent("Event C", datetime(2024, 1, 1, 12, 0), datetime(2024, 1, 1, 13, 0))
        
        relations_to_check = [
            (event_a, event_b, TemporalRelation.CONTAINS),
            (event_b, event_a, TemporalRelation.DURING),
            (event_a, event_c, TemporalRelation.MEETS),
            (event_c, event_a, TemporalRelation.MET_BY),
            (event_a, event_b, TemporalRelation.OVERLAPS)
        ]
        
        print("\nChecking temporal relationships:")
        for evt1, evt2, relation in relations_to_check:
            holds = agent.check_temporal_relation(evt1, evt2, relation)
            symbol = "✓" if holds else "✗"
            print(f"  {symbol} {evt1.name} {relation.value} {evt2.name}: {holds}")
        
        # Example 4: Duration calculations
        print("\n" + "=" * 80)
        print("Example 4: Duration Calculations")
        print("=" * 80)
        
        work_events = [
            TemporalEvent("Task 1", datetime(2024, 1, 1, 9, 0), datetime(2024, 1, 1, 11, 0)),
            TemporalEvent("Task 2", datetime(2024, 1, 1, 10, 30), datetime(2024, 1, 1, 12, 0)),
            TemporalEvent("Task 3", datetime(2024, 1, 1, 13, 0), datetime(2024, 1, 1, 15, 0)),
        ]
        
        total_simple = agent.calculate_total_duration(work_events, merge_overlaps=False)
        total_merged = agent.calculate_total_duration(work_events, merge_overlaps=True)
        
        print("\nWork events:")
        for event in work_events:
            print(f"  {event.name}: {event.duration}")
        
        print(f"\nTotal duration (simple sum): {total_simple}")
        print(f"Total duration (merged overlaps): {total_merged}")
        print(f"Overlap time: {total_simple - total_merged}")
        
        # Example 5: Gap detection
        print("\n" + "=" * 80)
        print("Example 5: Gap Detection")
        print("=" * 80)
        
        sparse_events = [
            TemporalEvent("Morning Session", datetime(2024, 1, 1, 9, 0), datetime(2024, 1, 1, 10, 0)),
            TemporalEvent("Afternoon Session", datetime(2024, 1, 1, 14, 0), datetime(2024, 1, 1, 15, 0)),
            TemporalEvent("Evening Session", datetime(2024, 1, 1, 19, 0), datetime(2024, 1, 1, 20, 0))
        ]
        
        gaps = agent.find_gaps(sparse_events)
        
        print("\nEvents:")
        for event in sparse_events:
            print(f"  {event.start_time.strftime('%H:%M')}-{event.end_time.strftime('%H:%M')}: {event.name}")
        
        print(f"\nGaps detected: {len(gaps)}")
        for start, end in gaps:
            duration = end - start
            print(f"  Gap from {start.strftime('%H:%M')} to {end.strftime('%H:%M')} ({duration})")
        
        # Example 6: Pattern detection
        print("\n" + "=" * 80)
        print("Example 6: Pattern Detection")
        print("=" * 80)
        
        regular_events = [
            TemporalEvent(f"Daily Standup {i}", datetime(2024, 1, i, 9, 0), datetime(2024, 1, i, 9, 15))
            for i in range(1, 6)
        ]
        
        patterns = agent.detect_temporal_patterns(regular_events)
        
        print("\nEvents:")
        for event in regular_events:
            print(f"  {event.start_time.strftime('%Y-%m-%d %H:%M')}: {event.name}")
        
        print(f"\nPattern Analysis:")
        print(f"  Total events: {patterns['total_events']}")
        print(f"  Time span: {patterns['time_span']['start'].strftime('%Y-%m-%d')} to {patterns['time_span']['end'].strftime('%Y-%m-%d')}")
        if patterns['regular_intervals']:
            print(f"  Regular interval detected: {patterns['regular_intervals']}")
        print(f"  Number of clusters: {patterns['clusters']}")
        print(f"  Gaps: {patterns['gaps']}")
        print(f"  Overlaps: {patterns['overlaps']}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        print("""
    The Temporal Reasoning pattern enables agents to:
    ✓ Understand temporal relationships (before, after, during, overlaps)
    ✓ Detect scheduling conflicts and overlaps
    ✓ Calculate durations and time gaps
    ✓ Identify temporal patterns and sequences
    ✓ Validate temporal constraints
    ✓ Reason about causality and event sequences
    ✓ Analyze timelines and schedules
    
    This pattern is valuable for:
    - Project and task scheduling
    - Calendar and meeting management
    - Event sequence analysis
    - Timeline construction and validation
    - Causal reasoning
    - Temporal pattern detection
    - Deadline and milestone tracking
        """)
    
    
    if __name__ == "__main__":
        demonstrate_temporal_reasoning()
    
        

    Agentic Design Pattern: Temporal Reasoning

    Pattern 164: Temporal Reasoning – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 164: Temporal Reasoning

    Description: The Temporal Reasoning pattern enables agents to understand and reason about time-based relationships, sequences, durations, and temporal logic. This pattern helps agents solve problems involving timelines, schedules, causality, and temporal constraints.

    Components: 1. Timeline Manager: Maintains ordered sequences of events 2. Temporal Relation Analyzer: Identifies before/after/during/overlapping relationships 3. Duration Calculator: Computes time intervals and durations 4. Temporal Constraint Checker: Validates temporal constraints 5. Sequence Analyzer: Analyzes event sequences and patterns 6. Temporal Query Engine: Answers time-based questions

    Use Cases: – Schedule planning and optimization – Event sequence analysis – Causal reasoning – Historical timeline construction – Project timeline management – Deadline tracking – Temporal pattern detection

    Benefits: – Enables time-aware reasoning – Supports scheduling and planning – Detects temporal patterns – Validates temporal constraints – Better understanding of causality

    Trade-offs: – Complex time representation – Timezone handling challenges – Requires precise temporal data – May need calendar logic – Limited by data accuracy

    LangChain Implementation: Uses LLM for temporal relationship interpretation with structured datetime management and interval calculations. Combines symbolic temporal logic with natural language understanding for temporal queries.


    File Statistics

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

    Module Documentation

    """
    Pattern 164: Temporal Reasoning
    
    Description:
        The Temporal Reasoning pattern enables agents to understand and reason about time-based
        relationships, sequences, durations, and temporal logic. This pattern helps agents solve
        problems involving timelines, schedules, causality, and temporal constraints.
    
    Components:
        1. Timeline Manager: Maintains ordered sequences of events
        2. Temporal Relation Analyzer: Identifies before/after/during/overlapping relationships
        3. Duration Calculator: Computes time intervals and durations
        4. Temporal Constraint Checker: Validates temporal constraints
        5. Sequence Analyzer: Analyzes event sequences and patterns
        6. Temporal Query Engine: Answers time-based questions
    
    Use Cases:
        - Schedule planning and optimization
        - Event sequence analysis
        - Causal reasoning
        - Historical timeline construction
        - Project timeline management
        - Deadline tracking
        - Temporal pattern detection
    
    Benefits:
        - Enables time-aware reasoning
        - Supports scheduling and planning
        - Detects temporal patterns
        - Validates temporal constraints
        - Better understanding of causality
    
    Trade-offs:
        - Complex time representation
        - Timezone handling challenges
        - Requires precise temporal data
        - May need calendar logic
        - Limited by data accuracy
    
    LangChain Implementation:
        Uses LLM for temporal relationship interpretation with structured datetime
        management and interval calculations. Combines symbolic temporal logic with
        natural language understanding for temporal queries.
    """
    

    Purpose: This module implements the Temporal Reasoning 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.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 – 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.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: TemporalRelation

    Lines 60-74

    Inherits from: Enum

    Purpose:

    Types of temporal relationships (Allen's interval algebra)
    

    Class: TimeUnit

    Lines 77-85

    Inherits from: Enum

    Purpose:

    Units of time measurement
    

    Class: TemporalEvent

    Lines 89-107

    Purpose:

    Represents an event with temporal properties
    

    Methods:

    duration(self)

    Lines 98-102

    Decorators: property

    Purpose:

    Calculate duration of the event
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D7B3590>

    is_point_event(self)

    Lines 105-107

    Decorators: property

    Purpose:

    Check if this is a point event (no duration)
    

    Parameters:

    Returns: bool


    Class: Timeline

    Lines 111-124

    Purpose:

    Represents a timeline with multiple events
    

    Methods:

    add_event(self, event)

    Lines 116-119

    Purpose:

    Add an event to the timeline
    

    Parameters: – event: Event

    get_events_in_range(self, start, end)

    Lines 121-124

    Purpose:

    Get all events within a time range
    

    Parameters: – start: Start – end: End

    Returns: <ast.Subscript object at 0x000001A06D826710>


    Class: TemporalQuery

    Lines 128-132

    Purpose:

    A temporal reasoning query
    

    Class: TemporalAnalysis

    Lines 136-142

    Purpose:

    Result of temporal reasoning
    

    Class: TemporalReasoningAgent

    Lines 145-452

    Purpose:

    Agent that performs temporal reasoning tasks
    

    Methods:

    __init__(self, model_name)

    Lines 148-177

    Purpose:

    Initialize the temporal reasoning agent
    

    Parameters: – model_name: Language model instance

    analyze_temporal_query(self, query)

    Lines 179-205

    Purpose:

    Analyze a temporal query and return comprehensive analysis
    

    Parameters: – query: Input query or question

    Returns: TemporalAnalysis

    check_temporal_relation(self, event1, event2, relation)

    Lines 207-243

    Purpose:

    Check if a specific temporal relationship holds between two events
    

    Parameters: – event1: Event1 – event2: Event2 – relation: Relation

    Returns: bool

    find_overlapping_events(self, events)

    Lines 245-254

    Purpose:

    Find all pairs of overlapping events
    

    Parameters: – events: Events

    Returns: <ast.Subscript object at 0x000001A06D759190>

    find_gaps(self, events)

    Lines 256-271

    Purpose:

    Find time gaps between consecutive events
    

    Parameters: – events: Events

    Returns: <ast.Subscript object at 0x000001A06D7A0E10>

    calculate_total_duration(self, events, merge_overlaps)

    Lines 273-313

    Purpose:

    Calculate total duration of events
    

    Parameters: – events: Events – merge_overlaps: Merge overlaps

    Returns: timedelta

    detect_temporal_patterns(self, events)

    Lines 315-334

    Purpose:

    Detect patterns in temporal data
    

    Parameters: – events: Events

    Returns: <ast.Subscript object at 0x000001A06D624BD0>

    _events_overlap(self, event1, event2)

    Lines 336-343

    Purpose:

    Check if two events overlap in time
    

    Parameters: – event1: Event1 – event2: Event2

    Returns: bool

    _identify_temporal_relationships(self, events)

    Lines 345-361

    Purpose:

    Identify temporal relationships between events
    

    Parameters: – events: Events

    Returns: <ast.Subscript object at 0x000001A06D867290>

    _identify_sequences(self, events)

    Lines 363-387

    Purpose:

    Identify sequences of consecutive events
    

    Parameters: – events: Events

    Returns: <ast.Subscript object at 0x000001A06D77F010>

    _detect_regular_intervals(self, sorted_events)

    Lines 389-406

    Purpose:

    Detect if events occur at regular intervals
    

    Parameters: – sorted_events: Sorted events

    Returns: <ast.Subscript object at 0x000001A06D72AD90>

    _detect_time_clusters(self, sorted_events)

    Lines 408-419

    Purpose:

    Detect clusters of events happening close together
    

    Parameters: – sorted_events: Sorted events

    Returns: int

    _create_event_descriptions(self, events)

    Lines 421-432

    Purpose:

    Create textual descriptions of events
    

    Parameters: – events: Events

    Returns: str

    _create_timeline_visualization(self, events)

    Lines 434-452

    Purpose:

    Create ASCII timeline visualization
    

    Parameters: – events: Events

    Returns: str


    Functions

    demonstrate_temporal_reasoning()

    Lines 455-632

    Purpose:

    Demonstrate temporal reasoning capabilities
    

    Key Concepts Summary

    Pattern Overview

    The Temporal Reasoning 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 163
    • Pattern 165

    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 Temporal Reasoning 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: 164_temporal_reasoning.py
    Total Lines: 637
    Pattern Category: AI Agent Design Pattern #164


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Spatial Reasoning

    
        """
    Pattern 163: Spatial Reasoning
    
    Description:
        The Spatial Reasoning pattern enables agents to understand and reason about spatial
        relationships, positions, orientations, and geometric properties. This pattern helps
        agents solve problems involving physical space, layouts, navigation, and spatial
        configurations.
    
    Components:
        1. Coordinate System: Tracks positions and locations
        2. Relationship Analyzer: Identifies spatial relationships (above, below, left, right, etc.)
        3. Distance Calculator: Computes distances and proximity
        4. Orientation Tracker: Manages directions and rotations
        5. Spatial Query Engine: Answers spatial questions
        6. Visualization Helper: Generates spatial descriptions
    
    Use Cases:
        - Navigation and pathfinding
        - Layout design and optimization
        - Object placement and arrangement
        - Map understanding and interpretation
        - Geometry problem solving
        - Spatial puzzle solving
        - Floor plan analysis
    
    Benefits:
        - Enables spatial understanding
        - Supports geometric reasoning
        - Facilitates navigation tasks
        - Improves layout optimization
        - Better scene understanding
    
    Trade-offs:
        - Complex coordinate management
        - Requires clear spatial representation
        - May need visualization capabilities
        - Challenging for abstract spaces
        - Limited by 2D/3D representation
    
    LangChain Implementation:
        Uses LLM for spatial relationship interpretation with structured coordinate
        systems and distance calculations. Combines symbolic representation with
        natural language understanding for spatial queries.
    """
    
    import os
    import math
    from typing import List, Dict, Any, Tuple, Optional
    from dataclasses import dataclass
    from enum import Enum
    from dotenv import load_dotenv
    
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    
    load_dotenv()
    
    
    class Direction(Enum):
        """Cardinal and ordinal directions"""
        NORTH = "north"
        SOUTH = "south"
        EAST = "east"
        WEST = "west"
        NORTHEAST = "northeast"
        NORTHWEST = "northwest"
        SOUTHEAST = "southeast"
        SOUTHWEST = "southwest"
        UP = "up"
        DOWN = "down"
    
    
    class SpatialRelation(Enum):
        """Types of spatial relationships"""
        ABOVE = "above"
        BELOW = "below"
        LEFT_OF = "left of"
        RIGHT_OF = "right of"
        IN_FRONT_OF = "in front of"
        BEHIND = "behind"
        INSIDE = "inside"
        OUTSIDE = "outside"
        ADJACENT = "adjacent"
        NEAR = "near"
        FAR = "far"
        BETWEEN = "between"
    
    
    @dataclass
    class SpatialObject:
        """Represents an object in space"""
        name: str
        x: float
        y: float
        z: float = 0.0
        width: float = 1.0
        height: float = 1.0
        depth: float = 1.0
        orientation: float = 0.0  # degrees
    
    
    @dataclass
    class SpatialQuery:
        """A spatial reasoning query"""
        question: str
        objects: List[SpatialObject]
        context: str = ""
    
    
    @dataclass
    class SpatialAnalysis:
        """Result of spatial reasoning"""
        answer: str
        relationships: List[str]
        distances: Dict[str, float]
        reasoning: str
        visualization: str
    
    
    class SpatialReasoningAgent:
        """Agent that performs spatial reasoning tasks"""
        
        def __init__(self, model_name: str = "gpt-3.5-turbo"):
            """Initialize the spatial reasoning agent"""
            self.llm = ChatOpenAI(model=model_name, temperature=0)
            
            # Prompts for different spatial reasoning tasks
            self.relation_prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a spatial reasoning expert. Analyze the positions 
                of objects and describe their spatial relationships clearly and accurately.
                Consider distances, directions, and relative positions."""),
                ("user", """Objects and their positions:
    {object_descriptions}
    
    Question: {question}
    
    Provide a clear answer based on the spatial relationships.""")
            ])
            
            self.path_prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a navigation expert. Given object positions, 
                determine optimal paths and directions."""),
                ("user", """Objects and their positions:
    {object_descriptions}
    
    Navigation task: {task}
    
    Describe the path or navigation instructions.""")
            ])
            
            self.relation_chain = self.relation_prompt | self.llm | StrOutputParser()
            self.path_chain = self.path_prompt | self.llm | StrOutputParser()
        
        def analyze_spatial_query(self, query: SpatialQuery) -> SpatialAnalysis:
            """Analyze a spatial query and return comprehensive analysis"""
            # Calculate all pairwise distances
            distances = self._calculate_distances(query.objects)
            
            # Identify spatial relationships
            relationships = self._identify_relationships(query.objects)
            
            # Create object descriptions
            object_descriptions = self._create_object_descriptions(query.objects)
            
            # Get LLM reasoning
            reasoning = self.relation_chain.invoke({
                "object_descriptions": object_descriptions,
                "question": query.question
            })
            
            # Create visualization
            visualization = self._create_visualization(query.objects)
            
            return SpatialAnalysis(
                answer=reasoning,
                relationships=relationships,
                distances=distances,
                reasoning=reasoning,
                visualization=visualization
            )
        
        def find_path(self, start: SpatialObject, end: SpatialObject, 
                      obstacles: List[SpatialObject]) -> Dict[str, Any]:
            """Find path from start to end avoiding obstacles"""
            # Calculate direct distance
            direct_distance = self._distance(start, end)
            
            # Calculate direction
            direction = self._get_direction(start, end)
            
            # Check for obstacles in path
            obstacles_in_path = self._check_obstacles(start, end, obstacles)
            
            # Get navigation instructions
            object_descriptions = self._create_object_descriptions(
                [start, end] + obstacles
            )
            
            instructions = self.path_chain.invoke({
                "object_descriptions": object_descriptions,
                "task": f"Navigate from {start.name} to {end.name}"
            })
            
            return {
                "direct_distance": direct_distance,
                "direction": direction,
                "obstacles_in_path": obstacles_in_path,
                "instructions": instructions,
                "path_clear": len(obstacles_in_path) == 0
            }
        
        def find_nearest(self, reference: SpatialObject, 
                         candidates: List[SpatialObject]) -> Tuple[SpatialObject, float]:
            """Find the nearest object to reference from candidates"""
            nearest = None
            min_distance = float('inf')
            
            for candidate in candidates:
                distance = self._distance(reference, candidate)
                if distance  bool:
            """Check if a specific spatial relationship holds between two objects"""
            if relation == SpatialRelation.ABOVE:
                return obj1.z > obj2.z
            elif relation == SpatialRelation.BELOW:
                return obj1.z < obj2.z
            elif relation == SpatialRelation.LEFT_OF:
                return obj1.x  obj2.x
            elif relation == SpatialRelation.IN_FRONT_OF:
                return obj1.y > obj2.y
            elif relation == SpatialRelation.BEHIND:
                return obj1.y < obj2.y
            elif relation == SpatialRelation.NEAR:
                distance = self._distance(obj1, obj2)
                return distance  10.0  # Threshold for "far"
            elif relation == SpatialRelation.ADJACENT:
                distance = self._distance(obj1, obj2)
                return distance  List[SpatialObject]:
            """Get all objects within a spherical region"""
            result = []
            cx, cy, cz = center
            
            for obj in objects:
                distance = math.sqrt(
                    (obj.x - cx)**2 + 
                    (obj.y - cy)**2 + 
                    (obj.z - cz)**2
                )
                if distance  float:
            """Calculate Euclidean distance between two objects"""
            return math.sqrt(
                (obj1.x - obj2.x)**2 + 
                (obj1.y - obj2.y)**2 + 
                (obj1.z - obj2.z)**2
            )
        
        def _calculate_distances(self, objects: List[SpatialObject]) -> Dict[str, float]:
            """Calculate all pairwise distances"""
            distances = {}
            for i, obj1 in enumerate(objects):
                for obj2 in objects[i+1:]:
                    key = f"{obj1.name} to {obj2.name}"
                    distances[key] = self._distance(obj1, obj2)
            return distances
        
        def _identify_relationships(self, objects: List[SpatialObject]) -> List[str]:
            """Identify spatial relationships between objects"""
            relationships = []
            
            for i, obj1 in enumerate(objects):
                for obj2 in objects[i+1:]:
                    # Check various relationships
                    if self.check_relationship(obj1, obj2, SpatialRelation.ABOVE):
                        relationships.append(f"{obj1.name} is above {obj2.name}")
                    if self.check_relationship(obj1, obj2, SpatialRelation.BELOW):
                        relationships.append(f"{obj1.name} is below {obj2.name}")
                    if self.check_relationship(obj1, obj2, SpatialRelation.LEFT_OF):
                        relationships.append(f"{obj1.name} is left of {obj2.name}")
                    if self.check_relationship(obj1, obj2, SpatialRelation.RIGHT_OF):
                        relationships.append(f"{obj1.name} is right of {obj2.name}")
                    if self.check_relationship(obj1, obj2, SpatialRelation.NEAR):
                        relationships.append(f"{obj1.name} is near {obj2.name}")
                    if self.check_relationship(obj1, obj2, SpatialRelation.ADJACENT):
                        relationships.append(f"{obj1.name} is adjacent to {obj2.name}")
            
            return relationships
        
        def _get_direction(self, from_obj: SpatialObject, 
                           to_obj: SpatialObject) -> Direction:
            """Determine cardinal direction from one object to another"""
            dx = to_obj.x - from_obj.x
            dy = to_obj.y - from_obj.y
            
            angle = math.atan2(dy, dx) * 180 / math.pi
            
            # Normalize to 0-360
            if angle < 0:
                angle += 360
            
            # Determine direction
            if 22.5 <= angle < 67.5:
                return Direction.NORTHEAST
            elif 67.5 <= angle < 112.5:
                return Direction.NORTH
            elif 112.5 <= angle < 157.5:
                return Direction.NORTHWEST
            elif 157.5 <= angle < 202.5:
                return Direction.WEST
            elif 202.5 <= angle < 247.5:
                return Direction.SOUTHWEST
            elif 247.5 <= angle < 292.5:
                return Direction.SOUTH
            elif 292.5 <= angle  List[str]:
            """Check which obstacles are in the path between start and end"""
            obstacles_in_path = []
            
            for obstacle in obstacles:
                # Simple line intersection check
                if self._is_near_line(start, end, obstacle):
                    obstacles_in_path.append(obstacle.name)
            
            return obstacles_in_path
        
        def _is_near_line(self, start: SpatialObject, end: SpatialObject,
                          point: SpatialObject, threshold: float = 1.0) -> bool:
            """Check if a point is near the line between start and end"""
            # Calculate perpendicular distance from point to line
            x1, y1 = start.x, start.y
            x2, y2 = end.x, end.y
            x0, y0 = point.x, point.y
            
            numerator = abs((y2-y1)*x0 - (x2-x1)*y0 + x2*y1 - y2*x1)
            denominator = math.sqrt((y2-y1)**2 + (x2-x1)**2)
            
            if denominator == 0:
                return False
            
            distance = numerator / denominator
            
            # Also check if point is between start and end
            between = (min(x1, x2) <= x0 <= max(x1, x2) and 
                      min(y1, y2) <= y0 <= max(y1, y2))
            
            return distance  str:
            """Create textual descriptions of object positions"""
            descriptions = []
            for obj in objects:
                desc = f"{obj.name}: position ({obj.x:.1f}, {obj.y:.1f}, {obj.z:.1f})"
                if obj.width != 1.0 or obj.height != 1.0:
                    desc += f", size ({obj.width:.1f}x{obj.height:.1f}x{obj.depth:.1f})"
                descriptions.append(desc)
            return "\n".join(descriptions)
        
        def _create_visualization(self, objects: List[SpatialObject]) -> str:
            """Create ASCII visualization of object positions"""
            if not objects:
                return "No objects to visualize"
            
            # Find bounds
            min_x = min(obj.x for obj in objects) - 2
            max_x = max(obj.x for obj in objects) + 2
            min_y = min(obj.y for obj in objects) - 2
            max_y = max(obj.y for obj in objects) + 2
            
            # Create grid
            width = int(max_x - min_x) + 1
            height = int(max_y - min_y) + 1
            
            grid = [[' ' for _ in range(width)] for _ in range(height)]
            
            # Place objects
            for i, obj in enumerate(objects):
                x = int(obj.x - min_x)
                y = int(obj.y - min_y)
                if 0 <= x < width and 0 <= y < height:
                    grid[height - 1 - y][x] = chr(65 + i)  # A, B, C, ...
            
            # Create visualization string
            vis = "\n" + "\n".join("".join(row) for row in grid)
            vis += "\n\nLegend: " + ", ".join(
                f"{chr(65+i)}={obj.name}" for i, obj in enumerate(objects)
            )
            
            return vis
    
    
    def demonstrate_spatial_reasoning():
        """Demonstrate spatial reasoning capabilities"""
        print("=" * 80)
        print("SPATIAL REASONING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = SpatialReasoningAgent()
        
        # Example 1: Room layout analysis
        print("\n" + "=" * 80)
        print("Example 1: Room Layout Analysis")
        print("=" * 80)
        
        room_objects = [
            SpatialObject("Table", 5, 5, 0, 2, 1, 3),
            SpatialObject("Chair1", 4, 5, 0, 1, 1, 1),
            SpatialObject("Chair2", 6, 5, 0, 1, 1, 1),
            SpatialObject("Lamp", 5, 5, 2, 0.5, 0.5, 1),
            SpatialObject("Door", 0, 5, 0, 1, 2, 0.1),
            SpatialObject("Window", 10, 5, 1, 2, 1, 0.1)
        ]
        
        query = SpatialQuery(
            question="What is the spatial arrangement of furniture in the room?",
            objects=room_objects,
            context="Living room layout"
        )
        
        analysis = agent.analyze_spatial_query(query)
        
        print(f"\nQuery: {query.question}")
        print(f"\nAnswer: {analysis.answer}")
        print(f"\nKey Relationships:")
        for rel in analysis.relationships[:5]:
            print(f"  - {rel}")
        print(f"\nVisualization:{analysis.visualization}")
        
        # Example 2: Navigation pathfinding
        print("\n" + "=" * 80)
        print("Example 2: Navigation Pathfinding")
        print("=" * 80)
        
        start = SpatialObject("Start", 0, 0, 0)
        end = SpatialObject("Goal", 10, 10, 0)
        obstacles = [
            SpatialObject("Obstacle1", 5, 5, 0, 2, 2, 1),
            SpatialObject("Obstacle2", 7, 3, 0, 1, 1, 1)
        ]
        
        path_result = agent.find_path(start, end, obstacles)
        
        print(f"\nNavigation from {start.name} to {end.name}")
        print(f"Direct distance: {path_result['direct_distance']:.2f} units")
        print(f"Direction: {path_result['direction'].value}")
        print(f"Path clear: {path_result['path_clear']}")
        if path_result['obstacles_in_path']:
            print(f"Obstacles in path: {', '.join(path_result['obstacles_in_path'])}")
        print(f"\nInstructions:\n{path_result['instructions']}")
        
        # Example 3: Nearest neighbor search
        print("\n" + "=" * 80)
        print("Example 3: Nearest Neighbor Search")
        print("=" * 80)
        
        reference = SpatialObject("Home", 5, 5, 0)
        stores = [
            SpatialObject("Grocery Store", 3, 4, 0),
            SpatialObject("Pharmacy", 8, 6, 0),
            SpatialObject("Coffee Shop", 5, 7, 0),
            SpatialObject("Bookstore", 10, 10, 0)
        ]
        
        nearest, distance = agent.find_nearest(reference, stores)
        
        print(f"\nFinding nearest store to {reference.name}")
        print(f"Nearest: {nearest.name}")
        print(f"Distance: {distance:.2f} units")
        
        print("\nAll distances:")
        for store in stores:
            dist = agent._distance(reference, store)
            direction = agent._get_direction(reference, store)
            print(f"  {store.name}: {dist:.2f} units {direction.value}")
        
        # Example 4: Spatial relationship checking
        print("\n" + "=" * 80)
        print("Example 4: Spatial Relationship Checking")
        print("=" * 80)
        
        ceiling_light = SpatialObject("Ceiling Light", 5, 5, 8)
        floor_lamp = SpatialObject("Floor Lamp", 5, 5, 0)
        couch = SpatialObject("Couch", 3, 5, 0)
        
        relations_to_check = [
            (ceiling_light, floor_lamp, SpatialRelation.ABOVE),
            (floor_lamp, ceiling_light, SpatialRelation.BELOW),
            (couch, floor_lamp, SpatialRelation.NEAR),
            (couch, floor_lamp, SpatialRelation.ADJACENT)
        ]
        
        print("\nChecking spatial relationships:")
        for obj1, obj2, relation in relations_to_check:
            holds = agent.check_relationship(obj1, obj2, relation)
            print(f"  {obj1.name} is {relation.value} {obj2.name}: {holds}")
        
        # Example 5: Regional search
        print("\n" + "=" * 80)
        print("Example 5: Regional Search")
        print("=" * 80)
        
        all_objects = [
            SpatialObject("Object1", 2, 2, 0),
            SpatialObject("Object2", 5, 5, 0),
            SpatialObject("Object3", 8, 8, 0),
            SpatialObject("Object4", 3, 7, 0),
            SpatialObject("Object5", 10, 2, 0)
        ]
        
        search_center = (5, 5, 0)
        search_radius = 4
        
        objects_in_region = agent.get_objects_in_region(
            all_objects, search_center, search_radius
        )
        
        print(f"\nSearching for objects within {search_radius} units of {search_center}")
        print(f"Found {len(objects_in_region)} objects:")
        for obj in objects_in_region:
            dist = math.sqrt(
                (obj.x - search_center[0])**2 + 
                (obj.y - search_center[1])**2 + 
                (obj.z - search_center[2])**2
            )
            print(f"  {obj.name} at ({obj.x}, {obj.y}, {obj.z}) - {dist:.2f} units away")
        
        # Example 6: Complex spatial query
        print("\n" + "=" * 80)
        print("Example 6: Complex Spatial Query")
        print("=" * 80)
        
        city_objects = [
            SpatialObject("City Hall", 5, 5, 0, 3, 3, 10),
            SpatialObject("Park", 8, 8, 0, 5, 5, 0),
            SpatialObject("School", 2, 8, 0, 2, 2, 5),
            SpatialObject("Hospital", 9, 2, 0, 3, 3, 8),
            SpatialObject("Library", 1, 2, 0, 2, 2, 4)
        ]
        
        complex_query = SpatialQuery(
            question="Which public facilities are located in the northern part of the city and which are in the south?",
            objects=city_objects,
            context="City planning analysis"
        )
        
        complex_analysis = agent.analyze_spatial_query(complex_query)
        
        print(f"\nQuery: {complex_query.question}")
        print(f"\nAnalysis:\n{complex_analysis.answer}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        print("""
    The Spatial Reasoning pattern enables agents to:
    ✓ Understand spatial relationships and positions
    ✓ Calculate distances and directions
    ✓ Find optimal paths and navigation routes
    ✓ Identify nearest neighbors
    ✓ Search within spatial regions
    ✓ Analyze complex spatial arrangements
    ✓ Combine geometric calculations with semantic understanding
    
    This pattern is valuable for:
    - Navigation and routing systems
    - Layout and design optimization
    - Geographic information systems
    - Robotics and autonomous vehicles
    - Game AI and virtual environments
    - Spatial data analysis
    - Map-based applications
        """)
    
    
    if __name__ == "__main__":
        demonstrate_spatial_reasoning()
    
        

    Agentic Design Pattern: Spatial Reasoning

    Pattern 163: Spatial Reasoning – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 163: Spatial Reasoning

    Description: The Spatial Reasoning pattern enables agents to understand and reason about spatial relationships, positions, orientations, and geometric properties. This pattern helps agents solve problems involving physical space, layouts, navigation, and spatial configurations.

    Components: 1. Coordinate System: Tracks positions and locations 2. Relationship Analyzer: Identifies spatial relationships (above, below, left, right, etc.) 3. Distance Calculator: Computes distances and proximity 4. Orientation Tracker: Manages directions and rotations 5. Spatial Query Engine: Answers spatial questions 6. Visualization Helper: Generates spatial descriptions

    Use Cases: – Navigation and pathfinding – Layout design and optimization – Object placement and arrangement – Map understanding and interpretation – Geometry problem solving – Spatial puzzle solving – Floor plan analysis

    Benefits: – Enables spatial understanding – Supports geometric reasoning – Facilitates navigation tasks – Improves layout optimization – Better scene understanding

    Trade-offs: – Complex coordinate management – Requires clear spatial representation – May need visualization capabilities – Challenging for abstract spaces – Limited by 2D/3D representation

    LangChain Implementation: Uses LLM for spatial relationship interpretation with structured coordinate systems and distance calculations. Combines symbolic representation with natural language understanding for spatial queries.


    File Statistics

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

    Module Documentation

    """
    Pattern 163: Spatial Reasoning
    
    Description:
        The Spatial Reasoning pattern enables agents to understand and reason about spatial
        relationships, positions, orientations, and geometric properties. This pattern helps
        agents solve problems involving physical space, layouts, navigation, and spatial
        configurations.
    
    Components:
        1. Coordinate System: Tracks positions and locations
        2. Relationship Analyzer: Identifies spatial relationships (above, below, left, right, etc.)
        3. Distance Calculator: Computes distances and proximity
        4. Orientation Tracker: Manages directions and rotations
        5. Spatial Query Engine: Answers spatial questions
        6. Visualization Helper: Generates spatial descriptions
    
    Use Cases:
        - Navigation and pathfinding
        - Layout design and optimization
        - Object placement and arrangement
        - Map understanding and interpretation
        - Geometry problem solving
        - Spatial puzzle solving
        - Floor plan analysis
    
    Benefits:
        - Enables spatial understanding
        - Supports geometric reasoning
        - Facilitates navigation tasks
        - Improves layout optimization
        - Better scene understanding
    
    Trade-offs:
        - Complex coordinate management
        - Requires clear spatial representation
        - May need visualization capabilities
        - Challenging for abstract spaces
        - Limited by 2D/3D representation
    
    LangChain Implementation:
        Uses LLM for spatial relationship interpretation with structured coordinate
        systems and distance calculations. Combines symbolic representation with
        natural language understanding for spatial queries.
    """
    

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


    Import Statements

    Standard Library Imports

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

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – enum.Enum: Enumeration support – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – 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
    from math
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: Direction

    Lines 61-72

    Inherits from: Enum

    Purpose:

    Cardinal and ordinal directions
    

    Class: SpatialRelation

    Lines 75-88

    Inherits from: Enum

    Purpose:

    Types of spatial relationships
    

    Class: SpatialObject

    Lines 92-101

    Purpose:

    Represents an object in space
    

    Class: SpatialQuery

    Lines 105-109

    Purpose:

    A spatial reasoning query
    

    Class: SpatialAnalysis

    Lines 113-119

    Purpose:

    Result of spatial reasoning
    

    Class: SpatialReasoningAgent

    Lines 122-416

    Purpose:

    Agent that performs spatial reasoning tasks
    

    Methods:

    __init__(self, model_name)

    Lines 125-154

    Purpose:

    Initialize the spatial reasoning agent
    

    Parameters: – model_name: Language model instance

    analyze_spatial_query(self, query)

    Lines 156-182

    Purpose:

    Analyze a spatial query and return comprehensive analysis
    

    Parameters: – query: Input query or question

    Returns: SpatialAnalysis

    find_path(self, start, end, obstacles)

    Lines 184-212

    Purpose:

    Find path from start to end avoiding obstacles
    

    Parameters: – start: Start – end: End – obstacles: Obstacles

    Returns: <ast.Subscript object at 0x000001A06D17A650>

    find_nearest(self, reference, candidates)

    Lines 214-226

    Purpose:

    Find the nearest object to reference from candidates
    

    Parameters: – reference: Reference – candidates: Candidates

    Returns: <ast.Subscript object at 0x000001A06D60A550>

    check_relationship(self, obj1, obj2, relation)

    Lines 228-253

    Purpose:

    Check if a specific spatial relationship holds between two objects
    

    Parameters: – obj1: Obj1 – obj2: Obj2 – relation: Relation

    Returns: bool

    get_objects_in_region(self, objects, center, radius)

    Lines 255-271

    Purpose:

    Get all objects within a spherical region
    

    Parameters: – objects: Objects – center: Center – radius: Radius

    Returns: <ast.Subscript object at 0x000001A06D74A710>

    _distance(self, obj1, obj2)

    Lines 273-279

    Purpose:

    Calculate Euclidean distance between two objects
    

    Parameters: – obj1: Obj1 – obj2: Obj2

    Returns: float

    _calculate_distances(self, objects)

    Lines 281-288

    Purpose:

    Calculate all pairwise distances
    

    Parameters: – objects: Objects

    Returns: <ast.Subscript object at 0x000001A06D669750>

    _identify_relationships(self, objects)

    Lines 290-310

    Purpose:

    Identify spatial relationships between objects
    

    Parameters: – objects: Objects

    Returns: <ast.Subscript object at 0x000001A06D63A550>

    _get_direction(self, from_obj, to_obj)

    Lines 312-340

    Purpose:

    Determine cardinal direction from one object to another
    

    Parameters: – from_obj: From obj – to_obj: To obj

    Returns: Direction

    _check_obstacles(self, start, end, obstacles)

    Lines 342-352

    Purpose:

    Check which obstacles are in the path between start and end
    

    Parameters: – start: Start – end: End – obstacles: Obstacles

    Returns: <ast.Subscript object at 0x000001A06D7EFDD0>

    _is_near_line(self, start, end, point, threshold)

    Lines 354-374

    Purpose:

    Check if a point is near the line between start and end
    

    Parameters: – start: Start – end: End – point: Point – threshold: Threshold

    Returns: bool

    _create_object_descriptions(self, objects)

    Lines 376-384

    Purpose:

    Create textual descriptions of object positions
    

    Parameters: – objects: Objects

    Returns: str

    _create_visualization(self, objects)

    Lines 386-416

    Purpose:

    Create ASCII visualization of object positions
    

    Parameters: – objects: Objects

    Returns: str


    Functions

    demonstrate_spatial_reasoning()

    Lines 419-600

    Purpose:

    Demonstrate spatial reasoning capabilities
    

    Key Concepts Summary

    Pattern Overview

    The Spatial Reasoning 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 162
    • Pattern 164

    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 Spatial Reasoning 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: 163_spatial_reasoning.py
    Total Lines: 605
    Pattern Category: AI Agent Design Pattern #163


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Counterfactual Reasoning

    
        """
    Pattern 162: Counterfactual Reasoning
    
    Description:
        Counterfactual reasoning explores "what if" scenarios by considering alternative
        outcomes if certain conditions were different. It helps understand causality,
        evaluate decisions, and plan for contingencies.
    
    Components:
        - Scenario modification
        - Alternative outcome prediction
        - Causal analysis
        - Comparison with actual outcomes
        - Impact assessment
    
    Use Cases:
        - Decision analysis
        - Risk assessment
        - Causal inference
        - Policy evaluation
        - Learning from mistakes
    
    Benefits:
        - Understand causality
        - Evaluate alternatives
        - Learn from hypotheticals
        - Improve decision-making
    
    Trade-offs:
        - Speculative nature
        - Multiple possibilities
        - Requires causal model
        - May be computationally intensive
    
    LangChain Implementation:
        Uses ChatOpenAI for scenario generation, outcome prediction,
        and comparative analysis
    """
    
    import os
    from typing import List, Dict, Any, Optional
    from dataclasses import dataclass, field
    from dotenv import load_dotenv
    
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    
    load_dotenv()
    
    
    @dataclass
    class Scenario:
        """Actual or counterfactual scenario"""
        description: str
        conditions: Dict[str, Any]
        outcome: Optional[str] = None
        is_actual: bool = True
    
    
    @dataclass
    class CounterfactualAnalysis:
        """Analysis of counterfactual scenario"""
        actual_scenario: Scenario
        counterfactual_scenario: Scenario
        predicted_outcome: str
        differences: List[str]
        causal_factors: List[str]
        likelihood: float  # 0-1
        reasoning: str
    
    
    class CounterfactualReasoningAgent:
        """Agent that performs counterfactual reasoning"""
        
        def __init__(self, model_name: str = "gpt-3.5-turbo"):
            self.llm = ChatOpenAI(model=model_name, temperature=0.7)
        
        def analyze(self, actual_scenario: Scenario,
                    modifications: Dict[str, Any]) -> CounterfactualAnalysis:
            """Analyze counterfactual scenario"""
            
            # Create counterfactual scenario
            cf_conditions = actual_scenario.conditions.copy()
            cf_conditions.update(modifications)
            
            cf_scenario = Scenario(
                description=self._generate_cf_description(actual_scenario, modifications),
                conditions=cf_conditions,
                is_actual=False
            )
            
            # Predict counterfactual outcome
            predicted_outcome = self._predict_outcome(cf_scenario, actual_scenario)
            
            # Identify differences
            differences = self._identify_differences(actual_scenario, cf_scenario)
            
            # Analyze causal factors
            causal_factors = self._analyze_causality(actual_scenario, cf_scenario, differences)
            
            # Assess likelihood
            likelihood = self._assess_likelihood(cf_scenario, actual_scenario)
            
            # Generate reasoning
            reasoning = self._generate_reasoning(actual_scenario, cf_scenario, predicted_outcome)
            
            return CounterfactualAnalysis(
                actual_scenario=actual_scenario,
                counterfactual_scenario=cf_scenario,
                predicted_outcome=predicted_outcome,
                differences=differences,
                causal_factors=causal_factors,
                likelihood=likelihood,
                reasoning=reasoning
            )
        
        def _generate_cf_description(self, actual: Scenario, mods: Dict[str, Any]) -> str:
            """Generate counterfactual description"""
            prompt = ChatPromptTemplate.from_messages([
                ("system", "Generate a counterfactual scenario description."),
                ("user", """Actual scenario: {actual}
    
    Modifications: {modifications}
    
    Describe the counterfactual scenario in one sentence:""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            return chain.invoke({
                "actual": actual.description,
                "modifications": str(mods)
            })
        
        def _predict_outcome(self, cf_scenario: Scenario, actual: Scenario) -> str:
            """Predict counterfactual outcome"""
            prompt = ChatPromptTemplate.from_messages([
                ("system", "Predict what would have happened in this counterfactual scenario."),
                ("user", """Actual scenario: {actual}
    Actual outcome: {actual_outcome}
    
    Counterfactual scenario: {counterfactual}
    Conditions: {conditions}
    
    What would be the likely outcome?""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            return chain.invoke({
                "actual": actual.description,
                "actual_outcome": actual.outcome or "Unknown",
                "counterfactual": cf_scenario.description,
                "conditions": str(cf_scenario.conditions)
            })
        
        def _identify_differences(self, actual: Scenario, cf: Scenario) -> List[str]:
            """Identify key differences"""
            differences = []
            for key in set(actual.conditions.keys()) | set(cf.conditions.keys()):
                actual_val = actual.conditions.get(key)
                cf_val = cf.conditions.get(key)
                if actual_val != cf_val:
                    differences.append(f"{key}: {actual_val} → {cf_val}")
            return differences
        
        def _analyze_causality(self, actual: Scenario, cf: Scenario, 
                              differences: List[str]) -> List[str]:
            """Analyze causal factors"""
            prompt = ChatPromptTemplate.from_messages([
                ("system", "Identify the causal factors that would lead to different outcomes."),
                ("user", """Differences: {differences}
    
    What are the key causal factors? List 2-3 main factors.""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            response = chain.invoke({"differences": "\n".join(differences)})
            
            factors = []
            for line in response.split('\n'):
                line = line.strip()
                if line and (line[0].isdigit() or line.startswith('-')):
                    clean = line.lstrip('0123456789.-) ')
                    if clean:
                        factors.append(clean)
            return factors
        
        def _assess_likelihood(self, cf: Scenario, actual: Scenario) -> float:
            """Assess likelihood of counterfactual"""
            # Simplified: count condition changes
            total_conditions = len(actual.conditions)
            changed = sum(1 for k in actual.conditions 
                         if k in cf.conditions and actual.conditions[k] != cf.conditions[k])
            
            if total_conditions == 0:
                return 0.5
            
            # More changes = less likely
            return max(0.1, 1.0 - (changed / total_conditions))
        
        def _generate_reasoning(self, actual: Scenario, cf: Scenario, outcome: str) -> str:
            """Generate reasoning explanation"""
            prompt = ChatPromptTemplate.from_messages([
                ("system", "Explain the counterfactual reasoning."),
                ("user", """Actual: {actual} → {actual_outcome}
    Counterfactual: {cf} → {cf_outcome}
    
    Explain why the outcome would be different:""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            return chain.invoke({
                "actual": actual.description,
                "actual_outcome": actual.outcome or "Unknown",
                "cf": cf.description,
                "cf_outcome": outcome
            })
    
    
    def demonstrate_counterfactual_reasoning():
        """Demonstrate counterfactual reasoning pattern"""
        print("=" * 80)
        print("COUNTERFACTUAL REASONING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = CounterfactualReasoningAgent()
        
        # Example 1: Business decision
        print("\n" + "="*80)
        print("EXAMPLE 1: Business Decision Analysis")
        print("="*80)
        actual = Scenario(
            description="Company launched product in Q4 with minimal marketing",
            conditions={"launch_quarter": "Q4", "marketing_budget": "low", "features": "basic"},
            outcome="Slow initial sales, 1000 units sold",
            is_actual=True
        )
        
        modifications = {"marketing_budget": "high", "launch_quarter": "Q1"}
        
        analysis = agent.analyze(actual, modifications)
        
        print(f"Actual: {actual.description}")
        print(f"Outcome: {actual.outcome}")
        print(f"\nCounterfactual: {analysis.counterfactual_scenario.description}")
        print(f"Predicted outcome: {analysis.predicted_outcome}")
        print(f"\nKey differences:")
        for diff in analysis.differences:
            print(f"  - {diff}")
        print(f"\nLikelihood: {analysis.likelihood:.1%}")
        
        # Example 2: Historical what-if
        print("\n" + "="*80)
        print("EXAMPLE 2: Historical Counterfactual")
        print("="*80)
        actual = Scenario(
            description="Student studied for 2 hours before the exam",
            conditions={"study_time": 2, "sleep": "adequate", "practice_tests": 1},
            outcome="Scored 75% on the exam",
            is_actual=True
        )
        
        modifications = {"study_time": 6, "practice_tests": 3}
        
        analysis = agent.analyze(actual, modifications)
        
        print(f"What actually happened: {actual.description}")
        print(f"Result: {actual.outcome}")
        print(f"\nWhat if: {analysis.counterfactual_scenario.description}")
        print(f"Predicted result: {analysis.predicted_outcome[:200]}...")
        print(f"\nCausal factors:")
        for factor in analysis.causal_factors:
            print(f"  - {factor}")
        
        # Example 3: Risk assessment
        print("\n" + "="*80)
        print("EXAMPLE 3: Risk Assessment")
        print("="*80)
        actual = Scenario(
            description="Project completed on time with original team",
            conditions={"team_size": 5, "timeline": "6 months", "requirements_stable": True},
            outcome="Successful delivery, high quality",
            is_actual=True
        )
        
        modifications = {"team_size": 3, "requirements_stable": False}
        
        analysis = agent.analyze(actual, modifications)
        
        print(f"Actual scenario: {actual.description}")
        print(f"Outcome: {actual.outcome}")
        print(f"\nRisk scenario: {analysis.counterfactual_scenario.description}")
        print(f"Predicted outcome: {analysis.predicted_outcome[:200]}...")
        print(f"\nReasoning: {analysis.reasoning[:300]}...")
        
        # Example 4: Decision evaluation
        print("\n" + "="*80)
        print("EXAMPLE 4: Evaluating Past Decisions")
        print("="*80)
        actual = Scenario(
            description="Chose to work from home during pandemic",
            conditions={"location": "home", "commute_time": 0, "office_interaction": "minimal"},
            outcome="Maintained productivity, saved commute time, missed social interaction",
            is_actual=True
        )
        
        modifications = {"location": "office", "commute_time": 90, "office_interaction": "high"}
        
        analysis = agent.analyze(actual, modifications)
        
        print(f"Decision made: {actual.description}")
        print(f"Result: {actual.outcome}")
        print(f"\nAlternative: {analysis.counterfactual_scenario.description}")
        print(f"Predicted result: {analysis.predicted_outcome[:200]}...")
        print(f"\nLikelihood of alternative: {analysis.likelihood:.1%}")
        
        # Example 5: Multiple counterfactuals
        print("\n" + "="*80)
        print("EXAMPLE 5: Comparing Multiple Counterfactuals")
        print("="*80)
        actual = Scenario(
            description="Invested in stocks during market downturn",
            conditions={"investment_type": "stocks", "timing": "downturn", "amount": 10000},
            outcome="Portfolio recovered after 18 months, 15% gain",
            is_actual=True
        )
        
        alternatives = [
            {"investment_type": "bonds"},
            {"timing": "peak"},
            {"amount": 5000}
        ]
        
        print(f"Actual: {actual.description}")
        print(f"Outcome: {actual.outcome}\n")
        
        for i, mods in enumerate(alternatives, 1):
            analysis = agent.analyze(actual, mods)
            print(f"Alternative {i}: {list(mods.values())[0]}")
            print(f"  Predicted: {analysis.predicted_outcome[:150]}...")
            print()
        
        # Summary
        print("\n" + "="*80)
        print("SUMMARY: Counterfactual Reasoning Best Practices")
        print("="*80)
        print("""
    1. SCENARIO DEFINITION:
       - Clearly define actual scenario
       - Identify key conditions
       - Document actual outcomes
       - Establish baseline
    
    2. COUNTERFACTUAL CONSTRUCTION:
       - Change one or few variables
       - Keep other conditions constant
       - Ensure plausible alternatives
       - Consider realistic modifications
    
    3. OUTCOME PREDICTION:
       - Use causal models
       - Consider downstream effects
       - Account for interactions
       - Assess uncertainty
    
    4. COMPARATIVE ANALYSIS:
       - Identify key differences
       - Analyze causal chains
       - Compare outcomes
       - Evaluate trade-offs
    
    5. LIKELIHOOD ASSESSMENT:
       - Consider feasibility
       - Assess probability
       - Account for constraints
       - Be realistic
    
    6. APPLICATIONS:
       - Decision evaluation
       - Risk assessment
       - Policy analysis
       - Learning from history
       - Strategy planning
    
    Benefits:
    ✓ Understand causality
    ✓ Evaluate decisions
    ✓ Assess risks
    ✓ Learn from alternatives
    ✓ Improve future decisions
    
    Limitations:
    - Speculative nature
    - Multiple valid interpretations
    - Requires good causal model
    - Uncertainty in predictions
    - May be influenced by biases
        """)
    
    
    if __name__ == "__main__":
        demonstrate_counterfactual_reasoning()
    
        

    Agentic Design Pattern: Counterfactual Reasoning

    Pattern 162: Counterfactual Reasoning – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 162: Counterfactual Reasoning

    Description: Counterfactual reasoning explores “what if” scenarios by considering alternative outcomes if certain conditions were different. It helps understand causality, evaluate decisions, and plan for contingencies.

    Components: – Scenario modification – Alternative outcome prediction – Causal analysis – Comparison with actual outcomes – Impact assessment

    Use Cases: – Decision analysis – Risk assessment – Causal inference – Policy evaluation – Learning from mistakes

    Benefits: – Understand causality – Evaluate alternatives – Learn from hypotheticals – Improve decision-making

    Trade-offs: – Speculative nature – Multiple possibilities – Requires causal model – May be computationally intensive

    LangChain Implementation: Uses ChatOpenAI for scenario generation, outcome prediction, and comparative analysis


    File Statistics

    • Total Lines: 403
    • Classes: 3
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 162: Counterfactual Reasoning
    
    Description:
        Counterfactual reasoning explores "what if" scenarios by considering alternative
        outcomes if certain conditions were different. It helps understand causality,
        evaluate decisions, and plan for contingencies.
    
    Components:
        - Scenario modification
        - Alternative outcome prediction
        - Causal analysis
        - Comparison with actual outcomes
        - Impact assessment
    
    Use Cases:
        - Decision analysis
        - Risk assessment
        - Causal inference
        - Policy evaluation
        - Learning from mistakes
    
    Benefits:
        - Understand causality
        - Evaluate alternatives
        - Learn from hypotheticals
        - Improve decision-making
    
    Trade-offs:
        - Speculative nature
        - Multiple possibilities
        - Requires causal model
        - May be computationally intensive
    
    LangChain Implementation:
        Uses ChatOpenAI for scenario generation, outcome prediction,
        and comparative analysis
    """
    

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


    Import Statements

    Standard Library Imports

    import dataclasses.dataclass
    import dataclasses.field
    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 – 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: Scenario

    Lines 53-58

    Purpose:

    Actual or counterfactual scenario
    

    Class: CounterfactualAnalysis

    Lines 62-70

    Purpose:

    Analysis of counterfactual scenario
    

    Class: CounterfactualReasoningAgent

    Lines 73-217

    Purpose:

    Agent that performs counterfactual reasoning
    

    Methods:

    __init__(self, model_name)

    Lines 76-77

    Purpose: Initialize the class instance with required parameters.

    Parameters: – model_name: Language model instance

    analyze(self, actual_scenario, modifications)

    Lines 79-116

    Purpose:

    Analyze counterfactual scenario
    

    Parameters: – actual_scenario: Actual scenario – modifications: Modifications

    Returns: CounterfactualAnalysis

    _generate_cf_description(self, actual, mods)

    Lines 118-133

    Purpose:

    Generate counterfactual description
    

    Parameters: – actual: Actual – mods: Mods

    Returns: str

    _predict_outcome(self, cf_scenario, actual)

    Lines 135-154

    Purpose:

    Predict counterfactual outcome
    

    Parameters: – cf_scenario: Cf scenario – actual: Actual

    Returns: str

    _identify_differences(self, actual, cf)

    Lines 156-164

    Purpose:

    Identify key differences
    

    Parameters: – actual: Actual – cf: Cf

    Returns: <ast.Subscript object at 0x000001A06D615F10>

    _analyze_causality(self, actual, cf, differences)

    Lines 166-186

    Purpose:

    Analyze causal factors
    

    Parameters: – actual: Actual – cf: Cf – differences: Differences

    Returns: <ast.Subscript object at 0x000001A06D72A110>

    _assess_likelihood(self, cf, actual)

    Lines 188-199

    Purpose:

    Assess likelihood of counterfactual
    

    Parameters: – cf: Cf – actual: Actual

    Returns: float

    _generate_reasoning(self, actual, cf, outcome)

    Lines 201-217

    Purpose:

    Generate reasoning explanation
    

    Parameters: – actual: Actual – cf: Cf – outcome: Outcome

    Returns: str


    Functions

    demonstrate_counterfactual_reasoning()

    Lines 220-398

    Purpose:

    Demonstrate counterfactual reasoning pattern
    

    Key Concepts Summary

    Pattern Overview

    The Counterfactual Reasoning 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 161
    • Pattern 163

    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 Counterfactual Reasoning 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: 162_counterfactual_reasoning.py
    Total Lines: 403
    Pattern Category: AI Agent Design Pattern #162


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Deductive Reasoning

    
        """
    Pattern 161: Deductive Reasoning
    
    Description:
        Deductive reasoning derives specific conclusions from general premises using
        logical rules. If the premises are true and the logic is valid, the conclusion
        must be true. This is the foundation of formal logic and mathematical proof.
    
    Components:
        - Premise collection and validation
        - Logical rule application
        - Inference chain construction
        - Conclusion derivation
        - Validity checking
    
    Use Cases:
        - Logical problem solving
        - Mathematical proofs
        - Rule-based systems
        - Legal reasoning
        - Formal verification
    
    Benefits:
        - Guaranteed valid conclusions
        - Transparent reasoning
        - Systematic approach
        - Verifiable logic
    
    Trade-offs:
        - Requires valid premises
        - Limited to formal logic
        - May be rigid
        - Doesn't discover new rules
    
    LangChain Implementation:
        Uses ChatOpenAI for logical inference, premise validation,
        and step-by-step deduction chains
    """
    
    import os
    from typing import List, Dict, Any, Optional
    from dataclasses import dataclass, field
    from enum import Enum
    from dotenv import load_dotenv
    
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    
    load_dotenv()
    
    
    class LogicalOperator(Enum):
        """Logical operators"""
        AND = "and"
        OR = "or"
        NOT = "not"
        IMPLIES = "implies"
        IF_AND_ONLY_IF = "iff"
    
    
    class InferenceRule(Enum):
        """Common inference rules"""
        MODUS_PONENS = "modus_ponens"  # If P→Q and P, then Q
        MODUS_TOLLENS = "modus_tollens"  # If P→Q and ¬Q, then ¬P
        HYPOTHETICAL_SYLLOGISM = "hypothetical_syllogism"  # If P→Q and Q→R, then P→R
        DISJUNCTIVE_SYLLOGISM = "disjunctive_syllogism"  # If P∨Q and ¬P, then Q
        CONTRAPOSITIVE = "contrapositive"  # P→Q equivalent to ¬Q→¬P
    
    
    @dataclass
    class Premise:
        """Logical premise or statement"""
        statement: str
        is_axiom: bool = False  # Assumed true
        confidence: float = 1.0
        source: Optional[str] = None
    
    
    @dataclass
    class InferenceStep:
        """Single step in deductive reasoning"""
        conclusion: str
        rule_applied: str
        premises_used: List[str]
        confidence: float = 1.0
    
    
    @dataclass
    class DeductiveProof:
        """Complete deductive proof"""
        premises: List[Premise]
        steps: List[InferenceStep]
        final_conclusion: str
        is_valid: bool
        reasoning_trace: List[str] = field(default_factory=list)
    
    
    class DeductiveReasoningAgent:
        """Agent that performs deductive reasoning"""
        
        def __init__(self, model_name: str = "gpt-3.5-turbo"):
            """
            Initialize deductive reasoning agent
            
            Args:
                model_name: LLM model to use
            """
            self.llm = ChatOpenAI(model=model_name, temperature=0)
            self.validator_llm = ChatOpenAI(model=model_name, temperature=0)
        
        def prove(self, premises: List[Premise], 
                  goal: str,
                  max_steps: int = 10) -> DeductiveProof:
            """
            Construct deductive proof from premises to goal
            
            Args:
                premises: List of premises
                goal: Goal conclusion to prove
                max_steps: Maximum reasoning steps
                
            Returns:
                DeductiveProof with reasoning chain
            """
            reasoning_trace = []
            steps = []
            
            reasoning_trace.append("Starting deductive proof...")
            reasoning_trace.append(f"Goal: {goal}")
            
            # Format premises
            premises_text = "\n".join([
                f"- {p.statement}" for p in premises
            ])
            reasoning_trace.append(f"Given premises:\n{premises_text}")
            
            # Generate proof steps
            for i in range(max_steps):
                # Check if goal is reached
                if steps and steps[-1].conclusion.lower() in goal.lower():
                    reasoning_trace.append(f"Goal reached in {i} steps!")
                    break
                
                # Generate next inference step
                step = self._generate_next_step(premises, steps, goal)
                if step:
                    steps.append(step)
                    reasoning_trace.append(
                        f"Step {i+1}: {step.conclusion} (via {step.rule_applied})"
                    )
                else:
                    reasoning_trace.append("No more valid inferences possible")
                    break
            
            # Validate proof
            is_valid = self._validate_proof(premises, steps, goal)
            
            final_conclusion = steps[-1].conclusion if steps else "No conclusion reached"
            
            return DeductiveProof(
                premises=premises,
                steps=steps,
                final_conclusion=final_conclusion,
                is_valid=is_valid,
                reasoning_trace=reasoning_trace
            )
        
        def _generate_next_step(self, premises: List[Premise],
                               existing_steps: List[InferenceStep],
                               goal: str) -> Optional[InferenceStep]:
            """Generate next logical inference step"""
            
            # Format context
            premises_text = "\n".join([f"- {p.statement}" for p in premises])
            
            if existing_steps:
                derived_text = "\n".join([
                    f"- {s.conclusion}" for s in existing_steps
                ])
            else:
                derived_text = "None yet"
            
            # Create inference prompt
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a logical reasoning expert. Given premises and a goal,
    generate the next logical inference step using valid deductive reasoning rules.
    
    Common rules:
    - Modus Ponens: If P→Q and P, then Q
    - Modus Tollens: If P→Q and ¬Q, then ¬P
    - Hypothetical Syllogism: If P→Q and Q→R, then P→R
    - Disjunctive Syllogism: If P∨Q and ¬P, then Q
    
    Return only the new conclusion and the rule used."""),
                ("user", """Premises:
    {premises}
    
    Already derived:
    {derived}
    
    Goal: {goal}
    
    What is the next logical inference? Format:
    Conclusion: [your conclusion]
    Rule: [rule name]
    Uses: [which premises/conclusions]""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            response = chain.invoke({
                "premises": premises_text,
                "derived": derived_text,
                "goal": goal
            })
            
            # Parse response
            return self._parse_inference_step(response)
        
        def _parse_inference_step(self, response: str) -> Optional[InferenceStep]:
            """Parse inference step from LLM response"""
            lines = response.strip().split('\n')
            conclusion = None
            rule = None
            uses = []
            
            for line in lines:
                line = line.strip()
                if line.lower().startswith('conclusion:'):
                    conclusion = line.split(':', 1)[1].strip()
                elif line.lower().startswith('rule:'):
                    rule = line.split(':', 1)[1].strip()
                elif line.lower().startswith('uses:'):
                    uses_text = line.split(':', 1)[1].strip()
                    uses = [u.strip() for u in uses_text.split(',')]
            
            if conclusion and rule:
                return InferenceStep(
                    conclusion=conclusion,
                    rule_applied=rule,
                    premises_used=uses,
                    confidence=1.0
                )
            
            return None
        
        def _validate_proof(self, premises: List[Premise],
                           steps: List[InferenceStep],
                           goal: str) -> bool:
            """Validate that the proof is logically sound"""
            
            if not steps:
                return False
            
            # Check if final conclusion matches goal
            final_matches = goal.lower() in steps[-1].conclusion.lower()
            
            # Use LLM to validate logical soundness
            premises_text = "\n".join([f"- {p.statement}" for p in premises])
            steps_text = "\n".join([
                f"{i+1}. {s.conclusion} (via {s.rule_applied})"
                for i, s in enumerate(steps)
            ])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "You are a logic validator. Check if the reasoning steps are logically valid."),
                ("user", """Premises:
    {premises}
    
    Reasoning steps:
    {steps}
    
    Goal: {goal}
    
    Is this proof logically valid? Respond with 'yes' or 'no' and brief explanation.""")
            ])
            
            chain = prompt | self.validator_llm | StrOutputParser()
            
            response = chain.invoke({
                "premises": premises_text,
                "steps": steps_text,
                "goal": goal
            })
            
            is_valid = 'yes' in response.lower() and final_matches
            return is_valid
        
        def apply_rule(self, rule: InferenceRule, 
                       statements: List[str]) -> Optional[str]:
            """Apply specific inference rule to statements"""
            
            rule_descriptions = {
                InferenceRule.MODUS_PONENS: "If P implies Q, and P is true, then Q is true",
                InferenceRule.MODUS_TOLLENS: "If P implies Q, and Q is false, then P is false",
                InferenceRule.HYPOTHETICAL_SYLLOGISM: "If P implies Q, and Q implies R, then P implies R",
                InferenceRule.DISJUNCTIVE_SYLLOGISM: "If P or Q, and not P, then Q",
            }
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", f"Apply the logical rule: {rule_descriptions.get(rule, rule.value)}"),
                ("user", """Given statements:
    {statements}
    
    What conclusion can be derived? Provide just the conclusion.""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            statements_text = "\n".join([f"- {s}" for s in statements])
            conclusion = chain.invoke({"statements": statements_text})
            
            return conclusion.strip()
    
    
    def demonstrate_deductive_reasoning():
        """Demonstrate deductive reasoning pattern"""
        print("=" * 80)
        print("DEDUCTIVE REASONING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = DeductiveReasoningAgent()
        
        # Example 1: Classic syllogism
        print("\n" + "="*80)
        print("EXAMPLE 1: Classic Syllogism (Socrates)")
        print("="*80)
        premises = [
            Premise("All humans are mortal", is_axiom=True),
            Premise("Socrates is a human", is_axiom=True)
        ]
        goal = "Socrates is mortal"
        
        proof = agent.prove(premises, goal, max_steps=5)
        
        print("Premises:")
        for p in proof.premises:
            print(f"  - {p.statement}")
        
        print(f"\nGoal: {goal}")
        print(f"\nProof steps:")
        for i, step in enumerate(proof.steps, 1):
            print(f"  {i}. {step.conclusion}")
            print(f"     Rule: {step.rule_applied}")
        
        print(f"\nConclusion: {proof.final_conclusion}")
        print(f"Valid: {proof.is_valid}")
        
        # Example 2: Modus Ponens
        print("\n" + "="*80)
        print("EXAMPLE 2: Modus Ponens")
        print("="*80)
        statements = [
            "If it rains, the ground gets wet",
            "It is raining"
        ]
        conclusion = agent.apply_rule(InferenceRule.MODUS_PONENS, statements)
        print("Given:")
        for s in statements:
            print(f"  - {s}")
        print(f"\nConclusion: {conclusion}")
        
        # Example 3: Mathematical reasoning
        print("\n" + "="*80)
        print("EXAMPLE 3: Mathematical Reasoning")
        print("="*80)
        premises = [
            Premise("If x > 5, then x > 3", is_axiom=True),
            Premise("x = 7", is_axiom=True),
            Premise("7 > 5", is_axiom=True)
        ]
        goal = "x > 3"
        
        proof = agent.prove(premises, goal, max_steps=5)
        
        print("Premises:")
        for p in proof.premises:
            print(f"  - {p.statement}")
        
        print(f"\nReasoning:")
        for trace in proof.reasoning_trace:
            if "Step" in trace or "Goal" in trace:
                print(f"  {trace}")
        
        print(f"\nFinal: {proof.final_conclusion}")
        
        # Example 4: Logical chain
        print("\n" + "="*80)
        print("EXAMPLE 4: Hypothetical Syllogism")
        print("="*80)
        statements = [
            "If it rains, the roads are wet",
            "If the roads are wet, driving is dangerous"
        ]
        conclusion = agent.apply_rule(InferenceRule.HYPOTHETICAL_SYLLOGISM, statements)
        print("Given:")
        for s in statements:
            print(f"  - {s}")
        print(f"\nConclusion: {conclusion}")
        
        # Example 5: Complex proof
        print("\n" + "="*80)
        print("EXAMPLE 5: Multi-Step Proof")
        print("="*80)
        premises = [
            Premise("All birds have feathers", is_axiom=True),
            Premise("All animals with feathers can regulate body temperature", is_axiom=True),
            Premise("A penguin is a bird", is_axiom=True)
        ]
        goal = "A penguin can regulate body temperature"
        
        proof = agent.prove(premises, goal, max_steps=10)
        
        print("Premises:")
        for p in proof.premises:
            print(f"  - {p.statement}")
        
        print(f"\nGoal: {goal}")
        print(f"\nProof trace:")
        for trace in proof.reasoning_trace[-5:]:
            print(f"  {trace}")
        
        # Example 6: Modus Tollens
        print("\n" + "="*80)
        print("EXAMPLE 6: Modus Tollens (Proof by Contrapositive)")
        print("="*80)
        statements = [
            "If the alarm is working, it would have gone off",
            "The alarm did not go off"
        ]
        conclusion = agent.apply_rule(InferenceRule.MODUS_TOLLENS, statements)
        print("Given:")
        for s in statements:
            print(f"  - {s}")
        print(f"\nConclusion: {conclusion}")
        
        # Summary
        print("\n" + "="*80)
        print("SUMMARY: Deductive Reasoning Best Practices")
        print("="*80)
        print("""
    1. PREMISE VALIDATION:
       - Verify premises are true
       - Identify axioms vs derived facts
       - Check premise consistency
       - Document premise sources
    
    2. LOGICAL RULES:
       - Use established inference rules
       - Apply rules systematically
       - Maintain logical validity
       - Document rule application
    
    3. PROOF CONSTRUCTION:
       - Build step-by-step chains
       - Connect premises to conclusion
       - Verify each step
       - Keep proofs as simple as possible
    
    4. COMMON RULES:
       - Modus Ponens: P→Q, P ⊢ Q
       - Modus Tollens: P→Q, ¬Q ⊢ ¬P
       - Hypothetical Syllogism: P→Q, Q→R ⊢ P→R
       - Disjunctive Syllogism: P∨Q, ¬P ⊢ Q
       - Conjunction: P, Q ⊢ P∧Q
    
    5. VALIDATION:
       - Check logical soundness
       - Verify conclusion follows
       - Test for fallacies
       - Review proof structure
    
    6. APPLICATIONS:
       - Mathematical proofs
       - Legal reasoning
       - Rule-based systems
       - Formal verification
       - Logical problem solving
    
    Benefits:
    ✓ Guarantees valid conclusions
    ✓ Transparent reasoning process
    ✓ Systematic and rigorous
    ✓ Verifiable logic
    ✓ Foundation for formal systems
    
    Limitations:
    - Requires true premises
    - Limited to formal logic
    - May be inflexible
    - Doesn't generate new knowledge
    - Can be verbose for complex proofs
        """)
    
    
    if __name__ == "__main__":
        demonstrate_deductive_reasoning()
    
        

    Agentic Design Pattern: Deductive Reasoning

    Pattern 161: Deductive Reasoning – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 161: Deductive Reasoning

    Description: Deductive reasoning derives specific conclusions from general premises using logical rules. If the premises are true and the logic is valid, the conclusion must be true. This is the foundation of formal logic and mathematical proof.

    Components: – Premise collection and validation – Logical rule application – Inference chain construction – Conclusion derivation – Validity checking

    Use Cases: – Logical problem solving – Mathematical proofs – Rule-based systems – Legal reasoning – Formal verification

    Benefits: – Guaranteed valid conclusions – Transparent reasoning – Systematic approach – Verifiable logic

    Trade-offs: – Requires valid premises – Limited to formal logic – May be rigid – Doesn’t discover new rules

    LangChain Implementation: Uses ChatOpenAI for logical inference, premise validation, and step-by-step deduction chains


    File Statistics

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

    Module Documentation

    """
    Pattern 161: Deductive Reasoning
    
    Description:
        Deductive reasoning derives specific conclusions from general premises using
        logical rules. If the premises are true and the logic is valid, the conclusion
        must be true. This is the foundation of formal logic and mathematical proof.
    
    Components:
        - Premise collection and validation
        - Logical rule application
        - Inference chain construction
        - Conclusion derivation
        - Validity checking
    
    Use Cases:
        - Logical problem solving
        - Mathematical proofs
        - Rule-based systems
        - Legal reasoning
        - Formal verification
    
    Benefits:
        - Guaranteed valid conclusions
        - Transparent reasoning
        - Systematic approach
        - Verifiable logic
    
    Trade-offs:
        - Requires valid premises
        - Limited to formal logic
        - May be rigid
        - Doesn't discover new rules
    
    LangChain Implementation:
        Uses ChatOpenAI for logical inference, premise validation,
        and step-by-step deduction chains
    """
    

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


    Import Statements

    Standard Library Imports

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

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

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: LogicalOperator

    Lines 53-59

    Inherits from: Enum

    Purpose:

    Logical operators
    

    Class: InferenceRule

    Lines 62-68

    Inherits from: Enum

    Purpose:

    Common inference rules
    

    Class: Premise

    Lines 72-77

    Purpose:

    Logical premise or statement
    

    Class: InferenceStep

    Lines 81-86

    Purpose:

    Single step in deductive reasoning
    

    Class: DeductiveProof

    Lines 90-96

    Purpose:

    Complete deductive proof
    

    Class: DeductiveReasoningAgent

    Lines 99-314

    Purpose:

    Agent that performs deductive reasoning
    

    Methods:

    __init__(self, model_name)

    Lines 102-110

    Purpose:

    Initialize deductive reasoning agent
    
    Args:
        model_name: LLM model to use
    

    Parameters: – model_name: Language model instance

    prove(self, premises, goal, max_steps)

    Lines 112-167

    Purpose:

    Construct deductive proof from premises to goal
    
    Args:
        premises: List of premises
        goal: Goal conclusion to prove
        max_steps: Maximum reasoning steps
    
    Returns:
        DeductiveProof with reasoning chain
    

    Parameters: – premises: Premises – goal: Goal – max_steps: Max steps

    Returns: DeductiveProof

    _generate_next_step(self, premises, existing_steps, goal)

    Lines 169-219

    Purpose:

    Generate next logical inference step
    

    Parameters: – premises: Premises – existing_steps: Existing steps – goal: Goal

    Returns: <ast.Subscript object at 0x000001A06D633F10>

    _parse_inference_step(self, response)

    Lines 221-246

    Purpose:

    Parse inference step from LLM response
    

    Parameters: – response: Response

    Returns: <ast.Subscript object at 0x000001A06E0A9710>

    _validate_proof(self, premises, steps, goal)

    Lines 248-288

    Purpose:

    Validate that the proof is logically sound
    

    Parameters: – premises: Premises – steps: Steps – goal: Goal

    Returns: bool

    apply_rule(self, rule, statements)

    Lines 290-314

    Purpose:

    Apply specific inference rule to statements
    

    Parameters: – rule: Rule – statements: Statements

    Returns: <ast.Subscript object at 0x000001A06D888690>


    Functions

    demonstrate_deductive_reasoning()

    Lines 317-494

    Purpose:

    Demonstrate deductive reasoning pattern
    

    Key Concepts Summary

    Pattern Overview

    The Deductive Reasoning 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 160
    • Pattern 162

    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 Deductive Reasoning 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: 161_deductive_reasoning.py
    Total Lines: 499
    Pattern Category: AI Agent Design Pattern #161


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Inductive Reasoning

    
        """
    Pattern 160: Inductive Reasoning
    
    Description:
        Inductive reasoning generalizes from specific observations to broader patterns
        or rules. It moves from particular instances to general conclusions, inferring
        patterns that likely hold across similar cases.
    
    Components:
        - Example collection
        - Pattern identification
        - Rule generalization
        - Confidence assessment
        - Counter-example checking
    
    Use Cases:
        - Pattern discovery
        - Rule learning
        - Trend analysis
        - Scientific generalization
        - Predictive modeling
    
    Benefits:
        - Discovers patterns from data
        - Enables prediction
        - Learns from examples
        - Practical applicability
    
    Trade-offs:
        - Conclusions not guaranteed
        - Vulnerable to biased samples
        - Requires sufficient examples
        - May over-generalize
    
    LangChain Implementation:
        Uses ChatOpenAI for pattern recognition, rule extraction,
        and validation through additional examples
    """
    
    import os
    from typing import List, Dict, Any, Optional
    from dataclasses import dataclass, field
    from dotenv import load_dotenv
    
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    import json
    
    load_dotenv()
    
    
    @dataclass
    class Example:
        """Single example/observation"""
        input: str
        output: str
        features: Dict[str, Any] = field(default_factory=dict)
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class Pattern:
        """Discovered pattern"""
        description: str
        rule: str
        confidence: float  # 0-1
        supporting_examples: List[str] = field(default_factory=list)
        counter_examples: List[str] = field(default_factory=list)
        generality: float = 0.5  # How general vs specific
    
    
    @dataclass
    class InductiveAnalysis:
        """Result of inductive reasoning"""
        examples: List[Example]
        patterns: List[Pattern]
        generalizations: List[str] = field(default_factory=list)
        confidence: float = 0.0
        reasoning_trace: List[str] = field(default_factory=list)
    
    
    class InductiveReasoningAgent:
        """Agent that performs inductive reasoning"""
        
        def __init__(self, model_name: str = "gpt-3.5-turbo"):
            """
            Initialize inductive reasoning agent
            
            Args:
                model_name: LLM model to use
            """
            self.llm = ChatOpenAI(model=model_name, temperature=0.7)
            self.validator_llm = ChatOpenAI(model=model_name, temperature=0)
        
        def reason(self, examples: List[Example],
                   domain: Optional[str] = None,
                   min_confidence: float = 0.5) -> InductiveAnalysis:
            """
            Perform inductive reasoning on examples
            
            Args:
                examples: List of examples to learn from
                domain: Optional domain context
                min_confidence: Minimum confidence threshold
                
            Returns:
                InductiveAnalysis with discovered patterns
            """
            reasoning_trace = []
            
            # Step 1: Identify patterns
            reasoning_trace.append(f"Analyzing {len(examples)} examples...")
            patterns = self._identify_patterns(examples, domain)
            reasoning_trace.append(f"Identified {len(patterns)} potential patterns")
            
            # Step 2: Validate patterns
            reasoning_trace.append("Validating patterns against examples...")
            for pattern in patterns:
                self._validate_pattern(pattern, examples)
            
            # Step 3: Filter by confidence
            patterns = [p for p in patterns if p.confidence >= min_confidence]
            reasoning_trace.append(f"Retained {len(patterns)} high-confidence patterns")
            
            # Step 4: Generate generalizations
            generalizations = self._generate_generalizations(patterns, examples)
            reasoning_trace.append(f"Generated {len(generalizations)} generalizations")
            
            # Step 5: Calculate overall confidence
            if patterns:
                avg_confidence = sum(p.confidence for p in patterns) / len(patterns)
            else:
                avg_confidence = 0.0
            
            return InductiveAnalysis(
                examples=examples,
                patterns=patterns,
                generalizations=generalizations,
                confidence=avg_confidence,
                reasoning_trace=reasoning_trace
            )
        
        def _identify_patterns(self, examples: List[Example],
                              domain: Optional[str]) -> List[Pattern]:
            """Identify patterns from examples"""
            # Format examples
            examples_text = "\n".join([
                f"Input: {ex.input} → Output: {ex.output}"
                for ex in examples
            ])
            
            # Create pattern identification prompt
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert at identifying patterns from examples.
    Analyze the given input-output examples and identify patterns or rules.
    
    {domain_context}
    
    For each pattern, provide:
    1. Clear description
    2. General rule
    3. How general vs specific it is (0-1)
    
    Return as JSON array: [{{"description": "...", "rule": "...", "generality": 0.0-1.0}}]"""),
                ("user", "Examples:\n{examples}\n\nIdentify patterns:")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            domain_context = f"Domain: {domain}" if domain else "Consider general patterns."
            
            response = chain.invoke({
                "examples": examples_text,
                "domain_context": domain_context
            })
            
            # Parse patterns
            patterns = self._parse_patterns(response)
            return patterns
        
        def _parse_patterns(self, response: str) -> List[Pattern]:
            """Parse patterns from LLM response"""
            try:
                # Try to extract JSON
                start = response.find('[')
                end = response.rfind(']') + 1
                if start >= 0 and end > start:
                    json_str = response[start:end]
                    data = json.loads(json_str)
                    
                    patterns = []
                    for item in data:
                        pattern = Pattern(
                            description=item.get('description', ''),
                            rule=item.get('rule', ''),
                            confidence=0.5,  # Initial, will validate
                            generality=float(item.get('generality', 0.5))
                        )
                        patterns.append(pattern)
                    return patterns
            except Exception as e:
                print(f"Error parsing patterns: {e}")
            
            # Fallback parsing
            patterns = []
            lines = response.split('\n')
            current_pattern = {}
            
            for line in lines:
                line = line.strip()
                if line.startswith(('1.', '2.', '3.', '4.', '5.')):
                    if current_pattern.get('description'):
                        patterns.append(Pattern(
                            description=current_pattern.get('description', ''),
                            rule=current_pattern.get('rule', ''),
                            confidence=0.5,
                            generality=0.5
                        ))
                    current_pattern = {'description': line[2:].strip()}
                elif 'rule:' in line.lower():
                    current_pattern['rule'] = line.split(':', 1)[1].strip()
            
            # Add last pattern
            if current_pattern.get('description'):
                patterns.append(Pattern(
                    description=current_pattern.get('description', ''),
                    rule=current_pattern.get('rule', ''),
                    confidence=0.5,
                    generality=0.5
                ))
            
            return patterns[:5]  # Limit to 5
        
        def _validate_pattern(self, pattern: Pattern, examples: List[Example]):
            """Validate pattern against examples"""
            supporting = 0
            contradicting = 0
            
            for example in examples:
                # Check if pattern applies to this example
                prompt = ChatPromptTemplate.from_messages([
                    ("system", "Determine if the pattern/rule applies to this example. Respond with 'yes' or 'no'."),
                    ("user", """Pattern: {pattern}
    Rule: {rule}
    
    Example - Input: {input}, Output: {output}
    
    Does the pattern apply?""")
                ])
                
                chain = prompt | self.validator_llm | StrOutputParser()
                
                response = chain.invoke({
                    "pattern": pattern.description,
                    "rule": pattern.rule,
                    "input": example.input,
                    "output": example.output
                })
                
                if 'yes' in response.lower():
                    supporting += 1
                    pattern.supporting_examples.append(f"{example.input} → {example.output}")
                else:
                    contradicting += 1
                    pattern.counter_examples.append(f"{example.input} → {example.output}")
            
            # Calculate confidence
            total = supporting + contradicting
            if total > 0:
                pattern.confidence = supporting / total
            else:
                pattern.confidence = 0.0
        
        def _generate_generalizations(self, patterns: List[Pattern],
                                     examples: List[Example]) -> List[str]:
            """Generate general rules from patterns"""
            if not patterns:
                return []
            
            # Format patterns
            patterns_text = "\n".join([
                f"- {p.description} (confidence: {p.confidence:.0%})"
                for p in patterns
            ])
            
            # Create generalization prompt
            prompt = ChatPromptTemplate.from_messages([
                ("system", "Synthesize the patterns into broader generalizations or principles."),
                ("user", """Patterns discovered:
    {patterns}
    
    Generate 2-3 general rules or principles that capture these patterns:""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            response = chain.invoke({"patterns": patterns_text})
            
            # Parse generalizations
            generalizations = []
            for line in response.split('\n'):
                line = line.strip()
                if line and (line[0].isdigit() or line.startswith('-')):
                    clean_line = line.lstrip('0123456789.-) ')
                    if clean_line:
                        generalizations.append(clean_line)
            
            return generalizations
        
        def predict(self, analysis: InductiveAnalysis, new_input: str) -> Dict[str, Any]:
            """Use learned patterns to make a prediction"""
            # Format patterns
            patterns_text = "\n".join([
                f"- {p.rule} (confidence: {p.confidence:.0%})"
                for p in analysis.patterns
            ])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """Use the learned patterns to predict the output for the new input.
    Explain which pattern(s) you're applying."""),
                ("user", """Learned patterns:
    {patterns}
    
    New input: {input}
    
    Predict the output and explain your reasoning:""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            response = chain.invoke({
                "patterns": patterns_text,
                "input": new_input
            })
            
            return {
                "input": new_input,
                "prediction": response,
                "based_on_patterns": len(analysis.patterns),
                "confidence": analysis.confidence
            }
    
    
    def demonstrate_inductive_reasoning():
        """Demonstrate inductive reasoning pattern"""
        print("=" * 80)
        print("INDUCTIVE REASONING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = InductiveReasoningAgent()
        
        # Example 1: Number patterns
        print("\n" + "="*80)
        print("EXAMPLE 1: Number Pattern Discovery")
        print("="*80)
        examples = [
            Example(input="2", output="4"),
            Example(input="3", output="9"),
            Example(input="4", output="16"),
            Example(input="5", output="25"),
            Example(input="6", output="36"),
        ]
        
        analysis = agent.reason(examples, domain="Mathematics")
        
        print("Examples:")
        for ex in examples:
            print(f"  {ex.input} → {ex.output}")
        
        print(f"\nDiscovered Patterns:")
        for i, pattern in enumerate(analysis.patterns, 1):
            print(f"\n{i}. {pattern.description}")
            print(f"   Rule: {pattern.rule}")
            print(f"   Confidence: {pattern.confidence:.0%}")
            print(f"   Generality: {pattern.generality:.1f}")
        
        # Test prediction
        if analysis.patterns:
            prediction = agent.predict(analysis, "7")
            print(f"\nPrediction for '7': {prediction['prediction'][:200]}...")
        
        # Example 2: Text transformation
        print("\n" + "="*80)
        print("EXAMPLE 2: Text Transformation Pattern")
        print("="*80)
        examples = [
            Example(input="cat", output="cats"),
            Example(input="dog", output="dogs"),
            Example(input="house", output="houses"),
            Example(input="car", output="cars"),
            Example(input="book", output="books"),
        ]
        
        analysis = agent.reason(examples, domain="English grammar")
        
        print("Examples:")
        for ex in examples:
            print(f"  {ex.input} → {ex.output}")
        
        print(f"\nPattern Analysis:")
        for pattern in analysis.patterns:
            print(f"\n- {pattern.description}")
            print(f"  Confidence: {pattern.confidence:.0%}")
            if pattern.supporting_examples:
                print(f"  Supporting: {len(pattern.supporting_examples)} examples")
        
        print(f"\nGeneralizations:")
        for gen in analysis.generalizations:
            print(f"  - {gen}")
        
        # Example 3: Category classification
        print("\n" + "="*80)
        print("EXAMPLE 3: Classification Pattern")
        print("="*80)
        examples = [
            Example(input="apple", output="fruit"),
            Example(input="banana", output="fruit"),
            Example(input="carrot", output="vegetable"),
            Example(input="broccoli", output="vegetable"),
            Example(input="orange", output="fruit"),
            Example(input="spinach", output="vegetable"),
        ]
        
        analysis = agent.reason(examples, domain="Food classification")
        
        print("Examples:")
        for ex in examples:
            print(f"  {ex.input} → {ex.output}")
        
        print(f"\nDiscovered Rules:")
        for pattern in analysis.patterns:
            print(f"\n- {pattern.rule}")
            print(f"  Confidence: {pattern.confidence:.0%}")
        
        # Test with new input
        prediction = agent.predict(analysis, "tomato")
        print(f"\nPrediction for 'tomato':")
        print(f"  {prediction['prediction'][:300]}...")
        
        # Example 4: Sequence patterns
        print("\n" + "="*80)
        print("EXAMPLE 4: Sequence Pattern")
        print("="*80)
        examples = [
            Example(input="position 1", output="2"),
            Example(input="position 2", output="4"),
            Example(input="position 3", output="6"),
            Example(input="position 4", output="8"),
            Example(input="position 5", output="10"),
        ]
        
        analysis = agent.reason(examples, domain="Sequences")
        
        print("Examples:")
        for ex in examples:
            print(f"  {ex.input} → {ex.output}")
        
        print(f"\nPattern:")
        if analysis.patterns:
            pattern = analysis.patterns[0]
            print(f"  {pattern.description}")
            print(f"  Rule: {pattern.rule}")
            print(f"  Confidence: {pattern.confidence:.0%}")
        
        # Example 5: Behavior pattern
        print("\n" + "="*80)
        print("EXAMPLE 5: Behavioral Pattern")
        print("="*80)
        examples = [
            Example(input="Monday morning", output="heavy traffic"),
            Example(input="Tuesday morning", output="heavy traffic"),
            Example(input="Wednesday morning", output="heavy traffic"),
            Example(input="Saturday morning", output="light traffic"),
            Example(input="Sunday morning", output="light traffic"),
        ]
        
        analysis = agent.reason(examples, domain="Traffic patterns")
        
        print("Examples:")
        for ex in examples:
            print(f"  {ex.input} → {ex.output}")
        
        print(f"\nInduced Patterns:")
        for i, pattern in enumerate(analysis.patterns, 1):
            print(f"\n{i}. {pattern.description}")
            print(f"   Confidence: {pattern.confidence:.0%}")
        
        print(f"\nGeneralizations:")
        for gen in analysis.generalizations:
            print(f"  - {gen}")
        
        # Prediction
        prediction = agent.predict(analysis, "Thursday morning")
        print(f"\nPrediction for 'Thursday morning':")
        print(f"  {prediction['prediction'][:200]}...")
        
        # Summary
        print("\n" + "="*80)
        print("SUMMARY: Inductive Reasoning Best Practices")
        print("="*80)
        print("""
    1. EXAMPLE COLLECTION:
       - Gather diverse, representative examples
       - Ensure sufficient quantity
       - Include edge cases
       - Verify example quality
    
    2. PATTERN IDENTIFICATION:
       - Look for commonalities
       - Consider multiple levels of abstraction
       - Identify exceptions
       - Test alternative patterns
    
    3. VALIDATION:
       - Check against all examples
       - Count supporting vs contradicting evidence
       - Calculate confidence scores
       - Seek counter-examples
    
    4. GENERALIZATION:
       - Move from specific to general
       - Define scope of applicability
       - Acknowledge limitations
       - Avoid over-generalization
    
    5. CONFIDENCE ASSESSMENT:
       - Based on supporting evidence ratio
       - Consider sample size
       - Account for example diversity
       - Update with new evidence
    
    6. APPLICATIONS:
       - Pattern discovery in data
       - Rule learning from examples
       - Trend analysis
       - Predictive modeling
       - Scientific generalization
    
    Benefits:
    ✓ Discovers patterns from data
    ✓ Learns from examples
    ✓ Enables predictions
    ✓ Practical and applicable
    ✓ Foundation for machine learning
    
    Limitations:
    - Conclusions not guaranteed
    - Sensitive to sample bias
    - May over-generalize
    - Requires sufficient examples
    - Exception handling needed
        """)
    
    
    if __name__ == "__main__":
        demonstrate_inductive_reasoning()
    
        

    Agentic Design Pattern: Inductive Reasoning

    Pattern 160: Inductive Reasoning – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 160: Inductive Reasoning

    Description: Inductive reasoning generalizes from specific observations to broader patterns or rules. It moves from particular instances to general conclusions, inferring patterns that likely hold across similar cases.

    Components: – Example collection – Pattern identification – Rule generalization – Confidence assessment – Counter-example checking

    Use Cases: – Pattern discovery – Rule learning – Trend analysis – Scientific generalization – Predictive modeling

    Benefits: – Discovers patterns from data – Enables prediction – Learns from examples – Practical applicability

    Trade-offs: – Conclusions not guaranteed – Vulnerable to biased samples – Requires sufficient examples – May over-generalize

    LangChain Implementation: Uses ChatOpenAI for pattern recognition, rule extraction, and validation through additional examples


    File Statistics

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

    Module Documentation

    """
    Pattern 160: Inductive Reasoning
    
    Description:
        Inductive reasoning generalizes from specific observations to broader patterns
        or rules. It moves from particular instances to general conclusions, inferring
        patterns that likely hold across similar cases.
    
    Components:
        - Example collection
        - Pattern identification
        - Rule generalization
        - Confidence assessment
        - Counter-example checking
    
    Use Cases:
        - Pattern discovery
        - Rule learning
        - Trend analysis
        - Scientific generalization
        - Predictive modeling
    
    Benefits:
        - Discovers patterns from data
        - Enables prediction
        - Learns from examples
        - Practical applicability
    
    Trade-offs:
        - Conclusions not guaranteed
        - Vulnerable to biased samples
        - Requires sufficient examples
        - May over-generalize
    
    LangChain Implementation:
        Uses ChatOpenAI for pattern recognition, rule extraction,
        and validation through additional examples
    """
    

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


    Import Statements

    Standard Library Imports

    import dataclasses.dataclass
    import dataclasses.field
    import json
    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 – 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 – 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: Example

    Lines 54-59

    Purpose:

    Single example/observation
    

    Class: Pattern

    Lines 63-70

    Purpose:

    Discovered pattern
    

    Class: InductiveAnalysis

    Lines 74-80

    Purpose:

    Result of inductive reasoning
    

    Class: InductiveReasoningAgent

    Lines 83-342

    Purpose:

    Agent that performs inductive reasoning
    

    Methods:

    __init__(self, model_name)

    Lines 86-94

    Purpose:

    Initialize inductive reasoning agent
    
    Args:
        model_name: LLM model to use
    

    Parameters: – model_name: Language model instance

    reason(self, examples, domain, min_confidence)

    Lines 96-142

    Purpose:

    Perform inductive reasoning on examples
    
    Args:
        examples: List of examples to learn from
        domain: Optional domain context
        min_confidence: Minimum confidence threshold
    
    Returns:
        InductiveAnalysis with discovered patterns
    

    Parameters: – examples: Examples – domain: Domain – min_confidence: Min confidence

    Returns: InductiveAnalysis

    _identify_patterns(self, examples, domain)

    Lines 144-180

    Purpose:

    Identify patterns from examples
    

    Parameters: – examples: Examples – domain: Domain

    Returns: <ast.Subscript object at 0x000001A06D684350>

    _parse_patterns(self, response)

    Lines 182-233

    Purpose:

    Parse patterns from LLM response
    

    Parameters: – response: Response

    Returns: <ast.Subscript object at 0x000001A06D615FD0>

    _validate_pattern(self, pattern, examples)

    Lines 235-273

    Purpose:

    Validate pattern against examples
    

    Parameters: – pattern: Pattern – examples: Examples

    _generate_generalizations(self, patterns, examples)

    Lines 275-309

    Purpose:

    Generate general rules from patterns
    

    Parameters: – patterns: Patterns – examples: Examples

    Returns: <ast.Subscript object at 0x000001A06D7CC990>

    predict(self, analysis, new_input)

    Lines 311-342

    Purpose:

    Use learned patterns to make a prediction
    

    Parameters: – analysis: Analysis – new_input: Input data

    Returns: <ast.Subscript object at 0x000001A06D8550D0>


    Functions

    demonstrate_inductive_reasoning()

    Lines 345-553

    Purpose:

    Demonstrate inductive reasoning pattern
    

    Key Concepts Summary

    Pattern Overview

    The Inductive Reasoning 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 159
    • Pattern 161

    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 Inductive Reasoning 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: 160_inductive_reasoning.py
    Total Lines: 558
    Pattern Category: AI Agent Design Pattern #160


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Abductive Reasoning

    
        """
    Pattern 159: Abductive Reasoning
    
    Description:
        Abductive reasoning infers the best explanation for a set of observations.
        Unlike deductive reasoning (which derives consequences from premises) or
        inductive reasoning (which generalizes from examples), abduction works
        backwards from observations to find the most likely cause or explanation.
    
    Components:
        - Observation collection
        - Hypothesis generation
        - Explanation evaluation
        - Likelihood ranking
        - Plausibility assessment
    
    Use Cases:
        - Diagnostic reasoning (medical, technical)
        - Root cause analysis
        - Mystery solving
        - Troubleshooting
        - Scientific hypothesis formation
    
    Benefits:
        - Finds likely explanations
        - Handles incomplete information
        - Supports decision-making
        - Enables inference to best explanation
    
    Trade-offs:
        - Conclusions not guaranteed
        - Multiple possible explanations
        - Requires domain knowledge
        - Computationally intensive
    
    LangChain Implementation:
        Uses ChatOpenAI for hypothesis generation, evaluation scoring,
        and ranking of explanations by plausibility
    """
    
    import os
    from typing import List, Dict, Any, Optional
    from dataclasses import dataclass, field
    from enum import Enum
    from dotenv import load_dotenv
    
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    import json
    
    load_dotenv()
    
    
    class ConfidenceLevel(Enum):
        """Confidence levels for hypotheses"""
        VERY_HIGH = "very_high"
        HIGH = "high"
        MEDIUM = "medium"
        LOW = "low"
        VERY_LOW = "very_low"
    
    
    @dataclass
    class Observation:
        """Single observation or evidence"""
        description: str
        importance: float = 1.0  # 0-1 scale
        timestamp: Optional[str] = None
        source: Optional[str] = None
    
    
    @dataclass
    class Hypothesis:
        """Potential explanation"""
        explanation: str
        confidence: float  # 0-1 scale
        supporting_evidence: List[str] = field(default_factory=list)
        contradicting_evidence: List[str] = field(default_factory=list)
        assumptions: List[str] = field(default_factory=list)
        plausibility_score: float = 0.0
    
    
    @dataclass
    class AbductiveAnalysis:
        """Result of abductive reasoning"""
        observations: List[Observation]
        hypotheses: List[Hypothesis]
        best_explanation: Optional[Hypothesis]
        reasoning_trace: List[str] = field(default_factory=list)
    
    
    class AbductiveReasoningAgent:
        """Agent that performs abductive reasoning"""
        
        def __init__(self, model_name: str = "gpt-3.5-turbo"):
            """
            Initialize abductive reasoning agent
            
            Args:
                model_name: LLM model to use
            """
            self.llm = ChatOpenAI(model=model_name, temperature=0.7)
            self.evaluator_llm = ChatOpenAI(model=model_name, temperature=0)
        
        def reason(self, observations: List[Observation], 
                   domain_context: Optional[str] = None,
                   num_hypotheses: int = 5) -> AbductiveAnalysis:
            """
            Perform abductive reasoning on observations
            
            Args:
                observations: List of observations
                domain_context: Optional domain-specific context
                num_hypotheses: Number of hypotheses to generate
                
            Returns:
                AbductiveAnalysis with ranked hypotheses
            """
            reasoning_trace = []
            
            # Step 1: Generate hypotheses
            reasoning_trace.append("Generating hypotheses from observations...")
            hypotheses = self._generate_hypotheses(observations, domain_context, num_hypotheses)
            reasoning_trace.append(f"Generated {len(hypotheses)} hypotheses")
            
            # Step 2: Evaluate each hypothesis
            reasoning_trace.append("Evaluating hypotheses...")
            for hypothesis in hypotheses:
                self._evaluate_hypothesis(hypothesis, observations)
            
            # Step 3: Rank hypotheses
            hypotheses.sort(key=lambda h: h.plausibility_score, reverse=True)
            reasoning_trace.append("Ranked hypotheses by plausibility")
            
            # Step 4: Select best explanation
            best_explanation = hypotheses[0] if hypotheses else None
            
            return AbductiveAnalysis(
                observations=observations,
                hypotheses=hypotheses,
                best_explanation=best_explanation,
                reasoning_trace=reasoning_trace
            )
        
        def _generate_hypotheses(self, observations: List[Observation],
                                domain_context: Optional[str],
                                num_hypotheses: int) -> List[Hypothesis]:
            """Generate potential hypotheses"""
            # Format observations
            obs_text = "\n".join([
                f"- {obs.description} (importance: {obs.importance:.1f})"
                for obs in observations
            ])
            
            # Create prompt for hypothesis generation
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert at abductive reasoning - inferring the best explanation for observations.
    Generate {num_hypotheses} plausible hypotheses that could explain the given observations.
    For each hypothesis, provide:
    1. Clear explanation
    2. Supporting reasoning
    3. Key assumptions
    
    {domain_context}
    
    Return as JSON array: [{{"explanation": "...", "reasoning": "...", "assumptions": ["..."]}}]"""),
                ("user", "Observations:\n{observations}\n\nGenerate {num_hypotheses} hypotheses:")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            context = f"Domain context: {domain_context}" if domain_context else "Consider general knowledge."
            
            response = chain.invoke({
                "observations": obs_text,
                "num_hypotheses": num_hypotheses,
                "domain_context": context
            })
            
            # Parse response
            hypotheses = self._parse_hypotheses(response)
            return hypotheses
        
        def _parse_hypotheses(self, response: str) -> List[Hypothesis]:
            """Parse hypotheses from LLM response"""
            try:
                # Try to extract JSON
                start = response.find('[')
                end = response.rfind(']') + 1
                if start >= 0 and end > start:
                    json_str = response[start:end]
                    data = json.loads(json_str)
                    
                    hypotheses = []
                    for item in data:
                        hypothesis = Hypothesis(
                            explanation=item.get('explanation', ''),
                            confidence=0.5,  # Initial neutral confidence
                            assumptions=item.get('assumptions', [])
                        )
                        hypotheses.append(hypothesis)
                    return hypotheses
            except Exception as e:
                print(f"Error parsing hypotheses: {e}")
            
            # Fallback: split by numbered items
            lines = response.split('\n')
            hypotheses = []
            current_explanation = []
            
            for line in lines:
                line = line.strip()
                if line and (line[0].isdigit() or line.startswith('-')):
                    if current_explanation:
                        explanation_text = ' '.join(current_explanation)
                        if explanation_text:
                            hypotheses.append(Hypothesis(
                                explanation=explanation_text,
                                confidence=0.5
                            ))
                        current_explanation = []
                    # Remove numbering/bullet
                    clean_line = line.lstrip('0123456789.-) ')
                    current_explanation.append(clean_line)
                elif current_explanation:
                    current_explanation.append(line)
            
            # Add last hypothesis
            if current_explanation:
                explanation_text = ' '.join(current_explanation)
                if explanation_text:
                    hypotheses.append(Hypothesis(
                        explanation=explanation_text,
                        confidence=0.5
                    ))
            
            return hypotheses[:5]  # Limit to 5
        
        def _evaluate_hypothesis(self, hypothesis: Hypothesis, 
                                observations: List[Observation]):
            """Evaluate a hypothesis against observations"""
            # Format observations
            obs_text = "\n".join([obs.description for obs in observations])
            
            # Create evaluation prompt
            prompt = ChatPromptTemplate.from_messages([
                ("system", """Evaluate how well this hypothesis explains the observations.
    Provide:
    1. Supporting evidence (which observations support it)
    2. Contradicting evidence (which observations contradict it)
    3. Confidence score (0-1)
    4. Plausibility assessment
    
    Return as JSON: {{
        "supporting": ["..."],
        "contradicting": ["..."],
        "confidence": 0.0-1.0,
        "plausibility": 0.0-1.0,
        "reasoning": "..."
    }}"""),
                ("user", """Hypothesis: {hypothesis}
    
    Observations:
    {observations}
    
    Evaluate:""")
            ])
            
            chain = prompt | self.evaluator_llm | StrOutputParser()
            
            response = chain.invoke({
                "hypothesis": hypothesis.explanation,
                "observations": obs_text
            })
            
            # Parse evaluation
            try:
                start = response.find('{')
                end = response.rfind('}') + 1
                if start >= 0 and end > start:
                    json_str = response[start:end]
                    data = json.loads(json_str)
                    
                    hypothesis.supporting_evidence = data.get('supporting', [])
                    hypothesis.contradicting_evidence = data.get('contradicting', [])
                    hypothesis.confidence = float(data.get('confidence', 0.5))
                    hypothesis.plausibility_score = float(data.get('plausibility', 0.5))
            except Exception as e:
                print(f"Error parsing evaluation: {e}")
                # Use default values
                hypothesis.confidence = 0.5
                hypothesis.plausibility_score = 0.5
        
        def explain_reasoning(self, analysis: AbductiveAnalysis) -> str:
            """Generate natural language explanation of reasoning"""
            prompt = ChatPromptTemplate.from_messages([
                ("system", "Explain the abductive reasoning process in clear, natural language."),
                ("user", """Observations:
    {observations}
    
    Best Explanation: {best_explanation}
    Confidence: {confidence:.2%}
    
    Supporting Evidence:
    {supporting}
    
    Explain why this is the best explanation:""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            obs_text = "\n".join([f"- {obs.description}" for obs in analysis.observations])
            supporting_text = "\n".join([f"- {s}" for s in analysis.best_explanation.supporting_evidence])
            
            explanation = chain.invoke({
                "observations": obs_text,
                "best_explanation": analysis.best_explanation.explanation,
                "confidence": analysis.best_explanation.confidence,
                "supporting": supporting_text
            })
            
            return explanation
    
    
    def demonstrate_abductive_reasoning():
        """Demonstrate abductive reasoning pattern"""
        print("=" * 80)
        print("ABDUCTIVE REASONING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = AbductiveReasoningAgent()
        
        # Example 1: Medical diagnosis
        print("\n" + "="*80)
        print("EXAMPLE 1: Medical Diagnosis")
        print("="*80)
        observations = [
            Observation("Patient has a fever of 101°F", importance=0.9),
            Observation("Patient reports sore throat", importance=0.8),
            Observation("Patient has swollen lymph nodes", importance=0.7),
            Observation("Patient has been in contact with sick individuals", importance=0.6)
        ]
        
        analysis = agent.reason(
            observations,
            domain_context="Medical diagnosis",
            num_hypotheses=4
        )
        
        print("Observations:")
        for obs in observations:
            print(f"  - {obs.description}")
        
        print(f"\nGenerated {len(analysis.hypotheses)} hypotheses:")
        for i, hyp in enumerate(analysis.hypotheses, 1):
            print(f"\n{i}. {hyp.explanation}")
            print(f"   Confidence: {hyp.confidence:.2%}")
            print(f"   Plausibility: {hyp.plausibility_score:.2%}")
        
        print(f"\nBest Explanation:")
        print(f"  {analysis.best_explanation.explanation}")
        print(f"  Confidence: {analysis.best_explanation.confidence:.2%}")
        
        # Example 2: Technical troubleshooting
        print("\n" + "="*80)
        print("EXAMPLE 2: Technical Troubleshooting")
        print("="*80)
        observations = [
            Observation("Website is loading slowly", importance=1.0),
            Observation("Database query times are normal", importance=0.8),
            Observation("CPU usage is at 95%", importance=0.9),
            Observation("Recent code deployment yesterday", importance=0.7),
            Observation("Memory usage is normal", importance=0.6)
        ]
        
        analysis = agent.reason(
            observations,
            domain_context="Software system performance troubleshooting",
            num_hypotheses=3
        )
        
        print("Observations:")
        for obs in observations:
            print(f"  - {obs.description}")
        
        print(f"\nTop 3 Hypotheses:")
        for i, hyp in enumerate(analysis.hypotheses[:3], 1):
            print(f"\n{i}. {hyp.explanation}")
            print(f"   Plausibility: {hyp.plausibility_score:.2%}")
            if hyp.supporting_evidence:
                print(f"   Supporting: {', '.join(hyp.supporting_evidence[:2])}")
        
        # Example 3: Mystery scenario
        print("\n" + "="*80)
        print("EXAMPLE 3: Mystery Investigation")
        print("="*80)
        observations = [
            Observation("Window is broken from the inside", importance=0.9),
            Observation("Valuable items are missing", importance=1.0),
            Observation("No signs of forced entry", importance=0.8),
            Observation("Alarm system was deactivated", importance=0.9),
            Observation("Security code was used correctly", importance=0.95)
        ]
        
        analysis = agent.reason(
            observations,
            domain_context="Criminal investigation",
            num_hypotheses=4
        )
        
        print("Observations:")
        for obs in observations:
            print(f"  - {obs.description}")
        
        print(f"\nBest Explanation:")
        best = analysis.best_explanation
        print(f"  {best.explanation}")
        print(f"  Confidence: {best.confidence:.2%}")
        print(f"  Plausibility: {best.plausibility_score:.2%}")
        
        # Generate natural language explanation
        explanation = agent.explain_reasoning(analysis)
        print(f"\nReasoning:")
        print(f"  {explanation[:300]}...")
        
        # Example 4: Business analysis
        print("\n" + "="*80)
        print("EXAMPLE 4: Business Analysis")
        print("="*80)
        observations = [
            Observation("Sales dropped 30% this month", importance=1.0),
            Observation("Competitor launched new product", importance=0.8),
            Observation("Customer complaints increased", importance=0.7),
            Observation("Website traffic is unchanged", importance=0.6),
            Observation("Pricing remained the same", importance=0.5)
        ]
        
        analysis = agent.reason(
            observations,
            domain_context="Business performance analysis",
            num_hypotheses=3
        )
        
        print("Observations:")
        for obs in observations:
            print(f"  - {obs.description}")
        
        print(f"\nHypotheses (ranked by plausibility):")
        for i, hyp in enumerate(analysis.hypotheses, 1):
            print(f"\n{i}. {hyp.explanation}")
            print(f"   Plausibility: {hyp.plausibility_score:.2%}")
        
        # Example 5: Scientific reasoning
        print("\n" + "="*80)
        print("EXAMPLE 5: Scientific Observation")
        print("="*80)
        observations = [
            Observation("Plant leaves are turning yellow", importance=0.9),
            Observation("Soil is consistently wet", importance=0.8),
            Observation("Plant is in low light area", importance=0.7),
            Observation("No visible pests or diseases", importance=0.6)
        ]
        
        analysis = agent.reason(
            observations,
            domain_context="Plant biology and care",
            num_hypotheses=4
        )
        
        print("Observations:")
        for obs in observations:
            print(f"  - {obs.description}")
        
        print(f"\nTop Hypothesis:")
        best = analysis.hypotheses[0]
        print(f"  Explanation: {best.explanation}")
        print(f"  Confidence: {best.confidence:.2%}")
        if best.assumptions:
            print(f"  Assumptions: {', '.join(best.assumptions)}")
        
        # Summary
        print("\n" + "="*80)
        print("SUMMARY: Abductive Reasoning Best Practices")
        print("="*80)
        print("""
    1. OBSERVATION COLLECTION:
       - Gather all available evidence
       - Note importance/relevance of each observation
       - Include timing and context
       - Consider source reliability
    
    2. HYPOTHESIS GENERATION:
       - Generate multiple plausible explanations
       - Consider diverse possibilities
       - Don't prematurely eliminate options
       - Use domain knowledge
    
    3. EVALUATION CRITERIA:
       - Explanatory power (how well it explains observations)
       - Simplicity (Occam's Razor)
       - Consistency with known facts
       - Plausibility given context
    
    4. RANKING FACTORS:
       - Supporting evidence count and quality
       - Contradicting evidence
       - Prior probability
       - Completeness of explanation
    
    5. REASONING PROCESS:
       - Work backwards from observations
       - Consider alternative explanations
       - Evaluate each hypothesis rigorously
       - Update beliefs with new evidence
    
    6. APPLICATIONS:
       - Diagnostic reasoning
       - Root cause analysis
       - Mystery solving
       - Scientific hypothesis formation
       - Decision support systems
    
    Benefits:
    ✓ Finds most likely explanations
    ✓ Handles incomplete information
    ✓ Supports diagnostic tasks
    ✓ Enables inference to best explanation
    ✓ Practical for real-world problems
    
    Limitations:
    - Conclusions not guaranteed correct
    - Multiple explanations may be equally plausible
    - Requires domain knowledge
    - Sensitive to observation quality
    - May miss novel explanations
        """)
    
    
    if __name__ == "__main__":
        demonstrate_abductive_reasoning()
    
        

    Agentic Design Pattern: Abductive Reasoning

    Pattern 159: Abductive Reasoning – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 159: Abductive Reasoning

    Description: Abductive reasoning infers the best explanation for a set of observations. Unlike deductive reasoning (which derives consequences from premises) or inductive reasoning (which generalizes from examples), abduction works backwards from observations to find the most likely cause or explanation.

    Components: – Observation collection – Hypothesis generation – Explanation evaluation – Likelihood ranking – Plausibility assessment

    Use Cases: – Diagnostic reasoning (medical, technical) – Root cause analysis – Mystery solving – Troubleshooting – Scientific hypothesis formation

    Benefits: – Finds likely explanations – Handles incomplete information – Supports decision-making – Enables inference to best explanation

    Trade-offs: – Conclusions not guaranteed – Multiple possible explanations – Requires domain knowledge – Computationally intensive

    LangChain Implementation: Uses ChatOpenAI for hypothesis generation, evaluation scoring, and ranking of explanations by plausibility


    File Statistics

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

    Module Documentation

    """
    Pattern 159: Abductive Reasoning
    
    Description:
        Abductive reasoning infers the best explanation for a set of observations.
        Unlike deductive reasoning (which derives consequences from premises) or
        inductive reasoning (which generalizes from examples), abduction works
        backwards from observations to find the most likely cause or explanation.
    
    Components:
        - Observation collection
        - Hypothesis generation
        - Explanation evaluation
        - Likelihood ranking
        - Plausibility assessment
    
    Use Cases:
        - Diagnostic reasoning (medical, technical)
        - Root cause analysis
        - Mystery solving
        - Troubleshooting
        - Scientific hypothesis formation
    
    Benefits:
        - Finds likely explanations
        - Handles incomplete information
        - Supports decision-making
        - Enables inference to best explanation
    
    Trade-offs:
        - Conclusions not guaranteed
        - Multiple possible explanations
        - Requires domain knowledge
        - Computationally intensive
    
    LangChain Implementation:
        Uses ChatOpenAI for hypothesis generation, evaluation scoring,
        and ranking of explanations by plausibility
    """
    

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


    Import Statements

    Standard Library Imports

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

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – enum.Enum: Enumeration support – 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 – 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: ConfidenceLevel

    Lines 55-61

    Inherits from: Enum

    Purpose:

    Confidence levels for hypotheses
    

    Class: Observation

    Lines 65-70

    Purpose:

    Single observation or evidence
    

    Class: Hypothesis

    Lines 74-81

    Purpose:

    Potential explanation
    

    Class: AbductiveAnalysis

    Lines 85-90

    Purpose:

    Result of abductive reasoning
    

    Class: AbductiveReasoningAgent

    Lines 93-323

    Purpose:

    Agent that performs abductive reasoning
    

    Methods:

    __init__(self, model_name)

    Lines 96-104

    Purpose:

    Initialize abductive reasoning agent
    
    Args:
        model_name: LLM model to use
    

    Parameters: – model_name: Language model instance

    reason(self, observations, domain_context, num_hypotheses)

    Lines 106-144

    Purpose:

    Perform abductive reasoning on observations
    
    Args:
        observations: List of observations
        domain_context: Optional domain-specific context
        num_hypotheses: Number of hypotheses to generate
    
    Returns:
        AbductiveAnalysis with ranked hypotheses
    

    Parameters: – observations: Observations – domain_context: Domain context – num_hypotheses: Num hypotheses

    Returns: AbductiveAnalysis

    _generate_hypotheses(self, observations, domain_context, num_hypotheses)

    Lines 146-183

    Purpose:

    Generate potential hypotheses
    

    Parameters: – observations: Observations – domain_context: Domain context – num_hypotheses: Num hypotheses

    Returns: <ast.Subscript object at 0x000001A06E0AB450>

    _parse_hypotheses(self, response)

    Lines 185-238

    Purpose:

    Parse hypotheses from LLM response
    

    Parameters: – response: Response

    Returns: <ast.Subscript object at 0x000001A06D6304D0>

    _evaluate_hypothesis(self, hypothesis, observations)

    Lines 240-293

    Purpose:

    Evaluate a hypothesis against observations
    

    Parameters: – hypothesis: Hypothesis – observations: Observations

    explain_reasoning(self, analysis)

    Lines 295-323

    Purpose:

    Generate natural language explanation of reasoning
    

    Parameters: – analysis: Analysis

    Returns: str


    Functions

    demonstrate_abductive_reasoning()

    Lines 326-537

    Purpose:

    Demonstrate abductive reasoning pattern
    

    Key Concepts Summary

    Pattern Overview

    The Abductive Reasoning 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 158
    • Pattern 160

    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 Abductive Reasoning 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: 159_abductive_reasoning.py
    Total Lines: 542
    Pattern Category: AI Agent Design Pattern #159


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Plugin Extension

    
        """
    Pattern 158: Plugin/Extension Architecture
    
    Description:
        Enables extensibility through a plugin system where new capabilities can be
        added dynamically without modifying core code. Plugins follow a standard
        interface and can be loaded, configured, and executed at runtime.
    
    Components:
        - Plugin interface/protocol
        - Plugin registry
        - Plugin loader
        - Lifecycle management
        - Dependency injection
        - Configuration system
    
    Use Cases:
        - Custom tool integration
        - Domain-specific capabilities
        - Third-party extensions
        - Modular agent systems
        - Dynamic feature addition
    
    Benefits:
        - Extensibility without core changes
        - Modular architecture
        - Third-party contributions
        - Dynamic capability loading
        - Separation of concerns
    
    Trade-offs:
        - Complexity overhead
        - Plugin management burden
        - Potential security risks
        - Version compatibility issues
    
    LangChain Implementation:
        Uses protocol classes for interfaces, dynamic loading,
        registry pattern, and lifecycle hooks
    """
    
    import os
    import importlib
    import inspect
    from typing import Dict, Any, List, Optional, Callable, Protocol
    from dataclasses import dataclass, field
    from enum import Enum
    from abc import ABC, abstractmethod
    from dotenv import load_dotenv
    
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    
    load_dotenv()
    
    
    class PluginStatus(Enum):
        """Plugin status"""
        UNLOADED = "unloaded"
        LOADED = "loaded"
        INITIALIZED = "initialized"
        ACTIVE = "active"
        DISABLED = "disabled"
        ERROR = "error"
    
    
    class PluginType(Enum):
        """Plugin types"""
        TOOL = "tool"
        PROCESSOR = "processor"
        FILTER = "filter"
        ANALYZER = "analyzer"
        GENERATOR = "generator"
    
    
    @dataclass
    class PluginMetadata:
        """Plugin metadata"""
        name: str
        version: str
        author: str
        description: str
        plugin_type: PluginType
        dependencies: List[str] = field(default_factory=list)
        config_schema: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class PluginConfig:
        """Plugin configuration"""
        enabled: bool = True
        priority: int = 0
        settings: Dict[str, Any] = field(default_factory=dict)
    
    
    class Plugin(Protocol):
        """Plugin interface using Protocol (structural typing)"""
        
        @property
        def metadata(self) -> PluginMetadata:
            """Get plugin metadata"""
            ...
        
        def initialize(self, config: PluginConfig) -> bool:
            """Initialize plugin"""
            ...
        
        def execute(self, input_data: Any, context: Dict[str, Any]) -> Any:
            """Execute plugin functionality"""
            ...
        
        def shutdown(self) -> bool:
            """Cleanup and shutdown"""
            ...
    
    
    class BasePlugin(ABC):
        """Base class for plugins"""
        
        def __init__(self):
            """Initialize base plugin"""
            self.config: Optional[PluginConfig] = None
            self.status = PluginStatus.UNLOADED
            self._metadata: Optional[PluginMetadata] = None
        
        @property
        def metadata(self) -> PluginMetadata:
            """Get plugin metadata"""
            if self._metadata is None:
                self._metadata = self._create_metadata()
            return self._metadata
        
        @abstractmethod
        def _create_metadata(self) -> PluginMetadata:
            """Create plugin metadata"""
            pass
        
        def initialize(self, config: PluginConfig) -> bool:
            """Initialize plugin"""
            try:
                self.config = config
                self._on_initialize()
                self.status = PluginStatus.INITIALIZED
                if config.enabled:
                    self.status = PluginStatus.ACTIVE
                return True
            except Exception as e:
                self.status = PluginStatus.ERROR
                print(f"Plugin initialization failed: {e}")
                return False
        
        def _on_initialize(self):
            """Override for custom initialization"""
            pass
        
        @abstractmethod
        def execute(self, input_data: Any, context: Dict[str, Any]) -> Any:
            """Execute plugin functionality"""
            pass
        
        def shutdown(self) -> bool:
            """Cleanup and shutdown"""
            try:
                self._on_shutdown()
                self.status = PluginStatus.UNLOADED
                return True
            except Exception as e:
                print(f"Plugin shutdown failed: {e}")
                return False
        
        def _on_shutdown(self):
            """Override for custom shutdown"""
            pass
    
    
    # Example Plugin Implementations
    
    class SentimentAnalysisPlugin(BasePlugin):
        """Plugin for sentiment analysis"""
        
        def _create_metadata(self) -> PluginMetadata:
            return PluginMetadata(
                name="sentiment_analyzer",
                version="1.0.0",
                author="Example Corp",
                description="Analyzes sentiment of text using LLM",
                plugin_type=PluginType.ANALYZER
            )
        
        def _on_initialize(self):
            """Initialize LLM"""
            self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
        
        def execute(self, input_data: Any, context: Dict[str, Any]) -> Any:
            """Analyze sentiment"""
            text = input_data if isinstance(input_data, str) else str(input_data)
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "Analyze the sentiment of the given text. Respond with: positive, negative, or neutral."),
                ("user", "{text}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            sentiment = chain.invoke({"text": text})
            
            return {
                "sentiment": sentiment.strip().lower(),
                "text": text,
                "plugin": self.metadata.name
            }
    
    
    class TextSummarizerPlugin(BasePlugin):
        """Plugin for text summarization"""
        
        def _create_metadata(self) -> PluginMetadata:
            return PluginMetadata(
                name="text_summarizer",
                version="1.0.0",
                author="Example Corp",
                description="Summarizes text content",
                plugin_type=PluginType.PROCESSOR
            )
        
        def _on_initialize(self):
            """Initialize LLM"""
            max_words = self.config.settings.get("max_words", 50) if self.config else 50
            self.max_words = max_words
            self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
        
        def execute(self, input_data: Any, context: Dict[str, Any]) -> Any:
            """Summarize text"""
            text = input_data if isinstance(input_data, str) else str(input_data)
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "Summarize the following text in {max_words} words or less."),
                ("user", "{text}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            summary = chain.invoke({"text": text, "max_words": self.max_words})
            
            return {
                "summary": summary,
                "original_length": len(text.split()),
                "summary_length": len(summary.split()),
                "plugin": self.metadata.name
            }
    
    
    class KeywordExtractorPlugin(BasePlugin):
        """Plugin for keyword extraction"""
        
        def _create_metadata(self) -> PluginMetadata:
            return PluginMetadata(
                name="keyword_extractor",
                version="1.0.0",
                author="Example Corp",
                description="Extracts keywords from text",
                plugin_type=PluginType.ANALYZER
            )
        
        def _on_initialize(self):
            """Initialize LLM"""
            self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
        
        def execute(self, input_data: Any, context: Dict[str, Any]) -> Any:
            """Extract keywords"""
            text = input_data if isinstance(input_data, str) else str(input_data)
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "Extract 5-7 key terms or phrases from the text. Return as comma-separated list."),
                ("user", "{text}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            keywords_str = chain.invoke({"text": text})
            
            keywords = [k.strip() for k in keywords_str.split(",")]
            
            return {
                "keywords": keywords,
                "count": len(keywords),
                "plugin": self.metadata.name
            }
    
    
    class ProfanityFilterPlugin(BasePlugin):
        """Plugin for filtering profanity"""
        
        def _create_metadata(self) -> PluginMetadata:
            return PluginMetadata(
                name="profanity_filter",
                version="1.0.0",
                author="Example Corp",
                description="Filters inappropriate content",
                plugin_type=PluginType.FILTER
            )
        
        def _on_initialize(self):
            """Initialize filter"""
            self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
        
        def execute(self, input_data: Any, context: Dict[str, Any]) -> Any:
            """Filter content"""
            text = input_data if isinstance(input_data, str) else str(input_data)
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "Analyze if the text contains profanity or inappropriate content. Respond with 'yes' or 'no'."),
                ("user", "{text}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            contains_profanity = chain.invoke({"text": text})
            
            is_inappropriate = "yes" in contains_profanity.lower()
            
            return {
                "is_appropriate": not is_inappropriate,
                "filtered": is_inappropriate,
                "text": text if not is_inappropriate else "[CONTENT FILTERED]",
                "plugin": self.metadata.name
            }
    
    
    class PluginRegistry:
        """Manages plugin registration and lifecycle"""
        
        def __init__(self):
            """Initialize plugin registry"""
            self.plugins: Dict[str, BasePlugin] = {}
            self.configs: Dict[str, PluginConfig] = {}
            self.execution_order: List[str] = []
        
        def register_plugin(self, plugin: BasePlugin, config: Optional[PluginConfig] = None):
            """Register a plugin"""
            if config is None:
                config = PluginConfig()
            
            plugin_name = plugin.metadata.name
            
            # Check dependencies
            for dep in plugin.metadata.dependencies:
                if dep not in self.plugins:
                    print(f"Warning: Missing dependency '{dep}' for plugin '{plugin_name}'")
            
            # Register plugin
            self.plugins[plugin_name] = plugin
            self.configs[plugin_name] = config
            
            # Initialize plugin
            success = plugin.initialize(config)
            
            if success:
                # Add to execution order based on priority
                self._update_execution_order(plugin_name, config.priority)
                print(f"✓ Registered and initialized plugin: {plugin_name}")
            else:
                print(f"✗ Failed to initialize plugin: {plugin_name}")
            
            return success
        
        def _update_execution_order(self, plugin_name: str, priority: int):
            """Update execution order based on priority"""
            if plugin_name in self.execution_order:
                self.execution_order.remove(plugin_name)
            
            # Insert based on priority (higher priority first)
            inserted = False
            for i, name in enumerate(self.execution_order):
                if self.configs[name].priority  bool:
            """Unregister a plugin"""
            if plugin_name not in self.plugins:
                return False
            
            plugin = self.plugins[plugin_name]
            plugin.shutdown()
            
            del self.plugins[plugin_name]
            del self.configs[plugin_name]
            self.execution_order.remove(plugin_name)
            
            print(f"✓ Unregistered plugin: {plugin_name}")
            return True
        
        def get_plugin(self, plugin_name: str) -> Optional[BasePlugin]:
            """Get plugin by name"""
            return self.plugins.get(plugin_name)
        
        def execute_plugin(self, plugin_name: str, input_data: Any, 
                          context: Optional[Dict[str, Any]] = None) -> Any:
            """Execute specific plugin"""
            if context is None:
                context = {}
            
            plugin = self.plugins.get(plugin_name)
            if not plugin:
                raise ValueError(f"Plugin not found: {plugin_name}")
            
            if plugin.status != PluginStatus.ACTIVE:
                raise RuntimeError(f"Plugin not active: {plugin_name}")
            
            return plugin.execute(input_data, context)
        
        def execute_pipeline(self, input_data: Any, 
                            plugin_types: Optional[List[PluginType]] = None,
                            context: Optional[Dict[str, Any]] = None) -> Any:
            """Execute plugins in pipeline"""
            if context is None:
                context = {}
            
            result = input_data
            
            for plugin_name in self.execution_order:
                plugin = self.plugins[plugin_name]
                config = self.configs[plugin_name]
                
                # Skip if not enabled
                if not config.enabled or plugin.status != PluginStatus.ACTIVE:
                    continue
                
                # Filter by type if specified
                if plugin_types and plugin.metadata.plugin_type not in plugin_types:
                    continue
                
                # Execute plugin
                result = plugin.execute(result, context)
            
            return result
        
        def list_plugins(self) -> List[Dict[str, Any]]:
            """List all registered plugins"""
            return [
                {
                    "name": plugin.metadata.name,
                    "version": plugin.metadata.version,
                    "type": plugin.metadata.plugin_type.value,
                    "status": plugin.status.value,
                    "enabled": self.configs[name].enabled,
                    "priority": self.configs[name].priority
                }
                for name, plugin in self.plugins.items()
            ]
        
        def shutdown_all(self):
            """Shutdown all plugins"""
            for plugin_name in list(self.plugins.keys()):
                self.unregister_plugin(plugin_name)
    
    
    def demonstrate_plugin_architecture():
        """Demonstrate plugin/extension architecture"""
        print("=" * 80)
        print("PLUGIN/EXTENSION ARCHITECTURE DEMONSTRATION")
        print("=" * 80)
        
        # Initialize registry
        registry = PluginRegistry()
        
        # Example 1: Register plugins
        print("\n" + "="*80)
        print("EXAMPLE 1: Registering Plugins")
        print("="*80)
        
        # Register sentiment analyzer
        sentiment_plugin = SentimentAnalysisPlugin()
        registry.register_plugin(sentiment_plugin, PluginConfig(
            enabled=True,
            priority=10
        ))
        
        # Register summarizer
        summarizer_plugin = TextSummarizerPlugin()
        registry.register_plugin(summarizer_plugin, PluginConfig(
            enabled=True,
            priority=5,
            settings={"max_words": 30}
        ))
        
        # Register keyword extractor
        keyword_plugin = KeywordExtractorPlugin()
        registry.register_plugin(keyword_plugin, PluginConfig(
            enabled=True,
            priority=3
        ))
        
        # Register profanity filter
        filter_plugin = ProfanityFilterPlugin()
        registry.register_plugin(filter_plugin, PluginConfig(
            enabled=True,
            priority=20  # High priority - run first
        ))
        
        # Example 2: List plugins
        print("\n" + "="*80)
        print("EXAMPLE 2: List Registered Plugins")
        print("="*80)
        plugins = registry.list_plugins()
        for plugin in plugins:
            print(f"- {plugin['name']} v{plugin['version']}")
            print(f"  Type: {plugin['type']}, Status: {plugin['status']}")
            print(f"  Priority: {plugin['priority']}, Enabled: {plugin['enabled']}")
        
        # Example 3: Execute individual plugin
        print("\n" + "="*80)
        print("EXAMPLE 3: Execute Individual Plugin")
        print("="*80)
        text = "This product is amazing! I love it so much!"
        result = registry.execute_plugin("sentiment_analyzer", text)
        print(f"Input: {text}")
        print(f"Result: {result}")
        
        # Example 4: Execute multiple plugins
        print("\n" + "="*80)
        print("EXAMPLE 4: Execute Multiple Plugins")
        print("="*80)
        text = "Artificial intelligence is transforming how we work and live. Machine learning models can now process vast amounts of data and make predictions with high accuracy."
        
        print(f"Input: {text[:100]}...\n")
        
        # Sentiment
        sentiment_result = registry.execute_plugin("sentiment_analyzer", text)
        print(f"Sentiment: {sentiment_result['sentiment']}")
        
        # Summary
        summary_result = registry.execute_plugin("text_summarizer", text)
        print(f"Summary: {summary_result['summary']}")
        
        # Keywords
        keyword_result = registry.execute_plugin("keyword_extractor", text)
        print(f"Keywords: {', '.join(keyword_result['keywords'])}")
        
        # Example 5: Filter by plugin type
        print("\n" + "="*80)
        print("EXAMPLE 5: Execute by Plugin Type")
        print("="*80)
        text = "The company announced record profits this quarter."
        
        # Execute only analyzers
        print("Executing ANALYZER plugins:")
        result = registry.execute_pipeline(text, plugin_types=[PluginType.ANALYZER])
        print(f"Result: {result}")
        
        # Example 6: Content filtering
        print("\n" + "="*80)
        print("EXAMPLE 6: Content Filtering")
        print("="*80)
        clean_text = "This is a nice and appropriate message."
        result = registry.execute_plugin("profanity_filter", clean_text)
        print(f"Text: {clean_text}")
        print(f"Is Appropriate: {result['is_appropriate']}")
        print(f"Output: {result['text']}")
        
        # Example 7: Execution order (priority)
        print("\n" + "="*80)
        print("EXAMPLE 7: Plugin Execution Order")
        print("="*80)
        print("Execution order (by priority):")
        for i, plugin_name in enumerate(registry.execution_order, 1):
            plugin = registry.get_plugin(plugin_name)
            priority = registry.configs[plugin_name].priority
            print(f"{i}. {plugin_name} (priority: {priority})")
        
        # Cleanup
        print("\n" + "="*80)
        print("CLEANUP: Shutting Down Plugins")
        print("="*80)
        registry.shutdown_all()
        
        # Summary
        print("\n" + "="*80)
        print("SUMMARY: Plugin Architecture Best Practices")
        print("="*80)
        print("""
    1. PLUGIN INTERFACE:
       - Define clear plugin interface/protocol
       - Standardize lifecycle methods (initialize, execute, shutdown)
       - Use metadata for plugin description
       - Support configuration schemas
    
    2. REGISTRY MANAGEMENT:
       - Centralized plugin registration
       - Dependency checking
       - Priority-based execution
       - Enable/disable plugins dynamically
    
    3. LIFECYCLE MANAGEMENT:
       - Proper initialization
       - Resource cleanup on shutdown
       - Error handling and status tracking
       - Graceful failure handling
    
    4. EXECUTION PATTERNS:
       - Individual plugin execution
       - Pipeline execution
       - Type-based filtering
       - Context passing between plugins
    
    5. CONFIGURATION:
       - Plugin-specific settings
       - Priority control
       - Enable/disable flags
       - Runtime configuration updates
    
    6. EXTENSIBILITY:
       - Easy to add new plugins
       - No core code modification
       - Third-party plugin support
       - Hot-loading capabilities (advanced)
    
    Benefits:
    ✓ Modular and extensible architecture
    ✓ Separation of concerns
    ✓ Easy feature addition
    ✓ Third-party contributions
    ✓ Dynamic capability enhancement
    ✓ Clean core codebase
    
    Challenges:
    - Plugin management complexity
    - Version compatibility
    - Security considerations for third-party plugins
    - Performance overhead
    - Dependency management
    - Testing across plugin combinations
        """)
    
    
    if __name__ == "__main__":
        demonstrate_plugin_architecture()
    
        

    Agentic Design Pattern: Plugin Extension

    Pattern 158: Plugin Extension – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 158: Plugin/Extension Architecture

    Description: Enables extensibility through a plugin system where new capabilities can be added dynamically without modifying core code. Plugins follow a standard interface and can be loaded, configured, and executed at runtime.

    Components: – Plugin interface/protocol – Plugin registry – Plugin loader – Lifecycle management – Dependency injection – Configuration system

    Use Cases: – Custom tool integration – Domain-specific capabilities – Third-party extensions – Modular agent systems – Dynamic feature addition

    Benefits: – Extensibility without core changes – Modular architecture – Third-party contributions – Dynamic capability loading – Separation of concerns

    Trade-offs: – Complexity overhead – Plugin management burden – Potential security risks – Version compatibility issues

    LangChain Implementation: Uses protocol classes for interfaces, dynamic loading, registry pattern, and lifecycle hooks


    File Statistics

    • Total Lines: 640
    • Classes: 11
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 158: Plugin/Extension Architecture
    
    Description:
        Enables extensibility through a plugin system where new capabilities can be
        added dynamically without modifying core code. Plugins follow a standard
        interface and can be loaded, configured, and executed at runtime.
    
    Components:
        - Plugin interface/protocol
        - Plugin registry
        - Plugin loader
        - Lifecycle management
        - Dependency injection
        - Configuration system
    
    Use Cases:
        - Custom tool integration
        - Domain-specific capabilities
        - Third-party extensions
        - Modular agent systems
        - Dynamic feature addition
    
    Benefits:
        - Extensibility without core changes
        - Modular architecture
        - Third-party contributions
        - Dynamic capability loading
        - Separation of concerns
    
    Trade-offs:
        - Complexity overhead
        - Plugin management burden
        - Potential security risks
        - Version compatibility issues
    
    LangChain Implementation:
        Uses protocol classes for interfaces, dynamic loading,
        registry pattern, and lifecycle hooks
    """
    

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


    Import Statements

    Standard Library Imports

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

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – enum.Enum: Enumeration support – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.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.Protocol: 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 abc.ABC
    from abc.abstractmethod
    from dotenv.load_dotenv
    from importlib
    from inspect
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: PluginStatus

    Lines 58-65

    Inherits from: Enum

    Purpose:

    Plugin status
    

    Class: PluginType

    Lines 68-74

    Inherits from: Enum

    Purpose:

    Plugin types
    

    Class: PluginMetadata

    Lines 78-86

    Purpose:

    Plugin metadata
    

    Class: PluginConfig

    Lines 90-94

    Purpose:

    Plugin configuration
    

    Class: Plugin

    Lines 97-115

    Inherits from: Protocol

    Purpose:

    Plugin interface using Protocol (structural typing)
    

    Methods:

    metadata(self)

    Lines 101-103

    Decorators: property

    Purpose:

    Get plugin metadata
    

    Parameters:

    Returns: PluginMetadata

    initialize(self, config)

    Lines 105-107

    Purpose:

    Initialize plugin
    

    Parameters: – config: Config

    Returns: bool

    execute(self, input_data, context)

    Lines 109-111

    Purpose:

    Execute plugin functionality
    

    Parameters: – input_data: Input data – context: Context

    Returns: Any

    shutdown(self)

    Lines 113-115

    Purpose:

    Cleanup and shutdown
    

    Parameters:

    Returns: bool


    Class: BasePlugin

    Lines 118-174

    Inherits from: ABC

    Purpose:

    Base class for plugins
    

    Methods:

    __init__(self)

    Lines 121-125

    Purpose:

    Initialize base plugin
    

    Parameters:

    metadata(self)

    Lines 128-132

    Decorators: property

    Purpose:

    Get plugin metadata
    

    Parameters:

    Returns: PluginMetadata

    _create_metadata(self)

    Lines 135-137

    Decorators: abstractmethod

    Purpose:

    Create plugin metadata
    

    Parameters:

    Returns: PluginMetadata

    initialize(self, config)

    Lines 139-151

    Purpose:

    Initialize plugin
    

    Parameters: – config: Config

    Returns: bool

    _on_initialize(self)

    Lines 153-155

    Purpose:

    Override for custom initialization
    

    Parameters:

    execute(self, input_data, context)

    Lines 158-160

    Decorators: abstractmethod

    Purpose:

    Execute plugin functionality
    

    Parameters: – input_data: Input data – context: Context

    Returns: Any

    shutdown(self)

    Lines 162-170

    Purpose:

    Cleanup and shutdown
    

    Parameters:

    Returns: bool

    _on_shutdown(self)

    Lines 172-174

    Purpose:

    Override for custom shutdown
    

    Parameters:


    Class: SentimentAnalysisPlugin

    Lines 179-211

    Inherits from: BasePlugin

    Purpose:

    Plugin for sentiment analysis
    

    Methods:

    _create_metadata(self)

    Lines 182-189

    Purpose: Create and return a new instance or configuration.

    Parameters:

    Returns: PluginMetadata

    _on_initialize(self)

    Lines 191-193

    Purpose:

    Initialize LLM
    

    Parameters:

    execute(self, input_data, context)

    Lines 195-211

    Purpose:

    Analyze sentiment
    

    Parameters: – input_data: Input data – context: Context

    Returns: Any


    Class: TextSummarizerPlugin

    Lines 214-249

    Inherits from: BasePlugin

    Purpose:

    Plugin for text summarization
    

    Methods:

    _create_metadata(self)

    Lines 217-224

    Purpose: Create and return a new instance or configuration.

    Parameters:

    Returns: PluginMetadata

    _on_initialize(self)

    Lines 226-230

    Purpose:

    Initialize LLM
    

    Parameters:

    execute(self, input_data, context)

    Lines 232-249

    Purpose:

    Summarize text
    

    Parameters: – input_data: Input data – context: Context

    Returns: Any


    Class: KeywordExtractorPlugin

    Lines 252-286

    Inherits from: BasePlugin

    Purpose:

    Plugin for keyword extraction
    

    Methods:

    _create_metadata(self)

    Lines 255-262

    Purpose: Create and return a new instance or configuration.

    Parameters:

    Returns: PluginMetadata

    _on_initialize(self)

    Lines 264-266

    Purpose:

    Initialize LLM
    

    Parameters:

    execute(self, input_data, context)

    Lines 268-286

    Purpose:

    Extract keywords
    

    Parameters: – input_data: Input data – context: Context

    Returns: Any


    Class: ProfanityFilterPlugin

    Lines 289-324

    Inherits from: BasePlugin

    Purpose:

    Plugin for filtering profanity
    

    Methods:

    _create_metadata(self)

    Lines 292-299

    Purpose: Create and return a new instance or configuration.

    Parameters:

    Returns: PluginMetadata

    _on_initialize(self)

    Lines 301-303

    Purpose:

    Initialize filter
    

    Parameters:

    execute(self, input_data, context)

    Lines 305-324

    Purpose:

    Filter content
    

    Parameters: – input_data: Input data – context: Context

    Returns: Any


    Class: PluginRegistry

    Lines 327-457

    Purpose:

    Manages plugin registration and lifecycle
    

    Methods:

    __init__(self)

    Lines 330-334

    Purpose:

    Initialize plugin registry
    

    Parameters:

    register_plugin(self, plugin, config)

    Lines 336-362

    Purpose:

    Register a plugin
    

    Parameters: – plugin: Plugin – config: Config

    _update_execution_order(self, plugin_name, priority)

    Lines 364-378

    Purpose:

    Update execution order based on priority
    

    Parameters: – plugin_name: Plugin name – priority: Priority

    unregister_plugin(self, plugin_name)

    Lines 380-393

    Purpose:

    Unregister a plugin
    

    Parameters: – plugin_name: Plugin name

    Returns: bool

    get_plugin(self, plugin_name)

    Lines 395-397

    Purpose:

    Get plugin by name
    

    Parameters: – plugin_name: Plugin name

    Returns: <ast.Subscript object at 0x000001A06D7CC850>

    execute_plugin(self, plugin_name, input_data, context)

    Lines 399-412

    Purpose:

    Execute specific plugin
    

    Parameters: – plugin_name: Plugin name – input_data: Input data – context: Context

    Returns: Any

    execute_pipeline(self, input_data, plugin_types, context)

    Lines 414-438

    Purpose:

    Execute plugins in pipeline
    

    Parameters: – input_data: Input data – plugin_types: Plugin types – context: Context

    Returns: Any

    list_plugins(self)

    Lines 440-452

    Purpose:

    List all registered plugins
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D6F6290>

    shutdown_all(self)

    Lines 454-457

    Purpose:

    Shutdown all plugins
    

    Parameters:


    Functions

    demonstrate_plugin_architecture()

    Lines 460-635

    Purpose:

    Demonstrate plugin/extension architecture
    

    Key Concepts Summary

    Pattern Overview

    The Plugin Extension 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 157
    • Pattern 159

    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 Plugin Extension 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: 158_plugin_extension.py
    Total Lines: 640
    Pattern Category: AI Agent Design Pattern #158


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Adapter Wrapper

    
        """
    Pattern 157: Adapter/Wrapper Pattern
    
    Description:
        Provides unified interfaces to different LLM providers and models, abstracting
        away provider-specific details. Enables easy switching between models without
        changing application code.
    
    Components:
        - Base interface definition
        - Provider-specific adapters
        - Response normalization
        - Error handling translation
        - Configuration management
    
    Use Cases:
        - Multi-provider support
        - Model switching
        - Vendor independence
        - Testing with different models
        - Cost optimization
    
    Benefits:
        - Provider independence
        - Simplified integration
        - Easy model switching
        - Consistent interface
        - Testability
    
    Trade-offs:
        - Abstraction overhead
        - May limit provider-specific features
        - Mapping complexity
        - Performance overhead
    
    LangChain Implementation:
        Uses abstract base classes, provider-specific implementations,
        and normalization layers for consistent responses
    """
    
    import os
    from typing import Dict, Any, List, Optional, Protocol
    from dataclasses import dataclass
    from abc import ABC, abstractmethod
    from enum import Enum
    from dotenv import load_dotenv
    
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
    
    load_dotenv()
    
    
    class ModelProvider(Enum):
        """Supported model providers"""
        OPENAI = "openai"
        ANTHROPIC = "anthropic"
        GOOGLE = "google"
        LOCAL = "local"
    
    
    @dataclass
    class ModelConfig:
        """Model configuration"""
        provider: ModelProvider
        model_name: str
        temperature: float = 0.7
        max_tokens: Optional[int] = None
        api_key: Optional[str] = None
    
    
    @dataclass
    class Message:
        """Standardized message format"""
        role: str  # system, user, assistant
        content: str
        metadata: Dict[str, Any] = None
        
        def __post_init__(self):
            if self.metadata is None:
                self.metadata = {}
    
    
    @dataclass
    class CompletionResponse:
        """Standardized completion response"""
        content: str
        model: str
        provider: ModelProvider
        tokens_used: Optional[int] = None
        finish_reason: Optional[str] = None
        metadata: Dict[str, Any] = None
        
        def __post_init__(self):
            if self.metadata is None:
                self.metadata = {}
    
    
    class LLMAdapter(ABC):
        """Abstract base class for LLM adapters"""
        
        @abstractmethod
        def complete(self, messages: List[Message], **kwargs) -> CompletionResponse:
            """Generate completion from messages"""
            pass
        
        @abstractmethod
        def stream(self, messages: List[Message], **kwargs):
            """Stream completion"""
            pass
        
        @abstractmethod
        def get_model_info(self) -> Dict[str, Any]:
            """Get model information"""
            pass
    
    
    class OpenAIAdapter(LLMAdapter):
        """Adapter for OpenAI models"""
        
        def __init__(self, config: ModelConfig):
            """Initialize OpenAI adapter"""
            self.config = config
            self.llm = ChatOpenAI(
                model=config.model_name,
                temperature=config.temperature,
                max_tokens=config.max_tokens,
                api_key=config.api_key or os.getenv("OPENAI_API_KEY")
            )
        
        def complete(self, messages: List[Message], **kwargs) -> CompletionResponse:
            """Generate completion"""
            # Convert messages to LangChain format
            lc_messages = self._convert_messages(messages)
            
            try:
                # Generate response
                response = self.llm.invoke(lc_messages)
                
                return CompletionResponse(
                    content=response.content,
                    model=self.config.model_name,
                    provider=ModelProvider.OPENAI,
                    finish_reason="stop",
                    metadata={
                        "response_type": type(response).__name__
                    }
                )
            except Exception as e:
                raise RuntimeError(f"OpenAI completion failed: {str(e)}")
        
        def stream(self, messages: List[Message], **kwargs):
            """Stream completion"""
            lc_messages = self._convert_messages(messages)
            
            try:
                for chunk in self.llm.stream(lc_messages):
                    yield chunk.content
            except Exception as e:
                raise RuntimeError(f"OpenAI streaming failed: {str(e)}")
        
        def get_model_info(self) -> Dict[str, Any]:
            """Get model information"""
            return {
                "provider": ModelProvider.OPENAI.value,
                "model": self.config.model_name,
                "temperature": self.config.temperature,
                "max_tokens": self.config.max_tokens
            }
        
        def _convert_messages(self, messages: List[Message]) -> List[BaseMessage]:
            """Convert standard messages to LangChain messages"""
            lc_messages = []
            for msg in messages:
                if msg.role == "system":
                    lc_messages.append(SystemMessage(content=msg.content))
                elif msg.role == "user":
                    lc_messages.append(HumanMessage(content=msg.content))
                elif msg.role == "assistant":
                    lc_messages.append(AIMessage(content=msg.content))
            return lc_messages
    
    
    class AnthropicAdapter(LLMAdapter):
        """Adapter for Anthropic models (simulated)"""
        
        def __init__(self, config: ModelConfig):
            """Initialize Anthropic adapter"""
            self.config = config
            # In real implementation, use actual Anthropic client
            # For demo, we'll use OpenAI as fallback
            self.llm = ChatOpenAI(
                model="gpt-3.5-turbo",
                temperature=config.temperature
            )
        
        def complete(self, messages: List[Message], **kwargs) -> CompletionResponse:
            """Generate completion"""
            # Convert to format expected by Anthropic (simulated)
            prompt = self._format_anthropic_prompt(messages)
            
            try:
                response = self.llm.invoke([HumanMessage(content=prompt)])
                
                return CompletionResponse(
                    content=response.content,
                    model=self.config.model_name,
                    provider=ModelProvider.ANTHROPIC,
                    finish_reason="end_turn",
                    metadata={
                        "simulated": True
                    }
                )
            except Exception as e:
                raise RuntimeError(f"Anthropic completion failed: {str(e)}")
        
        def stream(self, messages: List[Message], **kwargs):
            """Stream completion"""
            prompt = self._format_anthropic_prompt(messages)
            
            try:
                for chunk in self.llm.stream([HumanMessage(content=prompt)]):
                    yield chunk.content
            except Exception as e:
                raise RuntimeError(f"Anthropic streaming failed: {str(e)}")
        
        def get_model_info(self) -> Dict[str, Any]:
            """Get model information"""
            return {
                "provider": ModelProvider.ANTHROPIC.value,
                "model": self.config.model_name,
                "temperature": self.config.temperature,
                "simulated": True
            }
        
        def _format_anthropic_prompt(self, messages: List[Message]) -> str:
            """Format messages for Anthropic API"""
            # Anthropic uses a different format
            formatted = []
            for msg in messages:
                if msg.role == "system":
                    formatted.append(f"System: {msg.content}")
                elif msg.role == "user":
                    formatted.append(f"Human: {msg.content}")
                elif msg.role == "assistant":
                    formatted.append(f"Assistant: {msg.content}")
            formatted.append("Assistant:")
            return "\n\n".join(formatted)
    
    
    class GoogleAdapter(LLMAdapter):
        """Adapter for Google models (simulated)"""
        
        def __init__(self, config: ModelConfig):
            """Initialize Google adapter"""
            self.config = config
            # In real implementation, use actual Google client
            self.llm = ChatOpenAI(
                model="gpt-3.5-turbo",
                temperature=config.temperature
            )
        
        def complete(self, messages: List[Message], **kwargs) -> CompletionResponse:
            """Generate completion"""
            # Convert to Google format (simulated)
            lc_messages = []
            for msg in messages:
                if msg.role == "system":
                    lc_messages.append(SystemMessage(content=msg.content))
                elif msg.role == "user":
                    lc_messages.append(HumanMessage(content=msg.content))
            
            try:
                response = self.llm.invoke(lc_messages)
                
                return CompletionResponse(
                    content=response.content,
                    model=self.config.model_name,
                    provider=ModelProvider.GOOGLE,
                    finish_reason="STOP",
                    metadata={
                        "simulated": True
                    }
                )
            except Exception as e:
                raise RuntimeError(f"Google completion failed: {str(e)}")
        
        def stream(self, messages: List[Message], **kwargs):
            """Stream completion"""
            lc_messages = []
            for msg in messages:
                if msg.role == "user":
                    lc_messages.append(HumanMessage(content=msg.content))
            
            try:
                for chunk in self.llm.stream(lc_messages):
                    yield chunk.content
            except Exception as e:
                raise RuntimeError(f"Google streaming failed: {str(e)}")
        
        def get_model_info(self) -> Dict[str, Any]:
            """Get model information"""
            return {
                "provider": ModelProvider.GOOGLE.value,
                "model": self.config.model_name,
                "temperature": self.config.temperature,
                "simulated": True
            }
    
    
    class UnifiedLLMInterface:
        """Unified interface for multiple LLM providers"""
        
        def __init__(self):
            """Initialize unified interface"""
            self.adapters: Dict[ModelProvider, LLMAdapter] = {}
            self.default_provider: Optional[ModelProvider] = None
        
        def register_adapter(self, provider: ModelProvider, adapter: LLMAdapter):
            """Register a provider adapter"""
            self.adapters[provider] = adapter
            if self.default_provider is None:
                self.default_provider = provider
            print(f"✓ Registered adapter for {provider.value}")
        
        def set_default_provider(self, provider: ModelProvider):
            """Set default provider"""
            if provider not in self.adapters:
                raise ValueError(f"Provider {provider.value} not registered")
            self.default_provider = provider
            print(f"✓ Set default provider to {provider.value}")
        
        def complete(self, messages: List[Message], 
                    provider: Optional[ModelProvider] = None, 
                    **kwargs) -> CompletionResponse:
            """Generate completion using specified or default provider"""
            provider = provider or self.default_provider
            
            if provider not in self.adapters:
                raise ValueError(f"Provider {provider.value} not registered")
            
            adapter = self.adapters[provider]
            return adapter.complete(messages, **kwargs)
        
        def stream(self, messages: List[Message], 
                  provider: Optional[ModelProvider] = None, 
                  **kwargs):
            """Stream completion using specified or default provider"""
            provider = provider or self.default_provider
            
            if provider not in self.adapters:
                raise ValueError(f"Provider {provider.value} not registered")
            
            adapter = self.adapters[provider]
            return adapter.stream(messages, **kwargs)
        
        def complete_with_fallback(self, messages: List[Message], 
                                  providers: Optional[List[ModelProvider]] = None,
                                  **kwargs) -> CompletionResponse:
            """Try completion with fallback providers"""
            if providers is None:
                providers = [self.default_provider] + [
                    p for p in self.adapters.keys() if p != self.default_provider
                ]
            
            last_error = None
            for provider in providers:
                try:
                    print(f"Trying provider: {provider.value}")
                    return self.complete(messages, provider=provider, **kwargs)
                except Exception as e:
                    print(f"  Failed: {str(e)}")
                    last_error = e
                    continue
            
            raise RuntimeError(f"All providers failed. Last error: {last_error}")
        
        def get_all_model_info(self) -> Dict[str, Dict[str, Any]]:
            """Get information about all registered models"""
            return {
                provider.value: adapter.get_model_info()
                for provider, adapter in self.adapters.items()
            }
    
    
    def demonstrate_adapter_pattern():
        """Demonstrate adapter/wrapper pattern"""
        print("=" * 80)
        print("ADAPTER/WRAPPER PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Initialize unified interface
        interface = UnifiedLLMInterface()
        
        # Register adapters for different providers
        print("\n" + "="*80)
        print("SETUP: Registering Provider Adapters")
        print("="*80)
        
        openai_config = ModelConfig(
            provider=ModelProvider.OPENAI,
            model_name="gpt-3.5-turbo",
            temperature=0.7
        )
        interface.register_adapter(
            ModelProvider.OPENAI,
            OpenAIAdapter(openai_config)
        )
        
        anthropic_config = ModelConfig(
            provider=ModelProvider.ANTHROPIC,
            model_name="claude-3-sonnet",
            temperature=0.7
        )
        interface.register_adapter(
            ModelProvider.ANTHROPIC,
            AnthropicAdapter(anthropic_config)
        )
        
        google_config = ModelConfig(
            provider=ModelProvider.GOOGLE,
            model_name="gemini-pro",
            temperature=0.7
        )
        interface.register_adapter(
            ModelProvider.GOOGLE,
            GoogleAdapter(google_config)
        )
        
        # Example 1: Complete with default provider
        print("\n" + "="*80)
        print("EXAMPLE 1: Completion with Default Provider")
        print("="*80)
        messages = [
            Message(role="system", content="You are a helpful assistant."),
            Message(role="user", content="What is the capital of France?")
        ]
        response = interface.complete(messages)
        print(f"Provider: {response.provider.value}")
        print(f"Model: {response.model}")
        print(f"Response: {response.content[:200]}...")
        
        # Example 2: Complete with specific provider
        print("\n" + "="*80)
        print("EXAMPLE 2: Completion with Specific Provider (Anthropic)")
        print("="*80)
        response = interface.complete(messages, provider=ModelProvider.ANTHROPIC)
        print(f"Provider: {response.provider.value}")
        print(f"Model: {response.model}")
        print(f"Response: {response.content[:200]}...")
        
        # Example 3: Switch between providers
        print("\n" + "="*80)
        print("EXAMPLE 3: Switching Between Providers")
        print("="*80)
        question = Message(role="user", content="Explain quantum computing briefly.")
        
        for provider in [ModelProvider.OPENAI, ModelProvider.GOOGLE, ModelProvider.ANTHROPIC]:
            response = interface.complete([question], provider=provider)
            print(f"\n{provider.value}:")
            print(f"  {response.content[:150]}...")
        
        # Example 4: Streaming response
        print("\n" + "="*80)
        print("EXAMPLE 4: Streaming Response")
        print("="*80)
        stream_messages = [
            Message(role="user", content="Count from 1 to 5.")
        ]
        print("Streaming: ", end="", flush=True)
        for chunk in interface.stream(stream_messages):
            print(chunk, end="", flush=True)
        print()
        
        # Example 5: Fallback mechanism
        print("\n" + "="*80)
        print("EXAMPLE 5: Fallback Mechanism")
        print("="*80)
        messages = [
            Message(role="user", content="What is machine learning?")
        ]
        response = interface.complete_with_fallback(messages)
        print(f"Succeeded with provider: {response.provider.value}")
        print(f"Response: {response.content[:200]}...")
        
        # Example 6: Model information
        print("\n" + "="*80)
        print("EXAMPLE 6: Model Information")
        print("="*80)
        all_info = interface.get_all_model_info()
        for provider, info in all_info.items():
            print(f"\n{provider}:")
            for key, value in info.items():
                print(f"  {key}: {value}")
        
        # Example 7: Conversation across providers
        print("\n" + "="*80)
        print("EXAMPLE 7: Multi-Turn Conversation")
        print("="*80)
        conversation = [
            Message(role="user", content="What is Python?")
        ]
        
        # First turn with OpenAI
        response1 = interface.complete(conversation, provider=ModelProvider.OPENAI)
        print(f"Turn 1 ({response1.provider.value}): {response1.content[:100]}...")
        
        # Add response and continue
        conversation.append(Message(role="assistant", content=response1.content))
        conversation.append(Message(role="user", content="What are its main features?"))
        
        # Second turn with different provider
        response2 = interface.complete(conversation, provider=ModelProvider.GOOGLE)
        print(f"Turn 2 ({response2.provider.value}): {response2.content[:100]}...")
        
        # Summary
        print("\n" + "="*80)
        print("SUMMARY: Adapter/Wrapper Pattern Best Practices")
        print("="*80)
        print("""
    1. INTERFACE DESIGN:
       - Define clear, provider-agnostic interface
       - Standardize message formats
       - Normalize response structures
       - Handle provider-specific features gracefully
    
    2. ADAPTER IMPLEMENTATION:
       - Encapsulate provider-specific logic
       - Handle format conversions
       - Map error codes consistently
       - Maintain provider-specific optimizations
    
    3. RESPONSE NORMALIZATION:
       - Standardize response format
       - Map finish reasons consistently
       - Include provider metadata
       - Handle streaming uniformly
    
    4. ERROR HANDLING:
       - Translate provider-specific errors
       - Provide meaningful error messages
       - Implement fallback mechanisms
       - Log provider-specific details
    
    5. CONFIGURATION:
       - Centralize configuration management
       - Support provider-specific parameters
       - Enable easy provider switching
       - Validate configurations
    
    6. TESTING:
       - Test each adapter independently
       - Verify format conversions
       - Test fallback mechanisms
       - Mock external API calls
    
    Benefits:
    ✓ Provider independence and flexibility
    ✓ Easy model switching
    ✓ Simplified application code
    ✓ Consistent interface across providers
    ✓ Facilitates testing and mocking
    ✓ Vendor lock-in prevention
    
    Challenges:
    - Abstraction may limit provider features
    - Mapping complexity for edge cases
    - Performance overhead
    - Maintaining feature parity
    - Handling provider-specific capabilities
        """)
    
    
    if __name__ == "__main__":
        demonstrate_adapter_pattern()
    
        

    Agentic Design Pattern: Adapter Wrapper

    Pattern 157: Adapter Wrapper – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 157: Adapter/Wrapper Pattern

    Description: Provides unified interfaces to different LLM providers and models, abstracting away provider-specific details. Enables easy switching between models without changing application code.

    Components: – Base interface definition – Provider-specific adapters – Response normalization – Error handling translation – Configuration management

    Use Cases: – Multi-provider support – Model switching – Vendor independence – Testing with different models – Cost optimization

    Benefits: – Provider independence – Simplified integration – Easy model switching – Consistent interface – Testability

    Trade-offs: – Abstraction overhead – May limit provider-specific features – Mapping complexity – Performance overhead

    LangChain Implementation: Uses abstract base classes, provider-specific implementations, and normalization layers for consistent responses


    File Statistics

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

    Module Documentation

    """
    Pattern 157: Adapter/Wrapper Pattern
    
    Description:
        Provides unified interfaces to different LLM providers and models, abstracting
        away provider-specific details. Enables easy switching between models without
        changing application code.
    
    Components:
        - Base interface definition
        - Provider-specific adapters
        - Response normalization
        - Error handling translation
        - Configuration management
    
    Use Cases:
        - Multi-provider support
        - Model switching
        - Vendor independence
        - Testing with different models
        - Cost optimization
    
    Benefits:
        - Provider independence
        - Simplified integration
        - Easy model switching
        - Consistent interface
        - Testability
    
    Trade-offs:
        - Abstraction overhead
        - May limit provider-specific features
        - Mapping complexity
        - Performance overhead
    
    LangChain Implementation:
        Uses abstract base classes, provider-specific implementations,
        and normalization layers for consistent responses
    """
    

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


    Import Statements

    Standard Library Imports

    import dataclasses.dataclass
    import enum.Enum
    import langchain_core.messages.AIMessage
    import langchain_core.messages.BaseMessage
    import langchain_core.messages.HumanMessage
    import langchain_core.messages.SystemMessage
    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.Protocol
    

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – enum.Enum: Enumeration support – langchain_core.messages.AIMessage: Regular expression operations – langchain_core.messages.BaseMessage: Regular expression operations – langchain_core.messages.HumanMessage: Regular expression operations – langchain_core.messages.SystemMessage: Regular expression operations – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Protocol: 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 abc.ABC
    from abc.abstractmethod
    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: ModelProvider

    Lines 56-61

    Inherits from: Enum

    Purpose:

    Supported model providers
    

    Class: ModelConfig

    Lines 65-71

    Purpose:

    Model configuration
    

    Class: Message

    Lines 75-83

    Purpose:

    Standardized message format
    

    Methods:

    __post_init__(self)

    Lines 81-83

    Purpose: Post Init operation.

    Parameters:


    Class: CompletionResponse

    Lines 87-98

    Purpose:

    Standardized completion response
    

    Methods:

    __post_init__(self)

    Lines 96-98

    Purpose: Post Init operation.

    Parameters:


    Class: LLMAdapter

    Lines 101-117

    Inherits from: ABC

    Purpose:

    Abstract base class for LLM adapters
    

    Methods:

    complete(self, messages)

    Lines 105-107

    Decorators: abstractmethod

    Purpose:

    Generate completion from messages
    

    Parameters: – messages: Messages

    Returns: CompletionResponse

    stream(self, messages)

    Lines 110-112

    Decorators: abstractmethod

    Purpose:

    Stream completion
    

    Parameters: – messages: Messages

    get_model_info(self)

    Lines 115-117

    Decorators: abstractmethod

    Purpose:

    Get model information
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D75BC50>


    Class: OpenAIAdapter

    Lines 120-183

    Inherits from: LLMAdapter

    Purpose:

    Adapter for OpenAI models
    

    Methods:

    __init__(self, config)

    Lines 123-131

    Purpose:

    Initialize OpenAI adapter
    

    Parameters: – config: Config

    complete(self, messages)

    Lines 133-152

    Purpose:

    Generate completion
    

    Parameters: – messages: Messages

    Returns: CompletionResponse

    stream(self, messages)

    Lines 154-162

    Purpose:

    Stream completion
    

    Parameters: – messages: Messages

    get_model_info(self)

    Lines 164-171

    Purpose:

    Get model information
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D753690>

    _convert_messages(self, messages)

    Lines 173-183

    Purpose:

    Convert standard messages to LangChain messages
    

    Parameters: – messages: Messages

    Returns: <ast.Subscript object at 0x000001A06D7CB290>


    Class: AnthropicAdapter

    Lines 186-250

    Inherits from: LLMAdapter

    Purpose:

    Adapter for Anthropic models (simulated)
    

    Methods:

    __init__(self, config)

    Lines 189-197

    Purpose:

    Initialize Anthropic adapter
    

    Parameters: – config: Config

    complete(self, messages)

    Lines 199-217

    Purpose:

    Generate completion
    

    Parameters: – messages: Messages

    Returns: CompletionResponse

    stream(self, messages)

    Lines 219-227

    Purpose:

    Stream completion
    

    Parameters: – messages: Messages

    get_model_info(self)

    Lines 229-236

    Purpose:

    Get model information
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D7AB310>

    _format_anthropic_prompt(self, messages)

    Lines 238-250

    Purpose:

    Format messages for Anthropic API
    

    Parameters: – messages: Messages

    Returns: str


    Class: GoogleAdapter

    Lines 253-310

    Inherits from: LLMAdapter

    Purpose:

    Adapter for Google models (simulated)
    

    Methods:

    __init__(self, config)

    Lines 256-263

    Purpose:

    Initialize Google adapter
    

    Parameters: – config: Config

    complete(self, messages)

    Lines 265-288

    Purpose:

    Generate completion
    

    Parameters: – messages: Messages

    Returns: CompletionResponse

    stream(self, messages)

    Lines 290-301

    Purpose:

    Stream completion
    

    Parameters: – messages: Messages

    get_model_info(self)

    Lines 303-310

    Purpose:

    Get model information
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D80FD50>


    Class: UnifiedLLMInterface

    Lines 313-385

    Purpose:

    Unified interface for multiple LLM providers
    

    Methods:

    __init__(self)

    Lines 316-319

    Purpose:

    Initialize unified interface
    

    Parameters:

    register_adapter(self, provider, adapter)

    Lines 321-326

    Purpose:

    Register a provider adapter
    

    Parameters: – provider: Provider – adapter: Adapter

    set_default_provider(self, provider)

    Lines 328-333

    Purpose:

    Set default provider
    

    Parameters: – provider: Provider

    complete(self, messages, provider)

    Lines 335-345

    Purpose:

    Generate completion using specified or default provider
    

    Parameters: – messages: Messages – provider: Provider

    Returns: CompletionResponse

    stream(self, messages, provider)

    Lines 347-357

    Purpose:

    Stream completion using specified or default provider
    

    Parameters: – messages: Messages – provider: Provider

    complete_with_fallback(self, messages, providers)

    Lines 359-378

    Purpose:

    Try completion with fallback providers
    

    Parameters: – messages: Messages – providers: Providers

    Returns: CompletionResponse

    get_all_model_info(self)

    Lines 380-385

    Purpose:

    Get information about all registered models
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D622590>


    Functions

    demonstrate_adapter_pattern()

    Lines 388-573

    Purpose:

    Demonstrate adapter/wrapper pattern
    

    Key Concepts Summary

    Pattern Overview

    The Adapter Wrapper 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 156
    • Pattern 158

    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 Adapter Wrapper 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: 157_adapter_wrapper.py
    Total Lines: 578
    Pattern Category: AI Agent Design Pattern #157


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Api Gateway

    
        """
    Pattern 156: API Gateway Pattern
    
    Description:
        Centralized gateway for managing API requests to multiple agents/services.
        Provides routing, authentication, rate limiting, monitoring, and unified
        interface for diverse backend services.
    
    Components:
        - Request router
        - Authentication & authorization
        - Rate limiting
        - Load balancing
        - Monitoring & logging
        - Response transformation
        - Error handling
    
    Use Cases:
        - Multi-agent systems
        - Microservices architecture
        - API versioning
        - Security enforcement
        - Traffic management
    
    Benefits:
        - Centralized control
        - Security enforcement
        - Traffic management
        - Simplified client interface
        - Monitoring visibility
    
    Trade-offs:
        - Single point of failure
        - Added latency
        - Complexity overhead
        - Gateway bottleneck potential
    
    LangChain Implementation:
        Uses routing logic, rate limiting, authentication checks,
        and multiple LLM backends with unified interface
    """
    
    import os
    import time
    from typing import Dict, Any, Optional, Callable, List
    from dataclasses import dataclass, field
    from datetime import datetime, timedelta
    from enum import Enum
    from collections import defaultdict
    from dotenv import load_dotenv
    
    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    
    load_dotenv()
    
    
    class ServiceType(Enum):
        """Types of backend services"""
        CHAT = "chat"
        COMPLETION = "completion"
        ANALYSIS = "analysis"
        TRANSLATION = "translation"
        SUMMARIZATION = "summarization"
    
    
    class AuthLevel(Enum):
        """Authentication levels"""
        PUBLIC = "public"
        AUTHENTICATED = "authenticated"
        PREMIUM = "premium"
        ADMIN = "admin"
    
    
    @dataclass
    class Route:
        """API route configuration"""
        path: str
        service_type: ServiceType
        handler: Callable
        auth_level: AuthLevel = AuthLevel.PUBLIC
        rate_limit: int = 60  # requests per minute
        timeout: float = 30.0  # seconds
    
    
    @dataclass
    class ServiceConfig:
        """Backend service configuration"""
        name: str
        service_type: ServiceType
        model_name: str
        enabled: bool = True
        weight: int = 1  # For load balancing
        max_concurrent: int = 10
    
    
    @dataclass
    class Request:
        """Gateway request"""
        path: str
        method: str
        payload: Dict[str, Any]
        headers: Dict[str, str] = field(default_factory=dict)
        user_id: Optional[str] = None
        api_key: Optional[str] = None
        timestamp: datetime = field(default_factory=datetime.now)
    
    
    @dataclass
    class Response:
        """Gateway response"""
        status_code: int
        data: Any
        headers: Dict[str, str] = field(default_factory=dict)
        execution_time: float = 0.0
        service_name: Optional[str] = None
        error: Optional[str] = None
    
    
    @dataclass
    class RateLimitInfo:
        """Rate limit tracking"""
        requests: List[datetime] = field(default_factory=list)
        limit: int = 60
        window: int = 60  # seconds
    
    
    class APIGateway:
        """Centralized API gateway for agent services"""
        
        def __init__(self):
            """Initialize API gateway"""
            self.routes: Dict[str, Route] = {}
            self.services: Dict[str, ServiceConfig] = {}
            self.rate_limits: Dict[str, RateLimitInfo] = defaultdict(
                lambda: RateLimitInfo()
            )
            self.api_keys: Dict[str, AuthLevel] = {}
            self.request_log: List[Dict[str, Any]] = []
            
            # Initialize services
            self._initialize_services()
            
            # Set up routes
            self._setup_routes()
        
        def _initialize_services(self):
            """Initialize backend services"""
            self.services = {
                "chat_service": ServiceConfig(
                    name="chat_service",
                    service_type=ServiceType.CHAT,
                    model_name="gpt-3.5-turbo"
                ),
                "analysis_service": ServiceConfig(
                    name="analysis_service",
                    service_type=ServiceType.ANALYSIS,
                    model_name="gpt-3.5-turbo"
                ),
                "translation_service": ServiceConfig(
                    name="translation_service",
                    service_type=ServiceType.TRANSLATION,
                    model_name="gpt-3.5-turbo"
                ),
            }
            
            # Initialize LLMs for services
            self.llms = {
                name: ChatOpenAI(model=config.model_name, temperature=0.7)
                for name, config in self.services.items()
            }
        
        def _setup_routes(self):
            """Set up API routes"""
            self.routes = {
                "/chat": Route(
                    path="/chat",
                    service_type=ServiceType.CHAT,
                    handler=self._handle_chat,
                    auth_level=AuthLevel.AUTHENTICATED,
                    rate_limit=30
                ),
                "/analyze": Route(
                    path="/analyze",
                    service_type=ServiceType.ANALYSIS,
                    handler=self._handle_analysis,
                    auth_level=AuthLevel.PREMIUM,
                    rate_limit=10
                ),
                "/translate": Route(
                    path="/translate",
                    service_type=ServiceType.TRANSLATION,
                    handler=self._handle_translation,
                    auth_level=AuthLevel.AUTHENTICATED,
                    rate_limit=20
                ),
                "/summarize": Route(
                    path="/summarize",
                    service_type=ServiceType.SUMMARIZATION,
                    handler=self._handle_summarization,
                    auth_level=AuthLevel.AUTHENTICATED,
                    rate_limit=15
                ),
            }
        
        def register_api_key(self, api_key: str, auth_level: AuthLevel):
            """Register an API key with authentication level"""
            self.api_keys[api_key] = auth_level
        
        def authenticate(self, request: Request) -> tuple[bool, Optional[str]]:
            """Authenticate request"""
            # Get route
            route = self.routes.get(request.path)
            if not route:
                return False, "Route not found"
            
            # Check authentication level
            if route.auth_level == AuthLevel.PUBLIC:
                return True, None
            
            # Check API key
            if not request.api_key:
                return False, "API key required"
            
            user_level = self.api_keys.get(request.api_key)
            if not user_level:
                return False, "Invalid API key"
            
            # Check authorization level
            required_levels = {
                AuthLevel.AUTHENTICATED: [AuthLevel.AUTHENTICATED, AuthLevel.PREMIUM, AuthLevel.ADMIN],
                AuthLevel.PREMIUM: [AuthLevel.PREMIUM, AuthLevel.ADMIN],
                AuthLevel.ADMIN: [AuthLevel.ADMIN]
            }
            
            if user_level not in required_levels.get(route.auth_level, []):
                return False, f"Insufficient permissions. Requires {route.auth_level.value}"
            
            return True, None
        
        def check_rate_limit(self, request: Request) -> tuple[bool, Optional[str]]:
            """Check if request exceeds rate limit"""
            route = self.routes.get(request.path)
            if not route:
                return True, None
            
            # Get rate limit info for user
            key = request.api_key or request.user_id or "anonymous"
            rate_info = self.rate_limits[key]
            
            # Update limit if different from current
            if rate_info.limit != route.rate_limit:
                rate_info.limit = route.rate_limit
            
            # Clean old requests outside window
            now = datetime.now()
            cutoff = now - timedelta(seconds=rate_info.window)
            rate_info.requests = [
                req_time for req_time in rate_info.requests
                if req_time > cutoff
            ]
            
            # Check limit
            if len(rate_info.requests) >= rate_info.limit:
                return False, f"Rate limit exceeded. Max {rate_info.limit} requests per minute"
            
            # Add current request
            rate_info.requests.append(now)
            return True, None
        
        def route_request(self, request: Request) -> Response:
            """Route and process request through gateway"""
            start_time = time.time()
            
            # Log request
            self._log_request(request)
            
            # Authenticate
            auth_ok, auth_error = self.authenticate(request)
            if not auth_ok:
                return Response(
                    status_code=401,
                    data=None,
                    error=auth_error,
                    execution_time=time.time() - start_time
                )
            
            # Check rate limit
            rate_ok, rate_error = self.check_rate_limit(request)
            if not rate_ok:
                return Response(
                    status_code=429,
                    data=None,
                    error=rate_error,
                    execution_time=time.time() - start_time
                )
            
            # Get route
            route = self.routes.get(request.path)
            if not route:
                return Response(
                    status_code=404,
                    data=None,
                    error=f"Route not found: {request.path}",
                    execution_time=time.time() - start_time
                )
            
            # Select service
            service = self._select_service(route.service_type)
            if not service:
                return Response(
                    status_code=503,
                    data=None,
                    error="No available service",
                    execution_time=time.time() - start_time
                )
            
            # Execute request
            try:
                result = route.handler(request, service)
                execution_time = time.time() - start_time
                
                return Response(
                    status_code=200,
                    data=result,
                    execution_time=execution_time,
                    service_name=service.name
                )
                
            except Exception as e:
                execution_time = time.time() - start_time
                return Response(
                    status_code=500,
                    data=None,
                    error=str(e),
                    execution_time=execution_time,
                    service_name=service.name
                )
        
        def _select_service(self, service_type: ServiceType) -> Optional[ServiceConfig]:
            """Select service based on type and load balancing"""
            # Find services matching type
            matching = [
                s for s in self.services.values()
                if s.service_type == service_type and s.enabled
            ]
            
            if not matching:
                return None
            
            # Simple weighted random selection
            import random
            weights = [s.weight for s in matching]
            return random.choices(matching, weights=weights)[0]
        
        def _handle_chat(self, request: Request, service: ServiceConfig) -> str:
            """Handle chat request"""
            llm = self.llms[service.name]
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "You are a helpful AI assistant."),
                ("user", "{message}")
            ])
            
            chain = prompt | llm | StrOutputParser()
            return chain.invoke({"message": request.payload.get("message", "")})
        
        def _handle_analysis(self, request: Request, service: ServiceConfig) -> str:
            """Handle analysis request"""
            llm = self.llms[service.name]
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "You are an expert analyst. Provide detailed analysis."),
                ("user", "Analyze the following:\n\n{text}")
            ])
            
            chain = prompt | llm | StrOutputParser()
            return chain.invoke({"text": request.payload.get("text", "")})
        
        def _handle_translation(self, request: Request, service: ServiceConfig) -> str:
            """Handle translation request"""
            llm = self.llms[service.name]
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "You are a professional translator."),
                ("user", "Translate the following from {source} to {target}:\n\n{text}")
            ])
            
            chain = prompt | llm | StrOutputParser()
            return chain.invoke({
                "source": request.payload.get("source_lang", "auto"),
                "target": request.payload.get("target_lang", "English"),
                "text": request.payload.get("text", "")
            })
        
        def _handle_summarization(self, request: Request, service: ServiceConfig) -> str:
            """Handle summarization request"""
            llm = self.llms[service.name]
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "You are an expert at creating concise summaries."),
                ("user", "Summarize the following in {max_words} words:\n\n{text}")
            ])
            
            chain = prompt | llm | StrOutputParser()
            return chain.invoke({
                "max_words": request.payload.get("max_words", 100),
                "text": request.payload.get("text", "")
            })
        
        def _log_request(self, request: Request):
            """Log request for monitoring"""
            self.request_log.append({
                "timestamp": request.timestamp.isoformat(),
                "path": request.path,
                "method": request.method,
                "user_id": request.user_id,
                "api_key_hash": hash(request.api_key) if request.api_key else None
            })
        
        def get_metrics(self) -> Dict[str, Any]:
            """Get gateway metrics"""
            now = datetime.now()
            recent = now - timedelta(minutes=5)
            
            recent_requests = [
                log for log in self.request_log
                if datetime.fromisoformat(log["timestamp"]) > recent
            ]
            
            # Count by path
            by_path = defaultdict(int)
            for log in recent_requests:
                by_path[log["path"]] += 1
            
            # Rate limit status
            rate_status = {}
            for key, info in self.rate_limits.items():
                rate_status[key] = {
                    "current": len(info.requests),
                    "limit": info.limit,
                    "usage_pct": len(info.requests) / info.limit * 100
                }
            
            return {
                "total_requests_5min": len(recent_requests),
                "requests_by_path": dict(by_path),
                "rate_limit_status": rate_status,
                "active_services": len([s for s in self.services.values() if s.enabled]),
                "total_services": len(self.services)
            }
    
    
    def demonstrate_api_gateway():
        """Demonstrate API gateway pattern"""
        print("=" * 80)
        print("API GATEWAY PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Initialize gateway
        gateway = APIGateway()
        
        # Register API keys
        gateway.register_api_key("free_user_key", AuthLevel.AUTHENTICATED)
        gateway.register_api_key("premium_user_key", AuthLevel.PREMIUM)
        gateway.register_api_key("admin_key", AuthLevel.ADMIN)
        
        # Example 1: Successful authenticated request
        print("\n" + "="*80)
        print("EXAMPLE 1: Successful Chat Request (Authenticated)")
        print("="*80)
        request = Request(
            path="/chat",
            method="POST",
            payload={"message": "What is machine learning?"},
            api_key="free_user_key",
            user_id="user123"
        )
        response = gateway.route_request(request)
        print(f"Status: {response.status_code}")
        print(f"Service: {response.service_name}")
        print(f"Time: {response.execution_time:.2f}s")
        print(f"Response: {response.data[:200] if response.data else response.error}...")
        
        # Example 2: Premium service request
        print("\n" + "="*80)
        print("EXAMPLE 2: Analysis Request (Premium)")
        print("="*80)
        request = Request(
            path="/analyze",
            method="POST",
            payload={"text": "The market showed strong growth in Q1 with a 15% increase in sales."},
            api_key="premium_user_key"
        )
        response = gateway.route_request(request)
        print(f"Status: {response.status_code}")
        print(f"Service: {response.service_name}")
        print(f"Response: {response.data[:200] if response.data else response.error}...")
        
        # Example 3: Authentication failure
        print("\n" + "="*80)
        print("EXAMPLE 3: Authentication Failure")
        print("="*80)
        request = Request(
            path="/analyze",
            method="POST",
            payload={"text": "Test"},
            api_key="free_user_key"  # Not premium
        )
        response = gateway.route_request(request)
        print(f"Status: {response.status_code}")
        print(f"Error: {response.error}")
        
        # Example 4: Rate limiting
        print("\n" + "="*80)
        print("EXAMPLE 4: Rate Limiting")
        print("="*80)
        # Make multiple requests quickly
        for i in range(3):
            request = Request(
                path="/chat",
                method="POST",
                payload={"message": f"Request {i+1}"},
                api_key="free_user_key"
            )
            response = gateway.route_request(request)
            print(f"Request {i+1}: Status {response.status_code} - {response.error if response.error else 'Success'}")
        
        # Example 5: Translation service
        print("\n" + "="*80)
        print("EXAMPLE 5: Translation Service")
        print("="*80)
        request = Request(
            path="/translate",
            method="POST",
            payload={
                "text": "Hello, how are you?",
                "source_lang": "English",
                "target_lang": "Spanish"
            },
            api_key="free_user_key"
        )
        response = gateway.route_request(request)
        print(f"Status: {response.status_code}")
        print(f"Translation: {response.data if response.data else response.error}")
        
        # Example 6: Gateway metrics
        print("\n" + "="*80)
        print("EXAMPLE 6: Gateway Metrics")
        print("="*80)
        metrics = gateway.get_metrics()
        print("Gateway Metrics:")
        print(f"  Total requests (5 min): {metrics['total_requests_5min']}")
        print(f"  Requests by path: {metrics['requests_by_path']}")
        print(f"  Active services: {metrics['active_services']}/{metrics['total_services']}")
        
        # Example 7: Route not found
        print("\n" + "="*80)
        print("EXAMPLE 7: Route Not Found")
        print("="*80)
        request = Request(
            path="/nonexistent",
            method="POST",
            payload={},
            api_key="free_user_key"
        )
        response = gateway.route_request(request)
        print(f"Status: {response.status_code}")
        print(f"Error: {response.error}")
        
        # Summary
        print("\n" + "="*80)
        print("SUMMARY: API Gateway Pattern Best Practices")
        print("="*80)
        print("""
    1. ROUTING:
       - Centralized request routing
       - Service discovery and selection
       - Load balancing across backends
       - Flexible routing rules
    
    2. AUTHENTICATION & AUTHORIZATION:
       - API key management
       - Multiple authorization levels
       - Fine-grained access control
       - Secure credential handling
    
    3. RATE LIMITING:
       - Per-user rate limits
       - Per-route limits
       - Sliding window implementation
       - Graceful limit enforcement
    
    4. MONITORING:
       - Request logging
       - Performance metrics
       - Service health tracking
       - Real-time visibility
    
    5. ERROR HANDLING:
       - Standardized error responses
       - Proper HTTP status codes
       - Detailed error messages
       - Graceful degradation
    
    6. SERVICE MANAGEMENT:
       - Service registration
       - Health checks
       - Dynamic service discovery
       - Version management
    
    Benefits:
    ✓ Centralized control and management
    ✓ Security enforcement at gateway level
    ✓ Simplified client integration
    ✓ Traffic management and optimization
    ✓ Monitoring and observability
    ✓ Service abstraction
    
    Challenges:
    - Single point of failure risk
    - Added network latency
    - Gateway complexity
    - Potential bottleneck
    - Requires high availability
        """)
    
    
    if __name__ == "__main__":
        demonstrate_api_gateway()
    
        

    Agentic Design Pattern: Api Gateway

    Pattern 156: Api Gateway – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 156: API Gateway Pattern

    Description: Centralized gateway for managing API requests to multiple agents/services. Provides routing, authentication, rate limiting, monitoring, and unified interface for diverse backend services.

    Components: – Request router – Authentication & authorization – Rate limiting – Load balancing – Monitoring & logging – Response transformation – Error handling

    Use Cases: – Multi-agent systems – Microservices architecture – API versioning – Security enforcement – Traffic management

    Benefits: – Centralized control – Security enforcement – Traffic management – Simplified client interface – Monitoring visibility

    Trade-offs: – Single point of failure – Added latency – Complexity overhead – Gateway bottleneck potential

    LangChain Implementation: Uses routing logic, rate limiting, authentication checks, and multiple LLM backends with unified interface


    File Statistics

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

    Module Documentation

    """
    Pattern 156: API Gateway Pattern
    
    Description:
        Centralized gateway for managing API requests to multiple agents/services.
        Provides routing, authentication, rate limiting, monitoring, and unified
        interface for diverse backend services.
    
    Components:
        - Request router
        - Authentication & authorization
        - Rate limiting
        - Load balancing
        - Monitoring & logging
        - Response transformation
        - Error handling
    
    Use Cases:
        - Multi-agent systems
        - Microservices architecture
        - API versioning
        - Security enforcement
        - Traffic management
    
    Benefits:
        - Centralized control
        - Security enforcement
        - Traffic management
        - Simplified client interface
        - Monitoring visibility
    
    Trade-offs:
        - Single point of failure
        - Added latency
        - Complexity overhead
        - Gateway bottleneck potential
    
    LangChain Implementation:
        Uses routing logic, rate limiting, authentication checks,
        and multiple LLM backends with unified interface
    """
    

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


    Import Statements

    Standard Library Imports

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

    Purpose: Core Python functionality: – collections.defaultdict: Specialized container datatypes – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – datetime.timedelta: Date and time handling – enum.Enum: Enumeration support – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – 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: ServiceType

    Lines 59-65

    Inherits from: Enum

    Purpose:

    Types of backend services
    

    Class: AuthLevel

    Lines 68-73

    Inherits from: Enum

    Purpose:

    Authentication levels
    

    Class: Route

    Lines 77-84

    Purpose:

    API route configuration
    

    Class: ServiceConfig

    Lines 88-95

    Purpose:

    Backend service configuration
    

    Class: Request

    Lines 99-107

    Purpose:

    Gateway request
    

    Class: Response

    Lines 111-118

    Purpose:

    Gateway response
    

    Class: RateLimitInfo

    Lines 122-126

    Purpose:

    Rate limit tracking
    

    Class: APIGateway

    Lines 129-452

    Purpose:

    Centralized API gateway for agent services
    

    Methods:

    __init__(self)

    Lines 132-146

    Purpose:

    Initialize API gateway
    

    Parameters:

    _initialize_services(self)

    Lines 148-172

    Purpose:

    Initialize backend services
    

    Parameters:

    _setup_routes(self)

    Lines 174-205

    Purpose:

    Set up API routes
    

    Parameters:

    register_api_key(self, api_key, auth_level)

    Lines 207-209

    Purpose:

    Register an API key with authentication level
    

    Parameters: – api_key: Api key – auth_level: Auth level

    authenticate(self, request)

    Lines 211-240

    Purpose:

    Authenticate request
    

    Parameters: – request: Request

    Returns: <ast.Subscript object at 0x000001A06D65AE50>

    check_rate_limit(self, request)

    Lines 242-270

    Purpose:

    Check if request exceeds rate limit
    

    Parameters: – request: Request

    Returns: <ast.Subscript object at 0x000001A06D7CD210>

    route_request(self, request)

    Lines 272-339

    Purpose:

    Route and process request through gateway
    

    Parameters: – request: Request

    Returns: Response

    _select_service(self, service_type)

    Lines 341-355

    Purpose:

    Select service based on type and load balancing
    

    Parameters: – service_type: Service type

    Returns: <ast.Subscript object at 0x000001A06D6FDB90>

    _handle_chat(self, request, service)

    Lines 357-367

    Purpose:

    Handle chat request
    

    Parameters: – request: Request – service: Service

    Returns: str

    _handle_analysis(self, request, service)

    Lines 369-379

    Purpose:

    Handle analysis request
    

    Parameters: – request: Request – service: Service

    Returns: str

    _handle_translation(self, request, service)

    Lines 381-395

    Purpose:

    Handle translation request
    

    Parameters: – request: Request – service: Service

    Returns: str

    _handle_summarization(self, request, service)

    Lines 397-410

    Purpose:

    Handle summarization request
    

    Parameters: – request: Request – service: Service

    Returns: str

    _log_request(self, request)

    Lines 412-420

    Purpose:

    Log request for monitoring
    

    Parameters: – request: Request

    get_metrics(self)

    Lines 422-452

    Purpose:

    Get gateway metrics
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D60D450>


    Functions

    demonstrate_api_gateway()

    Lines 455-627

    Purpose:

    Demonstrate API gateway pattern
    

    Key Concepts Summary

    Pattern Overview

    The Api Gateway 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 155
    • Pattern 157

    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 Api Gateway 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: 156_api_gateway.py
    Total Lines: 632
    Pattern Category: AI Agent Design Pattern #156


    Generated from actual implementation code
    For educational and reference purposes