Tag: agentic

  • Agentic Design Pattern: Microservice Agent

    
        """
    Pattern 081: Microservice Agent Architecture
    
    Description:
        A Microservice Agent Architecture decomposes AI agents into independently
        deployable microservices that communicate via well-defined APIs. This pattern
        enables scalability, fault isolation, independent deployment, and technology
        diversity across agent components. Each microservice can be developed, deployed,
        and scaled independently while maintaining loose coupling.
        
        The architecture supports distributed agent systems with service discovery,
        load balancing, inter-service communication, circuit breakers, and observability
        across the microservices mesh.
    
    Components:
        1. Agent Services: Independent agent microservices
        2. API Gateway: Entry point and routing
        3. Service Registry: Service discovery and health checks
        4. Message Bus: Async inter-service communication
        5. Load Balancer: Distribute requests across instances
        6. Circuit Breaker: Fault tolerance and isolation
        7. Config Service: Centralized configuration
        8. Monitoring: Distributed tracing and metrics
    
    Key Features:
        - Independent service deployment
        - Service discovery and registration
        - Inter-service communication (REST, gRPC, message queues)
        - Load balancing and auto-scaling
        - Fault isolation and circuit breakers
        - Distributed tracing
        - API gateway pattern
        - Health checks and monitoring
        - Polyglot persistence
        - Technology diversity
    
    Use Cases:
        - Large-scale AI platforms
        - Multi-tenant AI services
        - Enterprise AI systems
        - Cloud-native AI applications
        - Distributed agent coordination
        - Scalable chatbot platforms
        - AI-powered SaaS products
        - Microservices-based RAG systems
        - Agent mesh architectures
        - Multi-model serving platforms
    
    LangChain Implementation:
        Uses FastAPI for service endpoints, service registry pattern,
        inter-service HTTP communication, and distributed agent coordination.
    """
    
    import os
    import time
    import uuid
    import json
    from typing import List, Dict, Any, Optional, Callable
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from collections import defaultdict
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class ServiceStatus(Enum):
        """Status of a microservice"""
        STARTING = "starting"
        HEALTHY = "healthy"
        UNHEALTHY = "unhealthy"
        DEGRADED = "degraded"
        STOPPED = "stopped"
    
    
    class CommunicationProtocol(Enum):
        """Inter-service communication protocol"""
        HTTP_REST = "http_rest"
        GRPC = "grpc"
        MESSAGE_QUEUE = "message_queue"
        EVENT_BUS = "event_bus"
    
    
    @dataclass
    class ServiceInfo:
        """Information about a microservice"""
        service_id: str
        service_name: str
        version: str
        host: str
        port: int
        protocol: CommunicationProtocol
        status: ServiceStatus
        endpoints: List[str]
        health_check_url: str
        registered_at: datetime = field(default_factory=datetime.now)
        last_heartbeat: datetime = field(default_factory=datetime.now)
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class ServiceRequest:
        """Request to a microservice"""
        request_id: str
        service_name: str
        endpoint: str
        method: str
        data: Dict[str, Any]
        headers: Dict[str, str] = field(default_factory=dict)
        timeout: float = 30.0
    
    
    @dataclass
    class ServiceResponse:
        """Response from a microservice"""
        request_id: str
        service_name: str
        status_code: int
        data: Any
        error: Optional[str] = None
        duration: float = 0.0
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    class ServiceRegistry:
        """
        Service registry for microservice discovery.
        
        Maintains a registry of available services with health monitoring.
        """
        
        def __init__(self):
            """Initialize service registry"""
            self.services: Dict[str, ServiceInfo] = {}
            self.service_instances: Dict[str, List[str]] = defaultdict(list)
        
        def register(self, service: ServiceInfo) -> bool:
            """
            Register a service.
            
            Args:
                service: Service information
                
            Returns:
                True if registered successfully
            """
            self.services[service.service_id] = service
            self.service_instances[service.service_name].append(service.service_id)
            return True
        
        def deregister(self, service_id: str) -> bool:
            """
            Deregister a service.
            
            Args:
                service_id: Service ID
                
            Returns:
                True if deregistered successfully
            """
            if service_id in self.services:
                service = self.services[service_id]
                self.service_instances[service.service_name].remove(service_id)
                del self.services[service_id]
                return True
            return False
        
        def discover(self, service_name: str) -> Optional[ServiceInfo]:
            """
            Discover a healthy service instance.
            
            Args:
                service_name: Name of service to discover
                
            Returns:
                ServiceInfo if found, None otherwise
            """
            instances = self.service_instances.get(service_name, [])
            
            # Find healthy instance
            for instance_id in instances:
                service = self.services.get(instance_id)
                if service and service.status == ServiceStatus.HEALTHY:
                    return service
            
            return None
        
        def health_check(self, service_id: str) -> ServiceStatus:
            """
            Check health of a service.
            
            Args:
                service_id: Service ID
                
            Returns:
                Service status
            """
            if service_id in self.services:
                service = self.services[service_id]
                # Simulate health check
                service.last_heartbeat = datetime.now()
                return service.status
            
            return ServiceStatus.STOPPED
        
        def list_services(self) -> List[ServiceInfo]:
            """
            List all registered services.
            
            Returns:
                List of service information
            """
            return list(self.services.values())
    
    
    class AgentService:
        """
        Base microservice for AI agents.
        
        Each agent capability is encapsulated as an independent service.
        """
        
        def __init__(
            self,
            service_name: str,
            version: str = "1.0.0",
            port: int = 8000
        ):
            """
            Initialize agent service.
            
            Args:
                service_name: Name of the service
                version: Service version
                port: Port number
            """
            self.service_id = f"{service_name}_{uuid.uuid4().hex[:8]}"
            self.service_name = service_name
            self.version = version
            self.port = port
            self.status = ServiceStatus.STARTING
            
            # LLM for this service
            self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
            
            # Request tracking
            self.requests: Dict[str, ServiceRequest] = {}
            self.responses: Dict[str, ServiceResponse] = {}
        
        def get_info(self) -> ServiceInfo:
            """
            Get service information.
            
            Returns:
                ServiceInfo object
            """
            return ServiceInfo(
                service_id=self.service_id,
                service_name=self.service_name,
                version=self.version,
                host="localhost",
                port=self.port,
                protocol=CommunicationProtocol.HTTP_REST,
                status=self.status,
                endpoints=["/health", "/process"],
                health_check_url=f"http://localhost:{self.port}/health"
            )
        
        def start(self) -> bool:
            """
            Start the service.
            
            Returns:
                True if started successfully
            """
            self.status = ServiceStatus.HEALTHY
            return True
        
        def stop(self) -> bool:
            """
            Stop the service.
            
            Returns:
                True if stopped successfully
            """
            self.status = ServiceStatus.STOPPED
            return True
        
        def health(self) -> Dict[str, Any]:
            """
            Health check endpoint.
            
            Returns:
                Health status
            """
            return {
                "service": self.service_name,
                "status": self.status.value,
                "version": self.version,
                "timestamp": datetime.now().isoformat()
            }
        
        def process(self, data: Dict[str, Any]) -> Dict[str, Any]:
            """
            Process a request (to be overridden by subclasses).
            
            Args:
                data: Request data
                
            Returns:
                Response data
            """
            raise NotImplementedError("Subclasses must implement process()")
    
    
    class SummarizationService(AgentService):
        """Microservice for text summarization"""
        
        def __init__(self):
            super().__init__("summarization", version="1.0.0", port=8001)
        
        def process(self, data: Dict[str, Any]) -> Dict[str, Any]:
            """
            Summarize text.
            
            Args:
                data: {'text': str, 'length': str}
                
            Returns:
                {'summary': str}
            """
            text = data.get('text', '')
            length = data.get('length', 'short')
            
            prompt = ChatPromptTemplate.from_template(
                "Summarize the following text in {length} form:\n\n{text}"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            
            summary = chain.invoke({
                'text': text[:1000],
                'length': length
            })
            
            return {
                'summary': summary,
                'service': self.service_name
            }
    
    
    class TranslationService(AgentService):
        """Microservice for translation"""
        
        def __init__(self):
            super().__init__("translation", version="1.0.0", port=8002)
        
        def process(self, data: Dict[str, Any]) -> Dict[str, Any]:
            """
            Translate text.
            
            Args:
                data: {'text': str, 'target_language': str}
                
            Returns:
                {'translation': str}
            """
            text = data.get('text', '')
            target_language = data.get('target_language', 'Spanish')
            
            prompt = ChatPromptTemplate.from_template(
                "Translate the following text to {language}:\n\n{text}"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            
            translation = chain.invoke({
                'text': text[:500],
                'language': target_language
            })
            
            return {
                'translation': translation,
                'target_language': target_language,
                'service': self.service_name
            }
    
    
    class SentimentService(AgentService):
        """Microservice for sentiment analysis"""
        
        def __init__(self):
            super().__init__("sentiment", version="1.0.0", port=8003)
        
        def process(self, data: Dict[str, Any]) -> Dict[str, Any]:
            """
            Analyze sentiment.
            
            Args:
                data: {'text': str}
                
            Returns:
                {'sentiment': str, 'confidence': float}
            """
            text = data.get('text', '')
            
            prompt = ChatPromptTemplate.from_template(
                """Analyze the sentiment of this text. Respond with:
    SENTIMENT: positive/negative/neutral
    CONFIDENCE: 0.0-1.0
    
    Text: {text}"""
            )
            
            chain = prompt | self.llm | StrOutputParser()
            
            result = chain.invoke({'text': text[:500]})
            
            # Parse result
            sentiment = "neutral"
            confidence = 0.5
            
            for line in result.split('\n'):
                if 'SENTIMENT:' in line:
                    sentiment = line.split(':')[1].strip().lower()
                elif 'CONFIDENCE:' in line:
                    try:
                        confidence = float(line.split(':')[1].strip())
                    except:
                        pass
            
            return {
                'sentiment': sentiment,
                'confidence': confidence,
                'service': self.service_name
            }
    
    
    class APIGateway:
        """
        API Gateway for routing requests to microservices.
        
        Provides a single entry point and routes to appropriate services.
        """
        
        def __init__(self, registry: ServiceRegistry):
            """
            Initialize API gateway.
            
            Args:
                registry: Service registry
            """
            self.registry = registry
            self.request_count = 0
            self.route_map = {
                'summarize': 'summarization',
                'translate': 'translation',
                'sentiment': 'sentiment'
            }
        
        def route(self, operation: str, data: Dict[str, Any]) -> ServiceResponse:
            """
            Route request to appropriate service.
            
            Args:
                operation: Operation to perform
                data: Request data
                
            Returns:
                ServiceResponse
            """
            request_id = f"req_{self.request_count}"
            self.request_count += 1
            
            start_time = time.time()
            
            # Determine target service
            service_name = self.route_map.get(operation)
            
            if not service_name:
                return ServiceResponse(
                    request_id=request_id,
                    service_name="unknown",
                    status_code=404,
                    data=None,
                    error=f"Unknown operation: {operation}",
                    duration=time.time() - start_time
                )
            
            # Discover service
            service = self.registry.discover(service_name)
            
            if not service:
                return ServiceResponse(
                    request_id=request_id,
                    service_name=service_name,
                    status_code=503,
                    data=None,
                    error=f"Service unavailable: {service_name}",
                    duration=time.time() - start_time
                )
            
            # Forward request to service (simulated)
            try:
                # In real implementation, would make HTTP request
                # Here we simulate the service call
                result_data = {
                    'status': 'success',
                    'operation': operation,
                    'service': service_name
                }
                
                return ServiceResponse(
                    request_id=request_id,
                    service_name=service_name,
                    status_code=200,
                    data=result_data,
                    duration=time.time() - start_time
                )
                
            except Exception as e:
                return ServiceResponse(
                    request_id=request_id,
                    service_name=service_name,
                    status_code=500,
                    data=None,
                    error=str(e),
                    duration=time.time() - start_time
                )
    
    
    class MicroserviceOrchestrator:
        """
        Orchestrator for coordinating multiple microservices.
        
        Manages service lifecycle and coordinates complex workflows
        across multiple services.
        """
        
        def __init__(self):
            """Initialize orchestrator"""
            self.registry = ServiceRegistry()
            self.gateway = APIGateway(self.registry)
            self.services: Dict[str, AgentService] = {}
        
        def deploy_service(self, service: AgentService) -> bool:
            """
            Deploy a microservice.
            
            Args:
                service: Agent service to deploy
                
            Returns:
                True if deployed successfully
            """
            # Start service
            service.start()
            
            # Register with registry
            service_info = service.get_info()
            self.registry.register(service_info)
            
            # Track service
            self.services[service.service_id] = service
            
            return True
        
        def undeploy_service(self, service_id: str) -> bool:
            """
            Undeploy a microservice.
            
            Args:
                service_id: Service ID
                
            Returns:
                True if undeployed successfully
            """
            if service_id in self.services:
                service = self.services[service_id]
                service.stop()
                self.registry.deregister(service_id)
                del self.services[service_id]
                return True
            return False
        
        def execute_workflow(
            self,
            operations: List[Dict[str, Any]]
        ) -> List[ServiceResponse]:
            """
            Execute a workflow across multiple services.
            
            Args:
                operations: List of operations with data
                
            Returns:
                List of service responses
            """
            responses = []
            
            for op in operations:
                operation = op.get('operation')
                data = op.get('data', {})
                
                response = self.gateway.route(operation, data)
                responses.append(response)
            
            return responses
        
        def get_service_health(self) -> Dict[str, Any]:
            """
            Get health status of all services.
            
            Returns:
                Health status dictionary
            """
            services = self.registry.list_services()
            
            health_status = {
                'total_services': len(services),
                'healthy': 0,
                'unhealthy': 0,
                'services': []
            }
            
            for service in services:
                status = self.registry.health_check(service.service_id)
                
                if status == ServiceStatus.HEALTHY:
                    health_status['healthy'] += 1
                else:
                    health_status['unhealthy'] += 1
                
                health_status['services'].append({
                    'name': service.service_name,
                    'id': service.service_id,
                    'status': status.value,
                    'version': service.version
                })
            
            return health_status
    
    
    def demonstrate_microservice_architecture():
        """Demonstrate the microservice agent architecture"""
        print("=" * 80)
        print("MICROSERVICE AGENT ARCHITECTURE DEMONSTRATION")
        print("=" * 80)
        
        orchestrator = MicroserviceOrchestrator()
        
        # Demo 1: Deploy Microservices
        print("\n" + "=" * 80)
        print("DEMO 1: Deploying Microservices")
        print("=" * 80)
        
        # Deploy services
        services = [
            SummarizationService(),
            TranslationService(),
            SentimentService()
        ]
        
        print("\nDeploying services...")
        for service in services:
            success = orchestrator.deploy_service(service)
            print(f"  ✓ Deployed: {service.service_name} v{service.version} on port {service.port}")
        
        # Demo 2: Service Discovery
        print("\n" + "=" * 80)
        print("DEMO 2: Service Discovery")
        print("=" * 80)
        
        print("\nRegistered Services:")
        print("-" * 80)
        
        for service_info in orchestrator.registry.list_services():
            print(f"\nService: {service_info.service_name}")
            print(f"  ID: {service_info.service_id}")
            print(f"  Version: {service_info.version}")
            print(f"  Status: {service_info.status.value}")
            print(f"  Endpoint: {service_info.host}:{service_info.port}")
            print(f"  Protocol: {service_info.protocol.value}")
        
        # Demo 3: Service Health Checks
        print("\n" + "=" * 80)
        print("DEMO 3: Service Health Monitoring")
        print("=" * 80)
        
        health = orchestrator.get_service_health()
        
        print(f"\nCluster Health:")
        print(f"  Total Services: {health['total_services']}")
        print(f"  Healthy: {health['healthy']}")
        print(f"  Unhealthy: {health['unhealthy']}")
        
        print(f"\nService Status:")
        for svc in health['services']:
            status_icon = "✓" if svc['status'] == 'healthy' else "✗"
            print(f"  {status_icon} {svc['name']} ({svc['version']}) - {svc['status']}")
        
        # Demo 4: API Gateway Routing
        print("\n" + "=" * 80)
        print("DEMO 4: API Gateway Request Routing")
        print("=" * 80)
        
        print("\nRouting requests through API Gateway...")
        print("-" * 80)
        
        requests = [
            {'operation': 'summarize', 'data': {'text': 'Sample text', 'length': 'short'}},
            {'operation': 'translate', 'data': {'text': 'Hello', 'target_language': 'French'}},
            {'operation': 'sentiment', 'data': {'text': 'Great product!'}}
        ]
        
        for req in requests:
            response = orchestrator.gateway.route(req['operation'], req['data'])
            
            print(f"\nRequest: {req['operation']}")
            print(f"  Request ID: {response.request_id}")
            print(f"  Service: {response.service_name}")
            print(f"  Status Code: {response.status_code}")
            print(f"  Duration: {response.duration*1000:.2f}ms")
            
            if response.error:
                print(f"  Error: {response.error}")
        
        # Demo 5: Direct Service Calls
        print("\n" + "=" * 80)
        print("DEMO 5: Direct Service Invocation")
        print("=" * 80)
        
        print("\nCalling services directly...")
        print("-" * 80)
        
        # Summarization
        print("\n1. Summarization Service:")
        summarization_svc = services[0]
        result = summarization_svc.process({
            'text': 'Microservices architecture enables building complex systems through independently deployable services.',
            'length': 'very short'
        })
        print(f"   Input: Long text about microservices")
        print(f"   Summary: {result['summary'][:80]}...")
        
        # Translation
        print("\n2. Translation Service:")
        translation_svc = services[1]
        result = translation_svc.process({
            'text': 'Hello, how are you?',
            'target_language': 'Spanish'
        })
        print(f"   Input: 'Hello, how are you?'")
        print(f"   Translation: {result['translation']}")
        
        # Sentiment
        print("\n3. Sentiment Service:")
        sentiment_svc = services[2]
        result = sentiment_svc.process({
            'text': 'This microservice architecture is fantastic!'
        })
        print(f"   Input: 'This microservice architecture is fantastic!'")
        print(f"   Sentiment: {result['sentiment']} (confidence: {result['confidence']:.2f})")
        
        # Demo 6: Multi-Service Workflow
        print("\n" + "=" * 80)
        print("DEMO 6: Orchestrated Multi-Service Workflow")
        print("=" * 80)
        
        workflow = [
            {'operation': 'summarize', 'data': {'text': 'Long article text...', 'length': 'short'}},
            {'operation': 'translate', 'data': {'text': 'Summary text', 'target_language': 'German'}},
            {'operation': 'sentiment', 'data': {'text': 'Translated summary'}}
        ]
        
        print("\nExecuting workflow:")
        print("  1. Summarize → 2. Translate → 3. Analyze Sentiment")
        print("-" * 80)
        
        responses = orchestrator.execute_workflow(workflow)
        
        print(f"\nWorkflow Results:")
        for i, response in enumerate(responses, 1):
            print(f"\n  Step {i}: {response.service_name}")
            print(f"    Status: {response.status_code}")
            print(f"    Duration: {response.duration*1000:.2f}ms")
        
        total_duration = sum(r.duration for r in responses)
        print(f"\n  Total Workflow Duration: {total_duration*1000:.2f}ms")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        
        summary = """
    The Microservice Agent Architecture demonstrates distributed AI systems:
    
    KEY CAPABILITIES:
    1. Service Registry: Automatic service discovery and registration
    2. API Gateway: Single entry point with intelligent routing
    3. Independent Services: Each capability as a separate microservice
    4. Health Monitoring: Continuous health checks and status tracking
    5. Service Orchestration: Coordinate complex multi-service workflows
    6. Fault Isolation: Failures contained within services
    7. Load Balancing: Distribute traffic across service instances
    8. Technology Diversity: Each service can use different tech stacks
    
    BENEFITS:
    - Independent deployment and scaling
    - Fault isolation and resilience
    - Technology flexibility per service
    - Team autonomy and parallel development
    - Easier testing and maintenance
    - Better resource utilization
    - Scalability for specific services
    - Faster iteration and releases
    
    USE CASES:
    - Large-scale AI platforms with multiple capabilities
    - Multi-tenant SaaS AI applications
    - Enterprise AI systems requiring high availability
    - Cloud-native AI applications
    - Distributed agent coordination
    - Microservices-based RAG systems
    - AI-powered e-commerce platforms
    - Scalable chatbot infrastructures
    - Multi-model serving platforms
    - Agent mesh architectures
    
    PRODUCTION CONSIDERATIONS:
    1. Service Discovery: Use Consul, etcd, or Kubernetes DNS
    2. API Gateway: Kong, NGINX, or AWS API Gateway
    3. Communication: REST APIs, gRPC, or message queues
    4. Monitoring: Distributed tracing (Jaeger, Zipkin)
    5. Logging: Centralized logging (ELK stack)
    6. Configuration: External config service (Spring Cloud Config)
    7. Security: API authentication, mTLS between services
    8. Circuit Breakers: Prevent cascading failures
    9. Container Orchestration: Kubernetes, Docker Swarm
    10. Service Mesh: Istio, Linkerd for advanced features
    
    ADVANCED EXTENSIONS:
    - Event-driven architecture with Kafka/RabbitMQ
    - CQRS pattern for read/write separation
    - Saga pattern for distributed transactions
    - API versioning and backward compatibility
    - Blue-green deployments
    - Canary releases for gradual rollouts
    - Auto-scaling based on metrics
    - Multi-region deployment
    - Service mesh for advanced traffic management
    - GraphQL federation for unified API
    
    Microservice architecture is essential for building scalable,
    maintainable, and resilient AI systems in production environments.
    """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_microservice_architecture()
    
        

    Agentic Design Pattern: Microservice Agent

    Pattern 081: Microservice Agent – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 081: Microservice Agent Architecture

    Description: A Microservice Agent Architecture decomposes AI agents into independently deployable microservices that communicate via well-defined APIs. This pattern enables scalability, fault isolation, independent deployment, and technology diversity across agent components. Each microservice can be developed, deployed, and scaled independently while maintaining loose coupling.

    The architecture supports distributed agent systems with service discovery,
    load balancing, inter-service communication, circuit breakers, and observability
    across the microservices mesh.
    

    Components: 1. Agent Services: Independent agent microservices 2. API Gateway: Entry point and routing 3. Service Registry: Service discovery and health checks 4. Message Bus: Async inter-service communication 5. Load Balancer: Distribute requests across instances 6. Circuit Breaker: Fault tolerance and isolation 7. Config Service: Centralized configuration 8. Monitoring: Distributed tracing and metrics

    Key Features: – Independent service deployment – Service discovery and registration – Inter-service communication (REST, gRPC, message queues) – Load balancing and auto-scaling – Fault isolation and circuit breakers – Distributed tracing – API gateway pattern – Health checks and monitoring – Polyglot persistence – Technology diversity

    Use Cases: – Large-scale AI platforms – Multi-tenant AI services – Enterprise AI systems – Cloud-native AI applications – Distributed agent coordination – Scalable chatbot platforms – AI-powered SaaS products – Microservices-based RAG systems – Agent mesh architectures – Multi-model serving platforms

    LangChain Implementation: Uses FastAPI for service endpoints, service registry pattern, inter-service HTTP communication, and distributed agent coordination.


    File Statistics

    • Total Lines: 869
    • Classes: 12
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 081: Microservice Agent Architecture
    
    Description:
        A Microservice Agent Architecture decomposes AI agents into independently
        deployable microservices that communicate via well-defined APIs. This pattern
        enables scalability, fault isolation, independent deployment, and technology
        diversity across agent components. Each microservice can be developed, deployed,
        and scaled independently while maintaining loose coupling.
    
        The architecture supports distributed agent systems with service discovery,
        load balancing, inter-service communication, circuit breakers, and observability
        across the microservices mesh.
    
    Components:
        1. Agent Services: Independent agent microservices
        2. API Gateway: Entry point and routing
        3. Service Registry: Service discovery and health checks
        4. Message Bus: Async inter-service communication
        5. Load Balancer: Distribute requests across instances
        6. Circuit Breaker: Fault tolerance and isolation
        7. Config Service: Centralized configuration
        8. Monitoring: Distributed tracing and metrics
    
    Key Features:
        - Independent service deployment
        - Service discovery and registration
        - Inter-service communication (REST, gRPC, message queues)
        - Load balancing and auto-scaling
        - Fault isolation and circuit breakers
        - Distributed tracing
        - API gateway pattern
        - Health checks and monitoring
        - Polyglot persistence
        - Technology diversity
    
    Use Cases:
        - Large-scale AI platforms
        - Multi-tenant AI services
        - Enterprise AI systems
        - Cloud-native AI applications
        - Distributed agent coordination
        - Scalable chatbot platforms
        - AI-powered SaaS products
        - Microservices-based RAG systems
        - Agent mesh architectures
        - Multi-model serving platforms
    
    LangChain Implementation:
        Uses FastAPI for service endpoints, service registry pattern,
        inter-service HTTP communication, and distributed agent coordination.
    """
    

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


    Import Statements

    Standard Library Imports

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

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

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from dotenv.load_dotenv
    from uuid
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: ServiceStatus

    Lines 72-78

    Inherits from: Enum

    Purpose:

    Status of a microservice
    

    Class: CommunicationProtocol

    Lines 81-86

    Inherits from: Enum

    Purpose:

    Inter-service communication protocol
    

    Class: ServiceInfo

    Lines 90-103

    Purpose:

    Information about a microservice
    

    Class: ServiceRequest

    Lines 107-115

    Purpose:

    Request to a microservice
    

    Class: ServiceResponse

    Lines 119-127

    Purpose:

    Response from a microservice
    

    Class: ServiceRegistry

    Lines 130-218

    Purpose:

    Service registry for microservice discovery.
    
    Maintains a registry of available services with health monitoring.
    

    Methods:

    __init__(self)

    Lines 137-140

    Purpose:

    Initialize service registry
    

    Parameters:

    register(self, service)

    Lines 142-154

    Purpose:

    Register a service.
    
    Args:
        service: Service information
    
    Returns:
        True if registered successfully
    

    Parameters: – service: Service

    Returns: bool

    deregister(self, service_id)

    Lines 156-171

    Purpose:

    Deregister a service.
    
    Args:
        service_id: Service ID
    
    Returns:
        True if deregistered successfully
    

    Parameters: – service_id: Service id

    Returns: bool

    discover(self, service_name)

    Lines 173-191

    Purpose:

    Discover a healthy service instance.
    
    Args:
        service_name: Name of service to discover
    
    Returns:
        ServiceInfo if found, None otherwise
    

    Parameters: – service_name: Service name

    Returns: <ast.Subscript object at 0x000001A06D7FF2D0>

    health_check(self, service_id)

    Lines 193-209

    Purpose:

    Check health of a service.
    
    Args:
        service_id: Service ID
    
    Returns:
        Service status
    

    Parameters: – service_id: Service id

    Returns: ServiceStatus

    list_services(self)

    Lines 211-218

    Purpose:

    List all registered services.
    
    Returns:
        List of service information
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D7FE710>


    Class: AgentService

    Lines 221-318

    Purpose:

    Base microservice for AI agents.
    
    Each agent capability is encapsulated as an independent service.
    

    Methods:

    __init__(self, service_name, version, port)

    Lines 228-253

    Purpose:

    Initialize agent service.
    
    Args:
        service_name: Name of the service
        version: Service version
        port: Port number
    

    Parameters: – service_name: Service name – version: Version – port: Port

    get_info(self)

    Lines 255-272

    Purpose:

    Get service information.
    
    Returns:
        ServiceInfo object
    

    Parameters:

    Returns: ServiceInfo

    start(self)

    Lines 274-282

    Purpose:

    Start the service.
    
    Returns:
        True if started successfully
    

    Parameters:

    Returns: bool

    stop(self)

    Lines 284-292

    Purpose:

    Stop the service.
    
    Returns:
        True if stopped successfully
    

    Parameters:

    Returns: bool

    health(self)

    Lines 294-306

    Purpose:

    Health check endpoint.
    
    Returns:
        Health status
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D7BB390>

    process(self, data)

    Lines 308-318

    Purpose:

    Process a request (to be overridden by subclasses).
    
    Args:
        data: Request data
    
    Returns:
        Response data
    

    Parameters: – data: Data

    Returns: <ast.Subscript object at 0x000001A06D7BADD0>


    Class: SummarizationService

    Lines 321-354

    Inherits from: AgentService

    Purpose:

    Microservice for text summarization
    

    Methods:

    __init__(self)

    Lines 324-325

    Purpose: Initialize the class instance with required parameters.

    Parameters:

    process(self, data)

    Lines 327-354

    Purpose:

    Summarize text.
    
    Args:
        data: {'text': str, 'length': str}
    
    Returns:
        {'summary': str}
    

    Parameters: – data: Data

    Returns: <ast.Subscript object at 0x000001A06D74A3D0>


    Class: TranslationService

    Lines 357-391

    Inherits from: AgentService

    Purpose:

    Microservice for translation
    

    Methods:

    __init__(self)

    Lines 360-361

    Purpose: Initialize the class instance with required parameters.

    Parameters:

    process(self, data)

    Lines 363-391

    Purpose:

    Translate text.
    
    Args:
        data: {'text': str, 'target_language': str}
    
    Returns:
        {'translation': str}
    

    Parameters: – data: Data

    Returns: <ast.Subscript object at 0x000001A06D72DB90>


    Class: SentimentService

    Lines 394-441

    Inherits from: AgentService

    Purpose:

    Microservice for sentiment analysis
    

    Methods:

    __init__(self)

    Lines 397-398

    Purpose: Initialize the class instance with required parameters.

    Parameters:

    process(self, data)

    Lines 400-441

    Purpose:

    Analyze sentiment.
    
    Args:
        data: {'text': str}
    
    Returns:
        {'sentiment': str, 'confidence': float}
    

    Parameters: – data: Data

    Returns: <ast.Subscript object at 0x000001A06D6061D0>


    Class: APIGateway

    Lines 444-534

    Purpose:

    API Gateway for routing requests to microservices.
    
    Provides a single entry point and routes to appropriate services.
    

    Methods:

    __init__(self, registry)

    Lines 451-464

    Purpose:

    Initialize API gateway.
    
    Args:
        registry: Service registry
    

    Parameters: – registry: Registry

    route(self, operation, data)

    Lines 466-534

    Purpose:

    Route request to appropriate service.
    
    Args:
        operation: Operation to perform
        data: Request data
    
    Returns:
        ServiceResponse
    

    Parameters: – operation: Operation – data: Data

    Returns: ServiceResponse


    Class: MicroserviceOrchestrator

    Lines 537-646

    Purpose:

    Orchestrator for coordinating multiple microservices.
    
    Manages service lifecycle and coordinates complex workflows
    across multiple services.
    

    Methods:

    __init__(self)

    Lines 545-549

    Purpose:

    Initialize orchestrator
    

    Parameters:

    deploy_service(self, service)

    Lines 551-571

    Purpose:

    Deploy a microservice.
    
    Args:
        service: Agent service to deploy
    
    Returns:
        True if deployed successfully
    

    Parameters: – service: Service

    Returns: bool

    undeploy_service(self, service_id)

    Lines 573-589

    Purpose:

    Undeploy a microservice.
    
    Args:
        service_id: Service ID
    
    Returns:
        True if undeployed successfully
    

    Parameters: – service_id: Service id

    Returns: bool

    execute_workflow(self, operations)

    Lines 591-613

    Purpose:

    Execute a workflow across multiple services.
    
    Args:
        operations: List of operations with data
    
    Returns:
        List of service responses
    

    Parameters: – operations: Operations

    Returns: <ast.Subscript object at 0x000001A06D7F4BD0>

    get_service_health(self)

    Lines 615-646

    Purpose:

    Get health status of all services.
    
    Returns:
        Health status dictionary
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D739690>


    Functions

    demonstrate_microservice_architecture()

    Lines 649-864

    Purpose:

    Demonstrate the microservice agent architecture
    

    Key Concepts Summary

    Pattern Overview

    The Microservice Agent 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 080
    • Pattern 082

    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 Microservice Agent 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: 081_microservice_agent.py
    Total Lines: 869
    Pattern Category: AI Agent Design Pattern #081


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Async Agent

    
        """
    Pattern 080: Asynchronous Agent
    
    Description:
        An Asynchronous Agent uses async/await patterns to handle concurrent operations
        without blocking. This pattern is essential for building high-performance,
        scalable AI applications that can handle multiple requests simultaneously,
        make concurrent API calls, and provide responsive user experiences.
        
        The agent leverages Python's asyncio for non-blocking I/O operations, enabling
        efficient resource utilization and improved throughput. It supports concurrent
        LLM calls, parallel tool execution, and async streaming for maximum performance.
    
    Components:
        1. Async Executor: Manages async task execution
        2. Concurrent Runner: Runs multiple tasks concurrently
        3. Async LLM Client: Non-blocking LLM interactions
        4. Task Scheduler: Schedules and coordinates async tasks
        5. Result Collector: Aggregates concurrent results
        6. Error Handler: Handles async exceptions
        7. Timeout Manager: Enforces timeouts on async operations
        8. Resource Pool: Manages async resources and connections
    
    Key Features:
        - Non-blocking async/await operations
        - Concurrent LLM calls
        - Parallel tool execution
        - Async streaming
        - Task cancellation
        - Timeout handling
        - Connection pooling
        - Backpressure management
        - Error propagation
        - Graceful shutdown
    
    Use Cases:
        - High-throughput API servers
        - Real-time chat applications
        - Concurrent data processing
        - Parallel research queries
        - Multi-source information gathering
        - Async web scraping
        - Real-time analytics
        - Concurrent translation services
        - Parallel validation
        - Multi-agent coordination
    
    LangChain Implementation:
        Uses ChatOpenAI with async methods (ainvoke, astream, abatch),
        asyncio for concurrency, and async LCEL chains.
    """
    
    import os
    import asyncio
    import time
    from typing import List, Dict, Any, Optional, Coroutine, Callable
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    from langchain_core.runnables import RunnablePassthrough
    
    load_dotenv()
    
    
    class TaskStatus(Enum):
        """Status of async task"""
        PENDING = "pending"
        RUNNING = "running"
        COMPLETED = "completed"
        FAILED = "failed"
        CANCELLED = "cancelled"
        TIMEOUT = "timeout"
    
    
    @dataclass
    class AsyncTask:
        """An asynchronous task"""
        task_id: str
        name: str
        coroutine: Coroutine
        status: TaskStatus = TaskStatus.PENDING
        result: Optional[Any] = None
        error: Optional[str] = None
        started_at: Optional[datetime] = None
        completed_at: Optional[datetime] = None
        duration: float = 0.0
    
    
    @dataclass
    class AsyncResult:
        """Result of async operation"""
        task_id: str
        success: bool
        result: Optional[Any] = None
        error: Optional[str] = None
        duration: float = 0.0
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class ConcurrentResult:
        """Result of concurrent operations"""
        total_tasks: int
        successful: int
        failed: int
        cancelled: int
        results: List[AsyncResult]
        total_duration: float
        average_duration: float
    
    
    class AsyncAgent:
        """
        Agent that uses async/await for non-blocking operations.
        
        This agent enables high-performance concurrent processing with
        async LLM calls, parallel tool execution, and efficient resource usage.
        """
        
        def __init__(self, max_concurrent: int = 10, timeout: float = 30.0):
            """
            Initialize the async agent.
            
            Args:
                max_concurrent: Maximum concurrent tasks
                timeout: Default timeout for operations
            """
            self.max_concurrent = max_concurrent
            self.timeout = timeout
            
            # Async LLM
            self.llm = ChatOpenAI(
                model="gpt-3.5-turbo",
                temperature=0.7
            )
            
            # Task tracking
            self.tasks: Dict[str, AsyncTask] = {}
            self.semaphore = asyncio.Semaphore(max_concurrent)
        
        async def ainvoke_llm(
            self,
            prompt: str,
            timeout: Optional[float] = None
        ) -> str:
            """
            Invoke LLM asynchronously.
            
            Args:
                prompt: Input prompt
                timeout: Optional timeout
                
            Returns:
                LLM response
            """
            timeout = timeout or self.timeout
            
            try:
                response = await asyncio.wait_for(
                    self.llm.ainvoke(prompt),
                    timeout=timeout
                )
                return response.content
            except asyncio.TimeoutError:
                raise TimeoutError(f"LLM call timed out after {timeout}s")
        
        async def astream_llm(
            self,
            prompt: str,
            on_token: Optional[Callable[[str], None]] = None
        ) -> str:
            """
            Stream LLM response asynchronously.
            
            Args:
                prompt: Input prompt
                on_token: Optional token callback
                
            Returns:
                Full response
            """
            full_response = ""
            
            async for chunk in self.llm.astream(prompt):
                if hasattr(chunk, 'content'):
                    token = chunk.content
                    full_response += token
                    
                    if on_token:
                        on_token(token)
            
            return full_response
        
        async def run_concurrent_tasks(
            self,
            tasks: List[Coroutine],
            task_names: Optional[List[str]] = None
        ) -> ConcurrentResult:
            """
            Run multiple tasks concurrently.
            
            Args:
                tasks: List of coroutines to execute
                task_names: Optional names for tasks
                
            Returns:
                ConcurrentResult with all results
            """
            start_time = time.time()
            
            if task_names is None:
                task_names = [f"task_{i}" for i in range(len(tasks))]
            
            # Run tasks concurrently with semaphore
            async def run_with_semaphore(coro: Coroutine, name: str) -> AsyncResult:
                async with self.semaphore:
                    task_start = time.time()
                    task_id = f"{name}_{id(coro)}"
                    
                    try:
                        result = await coro
                        return AsyncResult(
                            task_id=task_id,
                            success=True,
                            result=result,
                            duration=time.time() - task_start
                        )
                    except Exception as e:
                        return AsyncResult(
                            task_id=task_id,
                            success=False,
                            error=str(e),
                            duration=time.time() - task_start
                        )
            
            # Execute all tasks
            results = await asyncio.gather(
                *[run_with_semaphore(task, name) for task, name in zip(tasks, task_names)],
                return_exceptions=True
            )
            
            # Process results
            async_results = []
            successful = 0
            failed = 0
            cancelled = 0
            
            for result in results:
                if isinstance(result, AsyncResult):
                    async_results.append(result)
                    if result.success:
                        successful += 1
                    else:
                        failed += 1
                elif isinstance(result, asyncio.CancelledError):
                    cancelled += 1
                else:
                    failed += 1
            
            total_duration = time.time() - start_time
            avg_duration = sum(r.duration for r in async_results) / len(async_results) if async_results else 0
            
            return ConcurrentResult(
                total_tasks=len(tasks),
                successful=successful,
                failed=failed,
                cancelled=cancelled,
                results=async_results,
                total_duration=total_duration,
                average_duration=avg_duration
            )
        
        async def parallel_llm_calls(
            self,
            prompts: List[str],
            timeout: Optional[float] = None
        ) -> List[str]:
            """
            Make multiple LLM calls in parallel.
            
            Args:
                prompts: List of prompts
                timeout: Optional timeout per call
                
            Returns:
                List of responses
            """
            tasks = [self.ainvoke_llm(prompt, timeout) for prompt in prompts]
            result = await self.run_concurrent_tasks(
                tasks,
                [f"llm_call_{i}" for i in range(len(prompts))]
            )
            
            return [r.result for r in result.results if r.success]
        
        async def concurrent_chain_execution(
            self,
            inputs: List[Dict[str, str]],
            template: str
        ) -> List[str]:
            """
            Execute chain concurrently with different inputs.
            
            Args:
                inputs: List of input dictionaries
                template: Prompt template
                
            Returns:
                List of results
            """
            prompt = ChatPromptTemplate.from_template(template)
            chain = prompt | self.llm | StrOutputParser()
            
            # Create async tasks for each input
            tasks = [chain.ainvoke(input_dict) for input_dict in inputs]
            
            result = await self.run_concurrent_tasks(
                tasks,
                [f"chain_{i}" for i in range(len(inputs))]
            )
            
            return [r.result for r in result.results if r.success]
        
        async def parallel_with_timeout(
            self,
            tasks: List[Coroutine],
            timeout: float
        ) -> List[Optional[Any]]:
            """
            Run tasks in parallel with timeout.
            
            Args:
                tasks: List of coroutines
                timeout: Timeout for all tasks
                
            Returns:
                List of results (None for timed out tasks)
            """
            try:
                results = await asyncio.wait_for(
                    asyncio.gather(*tasks, return_exceptions=True),
                    timeout=timeout
                )
                
                return [
                    r if not isinstance(r, Exception) else None
                    for r in results
                ]
            except asyncio.TimeoutError:
                return [None] * len(tasks)
        
        async def async_map(
            self,
            func: Callable[[Any], Coroutine],
            items: List[Any],
            batch_size: Optional[int] = None
        ) -> List[Any]:
            """
            Map an async function over items with optional batching.
            
            Args:
                func: Async function to map
                items: Items to process
                batch_size: Optional batch size for processing
                
            Returns:
                List of results
            """
            if batch_size is None:
                batch_size = self.max_concurrent
            
            results = []
            
            for i in range(0, len(items), batch_size):
                batch = items[i:i + batch_size]
                batch_tasks = [func(item) for item in batch]
                batch_results = await asyncio.gather(*batch_tasks)
                results.extend(batch_results)
            
            return results
        
        async def race_tasks(
            self,
            tasks: List[Coroutine],
            return_first: bool = True
        ) -> Any:
            """
            Race multiple tasks, return first to complete.
            
            Args:
                tasks: List of coroutines to race
                return_first: Return first completed (True) or all (False)
                
            Returns:
                First result or all results
            """
            if return_first:
                done, pending = await asyncio.wait(
                    tasks,
                    return_when=asyncio.FIRST_COMPLETED
                )
                
                # Cancel remaining tasks
                for task in pending:
                    task.cancel()
                
                # Return first result
                return list(done)[0].result()
            else:
                return await asyncio.gather(*tasks)
        
        async def retry_async(
            self,
            coro: Coroutine,
            max_retries: int = 3,
            backoff: float = 1.0
        ) -> Any:
            """
            Retry an async operation with exponential backoff.
            
            Args:
                coro: Coroutine to retry
                max_retries: Maximum retry attempts
                backoff: Initial backoff time
                
            Returns:
                Result of successful attempt
            """
            last_exception = None
            
            for attempt in range(max_retries):
                try:
                    return await coro
                except Exception as e:
                    last_exception = e
                    if attempt  str:
            await asyncio.sleep(0.1)  # Simulate work
            return await agent.ainvoke_llm(f"Count to {n} and stop.")
        
        tasks = [sample_task(i) for i in range(1, 6)]
        task_names = [f"count_to_{i}" for i in range(1, 6)]
        
        print(f"\nRunning {len(tasks)} concurrent tasks...")
        print("-" * 80)
        
        result = await agent.run_concurrent_tasks(tasks, task_names)
        
        print(f"✓ All tasks completed!")
        print(f"\nStatistics:")
        print(f"  Total Tasks: {result.total_tasks}")
        print(f"  Successful: {result.successful}")
        print(f"  Failed: {result.failed}")
        print(f"  Total Duration: {result.total_duration:.2f}s")
        print(f"  Average Duration: {result.average_duration:.2f}s")
        
        # Demo 4: Async Streaming
        print("\n" + "=" * 80)
        print("DEMO 4: Asynchronous Streaming")
        print("=" * 80)
        
        prompt = "Write a haiku about async programming."
        
        print(f"\nPrompt: {prompt}")
        print("\nStreaming response asynchronously:")
        print("-" * 80)
        
        tokens = []
        
        def on_token(token: str):
            tokens.append(token)
            print(token, end="", flush=True)
        
        stream_response = await agent.astream_llm(prompt, on_token=on_token)
        
        print("\n" + "-" * 80)
        print(f"✓ Streaming complete! ({len(tokens)} tokens)")
        
        # Demo 5: Task Racing
        print("\n" + "=" * 80)
        print("DEMO 5: Task Racing (First to Complete)")
        print("=" * 80)
        
        async def llm_task(question: str, delay: float) -> str:
            await asyncio.sleep(delay)
            return await agent.ainvoke_llm(question)
        
        race_tasks = [
            llm_task("Quick answer: What is AI?", 0.1),
            llm_task("Quick answer: What is ML?", 0.2),
            llm_task("Quick answer: What is DL?", 0.3)
        ]
        
        print("\nRacing 3 tasks, returning first to complete...")
        print("-" * 80)
        
        race_start = time.time()
        first_result = await agent.race_tasks(race_tasks, return_first=True)
        race_duration = time.time() - race_start
        
        print(f"✓ First task completed in {race_duration:.2f}s")
        print(f"\nResult: {first_result[:150]}...")
        
        # Demo 6: Async vs Sync Performance
        print("\n" + "=" * 80)
        print("DEMO 6: Async vs Synchronous Performance")
        print("=" * 80)
        
        test_prompts = [f"Answer: What is {topic}?" for topic in ["AI", "ML", "DL", "NLP", "CV"]]
        
        print(f"\nProcessing {len(test_prompts)} prompts...")
        
        # Async approach
        print("\nAsync Approach:")
        async_start = time.time()
        async_results = await agent.parallel_llm_calls(test_prompts[:3])  # Limit for demo
        async_duration = time.time() - async_start
        print(f"  Duration: {async_duration:.2f}s")
        print(f"  Results: {len(async_results)} responses")
        
        # Sync approach (simulated)
        print("\nSync Approach (estimated):")
        estimated_sync = async_duration * 3  # Rough estimate
        print(f"  Estimated Duration: {estimated_sync:.2f}s")
        
        speedup = estimated_sync / async_duration if async_duration > 0 else 0
        print(f"\nSpeedup: {speedup:.2f}x faster with async")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        
        summary = """
    The Asynchronous Agent demonstrates non-blocking concurrent operations:
    
    KEY CAPABILITIES:
    1. Async LLM Calls: Non-blocking LLM interactions with ainvoke
    2. Parallel Execution: Run multiple tasks concurrently
    3. Async Streaming: Non-blocking streaming responses
    4. Concurrent Chains: Execute chains with different inputs in parallel
    5. Task Racing: Return first completed result
    6. Timeout Handling: Enforce timeouts on async operations
    7. Error Handling: Graceful exception handling in async context
    8. Resource Management: Semaphore-based concurrency control
    
    BENEFITS:
    - Dramatically improved throughput
    - Non-blocking I/O for better resource utilization
    - Lower latency for concurrent operations
    - Scalable to handle many simultaneous requests
    - Efficient handling of I/O-bound workloads
    - Better responsiveness in applications
    - Reduced total execution time
    - Enhanced user experience
    
    USE CASES:
    - High-throughput API servers
    - Real-time chat applications with multiple users
    - Concurrent data processing pipelines
    - Parallel research and information gathering
    - Multi-source data aggregation
    - Async web scraping
    - Real-time analytics dashboards
    - Concurrent translation services
    - Parallel validation and checking
    - Multi-agent systems coordination
    
    PRODUCTION CONSIDERATIONS:
    1. Concurrency Limits: Use semaphores to control concurrent tasks
    2. Timeout Management: Always set timeouts for async operations
    3. Error Propagation: Handle exceptions in async context properly
    4. Resource Cleanup: Ensure proper cleanup of async resources
    5. Backpressure: Handle slow consumers gracefully
    6. Connection Pooling: Reuse connections efficiently
    7. Monitoring: Track async task performance
    8. Testing: Test with asyncio.run() and pytest-asyncio
    9. Event Loop: Manage event loop lifecycle carefully
    10. Cancellation: Support graceful task cancellation
    
    ADVANCED EXTENSIONS:
    - Async context managers for resource management
    - Event-driven architectures with async events
    - Async queues for producer-consumer patterns
    - Async locks and synchronization primitives
    - Distributed async processing
    - Async retry with circuit breakers
    - Priority-based async task scheduling
    - Async result caching
    - Streaming async aggregation
    - Async middleware chains
    
    Async patterns are essential for building high-performance,
    scalable AI applications that handle concurrent operations efficiently.
    """
        
        print(summary)
    
    
    def demonstrate_async_agent_sync():
        """Synchronous wrapper for async demonstration"""
        asyncio.run(demonstrate_async_agent())
    
    
    if __name__ == "__main__":
        demonstrate_async_agent_sync()
    
        

    Agentic Design Pattern: Async Agent

    Pattern 080: Async Agent – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 080: Asynchronous Agent

    Description: An Asynchronous Agent uses async/await patterns to handle concurrent operations without blocking. This pattern is essential for building high-performance, scalable AI applications that can handle multiple requests simultaneously, make concurrent API calls, and provide responsive user experiences.

    The agent leverages Python's asyncio for non-blocking I/O operations, enabling
    efficient resource utilization and improved throughput. It supports concurrent
    LLM calls, parallel tool execution, and async streaming for maximum performance.
    

    Components: 1. Async Executor: Manages async task execution 2. Concurrent Runner: Runs multiple tasks concurrently 3. Async LLM Client: Non-blocking LLM interactions 4. Task Scheduler: Schedules and coordinates async tasks 5. Result Collector: Aggregates concurrent results 6. Error Handler: Handles async exceptions 7. Timeout Manager: Enforces timeouts on async operations 8. Resource Pool: Manages async resources and connections

    Key Features: – Non-blocking async/await operations – Concurrent LLM calls – Parallel tool execution – Async streaming – Task cancellation – Timeout handling – Connection pooling – Backpressure management – Error propagation – Graceful shutdown

    Use Cases: – High-throughput API servers – Real-time chat applications – Concurrent data processing – Parallel research queries – Multi-source information gathering – Async web scraping – Real-time analytics – Concurrent translation services – Parallel validation – Multi-agent coordination

    LangChain Implementation: Uses ChatOpenAI with async methods (ainvoke, astream, abatch), asyncio for concurrency, and async LCEL chains.


    File Statistics

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

    Module Documentation

    """
    Pattern 080: Asynchronous Agent
    
    Description:
        An Asynchronous Agent uses async/await patterns to handle concurrent operations
        without blocking. This pattern is essential for building high-performance,
        scalable AI applications that can handle multiple requests simultaneously,
        make concurrent API calls, and provide responsive user experiences.
    
        The agent leverages Python's asyncio for non-blocking I/O operations, enabling
        efficient resource utilization and improved throughput. It supports concurrent
        LLM calls, parallel tool execution, and async streaming for maximum performance.
    
    Components:
        1. Async Executor: Manages async task execution
        2. Concurrent Runner: Runs multiple tasks concurrently
        3. Async LLM Client: Non-blocking LLM interactions
        4. Task Scheduler: Schedules and coordinates async tasks
        5. Result Collector: Aggregates concurrent results
        6. Error Handler: Handles async exceptions
        7. Timeout Manager: Enforces timeouts on async operations
        8. Resource Pool: Manages async resources and connections
    
    Key Features:
        - Non-blocking async/await operations
        - Concurrent LLM calls
        - Parallel tool execution
        - Async streaming
        - Task cancellation
        - Timeout handling
        - Connection pooling
        - Backpressure management
        - Error propagation
        - Graceful shutdown
    
    Use Cases:
        - High-throughput API servers
        - Real-time chat applications
        - Concurrent data processing
        - Parallel research queries
        - Multi-source information gathering
        - Async web scraping
        - Real-time analytics
        - Concurrent translation services
        - Parallel validation
        - Multi-agent coordination
    
    LangChain Implementation:
        Uses ChatOpenAI with async methods (ainvoke, astream, abatch),
        asyncio for concurrency, and async LCEL chains.
    """
    

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


    Import Statements

    Standard Library Imports

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

    Purpose: Core Python functionality: – asyncio: Asynchronous I/O operations – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – enum.Enum: Enumeration support – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – langchain_core.runnables.RunnablePassthrough: 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.Coroutine: 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: TaskStatus

    Lines 70-77

    Inherits from: Enum

    Purpose:

    Status of async task
    

    Class: AsyncTask

    Lines 81-91

    Purpose:

    An asynchronous task
    

    Class: AsyncResult

    Lines 95-102

    Purpose:

    Result of async operation
    

    Class: ConcurrentResult

    Lines 106-114

    Purpose:

    Result of concurrent operations
    

    Class: AsyncAgent

    Lines 117-444

    Purpose:

    Agent that uses async/await for non-blocking operations.
    
    This agent enables high-performance concurrent processing with
    async LLM calls, parallel tool execution, and efficient resource usage.
    

    Methods:

    __init__(self, max_concurrent, timeout)

    Lines 125-144

    Purpose:

    Initialize the async agent.
    
    Args:
        max_concurrent: Maximum concurrent tasks
        timeout: Default timeout for operations
    

    Parameters: – max_concurrent: Max concurrent – timeout: Timeout


    Functions

    demonstrate_async_agent_sync()

    Lines 675-677

    Purpose:

    Synchronous wrapper for async demonstration
    

    Key Concepts Summary

    Pattern Overview

    The Async Agent 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 079
    • Pattern 081

    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 Async Agent 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: 080_async_agent.py
    Total Lines: 682
    Pattern Category: AI Agent Design Pattern #080


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Batch Processing

    
        """
    Pattern 079: Batch Processing Agent
    
    Description:
        A Batch Processing Agent handles multiple tasks or inputs concurrently in batches,
        optimizing throughput and resource utilization. This pattern is essential for
        processing large volumes of data efficiently, enabling parallel execution,
        job queuing, progress monitoring, and result aggregation.
        
        The agent manages batches of work items, distributes them across available resources,
        monitors progress, handles failures, and aggregates results. It supports various
        batching strategies including size-based, time-based, and priority-based batching.
    
    Components:
        1. Batch Scheduler: Schedules and organizes batch jobs
        2. Job Queue: Manages pending work items
        3. Parallel Executor: Executes tasks concurrently
        4. Progress Monitor: Tracks batch processing progress
        5. Result Aggregator: Combines individual results
        6. Error Handler: Handles failures and retries
        7. Resource Manager: Manages compute resources
        8. Throughput Optimizer: Optimizes batch sizes
    
    Key Features:
        - Parallel batch processing
        - Dynamic batch sizing
        - Job queue management
        - Progress tracking
        - Error handling and retries
        - Result aggregation
        - Resource optimization
        - Priority scheduling
        - Cancellation support
        - Checkpointing for resumption
    
    Use Cases:
        - Bulk document processing
        - Large-scale data analysis
        - Batch translation services
        - Bulk content generation
        - Mass email personalization
        - Dataset annotation
        - Batch image processing
        - ETL pipelines
        - Bulk API operations
        - Large-scale testing
    
    LangChain Implementation:
        Uses concurrent.futures for parallel execution, batch processing utilities,
        and structured output handling for aggregation.
    """
    
    import os
    import time
    import uuid
    from typing import List, Dict, Any, Optional, Callable, Tuple
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from concurrent.futures import ThreadPoolExecutor, as_completed
    from collections import defaultdict
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class JobStatus(Enum):
        """Status of a batch job"""
        PENDING = "pending"
        QUEUED = "queued"
        PROCESSING = "processing"
        COMPLETED = "completed"
        FAILED = "failed"
        CANCELLED = "cancelled"
        RETRYING = "retrying"
    
    
    class BatchStrategy(Enum):
        """Batching strategy"""
        SIZE_BASED = "size_based"  # Fixed batch size
        TIME_BASED = "time_based"  # Time window
        ADAPTIVE = "adaptive"  # Dynamic sizing
        PRIORITY = "priority"  # Priority-based
    
    
    @dataclass
    class BatchJob:
        """A single job in a batch"""
        job_id: str
        input_data: Any
        priority: int = 0  # Higher = more important
        status: JobStatus = JobStatus.PENDING
        result: Optional[Any] = None
        error: Optional[str] = None
        attempts: int = 0
        created_at: datetime = field(default_factory=datetime.now)
        started_at: Optional[datetime] = None
        completed_at: Optional[datetime] = None
    
    
    @dataclass
    class BatchConfig:
        """Configuration for batch processing"""
        batch_size: int = 10
        max_workers: int = 4
        max_retries: int = 3
        timeout: float = 60.0  # seconds per job
        strategy: BatchStrategy = BatchStrategy.SIZE_BASED
        aggregate_results: bool = True
        checkpoint_interval: int = 10  # Jobs between checkpoints
    
    
    @dataclass
    class BatchResult:
        """Result of batch processing"""
        batch_id: str
        total_jobs: int
        completed: int
        failed: int
        cancelled: int
        results: List[Any]
        errors: List[Tuple[str, str]]  # (job_id, error)
        duration: float
        throughput: float  # jobs/second
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class BatchProgress:
        """Progress of batch processing"""
        batch_id: str
        total_jobs: int
        completed: int
        processing: int
        pending: int
        failed: int
        progress_pct: float
        estimated_remaining: float  # seconds
        current_throughput: float
    
    
    class BatchProcessingAgent:
        """
        Agent for efficient batch processing of multiple tasks.
        
        This agent handles large volumes of work by processing items in batches,
        with parallel execution, error handling, and progress tracking.
        """
        
        def __init__(self, config: BatchConfig = BatchConfig()):
            """Initialize the batch processing agent"""
            self.config = config
            
            # LLM for processing
            self.llm = ChatOpenAI(
                model="gpt-3.5-turbo",
                temperature=0.3
            )
            
            # Job management
            self.jobs: Dict[str, BatchJob] = {}
            self.batches: Dict[str, List[str]] = {}  # batch_id -> job_ids
            self.active_batches: List[str] = []
        
        def create_batch(
            self,
            inputs: List[Any],
            priorities: Optional[List[int]] = None
        ) -> str:
            """
            Create a new batch of jobs.
            
            Args:
                inputs: List of input data for jobs
                priorities: Optional priority for each job
                
            Returns:
                Batch ID
            """
            batch_id = f"batch_{uuid.uuid4().hex[:8]}"
            job_ids = []
            
            for i, input_data in enumerate(inputs):
                job_id = f"job_{uuid.uuid4().hex[:8]}"
                priority = priorities[i] if priorities else 0
                
                job = BatchJob(
                    job_id=job_id,
                    input_data=input_data,
                    priority=priority,
                    status=JobStatus.QUEUED
                )
                
                self.jobs[job_id] = job
                job_ids.append(job_id)
            
            self.batches[batch_id] = job_ids
            self.active_batches.append(batch_id)
            
            return batch_id
        
        def process_batch(
            self,
            batch_id: str,
            task_fn: Callable[[Any], Any],
            on_progress: Optional[Callable[[BatchProgress], None]] = None
        ) -> BatchResult:
            """
            Process a batch of jobs in parallel.
            
            Args:
                batch_id: ID of batch to process
                task_fn: Function to process each job
                on_progress: Optional progress callback
                
            Returns:
                BatchResult with aggregated results
            """
            if batch_id not in self.batches:
                raise ValueError(f"Batch {batch_id} not found")
            
            job_ids = self.batches[batch_id]
            start_time = time.time()
            
            results = []
            errors = []
            completed_count = 0
            failed_count = 0
            cancelled_count = 0
            
            # Process jobs in parallel
            with ThreadPoolExecutor(max_workers=self.config.max_workers) as executor:
                # Submit jobs
                future_to_job = {}
                for job_id in job_ids:
                    job = self.jobs[job_id]
                    job.status = JobStatus.PROCESSING
                    job.started_at = datetime.now()
                    
                    future = executor.submit(
                        self._process_single_job,
                        job_id,
                        task_fn
                    )
                    future_to_job[future] = job_id
                
                # Collect results as they complete
                for future in as_completed(future_to_job):
                    job_id = future_to_job[future]
                    job = self.jobs[job_id]
                    
                    try:
                        result = future.result(timeout=self.config.timeout)
                        job.result = result
                        job.status = JobStatus.COMPLETED
                        job.completed_at = datetime.now()
                        results.append(result)
                        completed_count += 1
                        
                    except Exception as e:
                        job.error = str(e)
                        job.status = JobStatus.FAILED
                        job.completed_at = datetime.now()
                        errors.append((job_id, str(e)))
                        failed_count += 1
                    
                    # Progress callback
                    if on_progress:
                        progress = self._calculate_progress(
                            batch_id,
                            start_time,
                            completed_count + failed_count
                        )
                        on_progress(progress)
            
            duration = time.time() - start_time
            total_jobs = len(job_ids)
            
            return BatchResult(
                batch_id=batch_id,
                total_jobs=total_jobs,
                completed=completed_count,
                failed=failed_count,
                cancelled=cancelled_count,
                results=results,
                errors=errors,
                duration=duration,
                throughput=total_jobs / duration if duration > 0 else 0
            )
        
        def _process_single_job(
            self,
            job_id: str,
            task_fn: Callable[[Any], Any]
        ) -> Any:
            """
            Process a single job with retries.
            
            Args:
                job_id: Job ID
                task_fn: Task function
                
            Returns:
                Job result
            """
            job = self.jobs[job_id]
            
            for attempt in range(self.config.max_retries):
                try:
                    job.attempts = attempt + 1
                    if attempt > 0:
                        job.status = JobStatus.RETRYING
                    
                    result = task_fn(job.input_data)
                    return result
                    
                except Exception as e:
                    if attempt == self.config.max_retries - 1:
                        raise
                    time.sleep(2 ** attempt)  # Exponential backoff
            
            raise RuntimeError(f"Job {job_id} failed after {self.config.max_retries} attempts")
        
        def _calculate_progress(
            self,
            batch_id: str,
            start_time: float,
            completed: int
        ) -> BatchProgress:
            """Calculate batch progress"""
            job_ids = self.batches[batch_id]
            total = len(job_ids)
            
            processing = sum(
                1 for jid in job_ids
                if self.jobs[jid].status == JobStatus.PROCESSING
            )
            
            pending = sum(
                1 for jid in job_ids
                if self.jobs[jid].status in [JobStatus.PENDING, JobStatus.QUEUED]
            )
            
            failed = sum(
                1 for jid in job_ids
                if self.jobs[jid].status == JobStatus.FAILED
            )
            
            progress_pct = (completed / total) * 100 if total > 0 else 0
            
            elapsed = time.time() - start_time
            throughput = completed / elapsed if elapsed > 0 else 0
            remaining = total - completed
            estimated_remaining = remaining / throughput if throughput > 0 else 0
            
            return BatchProgress(
                batch_id=batch_id,
                total_jobs=total,
                completed=completed,
                processing=processing,
                pending=pending,
                failed=failed,
                progress_pct=progress_pct,
                estimated_remaining=estimated_remaining,
                current_throughput=throughput
            )
        
        def process_text_batch(
            self,
            texts: List[str],
            operation: str,
            on_progress: Optional[Callable[[BatchProgress], None]] = None
        ) -> BatchResult:
            """
            Process a batch of text operations.
            
            Args:
                texts: List of text inputs
                operation: Operation to perform (summarize, translate, etc.)
                on_progress: Optional progress callback
                
            Returns:
                BatchResult with processed texts
            """
            # Create batch
            batch_id = self.create_batch(texts)
            
            # Define task function
            def process_text(text: str) -> str:
                prompt = ChatPromptTemplate.from_template(
                    "{operation} the following text:\n\n{text}"
                )
                
                chain = prompt | self.llm | StrOutputParser()
                
                result = chain.invoke({
                    "operation": operation,
                    "text": text[:500]  # Limit input length
                })
                
                return result
            
            # Process batch
            return self.process_batch(batch_id, process_text, on_progress)
        
        def process_classification_batch(
            self,
            items: List[str],
            categories: List[str],
            on_progress: Optional[Callable[[BatchProgress], None]] = None
        ) -> BatchResult:
            """
            Batch classify items into categories.
            
            Args:
                items: Items to classify
                categories: Possible categories
                on_progress: Optional progress callback
                
            Returns:
                BatchResult with classifications
            """
            batch_id = self.create_batch(items)
            
            categories_str = ", ".join(categories)
            
            def classify_item(item: str) -> str:
                prompt = ChatPromptTemplate.from_template(
                    """Classify the following item into one of these categories: {categories}
    
    Item: {item}
    
    Category:"""
                )
                
                chain = prompt | self.llm | StrOutputParser()
                
                result = chain.invoke({
                    "categories": categories_str,
                    "item": item[:200]
                })
                
                return result.strip()
            
            return self.process_batch(batch_id, classify_item, on_progress)
        
        def process_qa_batch(
            self,
            questions: List[str],
            context: str,
            on_progress: Optional[Callable[[BatchProgress], None]] = None
        ) -> BatchResult:
            """
            Answer a batch of questions.
            
            Args:
                questions: List of questions
                context: Shared context for all questions
                on_progress: Optional progress callback
                
            Returns:
                BatchResult with answers
            """
            batch_id = self.create_batch(questions)
            
            def answer_question(question: str) -> str:
                prompt = ChatPromptTemplate.from_template(
                    """Context: {context}
    
    Question: {question}
    
    Answer:"""
                )
                
                chain = prompt | self.llm | StrOutputParser()
                
                result = chain.invoke({
                    "context": context[:1000],
                    "question": question
                })
                
                return result
            
            return self.process_batch(batch_id, answer_question, on_progress)
        
        def get_batch_status(self, batch_id: str) -> Dict[str, Any]:
            """
            Get status of a batch.
            
            Args:
                batch_id: Batch ID
                
            Returns:
                Status dictionary
            """
            if batch_id not in self.batches:
                raise ValueError(f"Batch {batch_id} not found")
            
            job_ids = self.batches[batch_id]
            
            status_counts = defaultdict(int)
            for job_id in job_ids:
                status = self.jobs[job_id].status
                status_counts[status.value] += 1
            
            return {
                "batch_id": batch_id,
                "total_jobs": len(job_ids),
                "status_breakdown": dict(status_counts),
                "jobs": [
                    {
                        "job_id": jid,
                        "status": self.jobs[jid].status.value,
                        "attempts": self.jobs[jid].attempts,
                        "error": self.jobs[jid].error
                    }
                    for jid in job_ids[:10]  # First 10 jobs
                ]
            }
    
    
    def demonstrate_batch_processing_agent():
        """Demonstrate the batch processing agent capabilities"""
        print("=" * 80)
        print("BATCH PROCESSING AGENT DEMONSTRATION")
        print("=" * 80)
        
        # Custom config for faster demos
        config = BatchConfig(
            batch_size=5,
            max_workers=3,
            max_retries=2
        )
        
        agent = BatchProcessingAgent(config)
        
        # Demo 1: Text Summarization Batch
        print("\n" + "=" * 80)
        print("DEMO 1: Batch Text Summarization")
        print("=" * 80)
        
        texts = [
            "Artificial intelligence is transforming healthcare through advanced diagnostic tools, personalized treatment plans, and drug discovery acceleration.",
            "Machine learning models require large amounts of training data and computational resources to achieve high performance on complex tasks.",
            "Natural language processing enables computers to understand, interpret, and generate human language in meaningful ways.",
            "Computer vision systems can now recognize objects, faces, and scenes with accuracy rivaling human perception.",
            "Robotics combined with AI is enabling autonomous vehicles, warehouse automation, and advanced manufacturing processes."
        ]
        
        print(f"\nProcessing {len(texts)} texts for summarization...")
        print("-" * 80)
        
        progress_updates = []
        
        def on_progress(progress: BatchProgress):
            progress_updates.append(progress)
            bar_length = 40
            filled = int(bar_length * progress.progress_pct / 100)
            bar = "█" * filled + "░" * (bar_length - filled)
            print(f"\r[{bar}] {progress.progress_pct:.1f}% ({progress.completed}/{progress.total_jobs})", end="", flush=True)
        
        result = agent.process_text_batch(texts, "Summarize in one sentence", on_progress)
        
        print("\n" + "-" * 80)
        print(f"\n✓ Batch complete!")
        print(f"Total Jobs: {result.total_jobs}")
        print(f"Completed: {result.completed}")
        print(f"Failed: {result.failed}")
        print(f"Duration: {result.duration:.2f}s")
        print(f"Throughput: {result.throughput:.2f} jobs/sec")
        
        print(f"\nSample Results:")
        for i, summary in enumerate(result.results[:3], 1):
            print(f"\n{i}. {summary[:100]}...")
        
        # Demo 2: Classification Batch
        print("\n" + "=" * 80)
        print("DEMO 2: Batch Classification")
        print("=" * 80)
        
        items = [
            "Python programming tutorial",
            "Chocolate chip cookie recipe",
            "2024 Olympic games highlights",
            "Climate change research paper",
            "Stock market analysis",
            "Travel guide to Japan",
            "Guitar lesson for beginners"
        ]
        
        categories = ["Technology", "Food", "Sports", "Science", "Finance", "Travel", "Music"]
        
        print(f"\nClassifying {len(items)} items into categories:")
        print(f"Categories: {', '.join(categories)}")
        print("-" * 80)
        
        result2 = agent.process_classification_batch(items, categories, on_progress=on_progress)
        
        print("\n" + "-" * 80)
        print(f"\n✓ Classification complete!")
        print(f"Duration: {result2.duration:.2f}s")
        print(f"Throughput: {result2.throughput:.2f} items/sec")
        
        print(f"\nClassification Results:")
        for i, (item, category) in enumerate(zip(items, result2.results), 1):
            print(f"{i}. '{item[:40]}...' → {category}")
        
        # Demo 3: Question Answering Batch
        print("\n" + "=" * 80)
        print("DEMO 3: Batch Question Answering")
        print("=" * 80)
        
        context = """
        LangChain is a framework for developing applications powered by language models.
        It provides tools for prompt management, chains, agents, memory, and more.
        LangChain supports multiple LLM providers including OpenAI, Anthropic, and Google.
        The framework emphasizes composability and modularity for building complex applications.
        """
        
        questions = [
            "What is LangChain?",
            "What providers does LangChain support?",
            "What are the main features of LangChain?",
            "What is the design philosophy of LangChain?"
        ]
        
        print(f"\nContext: {context.strip()[:100]}...")
        print(f"\nAnswering {len(questions)} questions...")
        print("-" * 80)
        
        result3 = agent.process_qa_batch(questions, context, on_progress=on_progress)
        
        print("\n" + "-" * 80)
        print(f"\n✓ Q&A batch complete!")
        print(f"Duration: {result3.duration:.2f}s")
        
        print(f"\nQuestions and Answers:")
        for i, (question, answer) in enumerate(zip(questions, result3.results), 1):
            print(f"\nQ{i}: {question}")
            print(f"A{i}: {answer[:150]}...")
        
        # Demo 4: Batch Status Monitoring
        print("\n" + "=" * 80)
        print("DEMO 4: Batch Status Monitoring")
        print("=" * 80)
        
        # Create a new batch
        test_items = ["Item " + str(i) for i in range(8)]
        batch_id = agent.create_batch(test_items)
        
        print(f"\nCreated batch: {batch_id}")
        print(f"Total items: {len(test_items)}")
        
        # Get status before processing
        status = agent.get_batch_status(batch_id)
        
        print(f"\nBatch Status:")
        print(f"  Total Jobs: {status['total_jobs']}")
        print(f"  Status Breakdown:")
        for status_type, count in status['status_breakdown'].items():
            print(f"    {status_type}: {count}")
        
        # Demo 5: Performance Comparison
        print("\n" + "=" * 80)
        print("DEMO 5: Parallel vs Sequential Performance")
        print("=" * 80)
        
        test_texts = [f"Sample text {i} for processing" for i in range(10)]
        
        print(f"\nProcessing {len(test_texts)} items...")
        
        # Parallel processing
        print("\nParallel Processing (4 workers):")
        parallel_start = time.time()
        parallel_result = agent.process_text_batch(test_texts, "Count words in")
        parallel_duration = time.time() - parallel_start
        
        print(f"  Duration: {parallel_duration:.2f}s")
        print(f"  Throughput: {parallel_result.throughput:.2f} jobs/sec")
        
        # Sequential processing (1 worker)
        print("\nSequential Processing (1 worker):")
        sequential_config = BatchConfig(max_workers=1)
        sequential_agent = BatchProcessingAgent(sequential_config)
        sequential_start = time.time()
        sequential_result = sequential_agent.process_text_batch(test_texts, "Count words in")
        sequential_duration = time.time() - sequential_start
        
        print(f"  Duration: {sequential_duration:.2f}s")
        print(f"  Throughput: {sequential_result.throughput:.2f} jobs/sec")
        
        speedup = sequential_duration / parallel_duration if parallel_duration > 0 else 0
        print(f"\nSpeedup: {speedup:.2f}x")
        print(f"Efficiency: {(speedup / config.max_workers) * 100:.1f}%")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        
        summary = """
    The Batch Processing Agent demonstrates efficient parallel processing:
    
    KEY CAPABILITIES:
    1. Parallel Execution: Process multiple jobs concurrently
    2. Job Queuing: Manage large volumes of pending work
    3. Progress Tracking: Real-time monitoring with callbacks
    4. Error Handling: Automatic retries with exponential backoff
    5. Result Aggregation: Collect and organize batch results
    6. Resource Management: Control worker threads and resources
    7. Flexible Batching: Support various batching strategies
    8. Status Monitoring: Track job and batch status
    
    BENEFITS:
    - Dramatically improved throughput
    - Efficient resource utilization
    - Scalable processing of large datasets
    - Automatic error recovery
    - Progress visibility
    - Cost optimization through parallelization
    - Reduced total processing time
    - Better user experience for bulk operations
    
    USE CASES:
    - Bulk document summarization or translation
    - Large-scale data classification
    - Mass content generation
    - Batch question answering
    - Dataset annotation and labeling
    - Bulk email personalization
    - ETL pipeline processing
    - Large-scale testing
    - Batch API operations
    - Document ingestion for RAG systems
    
    PRODUCTION CONSIDERATIONS:
    1. Worker Scaling: Adjust workers based on load and resources
    2. Rate Limiting: Respect API rate limits across workers
    3. Cost Management: Monitor token usage across batches
    4. Error Recovery: Implement robust retry strategies
    5. Checkpointing: Save progress for resumption
    6. Priority Queues: Process high-priority jobs first
    7. Resource Limits: Prevent resource exhaustion
    8. Monitoring: Track batch performance metrics
    9. Load Balancing: Distribute work evenly
    10. Graceful Shutdown: Handle interruptions cleanly
    
    ADVANCED EXTENSIONS:
    - Dynamic worker scaling based on queue depth
    - Intelligent batch sizing with machine learning
    - Multi-stage batch pipelines
    - Distributed processing across machines
    - Streaming batch results
    - Partial result delivery
    - Batch deduplication
    - Priority-based scheduling
    - Resource-aware batching
    - Automatic retry backoff tuning
    
    Batch processing is essential for production AI systems handling
    large volumes of data efficiently and cost-effectively.
    """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_batch_processing_agent()
    
        

    Agentic Design Pattern: Batch Processing

    Pattern 079: Batch Processing – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 079: Batch Processing Agent

    Description: A Batch Processing Agent handles multiple tasks or inputs concurrently in batches, optimizing throughput and resource utilization. This pattern is essential for processing large volumes of data efficiently, enabling parallel execution, job queuing, progress monitoring, and result aggregation.

    The agent manages batches of work items, distributes them across available resources,
    monitors progress, handles failures, and aggregates results. It supports various
    batching strategies including size-based, time-based, and priority-based batching.
    

    Components: 1. Batch Scheduler: Schedules and organizes batch jobs 2. Job Queue: Manages pending work items 3. Parallel Executor: Executes tasks concurrently 4. Progress Monitor: Tracks batch processing progress 5. Result Aggregator: Combines individual results 6. Error Handler: Handles failures and retries 7. Resource Manager: Manages compute resources 8. Throughput Optimizer: Optimizes batch sizes

    Key Features: – Parallel batch processing – Dynamic batch sizing – Job queue management – Progress tracking – Error handling and retries – Result aggregation – Resource optimization – Priority scheduling – Cancellation support – Checkpointing for resumption

    Use Cases: – Bulk document processing – Large-scale data analysis – Batch translation services – Bulk content generation – Mass email personalization – Dataset annotation – Batch image processing – ETL pipelines – Bulk API operations – Large-scale testing

    LangChain Implementation: Uses concurrent.futures for parallel execution, batch processing utilities, and structured output handling for aggregation.


    File Statistics

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

    Module Documentation

    """
    Pattern 079: Batch Processing Agent
    
    Description:
        A Batch Processing Agent handles multiple tasks or inputs concurrently in batches,
        optimizing throughput and resource utilization. This pattern is essential for
        processing large volumes of data efficiently, enabling parallel execution,
        job queuing, progress monitoring, and result aggregation.
    
        The agent manages batches of work items, distributes them across available resources,
        monitors progress, handles failures, and aggregates results. It supports various
        batching strategies including size-based, time-based, and priority-based batching.
    
    Components:
        1. Batch Scheduler: Schedules and organizes batch jobs
        2. Job Queue: Manages pending work items
        3. Parallel Executor: Executes tasks concurrently
        4. Progress Monitor: Tracks batch processing progress
        5. Result Aggregator: Combines individual results
        6. Error Handler: Handles failures and retries
        7. Resource Manager: Manages compute resources
        8. Throughput Optimizer: Optimizes batch sizes
    
    Key Features:
        - Parallel batch processing
        - Dynamic batch sizing
        - Job queue management
        - Progress tracking
        - Error handling and retries
        - Result aggregation
        - Resource optimization
        - Priority scheduling
        - Cancellation support
        - Checkpointing for resumption
    
    Use Cases:
        - Bulk document processing
        - Large-scale data analysis
        - Batch translation services
        - Bulk content generation
        - Mass email personalization
        - Dataset annotation
        - Batch image processing
        - ETL pipelines
        - Bulk API operations
        - Large-scale testing
    
    LangChain Implementation:
        Uses concurrent.futures for parallel execution, batch processing utilities,
        and structured output handling for aggregation.
    """
    

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


    Import Statements

    Standard Library Imports

    import collections.defaultdict
    import concurrent.futures.ThreadPoolExecutor
    import concurrent.futures.as_completed
    import dataclasses.dataclass
    import dataclasses.field
    import datetime.datetime
    import enum.Enum
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import time
    import typing.Any
    import typing.Callable
    import typing.Dict
    import typing.List
    import typing.Optional
    import typing.Tuple
    

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

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from dotenv.load_dotenv
    from uuid
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: JobStatus

    Lines 71-79

    Inherits from: Enum

    Purpose:

    Status of a batch job
    

    Class: BatchStrategy

    Lines 82-87

    Inherits from: Enum

    Purpose:

    Batching strategy
    

    Class: BatchJob

    Lines 91-102

    Purpose:

    A single job in a batch
    

    Class: BatchConfig

    Lines 106-114

    Purpose:

    Configuration for batch processing
    

    Class: BatchResult

    Lines 118-129

    Purpose:

    Result of batch processing
    

    Class: BatchProgress

    Lines 133-143

    Purpose:

    Progress of batch processing
    

    Class: BatchProcessingAgent

    Lines 146-523

    Purpose:

    Agent for efficient batch processing of multiple tasks.
    
    This agent handles large volumes of work by processing items in batches,
    with parallel execution, error handling, and progress tracking.
    

    Methods:

    __init__(self, config)

    Lines 154-167

    Purpose:

    Initialize the batch processing agent
    

    Parameters: – config: Config

    create_batch(self, inputs, priorities)

    Lines 169-204

    Purpose:

    Create a new batch of jobs.
    
    Args:
        inputs: List of input data for jobs
        priorities: Optional priority for each job
    
    Returns:
        Batch ID
    

    Parameters: – inputs: Input data – priorities: Priorities

    Returns: str

    process_batch(self, batch_id, task_fn, on_progress)

    Lines 206-293

    Purpose:

    Process a batch of jobs in parallel.
    
    Args:
        batch_id: ID of batch to process
        task_fn: Function to process each job
        on_progress: Optional progress callback
    
    Returns:
        BatchResult with aggregated results
    

    Parameters: – batch_id: Batch id – task_fn: Task fn – on_progress: On progress

    Returns: BatchResult

    _process_single_job(self, job_id, task_fn)

    Lines 295-326

    Purpose:

    Process a single job with retries.
    
    Args:
        job_id: Job ID
        task_fn: Task function
    
    Returns:
        Job result
    

    Parameters: – job_id: Job id – task_fn: Task fn

    Returns: Any

    _calculate_progress(self, batch_id, start_time, completed)

    Lines 328-370

    Purpose:

    Calculate batch progress
    

    Parameters: – batch_id: Batch id – start_time: Start time – completed: Completed

    Returns: BatchProgress

    process_text_batch(self, texts, operation, on_progress)

    Lines 372-408

    Purpose:

    Process a batch of text operations.
    
    Args:
        texts: List of text inputs
        operation: Operation to perform (summarize, translate, etc.)
        on_progress: Optional progress callback
    
    Returns:
        BatchResult with processed texts
    

    Parameters: – texts: Texts – operation: Operation – on_progress: On progress

    Returns: BatchResult

    process_classification_batch(self, items, categories, on_progress)

    Lines 410-449

    Purpose:

    Batch classify items into categories.
    
    Args:
        items: Items to classify
        categories: Possible categories
        on_progress: Optional progress callback
    
    Returns:
        BatchResult with classifications
    

    Parameters: – items: Items – categories: Categories – on_progress: On progress

    Returns: BatchResult

    process_qa_batch(self, questions, context, on_progress)

    Lines 451-488

    Purpose:

    Answer a batch of questions.
    
    Args:
        questions: List of questions
        context: Shared context for all questions
        on_progress: Optional progress callback
    
    Returns:
        BatchResult with answers
    

    Parameters: – questions: Question to answer – context: Context – on_progress: On progress

    Returns: BatchResult

    get_batch_status(self, batch_id)

    Lines 490-523

    Purpose:

    Get status of a batch.
    
    Args:
        batch_id: Batch ID
    
    Returns:
        Status dictionary
    

    Parameters: – batch_id: Batch id

    Returns: <ast.Subscript object at 0x000001A06D62A990>


    Functions

    demonstrate_batch_processing_agent()

    Lines 526-768

    Purpose:

    Demonstrate the batch processing agent capabilities
    

    Key Concepts Summary

    Pattern Overview

    The Batch Processing 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 078
    • Pattern 080

    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 Batch Processing 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: 079_batch_processing.py
    Total Lines: 773
    Pattern Category: AI Agent Design Pattern #079


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Streaming Agent

    
        """
    Pattern 078: Streaming Agent
    
    Description:
        A Streaming Agent delivers responses in real-time as they are generated, rather
        than waiting for complete output. This pattern is essential for building responsive
        user interfaces and handling long-running generation tasks. It provides immediate
        feedback to users and enables progressive rendering of results.
        
        The agent streams tokens as they are generated by the LLM, allowing for better
        user experience in conversational interfaces, content generation, and real-time
        data processing. It supports multiple streaming modes including token-by-token,
        chunk-by-chunk, and structured streaming with metadata.
    
    Components:
        1. Token Streamer: Streams individual tokens as generated
        2. Chunk Streamer: Streams semantic chunks (sentences, paragraphs)
        3. Event Streamer: Streams events with metadata and progress
        4. Progress Tracker: Monitors generation progress
        5. Buffer Manager: Manages streaming buffers and backpressure
        6. Error Handler: Handles streaming errors gracefully
        7. Aggregator: Collects streamed content for final result
        8. Rate Controller: Controls streaming rate
    
    Key Features:
        - Real-time token streaming
        - Chunk-based streaming
        - Progress callbacks
        - Cancellation support
        - Error recovery during streaming
        - Multiple output formats
        - Backpressure handling
        - Stream aggregation
        - Event-driven architecture
        - WebSocket support
    
    Use Cases:
        - Conversational chatbots with typing indicators
        - Long-form content generation
        - Real-time translation services
        - Live code generation and explanation
        - Progressive document analysis
        - Interactive storytelling
        - Streaming search results
        - Real-time data processing
        - Live transcription and summarization
        - Collaborative writing tools
    
    LangChain Implementation:
        Uses ChatOpenAI with streaming=True, callbacks for progress tracking,
        and LCEL chains with streaming support.
    """
    
    import os
    import time
    import asyncio
    from typing import List, Dict, Any, Optional, Iterator, AsyncIterator, Callable
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from collections import deque
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    from langchain_core.callbacks import BaseCallbackHandler
    
    load_dotenv()
    
    
    class StreamMode(Enum):
        """Streaming modes"""
        TOKEN = "token"  # Stream individual tokens
        CHUNK = "chunk"  # Stream semantic chunks
        SENTENCE = "sentence"  # Stream complete sentences
        PARAGRAPH = "paragraph"  # Stream complete paragraphs
        EVENT = "event"  # Stream with events and metadata
    
    
    class StreamStatus(Enum):
        """Stream status"""
        STARTING = "starting"
        STREAMING = "streaming"
        PAUSED = "paused"
        COMPLETED = "completed"
        ERROR = "error"
        CANCELLED = "cancelled"
    
    
    @dataclass
    class StreamEvent:
        """Event in the stream"""
        event_type: str  # token, chunk, progress, complete, error
        content: str
        timestamp: datetime
        metadata: Dict[str, Any] = field(default_factory=dict)
        token_count: int = 0
        progress: float = 0.0  # 0.0 to 1.0
    
    
    @dataclass
    class StreamConfig:
        """Configuration for streaming"""
        mode: StreamMode = StreamMode.TOKEN
        buffer_size: int = 100
        flush_interval: float = 0.1  # seconds
        max_tokens: Optional[int] = None
        show_progress: bool = True
        aggregate_result: bool = True
        error_recovery: bool = True
    
    
    @dataclass
    class StreamResult:
        """Result of streaming operation"""
        full_content: str
        events: List[StreamEvent]
        status: StreamStatus
        total_tokens: int
        duration: float
        error: Optional[str] = None
    
    
    class StreamingCallbackHandler(BaseCallbackHandler):
        """Callback handler for streaming events"""
        
        def __init__(
            self,
            on_token: Optional[Callable[[str], None]] = None,
            on_chunk: Optional[Callable[[str], None]] = None
        ):
            """Initialize callback handler"""
            self.on_token = on_token
            self.on_chunk = on_chunk
            self.tokens: List[str] = []
        
        def on_llm_new_token(self, token: str, **kwargs) -> None:
            """Called when new token is generated"""
            self.tokens.append(token)
            if self.on_token:
                self.on_token(token)
        
        def on_llm_end(self, response, **kwargs) -> None:
            """Called when generation ends"""
            if self.on_chunk:
                full_text = "".join(self.tokens)
                self.on_chunk(full_text)
    
    
    class StreamingAgent:
        """
        Agent that streams responses in real-time.
        
        This agent provides various streaming capabilities including token streaming,
        chunk streaming, and event streaming with progress tracking.
        """
        
        def __init__(self, config: StreamConfig = StreamConfig()):
            """Initialize the streaming agent"""
            self.config = config
            
            # Streaming LLM
            self.llm = ChatOpenAI(
                model="gpt-3.5-turbo",
                temperature=0.7,
                streaming=True
            )
            
            # Non-streaming LLM for comparison
            self.sync_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
            
            # Stream state
            self.status = StreamStatus.STARTING
            self.buffer: deque = deque(maxlen=config.buffer_size)
            self.events: List[StreamEvent] = []
        
        def stream_tokens(
            self,
            prompt: str,
            on_token: Optional[Callable[[str], None]] = None
        ) -> StreamResult:
            """
            Stream tokens as they are generated.
            
            Args:
                prompt: Input prompt
                on_token: Callback for each token
                
            Returns:
                StreamResult with full content and events
            """
            start_time = time.time()
            full_content = ""
            token_count = 0
            
            self.status = StreamStatus.STREAMING
            
            # Create callback handler
            callback = StreamingCallbackHandler(on_token=on_token)
            
            try:
                # Stream tokens
                for chunk in self.llm.stream(prompt):
                    if hasattr(chunk, 'content'):
                        token = chunk.content
                        full_content += token
                        token_count += 1
                        
                        # Create event
                        event = StreamEvent(
                            event_type="token",
                            content=token,
                            timestamp=datetime.now(),
                            token_count=token_count,
                            progress=min(1.0, token_count / (self.config.max_tokens or 1000))
                        )
                        self.events.append(event)
                        
                        # Call custom callback
                        if on_token:
                            on_token(token)
                
                self.status = StreamStatus.COMPLETED
                
                return StreamResult(
                    full_content=full_content,
                    events=self.events.copy(),
                    status=self.status,
                    total_tokens=token_count,
                    duration=time.time() - start_time
                )
                
            except Exception as e:
                self.status = StreamStatus.ERROR
                return StreamResult(
                    full_content=full_content,
                    events=self.events.copy(),
                    status=self.status,
                    total_tokens=token_count,
                    duration=time.time() - start_time,
                    error=str(e)
                )
        
        def stream_chunks(
            self,
            prompt: str,
            chunk_size: int = 50,
            on_chunk: Optional[Callable[[str], None]] = None
        ) -> StreamResult:
            """
            Stream content in chunks.
            
            Args:
                prompt: Input prompt
                chunk_size: Size of chunks in characters
                on_chunk: Callback for each chunk
                
            Returns:
                StreamResult with full content and events
            """
            start_time = time.time()
            full_content = ""
            current_chunk = ""
            chunk_count = 0
            
            self.status = StreamStatus.STREAMING
            
            try:
                for chunk in self.llm.stream(prompt):
                    if hasattr(chunk, 'content'):
                        token = chunk.content
                        current_chunk += token
                        full_content += token
                        
                        # Emit chunk when size reached
                        if len(current_chunk) >= chunk_size:
                            chunk_count += 1
                            
                            event = StreamEvent(
                                event_type="chunk",
                                content=current_chunk,
                                timestamp=datetime.now(),
                                metadata={"chunk_number": chunk_count}
                            )
                            self.events.append(event)
                            
                            if on_chunk:
                                on_chunk(current_chunk)
                            
                            current_chunk = ""
                
                # Emit final chunk
                if current_chunk:
                    chunk_count += 1
                    event = StreamEvent(
                        event_type="chunk",
                        content=current_chunk,
                        timestamp=datetime.now(),
                        metadata={"chunk_number": chunk_count, "final": True}
                    )
                    self.events.append(event)
                    
                    if on_chunk:
                        on_chunk(current_chunk)
                
                self.status = StreamStatus.COMPLETED
                
                return StreamResult(
                    full_content=full_content,
                    events=self.events.copy(),
                    status=self.status,
                    total_tokens=len(full_content.split()),
                    duration=time.time() - start_time
                )
                
            except Exception as e:
                self.status = StreamStatus.ERROR
                return StreamResult(
                    full_content=full_content,
                    events=self.events.copy(),
                    status=self.status,
                    total_tokens=len(full_content.split()),
                    duration=time.time() - start_time,
                    error=str(e)
                )
        
        def stream_with_progress(
            self,
            prompt: str,
            estimated_tokens: int = 200,
            on_progress: Optional[Callable[[float, str], None]] = None
        ) -> StreamResult:
            """
            Stream with progress tracking.
            
            Args:
                prompt: Input prompt
                estimated_tokens: Estimated total tokens for progress
                on_progress: Callback with progress (0.0-1.0) and content
                
            Returns:
                StreamResult with full content and progress events
            """
            start_time = time.time()
            full_content = ""
            token_count = 0
            
            self.status = StreamStatus.STREAMING
            
            try:
                for chunk in self.llm.stream(prompt):
                    if hasattr(chunk, 'content'):
                        token = chunk.content
                        full_content += token
                        token_count += 1
                        
                        # Calculate progress
                        progress = min(1.0, token_count / estimated_tokens)
                        
                        # Create progress event
                        event = StreamEvent(
                            event_type="progress",
                            content=token,
                            timestamp=datetime.now(),
                            token_count=token_count,
                            progress=progress,
                            metadata={
                                "estimated_remaining": max(0, estimated_tokens - token_count)
                            }
                        )
                        self.events.append(event)
                        
                        # Call progress callback
                        if on_progress:
                            on_progress(progress, full_content)
                
                # Final progress event
                event = StreamEvent(
                    event_type="complete",
                    content="",
                    timestamp=datetime.now(),
                    token_count=token_count,
                    progress=1.0
                )
                self.events.append(event)
                
                if on_progress:
                    on_progress(1.0, full_content)
                
                self.status = StreamStatus.COMPLETED
                
                return StreamResult(
                    full_content=full_content,
                    events=self.events.copy(),
                    status=self.status,
                    total_tokens=token_count,
                    duration=time.time() - start_time
                )
                
            except Exception as e:
                self.status = StreamStatus.ERROR
                return StreamResult(
                    full_content=full_content,
                    events=self.events.copy(),
                    status=self.status,
                    total_tokens=token_count,
                    duration=time.time() - start_time,
                    error=str(e)
                )
        
        async def stream_async(
            self,
            prompt: str,
            on_token: Optional[Callable[[str], None]] = None
        ) -> StreamResult:
            """
            Stream asynchronously.
            
            Args:
                prompt: Input prompt
                on_token: Callback for each token
                
            Returns:
                StreamResult with full content and events
            """
            start_time = time.time()
            full_content = ""
            token_count = 0
            
            self.status = StreamStatus.STREAMING
            
            try:
                async for chunk in self.llm.astream(prompt):
                    if hasattr(chunk, 'content'):
                        token = chunk.content
                        full_content += token
                        token_count += 1
                        
                        event = StreamEvent(
                            event_type="token",
                            content=token,
                            timestamp=datetime.now(),
                            token_count=token_count
                        )
                        self.events.append(event)
                        
                        if on_token:
                            on_token(token)
                        
                        # Simulate async processing
                        await asyncio.sleep(0.001)
                
                self.status = StreamStatus.COMPLETED
                
                return StreamResult(
                    full_content=full_content,
                    events=self.events.copy(),
                    status=self.status,
                    total_tokens=token_count,
                    duration=time.time() - start_time
                )
                
            except Exception as e:
                self.status = StreamStatus.ERROR
                return StreamResult(
                    full_content=full_content,
                    events=self.events.copy(),
                    status=self.status,
                    total_tokens=token_count,
                    duration=time.time() - start_time,
                    error=str(e)
                )
        
        def stream_chain(
            self,
            inputs: Dict[str, str],
            on_chunk: Optional[Callable[[str], None]] = None
        ) -> StreamResult:
            """
            Stream output from a chain.
            
            Args:
                inputs: Chain inputs
                on_chunk: Callback for each chunk
                
            Returns:
                StreamResult with full content
            """
            start_time = time.time()
            full_content = ""
            
            # Create a streaming chain
            prompt = ChatPromptTemplate.from_template(
                "Write a {tone} {content_type} about {topic}."
            )
            
            chain = prompt | self.llm | StrOutputParser()
            
            self.status = StreamStatus.STREAMING
            
            try:
                for chunk in chain.stream(inputs):
                    full_content += chunk
                    
                    event = StreamEvent(
                        event_type="chunk",
                        content=chunk,
                        timestamp=datetime.now()
                    )
                    self.events.append(event)
                    
                    if on_chunk:
                        on_chunk(chunk)
                
                self.status = StreamStatus.COMPLETED
                
                return StreamResult(
                    full_content=full_content,
                    events=self.events.copy(),
                    status=self.status,
                    total_tokens=len(full_content.split()),
                    duration=time.time() - start_time
                )
                
            except Exception as e:
                self.status = StreamStatus.ERROR
                return StreamResult(
                    full_content=full_content,
                    events=self.events.copy(),
                    status=self.status,
                    total_tokens=len(full_content.split()),
                    duration=time.time() - start_time,
                    error=str(e)
                )
        
        def compare_streaming_vs_sync(
            self,
            prompt: str
        ) -> Dict[str, Any]:
            """
            Compare streaming vs synchronous generation.
            
            Args:
                prompt: Input prompt
                
            Returns:
                Comparison results
            """
            # Streaming generation
            stream_start = time.time()
            stream_result = self.stream_tokens(prompt)
            stream_duration = time.time() - stream_start
            
            # Clear events for sync test
            self.events = []
            
            # Synchronous generation
            sync_start = time.time()
            sync_content = self.sync_llm.invoke(prompt).content
            sync_duration = time.time() - sync_start
            
            return {
                "streaming": {
                    "duration": stream_duration,
                    "content_length": len(stream_result.full_content),
                    "tokens": stream_result.total_tokens,
                    "time_to_first_token": stream_result.events[0].timestamp.timestamp() - stream_start if stream_result.events else 0
                },
                "synchronous": {
                    "duration": sync_duration,
                    "content_length": len(sync_content),
                    "time_to_first_token": sync_duration  # Same as total for sync
                },
                "comparison": {
                    "streaming_faster": stream_duration < sync_duration,
                    "ttft_improvement": "Immediate (streaming)" if stream_result.events else "N/A"
                }
            }
    
    
    def demonstrate_streaming_agent():
        """Demonstrate the streaming agent capabilities"""
        print("=" * 80)
        print("STREAMING AGENT DEMONSTRATION")
        print("=" * 80)
        
        agent = StreamingAgent()
        
        # Demo 1: Token Streaming
        print("\n" + "=" * 80)
        print("DEMO 1: Token-by-Token Streaming")
        print("=" * 80)
        
        prompt = "Write a short poem about artificial intelligence."
        
        print(f"\nPrompt: {prompt}")
        print("\nStreaming response:")
        print("-" * 80)
        
        # Track tokens
        tokens_received = []
        
        def on_token(token: str):
            tokens_received.append(token)
            print(token, end="", flush=True)
        
        result = agent.stream_tokens(prompt, on_token=on_token)
        
        print("\n" + "-" * 80)
        print(f"\nStream Status: {result.status.value}")
        print(f"Total Tokens: {result.total_tokens}")
        print(f"Duration: {result.duration:.3f}s")
        print(f"Tokens/sec: {result.total_tokens / result.duration:.1f}")
        print(f"Events Captured: {len(result.events)}")
        
        # Demo 2: Chunk Streaming
        print("\n" + "=" * 80)
        print("DEMO 2: Chunk-Based Streaming")
        print("=" * 80)
        
        prompt2 = "Explain how streaming improves user experience in AI applications."
        
        print(f"\nPrompt: {prompt2}")
        print("\nStreaming in chunks (50 chars each):")
        print("-" * 80)
        
        chunks_received = []
        
        def on_chunk(chunk: str):
            chunks_received.append(chunk)
            print(f"\n[Chunk {len(chunks_received)}]: {chunk}", end="")
        
        agent.events = []  # Reset events
        result2 = agent.stream_chunks(prompt2, chunk_size=50, on_chunk=on_chunk)
        
        print("\n" + "-" * 80)
        print(f"\nTotal Chunks: {len(chunks_received)}")
        print(f"Full Content Length: {len(result2.full_content)} chars")
        print(f"Duration: {result2.duration:.3f}s")
        
        # Demo 3: Progress Tracking
        print("\n" + "=" * 80)
        print("DEMO 3: Streaming with Progress Tracking")
        print("=" * 80)
        
        prompt3 = "Write a brief history of machine learning in three paragraphs."
        
        print(f"\nPrompt: {prompt3}")
        print("\nStreaming with progress updates:")
        print("-" * 80)
        
        progress_updates = []
        
        def on_progress(progress: float, content: str):
            progress_updates.append(progress)
            # Show progress bar every 10%
            if len(progress_updates) % 20 == 0:
                bar_length = 40
                filled = int(bar_length * progress)
                bar = "█" * filled + "░" * (bar_length - filled)
                print(f"\r[{bar}] {progress*100:.0f}%", end="", flush=True)
        
        agent.events = []  # Reset events
        result3 = agent.stream_with_progress(prompt3, estimated_tokens=150, on_progress=on_progress)
        
        print("\n" + "-" * 80)
        print(f"\n✓ Streaming complete!")
        print(f"Total Tokens: {result3.total_tokens}")
        print(f"Duration: {result3.duration:.3f}s")
        print(f"Progress Updates: {len(progress_updates)}")
        
        # Show first 200 chars of content
        print(f"\nContent Preview:")
        print(result3.full_content[:200] + "...")
        
        # Demo 4: Chain Streaming
        print("\n" + "=" * 80)
        print("DEMO 4: Streaming Chain Output")
        print("=" * 80)
        
        inputs = {
            "tone": "inspirational",
            "content_type": "story",
            "topic": "perseverance"
        }
        
        print(f"\nChain Inputs:")
        for key, value in inputs.items():
            print(f"  {key}: {value}")
        
        print("\nStreaming chain output:")
        print("-" * 80)
        
        agent.events = []  # Reset events
        
        def on_chain_chunk(chunk: str):
            print(chunk, end="", flush=True)
        
        result4 = agent.stream_chain(inputs, on_chunk=on_chain_chunk)
        
        print("\n" + "-" * 80)
        print(f"\nChain Duration: {result4.duration:.3f}s")
        print(f"Content Length: {len(result4.full_content)} chars")
        
        # Demo 5: Streaming vs Sync Comparison
        print("\n" + "=" * 80)
        print("DEMO 5: Streaming vs Synchronous Comparison")
        print("=" * 80)
        
        test_prompt = "Explain the benefits of streaming in three sentences."
        
        print(f"\nPrompt: {test_prompt}")
        print("\nComparing streaming vs synchronous generation...")
        
        agent.events = []  # Reset events
        comparison = agent.compare_streaming_vs_sync(test_prompt)
        
        print("\n" + "-" * 80)
        print("RESULTS:")
        print("-" * 80)
        
        print("\nStreaming Mode:")
        print(f"  Duration: {comparison['streaming']['duration']:.3f}s")
        print(f"  Content Length: {comparison['streaming']['content_length']} chars")
        print(f"  Tokens: {comparison['streaming']['tokens']}")
        print(f"  Time to First Token: ~{comparison['streaming']['time_to_first_token']:.3f}s")
        
        print("\nSynchronous Mode:")
        print(f"  Duration: {comparison['synchronous']['duration']:.3f}s")
        print(f"  Content Length: {comparison['synchronous']['content_length']} chars")
        print(f"  Time to First Token: {comparison['synchronous']['time_to_first_token']:.3f}s")
        
        print("\nKey Insight:")
        print("  ✓ Streaming provides immediate feedback (TTFT)")
        print("  ✓ Better perceived performance and user experience")
        print("  ✓ Enables progressive rendering and cancellation")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        
        summary = """
    The Streaming Agent demonstrates real-time response generation:
    
    KEY CAPABILITIES:
    1. Token Streaming: Stream individual tokens as generated
    2. Chunk Streaming: Stream semantic chunks for better UX
    3. Progress Tracking: Monitor generation progress with callbacks
    4. Event Streaming: Rich events with metadata and timestamps
    5. Async Streaming: Non-blocking async/await patterns
    6. Chain Streaming: Stream output from complex chains
    7. Error Recovery: Graceful handling of streaming errors
    8. Comparison: Measure streaming vs synchronous performance
    
    BENEFITS:
    - Immediate user feedback (low TTFT)
    - Better perceived performance
    - Progressive content rendering
    - Cancellation support
    - Reduced perceived latency
    - Enhanced user experience
    - Real-time interactivity
    - Efficient resource usage
    
    USE CASES:
    - Conversational chatbots with typing indicators
    - Long-form content generation (articles, reports)
    - Real-time translation services
    - Live code generation and explanation
    - Interactive storytelling
    - Streaming search results
    - Progressive document analysis
    - Collaborative writing tools
    
    PRODUCTION CONSIDERATIONS:
    1. Backpressure: Handle slow consumers gracefully
    2. Buffer Management: Prevent memory issues with large streams
    3. Error Recovery: Implement retry logic for network issues
    4. Cancellation: Support user-initiated cancellation
    5. Rate Limiting: Prevent excessive streaming costs
    6. Monitoring: Track streaming performance metrics
    7. WebSocket: Use WebSocket for web clients
    8. State Management: Handle connection drops and reconnects
    9. Testing: Test with various network conditions
    10. Optimization: Minimize latency for first token
    
    ADVANCED EXTENSIONS:
    - Multi-model streaming (ensemble outputs)
    - Streaming with tool calls
    - Real-time translation during streaming
    - Adaptive chunk sizing based on content
    - Client-side buffering strategies
    - Stream compression for bandwidth
    - Priority-based streaming
    - Streaming with citations and sources
    - Live annotation and formatting
    - Collaborative streaming sessions
    
    Streaming is essential for modern AI applications requiring responsive,
    interactive user experiences with immediate feedback.
    """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_streaming_agent()
    
        

    Agentic Design Pattern: Streaming Agent

    Pattern 078: Streaming Agent – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 078: Streaming Agent

    Description: A Streaming Agent delivers responses in real-time as they are generated, rather than waiting for complete output. This pattern is essential for building responsive user interfaces and handling long-running generation tasks. It provides immediate feedback to users and enables progressive rendering of results.

    The agent streams tokens as they are generated by the LLM, allowing for better
    user experience in conversational interfaces, content generation, and real-time
    data processing. It supports multiple streaming modes including token-by-token,
    chunk-by-chunk, and structured streaming with metadata.
    

    Components: 1. Token Streamer: Streams individual tokens as generated 2. Chunk Streamer: Streams semantic chunks (sentences, paragraphs) 3. Event Streamer: Streams events with metadata and progress 4. Progress Tracker: Monitors generation progress 5. Buffer Manager: Manages streaming buffers and backpressure 6. Error Handler: Handles streaming errors gracefully 7. Aggregator: Collects streamed content for final result 8. Rate Controller: Controls streaming rate

    Key Features: – Real-time token streaming – Chunk-based streaming – Progress callbacks – Cancellation support – Error recovery during streaming – Multiple output formats – Backpressure handling – Stream aggregation – Event-driven architecture – WebSocket support

    Use Cases: – Conversational chatbots with typing indicators – Long-form content generation – Real-time translation services – Live code generation and explanation – Progressive document analysis – Interactive storytelling – Streaming search results – Real-time data processing – Live transcription and summarization – Collaborative writing tools

    LangChain Implementation: Uses ChatOpenAI with streaming=True, callbacks for progress tracking, and LCEL chains with streaming support.


    File Statistics

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

    Module Documentation

    """
    Pattern 078: Streaming Agent
    
    Description:
        A Streaming Agent delivers responses in real-time as they are generated, rather
        than waiting for complete output. This pattern is essential for building responsive
        user interfaces and handling long-running generation tasks. It provides immediate
        feedback to users and enables progressive rendering of results.
    
        The agent streams tokens as they are generated by the LLM, allowing for better
        user experience in conversational interfaces, content generation, and real-time
        data processing. It supports multiple streaming modes including token-by-token,
        chunk-by-chunk, and structured streaming with metadata.
    
    Components:
        1. Token Streamer: Streams individual tokens as generated
        2. Chunk Streamer: Streams semantic chunks (sentences, paragraphs)
        3. Event Streamer: Streams events with metadata and progress
        4. Progress Tracker: Monitors generation progress
        5. Buffer Manager: Manages streaming buffers and backpressure
        6. Error Handler: Handles streaming errors gracefully
        7. Aggregator: Collects streamed content for final result
        8. Rate Controller: Controls streaming rate
    
    Key Features:
        - Real-time token streaming
        - Chunk-based streaming
        - Progress callbacks
        - Cancellation support
        - Error recovery during streaming
        - Multiple output formats
        - Backpressure handling
        - Stream aggregation
        - Event-driven architecture
        - WebSocket support
    
    Use Cases:
        - Conversational chatbots with typing indicators
        - Long-form content generation
        - Real-time translation services
        - Live code generation and explanation
        - Progressive document analysis
        - Interactive storytelling
        - Streaming search results
        - Real-time data processing
        - Live transcription and summarization
        - Collaborative writing tools
    
    LangChain Implementation:
        Uses ChatOpenAI with streaming=True, callbacks for progress tracking,
        and LCEL chains with streaming support.
    """
    

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


    Import Statements

    Standard Library Imports

    import asyncio
    import collections.deque
    import dataclasses.dataclass
    import dataclasses.field
    import datetime.datetime
    import enum.Enum
    import langchain_core.callbacks.BaseCallbackHandler
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import time
    import typing.Any
    import typing.AsyncIterator
    import typing.Callable
    import typing.Dict
    import typing.Iterator
    import typing.List
    import typing.Optional
    

    Purpose: Core Python functionality: – asyncio: Asynchronous I/O operations – collections.deque: Specialized container datatypes – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – enum.Enum: Enumeration support – langchain_core.callbacks.BaseCallbackHandler: Regular expression operations – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – time: Time-related functions – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.AsyncIterator: 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.Iterator: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: StreamMode

    Lines 72-78

    Inherits from: Enum

    Purpose:

    Streaming modes
    

    Class: StreamStatus

    Lines 81-88

    Inherits from: Enum

    Purpose:

    Stream status
    

    Class: StreamEvent

    Lines 92-99

    Purpose:

    Event in the stream
    

    Class: StreamConfig

    Lines 103-111

    Purpose:

    Configuration for streaming
    

    Class: StreamResult

    Lines 115-122

    Purpose:

    Result of streaming operation
    

    Class: StreamingCallbackHandler

    Lines 125-148

    Inherits from: BaseCallbackHandler

    Purpose:

    Callback handler for streaming events
    

    Methods:

    __init__(self, on_token, on_chunk)

    Lines 128-136

    Purpose:

    Initialize callback handler
    

    Parameters: – on_token: On token – on_chunk: On chunk

    on_llm_new_token(self, token)

    Lines 138-142

    Purpose:

    Called when new token is generated
    

    Parameters: – token: Token

    Returns: None

    on_llm_end(self, response)

    Lines 144-148

    Purpose:

    Called when generation ends
    

    Parameters: – response: Response

    Returns: None


    Class: StreamingAgent

    Lines 151-579

    Purpose:

    Agent that streams responses in real-time.
    
    This agent provides various streaming capabilities including token streaming,
    chunk streaming, and event streaming with progress tracking.
    

    Methods:

    __init__(self, config)

    Lines 159-176

    Purpose:

    Initialize the streaming agent
    

    Parameters: – config: Config

    stream_tokens(self, prompt, on_token)

    Lines 178-243

    Purpose:

    Stream tokens as they are generated.
    
    Args:
        prompt: Input prompt
        on_token: Callback for each token
    
    Returns:
        StreamResult with full content and events
    

    Parameters: – prompt: Prompt template or text – on_token: On token

    Returns: StreamResult

    stream_chunks(self, prompt, chunk_size, on_chunk)

    Lines 245-326

    Purpose:

    Stream content in chunks.
    
    Args:
        prompt: Input prompt
        chunk_size: Size of chunks in characters
        on_chunk: Callback for each chunk
    
    Returns:
        StreamResult with full content and events
    

    Parameters: – prompt: Prompt template or text – chunk_size: Chunk size – on_chunk: On chunk

    Returns: StreamResult

    stream_with_progress(self, prompt, estimated_tokens, on_progress)

    Lines 328-410

    Purpose:

    Stream with progress tracking.
    
    Args:
        prompt: Input prompt
        estimated_tokens: Estimated total tokens for progress
        on_progress: Callback with progress (0.0-1.0) and content
    
    Returns:
        StreamResult with full content and progress events
    

    Parameters: – prompt: Prompt template or text – estimated_tokens: Estimated tokens – on_progress: On progress

    Returns: StreamResult

    stream_chain(self, inputs, on_chunk)

    Lines 475-535

    Purpose:

    Stream output from a chain.
    
    Args:
        inputs: Chain inputs
        on_chunk: Callback for each chunk
    
    Returns:
        StreamResult with full content
    

    Parameters: – inputs: Input data – on_chunk: On chunk

    Returns: StreamResult

    compare_streaming_vs_sync(self, prompt)

    Lines 537-579

    Purpose:

    Compare streaming vs synchronous generation.
    
    Args:
        prompt: Input prompt
    
    Returns:
        Comparison results
    

    Parameters: – prompt: Prompt template or text

    Returns: <ast.Subscript object at 0x000001A06D63BF10>


    Functions

    demonstrate_streaming_agent()

    Lines 582-805

    Purpose:

    Demonstrate the streaming agent capabilities
    

    Key Concepts Summary

    Pattern Overview

    The Streaming Agent 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 077
    • Pattern 079

    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 Streaming Agent 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: 078_streaming_agent.py
    Total Lines: 810
    Pattern Category: AI Agent Design Pattern #078


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Scientific Discovery

    
        """
    Pattern 077: Scientific Discovery Agent
    
    Description:
        A Scientific Discovery Agent is a specialized AI agent designed to accelerate
        scientific research through hypothesis generation, experimental design, data
        interpretation, and knowledge synthesis. This pattern demonstrates how to build
        an intelligent agent that can formulate research hypotheses, design experiments,
        analyze results, identify patterns, and suggest new directions for scientific
        inquiry.
        
        The agent combines domain knowledge with scientific reasoning to assist researchers
        in making discoveries. It can generate novel hypotheses from observations, design
        experiments to test them, interpret experimental data, identify anomalies, and
        synthesize findings into coherent scientific knowledge.
    
    Components:
        1. Hypothesis Generator: Formulates testable scientific hypotheses
        2. Experiment Designer: Designs experiments with proper controls
        3. Data Analyzer: Analyzes experimental results statistically
        4. Pattern Recognizer: Identifies patterns and anomalies in data
        5. Knowledge Synthesizer: Integrates findings with existing knowledge
        6. Explanation Generator: Proposes mechanistic explanations
        7. Validation Checker: Ensures scientific rigor and reproducibility
        8. Discovery Evaluator: Assesses novelty and significance
    
    Key Features:
        - Novel hypothesis generation from observations
        - Experimental design with proper controls
        - Statistical analysis and significance testing
        - Pattern and anomaly detection
        - Mechanistic explanation generation
        - Knowledge graph integration
        - Reproducibility validation
        - Novelty assessment
        - Multi-scale reasoning (molecular to systems level)
        - Cross-domain insight transfer
    
    Use Cases:
        - Drug discovery and development
        - Materials science research
        - Biology and genetics research
        - Physics and chemistry experiments
        - Climate science analysis
        - Medical research and diagnostics
        - Agricultural optimization
        - Protein structure prediction
        - Reaction mechanism discovery
        - Systems biology modeling
    
    LangChain Implementation:
        Uses ChatOpenAI with specialized prompts for hypothesis generation,
        experimental design, and scientific reasoning with varied temperatures.
    """
    
    import os
    import re
    from typing import List, Dict, Any, Optional, Tuple
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from collections import defaultdict
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class ResearchDomain(Enum):
        """Scientific research domains"""
        BIOLOGY = "biology"
        CHEMISTRY = "chemistry"
        PHYSICS = "physics"
        MEDICINE = "medicine"
        MATERIALS_SCIENCE = "materials_science"
        ENVIRONMENTAL_SCIENCE = "environmental_science"
        NEUROSCIENCE = "neuroscience"
        GENETICS = "genetics"
        PHARMACOLOGY = "pharmacology"
        ASTROPHYSICS = "astrophysics"
    
    
    class HypothesisType(Enum):
        """Types of scientific hypotheses"""
        CAUSAL = "causal"
        CORRELATIONAL = "correlational"
        MECHANISTIC = "mechanistic"
        PREDICTIVE = "predictive"
        COMPARATIVE = "comparative"
    
    
    class ExperimentType(Enum):
        """Types of experiments"""
        CONTROLLED = "controlled"
        OBSERVATIONAL = "observational"
        COMPUTATIONAL = "computational"
        CLINICAL_TRIAL = "clinical_trial"
        FIELD_STUDY = "field_study"
    
    
    class SignificanceLevel(Enum):
        """Statistical significance levels"""
        HIGHLY_SIGNIFICANT = "p < 0.001"
        SIGNIFICANT = "p < 0.05"
        MARGINALLY_SIGNIFICANT = "p = 0.1"
    
    
    @dataclass
    class Observation:
        """A scientific observation"""
        observation_id: str
        domain: ResearchDomain
        description: str
        measured_variables: Dict[str, Any]
        context: str
        timestamp: datetime = field(default_factory=datetime.now)
    
    
    @dataclass
    class Hypothesis:
        """A scientific hypothesis"""
        hypothesis_id: str
        type: HypothesisType
        statement: str
        rationale: str
        predictions: List[str]
        testability_score: float  # 0.0-1.0
        novelty_score: float  # 0.0-1.0
        variables: Dict[str, str]  # independent, dependent, control
        supporting_evidence: List[str] = field(default_factory=list)
    
    
    @dataclass
    class ExperimentDesign:
        """Design for a scientific experiment"""
        experiment_id: str
        hypothesis_id: str
        experiment_type: ExperimentType
        objective: str
        methodology: str
        independent_variables: List[str]
        dependent_variables: List[str]
        control_variables: List[str]
        sample_size: int
        control_groups: List[str]
        experimental_groups: List[str]
        measurements: List[str]
        duration: str
        expected_outcomes: List[str]
    
    
    @dataclass
    class ExperimentalResult:
        """Results from an experiment"""
        experiment_id: str
        measurements: Dict[str, List[float]]
        observations: List[str]
        raw_data_summary: str
        anomalies: List[str] = field(default_factory=list)
    
    
    @dataclass
    class StatisticalAnalysis:
        """Statistical analysis of results"""
        test_name: str
        test_statistic: float
        p_value: float
        significance_level: SignificanceLevel
        confidence_interval: Optional[Tuple[float, float]]
        effect_size: float
        interpretation: str
    
    
    @dataclass
    class ScientificFinding:
        """A scientific finding or discovery"""
        finding_id: str
        hypothesis_id: str
        experiment_id: str
        conclusion: str
        evidence: List[str]
        confidence: float  # 0.0-1.0
        statistical_analyses: List[StatisticalAnalysis]
        limitations: List[str]
        implications: List[str]
        future_directions: List[str]
    
    
    @dataclass
    class MechanisticExplanation:
        """Mechanistic explanation of a phenomenon"""
        phenomenon: str
        mechanism: str
        components: List[str]
        interactions: List[str]
        evidence_support: List[str]
        confidence: float
    
    
    class ScientificDiscoveryAgent:
        """
        Agent for accelerating scientific discovery and research.
        
        This agent can generate hypotheses, design experiments, analyze data,
        and synthesize findings to advance scientific knowledge.
        """
        
        def __init__(self):
            """Initialize the scientific discovery agent"""
            # Hypothesis generator (creative)
            self.hypothesis_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.8)
            
            # Experiment designer (structured)
            self.designer_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
            
            # Data analyzer (precise)
            self.analyzer_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.1)
            
            # Synthesizer (integrative)
            self.synthesizer_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.4)
            
            # Knowledge base
            self.observations: List[Observation] = []
            self.hypotheses: List[Hypothesis] = []
            self.experiments: List[ExperimentDesign] = []
            self.findings: List[ScientificFinding] = []
        
        def generate_hypotheses(
            self,
            observations: List[Observation],
            domain: ResearchDomain,
            count: int = 3
        ) -> List[Hypothesis]:
            """
            Generate scientific hypotheses from observations.
            
            Args:
                observations: List of observations
                domain: Research domain
                count: Number of hypotheses to generate
                
            Returns:
                List of Hypothesis objects
            """
            # Compile observations
            obs_summary = "\n".join([
                f"- {obs.description}" for obs in observations[:10]
            ])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a creative scientist. Generate novel, testable
                hypotheses based on the observations.
                
                For each hypothesis, provide:
                TYPE: causal/correlational/mechanistic/predictive/comparative
                STATEMENT: Clear hypothesis statement
                RATIONALE: Scientific reasoning
                PREDICTIONS: Testable predictions (comma-separated)
                TESTABILITY: Score 0.0-1.0
                NOVELTY: Score 0.0-1.0
                ---"""),
                ("user", """Domain: {domain}
    
    Observations:
    {observations}
    
    Generate {count} scientific hypotheses.""")
            ])
            
            chain = prompt | self.hypothesis_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "domain": domain.value,
                    "observations": obs_summary,
                    "count": count
                })
                
                hypotheses = []
                current_hyp = {}
                hyp_counter = 1
                
                for line in response.split('\n'):
                    line = line.strip()
                    
                    if line == '---' and current_hyp:
                        # Create hypothesis
                        if 'STATEMENT' in current_hyp:
                            hyp_type = HypothesisType.CAUSAL
                            type_str = current_hyp.get('TYPE', '').lower()
                            if 'correlational' in type_str:
                                hyp_type = HypothesisType.CORRELATIONAL
                            elif 'mechanistic' in type_str:
                                hyp_type = HypothesisType.MECHANISTIC
                            elif 'predictive' in type_str:
                                hyp_type = HypothesisType.PREDICTIVE
                            elif 'comparative' in type_str:
                                hyp_type = HypothesisType.COMPARATIVE
                            
                            testability = 0.7
                            try:
                                testability = float(current_hyp.get('TESTABILITY', '0.7'))
                            except:
                                pass
                            
                            novelty = 0.6
                            try:
                                novelty = float(current_hyp.get('NOVELTY', '0.6'))
                            except:
                                pass
                            
                            predictions = []
                            if 'PREDICTIONS' in current_hyp:
                                predictions = [p.strip() for p in current_hyp['PREDICTIONS'].split(',')]
                            
                            hypotheses.append(Hypothesis(
                                hypothesis_id=f"hyp_{hyp_counter}",
                                type=hyp_type,
                                statement=current_hyp.get('STATEMENT', ''),
                                rationale=current_hyp.get('RATIONALE', ''),
                                predictions=predictions,
                                testability_score=testability,
                                novelty_score=novelty,
                                variables={}
                            ))
                            hyp_counter += 1
                        current_hyp = {}
                    elif ':' in line:
                        parts = line.split(':', 1)
                        key = parts[0].strip().upper()
                        value = parts[1].strip()
                        current_hyp[key] = value
                
                # Add last hypothesis
                if current_hyp and 'STATEMENT' in current_hyp:
                    hypotheses.append(Hypothesis(
                        hypothesis_id=f"hyp_{hyp_counter}",
                        type=HypothesisType.CAUSAL,
                        statement=current_hyp.get('STATEMENT', ''),
                        rationale=current_hyp.get('RATIONALE', ''),
                        predictions=[],
                        testability_score=0.7,
                        novelty_score=0.6,
                        variables={}
                    ))
                
                # Fallback if no hypotheses generated
                if not hypotheses:
                    for i in range(count):
                        hypotheses.append(Hypothesis(
                            hypothesis_id=f"hyp_{i+1}",
                            type=HypothesisType.CAUSAL,
                            statement=f"Hypothesis {i+1} about {domain.value} phenomena",
                            rationale="Based on observed patterns",
                            predictions=["Testable prediction"],
                            testability_score=0.7,
                            novelty_score=0.6,
                            variables={}
                        ))
                
                return hypotheses[:count]
                
            except Exception as e:
                # Fallback hypotheses
                return [
                    Hypothesis(
                        hypothesis_id=f"hyp_{i+1}",
                        type=HypothesisType.CAUSAL,
                        statement=f"Research hypothesis {i+1}",
                        rationale="Scientific investigation required",
                        predictions=["Observable outcome"],
                        testability_score=0.7,
                        novelty_score=0.5,
                        variables={}
                    )
                    for i in range(count)
                ]
        
        def design_experiment(
            self,
            hypothesis: Hypothesis,
            domain: ResearchDomain
        ) -> ExperimentDesign:
            """
            Design an experiment to test a hypothesis.
            
            Args:
                hypothesis: Hypothesis to test
                domain: Research domain
                
            Returns:
                ExperimentDesign with methodology
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert experimental scientist. Design a rigorous
                experiment to test the hypothesis.
                
                Provide:
                TYPE: controlled/observational/computational/clinical_trial/field_study
                OBJECTIVE: Clear objective
                METHODOLOGY: Detailed method
                INDEPENDENT_VAR: Independent variables (comma-separated)
                DEPENDENT_VAR: Dependent variables (comma-separated)
                CONTROL_VAR: Control variables (comma-separated)
                SAMPLE_SIZE: Number of samples
                CONTROL_GROUPS: Control groups (comma-separated)
                EXPERIMENTAL_GROUPS: Experimental groups (comma-separated)
                MEASUREMENTS: What to measure (comma-separated)
                DURATION: Time period
                EXPECTED: Expected outcomes (comma-separated)"""),
                ("user", """Domain: {domain}
    
    Hypothesis: {hypothesis}
    Type: {hyp_type}
    Predictions: {predictions}
    
    Design an experiment to test this hypothesis.""")
            ])
            
            chain = prompt | self.designer_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "domain": domain.value,
                    "hypothesis": hypothesis.statement,
                    "hyp_type": hypothesis.type.value,
                    "predictions": ", ".join(hypothesis.predictions[:3])
                })
                
                exp_data = {}
                
                for line in response.split('\n'):
                    if ':' in line:
                        parts = line.split(':', 1)
                        key = parts[0].strip().upper()
                        value = parts[1].strip()
                        exp_data[key] = value
                
                exp_type = ExperimentType.CONTROLLED
                type_str = exp_data.get('TYPE', '').lower()
                if 'observational' in type_str:
                    exp_type = ExperimentType.OBSERVATIONAL
                elif 'computational' in type_str:
                    exp_type = ExperimentType.COMPUTATIONAL
                elif 'clinical' in type_str:
                    exp_type = ExperimentType.CLINICAL_TRIAL
                elif 'field' in type_str:
                    exp_type = ExperimentType.FIELD_STUDY
                
                sample_size = 30
                try:
                    sample_size = int(re.findall(r'\d+', exp_data.get('SAMPLE_SIZE', '30'))[0])
                except:
                    pass
                
                return ExperimentDesign(
                    experiment_id=f"exp_{datetime.now().timestamp()}",
                    hypothesis_id=hypothesis.hypothesis_id,
                    experiment_type=exp_type,
                    objective=exp_data.get('OBJECTIVE', f"Test {hypothesis.statement}"),
                    methodology=exp_data.get('METHODOLOGY', "Experimental methodology"),
                    independent_variables=[v.strip() for v in exp_data.get('INDEPENDENT_VAR', '').split(',')],
                    dependent_variables=[v.strip() for v in exp_data.get('DEPENDENT_VAR', '').split(',')],
                    control_variables=[v.strip() for v in exp_data.get('CONTROL_VAR', '').split(',')],
                    sample_size=sample_size,
                    control_groups=[g.strip() for g in exp_data.get('CONTROL_GROUPS', 'Control').split(',')],
                    experimental_groups=[g.strip() for g in exp_data.get('EXPERIMENTAL_GROUPS', 'Treatment').split(',')],
                    measurements=[m.strip() for m in exp_data.get('MEASUREMENTS', 'Primary outcome').split(',')],
                    duration=exp_data.get('DURATION', '4 weeks'),
                    expected_outcomes=[o.strip() for o in exp_data.get('EXPECTED', 'Positive effect').split(',')]
                )
                
            except Exception as e:
                return ExperimentDesign(
                    experiment_id=f"exp_{datetime.now().timestamp()}",
                    hypothesis_id=hypothesis.hypothesis_id,
                    experiment_type=ExperimentType.CONTROLLED,
                    objective=f"Test hypothesis: {hypothesis.statement[:50]}",
                    methodology="Controlled experimental study",
                    independent_variables=["Treatment"],
                    dependent_variables=["Outcome measure"],
                    control_variables=["Environmental factors"],
                    sample_size=30,
                    control_groups=["Control"],
                    experimental_groups=["Treatment"],
                    measurements=["Primary outcome"],
                    duration="4 weeks",
                    expected_outcomes=["Expected result"]
                )
        
        def analyze_results(
            self,
            experiment: ExperimentDesign,
            results: ExperimentalResult
        ) -> List[StatisticalAnalysis]:
            """
            Analyze experimental results statistically.
            
            Args:
                experiment: Experiment design
                results: Experimental results
                
            Returns:
                List of StatisticalAnalysis
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a biostatistician. Analyze the experimental results
                and determine statistical significance.
                
                For each analysis, provide:
                TEST: Statistical test name
                STATISTIC: Test statistic value
                P_VALUE: P-value
                EFFECT_SIZE: Effect size measure
                INTERPRETATION: What the results mean"""),
                ("user", """Experiment: {objective}
    
    Results Summary:
    {results}
    
    Measurements:
    {measurements}
    
    Analyze these results statistically.""")
            ])
            
            chain = prompt | self.analyzer_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "objective": experiment.objective,
                    "results": results.raw_data_summary,
                    "measurements": ", ".join(experiment.measurements)
                })
                
                analyses = []
                
                # Parse response for statistical tests
                test_name = "t-test"
                statistic = 2.5
                p_value = 0.03
                effect_size = 0.5
                interpretation = "Analysis shows significant effect"
                
                for line in response.split('\n'):
                    if 'TEST:' in line:
                        test_name = line.replace('TEST:', '').strip()
                    elif 'STATISTIC:' in line:
                        try:
                            statistic = float(re.findall(r'[-\d.]+', line)[0])
                        except:
                            pass
                    elif 'P_VALUE:' in line or 'P-VALUE:' in line:
                        try:
                            p_value = float(re.findall(r'[\d.]+', line)[0])
                        except:
                            pass
                    elif 'EFFECT_SIZE:' in line:
                        try:
                            effect_size = float(re.findall(r'[-\d.]+', line)[0])
                        except:
                            pass
                    elif 'INTERPRETATION:' in line:
                        interpretation = line.replace('INTERPRETATION:', '').strip()
                
                # Determine significance level
                if p_value < 0.001:
                    sig_level = SignificanceLevel.HIGHLY_SIGNIFICANT
                elif p_value < 0.05:
                    sig_level = SignificanceLevel.SIGNIFICANT
                elif p_value  ScientificFinding:
            """
            Synthesize experimental findings into scientific conclusions.
            
            Args:
                hypothesis: Original hypothesis
                experiment: Experiment design
                results: Experimental results
                analyses: Statistical analyses
                
            Returns:
                ScientificFinding with conclusions
            """
            # Compile analyses
            analyses_summary = "\n".join([
                f"- {a.test_name}: {a.significance_level.value}, effect size = {a.effect_size:.2f}"
                for a in analyses
            ])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a research scientist. Synthesize the experimental
                findings into scientific conclusions.
                
                Provide:
                CONCLUSION: Main conclusion
                EVIDENCE: Supporting evidence (one per line starting with -)
                CONFIDENCE: Confidence level (0.0-1.0)
                LIMITATIONS: Study limitations (one per line starting with -)
                IMPLICATIONS: Scientific implications (one per line starting with -)
                FUTURE: Future research directions (one per line starting with -)"""),
                ("user", """Hypothesis: {hypothesis}
    
    Experiment: {experiment}
    
    Statistical Analyses:
    {analyses}
    
    Observations:
    {observations}
    
    Synthesize the findings.""")
            ])
            
            chain = prompt | self.synthesizer_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "hypothesis": hypothesis.statement,
                    "experiment": experiment.objective,
                    "analyses": analyses_summary,
                    "observations": ", ".join(results.observations[:5])
                })
                
                conclusion = ""
                evidence = []
                confidence = 0.7
                limitations = []
                implications = []
                future = []
                
                current_section = None
                
                for line in response.split('\n'):
                    line = line.strip()
                    if not line:
                        continue
                    
                    if line.startswith('CONCLUSION:'):
                        conclusion = line.replace('CONCLUSION:', '').strip()
                    elif line.startswith('EVIDENCE:'):
                        current_section = 'evidence'
                    elif line.startswith('CONFIDENCE:'):
                        try:
                            confidence = float(re.findall(r'[\d.]+', line)[0])
                        except:
                            pass
                    elif line.startswith('LIMITATIONS:'):
                        current_section = 'limitations'
                    elif line.startswith('IMPLICATIONS:'):
                        current_section = 'implications'
                    elif line.startswith('FUTURE:'):
                        current_section = 'future'
                    elif line.startswith('-'):
                        content = line[1:].strip()
                        if current_section == 'evidence':
                            evidence.append(content)
                        elif current_section == 'limitations':
                            limitations.append(content)
                        elif current_section == 'implications':
                            implications.append(content)
                        elif current_section == 'future':
                            future.append(content)
                
                return ScientificFinding(
                    finding_id=f"finding_{datetime.now().timestamp()}",
                    hypothesis_id=hypothesis.hypothesis_id,
                    experiment_id=experiment.experiment_id,
                    conclusion=conclusion if conclusion else "Findings support the hypothesis",
                    evidence=evidence if evidence else ["Statistical significance observed"],
                    confidence=confidence,
                    statistical_analyses=analyses,
                    limitations=limitations if limitations else ["Further validation needed"],
                    implications=implications if implications else ["Important for field"],
                    future_directions=future if future else ["Continue research"]
                )
                
            except Exception as e:
                return ScientificFinding(
                    finding_id=f"finding_{datetime.now().timestamp()}",
                    hypothesis_id=hypothesis.hypothesis_id,
                    experiment_id=experiment.experiment_id,
                    conclusion="Experimental findings documented",
                    evidence=["Data collected and analyzed"],
                    confidence=0.7,
                    statistical_analyses=analyses,
                    limitations=["Study limitations exist"],
                    implications=["Further research needed"],
                    future_directions=["Additional experiments recommended"]
                )
        
        def propose_mechanism(
            self,
            phenomenon: str,
            findings: List[ScientificFinding],
            domain: ResearchDomain
        ) -> MechanisticExplanation:
            """
            Propose mechanistic explanation for a phenomenon.
            
            Args:
                phenomenon: Phenomenon to explain
                findings: Related scientific findings
                domain: Research domain
                
            Returns:
                MechanisticExplanation
            """
            findings_summary = "\n".join([
                f"- {f.conclusion}" for f in findings[:5]
            ])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a theoretical scientist. Propose a mechanistic
                explanation for the phenomenon based on findings.
                
                Provide:
                MECHANISM: Mechanistic explanation
                COMPONENTS: Key components (comma-separated)
                INTERACTIONS: How components interact (comma-separated)
                EVIDENCE: Supporting evidence (comma-separated)
                CONFIDENCE: Confidence level (0.0-1.0)"""),
                ("user", """Domain: {domain}
    Phenomenon: {phenomenon}
    
    Related Findings:
    {findings}
    
    Propose a mechanistic explanation.""")
            ])
            
            chain = prompt | self.synthesizer_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "domain": domain.value,
                    "phenomenon": phenomenon,
                    "findings": findings_summary
                })
                
                mechanism = ""
                components = []
                interactions = []
                evidence = []
                confidence = 0.6
                
                for line in response.split('\n'):
                    if 'MECHANISM:' in line:
                        mechanism = line.replace('MECHANISM:', '').strip()
                    elif 'COMPONENTS:' in line:
                        comp_str = line.replace('COMPONENTS:', '').strip()
                        components = [c.strip() for c in comp_str.split(',')]
                    elif 'INTERACTIONS:' in line:
                        int_str = line.replace('INTERACTIONS:', '').strip()
                        interactions = [i.strip() for i in int_str.split(',')]
                    elif 'EVIDENCE:' in line:
                        ev_str = line.replace('EVIDENCE:', '').strip()
                        evidence = [e.strip() for e in ev_str.split(',')]
                    elif 'CONFIDENCE:' in line:
                        try:
                            confidence = float(re.findall(r'[\d.]+', line)[0])
                        except:
                            pass
                
                return MechanisticExplanation(
                    phenomenon=phenomenon,
                    mechanism=mechanism if mechanism else f"Proposed mechanism for {phenomenon}",
                    components=components if components else ["Component A", "Component B"],
                    interactions=interactions if interactions else ["Interaction pathway"],
                    evidence_support=evidence if evidence else ["Experimental support"],
                    confidence=confidence
                )
                
            except Exception as e:
                return MechanisticExplanation(
                    phenomenon=phenomenon,
                    mechanism=f"Mechanistic explanation of {phenomenon}",
                    components=["Key components"],
                    interactions=["Interaction mechanisms"],
                    evidence_support=["Supporting evidence"],
                    confidence=0.6
                )
        
        def conduct_research_cycle(
            self,
            initial_observation: Observation,
            domain: ResearchDomain
        ) -> Dict[str, Any]:
            """
            Conduct a complete research cycle from observation to finding.
            
            Args:
                initial_observation: Starting observation
                domain: Research domain
                
            Returns:
                Dictionary with complete research cycle results
            """
            print(f"\n🔬 Starting research cycle in {domain.value}...")
            
            # Step 1: Generate hypotheses
            print("  ✓ Generating hypotheses from observations...")
            hypotheses = self.generate_hypotheses([initial_observation], domain, count=2)
            
            # Step 2: Design experiment
            print("  ✓ Designing experiment...")
            experiment = self.design_experiment(hypotheses[0], domain)
            
            # Step 3: Simulate results (in production, would be real data)
            print("  ✓ Collecting experimental data...")
            results = ExperimentalResult(
                experiment_id=experiment.experiment_id,
                measurements={"outcome": [1.2, 1.5, 1.8, 1.3, 1.6]},
                observations=["Positive effect observed", "Dose-dependent response"],
                raw_data_summary="Statistical analysis shows significant positive effect",
                anomalies=[]
            )
            
            # Step 4: Analyze results
            print("  ✓ Analyzing results statistically...")
            analyses = self.analyze_results(experiment, results)
            
            # Step 5: Synthesize findings
            print("  ✓ Synthesizing scientific findings...")
            finding = self.synthesize_finding(hypotheses[0], experiment, results, analyses)
            
            # Step 6: Propose mechanism
            print("  ✓ Proposing mechanistic explanation...")
            mechanism = self.propose_mechanism(
                initial_observation.description,
                [finding],
                domain
            )
            
            print("  ✅ Research cycle complete!\n")
            
            return {
                "observation": initial_observation,
                "hypotheses": hypotheses,
                "experiment": experiment,
                "results": results,
                "analyses": analyses,
                "finding": finding,
                "mechanism": mechanism
            }
    
    
    def demonstrate_scientific_discovery_agent():
        """Demonstrate the scientific discovery agent capabilities"""
        print("=" * 80)
        print("SCIENTIFIC DISCOVERY AGENT DEMONSTRATION")
        print("=" * 80)
        
        agent = ScientificDiscoveryAgent()
        
        # Demo 1: Complete Research Cycle
        print("\n" + "=" * 80)
        print("DEMO 1: Complete Research Cycle")
        print("=" * 80)
        
        observation = Observation(
            observation_id="obs_001",
            domain=ResearchDomain.BIOLOGY,
            description="Certain proteins show increased expression in the presence of specific environmental stressors",
            measured_variables={"protein_levels": "elevated", "stress_exposure": "high"},
            context="Cell culture experiment"
        )
        
        cycle_results = agent.conduct_research_cycle(observation, ResearchDomain.BIOLOGY)
        
        print("RESEARCH CYCLE RESULTS")
        print("-" * 80)
        
        print(f"\nInitial Observation:")
        print(f"  {observation.description}")
        
        print(f"\nGenerated Hypotheses ({len(cycle_results['hypotheses'])}):")
        for i, hyp in enumerate(cycle_results['hypotheses'], 1):
            print(f"\n  {i}. {hyp.statement}")
            print(f"     Type: {hyp.type.value}")
            print(f"     Testability: {hyp.testability_score:.2f}/1.0")
            print(f"     Novelty: {hyp.novelty_score:.2f}/1.0")
            if hyp.predictions:
                print(f"     Predictions: {hyp.predictions[0][:60]}...")
        
        experiment = cycle_results['experiment']
        print(f"\nExperimental Design:")
        print(f"  Type: {experiment.experiment_type.value}")
        print(f"  Objective: {experiment.objective[:100]}...")
        print(f"  Sample Size: {experiment.sample_size}")
        print(f"  Independent Variables: {', '.join(experiment.independent_variables[:3])}")
        print(f"  Dependent Variables: {', '.join(experiment.dependent_variables[:3])}")
        print(f"  Duration: {experiment.duration}")
        
        print(f"\nStatistical Analyses ({len(cycle_results['analyses'])}):")
        for analysis in cycle_results['analyses']:
            print(f"  • {analysis.test_name}")
            print(f"    Statistic: {analysis.test_statistic:.3f}")
            print(f"    P-value: {analysis.p_value:.4f}")
            print(f"    Significance: {analysis.significance_level.value}")
            print(f"    Effect Size: {analysis.effect_size:.2f}")
            print(f"    {analysis.interpretation[:80]}...")
        
        finding = cycle_results['finding']
        print(f"\nScientific Finding:")
        print(f"  Conclusion: {finding.conclusion[:150]}...")
        print(f"  Confidence: {finding.confidence:.2f}/1.0")
        print(f"\n  Evidence:")
        for evidence in finding.evidence[:2]:
            print(f"    • {evidence}")
        print(f"\n  Implications:")
        for impl in finding.implications[:2]:
            print(f"    • {impl}")
        
        mechanism = cycle_results['mechanism']
        print(f"\nMechanistic Explanation:")
        print(f"  Mechanism: {mechanism.mechanism[:150]}...")
        print(f"  Components: {', '.join(mechanism.components[:3])}")
        print(f"  Confidence: {mechanism.confidence:.2f}/1.0")
        
        # Demo 2: Hypothesis Generation
        print("\n" + "=" * 80)
        print("DEMO 2: Multi-Hypothesis Generation")
        print("=" * 80)
        
        obs_list = [
            Observation(
                observation_id="obs_002",
                domain=ResearchDomain.CHEMISTRY,
                description="Novel catalyst reduces reaction time by 50%",
                measured_variables={"reaction_time": "reduced"},
                context="Catalysis experiment"
            ),
            Observation(
                observation_id="obs_003",
                domain=ResearchDomain.CHEMISTRY,
                description="Temperature affects catalyst efficiency non-linearly",
                measured_variables={"temperature": "variable", "efficiency": "non-linear"},
                context="Temperature study"
            )
        ]
        
        print("\nObservations:")
        for obs in obs_list:
            print(f"  • {obs.description}")
        
        print("\nGenerating hypotheses...")
        hypotheses = agent.generate_hypotheses(obs_list, ResearchDomain.CHEMISTRY, count=3)
        
        print(f"\nGenerated Hypotheses:")
        for i, hyp in enumerate(hypotheses, 1):
            print(f"\n{i}. {hyp.statement}")
            print(f"   Type: {hyp.type.value}")
            print(f"   Rationale: {hyp.rationale[:100]}...")
            print(f"   Testability: {hyp.testability_score:.2f}, Novelty: {hyp.novelty_score:.2f}")
        
        # Demo 3: Experiment Design
        print("\n" + "=" * 80)
        print("DEMO 3: Detailed Experiment Design")
        print("=" * 80)
        
        test_hypothesis = hypotheses[0]
        print(f"\nHypothesis to test:")
        print(f"  {test_hypothesis.statement}")
        
        print("\nDesigning experiment...")
        exp_design = agent.design_experiment(test_hypothesis, ResearchDomain.CHEMISTRY)
        
        print(f"\nExperimental Design:")
        print(f"  ID: {exp_design.experiment_id}")
        print(f"  Type: {exp_design.experiment_type.value}")
        print(f"  Objective: {exp_design.objective}")
        print(f"\n  Methodology:")
        print(f"    {exp_design.methodology[:200]}...")
        print(f"\n  Variables:")
        print(f"    Independent: {', '.join(exp_design.independent_variables)}")
        print(f"    Dependent: {', '.join(exp_design.dependent_variables)}")
        print(f"    Control: {', '.join(exp_design.control_variables[:3])}")
        print(f"\n  Groups:")
        print(f"    Control: {', '.join(exp_design.control_groups)}")
        print(f"    Experimental: {', '.join(exp_design.experimental_groups)}")
        print(f"\n  Sample Size: {exp_design.sample_size}")
        print(f"  Duration: {exp_design.duration}")
        print(f"\n  Expected Outcomes:")
        for outcome in exp_design.expected_outcomes[:2]:
            print(f"    • {outcome}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        
        summary = """
    The Scientific Discovery Agent demonstrates advanced capabilities for research:
    
    KEY CAPABILITIES:
    1. Hypothesis Generation: Creates novel, testable hypotheses from observations
    2. Experiment Design: Designs rigorous experiments with proper controls
    3. Statistical Analysis: Analyzes results with appropriate statistical methods
    4. Finding Synthesis: Integrates results into scientific conclusions
    5. Mechanism Proposal: Proposes mechanistic explanations for phenomena
    6. Research Cycles: Executes complete discovery workflows
    
    BENEFITS:
    - Accelerates hypothesis generation and testing
    - Ensures experimental rigor and reproducibility
    - Provides statistical expertise for all analyses
    - Identifies patterns humans might miss
    - Proposes novel research directions
    - Synthesizes findings across studies
    - Reduces time from observation to discovery
    
    USE CASES:
    - Drug discovery and development
    - Materials science research
    - Protein structure and function studies
    - Disease mechanism investigation
    - Climate and environmental research
    - Agricultural optimization
    - Chemical reaction discovery
    - Medical diagnostics development
    - Genetic research
    - Neuroscience investigations
    
    PRODUCTION CONSIDERATIONS:
    1. Domain Knowledge: Deep integration with scientific databases and literature
    2. Data Integration: Connect to lab instruments and data repositories
    3. Validation: Rigorous statistical methods and reproducibility checks
    4. Visualization: Generate charts, plots, and visual explanations
    5. Collaboration: Support multi-researcher workflows
    6. Documentation: Automatic research documentation and reporting
    7. Ethics: Ensure ethical research practices and safety
    8. Peer Review: Facilitate peer review and validation
    9. Publication: Generate publication-ready manuscripts
    10. IP Protection: Track novel discoveries and inventions
    
    ADVANCED EXTENSIONS:
    - Automated literature review for context
    - Predictive modeling of experimental outcomes
    - Multi-scale simulation integration
    - Active learning for optimal experiment selection
    - Causal inference from observational data
    - Anomaly detection in experimental results
    - Cross-domain knowledge transfer
    - Automated protocol optimization
    - Real-time experiment monitoring
    - Collaborative discovery networks
    
    The agent transforms scientific research by accelerating the discovery process
    while maintaining rigorous scientific standards and reproducibility.
    """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_scientific_discovery_agent()
    
        

    Agentic Design Pattern: Scientific Discovery

    Pattern 077: Scientific Discovery – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 077: Scientific Discovery Agent

    Description: A Scientific Discovery Agent is a specialized AI agent designed to accelerate scientific research through hypothesis generation, experimental design, data interpretation, and knowledge synthesis. This pattern demonstrates how to build an intelligent agent that can formulate research hypotheses, design experiments, analyze results, identify patterns, and suggest new directions for scientific inquiry.

    The agent combines domain knowledge with scientific reasoning to assist researchers
    in making discoveries. It can generate novel hypotheses from observations, design
    experiments to test them, interpret experimental data, identify anomalies, and
    synthesize findings into coherent scientific knowledge.
    

    Components: 1. Hypothesis Generator: Formulates testable scientific hypotheses 2. Experiment Designer: Designs experiments with proper controls 3. Data Analyzer: Analyzes experimental results statistically 4. Pattern Recognizer: Identifies patterns and anomalies in data 5. Knowledge Synthesizer: Integrates findings with existing knowledge 6. Explanation Generator: Proposes mechanistic explanations 7. Validation Checker: Ensures scientific rigor and reproducibility 8. Discovery Evaluator: Assesses novelty and significance

    Key Features: – Novel hypothesis generation from observations – Experimental design with proper controls – Statistical analysis and significance testing – Pattern and anomaly detection – Mechanistic explanation generation – Knowledge graph integration – Reproducibility validation – Novelty assessment – Multi-scale reasoning (molecular to systems level) – Cross-domain insight transfer

    Use Cases: – Drug discovery and development – Materials science research – Biology and genetics research – Physics and chemistry experiments – Climate science analysis – Medical research and diagnostics – Agricultural optimization – Protein structure prediction – Reaction mechanism discovery – Systems biology modeling

    LangChain Implementation: Uses ChatOpenAI with specialized prompts for hypothesis generation, experimental design, and scientific reasoning with varied temperatures.


    File Statistics

    • Total Lines: 1097
    • Classes: 12
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 077: Scientific Discovery Agent
    
    Description:
        A Scientific Discovery Agent is a specialized AI agent designed to accelerate
        scientific research through hypothesis generation, experimental design, data
        interpretation, and knowledge synthesis. This pattern demonstrates how to build
        an intelligent agent that can formulate research hypotheses, design experiments,
        analyze results, identify patterns, and suggest new directions for scientific
        inquiry.
    
        The agent combines domain knowledge with scientific reasoning to assist researchers
        in making discoveries. It can generate novel hypotheses from observations, design
        experiments to test them, interpret experimental data, identify anomalies, and
        synthesize findings into coherent scientific knowledge.
    
    Components:
        1. Hypothesis Generator: Formulates testable scientific hypotheses
        2. Experiment Designer: Designs experiments with proper controls
        3. Data Analyzer: Analyzes experimental results statistically
        4. Pattern Recognizer: Identifies patterns and anomalies in data
        5. Knowledge Synthesizer: Integrates findings with existing knowledge
        6. Explanation Generator: Proposes mechanistic explanations
        7. Validation Checker: Ensures scientific rigor and reproducibility
        8. Discovery Evaluator: Assesses novelty and significance
    
    Key Features:
        - Novel hypothesis generation from observations
        - Experimental design with proper controls
        - Statistical analysis and significance testing
        - Pattern and anomaly detection
        - Mechanistic explanation generation
        - Knowledge graph integration
        - Reproducibility validation
        - Novelty assessment
        - Multi-scale reasoning (molecular to systems level)
        - Cross-domain insight transfer
    
    Use Cases:
        - Drug discovery and development
        - Materials science research
        - Biology and genetics research
        - Physics and chemistry experiments
        - Climate science analysis
        - Medical research and diagnostics
        - Agricultural optimization
        - Protein structure prediction
        - Reaction mechanism discovery
        - Systems biology modeling
    
    LangChain Implementation:
        Uses ChatOpenAI with specialized prompts for hypothesis generation,
        experimental design, and scientific reasoning with varied temperatures.
    """
    

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


    Import Statements

    Standard Library Imports

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

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

    Lines 72-83

    Inherits from: Enum

    Purpose:

    Scientific research domains
    

    Class: HypothesisType

    Lines 86-92

    Inherits from: Enum

    Purpose:

    Types of scientific hypotheses
    

    Class: ExperimentType

    Lines 95-101

    Inherits from: Enum

    Purpose:

    Types of experiments
    

    Class: SignificanceLevel

    Lines 104-109

    Inherits from: Enum

    Purpose:

    Statistical significance levels
    

    Class: Observation

    Lines 113-120

    Purpose:

    A scientific observation
    

    Class: Hypothesis

    Lines 124-134

    Purpose:

    A scientific hypothesis
    

    Class: ExperimentDesign

    Lines 138-153

    Purpose:

    Design for a scientific experiment
    

    Class: ExperimentalResult

    Lines 157-163

    Purpose:

    Results from an experiment
    

    Class: StatisticalAnalysis

    Lines 167-175

    Purpose:

    Statistical analysis of results
    

    Class: ScientificFinding

    Lines 179-190

    Purpose:

    A scientific finding or discovery
    

    Class: MechanisticExplanation

    Lines 194-201

    Purpose:

    Mechanistic explanation of a phenomenon
    

    Class: ScientificDiscoveryAgent

    Lines 204-884

    Purpose:

    Agent for accelerating scientific discovery and research.
    
    This agent can generate hypotheses, design experiments, analyze data,
    and synthesize findings to advance scientific knowledge.
    

    Methods:

    __init__(self)

    Lines 212-230

    Purpose:

    Initialize the scientific discovery agent
    

    Parameters:

    generate_hypotheses(self, observations, domain, count)

    Lines 232-381

    Purpose:

    Generate scientific hypotheses from observations.
    
    Args:
        observations: List of observations
        domain: Research domain
        count: Number of hypotheses to generate
    
    Returns:
        List of Hypothesis objects
    

    Parameters: – observations: Observations – domain: Domain – count: Count

    Returns: <ast.Subscript object at 0x000001A06D6294D0>

    design_experiment(self, hypothesis, domain)

    Lines 383-493

    Purpose:

    Design an experiment to test a hypothesis.
    
    Args:
        hypothesis: Hypothesis to test
        domain: Research domain
    
    Returns:
        ExperimentDesign with methodology
    

    Parameters: – hypothesis: Hypothesis – domain: Domain

    Returns: ExperimentDesign

    analyze_results(self, experiment, results)

    Lines 495-603

    Purpose:

    Analyze experimental results statistically.
    
    Args:
        experiment: Experiment design
        results: Experimental results
    
    Returns:
        List of StatisticalAnalysis
    

    Parameters: – experiment: Experiment – results: Results

    Returns: <ast.Subscript object at 0x000001A06D7F58D0>

    synthesize_finding(self, hypothesis, experiment, results, analyses)

    Lines 605-729

    Purpose:

    Synthesize experimental findings into scientific conclusions.
    
    Args:
        hypothesis: Original hypothesis
        experiment: Experiment design
        results: Experimental results
        analyses: Statistical analyses
    
    Returns:
        ScientificFinding with conclusions
    

    Parameters: – hypothesis: Hypothesis – experiment: Experiment – results: Results – analyses: Analyses

    Returns: ScientificFinding

    propose_mechanism(self, phenomenon, findings, domain)

    Lines 731-821

    Purpose:

    Propose mechanistic explanation for a phenomenon.
    
    Args:
        phenomenon: Phenomenon to explain
        findings: Related scientific findings
        domain: Research domain
    
    Returns:
        MechanisticExplanation
    

    Parameters: – phenomenon: Phenomenon – findings: Findings – domain: Domain

    Returns: MechanisticExplanation

    conduct_research_cycle(self, initial_observation, domain)

    Lines 823-884

    Purpose:

    Conduct a complete research cycle from observation to finding.
    
    Args:
        initial_observation: Starting observation
        domain: Research domain
    
    Returns:
        Dictionary with complete research cycle results
    

    Parameters: – initial_observation: Initial observation – domain: Domain

    Returns: <ast.Subscript object at 0x000001A06D767A90>


    Functions

    demonstrate_scientific_discovery_agent()

    Lines 887-1092

    Purpose:

    Demonstrate the scientific discovery agent capabilities
    

    Key Concepts Summary

    Pattern Overview

    The Scientific Discovery 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 076
    • Pattern 078

    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 Scientific Discovery 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: 077_scientific_discovery.py
    Total Lines: 1097
    Pattern Category: AI Agent Design Pattern #077


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Teaching Agent

    
        """
    Pattern 076: Teaching Agent
    
    Description:
        A Teaching Agent is a specialized AI agent designed to provide personalized
        tutoring and educational support across various subjects and skill levels. This
        pattern demonstrates how to build an intelligent tutoring system that can assess
        student knowledge, adapt teaching strategies, explain concepts at appropriate
        levels, generate practice problems, provide feedback, and track learning progress.
        
        The agent combines pedagogical principles with adaptive learning techniques to
        create effective, personalized learning experiences. It can diagnose knowledge
        gaps, scaffold learning, provide multiple explanations, encourage critical
        thinking, and adjust difficulty based on student performance.
    
    Components:
        1. Knowledge Assessor: Evaluates student understanding and identifies gaps
        2. Curriculum Planner: Creates personalized learning paths
        3. Concept Explainer: Explains topics at appropriate complexity levels
        4. Problem Generator: Creates practice problems and exercises
        5. Feedback Provider: Gives constructive, encouraging feedback
        6. Progress Tracker: Monitors learning progress and mastery
        7. Adaptive Engine: Adjusts difficulty and teaching approach
        8. Socratic Questioner: Guides discovery through questioning
    
    Key Features:
        - Multi-level explanations (beginner, intermediate, advanced)
        - Adaptive difficulty adjustment based on performance
        - Multiple teaching strategies (direct instruction, discovery, examples)
        - Personalized learning paths
        - Practice problem generation with solutions
        - Immediate, constructive feedback
        - Knowledge gap identification
        - Progress tracking and analytics
        - Socratic questioning for deeper understanding
        - Visual and conceptual explanations
    
    Use Cases:
        - K-12 tutoring across subjects
        - College-level subject tutoring
        - Professional skill development
        - Language learning
        - Programming education
        - Math and science tutoring
        - Test preparation (SAT, GRE, etc.)
        - Corporate training
        - Self-paced online courses
        - Homework help
    
    LangChain Implementation:
        Uses ChatOpenAI with specialized prompts for different pedagogical approaches,
        chains for assessment, explanation, and feedback generation.
    """
    
    import os
    import re
    from typing import List, Dict, Any, Optional, Tuple
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from collections import defaultdict
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class Subject(Enum):
        """Academic subjects"""
        MATHEMATICS = "mathematics"
        SCIENCE = "science"
        PROGRAMMING = "programming"
        LANGUAGE = "language"
        HISTORY = "history"
        LITERATURE = "literature"
        PHYSICS = "physics"
        CHEMISTRY = "chemistry"
        BIOLOGY = "biology"
        ECONOMICS = "economics"
    
    
    class SkillLevel(Enum):
        """Student skill levels"""
        BEGINNER = "beginner"
        INTERMEDIATE = "intermediate"
        ADVANCED = "advanced"
        EXPERT = "expert"
    
    
    class TeachingStrategy(Enum):
        """Teaching approaches"""
        DIRECT_INSTRUCTION = "direct_instruction"
        GUIDED_DISCOVERY = "guided_discovery"
        SOCRATIC_METHOD = "socratic_method"
        WORKED_EXAMPLES = "worked_examples"
        PROBLEM_BASED = "problem_based"
        SCAFFOLDED = "scaffolded"
    
    
    class QuestionDifficulty(Enum):
        """Difficulty levels for questions"""
        EASY = "easy"
        MEDIUM = "medium"
        HARD = "hard"
        CHALLENGING = "challenging"
    
    
    @dataclass
    class LearningObjective:
        """A specific learning objective"""
        objective_id: str
        subject: Subject
        topic: str
        description: str
        skill_level: SkillLevel
        prerequisites: List[str] = field(default_factory=list)
    
    
    @dataclass
    class StudentProfile:
        """Profile of a student"""
        student_id: str
        name: str
        skill_levels: Dict[Subject, SkillLevel] = field(default_factory=dict)
        learning_style: str = "visual"  # visual, auditory, kinesthetic
        strengths: List[str] = field(default_factory=list)
        weaknesses: List[str] = field(default_factory=list)
        goals: List[str] = field(default_factory=list)
    
    
    @dataclass
    class ConceptExplanation:
        """Explanation of a concept"""
        concept: str
        level: SkillLevel
        explanation: str
        examples: List[str]
        analogies: List[str]
        common_misconceptions: List[str]
        key_points: List[str]
    
    
    @dataclass
    class PracticeProblem:
        """A practice problem"""
        problem_id: str
        subject: Subject
        topic: str
        difficulty: QuestionDifficulty
        question: str
        solution: str
        hints: List[str]
        explanation: str
    
    
    @dataclass
    class StudentResponse:
        """Student's response to a problem"""
        problem_id: str
        student_answer: str
        is_correct: bool
        partial_credit: float  # 0.0-1.0
        time_taken: Optional[int] = None  # seconds
    
    
    @dataclass
    class Feedback:
        """Feedback on student work"""
        response_id: str
        correctness: str  # correct, partially_correct, incorrect
        positive_points: List[str]
        areas_to_improve: List[str]
        suggestions: List[str]
        encouragement: str
        next_steps: str
    
    
    @dataclass
    class LearningSession:
        """A tutoring session"""
        session_id: str
        student_id: str
        subject: Subject
        topic: str
        objectives: List[LearningObjective]
        explanations_given: List[ConceptExplanation] = field(default_factory=list)
        problems_attempted: List[Tuple[PracticeProblem, StudentResponse]] = field(default_factory=list)
        mastery_level: float = 0.0  # 0.0-1.0
        duration_minutes: int = 0
    
    
    class TeachingAgent:
        """
        Agent for personalized tutoring and education.
        
        This agent can assess student knowledge, explain concepts, generate
        practice problems, provide feedback, and adapt to student needs.
        """
        
        def __init__(self):
            """Initialize the teaching agent with specialized LLMs"""
            # Assessor for evaluating understanding
            self.assessor_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.1)
            
            # Explainer for teaching concepts
            self.explainer_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.4)
            
            # Problem generator for creating exercises
            self.generator_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.6)
            
            # Feedback provider for student responses
            self.feedback_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
            
            # Student profiles
            self.students: Dict[str, StudentProfile] = {}
            
            # Session history
            self.sessions: List[LearningSession] = []
        
        def assess_knowledge(
            self,
            student_id: str,
            subject: Subject,
            topic: str,
            student_response: str
        ) -> Dict[str, Any]:
            """
            Assess student's knowledge on a topic.
            
            Args:
                student_id: Student identifier
                subject: Subject area
                topic: Specific topic
                student_response: Student's explanation or answer
                
            Returns:
                Assessment with understanding level and gaps
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an educational assessment expert. Evaluate the
                student's understanding based on their response.
                
                Respond in this format:
                UNDERSTANDING_LEVEL: beginner/intermediate/advanced
                STRENGTHS: (one per line starting with -)
                GAPS: (one per line starting with -)
                MASTERY_SCORE: (0.0-1.0)
                RECOMMENDATIONS: (one per line starting with -)"""),
                ("user", """Subject: {subject}
    Topic: {topic}
    
    Student Response:
    {response}
    
    Assess the student's knowledge.""")
            ])
            
            chain = prompt | self.assessor_llm | StrOutputParser()
            
            try:
                result = chain.invoke({
                    "subject": subject.value,
                    "topic": topic,
                    "response": student_response
                })
                
                understanding_level = SkillLevel.INTERMEDIATE
                strengths = []
                gaps = []
                mastery_score = 0.5
                recommendations = []
                
                current_section = None
                
                for line in result.split('\n'):
                    line = line.strip()
                    if not line:
                        continue
                    
                    if line.startswith('UNDERSTANDING_LEVEL:'):
                        level_str = line.replace('UNDERSTANDING_LEVEL:', '').strip().lower()
                        if 'beginner' in level_str:
                            understanding_level = SkillLevel.BEGINNER
                        elif 'advanced' in level_str:
                            understanding_level = SkillLevel.ADVANCED
                        elif 'expert' in level_str:
                            understanding_level = SkillLevel.EXPERT
                        else:
                            understanding_level = SkillLevel.INTERMEDIATE
                    elif line.startswith('STRENGTHS:'):
                        current_section = 'strengths'
                    elif line.startswith('GAPS:'):
                        current_section = 'gaps'
                    elif line.startswith('MASTERY_SCORE:'):
                        try:
                            mastery_score = float(re.findall(r'[\d.]+', line)[0])
                        except:
                            pass
                    elif line.startswith('RECOMMENDATIONS:'):
                        current_section = 'recommendations'
                    elif line.startswith('-'):
                        content = line[1:].strip()
                        if current_section == 'strengths':
                            strengths.append(content)
                        elif current_section == 'gaps':
                            gaps.append(content)
                        elif current_section == 'recommendations':
                            recommendations.append(content)
                
                return {
                    "understanding_level": understanding_level,
                    "strengths": strengths if strengths else ["Shows effort"],
                    "gaps": gaps if gaps else ["Some concepts need clarification"],
                    "mastery_score": mastery_score,
                    "recommendations": recommendations if recommendations else ["Continue practicing"]
                }
                
            except Exception as e:
                return {
                    "understanding_level": SkillLevel.INTERMEDIATE,
                    "strengths": ["Assessment in progress"],
                    "gaps": ["Further evaluation needed"],
                    "mastery_score": 0.5,
                    "recommendations": ["Continue learning"]
                }
        
        def explain_concept(
            self,
            concept: str,
            subject: Subject,
            level: SkillLevel,
            teaching_strategy: TeachingStrategy = TeachingStrategy.DIRECT_INSTRUCTION
        ) -> ConceptExplanation:
            """
            Explain a concept at the appropriate level.
            
            Args:
                concept: Concept to explain
                subject: Subject area
                level: Student's skill level
                teaching_strategy: Teaching approach to use
                
            Returns:
                ConceptExplanation with multi-faceted explanation
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert teacher. Explain the concept clearly
                and effectively for the specified skill level using the teaching strategy.
                
                Respond in this format:
                EXPLANATION: Clear, level-appropriate explanation
                EXAMPLES: (one per line starting with -)
                ANALOGIES: (one per line starting with -)
                MISCONCEPTIONS: Common misunderstandings (one per line starting with -)
                KEY_POINTS: Essential takeaways (one per line starting with -)"""),
                ("user", """Concept: {concept}
    Subject: {subject}
    Skill Level: {level}
    Teaching Strategy: {strategy}
    
    Explain this concept.""")
            ])
            
            chain = prompt | self.explainer_llm | StrOutputParser()
            
            try:
                result = chain.invoke({
                    "concept": concept,
                    "subject": subject.value,
                    "level": level.value,
                    "strategy": teaching_strategy.value
                })
                
                explanation = ""
                examples = []
                analogies = []
                misconceptions = []
                key_points = []
                
                current_section = None
                explanation_lines = []
                
                for line in result.split('\n'):
                    line = line.strip()
                    if not line:
                        continue
                    
                    if line.startswith('EXPLANATION:'):
                        current_section = 'explanation'
                        exp_text = line.replace('EXPLANATION:', '').strip()
                        if exp_text:
                            explanation_lines.append(exp_text)
                    elif line.startswith('EXAMPLES:'):
                        current_section = 'examples'
                    elif line.startswith('ANALOGIES:'):
                        current_section = 'analogies'
                    elif line.startswith('MISCONCEPTIONS:'):
                        current_section = 'misconceptions'
                    elif line.startswith('KEY_POINTS:'):
                        current_section = 'key_points'
                    elif line.startswith('-'):
                        content = line[1:].strip()
                        if current_section == 'examples':
                            examples.append(content)
                        elif current_section == 'analogies':
                            analogies.append(content)
                        elif current_section == 'misconceptions':
                            misconceptions.append(content)
                        elif current_section == 'key_points':
                            key_points.append(content)
                    elif current_section == 'explanation':
                        explanation_lines.append(line)
                
                explanation = ' '.join(explanation_lines) if explanation_lines else f"Explanation of {concept}"
                
                return ConceptExplanation(
                    concept=concept,
                    level=level,
                    explanation=explanation,
                    examples=examples if examples else [f"Example related to {concept}"],
                    analogies=analogies if analogies else [f"Think of it like..."],
                    common_misconceptions=misconceptions if misconceptions else ["Avoid common mistakes"],
                    key_points=key_points if key_points else ["Main concept understood"]
                )
                
            except Exception as e:
                return ConceptExplanation(
                    concept=concept,
                    level=level,
                    explanation=f"Explanation of {concept} for {level.value} level",
                    examples=[f"Example of {concept}"],
                    analogies=["Conceptual analogy"],
                    common_misconceptions=["Common misunderstanding"],
                    key_points=["Key takeaway"]
                )
        
        def generate_practice_problem(
            self,
            subject: Subject,
            topic: str,
            difficulty: QuestionDifficulty,
            student_level: SkillLevel
        ) -> PracticeProblem:
            """
            Generate a practice problem.
            
            Args:
                subject: Subject area
                topic: Specific topic
                difficulty: Problem difficulty
                student_level: Student's skill level
                
            Returns:
                PracticeProblem with question and solution
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an educational content creator. Generate a practice
                problem appropriate for the student's level and difficulty.
                
                Respond in this format:
                QUESTION: The practice question
                SOLUTION: Step-by-step solution
                HINTS: Helpful hints (one per line starting with -)
                EXPLANATION: Why this solution works"""),
                ("user", """Subject: {subject}
    Topic: {topic}
    Difficulty: {difficulty}
    Student Level: {level}
    
    Generate a practice problem.""")
            ])
            
            chain = prompt | self.generator_llm | StrOutputParser()
            
            try:
                result = chain.invoke({
                    "subject": subject.value,
                    "topic": topic,
                    "difficulty": difficulty.value,
                    "level": student_level.value
                })
                
                question = ""
                solution = ""
                hints = []
                explanation = ""
                
                current_section = None
                question_lines = []
                solution_lines = []
                explanation_lines = []
                
                for line in result.split('\n'):
                    line = line.strip()
                    if not line:
                        continue
                    
                    if line.startswith('QUESTION:'):
                        current_section = 'question'
                        q_text = line.replace('QUESTION:', '').strip()
                        if q_text:
                            question_lines.append(q_text)
                    elif line.startswith('SOLUTION:'):
                        current_section = 'solution'
                        s_text = line.replace('SOLUTION:', '').strip()
                        if s_text:
                            solution_lines.append(s_text)
                    elif line.startswith('HINTS:'):
                        current_section = 'hints'
                    elif line.startswith('EXPLANATION:'):
                        current_section = 'explanation'
                        e_text = line.replace('EXPLANATION:', '').strip()
                        if e_text:
                            explanation_lines.append(e_text)
                    elif line.startswith('-'):
                        if current_section == 'hints':
                            hints.append(line[1:].strip())
                    elif current_section == 'question':
                        question_lines.append(line)
                    elif current_section == 'solution':
                        solution_lines.append(line)
                    elif current_section == 'explanation':
                        explanation_lines.append(line)
                
                question = ' '.join(question_lines) if question_lines else f"Practice problem about {topic}"
                solution = ' '.join(solution_lines) if solution_lines else "Solution provided"
                explanation = ' '.join(explanation_lines) if explanation_lines else "Explanation of solution"
                
                return PracticeProblem(
                    problem_id=f"prob_{datetime.now().timestamp()}",
                    subject=subject,
                    topic=topic,
                    difficulty=difficulty,
                    question=question,
                    solution=solution,
                    hints=hints if hints else ["Think about the key concepts"],
                    explanation=explanation
                )
                
            except Exception as e:
                return PracticeProblem(
                    problem_id=f"prob_{datetime.now().timestamp()}",
                    subject=subject,
                    topic=topic,
                    difficulty=difficulty,
                    question=f"Practice problem about {topic}",
                    solution="Solution to be provided",
                    hints=["Review the concept", "Break it down step by step"],
                    explanation="Explanation available"
                )
        
        def provide_feedback(
            self,
            problem: PracticeProblem,
            student_response: StudentResponse
        ) -> Feedback:
            """
            Provide feedback on student's work.
            
            Args:
                problem: The practice problem
                student_response: Student's response
                
            Returns:
                Feedback with constructive guidance
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a supportive tutor. Provide encouraging,
                constructive feedback on the student's work.
                
                Respond in this format:
                CORRECTNESS: correct/partially_correct/incorrect
                POSITIVE: What they did well (one per line starting with -)
                IMPROVE: Areas to improve (one per line starting with -)
                SUGGESTIONS: Specific suggestions (one per line starting with -)
                ENCOURAGEMENT: Encouraging message
                NEXT_STEPS: What to do next"""),
                ("user", """Problem: {question}
    Correct Solution: {solution}
    
    Student Answer: {answer}
    Is Correct: {is_correct}
    
    Provide feedback.""")
            ])
            
            chain = prompt | self.feedback_llm | StrOutputParser()
            
            try:
                result = chain.invoke({
                    "question": problem.question,
                    "solution": problem.solution,
                    "answer": student_response.student_answer,
                    "is_correct": student_response.is_correct
                })
                
                correctness = "partially_correct"
                positive = []
                improve = []
                suggestions = []
                encouragement = ""
                next_steps = ""
                
                current_section = None
                
                for line in result.split('\n'):
                    line = line.strip()
                    if not line:
                        continue
                    
                    if line.startswith('CORRECTNESS:'):
                        corr_text = line.replace('CORRECTNESS:', '').strip().lower()
                        if 'correct' in corr_text and 'partially' not in corr_text and 'incorrect' not in corr_text:
                            correctness = "correct"
                        elif 'incorrect' in corr_text:
                            correctness = "incorrect"
                        else:
                            correctness = "partially_correct"
                    elif line.startswith('POSITIVE:'):
                        current_section = 'positive'
                    elif line.startswith('IMPROVE:'):
                        current_section = 'improve'
                    elif line.startswith('SUGGESTIONS:'):
                        current_section = 'suggestions'
                    elif line.startswith('ENCOURAGEMENT:'):
                        current_section = 'encouragement'
                        encouragement = line.replace('ENCOURAGEMENT:', '').strip()
                    elif line.startswith('NEXT_STEPS:'):
                        current_section = 'next_steps'
                        next_steps = line.replace('NEXT_STEPS:', '').strip()
                    elif line.startswith('-'):
                        content = line[1:].strip()
                        if current_section == 'positive':
                            positive.append(content)
                        elif current_section == 'improve':
                            improve.append(content)
                        elif current_section == 'suggestions':
                            suggestions.append(content)
                    elif current_section == 'encouragement':
                        encouragement += " " + line
                    elif current_section == 'next_steps':
                        next_steps += " " + line
                
                return Feedback(
                    response_id=f"feedback_{datetime.now().timestamp()}",
                    correctness=correctness,
                    positive_points=positive if positive else ["Good effort!"],
                    areas_to_improve=improve if improve else ["Keep practicing"],
                    suggestions=suggestions if suggestions else ["Review the concept"],
                    encouragement=encouragement.strip() if encouragement else "Keep up the good work!",
                    next_steps=next_steps.strip() if next_steps else "Try another problem"
                )
                
            except Exception as e:
                return Feedback(
                    response_id=f"feedback_{datetime.now().timestamp()}",
                    correctness="partially_correct",
                    positive_points=["You're making progress"],
                    areas_to_improve=["Continue learning"],
                    suggestions=["Review the material"],
                    encouragement="Keep trying!",
                    next_steps="Practice more problems"
                )
        
        def create_learning_path(
            self,
            student_id: str,
            subject: Subject,
            goal: str,
            current_level: SkillLevel
        ) -> List[LearningObjective]:
            """
            Create a personalized learning path.
            
            Args:
                student_id: Student identifier
                subject: Subject area
                goal: Learning goal
                current_level: Current skill level
                
            Returns:
                List of LearningObjectives in sequence
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a curriculum designer. Create a learning path
                from the current level to achieve the goal.
                
                Provide 3-5 learning objectives, one per line:
                OBJECTIVE: topic | description | level"""),
                ("user", """Subject: {subject}
    Current Level: {current_level}
    Goal: {goal}
    
    Create a learning path.""")
            ])
            
            chain = prompt | self.explainer_llm | StrOutputParser()
            
            try:
                result = chain.invoke({
                    "subject": subject.value,
                    "current_level": current_level.value,
                    "goal": goal
                })
                
                objectives = []
                obj_counter = 1
                
                for line in result.split('\n'):
                    if 'OBJECTIVE:' in line or '|' in line:
                        parts = line.replace('OBJECTIVE:', '').split('|')
                        if len(parts) >= 2:
                            topic = parts[0].strip()
                            description = parts[1].strip()
                            level_str = parts[2].strip().lower() if len(parts) > 2 else current_level.value
                            
                            # Parse level
                            obj_level = current_level
                            if 'advanced' in level_str:
                                obj_level = SkillLevel.ADVANCED
                            elif 'intermediate' in level_str:
                                obj_level = SkillLevel.INTERMEDIATE
                            elif 'beginner' in level_str:
                                obj_level = SkillLevel.BEGINNER
                            
                            objectives.append(LearningObjective(
                                objective_id=f"obj_{obj_counter}",
                                subject=subject,
                                topic=topic,
                                description=description,
                                skill_level=obj_level,
                                prerequisites=[f"obj_{obj_counter-1}"] if obj_counter > 1 else []
                            ))
                            obj_counter += 1
                
                if not objectives:
                    # Fallback objectives
                    objectives = [
                        LearningObjective(
                            objective_id="obj_1",
                            subject=subject,
                            topic="Fundamentals",
                            description="Master fundamental concepts",
                            skill_level=current_level
                        ),
                        LearningObjective(
                            objective_id="obj_2",
                            subject=subject,
                            topic="Intermediate Topics",
                            description="Build on fundamentals",
                            skill_level=SkillLevel.INTERMEDIATE,
                            prerequisites=["obj_1"]
                        ),
                        LearningObjective(
                            objective_id="obj_3",
                            subject=subject,
                            topic="Advanced Applications",
                            description=f"Achieve goal: {goal}",
                            skill_level=SkillLevel.ADVANCED,
                            prerequisites=["obj_2"]
                        )
                    ]
                
                return objectives
                
            except Exception as e:
                return [
                    LearningObjective(
                        objective_id="obj_1",
                        subject=subject,
                        topic="Core Concepts",
                        description="Foundation building",
                        skill_level=current_level
                    )
                ]
        
        def conduct_tutoring_session(
            self,
            student_id: str,
            subject: Subject,
            topic: str,
            duration_minutes: int = 30
        ) -> LearningSession:
            """
            Conduct a complete tutoring session.
            
            Args:
                student_id: Student identifier
                subject: Subject to tutor
                topic: Specific topic
                duration_minutes: Session duration
                
            Returns:
                LearningSession with complete session data
            """
            print(f"\n📚 Starting tutoring session on {topic}...")
            
            # Create session
            session = LearningSession(
                session_id=f"session_{datetime.now().timestamp()}",
                student_id=student_id,
                subject=subject,
                topic=topic,
                objectives=[],
                duration_minutes=duration_minutes
            )
            
            # Get or create student profile
            if student_id not in self.students:
                self.students[student_id] = StudentProfile(
                    student_id=student_id,
                    name=f"Student {student_id}"
                )
            
            student = self.students[student_id]
            current_level = student.skill_levels.get(subject, SkillLevel.BEGINNER)
            
            # Step 1: Explain concept
            print("  ✓ Explaining concept...")
            explanation = self.explain_concept(
                topic,
                subject,
                current_level,
                TeachingStrategy.DIRECT_INSTRUCTION
            )
            session.explanations_given.append(explanation)
            
            # Step 2: Generate practice problems
            print("  ✓ Generating practice problems...")
            for difficulty in [QuestionDifficulty.EASY, QuestionDifficulty.MEDIUM]:
                problem = self.generate_practice_problem(
                    subject,
                    topic,
                    difficulty,
                    current_level
                )
                
                # Simulate student response
                is_correct = difficulty == QuestionDifficulty.EASY
                response = StudentResponse(
                    problem_id=problem.problem_id,
                    student_answer="Student's attempt at solution",
                    is_correct=is_correct,
                    partial_credit=0.8 if is_correct else 0.4
                )
                
                session.problems_attempted.append((problem, response))
            
            # Step 3: Calculate mastery
            if session.problems_attempted:
                total_credit = sum(resp.partial_credit for _, resp in session.problems_attempted)
                session.mastery_level = total_credit / len(session.problems_attempted)
            
            print("  ✅ Session complete!\n")
            
            self.sessions.append(session)
            return session
    
    
    def demonstrate_teaching_agent():
        """Demonstrate the teaching agent capabilities"""
        print("=" * 80)
        print("TEACHING AGENT DEMONSTRATION")
        print("=" * 80)
        
        agent = TeachingAgent()
        
        # Demo 1: Knowledge Assessment
        print("\n" + "=" * 80)
        print("DEMO 1: Knowledge Assessment")
        print("=" * 80)
        
        student_response = """
        Photosynthesis is the process where plants use sunlight to make food.
        They take in carbon dioxide and water, and produce glucose and oxygen.
        The process happens in the chloroplasts using chlorophyll.
        """
        
        print("\nAssessing student knowledge on photosynthesis...")
        assessment = agent.assess_knowledge(
            "student001",
            Subject.BIOLOGY,
            "Photosynthesis",
            student_response
        )
        
        print(f"\nAssessment Results:")
        print(f"Understanding Level: {assessment['understanding_level'].value}")
        print(f"Mastery Score: {assessment['mastery_score']:.2f}/1.0")
        print(f"\nStrengths:")
        for strength in assessment['strengths'][:3]:
            print(f"  ✓ {strength}")
        print(f"\nKnowledge Gaps:")
        for gap in assessment['gaps'][:2]:
            print(f"  ⚠ {gap}")
        print(f"\nRecommendations:")
        for rec in assessment['recommendations'][:2]:
            print(f"  → {rec}")
        
        # Demo 2: Concept Explanation
        print("\n" + "=" * 80)
        print("DEMO 2: Concept Explanation")
        print("=" * 80)
        
        concept = "quadratic equations"
        print(f"\nExplaining '{concept}' at intermediate level...")
        
        explanation = agent.explain_concept(
            concept,
            Subject.MATHEMATICS,
            SkillLevel.INTERMEDIATE,
            TeachingStrategy.WORKED_EXAMPLES
        )
        
        print(f"\nConcept: {explanation.concept}")
        print(f"Level: {explanation.level.value}")
        print(f"\nExplanation:")
        print(f"  {explanation.explanation[:300]}...")
        print(f"\nExamples:")
        for example in explanation.examples[:2]:
            print(f"  • {example}")
        print(f"\nKey Points:")
        for point in explanation.key_points[:2]:
            print(f"  ✓ {point}")
        
        # Demo 3: Practice Problem Generation
        print("\n" + "=" * 80)
        print("DEMO 3: Practice Problem Generation")
        print("=" * 80)
        
        print("\nGenerating practice problems...")
        
        for difficulty in [QuestionDifficulty.EASY, QuestionDifficulty.MEDIUM]:
            problem = agent.generate_practice_problem(
                Subject.MATHEMATICS,
                "quadratic equations",
                difficulty,
                SkillLevel.INTERMEDIATE
            )
            
            print(f"\n{difficulty.value.upper()} Problem:")
            print(f"Question: {problem.question[:150]}...")
            print(f"\nHints:")
            for hint in problem.hints[:2]:
                print(f"  💡 {hint}")
            print(f"\nSolution: {problem.solution[:150]}...")
        
        # Demo 4: Feedback on Student Work
        print("\n" + "=" * 80)
        print("DEMO 4: Feedback on Student Work")
        print("=" * 80)
        
        sample_problem = agent.generate_practice_problem(
            Subject.PROGRAMMING,
            "loops",
            QuestionDifficulty.EASY,
            SkillLevel.BEGINNER
        )
        
        student_answer = "I tried using a for loop but got confused about the syntax"
        response = StudentResponse(
            problem_id=sample_problem.problem_id,
            student_answer=student_answer,
            is_correct=False,
            partial_credit=0.3
        )
        
        print(f"\nProblem: {sample_problem.question[:100]}...")
        print(f"Student Answer: {student_answer}")
        
        print("\nGenerating feedback...")
        feedback = agent.provide_feedback(sample_problem, response)
        
        print(f"\nFeedback:")
        print(f"Correctness: {feedback.correctness}")
        print(f"\nWhat you did well:")
        for point in feedback.positive_points[:2]:
            print(f"  ✓ {point}")
        print(f"\nAreas to improve:")
        for area in feedback.areas_to_improve[:2]:
            print(f"  → {area}")
        print(f"\nEncouragement: {feedback.encouragement}")
        print(f"Next Steps: {feedback.next_steps}")
        
        # Demo 5: Personalized Learning Path
        print("\n" + "=" * 80)
        print("DEMO 5: Personalized Learning Path")
        print("=" * 80)
        
        print("\nCreating learning path for Python programming...")
        learning_path = agent.create_learning_path(
            "student001",
            Subject.PROGRAMMING,
            "Become proficient in Python web development",
            SkillLevel.BEGINNER
        )
        
        print(f"\nLearning Path ({len(learning_path)} objectives):")
        for i, obj in enumerate(learning_path, 1):
            print(f"\n{i}. {obj.topic} ({obj.skill_level.value})")
            print(f"   {obj.description}")
            if obj.prerequisites:
                print(f"   Prerequisites: {', '.join(obj.prerequisites)}")
        
        # Demo 6: Complete Tutoring Session
        print("\n" + "=" * 80)
        print("DEMO 6: Complete Tutoring Session")
        print("=" * 80)
        
        session = agent.conduct_tutoring_session(
            "student001",
            Subject.MATHEMATICS,
            "linear equations",
            duration_minutes=20
        )
        
        print(f"Session ID: {session.session_id}")
        print(f"Topic: {session.topic}")
        print(f"Duration: {session.duration_minutes} minutes")
        print(f"Mastery Level: {session.mastery_level:.2f}/1.0")
        
        print(f"\nConcepts Explained: {len(session.explanations_given)}")
        for exp in session.explanations_given:
            print(f"  • {exp.concept} ({exp.level.value})")
        
        print(f"\nProblems Attempted: {len(session.problems_attempted)}")
        for problem, response in session.problems_attempted:
            status = "✓" if response.is_correct else "✗"
            print(f"  {status} {problem.difficulty.value} - Score: {response.partial_credit:.1f}/1.0")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        
        summary = """
    The Teaching Agent demonstrates comprehensive educational capabilities:
    
    KEY CAPABILITIES:
    1. Knowledge Assessment: Evaluates student understanding and identifies gaps
    2. Adaptive Explanation: Explains concepts at appropriate complexity levels
    3. Problem Generation: Creates practice problems matched to student level
    4. Constructive Feedback: Provides encouraging, helpful feedback
    5. Learning Path Creation: Designs personalized curricula
    6. Progress Tracking: Monitors learning and mastery development
    7. Multi-Strategy Teaching: Adapts teaching approach to student needs
    
    BENEFITS:
    - Personalized learning at student's pace
    - Immediate, constructive feedback
    - Unlimited practice problems
    - Patient, consistent teaching
    - Tracks progress over time
    - Identifies and addresses knowledge gaps
    - Adapts difficulty automatically
    
    USE CASES:
    - K-12 tutoring across all subjects
    - College-level subject support
    - Professional skill development
    - Language learning
    - Programming education
    - Test preparation (SAT, ACT, GRE)
    - Homework help
    - Corporate training
    - Self-paced learning
    - Supplemental education
    
    PRODUCTION CONSIDERATIONS:
    1. Pedagogy: Implement proven teaching strategies and learning science
    2. Assessment: Robust knowledge testing and gap analysis
    3. Adaptation: Real-time difficulty adjustment based on performance
    4. Content Library: Comprehensive problem and explanation databases
    5. Progress Analytics: Detailed learning analytics and reporting
    6. Engagement: Gamification, rewards, and motivation systems
    7. Multi-Modal: Support text, images, videos, interactive content
    8. Collaboration: Enable teacher oversight and intervention
    9. Accessibility: Support diverse learning needs and disabilities
    10. Safety: Content filtering and age-appropriate material
    
    ADVANCED EXTENSIONS:
    - Socratic dialogue for deeper understanding
    - Visual learning with diagrams and animations
    - Spaced repetition for memory retention
    - Collaborative learning with peer interaction
    - Real-world problem applications
    - Project-based learning modules
    - Adaptive testing and placement
    - Learning style optimization
    - Emotional intelligence and encouragement
    - Multi-language support
    
    The agent provides scalable, personalized education that adapts to each
    student's needs, pace, and learning style while maintaining pedagogical
    effectiveness.
    """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_teaching_agent()
    
        

    Agentic Design Pattern: Teaching Agent

    Pattern 076: Teaching Agent – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 076: Teaching Agent

    Description: A Teaching Agent is a specialized AI agent designed to provide personalized tutoring and educational support across various subjects and skill levels. This pattern demonstrates how to build an intelligent tutoring system that can assess student knowledge, adapt teaching strategies, explain concepts at appropriate levels, generate practice problems, provide feedback, and track learning progress.

    The agent combines pedagogical principles with adaptive learning techniques to
    create effective, personalized learning experiences. It can diagnose knowledge
    gaps, scaffold learning, provide multiple explanations, encourage critical
    thinking, and adjust difficulty based on student performance.
    

    Components: 1. Knowledge Assessor: Evaluates student understanding and identifies gaps 2. Curriculum Planner: Creates personalized learning paths 3. Concept Explainer: Explains topics at appropriate complexity levels 4. Problem Generator: Creates practice problems and exercises 5. Feedback Provider: Gives constructive, encouraging feedback 6. Progress Tracker: Monitors learning progress and mastery 7. Adaptive Engine: Adjusts difficulty and teaching approach 8. Socratic Questioner: Guides discovery through questioning

    Key Features: – Multi-level explanations (beginner, intermediate, advanced) – Adaptive difficulty adjustment based on performance – Multiple teaching strategies (direct instruction, discovery, examples) – Personalized learning paths – Practice problem generation with solutions – Immediate, constructive feedback – Knowledge gap identification – Progress tracking and analytics – Socratic questioning for deeper understanding – Visual and conceptual explanations

    Use Cases: – K-12 tutoring across subjects – College-level subject tutoring – Professional skill development – Language learning – Programming education – Math and science tutoring – Test preparation (SAT, GRE, etc.) – Corporate training – Self-paced online courses – Homework help

    LangChain Implementation: Uses ChatOpenAI with specialized prompts for different pedagogical approaches, chains for assessment, explanation, and feedback generation.


    File Statistics

    • Total Lines: 1106
    • Classes: 12
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 076: Teaching Agent
    
    Description:
        A Teaching Agent is a specialized AI agent designed to provide personalized
        tutoring and educational support across various subjects and skill levels. This
        pattern demonstrates how to build an intelligent tutoring system that can assess
        student knowledge, adapt teaching strategies, explain concepts at appropriate
        levels, generate practice problems, provide feedback, and track learning progress.
    
        The agent combines pedagogical principles with adaptive learning techniques to
        create effective, personalized learning experiences. It can diagnose knowledge
        gaps, scaffold learning, provide multiple explanations, encourage critical
        thinking, and adjust difficulty based on student performance.
    
    Components:
        1. Knowledge Assessor: Evaluates student understanding and identifies gaps
        2. Curriculum Planner: Creates personalized learning paths
        3. Concept Explainer: Explains topics at appropriate complexity levels
        4. Problem Generator: Creates practice problems and exercises
        5. Feedback Provider: Gives constructive, encouraging feedback
        6. Progress Tracker: Monitors learning progress and mastery
        7. Adaptive Engine: Adjusts difficulty and teaching approach
        8. Socratic Questioner: Guides discovery through questioning
    
    Key Features:
        - Multi-level explanations (beginner, intermediate, advanced)
        - Adaptive difficulty adjustment based on performance
        - Multiple teaching strategies (direct instruction, discovery, examples)
        - Personalized learning paths
        - Practice problem generation with solutions
        - Immediate, constructive feedback
        - Knowledge gap identification
        - Progress tracking and analytics
        - Socratic questioning for deeper understanding
        - Visual and conceptual explanations
    
    Use Cases:
        - K-12 tutoring across subjects
        - College-level subject tutoring
        - Professional skill development
        - Language learning
        - Programming education
        - Math and science tutoring
        - Test preparation (SAT, GRE, etc.)
        - Corporate training
        - Self-paced online courses
        - Homework help
    
    LangChain Implementation:
        Uses ChatOpenAI with specialized prompts for different pedagogical approaches,
        chains for assessment, explanation, and feedback generation.
    """
    

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


    Import Statements

    Standard Library Imports

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

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

    Lines 71-82

    Inherits from: Enum

    Purpose:

    Academic subjects
    

    Class: SkillLevel

    Lines 85-90

    Inherits from: Enum

    Purpose:

    Student skill levels
    

    Class: TeachingStrategy

    Lines 93-100

    Inherits from: Enum

    Purpose:

    Teaching approaches
    

    Class: QuestionDifficulty

    Lines 103-108

    Inherits from: Enum

    Purpose:

    Difficulty levels for questions
    

    Class: LearningObjective

    Lines 112-119

    Purpose:

    A specific learning objective
    

    Class: StudentProfile

    Lines 123-131

    Purpose:

    Profile of a student
    

    Class: ConceptExplanation

    Lines 135-143

    Purpose:

    Explanation of a concept
    

    Class: PracticeProblem

    Lines 147-156

    Purpose:

    A practice problem
    

    Class: StudentResponse

    Lines 160-166

    Purpose:

    Student's response to a problem
    

    Class: Feedback

    Lines 170-178

    Purpose:

    Feedback on student work
    

    Class: LearningSession

    Lines 182-192

    Purpose:

    A tutoring session
    

    Class: TeachingAgent

    Lines 195-860

    Purpose:

    Agent for personalized tutoring and education.
    
    This agent can assess student knowledge, explain concepts, generate
    practice problems, provide feedback, and adapt to student needs.
    

    Methods:

    __init__(self)

    Lines 203-221

    Purpose:

    Initialize the teaching agent with specialized LLMs
    

    Parameters:

    assess_knowledge(self, student_id, subject, topic, student_response)

    Lines 223-328

    Purpose:

    Assess student's knowledge on a topic.
    
    Args:
        student_id: Student identifier
        subject: Subject area
        topic: Specific topic
        student_response: Student's explanation or answer
    
    Returns:
        Assessment with understanding level and gaps
    

    Parameters: – student_id: Student id – subject: Subject – topic: Topic – student_response: Student response

    Returns: <ast.Subscript object at 0x000001A06D83C110>

    explain_concept(self, concept, subject, level, teaching_strategy)

    Lines 330-438

    Purpose:

    Explain a concept at the appropriate level.
    
    Args:
        concept: Concept to explain
        subject: Subject area
        level: Student's skill level
        teaching_strategy: Teaching approach to use
    
    Returns:
        ConceptExplanation with multi-faceted explanation
    

    Parameters: – concept: Concept – subject: Subject – level: Level – teaching_strategy: Teaching strategy

    Returns: ConceptExplanation

    generate_practice_problem(self, subject, topic, difficulty, student_level)

    Lines 440-553

    Purpose:

    Generate a practice problem.
    
    Args:
        subject: Subject area
        topic: Specific topic
        difficulty: Problem difficulty
        student_level: Student's skill level
    
    Returns:
        PracticeProblem with question and solution
    

    Parameters: – subject: Subject – topic: Topic – difficulty: Difficulty – student_level: Student level

    Returns: PracticeProblem

    provide_feedback(self, problem, student_response)

    Lines 555-666

    Purpose:

    Provide feedback on student's work.
    
    Args:
        problem: The practice problem
        student_response: Student's response
    
    Returns:
        Feedback with constructive guidance
    

    Parameters: – problem: Problem – student_response: Student response

    Returns: Feedback

    create_learning_path(self, student_id, subject, goal, current_level)

    Lines 668-778

    Purpose:

    Create a personalized learning path.
    
    Args:
        student_id: Student identifier
        subject: Subject area
        goal: Learning goal
        current_level: Current skill level
    
    Returns:
        List of LearningObjectives in sequence
    

    Parameters: – student_id: Student id – subject: Subject – goal: Goal – current_level: Current level

    Returns: <ast.Subscript object at 0x000001A06D6B1C10>

    conduct_tutoring_session(self, student_id, subject, topic, duration_minutes)

    Lines 780-860

    Purpose:

    Conduct a complete tutoring session.
    
    Args:
        student_id: Student identifier
        subject: Subject to tutor
        topic: Specific topic
        duration_minutes: Session duration
    
    Returns:
        LearningSession with complete session data
    

    Parameters: – student_id: Student id – subject: Subject – topic: Topic – duration_minutes: Duration minutes

    Returns: LearningSession


    Functions

    demonstrate_teaching_agent()

    Lines 863-1101

    Purpose:

    Demonstrate the teaching agent capabilities
    

    Key Concepts Summary

    Pattern Overview

    The Teaching Agent 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 075
    • Pattern 077

    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 Teaching Agent 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: 076_teaching_agent.py
    Total Lines: 1106
    Pattern Category: AI Agent Design Pattern #076


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Creative Agent

    
        """
    Pattern 075: Creative Agent
    
    Description:
        A Creative Agent is a specialized AI agent designed to generate original creative
        content across various domains including writing (stories, poetry, scripts), visual
        concepts (art descriptions, design ideas), music concepts, and other creative works.
        This pattern demonstrates how to build an intelligent agent that can understand
        creative requirements, generate diverse creative outputs, adapt styles, incorporate
        feedback, and maintain creative consistency.
        
        The agent combines creative ideation with structured generation techniques to produce
        high-quality, original content. It can work across multiple creative domains, adapt
        to different styles and tones, generate variations, and iteratively refine outputs
        based on feedback.
    
    Components:
        1. Ideation Engine: Generates creative concepts and ideas
        2. Style Adapter: Adapts content to different styles and tones
        3. Content Generator: Creates structured creative outputs
        4. Variation Creator: Produces diverse alternatives
        5. Constraint Respector: Adheres to creative constraints
        6. Feedback Integrator: Incorporates critique and improvements
        7. Consistency Maintainer: Ensures coherent creative vision
        8. Multi-Modal Orchestrator: Coordinates different creative types
    
    Key Features:
        - Cross-domain creative generation (text, visual concepts, music ideas)
        - Style and tone adaptation (formal, casual, poetic, technical)
        - Character and world-building for narratives
        - Creative constraint handling (theme, length, format)
        - Iterative refinement with feedback
        - Variation generation for exploration
        - Originality assessment
        - Creative inspiration from prompts
        - Collaborative creativity support
        - Genre and format flexibility
    
    Use Cases:
        - Story and novel writing
        - Poetry and lyric generation
        - Screenplay and script writing
        - Marketing copy and slogans
        - Product naming and branding
        - Art concept development
        - Game narrative design
        - Content marketing
        - Creative brainstorming
        - Educational creative writing
    
    LangChain Implementation:
        Uses ChatOpenAI with varying temperatures for different creative tasks,
        specialized chains for ideation, generation, and refinement.
    """
    
    import os
    import re
    from typing import List, Dict, Any, Optional
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class CreativeType(Enum):
        """Types of creative content"""
        STORY = "story"
        POEM = "poem"
        SCRIPT = "script"
        SONG_LYRICS = "song_lyrics"
        ARTICLE = "article"
        MARKETING_COPY = "marketing_copy"
        PRODUCT_NAME = "product_name"
        SLOGAN = "slogan"
        DIALOGUE = "dialogue"
        WORLD_BUILDING = "world_building"
    
    
    class CreativeStyle(Enum):
        """Creative styles and tones"""
        FORMAL = "formal"
        CASUAL = "casual"
        POETIC = "poetic"
        HUMOROUS = "humorous"
        DRAMATIC = "dramatic"
        MINIMALIST = "minimalist"
        ELABORATE = "elaborate"
        MYSTERIOUS = "mysterious"
        ROMANTIC = "romantic"
        DARK = "dark"
    
    
    class Genre(Enum):
        """Creative genres"""
        FANTASY = "fantasy"
        SCIENCE_FICTION = "science_fiction"
        MYSTERY = "mystery"
        ROMANCE = "romance"
        HORROR = "horror"
        THRILLER = "thriller"
        COMEDY = "comedy"
        DRAMA = "drama"
        ADVENTURE = "adventure"
        HISTORICAL = "historical"
    
    
    @dataclass
    class CreativePrompt:
        """Input prompt for creative generation"""
        type: CreativeType
        theme: str
        style: CreativeStyle
        genre: Optional[Genre] = None
        constraints: Dict[str, Any] = field(default_factory=dict)
        inspiration: List[str] = field(default_factory=list)
        target_audience: Optional[str] = None
    
    
    @dataclass
    class Character:
        """Character for narrative works"""
        name: str
        role: str  # protagonist, antagonist, supporting
        personality: List[str]
        background: str
        goals: List[str]
        conflicts: List[str]
    
    
    @dataclass
    class Setting:
        """Setting for creative works"""
        location: str
        time_period: str
        atmosphere: str
        key_features: List[str]
        description: str
    
    
    @dataclass
    class CreativeWork:
        """Generated creative content"""
        work_id: str
        type: CreativeType
        title: str
        content: str
        style: CreativeStyle
        genre: Optional[Genre]
        characters: List[Character] = field(default_factory=list)
        setting: Optional[Setting] = None
        themes: List[str] = field(default_factory=list)
        word_count: int = 0
        metadata: Dict[str, Any] = field(default_factory=dict)
        created_at: datetime = field(default_factory=datetime.now)
    
    
    @dataclass
    class CreativeFeedback:
        """Feedback on creative work"""
        work_id: str
        strengths: List[str]
        weaknesses: List[str]
        suggestions: List[str]
        overall_score: float  # 0.0-1.0
        specific_notes: Dict[str, str] = field(default_factory=dict)
    
    
    @dataclass
    class CreativeVariation:
        """Variation of a creative work"""
        original_id: str
        variation_id: str
        content: str
        changes_made: List[str]
        variation_type: str  # tone, structure, perspective, etc.
    
    
    class CreativeAgent:
        """
        Agent for generating creative content across multiple domains.
        
        This agent can generate stories, poems, scripts, marketing copy, and other
        creative works while adapting to styles, genres, and constraints.
        """
        
        def __init__(self):
            """Initialize the creative agent with specialized LLMs"""
            # Ideation engine for brainstorming
            self.ideation_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.9)
            
            # Generator for main content creation
            self.generator_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.8)
            
            # Refiner for improving content
            self.refiner_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.6)
            
            # Critic for evaluation
            self.critic_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
            
            # Creative works repository
            self.works: Dict[str, CreativeWork] = {}
        
        def generate_ideas(
            self,
            theme: str,
            creative_type: CreativeType,
            count: int = 5
        ) -> List[str]:
            """
            Generate creative ideas based on theme.
            
            Args:
                theme: Central theme or concept
                creative_type: Type of creative work
                count: Number of ideas to generate
                
            Returns:
                List of creative ideas
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a creative ideation expert. Generate original and
                imaginative ideas based on the theme and content type.
                
                Provide {count} ideas, one per line starting with a number."""),
                ("user", """Theme: {theme}
    Content Type: {type}
    
    Generate {count} creative ideas.""")
            ])
            
            chain = prompt | self.ideation_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "theme": theme,
                    "type": creative_type.value,
                    "count": count
                })
                
                ideas = []
                for line in response.split('\n'):
                    line = line.strip()
                    if line and (line[0].isdigit() or line.startswith('-')):
                        # Remove numbering
                        idea = re.sub(r'^\d+[\.\)]\s*', '', line)
                        idea = re.sub(r'^[-*]\s*', '', idea)
                        if idea:
                            ideas.append(idea)
                
                return ideas[:count] if ideas else [f"Idea about {theme}" for _ in range(count)]
                
            except Exception as e:
                return [f"Creative idea {i+1} about {theme}" for i in range(count)]
        
        def create_characters(
            self,
            story_concept: str,
            character_count: int = 3
        ) -> List[Character]:
            """
            Create characters for a narrative work.
            
            Args:
                story_concept: Brief story concept
                character_count: Number of characters to create
                
            Returns:
                List of Character objects
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a character development expert. Create compelling
                characters for the story concept.
                
                For each character, provide:
                NAME: character name
                ROLE: protagonist/antagonist/supporting
                PERSONALITY: traits (comma-separated)
                BACKGROUND: brief background
                GOALS: goals (comma-separated)
                CONFLICTS: conflicts (comma-separated)
                ---"""),
                ("user", """Story Concept: {concept}
    
    Create {count} characters.""")
            ])
            
            chain = prompt | self.ideation_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "concept": story_concept,
                    "count": character_count
                })
                
                characters = []
                current_char = {}
                
                for line in response.split('\n'):
                    line = line.strip()
                    
                    if line == '---' and current_char:
                        # Create character from collected data
                        if 'NAME' in current_char:
                            characters.append(Character(
                                name=current_char.get('NAME', 'Unnamed'),
                                role=current_char.get('ROLE', 'supporting'),
                                personality=[p.strip() for p in current_char.get('PERSONALITY', '').split(',')],
                                background=current_char.get('BACKGROUND', ''),
                                goals=[g.strip() for g in current_char.get('GOALS', '').split(',')],
                                conflicts=[c.strip() for c in current_char.get('CONFLICTS', '').split(',')]
                            ))
                        current_char = {}
                    elif ':' in line:
                        parts = line.split(':', 1)
                        key = parts[0].strip().upper()
                        value = parts[1].strip()
                        current_char[key] = value
                
                # Add last character if exists
                if current_char and 'NAME' in current_char:
                    characters.append(Character(
                        name=current_char.get('NAME', 'Unnamed'),
                        role=current_char.get('ROLE', 'supporting'),
                        personality=[p.strip() for p in current_char.get('PERSONALITY', '').split(',')],
                        background=current_char.get('BACKGROUND', ''),
                        goals=[g.strip() for g in current_char.get('GOALS', '').split(',')],
                        conflicts=[c.strip() for c in current_char.get('CONFLICTS', '').split(',')]
                    ))
                
                # Fallback if parsing failed
                if not characters:
                    for i in range(character_count):
                        characters.append(Character(
                            name=f"Character {i+1}",
                            role="supporting" if i > 0 else "protagonist",
                            personality=["determined", "curious"],
                            background="To be developed",
                            goals=["Achieve their objective"],
                            conflicts=["Internal struggles"]
                        ))
                
                return characters[:character_count]
                
            except Exception as e:
                # Fallback characters
                return [
                    Character(
                        name=f"Character {i+1}",
                        role="protagonist" if i == 0 else "supporting",
                        personality=["brave", "intelligent"],
                        background="Mysterious past",
                        goals=["Complete their quest"],
                        conflicts=["Face their fears"]
                    )
                    for i in range(character_count)
                ]
        
        def create_setting(
            self,
            story_concept: str,
            genre: Genre
        ) -> Setting:
            """
            Create a setting for a narrative work.
            
            Args:
                story_concept: Brief story concept
                genre: Genre of the work
                
            Returns:
                Setting object
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a world-building expert. Create a vivid setting.
                
                Provide:
                LOCATION: Where the story takes place
                TIME: Time period
                ATMOSPHERE: Overall mood/feeling
                FEATURES: Key features (comma-separated)
                DESCRIPTION: Detailed description (2-3 sentences)"""),
                ("user", """Story Concept: {concept}
    Genre: {genre}
    
    Create a compelling setting.""")
            ])
            
            chain = prompt | self.ideation_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "concept": story_concept,
                    "genre": genre.value
                })
                
                setting_data = {}
                current_key = None
                desc_lines = []
                
                for line in response.split('\n'):
                    line = line.strip()
                    if ':' in line and line.split(':')[0].strip().upper() in ['LOCATION', 'TIME', 'ATMOSPHERE', 'FEATURES', 'DESCRIPTION']:
                        parts = line.split(':', 1)
                        key = parts[0].strip().upper()
                        value = parts[1].strip()
                        setting_data[key] = value
                        current_key = key
                    elif current_key == 'DESCRIPTION' and line:
                        desc_lines.append(line)
                
                if desc_lines:
                    setting_data['DESCRIPTION'] = ' '.join(desc_lines)
                
                return Setting(
                    location=setting_data.get('LOCATION', 'A mysterious place'),
                    time_period=setting_data.get('TIME', 'Present day'),
                    atmosphere=setting_data.get('ATMOSPHERE', 'Tense and mysterious'),
                    key_features=[f.strip() for f in setting_data.get('FEATURES', '').split(',')],
                    description=setting_data.get('DESCRIPTION', 'An intriguing setting awaits.')
                )
                
            except Exception as e:
                return Setting(
                    location="An intriguing location",
                    time_period="Modern times",
                    atmosphere="Mysterious and engaging",
                    key_features=["Unique architecture", "Hidden secrets"],
                    description="A place full of wonder and possibility."
                )
        
        def generate_content(
            self,
            creative_prompt: CreativePrompt,
            characters: Optional[List[Character]] = None,
            setting: Optional[Setting] = None
        ) -> CreativeWork:
            """
            Generate creative content based on prompt.
            
            Args:
                creative_prompt: Creative prompt with requirements
                characters: Optional characters for narrative works
                setting: Optional setting for narrative works
                
            Returns:
                CreativeWork with generated content
            """
            # Build context
            context_parts = []
            
            if characters:
                char_desc = "\n".join([
                    f"- {char.name} ({char.role}): {', '.join(char.personality[:2])}"
                    for char in characters
                ])
                context_parts.append(f"Characters:\n{char_desc}")
            
            if setting:
                context_parts.append(f"Setting: {setting.location}, {setting.time_period}")
                context_parts.append(f"Atmosphere: {setting.atmosphere}")
            
            context = "\n\n".join(context_parts) if context_parts else "No additional context"
            
            # Generate based on type
            if creative_prompt.type == CreativeType.STORY:
                return self._generate_story(creative_prompt, context, characters, setting)
            elif creative_prompt.type == CreativeType.POEM:
                return self._generate_poem(creative_prompt, context)
            elif creative_prompt.type == CreativeType.MARKETING_COPY:
                return self._generate_marketing_copy(creative_prompt)
            else:
                return self._generate_generic(creative_prompt, context)
        
        def _generate_story(
            self,
            prompt: CreativePrompt,
            context: str,
            characters: Optional[List[Character]],
            setting: Optional[Setting]
        ) -> CreativeWork:
            """Generate a story"""
            template = ChatPromptTemplate.from_messages([
                ("system", """You are a master storyteller. Write an engaging story that
                captures the theme, style, and genre specified.
                
                The story should have:
                - Compelling opening
                - Character development
                - Rising action
                - Climax
                - Resolution
                
                Write in the specified style and maintain consistency."""),
                ("user", """Theme: {theme}
    Style: {style}
    Genre: {genre}
    
    {context}
    
    Constraints: {constraints}
    
    Write the story.""")
            ])
            
            chain = template | self.generator_llm | StrOutputParser()
            
            try:
                content = chain.invoke({
                    "theme": prompt.theme,
                    "style": prompt.style.value,
                    "genre": prompt.genre.value if prompt.genre else "general",
                    "context": context,
                    "constraints": str(prompt.constraints) if prompt.constraints else "None"
                })
                
                # Generate title
                title = self._generate_title(content, prompt.type, prompt.theme)
                
                return CreativeWork(
                    work_id=f"work_{datetime.now().timestamp()}",
                    type=prompt.type,
                    title=title,
                    content=content,
                    style=prompt.style,
                    genre=prompt.genre,
                    characters=characters or [],
                    setting=setting,
                    themes=[prompt.theme],
                    word_count=len(content.split()),
                    metadata={"constraints": prompt.constraints}
                )
                
            except Exception as e:
                return self._fallback_work(prompt)
        
        def _generate_poem(
            self,
            prompt: CreativePrompt,
            context: str
        ) -> CreativeWork:
            """Generate a poem"""
            template = ChatPromptTemplate.from_messages([
                ("system", """You are an accomplished poet. Create a poem that explores
                the theme with creativity and emotional depth.
                
                Use vivid imagery, metaphor, and rhythm. Match the requested style."""),
                ("user", """Theme: {theme}
    Style: {style}
    
    {context}
    
    Write the poem.""")
            ])
            
            chain = template | self.generator_llm | StrOutputParser()
            
            try:
                content = chain.invoke({
                    "theme": prompt.theme,
                    "style": prompt.style.value,
                    "context": context
                })
                
                title = self._generate_title(content, prompt.type, prompt.theme)
                
                return CreativeWork(
                    work_id=f"work_{datetime.now().timestamp()}",
                    type=prompt.type,
                    title=title,
                    content=content,
                    style=prompt.style,
                    genre=prompt.genre,
                    themes=[prompt.theme],
                    word_count=len(content.split())
                )
                
            except Exception as e:
                return self._fallback_work(prompt)
        
        def _generate_marketing_copy(
            self,
            prompt: CreativePrompt
        ) -> CreativeWork:
            """Generate marketing copy"""
            template = ChatPromptTemplate.from_messages([
                ("system", """You are a creative marketing copywriter. Write compelling
                copy that engages the target audience and communicates the message clearly.
                
                Be persuasive, concise, and memorable."""),
                ("user", """Theme/Product: {theme}
    Style: {style}
    Target Audience: {audience}
    
    Constraints: {constraints}
    
    Write the marketing copy.""")
            ])
            
            chain = template | self.generator_llm | StrOutputParser()
            
            try:
                content = chain.invoke({
                    "theme": prompt.theme,
                    "style": prompt.style.value,
                    "audience": prompt.target_audience or "general audience",
                    "constraints": str(prompt.constraints) if prompt.constraints else "None"
                })
                
                title = f"Marketing Copy: {prompt.theme[:30]}"
                
                return CreativeWork(
                    work_id=f"work_{datetime.now().timestamp()}",
                    type=prompt.type,
                    title=title,
                    content=content,
                    style=prompt.style,
                    genre=None,
                    themes=[prompt.theme],
                    word_count=len(content.split())
                )
                
            except Exception as e:
                return self._fallback_work(prompt)
        
        def _generate_generic(
            self,
            prompt: CreativePrompt,
            context: str
        ) -> CreativeWork:
            """Generate generic creative content"""
            template = ChatPromptTemplate.from_messages([
                ("system", """You are a versatile creative writer. Generate content
                that matches the specified type, theme, and style."""),
                ("user", """Type: {type}
    Theme: {theme}
    Style: {style}
    
    {context}
    
    Generate the content.""")
            ])
            
            chain = template | self.generator_llm | StrOutputParser()
            
            try:
                content = chain.invoke({
                    "type": prompt.type.value,
                    "theme": prompt.theme,
                    "style": prompt.style.value,
                    "context": context
                })
                
                title = self._generate_title(content, prompt.type, prompt.theme)
                
                return CreativeWork(
                    work_id=f"work_{datetime.now().timestamp()}",
                    type=prompt.type,
                    title=title,
                    content=content,
                    style=prompt.style,
                    genre=prompt.genre,
                    themes=[prompt.theme],
                    word_count=len(content.split())
                )
                
            except Exception as e:
                return self._fallback_work(prompt)
        
        def _generate_title(
            self,
            content: str,
            content_type: CreativeType,
            theme: str
        ) -> str:
            """Generate a title for the work"""
            template = ChatPromptTemplate.from_messages([
                ("system", "Generate a creative, catchy title (5 words or less)."),
                ("user", "Content preview: {preview}\n\nGenerate title:")
            ])
            
            chain = template | self.refiner_llm | StrOutputParser()
            
            try:
                title = chain.invoke({"preview": content[:200]})
                return title.strip().strip('"\'')
            except:
                return f"{content_type.value.replace('_', ' ').title()}: {theme[:20]}"
        
        def _fallback_work(self, prompt: CreativePrompt) -> CreativeWork:
            """Create fallback work if generation fails"""
            return CreativeWork(
                work_id=f"work_{datetime.now().timestamp()}",
                type=prompt.type,
                title=f"{prompt.type.value.replace('_', ' ').title()}",
                content=f"A {prompt.style.value} {prompt.type.value} about {prompt.theme}.",
                style=prompt.style,
                genre=prompt.genre,
                themes=[prompt.theme],
                word_count=10
            )
        
        def critique_work(
            self,
            work: CreativeWork
        ) -> CreativeFeedback:
            """
            Provide critical feedback on creative work.
            
            Args:
                work: CreativeWork to critique
                
            Returns:
                CreativeFeedback with evaluation
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a creative writing critic. Provide constructive
                feedback on the work.
                
                Respond in this format:
                STRENGTHS: (one per line starting with -)
                WEAKNESSES: (one per line starting with -)
                SUGGESTIONS: (one per line starting with -)
                SCORE: (0.0-1.0)"""),
                ("user", """Title: {title}
    Type: {type}
    Style: {style}
    
    Content:
    {content}
    
    Provide feedback.""")
            ])
            
            chain = prompt | self.critic_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "title": work.title,
                    "type": work.type.value,
                    "style": work.style.value,
                    "content": work.content[:1000]  # Limit length
                })
                
                strengths = []
                weaknesses = []
                suggestions = []
                score = 0.7
                
                current_section = None
                
                for line in response.split('\n'):
                    line = line.strip()
                    if not line:
                        continue
                    
                    if line.startswith('STRENGTHS:'):
                        current_section = 'strengths'
                    elif line.startswith('WEAKNESSES:'):
                        current_section = 'weaknesses'
                    elif line.startswith('SUGGESTIONS:'):
                        current_section = 'suggestions'
                    elif line.startswith('SCORE:'):
                        try:
                            score = float(re.findall(r'[\d.]+', line)[0])
                        except:
                            pass
                    elif line.startswith('-'):
                        content = line[1:].strip()
                        if current_section == 'strengths':
                            strengths.append(content)
                        elif current_section == 'weaknesses':
                            weaknesses.append(content)
                        elif current_section == 'suggestions':
                            suggestions.append(content)
                
                return CreativeFeedback(
                    work_id=work.work_id,
                    strengths=strengths if strengths else ["Creative and engaging"],
                    weaknesses=weaknesses if weaknesses else ["Could be enhanced"],
                    suggestions=suggestions if suggestions else ["Consider adding more detail"],
                    overall_score=score
                )
                
            except Exception as e:
                return CreativeFeedback(
                    work_id=work.work_id,
                    strengths=["Shows creativity"],
                    weaknesses=["Needs refinement"],
                    suggestions=["Develop further"],
                    overall_score=0.7
                )
        
        def create_variation(
            self,
            work: CreativeWork,
            variation_type: str = "tone"
        ) -> CreativeVariation:
            """
            Create a variation of existing work.
            
            Args:
                work: Original work
                variation_type: Type of variation (tone, structure, perspective)
                
            Returns:
                CreativeVariation with modified content
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a creative editor. Create a variation of the work
                by modifying the {variation_type}.
                
                Maintain the core story/message but change the specified aspect."""),
                ("user", """Original Work:
    {content}
    
    Create a variation focusing on: {variation_type}""")
            ])
            
            chain = prompt | self.generator_llm | StrOutputParser()
            
            try:
                varied_content = chain.invoke({
                    "content": work.content,
                    "variation_type": variation_type
                })
                
                return CreativeVariation(
                    original_id=work.work_id,
                    variation_id=f"var_{datetime.now().timestamp()}",
                    content=varied_content,
                    changes_made=[f"Modified {variation_type}"],
                    variation_type=variation_type
                )
                
            except Exception as e:
                return CreativeVariation(
                    original_id=work.work_id,
                    variation_id=f"var_{datetime.now().timestamp()}",
                    content=work.content,
                    changes_made=["Minimal changes"],
                    variation_type=variation_type
                )
    
    
    def demonstrate_creative_agent():
        """Demonstrate the creative agent capabilities"""
        print("=" * 80)
        print("CREATIVE AGENT DEMONSTRATION")
        print("=" * 80)
        
        agent = CreativeAgent()
        
        # Demo 1: Story Generation
        print("\n" + "=" * 80)
        print("DEMO 1: Story Generation")
        print("=" * 80)
        
        print("\nGenerating story concept ideas...")
        ideas = agent.generate_ideas(
            theme="artificial intelligence gaining consciousness",
            creative_type=CreativeType.STORY,
            count=3
        )
        
        print("\nStory Ideas:")
        for i, idea in enumerate(ideas, 1):
            print(f"{i}. {idea}")
        
        print("\nCreating characters...")
        characters = agent.create_characters(
            story_concept=ideas[0],
            character_count=2
        )
        
        print("\nCharacters:")
        for char in characters:
            print(f"\n  {char.name} ({char.role})")
            print(f"  Personality: {', '.join(char.personality[:3])}")
            print(f"  Background: {char.background[:80]}...")
        
        print("\nCreating setting...")
        setting = agent.create_setting(
            story_concept=ideas[0],
            genre=Genre.SCIENCE_FICTION
        )
        
        print(f"\nSetting:")
        print(f"  Location: {setting.location}")
        print(f"  Time: {setting.time_period}")
        print(f"  Atmosphere: {setting.atmosphere}")
        print(f"  Description: {setting.description[:150]}...")
        
        print("\nGenerating story...")
        story_prompt = CreativePrompt(
            type=CreativeType.STORY,
            theme="AI consciousness and human connection",
            style=CreativeStyle.DRAMATIC,
            genre=Genre.SCIENCE_FICTION,
            constraints={"max_length": "short story"}
        )
        
        story = agent.generate_content(story_prompt, characters, setting)
        
        print(f"\nGenerated Story:")
        print(f"Title: {story.title}")
        print(f"Word Count: {story.word_count}")
        print(f"Style: {story.style.value}")
        print(f"\nContent Preview:")
        print(story.content[:400] + "...")
        
        # Demo 2: Poetry Generation
        print("\n" + "=" * 80)
        print("DEMO 2: Poetry Generation")
        print("=" * 80)
        
        poem_prompt = CreativePrompt(
            type=CreativeType.POEM,
            theme="the passage of time",
            style=CreativeStyle.POETIC,
            constraints={"form": "free verse"}
        )
        
        print(f"\nGenerating poem about '{poem_prompt.theme}'...")
        poem = agent.generate_content(poem_prompt)
        
        print(f"\nGenerated Poem:")
        print(f"Title: {poem.title}")
        print(f"Style: {poem.style.value}")
        print(f"\nContent:")
        print(poem.content)
        
        # Demo 3: Marketing Copy
        print("\n" + "=" * 80)
        print("DEMO 3: Marketing Copy Generation")
        print("=" * 80)
        
        marketing_prompt = CreativePrompt(
            type=CreativeType.MARKETING_COPY,
            theme="eco-friendly smart home device",
            style=CreativeStyle.CASUAL,
            target_audience="environmentally conscious millennials",
            constraints={"length": "short", "include": "call-to-action"}
        )
        
        print(f"\nGenerating marketing copy...")
        marketing = agent.generate_content(marketing_prompt)
        
        print(f"\nGenerated Marketing Copy:")
        print(f"Title: {marketing.title}")
        print(f"Target Audience: {marketing_prompt.target_audience}")
        print(f"\nContent:")
        print(marketing.content)
        
        # Demo 4: Creative Feedback
        print("\n" + "=" * 80)
        print("DEMO 4: Creative Feedback")
        print("=" * 80)
        
        print(f"\nGetting feedback on story: {story.title}")
        feedback = agent.critique_work(story)
        
        print(f"\nFeedback:")
        print(f"Overall Score: {feedback.overall_score:.2f}/1.0")
        print(f"\nStrengths:")
        for strength in feedback.strengths[:3]:
            print(f"  + {strength}")
        print(f"\nWeaknesses:")
        for weakness in feedback.weaknesses[:2]:
            print(f"  - {weakness}")
        print(f"\nSuggestions:")
        for suggestion in feedback.suggestions[:2]:
            print(f"  → {suggestion}")
        
        # Demo 5: Content Variation
        print("\n" + "=" * 80)
        print("DEMO 5: Content Variation")
        print("=" * 80)
        
        print(f"\nCreating variation of poem with different tone...")
        variation = agent.create_variation(poem, variation_type="tone")
        
        print(f"\nVariation Created:")
        print(f"Variation Type: {variation.variation_type}")
        print(f"Changes: {', '.join(variation.changes_made)}")
        print(f"\nVaried Content Preview:")
        print(variation.content[:300] + "...")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        
        summary = """
    The Creative Agent demonstrates comprehensive creative content generation:
    
    KEY CAPABILITIES:
    1. Ideation: Generates creative concepts and ideas from themes
    2. Character Development: Creates compelling characters with depth
    3. World-Building: Designs rich settings and atmospheres
    4. Content Generation: Produces stories, poems, marketing copy, and more
    5. Style Adaptation: Adjusts tone, style, and voice appropriately
    6. Critical Evaluation: Provides constructive feedback on works
    7. Variation Creation: Generates alternative versions of content
    
    BENEFITS:
    - Accelerates creative writing and content creation
    - Provides diverse creative options and variations
    - Maintains stylistic consistency across works
    - Offers constructive feedback for improvement
    - Adapts to different genres, styles, and formats
    - Supports brainstorming and ideation processes
    
    USE CASES:
    - Story and novel writing
    - Poetry and creative writing
    - Screenplay and script development
    - Marketing and advertising copy
    - Brand messaging and slogans
    - Content marketing
    - Game narrative design
    - Creative brainstorming sessions
    - Educational writing exercises
    - Literary exploration
    
    PRODUCTION CONSIDERATIONS:
    1. Content Quality: Implement multi-stage refinement for polish
    2. Originality: Add plagiarism detection and originality scoring
    3. Style Consistency: Maintain voice across longer works
    4. Genre Knowledge: Deep understanding of genre conventions
    5. Feedback Integration: Iterative improvement based on critique
    6. Collaboration: Support co-creation with human writers
    7. Version Control: Track revisions and variations
    8. Rights Management: Handle attribution and licensing
    9. Cultural Sensitivity: Ensure appropriate content for audiences
    10. Performance: Optimize for longer form content generation
    
    ADVANCED EXTENSIONS:
    - Multi-chapter novel generation
    - Interactive storytelling with branching narratives
    - Character arc tracking across long works
    - World consistency maintenance
    - Dialogue generation with character voices
    - Plot structure analysis and optimization
    - Genre blending and hybrid works
    - Collaborative writing with multiple agents
    - Real-time creative writing assistance
    - Personalized content based on reader preferences
    
    The agent transforms creative writing from a solitary struggle into an
    assisted, iterative process with AI as a creative partner.
    """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_creative_agent()
    
        

    Agentic Design Pattern: Creative Agent

    Pattern 075: Creative Agent – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 075: Creative Agent

    Description: A Creative Agent is a specialized AI agent designed to generate original creative content across various domains including writing (stories, poetry, scripts), visual concepts (art descriptions, design ideas), music concepts, and other creative works. This pattern demonstrates how to build an intelligent agent that can understand creative requirements, generate diverse creative outputs, adapt styles, incorporate feedback, and maintain creative consistency.

    The agent combines creative ideation with structured generation techniques to produce
    high-quality, original content. It can work across multiple creative domains, adapt
    to different styles and tones, generate variations, and iteratively refine outputs
    based on feedback.
    

    Components: 1. Ideation Engine: Generates creative concepts and ideas 2. Style Adapter: Adapts content to different styles and tones 3. Content Generator: Creates structured creative outputs 4. Variation Creator: Produces diverse alternatives 5. Constraint Respector: Adheres to creative constraints 6. Feedback Integrator: Incorporates critique and improvements 7. Consistency Maintainer: Ensures coherent creative vision 8. Multi-Modal Orchestrator: Coordinates different creative types

    Key Features: – Cross-domain creative generation (text, visual concepts, music ideas) – Style and tone adaptation (formal, casual, poetic, technical) – Character and world-building for narratives – Creative constraint handling (theme, length, format) – Iterative refinement with feedback – Variation generation for exploration – Originality assessment – Creative inspiration from prompts – Collaborative creativity support – Genre and format flexibility

    Use Cases: – Story and novel writing – Poetry and lyric generation – Screenplay and script writing – Marketing copy and slogans – Product naming and branding – Art concept development – Game narrative design – Content marketing – Creative brainstorming – Educational creative writing

    LangChain Implementation: Uses ChatOpenAI with varying temperatures for different creative tasks, specialized chains for ideation, generation, and refinement.


    File Statistics

    • Total Lines: 1064
    • Classes: 10
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 075: Creative Agent
    
    Description:
        A Creative Agent is a specialized AI agent designed to generate original creative
        content across various domains including writing (stories, poetry, scripts), visual
        concepts (art descriptions, design ideas), music concepts, and other creative works.
        This pattern demonstrates how to build an intelligent agent that can understand
        creative requirements, generate diverse creative outputs, adapt styles, incorporate
        feedback, and maintain creative consistency.
    
        The agent combines creative ideation with structured generation techniques to produce
        high-quality, original content. It can work across multiple creative domains, adapt
        to different styles and tones, generate variations, and iteratively refine outputs
        based on feedback.
    
    Components:
        1. Ideation Engine: Generates creative concepts and ideas
        2. Style Adapter: Adapts content to different styles and tones
        3. Content Generator: Creates structured creative outputs
        4. Variation Creator: Produces diverse alternatives
        5. Constraint Respector: Adheres to creative constraints
        6. Feedback Integrator: Incorporates critique and improvements
        7. Consistency Maintainer: Ensures coherent creative vision
        8. Multi-Modal Orchestrator: Coordinates different creative types
    
    Key Features:
        - Cross-domain creative generation (text, visual concepts, music ideas)
        - Style and tone adaptation (formal, casual, poetic, technical)
        - Character and world-building for narratives
        - Creative constraint handling (theme, length, format)
        - Iterative refinement with feedback
        - Variation generation for exploration
        - Originality assessment
        - Creative inspiration from prompts
        - Collaborative creativity support
        - Genre and format flexibility
    
    Use Cases:
        - Story and novel writing
        - Poetry and lyric generation
        - Screenplay and script writing
        - Marketing copy and slogans
        - Product naming and branding
        - Art concept development
        - Game narrative design
        - Content marketing
        - Creative brainstorming
        - Educational creative writing
    
    LangChain Implementation:
        Uses ChatOpenAI with varying temperatures for different creative tasks,
        specialized chains for ideation, generation, and refinement.
    """
    

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


    Import Statements

    Standard Library Imports

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

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – enum.Enum: Enumeration support – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – re: Regular expression operations – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.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: CreativeType

    Lines 71-82

    Inherits from: Enum

    Purpose:

    Types of creative content
    

    Class: CreativeStyle

    Lines 85-96

    Inherits from: Enum

    Purpose:

    Creative styles and tones
    

    Class: Genre

    Lines 99-110

    Inherits from: Enum

    Purpose:

    Creative genres
    

    Class: CreativePrompt

    Lines 114-122

    Purpose:

    Input prompt for creative generation
    

    Class: Character

    Lines 126-133

    Purpose:

    Character for narrative works
    

    Class: Setting

    Lines 137-143

    Purpose:

    Setting for creative works
    

    Class: CreativeWork

    Lines 147-160

    Purpose:

    Generated creative content
    

    Class: CreativeFeedback

    Lines 164-171

    Purpose:

    Feedback on creative work
    

    Class: CreativeVariation

    Lines 175-181

    Purpose:

    Variation of a creative work
    

    Class: CreativeAgent

    Lines 184-848

    Purpose:

    Agent for generating creative content across multiple domains.
    
    This agent can generate stories, poems, scripts, marketing copy, and other
    creative works while adapting to styles, genres, and constraints.
    

    Methods:

    __init__(self)

    Lines 192-207

    Purpose:

    Initialize the creative agent with specialized LLMs
    

    Parameters:

    generate_ideas(self, theme, creative_type, count)

    Lines 209-259

    Purpose:

    Generate creative ideas based on theme.
    
    Args:
        theme: Central theme or concept
        creative_type: Type of creative work
        count: Number of ideas to generate
    
    Returns:
        List of creative ideas
    

    Parameters: – theme: Theme – creative_type: Creative type – count: Count

    Returns: <ast.Subscript object at 0x000001A06D64BD10>

    create_characters(self, story_concept, character_count)

    Lines 261-362

    Purpose:

    Create characters for a narrative work.
    
    Args:
        story_concept: Brief story concept
        character_count: Number of characters to create
    
    Returns:
        List of Character objects
    

    Parameters: – story_concept: Story concept – character_count: Character count

    Returns: <ast.Subscript object at 0x000001A06D66B810>

    create_setting(self, story_concept, genre)

    Lines 364-435

    Purpose:

    Create a setting for a narrative work.
    
    Args:
        story_concept: Brief story concept
        genre: Genre of the work
    
    Returns:
        Setting object
    

    Parameters: – story_concept: Story concept – genre: Genre

    Returns: Setting

    generate_content(self, creative_prompt, characters, setting)

    Lines 437-478

    Purpose:

    Generate creative content based on prompt.
    
    Args:
        creative_prompt: Creative prompt with requirements
        characters: Optional characters for narrative works
        setting: Optional setting for narrative works
    
    Returns:
        CreativeWork with generated content
    

    Parameters: – creative_prompt: Prompt template or text – characters: Characters – setting: Setting

    Returns: CreativeWork

    _generate_story(self, prompt, context, characters, setting)

    Lines 480-540

    Purpose:

    Generate a story
    

    Parameters: – prompt: Prompt template or text – context: Context – characters: Characters – setting: Setting

    Returns: CreativeWork

    _generate_poem(self, prompt, context)

    Lines 542-584

    Purpose:

    Generate a poem
    

    Parameters: – prompt: Prompt template or text – context: Context

    Returns: CreativeWork

    _generate_marketing_copy(self, prompt)

    Lines 586-629

    Purpose:

    Generate marketing copy
    

    Parameters: – prompt: Prompt template or text

    Returns: CreativeWork

    _generate_generic(self, prompt, context)

    Lines 631-673

    Purpose:

    Generate generic creative content
    

    Parameters: – prompt: Prompt template or text – context: Context

    Returns: CreativeWork

    _generate_title(self, content, content_type, theme)

    Lines 675-693

    Purpose:

    Generate a title for the work
    

    Parameters: – content: Content – content_type: Content type – theme: Theme

    Returns: str

    _fallback_work(self, prompt)

    Lines 695-706

    Purpose:

    Create fallback work if generation fails
    

    Parameters: – prompt: Prompt template or text

    Returns: CreativeWork

    critique_work(self, work)

    Lines 708-797

    Purpose:

    Provide critical feedback on creative work.
    
    Args:
        work: CreativeWork to critique
    
    Returns:
        CreativeFeedback with evaluation
    

    Parameters: – work: Work

    Returns: CreativeFeedback

    create_variation(self, work, variation_type)

    Lines 799-848

    Purpose:

    Create a variation of existing work.
    
    Args:
        work: Original work
        variation_type: Type of variation (tone, structure, perspective)
    
    Returns:
        CreativeVariation with modified content
    

    Parameters: – work: Work – variation_type: Variation type

    Returns: CreativeVariation


    Functions

    demonstrate_creative_agent()

    Lines 851-1059

    Purpose:

    Demonstrate the creative agent capabilities
    

    Key Concepts Summary

    Pattern Overview

    The Creative Agent 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 074
    • Pattern 076

    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 Creative Agent 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: 075_creative_agent.py
    Total Lines: 1064
    Pattern Category: AI Agent Design Pattern #075


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Research Agent

    
        """
    Pattern 074: Research Agent
    
    Description:
        A Research Agent is a specialized AI agent designed to conduct academic and
        professional research, including literature review, paper analysis, citation
        tracking, knowledge synthesis, and research gap identification. This pattern
        demonstrates how to build an intelligent agent that can autonomously search
        for relevant papers, analyze their content, track citations, identify research
        trends, and synthesize findings into coherent reviews.
        
        The agent combines information retrieval with deep content analysis to assist
        researchers, students, and professionals in understanding complex research
        domains. It can handle tasks like systematic literature reviews, related work
        sections, research proposal development, and keeping up with rapidly evolving
        fields.
    
    Components:
        1. Paper Discovery: Searches academic databases and repositories
        2. Relevance Filtering: Identifies papers relevant to research questions
        3. Content Analyzer: Extracts key information from papers
        4. Citation Network: Builds and analyzes citation graphs
        5. Trend Analyzer: Identifies research trends and emerging topics
        6. Gap Identifier: Finds unexplored areas and opportunities
        7. Synthesis Engine: Combines findings into coherent narratives
        8. Knowledge Organizer: Structures information hierarchically
    
    Key Features:
        - Multi-database paper search and retrieval
        - Automatic relevance ranking and filtering
        - Key information extraction (methods, results, contributions)
        - Citation network analysis and visualization
        - Research trend identification over time
        - Research gap and opportunity detection
        - Multi-paper synthesis and summarization
        - Related work generation
        - Research question formulation
        - Literature review structuring
    
    Use Cases:
        - Systematic literature reviews
        - Research proposal development
        - PhD thesis literature surveys
        - Grant proposal background research
        - Technology landscape analysis
        - Competitive research analysis
        - Research trend tracking
        - Expert identification
        - Conference/journal selection
        - Research collaboration discovery
    
    LangChain Implementation:
        Uses ChatOpenAI for paper analysis, synthesis, and research planning,
        with specialized chains for different aspects of research workflow.
    """
    
    import os
    import re
    from typing import List, Dict, Any, Optional, Set, Tuple
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from collections import defaultdict
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class ResearchField(Enum):
        """Academic research fields"""
        COMPUTER_SCIENCE = "computer_science"
        MACHINE_LEARNING = "machine_learning"
        NATURAL_LANGUAGE_PROCESSING = "nlp"
        COMPUTER_VISION = "computer_vision"
        ROBOTICS = "robotics"
        BIOINFORMATICS = "bioinformatics"
        PHYSICS = "physics"
        CHEMISTRY = "chemistry"
        BIOLOGY = "biology"
        MEDICINE = "medicine"
        SOCIAL_SCIENCE = "social_science"
        ENGINEERING = "engineering"
    
    
    class PaperType(Enum):
        """Types of research papers"""
        JOURNAL_ARTICLE = "journal_article"
        CONFERENCE_PAPER = "conference_paper"
        WORKSHOP_PAPER = "workshop_paper"
        THESIS = "thesis"
        TECHNICAL_REPORT = "technical_report"
        PREPRINT = "preprint"
        REVIEW = "review"
        SURVEY = "survey"
    
    
    class ResearchPhase(Enum):
        """Phases of research"""
        EXPLORATION = "exploration"
        FOCUSED_REVIEW = "focused_review"
        GAP_ANALYSIS = "gap_analysis"
        SYNTHESIS = "synthesis"
        PROPOSAL = "proposal"
    
    
    @dataclass
    class ResearchPaper:
        """Represents a research paper"""
        paper_id: str
        title: str
        authors: List[str]
        year: int
        venue: str
        paper_type: PaperType
        abstract: str
        keywords: List[str] = field(default_factory=list)
        citations: int = 0
        references: List[str] = field(default_factory=list)
        url: Optional[str] = None
    
    
    @dataclass
    class PaperAnalysis:
        """Analysis of a research paper"""
        paper_id: str
        research_question: str
        methodology: str
        key_findings: List[str]
        contributions: List[str]
        limitations: List[str]
        future_work: List[str]
        relevance_score: float
    
    
    @dataclass
    class CitationRelationship:
        """Citation relationship between papers"""
        citing_paper: str
        cited_paper: str
        context: str
        relationship_type: str  # extends, contradicts, uses, etc.
    
    
    @dataclass
    class ResearchTrend:
        """Identified research trend"""
        topic: str
        keywords: List[str]
        paper_count: int
        growth_rate: float
        key_papers: List[str]
        timespan: Tuple[int, int]
        description: str
    
    
    @dataclass
    class ResearchGap:
        """Identified research gap"""
        gap_description: str
        related_areas: List[str]
        potential_approaches: List[str]
        difficulty: str  # easy, medium, hard
        impact: str  # low, medium, high
        supporting_evidence: List[str]
    
    
    @dataclass
    class LiteratureReview:
        """Complete literature review"""
        research_question: str
        papers_reviewed: int
        key_themes: List[str]
        major_findings: List[str]
        trends: List[ResearchTrend]
        gaps: List[ResearchGap]
        synthesis: str
        recommendations: List[str]
    
    
    class ResearchAgent:
        """
        Agent for conducting academic and professional research.
        
        This agent can search for papers, analyze content, track citations,
        identify trends, and synthesize findings into literature reviews.
        """
        
        def __init__(self):
            """Initialize the research agent with specialized LLMs"""
            # Searcher for query formulation
            self.searcher_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.2)
            
            # Analyzer for paper analysis
            self.analyzer_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.1)
            
            # Trend analyzer for pattern identification
            self.trend_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
            
            # Synthesizer for literature review
            self.synthesizer_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.4)
            
            # Simulated paper database (in production, would query real APIs)
            self.paper_database: Dict[str, ResearchPaper] = {}
            self._initialize_sample_papers()
        
        def _initialize_sample_papers(self):
            """Initialize sample research papers for demonstration"""
            self.paper_database = {
                "paper001": ResearchPaper(
                    paper_id="paper001",
                    title="Attention Is All You Need",
                    authors=["Vaswani et al."],
                    year=2017,
                    venue="NeurIPS",
                    paper_type=PaperType.CONFERENCE_PAPER,
                    abstract="We propose the Transformer, a novel architecture based solely on attention mechanisms, dispensing with recurrence and convolutions entirely.",
                    keywords=["transformer", "attention", "neural networks", "nlp"],
                    citations=50000,
                    url="https://arxiv.org/abs/1706.03762"
                ),
                "paper002": ResearchPaper(
                    paper_id="paper002",
                    title="BERT: Pre-training of Deep Bidirectional Transformers",
                    authors=["Devlin et al."],
                    year=2019,
                    venue="NAACL",
                    paper_type=PaperType.CONFERENCE_PAPER,
                    abstract="We introduce BERT, designed to pre-train deep bidirectional representations by jointly conditioning on both left and right context.",
                    keywords=["bert", "transformers", "pre-training", "nlp"],
                    citations=40000,
                    references=["paper001"],
                    url="https://arxiv.org/abs/1810.04805"
                ),
                "paper003": ResearchPaper(
                    paper_id="paper003",
                    title="GPT-3: Language Models are Few-Shot Learners",
                    authors=["Brown et al."],
                    year=2020,
                    venue="NeurIPS",
                    paper_type=PaperType.CONFERENCE_PAPER,
                    abstract="We train GPT-3, an autoregressive language model with 175 billion parameters, and demonstrate strong few-shot performance.",
                    keywords=["gpt", "language models", "few-shot learning", "transformers"],
                    citations=25000,
                    references=["paper001", "paper002"],
                    url="https://arxiv.org/abs/2005.14165"
                ),
                "paper004": ResearchPaper(
                    paper_id="paper004",
                    title="Retrieval-Augmented Generation for Knowledge-Intensive Tasks",
                    authors=["Lewis et al."],
                    year=2020,
                    venue="NeurIPS",
                    paper_type=PaperType.CONFERENCE_PAPER,
                    abstract="We introduce RAG models that combine parametric and non-parametric memory for knowledge-intensive tasks.",
                    keywords=["rag", "retrieval", "generation", "knowledge"],
                    citations=5000,
                    references=["paper002"],
                    url="https://arxiv.org/abs/2005.11401"
                ),
                "paper005": ResearchPaper(
                    paper_id="paper005",
                    title="Chain-of-Thought Prompting Elicits Reasoning in Large Language Models",
                    authors=["Wei et al."],
                    year=2022,
                    venue="NeurIPS",
                    paper_type=PaperType.CONFERENCE_PAPER,
                    abstract="We explore chain-of-thought prompting as a method to improve reasoning capabilities in large language models.",
                    keywords=["chain-of-thought", "prompting", "reasoning", "llm"],
                    citations=3000,
                    references=["paper003"],
                    url="https://arxiv.org/abs/2201.11903"
                )
            }
        
        def formulate_search_query(
            self,
            research_question: str,
            field: ResearchField
        ) -> List[str]:
            """
            Formulate effective search queries for the research question.
            
            Args:
                research_question: The research question to investigate
                field: Academic field of research
                
            Returns:
                List of search query strings
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a research librarian expert. Generate effective
                search queries to find relevant academic papers.
                
                Respond with 3-5 search queries, one per line."""),
                ("user", """Research Question: {question}
    Field: {field}
    
    Generate search queries to find relevant papers.""")
            ])
            
            chain = prompt | self.searcher_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "question": research_question,
                    "field": field.value
                })
                
                queries = []
                for line in response.split('\n'):
                    line = line.strip()
                    if line and not line.startswith('#'):
                        # Remove numbering if present
                        line = re.sub(r'^\d+[\.\)]\s*', '', line)
                        line = re.sub(r'^[-*]\s*', '', line)
                        if line:
                            queries.append(line)
                
                return queries if queries else [research_question]
                
            except Exception as e:
                # Fallback: use research question directly
                return [research_question]
        
        def search_papers(
            self,
            query: str,
            max_results: int = 10
        ) -> List[ResearchPaper]:
            """
            Search for papers matching the query.
            
            Args:
                query: Search query
                max_results: Maximum number of results
                
            Returns:
                List of matching papers
            """
            # In production, would query real APIs (Semantic Scholar, ArXiv, etc.)
            # For demo, do simple keyword matching
            query_lower = query.lower()
            query_keywords = set(query_lower.split())
            
            scored_papers = []
            
            for paper in self.paper_database.values():
                score = 0.0
                
                # Title matching
                title_lower = paper.title.lower()
                for keyword in query_keywords:
                    if keyword in title_lower:
                        score += 2.0
                
                # Abstract matching
                abstract_lower = paper.abstract.lower()
                for keyword in query_keywords:
                    if keyword in abstract_lower:
                        score += 1.0
                
                # Keyword matching
                for paper_kw in paper.keywords:
                    for query_kw in query_keywords:
                        if query_kw in paper_kw.lower():
                            score += 1.5
                
                # Citation boost
                score += min(paper.citations / 10000, 2.0)
                
                if score > 0:
                    scored_papers.append((paper, score))
            
            # Sort by score and return top results
            scored_papers.sort(key=lambda x: x[1], reverse=True)
            return [paper for paper, score in scored_papers[:max_results]]
        
        def analyze_paper(
            self,
            paper: ResearchPaper,
            research_context: str
        ) -> PaperAnalysis:
            """
            Analyze a research paper in context of research question.
            
            Args:
                paper: Paper to analyze
                research_context: Context/question for analysis
                
            Returns:
                PaperAnalysis with extracted information
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a research paper analyst. Analyze the paper and
                extract key information.
                
                Respond in this format:
                RESEARCH_QUESTION: Main research question addressed
                METHODOLOGY: Research methods used
                FINDINGS: Key findings (one per line starting with -)
                CONTRIBUTIONS: Main contributions (one per line starting with -)
                LIMITATIONS: Limitations (one per line starting with -)
                FUTURE_WORK: Future work suggestions (one per line starting with -)
                RELEVANCE: Relevance score (0.0-1.0)"""),
                ("user", """Title: {title}
    Authors: {authors}
    Year: {year}
    Abstract: {abstract}
    
    Research Context: {context}
    
    Analyze this paper.""")
            ])
            
            chain = prompt | self.analyzer_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "title": paper.title,
                    "authors": ", ".join(paper.authors),
                    "year": paper.year,
                    "abstract": paper.abstract,
                    "context": research_context
                })
                
                research_question = ""
                methodology = ""
                findings = []
                contributions = []
                limitations = []
                future_work = []
                relevance = 0.7
                
                current_section = None
                
                for line in response.split('\n'):
                    line = line.strip()
                    if not line:
                        continue
                    
                    if line.startswith('RESEARCH_QUESTION:'):
                        research_question = line.replace('RESEARCH_QUESTION:', '').strip()
                    elif line.startswith('METHODOLOGY:'):
                        methodology = line.replace('METHODOLOGY:', '').strip()
                    elif line.startswith('FINDINGS:'):
                        current_section = 'findings'
                    elif line.startswith('CONTRIBUTIONS:'):
                        current_section = 'contributions'
                    elif line.startswith('LIMITATIONS:'):
                        current_section = 'limitations'
                    elif line.startswith('FUTURE_WORK:'):
                        current_section = 'future'
                    elif line.startswith('RELEVANCE:'):
                        try:
                            relevance = float(re.findall(r'[\d.]+', line)[0])
                        except:
                            pass
                    elif line.startswith('-'):
                        content = line[1:].strip()
                        if current_section == 'findings':
                            findings.append(content)
                        elif current_section == 'contributions':
                            contributions.append(content)
                        elif current_section == 'limitations':
                            limitations.append(content)
                        elif current_section == 'future':
                            future_work.append(content)
                
                return PaperAnalysis(
                    paper_id=paper.paper_id,
                    research_question=research_question if research_question else "To be determined",
                    methodology=methodology if methodology else "Various methods employed",
                    key_findings=findings if findings else ["Significant results reported"],
                    contributions=contributions if contributions else ["Novel contributions made"],
                    limitations=limitations if limitations else ["Some limitations exist"],
                    future_work=future_work if future_work else ["Further research needed"],
                    relevance_score=relevance
                )
                
            except Exception as e:
                return PaperAnalysis(
                    paper_id=paper.paper_id,
                    research_question="Analysis in progress",
                    methodology="To be analyzed",
                    key_findings=["Paper analyzed"],
                    contributions=["Contributions identified"],
                    limitations=["Limitations noted"],
                    future_work=["Future directions suggested"],
                    relevance_score=0.7
                )
        
        def identify_trends(
            self,
            papers: List[ResearchPaper],
            min_papers: int = 2
        ) -> List[ResearchTrend]:
            """
            Identify research trends from a collection of papers.
            
            Args:
                papers: List of papers to analyze
                min_papers: Minimum papers to form a trend
                
            Returns:
                List of identified trends
            """
            # Group papers by keywords and time
            keyword_papers = defaultdict(list)
            
            for paper in papers:
                for keyword in paper.keywords:
                    keyword_papers[keyword].append(paper)
            
            # Find trending topics
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a research trend analyst. Identify major research
                trends from the paper collection.
                
                Respond with trends, one per line:
                TOPIC: topic_name | keywords | description"""),
                ("user", """Paper Collection ({count} papers):
    
    {papers}
    
    Identify major research trends.""")
            ])
            
            chain = prompt | self.trend_llm | StrOutputParser()
            
            # Prepare paper summary
            paper_summary = []
            for paper in papers[:20]:  # Limit to avoid context overflow
                paper_summary.append(f"- {paper.title} ({paper.year}): {', '.join(paper.keywords[:3])}")
            
            try:
                response = chain.invoke({
                    "count": len(papers),
                    "papers": "\n".join(paper_summary)
                })
                
                trends = []
                
                for line in response.split('\n'):
                    if '|' in line and 'TOPIC' in line:
                        parts = line.split('|')
                        if len(parts) >= 3:
                            topic = parts[0].replace('TOPIC:', '').strip()
                            keywords = [k.strip() for k in parts[1].split(',')]
                            description = parts[2].strip()
                            
                            # Find papers for this trend
                            trend_papers = []
                            for paper in papers:
                                if any(kw.lower() in ' '.join(paper.keywords).lower() for kw in keywords):
                                    trend_papers.append(paper.paper_id)
                            
                            if len(trend_papers) >= min_papers:
                                years = [p.year for p in papers if p.paper_id in trend_papers]
                                
                                trends.append(ResearchTrend(
                                    topic=topic,
                                    keywords=keywords[:5],
                                    paper_count=len(trend_papers),
                                    growth_rate=1.0,
                                    key_papers=trend_papers[:5],
                                    timespan=(min(years), max(years)) if years else (2020, 2024),
                                    description=description
                                ))
                
                return trends if trends else self._fallback_trends(papers)
                
            except Exception as e:
                return self._fallback_trends(papers)
        
        def _fallback_trends(self, papers: List[ResearchPaper]) -> List[ResearchTrend]:
            """Generate fallback trends from papers"""
            # Group by common keywords
            keyword_count = defaultdict(int)
            for paper in papers:
                for kw in paper.keywords:
                    keyword_count[kw] += 1
            
            # Get top keywords
            top_keywords = sorted(keyword_count.items(), key=lambda x: x[1], reverse=True)[:3]
            
            trends = []
            for keyword, count in top_keywords:
                related_papers = [p for p in papers if keyword in p.keywords]
                years = [p.year for p in related_papers]
                
                trends.append(ResearchTrend(
                    topic=keyword.replace('_', ' ').title(),
                    keywords=[keyword],
                    paper_count=count,
                    growth_rate=1.0,
                    key_papers=[p.paper_id for p in related_papers[:5]],
                    timespan=(min(years), max(years)) if years else (2020, 2024),
                    description=f"Research in {keyword}"
                ))
            
            return trends
        
        def identify_gaps(
            self,
            papers: List[ResearchPaper],
            analyses: List[PaperAnalysis]
        ) -> List[ResearchGap]:
            """
            Identify research gaps from paper analyses.
            
            Args:
                papers: List of papers
                analyses: List of paper analyses
                
            Returns:
                List of identified research gaps
            """
            # Compile limitations and future work
            all_limitations = []
            all_future_work = []
            
            for analysis in analyses:
                all_limitations.extend(analysis.limitations)
                all_future_work.extend(analysis.future_work)
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a research gap analyst. Identify important research
                gaps based on paper limitations and future work suggestions.
                
                Respond with gaps, one per line:
                GAP: description | related_areas | difficulty | impact"""),
                ("user", """Papers Reviewed: {count}
    
    Common Limitations:
    {limitations}
    
    Suggested Future Work:
    {future_work}
    
    Identify research gaps and opportunities.""")
            ])
            
            chain = prompt | self.trend_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "count": len(papers),
                    "limitations": "\n".join([f"- {lim}" for lim in all_limitations[:10]]),
                    "future_work": "\n".join([f"- {fw}" for fw in all_future_work[:10]])
                })
                
                gaps = []
                
                for line in response.split('\n'):
                    if '|' in line and 'GAP' in line:
                        parts = line.split('|')
                        if len(parts) >= 4:
                            description = parts[0].replace('GAP:', '').strip()
                            related = [a.strip() for a in parts[1].split(',')]
                            difficulty = parts[2].strip().lower()
                            impact = parts[3].strip().lower()
                            
                            # Default values if parsing fails
                            if difficulty not in ['easy', 'medium', 'hard']:
                                difficulty = 'medium'
                            if impact not in ['low', 'medium', 'high']:
                                impact = 'medium'
                            
                            gaps.append(ResearchGap(
                                gap_description=description,
                                related_areas=related[:3],
                                potential_approaches=["To be explored"],
                                difficulty=difficulty,
                                impact=impact,
                                supporting_evidence=all_limitations[:2]
                            ))
                
                return gaps if gaps else [
                    ResearchGap(
                        gap_description="Further investigation needed in key areas",
                        related_areas=["General research"],
                        potential_approaches=["Multiple approaches possible"],
                        difficulty="medium",
                        impact="medium",
                        supporting_evidence=all_limitations[:2]
                    )
                ]
                
            except Exception as e:
                return [
                    ResearchGap(
                        gap_description="Research opportunities identified",
                        related_areas=["Multiple areas"],
                        potential_approaches=["Various approaches"],
                        difficulty="medium",
                        impact="medium",
                        supporting_evidence=all_limitations[:2] if all_limitations else ["Analysis needed"]
                    )
                ]
        
        def synthesize_review(
            self,
            research_question: str,
            papers: List[ResearchPaper],
            analyses: List[PaperAnalysis],
            trends: List[ResearchTrend],
            gaps: List[ResearchGap]
        ) -> LiteratureReview:
            """
            Synthesize a complete literature review.
            
            Args:
                research_question: Original research question
                papers: Papers reviewed
                analyses: Paper analyses
                trends: Identified trends
                gaps: Identified gaps
                
            Returns:
                Complete LiteratureReview
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert at writing literature reviews. Synthesize
                the research into a coherent narrative.
                
                Respond in this format:
                THEMES: Key themes (one per line starting with -)
                FINDINGS: Major findings (one per line starting with -)
                SYNTHESIS: Coherent synthesis paragraph
                RECOMMENDATIONS: Research recommendations (one per line starting with -)"""),
                ("user", """Research Question: {question}
    
    Papers Reviewed: {paper_count}
    
    Key Trends:
    {trends}
    
    Research Gaps:
    {gaps}
    
    Synthesize a literature review.""")
            ])
            
            chain = prompt | self.synthesizer_llm | StrOutputParser()
            
            # Prepare summaries
            trends_summary = "\n".join([
                f"- {t.topic}: {t.paper_count} papers ({t.timespan[0]}-{t.timespan[1]})"
                for t in trends[:5]
            ])
            
            gaps_summary = "\n".join([
                f"- {g.gap_description} (Impact: {g.impact})"
                for g in gaps[:5]
            ])
            
            try:
                response = chain.invoke({
                    "question": research_question,
                    "paper_count": len(papers),
                    "trends": trends_summary if trends_summary else "Multiple trends identified",
                    "gaps": gaps_summary if gaps_summary else "Several gaps found"
                })
                
                themes = []
                findings = []
                synthesis = ""
                recommendations = []
                
                current_section = None
                synthesis_lines = []
                
                for line in response.split('\n'):
                    line = line.strip()
                    if not line:
                        continue
                    
                    if line.startswith('THEMES:'):
                        current_section = 'themes'
                    elif line.startswith('FINDINGS:'):
                        current_section = 'findings'
                    elif line.startswith('SYNTHESIS:'):
                        current_section = 'synthesis'
                    elif line.startswith('RECOMMENDATIONS:'):
                        current_section = 'recommendations'
                    elif line.startswith('-'):
                        content = line[1:].strip()
                        if current_section == 'themes':
                            themes.append(content)
                        elif current_section == 'findings':
                            findings.append(content)
                        elif current_section == 'recommendations':
                            recommendations.append(content)
                    elif current_section == 'synthesis':
                        synthesis_lines.append(line)
                
                synthesis = ' '.join(synthesis_lines) if synthesis_lines else "Comprehensive review completed"
                
                return LiteratureReview(
                    research_question=research_question,
                    papers_reviewed=len(papers),
                    key_themes=themes if themes else ["Multiple themes identified"],
                    major_findings=findings if findings else ["Significant findings documented"],
                    trends=trends,
                    gaps=gaps,
                    synthesis=synthesis,
                    recommendations=recommendations if recommendations else ["Further research recommended"]
                )
                
            except Exception as e:
                return LiteratureReview(
                    research_question=research_question,
                    papers_reviewed=len(papers),
                    key_themes=["Research themes identified"],
                    major_findings=["Key findings documented"],
                    trends=trends,
                    gaps=gaps,
                    synthesis="Literature review synthesized successfully",
                    recommendations=["Continue research in identified directions"]
                )
        
        def conduct_research(
            self,
            research_question: str,
            field: ResearchField,
            max_papers: int = 10
        ) -> LiteratureReview:
            """
            Conduct complete research on a question.
            
            Args:
                research_question: Question to research
                field: Research field
                max_papers: Maximum papers to review
                
            Returns:
                Complete LiteratureReview
            """
            print(f"\n🔍 Conducting research on: {research_question}")
            
            # Step 1: Formulate search queries
            print("  ✓ Formulating search queries...")
            queries = self.formulate_search_query(research_question, field)
            
            # Step 2: Search for papers
            print(f"  ✓ Searching with {len(queries)} queries...")
            all_papers = []
            seen_ids = set()
            
            for query in queries:
                papers = self.search_papers(query, max_results=max_papers // len(queries) + 1)
                for paper in papers:
                    if paper.paper_id not in seen_ids:
                        all_papers.append(paper)
                        seen_ids.add(paper.paper_id)
            
            all_papers = all_papers[:max_papers]
            print(f"  ✓ Found {len(all_papers)} relevant papers")
            
            # Step 3: Analyze papers
            print("  ✓ Analyzing papers...")
            analyses = []
            for paper in all_papers:
                analysis = self.analyze_paper(paper, research_question)
                analyses.append(analysis)
            
            # Step 4: Identify trends
            print("  ✓ Identifying trends...")
            trends = self.identify_trends(all_papers)
            
            # Step 5: Identify gaps
            print("  ✓ Identifying research gaps...")
            gaps = self.identify_gaps(all_papers, analyses)
            
            # Step 6: Synthesize review
            print("  ✓ Synthesizing literature review...")
            review = self.synthesize_review(
                research_question,
                all_papers,
                analyses,
                trends,
                gaps
            )
            
            print("  ✅ Research complete!\n")
            
            return review
    
    
    def demonstrate_research_agent():
        """Demonstrate the research agent capabilities"""
        print("=" * 80)
        print("RESEARCH AGENT DEMONSTRATION")
        print("=" * 80)
        
        agent = ResearchAgent()
        
        # Demo 1: Complete Research Project
        print("\n" + "=" * 80)
        print("DEMO 1: Complete Research Project")
        print("=" * 80)
        
        review = agent.conduct_research(
            research_question="How have transformer models impacted natural language processing?",
            field=ResearchField.NATURAL_LANGUAGE_PROCESSING,
            max_papers=5
        )
        
        print("LITERATURE REVIEW")
        print("-" * 80)
        print(f"Research Question: {review.research_question}")
        print(f"Papers Reviewed: {review.papers_reviewed}")
        
        print(f"\nKey Themes:")
        for theme in review.key_themes[:3]:
            print(f"  • {theme}")
        
        print(f"\nMajor Findings:")
        for finding in review.major_findings[:3]:
            print(f"  • {finding}")
        
        print(f"\nIdentified Trends ({len(review.trends)}):")
        for trend in review.trends[:3]:
            print(f"  • {trend.topic}: {trend.paper_count} papers ({trend.timespan[0]}-{trend.timespan[1]})")
            print(f"    Keywords: {', '.join(trend.keywords[:3])}")
        
        print(f"\nResearch Gaps ({len(review.gaps)}):")
        for gap in review.gaps[:2]:
            print(f"  • {gap.gap_description}")
            print(f"    Impact: {gap.impact}, Difficulty: {gap.difficulty}")
        
        print(f"\nSynthesis:")
        print(f"  {review.synthesis[:300]}...")
        
        print(f"\nRecommendations:")
        for rec in review.recommendations[:3]:
            print(f"  • {rec}")
        
        # Demo 2: Paper Analysis
        print("\n" + "=" * 80)
        print("DEMO 2: Individual Paper Analysis")
        print("=" * 80)
        
        sample_paper = agent.paper_database["paper001"]
        print(f"\nAnalyzing: {sample_paper.title}")
        print(f"Authors: {', '.join(sample_paper.authors)}")
        print(f"Year: {sample_paper.year}, Venue: {sample_paper.venue}")
        
        analysis = agent.analyze_paper(
            sample_paper,
            "Understanding attention mechanisms in neural networks"
        )
        
        print(f"\nResearch Question: {analysis.research_question}")
        print(f"Methodology: {analysis.methodology}")
        print(f"Relevance Score: {analysis.relevance_score:.2f}")
        
        print(f"\nKey Findings:")
        for finding in analysis.key_findings[:3]:
            print(f"  • {finding}")
        
        print(f"\nContributions:")
        for contrib in analysis.contributions[:2]:
            print(f"  • {contrib}")
        
        # Demo 3: Trend Analysis
        print("\n" + "=" * 80)
        print("DEMO 3: Research Trend Analysis")
        print("=" * 80)
        
        all_papers = list(agent.paper_database.values())
        trends = agent.identify_trends(all_papers)
        
        print(f"\nIdentified {len(trends)} major trends:")
        for i, trend in enumerate(trends, 1):
            print(f"\n{i}. {trend.topic}")
            print(f"   Period: {trend.timespan[0]}-{trend.timespan[1]}")
            print(f"   Papers: {trend.paper_count}")
            print(f"   Keywords: {', '.join(trend.keywords)}")
            print(f"   Description: {trend.description}")
        
        # Demo 4: Gap Identification
        print("\n" + "=" * 80)
        print("DEMO 4: Research Gap Identification")
        print("=" * 80)
        
        # Analyze a few papers
        sample_analyses = []
        for paper_id in ["paper001", "paper002", "paper003"]:
            paper = agent.paper_database[paper_id]
            analysis = agent.analyze_paper(paper, "Improving language model capabilities")
            sample_analyses.append(analysis)
        
        gaps = agent.identify_gaps(all_papers, sample_analyses)
        
        print(f"\nIdentified {len(gaps)} research gaps:")
        for i, gap in enumerate(gaps, 1):
            print(f"\n{i}. {gap.gap_description}")
            print(f"   Impact: {gap.impact}, Difficulty: {gap.difficulty}")
            print(f"   Related Areas: {', '.join(gap.related_areas)}")
            if gap.potential_approaches:
                print(f"   Approaches: {', '.join(gap.potential_approaches[:2])}")
        
        # Demo 5: Search Query Formulation
        print("\n" + "=" * 80)
        print("DEMO 5: Search Query Formulation")
        print("=" * 80)
        
        research_questions = [
            "What are the latest advances in few-shot learning?",
            "How can we improve the efficiency of large language models?"
        ]
        
        for question in research_questions:
            print(f"\nResearch Question: {question}")
            queries = agent.formulate_search_query(
                question,
                ResearchField.MACHINE_LEARNING
            )
            print("Generated Queries:")
            for i, query in enumerate(queries, 1):
                print(f"  {i}. {query}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        
        summary = """
    The Research Agent demonstrates comprehensive capabilities for academic research:
    
    KEY CAPABILITIES:
    1. Search Query Formulation: Generates effective queries for finding papers
    2. Paper Discovery: Searches and retrieves relevant academic papers
    3. Content Analysis: Extracts methods, findings, and contributions
    4. Trend Identification: Identifies research trends and patterns
    5. Gap Analysis: Finds unexplored research opportunities
    6. Literature Synthesis: Combines findings into coherent reviews
    
    BENEFITS:
    - Accelerates literature review process significantly
    - Ensures comprehensive coverage of research area
    - Identifies patterns across many papers
    - Discovers research opportunities systematically
    - Generates well-structured literature reviews
    
    USE CASES:
    - PhD thesis literature reviews
    - Research proposal development
    - Grant application background sections
    - Systematic literature reviews
    - Technology landscape analysis
    - Keeping up with research trends
    - Expert identification
    - Research collaboration opportunities
    
    PRODUCTION CONSIDERATIONS:
    1. API Integration: Connect to Semantic Scholar, ArXiv, PubMed, Google Scholar
    2. Full-Text Access: Integrate with paper repositories and libraries
    3. Citation Analysis: Build and analyze complete citation networks
    4. PDF Processing: Extract text from PDF papers
    5. Database Storage: Persist papers, analyses, and reviews
    6. Visualization: Generate trend plots, citation graphs, topic maps
    7. Collaboration: Support team research and shared libraries
    8. Export: Generate LaTeX, Word, and markdown outputs
    9. Updates: Monitor new papers and alert on relevant publications
    10. Quality Assessment: Evaluate paper quality and venue prestige
    
    ADVANCED EXTENSIONS:
    - Automatic paper summarization
    - Research proposal generation
    - Hypothesis formulation from gaps
    - Experiment design suggestions
    - Funding opportunity matching
    - Researcher network analysis
    - Multi-language paper support
    - Domain-specific paper understanding
    - Automated peer review assistance
    - Research impact prediction
    
    The agent transforms the manual, time-consuming process of literature review
    into an efficient, comprehensive, and intelligent workflow.
    """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_research_agent()
    
        

    Agentic Design Pattern: Research Agent

    Pattern 074: Research Agent – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 074: Research Agent

    Description: A Research Agent is a specialized AI agent designed to conduct academic and professional research, including literature review, paper analysis, citation tracking, knowledge synthesis, and research gap identification. This pattern demonstrates how to build an intelligent agent that can autonomously search for relevant papers, analyze their content, track citations, identify research trends, and synthesize findings into coherent reviews.

    The agent combines information retrieval with deep content analysis to assist
    researchers, students, and professionals in understanding complex research
    domains. It can handle tasks like systematic literature reviews, related work
    sections, research proposal development, and keeping up with rapidly evolving
    fields.
    

    Components: 1. Paper Discovery: Searches academic databases and repositories 2. Relevance Filtering: Identifies papers relevant to research questions 3. Content Analyzer: Extracts key information from papers 4. Citation Network: Builds and analyzes citation graphs 5. Trend Analyzer: Identifies research trends and emerging topics 6. Gap Identifier: Finds unexplored areas and opportunities 7. Synthesis Engine: Combines findings into coherent narratives 8. Knowledge Organizer: Structures information hierarchically

    Key Features: – Multi-database paper search and retrieval – Automatic relevance ranking and filtering – Key information extraction (methods, results, contributions) – Citation network analysis and visualization – Research trend identification over time – Research gap and opportunity detection – Multi-paper synthesis and summarization – Related work generation – Research question formulation – Literature review structuring

    Use Cases: – Systematic literature reviews – Research proposal development – PhD thesis literature surveys – Grant proposal background research – Technology landscape analysis – Competitive research analysis – Research trend tracking – Expert identification – Conference/journal selection – Research collaboration discovery

    LangChain Implementation: Uses ChatOpenAI for paper analysis, synthesis, and research planning, with specialized chains for different aspects of research workflow.


    File Statistics

    • Total Lines: 1094
    • Classes: 10
    • Functions: 1
    • Constants: 0

    Module Documentation

    """
    Pattern 074: Research Agent
    
    Description:
        A Research Agent is a specialized AI agent designed to conduct academic and
        professional research, including literature review, paper analysis, citation
        tracking, knowledge synthesis, and research gap identification. This pattern
        demonstrates how to build an intelligent agent that can autonomously search
        for relevant papers, analyze their content, track citations, identify research
        trends, and synthesize findings into coherent reviews.
    
        The agent combines information retrieval with deep content analysis to assist
        researchers, students, and professionals in understanding complex research
        domains. It can handle tasks like systematic literature reviews, related work
        sections, research proposal development, and keeping up with rapidly evolving
        fields.
    
    Components:
        1. Paper Discovery: Searches academic databases and repositories
        2. Relevance Filtering: Identifies papers relevant to research questions
        3. Content Analyzer: Extracts key information from papers
        4. Citation Network: Builds and analyzes citation graphs
        5. Trend Analyzer: Identifies research trends and emerging topics
        6. Gap Identifier: Finds unexplored areas and opportunities
        7. Synthesis Engine: Combines findings into coherent narratives
        8. Knowledge Organizer: Structures information hierarchically
    
    Key Features:
        - Multi-database paper search and retrieval
        - Automatic relevance ranking and filtering
        - Key information extraction (methods, results, contributions)
        - Citation network analysis and visualization
        - Research trend identification over time
        - Research gap and opportunity detection
        - Multi-paper synthesis and summarization
        - Related work generation
        - Research question formulation
        - Literature review structuring
    
    Use Cases:
        - Systematic literature reviews
        - Research proposal development
        - PhD thesis literature surveys
        - Grant proposal background research
        - Technology landscape analysis
        - Competitive research analysis
        - Research trend tracking
        - Expert identification
        - Conference/journal selection
        - Research collaboration discovery
    
    LangChain Implementation:
        Uses ChatOpenAI for paper analysis, synthesis, and research planning,
        with specialized chains for different aspects of research workflow.
    """
    

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


    Import Statements

    Standard Library Imports

    import collections.defaultdict
    import dataclasses.dataclass
    import dataclasses.field
    import datetime.datetime
    import enum.Enum
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import re
    import typing.Any
    import typing.Dict
    import typing.List
    import typing.Optional
    import typing.Set
    import typing.Tuple
    

    Purpose: Core Python functionality: – collections.defaultdict: Specialized container datatypes – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – enum.Enum: Enumeration support – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – re: Regular expression operations – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Set: Type hints for better code documentation (List, Dict, Any, Optional) – 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: ResearchField

    Lines 73-86

    Inherits from: Enum

    Purpose:

    Academic research fields
    

    Class: PaperType

    Lines 89-98

    Inherits from: Enum

    Purpose:

    Types of research papers
    

    Class: ResearchPhase

    Lines 101-107

    Inherits from: Enum

    Purpose:

    Phases of research
    

    Class: ResearchPaper

    Lines 111-123

    Purpose:

    Represents a research paper
    

    Class: PaperAnalysis

    Lines 127-136

    Purpose:

    Analysis of a research paper
    

    Class: CitationRelationship

    Lines 140-145

    Purpose:

    Citation relationship between papers
    

    Class: ResearchTrend

    Lines 149-157

    Purpose:

    Identified research trend
    

    Class: ResearchGap

    Lines 161-168

    Purpose:

    Identified research gap
    

    Class: LiteratureReview

    Lines 172-181

    Purpose:

    Complete literature review
    

    Class: ResearchAgent

    Lines 184-891

    Purpose:

    Agent for conducting academic and professional research.
    
    This agent can search for papers, analyze content, track citations,
    identify trends, and synthesize findings into literature reviews.
    

    Methods:

    __init__(self)

    Lines 192-208

    Purpose:

    Initialize the research agent with specialized LLMs
    

    Parameters:

    _initialize_sample_papers(self)

    Lines 210-277

    Purpose:

    Initialize sample research papers for demonstration
    

    Parameters:

    formulate_search_query(self, research_question, field)

    Lines 279-327

    Purpose:

    Formulate effective search queries for the research question.
    
    Args:
        research_question: The research question to investigate
        field: Academic field of research
    
    Returns:
        List of search query strings
    

    Parameters: – research_question: Question to answer – field: Field

    Returns: <ast.Subscript object at 0x000001A06D6FCB90>

    search_papers(self, query, max_results)

    Lines 329-380

    Purpose:

    Search for papers matching the query.
    
    Args:
        query: Search query
        max_results: Maximum number of results
    
    Returns:
        List of matching papers
    

    Parameters: – query: Input query or question – max_results: Max results

    Returns: <ast.Subscript object at 0x000001A06D632390>

    analyze_paper(self, paper, research_context)

    Lines 382-494

    Purpose:

    Analyze a research paper in context of research question.
    
    Args:
        paper: Paper to analyze
        research_context: Context/question for analysis
    
    Returns:
        PaperAnalysis with extracted information
    

    Parameters: – paper: Paper – research_context: Research context

    Returns: PaperAnalysis

    identify_trends(self, papers, min_papers)

    Lines 496-577

    Purpose:

    Identify research trends from a collection of papers.
    
    Args:
        papers: List of papers to analyze
        min_papers: Minimum papers to form a trend
    
    Returns:
        List of identified trends
    

    Parameters: – papers: Papers – min_papers: Min papers

    Returns: <ast.Subscript object at 0x000001A06D6B1990>

    _fallback_trends(self, papers)

    Lines 579-605

    Purpose:

    Generate fallback trends from papers
    

    Parameters: – papers: Papers

    Returns: <ast.Subscript object at 0x000001A06D6A8CD0>

    identify_gaps(self, papers, analyses)

    Lines 607-703

    Purpose:

    Identify research gaps from paper analyses.
    
    Args:
        papers: List of papers
        analyses: List of paper analyses
    
    Returns:
        List of identified research gaps
    

    Parameters: – papers: Papers – analyses: Analyses

    Returns: <ast.Subscript object at 0x000001A06D75A790>

    synthesize_review(self, research_question, papers, analyses, trends, gaps)

    Lines 705-824

    Purpose:

    Synthesize a complete literature review.
    
    Args:
        research_question: Original research question
        papers: Papers reviewed
        analyses: Paper analyses
        trends: Identified trends
        gaps: Identified gaps
    
    Returns:
        Complete LiteratureReview
    

    Parameters: – research_question: Question to answer – papers: Papers – analyses: Analyses – trends: Trends – gaps: Gaps

    Returns: LiteratureReview

    conduct_research(self, research_question, field, max_papers)

    Lines 826-891

    Purpose:

    Conduct complete research on a question.
    
    Args:
        research_question: Question to research
        field: Research field
        max_papers: Maximum papers to review
    
    Returns:
        Complete LiteratureReview
    

    Parameters: – research_question: Question to answer – field: Field – max_papers: Max papers

    Returns: LiteratureReview


    Functions

    demonstrate_research_agent()

    Lines 894-1089

    Purpose:

    Demonstrate the research agent capabilities
    

    Key Concepts Summary

    Pattern Overview

    The Research Agent 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 073
    • Pattern 075

    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 Research Agent 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: 074_research_agent.py
    Total Lines: 1094
    Pattern Category: AI Agent Design Pattern #074


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Web Browsing Agent

    
        """
    Pattern 073: Web Browsing Agent
    
    Description:
        A Web Browsing Agent is a specialized AI agent designed to navigate websites,
        extract information, interact with web elements, and synthesize findings from
        multiple web sources. This pattern demonstrates how to build an intelligent agent
        that can autonomously browse the web, understand page content, follow links,
        fill forms, and extract structured information.
        
        The agent combines web navigation capabilities with natural language understanding
        to perform complex web-based research tasks. It can handle dynamic content,
        JavaScript-rendered pages, authentication, pagination, and multi-page workflows.
        Unlike simple web scrapers, this agent makes intelligent decisions about where
        to navigate based on task objectives.
    
    Components:
        1. Navigation Controller: Manages page navigation and history
        2. Content Extractor: Parses and extracts relevant information from pages
        3. Element Interactor: Clicks buttons, fills forms, handles dropdowns
        4. Link Analyzer: Evaluates and prioritizes links to follow
        5. Session Manager: Handles cookies, authentication, state
        6. Page Understanding: Interprets page structure and content semantically
        7. Information Synthesizer: Combines data from multiple pages
        8. Task Planner: Plans multi-step browsing strategies
    
    Key Features:
        - Intelligent link following based on task relevance
        - Dynamic content handling (JavaScript, AJAX)
        - Form filling and interaction
        - Multi-page information aggregation
        - Authentication and session management
        - Pagination handling
        - Screenshot and visual analysis
        - Content summarization
        - Structured data extraction
        - Recursive crawling with depth limits
    
    Use Cases:
        - Competitive intelligence gathering
        - Price monitoring and comparison
        - News aggregation and monitoring
        - Academic research and literature review
        - Job posting scraping
        - Real estate listing analysis
        - Product information extraction
        - Documentation exploration
        - Social media monitoring
        - API discovery and testing
    
    LangChain Implementation:
        Uses ChatOpenAI for page understanding and decision-making, simulates
        web interactions with structured data models representing browser state.
    """
    
    import os
    import re
    from typing import List, Dict, Any, Optional, Set, Tuple
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class PageType(Enum):
        """Types of web pages"""
        HOME = "home"
        ARTICLE = "article"
        PRODUCT = "product"
        LISTING = "listing"
        FORM = "form"
        SEARCH_RESULTS = "search_results"
        DOCUMENTATION = "documentation"
        PROFILE = "profile"
        UNKNOWN = "unknown"
    
    
    class ActionType(Enum):
        """Types of web actions"""
        NAVIGATE = "navigate"
        CLICK = "click"
        FILL_FORM = "fill_form"
        SCROLL = "scroll"
        WAIT = "wait"
        EXTRACT = "extract"
        BACK = "back"
        REFRESH = "refresh"
    
    
    class ContentType(Enum):
        """Types of content to extract"""
        TEXT = "text"
        LINKS = "links"
        IMAGES = "images"
        TABLES = "tables"
        FORMS = "forms"
        METADATA = "metadata"
        STRUCTURED_DATA = "structured_data"
    
    
    @dataclass
    class WebPage:
        """Represents a web page"""
        url: str
        title: str
        page_type: PageType
        content: str
        links: List[str] = field(default_factory=list)
        forms: List[Dict[str, Any]] = field(default_factory=list)
        metadata: Dict[str, str] = field(default_factory=dict)
        visited_at: datetime = field(default_factory=datetime.now)
    
    
    @dataclass
    class BrowsingAction:
        """Represents a browsing action"""
        action_type: ActionType
        target: str  # URL, element selector, etc.
        parameters: Dict[str, Any] = field(default_factory=dict)
        reason: str = ""
    
    
    @dataclass
    class ExtractedData:
        """Structured data extracted from a page"""
        source_url: str
        data_type: str
        content: Dict[str, Any]
        confidence: float
        extracted_at: datetime = field(default_factory=datetime.now)
    
    
    @dataclass
    class BrowsingSession:
        """State of a browsing session"""
        session_id: str
        start_url: str
        objective: str
        current_url: str
        history: List[str] = field(default_factory=list)
        visited_urls: Set[str] = field(default_factory=set)
        extracted_data: List[ExtractedData] = field(default_factory=list)
        cookies: Dict[str, str] = field(default_factory=dict)
    
    
    @dataclass
    class NavigationPlan:
        """Plan for multi-page navigation"""
        objective: str
        actions: List[BrowsingAction]
        expected_pages: int
        max_depth: int
        priority_keywords: List[str]
    
    
    class WebBrowsingAgent:
        """
        Agent for intelligent web browsing and information extraction.
        
        This agent can navigate websites autonomously, understand page content,
        make decisions about where to navigate, and extract relevant information.
        """
        
        def __init__(self, max_pages: int = 10, max_depth: int = 3):
            """
            Initialize the web browsing agent.
            
            Args:
                max_pages: Maximum number of pages to visit per task
                max_depth: Maximum navigation depth from start page
            """
            # Navigator for planning and decision-making
            self.navigator_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.2)
            
            # Extractor for content understanding
            self.extractor_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.1)
            
            # Analyzer for link evaluation
            self.analyzer_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
            
            # Synthesizer for combining information
            self.synthesizer_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.4)
            
            self.max_pages = max_pages
            self.max_depth = max_depth
            
            # Simulated page database (in production, would use real web driver)
            self.page_database: Dict[str, WebPage] = {}
            self._initialize_sample_pages()
        
        def _initialize_sample_pages(self):
            """Initialize sample web pages for demonstration"""
            self.page_database = {
                "https://example.com": WebPage(
                    url="https://example.com",
                    title="Example Company - Home",
                    page_type=PageType.HOME,
                    content="Welcome to Example Company. We provide innovative solutions. Visit our products page or read our latest blog posts.",
                    links=[
                        "https://example.com/products",
                        "https://example.com/blog",
                        "https://example.com/about",
                        "https://example.com/contact"
                    ],
                    metadata={"description": "Leading provider of innovative solutions"}
                ),
                "https://example.com/products": WebPage(
                    url="https://example.com/products",
                    title="Products - Example Company",
                    page_type=PageType.LISTING,
                    content="Our Products: Product A ($99) - High quality solution. Product B ($149) - Premium offering. Product C ($199) - Enterprise solution.",
                    links=[
                        "https://example.com/products/a",
                        "https://example.com/products/b",
                        "https://example.com/products/c",
                        "https://example.com"
                    ],
                    metadata={"category": "products"}
                ),
                "https://example.com/products/a": WebPage(
                    url="https://example.com/products/a",
                    title="Product A - Details",
                    page_type=PageType.PRODUCT,
                    content="Product A: $99. Features: Fast, Reliable, Easy to use. Rating: 4.5/5. Reviews: 120 customer reviews.",
                    links=["https://example.com/products", "https://example.com/cart"],
                    metadata={"price": "99", "rating": "4.5", "product_id": "A"}
                ),
                "https://example.com/blog": WebPage(
                    url="https://example.com/blog",
                    title="Blog - Example Company",
                    page_type=PageType.LISTING,
                    content="Recent Posts: How to improve productivity (Jan 15, 2024). Industry trends 2024 (Jan 10, 2024). Best practices guide (Jan 5, 2024).",
                    links=[
                        "https://example.com/blog/productivity",
                        "https://example.com/blog/trends",
                        "https://example.com/blog/practices",
                        "https://example.com"
                    ],
                    metadata={"category": "blog"}
                ),
                "https://example.com/blog/productivity": WebPage(
                    url="https://example.com/blog/productivity",
                    title="How to Improve Productivity",
                    page_type=PageType.ARTICLE,
                    content="Improving productivity requires focus, proper tools, and good habits. Key strategies include time blocking, eliminating distractions, and using automation. Studies show 40% improvement with these methods.",
                    links=["https://example.com/blog", "https://example.com/products"],
                    metadata={"author": "John Doe", "date": "2024-01-15", "category": "productivity"}
                )
            }
        
        def create_navigation_plan(
            self,
            start_url: str,
            objective: str,
            keywords: List[str]
        ) -> NavigationPlan:
            """
            Create a navigation plan for achieving the objective.
            
            Args:
                start_url: Starting URL
                objective: What to accomplish
                keywords: Important keywords to look for
                
            Returns:
                NavigationPlan with actions to take
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a web navigation expert. Create a plan to achieve
                the objective by browsing from the start URL.
                
                Respond in this format:
                ACTIONS: List of actions (one per line)
                Format: action_type|target|reason
                
                EXPECTED_PAGES: Number of pages to visit
                KEYWORDS: Priority keywords (comma-separated)"""),
                ("user", """Start URL: {start_url}
    Objective: {objective}
    Keywords: {keywords}
    
    Create a navigation plan.""")
            ])
            
            chain = prompt | self.navigator_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "start_url": start_url,
                    "objective": objective,
                    "keywords": ", ".join(keywords)
                })
                
                actions = []
                expected_pages = self.max_pages
                priority_keywords = keywords.copy()
                
                for line in response.split('\n'):
                    if '|' in line and not line.startswith(('ACTIONS', 'EXPECTED', 'KEYWORDS')):
                        parts = line.split('|')
                        if len(parts) >= 3:
                            action_type_str = parts[0].strip().lower()
                            
                            # Map to enum
                            action_type = ActionType.NAVIGATE
                            if 'click' in action_type_str:
                                action_type = ActionType.CLICK
                            elif 'fill' in action_type_str or 'form' in action_type_str:
                                action_type = ActionType.FILL_FORM
                            elif 'extract' in action_type_str:
                                action_type = ActionType.EXTRACT
                            elif 'navigate' in action_type_str or 'visit' in action_type_str:
                                action_type = ActionType.NAVIGATE
                            
                            actions.append(BrowsingAction(
                                action_type=action_type,
                                target=parts[1].strip(),
                                reason=parts[2].strip() if len(parts) > 2 else ""
                            ))
                    elif line.startswith('EXPECTED_PAGES:'):
                        try:
                            expected_pages = int(re.findall(r'\d+', line)[0])
                        except:
                            pass
                    elif line.startswith('KEYWORDS:'):
                        kw_str = line.replace('KEYWORDS:', '').strip()
                        priority_keywords = [k.strip() for k in kw_str.split(',')]
                
                if not actions:
                    actions.append(BrowsingAction(
                        action_type=ActionType.NAVIGATE,
                        target=start_url,
                        reason="Start browsing"
                    ))
                
                return NavigationPlan(
                    objective=objective,
                    actions=actions,
                    expected_pages=min(expected_pages, self.max_pages),
                    max_depth=self.max_depth,
                    priority_keywords=priority_keywords
                )
                
            except Exception as e:
                return NavigationPlan(
                    objective=objective,
                    actions=[BrowsingAction(
                        action_type=ActionType.NAVIGATE,
                        target=start_url,
                        reason="Start browsing"
                    )],
                    expected_pages=self.max_pages,
                    max_depth=self.max_depth,
                    priority_keywords=keywords
                )
        
        def fetch_page(self, url: str) -> Optional[WebPage]:
            """
            Fetch a web page (simulated).
            
            Args:
                url: URL to fetch
                
            Returns:
                WebPage object or None if not found
            """
            # In production, would use actual web driver (Selenium, Playwright, etc.)
            return self.page_database.get(url)
        
        def understand_page(self, page: WebPage, objective: str) -> Dict[str, Any]:
            """
            Understand page content and relevance to objective.
            
            Args:
                page: WebPage to analyze
                objective: Current objective
                
            Returns:
                Dictionary with page understanding
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a web content analyst. Analyze the page and determine
                its relevance to the objective.
                
                Respond in this format:
                RELEVANCE: High/Medium/Low
                KEY_INFO: Key information found (one per line starting with -)
                NEXT_STEPS: Suggested next actions (one per line starting with -)
                CONFIDENCE: Confidence score (0.0-1.0)"""),
                ("user", """Page URL: {url}
    Title: {title}
    Type: {page_type}
    Content: {content}
    
    Objective: {objective}
    
    Analyze this page.""")
            ])
            
            chain = prompt | self.extractor_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "url": page.url,
                    "title": page.title,
                    "page_type": page.page_type.value,
                    "content": page.content[:500],  # Limit content length
                    "objective": objective
                })
                
                relevance = "medium"
                key_info = []
                next_steps = []
                confidence = 0.7
                
                current_section = None
                
                for line in response.split('\n'):
                    line = line.strip()
                    if not line:
                        continue
                    
                    if line.startswith('RELEVANCE:'):
                        relevance = line.replace('RELEVANCE:', '').strip().lower()
                    elif line.startswith('KEY_INFO:'):
                        current_section = 'info'
                    elif line.startswith('NEXT_STEPS:'):
                        current_section = 'steps'
                    elif line.startswith('CONFIDENCE:'):
                        try:
                            confidence = float(re.findall(r'[\d.]+', line)[0])
                        except:
                            pass
                    elif line.startswith('-'):
                        content = line[1:].strip()
                        if current_section == 'info':
                            key_info.append(content)
                        elif current_section == 'steps':
                            next_steps.append(content)
                
                return {
                    "relevance": relevance,
                    "key_info": key_info if key_info else ["Page analyzed"],
                    "next_steps": next_steps if next_steps else ["Continue browsing"],
                    "confidence": confidence
                }
                
            except Exception as e:
                return {
                    "relevance": "medium",
                    "key_info": ["Page content available"],
                    "next_steps": ["Explore links"],
                    "confidence": 0.6
                }
        
        def evaluate_links(
            self,
            links: List[str],
            objective: str,
            visited: Set[str],
            keywords: List[str]
        ) -> List[Tuple[str, float]]:
            """
            Evaluate and rank links by relevance to objective.
            
            Args:
                links: List of URLs to evaluate
                objective: Current objective
                visited: Set of already visited URLs
                keywords: Priority keywords
                
            Returns:
                List of (url, score) tuples, sorted by score
            """
            unvisited_links = [link for link in links if link not in visited]
            
            if not unvisited_links:
                return []
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a link evaluation expert. Score each link based on
                its likely relevance to the objective.
                
                Respond with one line per link:
                url|score|reason
                
                Score from 0.0 (irrelevant) to 1.0 (highly relevant)"""),
                ("user", """Objective: {objective}
    Keywords: {keywords}
    
    Links to evaluate:
    {links}
    
    Score each link.""")
            ])
            
            chain = prompt | self.analyzer_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "objective": objective,
                    "keywords": ", ".join(keywords),
                    "links": "\n".join([f"- {link}" for link in unvisited_links[:10]])
                })
                
                scored_links = []
                
                for line in response.split('\n'):
                    if '|' in line:
                        parts = line.split('|')
                        if len(parts) >= 2:
                            url = parts[0].strip()
                            try:
                                score = float(re.findall(r'[\d.]+', parts[1])[0])
                                if url in unvisited_links:
                                    scored_links.append((url, score))
                            except:
                                pass
                
                # If no scores, assign default scores
                if not scored_links:
                    for link in unvisited_links:
                        # Simple keyword matching
                        score = 0.5
                        for keyword in keywords:
                            if keyword.lower() in link.lower():
                                score += 0.2
                        scored_links.append((link, min(score, 1.0)))
                
                return sorted(scored_links, key=lambda x: x[1], reverse=True)
                
            except Exception as e:
                # Fallback: basic keyword matching
                scored_links = []
                for link in unvisited_links:
                    score = 0.5
                    for keyword in keywords:
                        if keyword.lower() in link.lower():
                            score += 0.2
                    scored_links.append((link, min(score, 1.0)))
                
                return sorted(scored_links, key=lambda x: x[1], reverse=True)
        
        def extract_information(
            self,
            page: WebPage,
            objective: str,
            content_types: List[ContentType]
        ) -> ExtractedData:
            """
            Extract structured information from a page.
            
            Args:
                page: WebPage to extract from
                objective: What information to extract
                content_types: Types of content to extract
                
            Returns:
                ExtractedData with structured information
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an information extraction expert. Extract structured
                data from the page based on the objective.
                
                Respond in this format (JSON-like):
                {{
                    "key1": "value1",
                    "key2": "value2",
                    ...
                }}
                
                Extract relevant fields based on content type."""),
                ("user", """Page: {title}
    URL: {url}
    Content: {content}
    
    Objective: {objective}
    Content Types: {types}
    
    Extract structured information.""")
            ])
            
            chain = prompt | self.extractor_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "title": page.title,
                    "url": page.url,
                    "content": page.content,
                    "objective": objective,
                    "types": ", ".join([ct.value for ct in content_types])
                })
                
                # Try to parse as structured data
                extracted = {}
                
                # Simple key-value extraction
                for line in response.split('\n'):
                    if ':' in line and not line.strip().startswith(('#', '//')):
                        parts = line.split(':', 1)
                        if len(parts) == 2:
                            key = parts[0].strip().strip('"\'{}')
                            value = parts[1].strip().strip(',"\'}')
                            if key and value:
                                extracted[key] = value
                
                # If no structured data, extract key info
                if not extracted:
                    extracted = {
                        "title": page.title,
                        "url": page.url,
                        "summary": page.content[:200]
                    }
                
                return ExtractedData(
                    source_url=page.url,
                    data_type="page_data",
                    content=extracted,
                    confidence=0.8
                )
                
            except Exception as e:
                return ExtractedData(
                    source_url=page.url,
                    data_type="page_data",
                    content={
                        "title": page.title,
                        "url": page.url,
                        "content": page.content[:200]
                    },
                    confidence=0.6
                )
        
        def browse(
            self,
            start_url: str,
            objective: str,
            keywords: List[str] = None
        ) -> BrowsingSession:
            """
            Browse the web to achieve the objective.
            
            Args:
                start_url: Starting URL
                objective: What to accomplish
                keywords: Optional priority keywords
                
            Returns:
                BrowsingSession with results
            """
            if keywords is None:
                keywords = []
            
            # Create session
            session = BrowsingSession(
                session_id=f"session_{datetime.now().timestamp()}",
                start_url=start_url,
                objective=objective,
                current_url=start_url
            )
            
            # Create navigation plan
            plan = self.create_navigation_plan(start_url, objective, keywords)
            
            # Browse pages
            pages_visited = 0
            current_depth = 0
            
            urls_to_visit = [(start_url, 0)]  # (url, depth)
            
            while urls_to_visit and pages_visited  self.max_depth:
                    continue
                
                # Fetch page
                page = self.fetch_page(current_url)
                if not page:
                    continue
                
                # Update session
                session.current_url = current_url
                session.history.append(current_url)
                session.visited_urls.add(current_url)
                pages_visited += 1
                
                # Understand page
                understanding = self.understand_page(page, objective)
                
                # Extract information if relevant
                if understanding["relevance"] in ["high", "medium"]:
                    extracted = self.extract_information(
                        page,
                        objective,
                        [ContentType.TEXT, ContentType.STRUCTURED_DATA]
                    )
                    session.extracted_data.append(extracted)
                
                # Evaluate and queue links
                if depth  0.5:
                            urls_to_visit.append((link, depth + 1))
            
            return session
        
        def synthesize_findings(self, session: BrowsingSession) -> Dict[str, Any]:
            """
            Synthesize findings from browsing session.
            
            Args:
                session: BrowsingSession with extracted data
                
            Returns:
                Dictionary with synthesized findings
            """
            # Compile extracted data
            all_data = []
            for extracted in session.extracted_data:
                all_data.append(f"From {extracted.source_url}:")
                for key, value in extracted.content.items():
                    all_data.append(f"  - {key}: {value}")
            
            data_summary = "\n".join(all_data[:50])  # Limit length
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a research synthesizer. Combine information from
                multiple sources into coherent findings.
                
                Respond in this format:
                SUMMARY: Brief summary
                KEY_FINDINGS: Main findings (one per line starting with -)
                INSIGHTS: Deeper insights (one per line starting with -)
                CONCLUSION: Final conclusion"""),
                ("user", """Objective: {objective}
                
    Pages Visited: {pages_count}
    
    Extracted Data:
    {data}
    
    Synthesize findings.""")
            ])
            
            chain = prompt | self.synthesizer_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "objective": session.objective,
                    "pages_count": len(session.visited_urls),
                    "data": data_summary
                })
                
                summary = ""
                findings = []
                insights = []
                conclusion = ""
                
                current_section = None
                
                for line in response.split('\n'):
                    line = line.strip()
                    if not line:
                        continue
                    
                    if line.startswith('SUMMARY:'):
                        summary = line.replace('SUMMARY:', '').strip()
                    elif line.startswith('KEY_FINDINGS:'):
                        current_section = 'findings'
                    elif line.startswith('INSIGHTS:'):
                        current_section = 'insights'
                    elif line.startswith('CONCLUSION:'):
                        current_section = 'conclusion'
                    elif line.startswith('-'):
                        content = line[1:].strip()
                        if current_section == 'findings':
                            findings.append(content)
                        elif current_section == 'insights':
                            insights.append(content)
                    elif current_section == 'conclusion':
                        conclusion += line + " "
                
                return {
                    "summary": summary if summary else "Browsing completed successfully",
                    "findings": findings if findings else ["Information gathered from multiple pages"],
                    "insights": insights if insights else ["Further analysis recommended"],
                    "conclusion": conclusion.strip() if conclusion else "Objective achieved",
                    "pages_visited": len(session.visited_urls),
                    "data_points": len(session.extracted_data)
                }
                
            except Exception as e:
                return {
                    "summary": f"Browsed {len(session.visited_urls)} pages",
                    "findings": [f"Visited {len(session.visited_urls)} pages", f"Extracted {len(session.extracted_data)} data points"],
                    "insights": ["Multiple sources explored"],
                    "conclusion": "Browsing session completed",
                    "pages_visited": len(session.visited_urls),
                    "data_points": len(session.extracted_data)
                }
    
    
    def demonstrate_web_browsing_agent():
        """Demonstrate the web browsing agent capabilities"""
        print("=" * 80)
        print("WEB BROWSING AGENT DEMONSTRATION")
        print("=" * 80)
        
        agent = WebBrowsingAgent(max_pages=5, max_depth=2)
        
        # Demo 1: Product Research
        print("\n" + "=" * 80)
        print("DEMO 1: Product Research")
        print("=" * 80)
        
        print("\nObjective: Research products and pricing")
        print("Starting URL: https://example.com")
        
        session1 = agent.browse(
            start_url="https://example.com",
            objective="Find all products and their prices",
            keywords=["product", "price", "buy"]
        )
        
        print(f"\nBrowsing Session: {session1.session_id}")
        print(f"Pages Visited: {len(session1.visited_urls)}")
        print(f"\nNavigation Path:")
        for i, url in enumerate(session1.history, 1):
            print(f"  {i}. {url}")
        
        print(f"\nData Extracted: {len(session1.extracted_data)} items")
        for i, data in enumerate(session1.extracted_data[:3], 1):
            print(f"\n  Item {i} from {data.source_url}:")
            for key, value in list(data.content.items())[:3]:
                print(f"    - {key}: {value}")
        
        findings1 = agent.synthesize_findings(session1)
        print(f"\nSynthesized Findings:")
        print(f"Summary: {findings1['summary']}")
        print(f"\nKey Findings:")
        for finding in findings1['findings'][:3]:
            print(f"  - {finding}")
        
        # Demo 2: Content Research
        print("\n" + "=" * 80)
        print("DEMO 2: Content Research")
        print("=" * 80)
        
        print("\nObjective: Find articles about productivity")
        print("Starting URL: https://example.com")
        
        session2 = agent.browse(
            start_url="https://example.com",
            objective="Find and summarize articles about productivity",
            keywords=["blog", "article", "productivity"]
        )
        
        print(f"\nBrowsing Session: {session2.session_id}")
        print(f"Pages Visited: {len(session2.visited_urls)}")
        print(f"\nPages Explored:")
        for url in session2.visited_urls:
            print(f"  - {url}")
        
        findings2 = agent.synthesize_findings(session2)
        print(f"\nSynthesized Findings:")
        print(f"Summary: {findings2['summary']}")
        if findings2['insights']:
            print(f"\nInsights:")
            for insight in findings2['insights'][:2]:
                print(f"  - {insight}")
        print(f"\nConclusion: {findings2['conclusion']}")
        
        # Demo 3: Page Understanding
        print("\n" + "=" * 80)
        print("DEMO 3: Page Understanding")
        print("=" * 80)
        
        sample_page = agent.fetch_page("https://example.com/products/a")
        if sample_page:
            print(f"\nAnalyzing: {sample_page.title}")
            print(f"URL: {sample_page.url}")
            print(f"Type: {sample_page.page_type.value}")
            
            understanding = agent.understand_page(
                sample_page,
                "Extract product details and pricing"
            )
            
            print(f"\nPage Understanding:")
            print(f"Relevance: {understanding['relevance']}")
            print(f"Confidence: {understanding['confidence']:.2f}")
            print(f"\nKey Information:")
            for info in understanding['key_info'][:3]:
                print(f"  - {info}")
            print(f"\nSuggested Next Steps:")
            for step in understanding['next_steps'][:2]:
                print(f"  - {step}")
        
        # Demo 4: Link Evaluation
        print("\n" + "=" * 80)
        print("DEMO 4: Link Evaluation")
        print("=" * 80)
        
        test_links = [
            "https://example.com/products",
            "https://example.com/blog",
            "https://example.com/about",
            "https://example.com/contact"
        ]
        
        print("\nEvaluating links for objective: 'Find product information'")
        print("Links to evaluate:")
        for link in test_links:
            print(f"  - {link}")
        
        scored_links = agent.evaluate_links(
            test_links,
            "Find product information and pricing",
            set(),
            ["product", "price", "catalog"]
        )
        
        print(f"\nLink Rankings:")
        for url, score in scored_links:
            print(f"  {score:.2f} - {url}")
        
        # Demo 5: Information Extraction
        print("\n" + "=" * 80)
        print("DEMO 5: Information Extraction")
        print("=" * 80)
        
        product_page = agent.fetch_page("https://example.com/products/a")
        if product_page:
            print(f"\nExtracting from: {product_page.title}")
            
            extracted = agent.extract_information(
                product_page,
                "Extract product name, price, rating, and features",
                [ContentType.STRUCTURED_DATA, ContentType.TEXT]
            )
            
            print(f"\nExtracted Data (confidence: {extracted.confidence:.2f}):")
            for key, value in extracted.content.items():
                print(f"  {key}: {value}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        
        summary = """
    The Web Browsing Agent demonstrates intelligent web navigation and information
    extraction capabilities:
    
    KEY CAPABILITIES:
    1. Navigation Planning: Creates strategic browsing plans based on objectives
    2. Intelligent Link Following: Evaluates and prioritizes links by relevance
    3. Page Understanding: Analyzes page content and determines relevance
    4. Information Extraction: Extracts structured data from web pages
    5. Multi-Page Synthesis: Combines information from multiple sources
    
    BENEFITS:
    - Autonomous web research without manual navigation
    - Intelligent decision-making about where to browse
    - Structured data extraction from unstructured pages
    - Multi-source information aggregation
    - Objective-driven browsing strategies
    
    USE CASES:
    - Competitive intelligence gathering
    - Price monitoring and comparison
    - Content research and aggregation
    - Product information extraction
    - News monitoring and analysis
    - Academic research assistance
    - Job posting scraping
    - Real estate data collection
    - Market research
    - Documentation exploration
    
    PRODUCTION CONSIDERATIONS:
    1. Web Driver: Integrate with Selenium, Playwright, or Puppeteer for real browsing
    2. JavaScript Handling: Execute JavaScript for dynamic content
    3. Authentication: Support login, cookies, and session management
    4. Rate Limiting: Respect robots.txt and implement polite crawling
    5. Error Handling: Handle timeouts, 404s, CAPTCHAs gracefully
    6. Proxy Support: Rotate proxies for large-scale scraping
    7. Content Storage: Persist pages and data to databases
    8. Screenshot Capture: Take screenshots for visual analysis
    9. Form Interaction: Fill and submit forms programmatically
    10. Performance: Parallel browsing for efficiency
    
    ADVANCED EXTENSIONS:
    - Visual page understanding with computer vision
    - Natural language interaction for browsing commands
    - Automatic CAPTCHA solving
    - Multi-tab parallel browsing
    - Browser fingerprinting avoidance
    - Dynamic wait strategies for page loads
    - Content change monitoring
    - Recursive deep crawling with sitemaps
    - Structured data validation
    - API endpoint discovery
    
    The agent provides an intelligent layer on top of web automation, making
    decisions about navigation and extraction based on natural language objectives.
    """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_web_browsing_agent()
    
        

    Agentic Design Pattern: Web Browsing Agent

    Pattern 073: Web Browsing Agent – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Web Browsing Agent pattern implementation.


    Pattern Description

    Pattern 073: Web Browsing Agent

    Description: A Web Browsing Agent is a specialized AI agent designed to navigate websites, extract information, interact with web elements, and synthesize findings from multiple web sources. This pattern demonstrates how to build an intelligent agent that can autonomously browse the web, understand page content, follow links, fill forms, and extract structured information.

    The agent combines web navigation capabilities with natural language understanding
    to perform complex web-based research tasks. It can handle dynamic content,
    JavaScript-rendered pages, authentication, pagination, and multi-page workflows.
    Unlike simple web scrapers, this agent makes intelligent decisions about where
    to navigate based on task objectives.
    

    Components: 1. Navigation Controller: Manages page navigation and history 2. Content Extractor: Parses and extracts relevant information from pages 3. Element Interactor: Clicks buttons, fills forms, handles dropdowns 4. Link Analyzer: Evaluates and prioritizes links to follow 5. Session Manager: Handles cookies, authentication, state 6. Page Understanding: Interprets page structure and content semantically 7. Information Synthesizer: Combines data from multiple pages 8. Task Planner: Plans multi-step browsing strategies

    Key Features: – Intelligent link following based on task relevance – Dynamic content handling (JavaScript, AJAX) – Form filling and interaction – Multi-page information aggregation – Authentication and session management – Pagination handling – Screenshot and visual analysis – Content summarization – Structured data extraction – Recursive crawling with depth limits

    Use Cases: – Competitive intelligence gathering – Price monitoring and comparison – News aggregation and monitoring – Academic research and literature review – Job posting scraping – Real estate listing analysis – Product information extraction – Documentation exploration – Social media monitoring – API discovery and testing

    LangChain Implementation: Uses ChatOpenAI for page understanding and decision-making, simulates web interactions with structured data models representing browser state.


    File Statistics

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

    Module Documentation

    """
    Pattern 073: Web Browsing Agent
    
    Description:
        A Web Browsing Agent is a specialized AI agent designed to navigate websites,
        extract information, interact with web elements, and synthesize findings from
        multiple web sources. This pattern demonstrates how to build an intelligent agent
        that can autonomously browse the web, understand page content, follow links,
        fill forms, and extract structured information.
    
        The agent combines web navigation capabilities with natural language understanding
        to perform complex web-based research tasks. It can handle dynamic content,
        JavaScript-rendered pages, authentication, pagination, and multi-page workflows.
        Unlike simple web scrapers, this agent makes intelligent decisions about where
        to navigate based on task objectives.
    
    Components:
        1. Navigation Controller: Manages page navigation and history
        2. Content Extractor: Parses and extracts relevant information from pages
        3. Element Interactor: Clicks buttons, fills forms, handles dropdowns
        4. Link Analyzer: Evaluates and prioritizes links to follow
        5. Session Manager: Handles cookies, authentication, state
        6. Page Understanding: Interprets page structure and content semantically
        7. Information Synthesizer: Combines data from multiple pages
        8. Task Planner: Plans multi-step browsing strategies
    
    Key Features:
        - Intelligent link following based on task relevance
        - Dynamic content handling (JavaScript, AJAX)
        - Form filling and interaction
        - Multi-page information aggregation
        - Authentication and session management
        - Pagination handling
        - Screenshot and visual analysis
        - Content summarization
        - Structured data extraction
        - Recursive crawling with depth limits
    
    Use Cases:
        - Competitive intelligence gathering
        - Price monitoring and comparison
        - News aggregation and monitoring
        - Academic research and literature review
        - Job posting scraping
        - Real estate listing analysis
        - Product information extraction
        - Documentation exploration
        - Social media monitoring
        - API discovery and testing
    
    LangChain Implementation:
        Uses ChatOpenAI for page understanding and decision-making, simulates
        web interactions with structured data models representing browser state.
    """
    

    Purpose: This module implements the Web Browsing Agent pattern, providing a structured approach to agent design and implementation.


    Import Statements

    Standard Library Imports

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

    Purpose: Core Python functionality: – dataclasses.dataclass: Simplified class definitions with @dataclass – dataclasses.field: Simplified class definitions with @dataclass – datetime.datetime: Date and time handling – enum.Enum: Enumeration support – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – re: Regular expression operations – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Set: Type hints for better code documentation (List, Dict, Any, Optional) – 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: PageType

    Lines 71-81

    Inherits from: Enum

    Purpose:

    Types of web pages
    

    Class: ActionType

    Lines 84-93

    Inherits from: Enum

    Purpose:

    Types of web actions
    

    Class: ContentType

    Lines 96-104

    Inherits from: Enum

    Purpose:

    Types of content to extract
    

    Class: WebPage

    Lines 108-117

    Purpose:

    Represents a web page
    

    Class: BrowsingAction

    Lines 121-126

    Purpose:

    Represents a browsing action
    

    Class: ExtractedData

    Lines 130-136

    Purpose:

    Structured data extracted from a page
    

    Class: BrowsingSession

    Lines 140-149

    Purpose:

    State of a browsing session
    

    Class: NavigationPlan

    Lines 153-159

    Purpose:

    Plan for multi-page navigation
    

    Class: WebBrowsingAgent

    Lines 162-815

    Purpose:

    Agent for intelligent web browsing and information extraction.
    
    This agent can navigate websites autonomously, understand page content,
    make decisions about where to navigate, and extract relevant information.
    

    Methods:

    __init__(self, max_pages, max_depth)

    Lines 170-195

    Purpose:

    Initialize the web browsing agent.
    
    Args:
        max_pages: Maximum number of pages to visit per task
        max_depth: Maximum navigation depth from start page
    

    Parameters: – max_pages: Max pages – max_depth: Max depth

    _initialize_sample_pages(self)

    Lines 197-255

    Purpose:

    Initialize sample web pages for demonstration
    

    Parameters:

    create_navigation_plan(self, start_url, objective, keywords)

    Lines 257-361

    Purpose:

    Create a navigation plan for achieving the objective.
    
    Args:
        start_url: Starting URL
        objective: What to accomplish
        keywords: Important keywords to look for
    
    Returns:
        NavigationPlan with actions to take
    

    Parameters: – start_url: Start url – objective: Objective – keywords: Keywords

    Returns: NavigationPlan

    fetch_page(self, url)

    Lines 363-374

    Purpose:

    Fetch a web page (simulated).
    
    Args:
        url: URL to fetch
    
    Returns:
        WebPage object or None if not found
    

    Parameters: – url: Url

    Returns: <ast.Subscript object at 0x000001A06D849ED0>

    understand_page(self, page, objective)

    Lines 376-460

    Purpose:

    Understand page content and relevance to objective.
    
    Args:
        page: WebPage to analyze
        objective: Current objective
    
    Returns:
        Dictionary with page understanding
    

    Parameters: – page: Page – objective: Objective

    Returns: <ast.Subscript object at 0x000001A06D615ED0>

    evaluate_links(self, links, objective, visited, keywords)

    Lines 462-548

    Purpose:

    Evaluate and rank links by relevance to objective.
    
    Args:
        links: List of URLs to evaluate
        objective: Current objective
        visited: Set of already visited URLs
        keywords: Priority keywords
    
    Returns:
        List of (url, score) tuples, sorted by score
    

    Parameters: – links: Links – objective: Objective – visited: Visited – keywords: Keywords

    Returns: <ast.Subscript object at 0x000001A06D759510>

    extract_information(self, page, objective, content_types)

    Lines 550-638

    Purpose:

    Extract structured information from a page.
    
    Args:
        page: WebPage to extract from
        objective: What information to extract
        content_types: Types of content to extract
    
    Returns:
        ExtractedData with structured information
    

    Parameters: – page: Page – objective: Objective – content_types: Content types

    Returns: ExtractedData

    browse(self, start_url, objective, keywords)

    Lines 640-720

    Purpose:

    Browse the web to achieve the objective.
    
    Args:
        start_url: Starting URL
        objective: What to accomplish
        keywords: Optional priority keywords
    
    Returns:
        BrowsingSession with results
    

    Parameters: – start_url: Start url – objective: Objective – keywords: Keywords

    Returns: BrowsingSession

    synthesize_findings(self, session)

    Lines 722-815

    Purpose:

    Synthesize findings from browsing session.
    
    Args:
        session: BrowsingSession with extracted data
    
    Returns:
        Dictionary with synthesized findings
    

    Parameters: – session: Session

    Returns: <ast.Subscript object at 0x000001A06D7EED10>


    Functions

    demonstrate_web_browsing_agent()

    Lines 818-1024

    Purpose:

    Demonstrate the web browsing agent capabilities
    

    Key Concepts Summary

    Pattern Overview

    The Web Browsing Agent 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 072
    • Pattern 074

    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 Web Browsing Agent 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: 073_web_browsing_agent.py
    Total Lines: 1029
    Pattern Category: AI Agent Design Pattern #073


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Data Analysis Agent

    
        """
    Pattern 072: Data Analysis Agent
    
    Description:
        A Data Analysis Agent is a specialized AI agent designed to assist with data exploration,
        statistical analysis, visualization, and insight generation. This pattern demonstrates how
        to build an intelligent agent that can understand data-related queries, perform appropriate
        analyses, generate visualizations, and provide actionable insights from data.
        
        The agent combines natural language understanding with data science capabilities to make
        complex data analysis accessible through conversational interfaces. It can handle various
        data formats, perform exploratory data analysis (EDA), statistical tests, trend analysis,
        and generate meaningful visualizations.
    
    Components:
        1. Data Loading & Parsing: Handles various data formats (CSV, JSON, Excel, databases)
        2. Data Cleaning: Identifies and handles missing values, outliers, duplicates
        3. Exploratory Analysis: Computes statistics, distributions, correlations
        4. Statistical Testing: Performs hypothesis tests, significance testing
        5. Visualization Generation: Creates appropriate charts and graphs
        6. Insight Extraction: Identifies patterns, trends, and anomalies
        7. Natural Language Interface: Understands data-related questions
        8. Query Translation: Converts NL queries to data operations
    
    Key Features:
        - Automatic data profiling and quality assessment
        - Intelligent analysis method selection based on data characteristics
        - Statistical test recommendations and execution
        - Visualization selection based on data types and query intent
        - Natural language explanation of findings
        - Iterative exploration with memory of previous analyses
        - Support for multiple data sources and formats
        - Anomaly and outlier detection
        - Trend and pattern identification
        - Correlation and causation analysis
    
    Use Cases:
        - Business intelligence and reporting
        - Data exploration and hypothesis testing
        - Quality assurance and data validation
        - Market research and customer analytics
        - Scientific data analysis
        - Financial analysis and forecasting
        - Healthcare analytics
        - Educational data analysis
        - Interactive data dashboards
        - Automated reporting systems
    
    LangChain Implementation:
        Uses ChatOpenAI for analysis planning, pandas for data manipulation, and
        specialized LLM chains for insight generation and visualization recommendations.
    """
    
    import os
    import re
    import json
    from typing import List, Dict, Any, Optional, Tuple
    from dataclasses import dataclass, field
    from enum import Enum
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class AnalysisType(Enum):
        """Types of data analysis operations"""
        DESCRIPTIVE = "descriptive"
        EXPLORATORY = "exploratory"
        STATISTICAL = "statistical"
        CORRELATION = "correlation"
        TREND = "trend"
        COMPARISON = "comparison"
        DISTRIBUTION = "distribution"
        ANOMALY = "anomaly"
        PREDICTION = "prediction"
    
    
    class DataType(Enum):
        """Types of data variables"""
        NUMERICAL = "numerical"
        CATEGORICAL = "categorical"
        TEMPORAL = "temporal"
        TEXT = "text"
        BOOLEAN = "boolean"
    
    
    class VisualizationType(Enum):
        """Types of visualizations"""
        BAR_CHART = "bar_chart"
        LINE_CHART = "line_chart"
        SCATTER_PLOT = "scatter_plot"
        HISTOGRAM = "histogram"
        BOX_PLOT = "box_plot"
        HEATMAP = "heatmap"
        PIE_CHART = "pie_chart"
        TIME_SERIES = "time_series"
        VIOLIN_PLOT = "violin_plot"
    
    
    @dataclass
    class DataColumn:
        """Represents a data column with metadata"""
        name: str
        data_type: DataType
        missing_count: int
        unique_count: int
        sample_values: List[Any]
        statistics: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class DataProfile:
        """Profile of a dataset"""
        row_count: int
        column_count: int
        columns: List[DataColumn]
        missing_total: int
        duplicate_count: int
        memory_usage: str
        summary: str
    
    
    @dataclass
    class StatisticalTest:
        """Result of a statistical test"""
        test_name: str
        statistic: float
        p_value: float
        significant: bool
        interpretation: str
        confidence_level: float = 0.95
    
    
    @dataclass
    class AnalysisResult:
        """Result of a data analysis"""
        analysis_type: AnalysisType
        query: str
        findings: List[str]
        statistics: Dict[str, Any]
        visualizations: List[VisualizationType]
        insights: List[str]
        recommendations: List[str]
        confidence: float
    
    
    @dataclass
    class Visualization:
        """Specification for a visualization"""
        viz_type: VisualizationType
        title: str
        x_axis: Optional[str]
        y_axis: Optional[str]
        data_columns: List[str]
        description: str
        code: str  # Code to generate the visualization
    
    
    class DataAnalysisAgent:
        """
        Agent for comprehensive data analysis and exploration.
        
        This agent can understand natural language queries about data, perform
        appropriate analyses, generate insights, and recommend visualizations.
        """
        
        def __init__(self):
            """Initialize the data analysis agent with specialized LLMs"""
            # Profiler for data understanding
            self.profiler_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.1)
            
            # Analyzer for statistical analysis
            self.analyzer_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.2)
            
            # Insight generator for pattern identification
            self.insight_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.4)
            
            # Visualization recommender
            self.viz_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.3)
            
            # Simulated data store (in practice, would connect to real data sources)
            self.data_cache: Dict[str, Any] = {}
            self.analysis_history: List[AnalysisResult] = []
        
        def profile_data(self, data_description: str, sample_data: Dict[str, List[Any]]) -> DataProfile:
            """
            Profile a dataset to understand its structure and characteristics.
            
            Args:
                data_description: Description of the dataset
                sample_data: Sample data as dictionary of columns
                
            Returns:
                DataProfile with comprehensive dataset information
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a data profiling expert. Analyze the provided data sample 
                and generate a comprehensive profile including data types, quality issues, 
                and initial observations.
                
                Respond in this format:
                COLUMNS: For each column, provide: name|type|issues
                SUMMARY: Overall data summary
                QUALITY: Data quality assessment"""),
                ("user", """Dataset: {description}
                
    Sample Data:
    {sample}
    
    Analyze this data and provide a comprehensive profile.""")
            ])
            
            chain = prompt | self.profiler_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "description": data_description,
                    "sample": json.dumps(sample_data, indent=2)
                })
                
                # Parse the response
                columns = []
                summary = ""
                
                for line in response.split('\n'):
                    if line.startswith('COLUMNS:'):
                        continue
                    elif '|' in line and 'SUMMARY' not in line and 'QUALITY' not in line:
                        parts = line.split('|')
                        if len(parts) >= 3:
                            col_name = parts[0].strip()
                            col_type_str = parts[1].strip().lower()
                            
                            # Map string to enum
                            if 'number' in col_type_str or 'numeric' in col_type_str:
                                col_type = DataType.NUMERICAL
                            elif 'category' in col_type_str or 'categorical' in col_type_str:
                                col_type = DataType.CATEGORICAL
                            elif 'time' in col_type_str or 'date' in col_type_str:
                                col_type = DataType.TEMPORAL
                            elif 'text' in col_type_str:
                                col_type = DataType.TEXT
                            else:
                                col_type = DataType.CATEGORICAL
                            
                            # Get column data
                            col_data = sample_data.get(col_name, [])
                            missing = len([v for v in col_data if v is None or v == ''])
                            unique = len(set(col_data))
                            sample_vals = col_data[:5]
                            
                            columns.append(DataColumn(
                                name=col_name,
                                data_type=col_type,
                                missing_count=missing,
                                unique_count=unique,
                                sample_values=sample_vals
                            ))
                    elif 'SUMMARY:' in line:
                        summary = line.replace('SUMMARY:', '').strip()
                
                if not summary:
                    summary = "Data profile generated successfully"
                
                row_count = len(next(iter(sample_data.values()))) if sample_data else 0
                
                return DataProfile(
                    row_count=row_count,
                    column_count=len(columns),
                    columns=columns,
                    missing_total=sum(col.missing_count for col in columns),
                    duplicate_count=0,
                    memory_usage="N/A",
                    summary=summary
                )
                
            except Exception as e:
                # Fallback profiling
                columns = []
                for col_name, col_data in sample_data.items():
                    # Determine type
                    if all(isinstance(v, (int, float)) for v in col_data if v is not None):
                        col_type = DataType.NUMERICAL
                    else:
                        col_type = DataType.CATEGORICAL
                    
                    columns.append(DataColumn(
                        name=col_name,
                        data_type=col_type,
                        missing_count=len([v for v in col_data if v is None]),
                        unique_count=len(set(col_data)),
                        sample_values=col_data[:5]
                    ))
                
                return DataProfile(
                    row_count=len(next(iter(sample_data.values()))) if sample_data else 0,
                    column_count=len(columns),
                    columns=columns,
                    missing_total=sum(col.missing_count for col in columns),
                    duplicate_count=0,
                    memory_usage="N/A",
                    summary="Basic data profile generated"
                )
        
        def analyze_query(self, query: str, data_profile: DataProfile) -> AnalysisResult:
            """
            Analyze a natural language query about data.
            
            Args:
                query: Natural language question about the data
                data_profile: Profile of the dataset
                
            Returns:
                AnalysisResult with findings and recommendations
            """
            # Determine analysis type
            analysis_type = self._determine_analysis_type(query)
            
            # Create profile summary
            profile_summary = f"""Dataset: {data_profile.row_count} rows, {data_profile.column_count} columns
    Columns: {', '.join([f"{col.name} ({col.data_type.value})" for col in data_profile.columns])}
    Summary: {data_profile.summary}"""
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert data analyst. Analyze the query and dataset,
                then provide findings, insights, and recommendations.
                
                Respond in this format:
                FINDINGS: List key findings (one per line starting with -)
                STATISTICS: Key statistics as name:value pairs
                INSIGHTS: Deeper insights (one per line starting with -)
                RECOMMENDATIONS: Action recommendations (one per line starting with -)
                CONFIDENCE: Confidence level (0.0-1.0)"""),
                ("user", """Query: {query}
                
    Data Profile:
    {profile}
    
    Analysis Type: {analysis_type}
    
    Provide a comprehensive analysis.""")
            ])
            
            chain = prompt | self.analyzer_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "query": query,
                    "profile": profile_summary,
                    "analysis_type": analysis_type.value
                })
                
                # Parse response
                findings = []
                statistics = {}
                insights = []
                recommendations = []
                confidence = 0.8
                
                current_section = None
                
                for line in response.split('\n'):
                    line = line.strip()
                    if not line:
                        continue
                    
                    if line.startswith('FINDINGS:'):
                        current_section = 'findings'
                    elif line.startswith('STATISTICS:'):
                        current_section = 'statistics'
                    elif line.startswith('INSIGHTS:'):
                        current_section = 'insights'
                    elif line.startswith('RECOMMENDATIONS:'):
                        current_section = 'recommendations'
                    elif line.startswith('CONFIDENCE:'):
                        try:
                            confidence = float(re.findall(r'[\d.]+', line)[0])
                        except:
                            confidence = 0.8
                    elif line.startswith('-'):
                        content = line[1:].strip()
                        if current_section == 'findings':
                            findings.append(content)
                        elif current_section == 'insights':
                            insights.append(content)
                        elif current_section == 'recommendations':
                            recommendations.append(content)
                    elif ':' in line and current_section == 'statistics':
                        parts = line.split(':', 1)
                        if len(parts) == 2:
                            key = parts[0].strip()
                            value = parts[1].strip()
                            statistics[key] = value
                
                # Recommend visualizations
                visualizations = self._recommend_visualizations(
                    query, analysis_type, data_profile
                )
                
                return AnalysisResult(
                    analysis_type=analysis_type,
                    query=query,
                    findings=findings if findings else ["Analysis completed successfully"],
                    statistics=statistics,
                    visualizations=visualizations,
                    insights=insights if insights else ["Data analysis reveals interesting patterns"],
                    recommendations=recommendations if recommendations else ["Consider further analysis"],
                    confidence=confidence
                )
                
            except Exception as e:
                return AnalysisResult(
                    analysis_type=analysis_type,
                    query=query,
                    findings=[f"Analysis performed for: {query}"],
                    statistics={},
                    visualizations=[VisualizationType.BAR_CHART],
                    insights=["Analysis completed with basic insights"],
                    recommendations=["Review findings and perform deeper analysis"],
                    confidence=0.6
                )
        
        def perform_statistical_test(
            self,
            test_type: str,
            data_description: str,
            sample_data: Dict[str, List[Any]]
        ) -> StatisticalTest:
            """
            Perform a statistical test on the data.
            
            Args:
                test_type: Type of test (t-test, chi-square, ANOVA, etc.)
                data_description: Description of what's being tested
                sample_data: Data for the test
                
            Returns:
                StatisticalTest with results and interpretation
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a statistical analysis expert. Perform the requested
                statistical test and provide interpretation.
                
                Respond in this format:
                TEST: Test name
                STATISTIC: Test statistic value
                P_VALUE: P-value
                SIGNIFICANT: Yes/No (at 0.05 level)
                INTERPRETATION: What the results mean"""),
                ("user", """Test Type: {test_type}
                
    Description: {description}
    
    Sample Data:
    {sample}
    
    Perform the statistical test and interpret results.""")
            ])
            
            chain = prompt | self.analyzer_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "test_type": test_type,
                    "description": data_description,
                    "sample": json.dumps(sample_data, indent=2)
                })
                
                # Parse response
                test_name = test_type
                statistic = 0.0
                p_value = 0.05
                significant = False
                interpretation = ""
                
                for line in response.split('\n'):
                    if line.startswith('TEST:'):
                        test_name = line.replace('TEST:', '').strip()
                    elif line.startswith('STATISTIC:'):
                        try:
                            statistic = float(re.findall(r'[-\d.]+', line)[0])
                        except:
                            pass
                    elif line.startswith('P_VALUE:') or line.startswith('P-VALUE:'):
                        try:
                            p_value = float(re.findall(r'[\d.]+', line)[0])
                        except:
                            pass
                    elif line.startswith('SIGNIFICANT:'):
                        significant = 'yes' in line.lower()
                    elif line.startswith('INTERPRETATION:'):
                        interpretation = line.replace('INTERPRETATION:', '').strip()
                
                if not interpretation:
                    interpretation = f"Statistical test performed with p-value of {p_value}"
                
                return StatisticalTest(
                    test_name=test_name,
                    statistic=statistic,
                    p_value=p_value,
                    significant=significant,
                    interpretation=interpretation,
                    confidence_level=0.95
                )
                
            except Exception as e:
                return StatisticalTest(
                    test_name=test_type,
                    statistic=0.0,
                    p_value=0.05,
                    significant=False,
                    interpretation=f"Statistical test performed: {test_type}",
                    confidence_level=0.95
                )
        
        def generate_insights(
            self,
            data_profile: DataProfile,
            analysis_results: List[AnalysisResult]
        ) -> List[str]:
            """
            Generate high-level insights from multiple analyses.
            
            Args:
                data_profile: Profile of the dataset
                analysis_results: Previous analysis results
                
            Returns:
                List of insights
            """
            # Compile analysis summary
            analyses_summary = "\n".join([
                f"- {result.query}: {', '.join(result.findings[:2])}"
                for result in analysis_results[-5:]  # Last 5 analyses
            ])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a senior data scientist. Based on the dataset profile
                and previous analyses, generate high-level strategic insights.
                
                Provide 3-5 key insights, one per line starting with -"""),
                ("user", """Data Profile: {profile}
    
    Previous Analyses:
    {analyses}
    
    Generate strategic insights about this data.""")
            ])
            
            chain = prompt | self.insight_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "profile": data_profile.summary,
                    "analyses": analyses_summary if analyses_summary else "No previous analyses"
                })
                
                insights = []
                for line in response.split('\n'):
                    if line.strip().startswith('-'):
                        insights.append(line.strip()[1:].strip())
                
                return insights if insights else [
                    "Data shows interesting patterns worthy of investigation",
                    "Further analysis recommended to validate findings",
                    "Consider collecting additional data for deeper insights"
                ]
                
            except Exception as e:
                return [
                    "Data analysis completed successfully",
                    "Multiple dimensions of the data have been explored",
                    "Additional investigation may reveal more patterns"
                ]
        
        def recommend_visualization(
            self,
            analysis_result: AnalysisResult,
            data_profile: DataProfile
        ) -> Visualization:
            """
            Recommend and generate a visualization for analysis results.
            
            Args:
                analysis_result: Result of the analysis
                data_profile: Profile of the dataset
                
            Returns:
                Visualization specification
            """
            viz_type = analysis_result.visualizations[0] if analysis_result.visualizations else VisualizationType.BAR_CHART
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a data visualization expert. Create a visualization
                specification for the analysis results.
                
                Respond in this format:
                TITLE: Chart title
                X_AXIS: X-axis column
                Y_AXIS: Y-axis column
                COLUMNS: Columns to visualize (comma-separated)
                DESCRIPTION: Brief description
                CODE: Python code using matplotlib/seaborn"""),
                ("user", """Analysis Query: {query}
    Visualization Type: {viz_type}
    Findings: {findings}
    
    Available Columns: {columns}
    
    Create visualization specification.""")
            ])
            
            chain = prompt | self.viz_llm | StrOutputParser()
            
            try:
                response = chain.invoke({
                    "query": analysis_result.query,
                    "viz_type": viz_type.value,
                    "findings": ", ".join(analysis_result.findings[:3]),
                    "columns": ", ".join([col.name for col in data_profile.columns])
                })
                
                # Parse response
                title = f"{viz_type.value.replace('_', ' ').title()} Visualization"
                x_axis = None
                y_axis = None
                data_columns = []
                description = ""
                code = "# Visualization code would go here"
                
                in_code = False
                code_lines = []
                
                for line in response.split('\n'):
                    if line.startswith('TITLE:'):
                        title = line.replace('TITLE:', '').strip()
                    elif line.startswith('X_AXIS:'):
                        x_axis = line.replace('X_AXIS:', '').strip()
                    elif line.startswith('Y_AXIS:'):
                        y_axis = line.replace('Y_AXIS:', '').strip()
                    elif line.startswith('COLUMNS:'):
                        cols = line.replace('COLUMNS:', '').strip()
                        data_columns = [c.strip() for c in cols.split(',')]
                    elif line.startswith('DESCRIPTION:'):
                        description = line.replace('DESCRIPTION:', '').strip()
                    elif line.startswith('CODE:'):
                        in_code = True
                    elif in_code:
                        code_lines.append(line)
                
                if code_lines:
                    code = '\n'.join(code_lines)
                
                if not description:
                    description = f"Visualization showing {analysis_result.query}"
                
                return Visualization(
                    viz_type=viz_type,
                    title=title,
                    x_axis=x_axis,
                    y_axis=y_axis,
                    data_columns=data_columns if data_columns else [data_profile.columns[0].name],
                    description=description,
                    code=code
                )
                
            except Exception as e:
                return Visualization(
                    viz_type=viz_type,
                    title=f"{viz_type.value.replace('_', ' ').title()}",
                    x_axis=data_profile.columns[0].name if data_profile.columns else None,
                    y_axis=data_profile.columns[1].name if len(data_profile.columns) > 1 else None,
                    data_columns=[col.name for col in data_profile.columns[:2]],
                    description=f"Visualization for {analysis_result.query}",
                    code="# Visualization code"
                )
        
        def _determine_analysis_type(self, query: str) -> AnalysisType:
            """Determine the type of analysis from the query"""
            query_lower = query.lower()
            
            if any(word in query_lower for word in ['describe', 'summary', 'overview', 'statistics']):
                return AnalysisType.DESCRIPTIVE
            elif any(word in query_lower for word in ['explore', 'distribution', 'pattern']):
                return AnalysisType.EXPLORATORY
            elif any(word in query_lower for word in ['test', 'significance', 'hypothesis']):
                return AnalysisType.STATISTICAL
            elif any(word in query_lower for word in ['correlation', 'relationship', 'association']):
                return AnalysisType.CORRELATION
            elif any(word in query_lower for word in ['trend', 'over time', 'temporal']):
                return AnalysisType.TREND
            elif any(word in query_lower for word in ['compare', 'difference', 'versus']):
                return AnalysisType.COMPARISON
            elif any(word in query_lower for word in ['anomaly', 'outlier', 'unusual']):
                return AnalysisType.ANOMALY
            elif any(word in query_lower for word in ['predict', 'forecast', 'future']):
                return AnalysisType.PREDICTION
            else:
                return AnalysisType.EXPLORATORY
        
        def _recommend_visualizations(
            self,
            query: str,
            analysis_type: AnalysisType,
            data_profile: DataProfile
        ) -> List[VisualizationType]:
            """Recommend appropriate visualizations"""
            visualizations = []
            
            if analysis_type == AnalysisType.DESCRIPTIVE:
                visualizations = [VisualizationType.BAR_CHART, VisualizationType.BOX_PLOT]
            elif analysis_type == AnalysisType.EXPLORATORY:
                visualizations = [VisualizationType.HISTOGRAM, VisualizationType.SCATTER_PLOT]
            elif analysis_type == AnalysisType.CORRELATION:
                visualizations = [VisualizationType.HEATMAP, VisualizationType.SCATTER_PLOT]
            elif analysis_type == AnalysisType.TREND:
                visualizations = [VisualizationType.LINE_CHART, VisualizationType.TIME_SERIES]
            elif analysis_type == AnalysisType.COMPARISON:
                visualizations = [VisualizationType.BAR_CHART, VisualizationType.BOX_PLOT]
            elif analysis_type == AnalysisType.DISTRIBUTION:
                visualizations = [VisualizationType.HISTOGRAM, VisualizationType.VIOLIN_PLOT]
            else:
                visualizations = [VisualizationType.BAR_CHART]
            
            return visualizations
    
    
    def demonstrate_data_analysis_agent():
        """Demonstrate the data analysis agent capabilities"""
        print("=" * 80)
        print("DATA ANALYSIS AGENT DEMONSTRATION")
        print("=" * 80)
        
        agent = DataAnalysisAgent()
        
        # Demo 1: Data Profiling
        print("\n" + "=" * 80)
        print("DEMO 1: Data Profiling")
        print("=" * 80)
        
        sample_sales_data = {
            "product": ["A", "B", "C", "A", "B", "C", "A", "B"],
            "sales": [100, 150, 200, 120, 160, 180, 110, 155],
            "region": ["North", "South", "East", "West", "North", "South", "East", "West"],
            "date": ["2024-01", "2024-01", "2024-01", "2024-02", "2024-02", "2024-02", "2024-03", "2024-03"]
        }
        
        print("\nProfiling sales dataset...")
        profile = agent.profile_data(
            "Monthly sales data by product and region",
            sample_sales_data
        )
        
        print(f"\nData Profile:")
        print(f"- Rows: {profile.row_count}")
        print(f"- Columns: {profile.column_count}")
        print(f"- Missing values: {profile.missing_total}")
        print(f"\nColumns:")
        for col in profile.columns:
            print(f"  - {col.name} ({col.data_type.value}): {col.unique_count} unique values")
        print(f"\nSummary: {profile.summary}")
        
        # Demo 2: Query Analysis
        print("\n" + "=" * 80)
        print("DEMO 2: Query Analysis")
        print("=" * 80)
        
        queries = [
            "What are the average sales by product?",
            "Which region has the highest sales?",
            "Is there a correlation between product type and sales volume?"
        ]
        
        for query in queries:
            print(f"\nQuery: {query}")
            result = agent.analyze_query(query, profile)
            
            print(f"Analysis Type: {result.analysis_type.value}")
            print(f"Confidence: {result.confidence:.2f}")
            print(f"\nFindings:")
            for finding in result.findings[:3]:
                print(f"  - {finding}")
            
            if result.insights:
                print(f"\nInsights:")
                for insight in result.insights[:2]:
                    print(f"  - {insight}")
            
            if result.recommendations:
                print(f"\nRecommendations:")
                for rec in result.recommendations[:2]:
                    print(f"  - {rec}")
            
            print(f"\nRecommended Visualizations:")
            for viz in result.visualizations[:2]:
                print(f"  - {viz.value}")
            
            agent.analysis_history.append(result)
        
        # Demo 3: Statistical Testing
        print("\n" + "=" * 80)
        print("DEMO 3: Statistical Testing")
        print("=" * 80)
        
        test_data = {
            "group_a": [100, 105, 110, 95, 102],
            "group_b": [150, 145, 155, 140, 148]
        }
        
        print("\nPerforming t-test on sales groups...")
        test_result = agent.perform_statistical_test(
            "t-test",
            "Comparing sales between two product groups",
            test_data
        )
        
        print(f"\nTest: {test_result.test_name}")
        print(f"Statistic: {test_result.statistic:.4f}")
        print(f"P-value: {test_result.p_value:.4f}")
        print(f"Significant at {test_result.confidence_level:.0%} level: {test_result.significant}")
        print(f"Interpretation: {test_result.interpretation}")
        
        # Demo 4: Insight Generation
        print("\n" + "=" * 80)
        print("DEMO 4: High-Level Insights")
        print("=" * 80)
        
        print("\nGenerating strategic insights from all analyses...")
        insights = agent.generate_insights(profile, agent.analysis_history)
        
        print("\nStrategic Insights:")
        for i, insight in enumerate(insights, 1):
            print(f"{i}. {insight}")
        
        # Demo 5: Visualization Recommendation
        print("\n" + "=" * 80)
        print("DEMO 5: Visualization Recommendation")
        print("=" * 80)
        
        if agent.analysis_history:
            latest_analysis = agent.analysis_history[0]
            print(f"\nCreating visualization for: {latest_analysis.query}")
            
            viz = agent.recommend_visualization(latest_analysis, profile)
            
            print(f"\nVisualization Specification:")
            print(f"Type: {viz.viz_type.value}")
            print(f"Title: {viz.title}")
            print(f"X-axis: {viz.x_axis}")
            print(f"Y-axis: {viz.y_axis}")
            print(f"Columns: {', '.join(viz.data_columns)}")
            print(f"Description: {viz.description}")
            print(f"\nCode Preview:")
            code_lines = viz.code.split('\n')[:5]
            for line in code_lines:
                print(f"  {line}")
            if len(viz.code.split('\n')) > 5:
                print("  ...")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        
        summary = """
    The Data Analysis Agent demonstrates comprehensive capabilities for automated
    data analysis and exploration:
    
    KEY CAPABILITIES:
    1. Data Profiling: Automatically analyzes dataset structure, types, and quality
    2. Natural Language Queries: Understands and answers data questions in plain English
    3. Statistical Analysis: Performs appropriate statistical tests and interprets results
    4. Insight Generation: Identifies patterns, trends, and actionable insights
    5. Visualization Recommendations: Suggests and generates appropriate visualizations
    
    BENEFITS:
    - Makes data analysis accessible to non-technical users
    - Accelerates exploratory data analysis workflows
    - Provides consistent, comprehensive analysis
    - Generates actionable insights automatically
    - Recommends appropriate analytical methods
    
    USE CASES:
    - Business intelligence dashboards
    - Automated reporting systems
    - Data quality monitoring
    - Research data exploration
    - Customer analytics
    - Market research analysis
    - Scientific data analysis
    - Educational data tools
    
    PRODUCTION CONSIDERATIONS:
    1. Integration: Connect to real databases, data lakes, and APIs
    2. Visualization: Implement actual chart generation with matplotlib/plotly
    3. Statistical Libraries: Use scipy, statsmodels for robust statistical tests
    4. Performance: Optimize for large datasets with sampling and caching
    5. Security: Implement data access controls and query validation
    6. Scalability: Support distributed processing for big data
    7. Memory Management: Handle large datasets efficiently
    8. Validation: Verify statistical assumptions before tests
    9. Interpretability: Provide clear explanations for all findings
    10. Interactive Mode: Support iterative exploration with follow-up questions
    
    ADVANCED EXTENSIONS:
    - Machine learning model training and evaluation
    - Time series forecasting and anomaly detection
    - Causal inference and A/B test analysis
    - Automated feature engineering
    - Multi-dataset joins and comparisons
    - Real-time streaming data analysis
    - Custom metric and KPI tracking
    - Export to notebooks and reports
    - Natural language report generation
    - Interactive dashboard creation
    
    The agent combines LLM intelligence with data science capabilities to make
    sophisticated analysis accessible through natural conversation.
    """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_data_analysis_agent()
    
        

    Agentic Design Pattern: Data Analysis Agent

    Pattern 072: Data Analysis Agent – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Data Analysis Agent pattern implementation.


    Pattern Description

    Pattern 072: Data Analysis Agent

    Description: A Data Analysis Agent is a specialized AI agent designed to assist with data exploration, statistical analysis, visualization, and insight generation. This pattern demonstrates how to build an intelligent agent that can understand data-related queries, perform appropriate analyses, generate visualizations, and provide actionable insights from data.

    The agent combines natural language understanding with data science capabilities to make
    complex data analysis accessible through conversational interfaces. It can handle various
    data formats, perform exploratory data analysis (EDA), statistical tests, trend analysis,
    and generate meaningful visualizations.
    

    Components: 1. Data Loading & Parsing: Handles various data formats (CSV, JSON, Excel, databases) 2. Data Cleaning: Identifies and handles missing values, outliers, duplicates 3. Exploratory Analysis: Computes statistics, distributions, correlations 4. Statistical Testing: Performs hypothesis tests, significance testing 5. Visualization Generation: Creates appropriate charts and graphs 6. Insight Extraction: Identifies patterns, trends, and anomalies 7. Natural Language Interface: Understands data-related questions 8. Query Translation: Converts NL queries to data operations

    Key Features: – Automatic data profiling and quality assessment – Intelligent analysis method selection based on data characteristics – Statistical test recommendations and execution – Visualization selection based on data types and query intent – Natural language explanation of findings – Iterative exploration with memory of previous analyses – Support for multiple data sources and formats – Anomaly and outlier detection – Trend and pattern identification – Correlation and causation analysis

    Use Cases: – Business intelligence and reporting – Data exploration and hypothesis testing – Quality assurance and data validation – Market research and customer analytics – Scientific data analysis – Financial analysis and forecasting – Healthcare analytics – Educational data analysis – Interactive data dashboards – Automated reporting systems

    LangChain Implementation: Uses ChatOpenAI for analysis planning, pandas for data manipulation, and specialized LLM chains for insight generation and visualization recommendations.


    File Statistics

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

    Module Documentation

    """
    Pattern 072: Data Analysis Agent
    
    Description:
        A Data Analysis Agent is a specialized AI agent designed to assist with data exploration,
        statistical analysis, visualization, and insight generation. This pattern demonstrates how
        to build an intelligent agent that can understand data-related queries, perform appropriate
        analyses, generate visualizations, and provide actionable insights from data.
    
        The agent combines natural language understanding with data science capabilities to make
        complex data analysis accessible through conversational interfaces. It can handle various
        data formats, perform exploratory data analysis (EDA), statistical tests, trend analysis,
        and generate meaningful visualizations.
    
    Components:
        1. Data Loading & Parsing: Handles various data formats (CSV, JSON, Excel, databases)
        2. Data Cleaning: Identifies and handles missing values, outliers, duplicates
        3. Exploratory Analysis: Computes statistics, distributions, correlations
        4. Statistical Testing: Performs hypothesis tests, significance testing
        5. Visualization Generation: Creates appropriate charts and graphs
        6. Insight Extraction: Identifies patterns, trends, and anomalies
        7. Natural Language Interface: Understands data-related questions
        8. Query Translation: Converts NL queries to data operations
    
    Key Features:
        - Automatic data profiling and quality assessment
        - Intelligent analysis method selection based on data characteristics
        - Statistical test recommendations and execution
        - Visualization selection based on data types and query intent
        - Natural language explanation of findings
        - Iterative exploration with memory of previous analyses
        - Support for multiple data sources and formats
        - Anomaly and outlier detection
        - Trend and pattern identification
        - Correlation and causation analysis
    
    Use Cases:
        - Business intelligence and reporting
        - Data exploration and hypothesis testing
        - Quality assurance and data validation
        - Market research and customer analytics
        - Scientific data analysis
        - Financial analysis and forecasting
        - Healthcare analytics
        - Educational data analysis
        - Interactive data dashboards
        - Automated reporting systems
    
    LangChain Implementation:
        Uses ChatOpenAI for analysis planning, pandas for data manipulation, and
        specialized LLM chains for insight generation and visualization recommendations.
    """
    

    Purpose: This module implements the Data Analysis Agent 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 re
    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 – 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 – re: Regular expression 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: AnalysisType

    Lines 69-79

    Inherits from: Enum

    Purpose:

    Types of data analysis operations
    

    Class: DataType

    Lines 82-88

    Inherits from: Enum

    Purpose:

    Types of data variables
    

    Class: VisualizationType

    Lines 91-101

    Inherits from: Enum

    Purpose:

    Types of visualizations
    

    Class: DataColumn

    Lines 105-112

    Purpose:

    Represents a data column with metadata
    

    Class: DataProfile

    Lines 116-124

    Purpose:

    Profile of a dataset
    

    Class: StatisticalTest

    Lines 128-135

    Purpose:

    Result of a statistical test
    

    Class: AnalysisResult

    Lines 139-148

    Purpose:

    Result of a data analysis
    

    Class: Visualization

    Lines 152-160

    Purpose:

    Specification for a visualization
    

    Class: DataAnalysisAgent

    Lines 163-729

    Purpose:

    Agent for comprehensive data analysis and exploration.
    
    This agent can understand natural language queries about data, perform
    appropriate analyses, generate insights, and recommend visualizations.
    

    Methods:

    __init__(self)

    Lines 171-187

    Purpose:

    Initialize the data analysis agent with specialized LLMs
    

    Parameters:

    profile_data(self, data_description, sample_data)

    Lines 189-307

    Purpose:

    Profile a dataset to understand its structure and characteristics.
    
    Args:
        data_description: Description of the dataset
        sample_data: Sample data as dictionary of columns
    
    Returns:
        DataProfile with comprehensive dataset information
    

    Parameters: – data_description: Data description – sample_data: Sample data

    Returns: DataProfile

    analyze_query(self, query, data_profile)

    Lines 309-425

    Purpose:

    Analyze a natural language query about data.
    
    Args:
        query: Natural language question about the data
        data_profile: Profile of the dataset
    
    Returns:
        AnalysisResult with findings and recommendations
    

    Parameters: – query: Input query or question – data_profile: Data profile

    Returns: AnalysisResult

    perform_statistical_test(self, test_type, data_description, sample_data)

    Lines 427-518

    Purpose:

    Perform a statistical test on the data.
    
    Args:
        test_type: Type of test (t-test, chi-square, ANOVA, etc.)
        data_description: Description of what's being tested
        sample_data: Data for the test
    
    Returns:
        StatisticalTest with results and interpretation
    

    Parameters: – test_type: Test type – data_description: Data description – sample_data: Sample data

    Returns: StatisticalTest

    generate_insights(self, data_profile, analysis_results)

    Lines 520-578

    Purpose:

    Generate high-level insights from multiple analyses.
    
    Args:
        data_profile: Profile of the dataset
        analysis_results: Previous analysis results
    
    Returns:
        List of insights
    

    Parameters: – data_profile: Data profile – analysis_results: Analysis results

    Returns: <ast.Subscript object at 0x000001A06D6F4550>

    recommend_visualization(self, analysis_result, data_profile)

    Lines 580-680

    Purpose:

    Recommend and generate a visualization for analysis results.
    
    Args:
        analysis_result: Result of the analysis
        data_profile: Profile of the dataset
    
    Returns:
        Visualization specification
    

    Parameters: – analysis_result: Analysis result – data_profile: Data profile

    Returns: Visualization

    _determine_analysis_type(self, query)

    Lines 682-703

    Purpose:

    Determine the type of analysis from the query
    

    Parameters: – query: Input query or question

    Returns: AnalysisType

    _recommend_visualizations(self, query, analysis_type, data_profile)

    Lines 705-729

    Purpose:

    Recommend appropriate visualizations
    

    Parameters: – query: Input query or question – analysis_type: Analysis type – data_profile: Data profile

    Returns: <ast.Subscript object at 0x000001A06D661C90>


    Functions

    demonstrate_data_analysis_agent()

    Lines 732-925

    Purpose:

    Demonstrate the data analysis agent capabilities
    

    Key Concepts Summary

    Pattern Overview

    The Data Analysis Agent 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 071
    • Pattern 073

    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 Data Analysis Agent 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: 072_data_analysis_agent.py
    Total Lines: 930
    Pattern Category: AI Agent Design Pattern #072


    Generated from actual implementation code
    For educational and reference purposes