Tag: llm

  • Agentic Design Pattern: Collaborative Planning

    
        """
    Pattern 108: Collaborative Planning
    
    Description:
        The Collaborative Planning pattern enables multiple agents to work together
        in creating and executing plans toward shared goals. Rather than planning in
        isolation, agents coordinate their planning efforts, share information, divide
        tasks, resolve conflicts, and synchronize their actions to achieve objectives
        that would be difficult or impossible for individual agents.
        
        Collaborative planning is essential for multi-agent systems where agents have
        complementary capabilities, shared resources, or interdependent goals. The
        pattern addresses challenges like distributed knowledge, conflicting preferences,
        resource contention, and coordination overhead.
        
        This pattern includes mechanisms for goal sharing, task decomposition and
        allocation, plan merging, conflict resolution, resource negotiation, and
        synchronized execution. Agents may use various collaboration strategies from
        centralized coordination to fully distributed consensus-based planning.
    
    Key Components:
        1. Shared Goal Repository: Common objectives
        2. Task Allocator: Assigns tasks to agents
        3. Plan Merger: Combines individual plans
        4. Conflict Resolver: Handles plan conflicts
        5. Resource Negotiator: Allocates shared resources
        6. Synchronization Manager: Coordinates timing
        7. Communication Protocol: Agent interaction
    
    Collaboration Strategies:
        1. Centralized: Single coordinator assigns tasks
        2. Hierarchical: Tree of coordinators
        3. Distributed: Peer-to-peer negotiation
        4. Auction-based: Agents bid for tasks
        5. Consensus: Democratic decision making
        6. Blackboard: Shared workspace
    
    Planning Phases:
        1. Goal Identification: Define shared objectives
        2. Capability Sharing: Agents advertise skills
        3. Task Decomposition: Break into subtasks
        4. Task Allocation: Assign to appropriate agents
        5. Plan Integration: Merge individual plans
        6. Conflict Resolution: Resolve incompatibilities
        7. Synchronized Execution: Coordinate timing
    
    Use Cases:
        - Multi-robot coordination
        - Distributed project management
        - Supply chain planning
        - Military operations planning
        - Smart city management
        - Collaborative problem solving
        - Team-based game AI
    
    Advantages:
        - Leverages diverse capabilities
        - Handles complex tasks beyond single agent
        - Load balancing across agents
        - Fault tolerance through redundancy
        - Parallel execution speedup
        - Shared knowledge utilization
        - Flexible task distribution
    
    Challenges:
        - Communication overhead
        - Conflict resolution complexity
        - Synchronization requirements
        - Trust and verification
        - Resource contention
        - Plan consistency maintenance
        - Scalability with agent count
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based task decomposition
        - Agent capability matching
        - Plan integration strategies
        - Natural language coordination
        
    Production Considerations:
        - Implement efficient communication protocols
        - Use asynchronous messaging
        - Handle agent failures gracefully
        - Monitor collaboration overhead
        - Implement timeout mechanisms
        - Log all coordination decisions
        - Support dynamic agent addition/removal
        - Enable human oversight
    """
    
    import os
    from typing import List, Dict, Any, Optional, Set
    from dataclasses import dataclass, field
    from datetime import datetime
    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 TaskStatus(Enum):
        """Status of a task."""
        UNASSIGNED = "unassigned"
        ASSIGNED = "assigned"
        IN_PROGRESS = "in_progress"
        COMPLETED = "completed"
        FAILED = "failed"
    
    
    class MessageType(Enum):
        """Types of coordination messages."""
        TASK_OFFER = "task_offer"
        TASK_BID = "task_bid"
        TASK_ASSIGNMENT = "task_assignment"
        RESOURCE_REQUEST = "resource_request"
        RESOURCE_GRANT = "resource_grant"
        STATUS_UPDATE = "status_update"
        PLAN_PROPOSAL = "plan_proposal"
    
    
    @dataclass
    class Task:
        """
        Represents a task in collaborative plan.
        
        Attributes:
            task_id: Unique identifier
            description: Task description
            required_capabilities: Capabilities needed
            estimated_effort: Effort estimate
            dependencies: Tasks that must complete first
            assigned_to: Agent ID assigned to
            status: Current status
            priority: Task priority
        """
        task_id: str
        description: str
        required_capabilities: List[str] = field(default_factory=list)
        estimated_effort: float = 1.0
        dependencies: List[str] = field(default_factory=list)
        assigned_to: Optional[str] = None
        status: TaskStatus = TaskStatus.UNASSIGNED
        priority: int = 5
        started_at: Optional[datetime] = None
        completed_at: Optional[datetime] = None
    
    
    @dataclass
    class CollaborativeAgent:
        """
        Agent participating in collaborative planning.
        
        Attributes:
            agent_id: Unique identifier
            name: Agent name
            capabilities: List of capabilities
            capacity: Available work capacity
            current_load: Current workload
            assigned_tasks: Tasks assigned to this agent
        """
        agent_id: str
        name: str
        capabilities: List[str] = field(default_factory=list)
        capacity: float = 10.0
        current_load: float = 0.0
        assigned_tasks: List[str] = field(default_factory=list)
        
        def can_handle(self, task: Task) -> bool:
            """Check if agent can handle a task."""
            # Check capabilities
            has_capabilities = all(
                cap in self.capabilities for cap in task.required_capabilities
            )
            # Check capacity
            has_capacity = (self.current_load + task.estimated_effort)  float:
            """Get remaining capacity."""
            return self.capacity - self.current_load
    
    
    @dataclass
    class Message:
        """
        Communication message between agents.
        
        Attributes:
            sender_id: Sending agent ID
            receiver_id: Receiving agent ID (None for broadcast)
            message_type: Type of message
            content: Message payload
            timestamp: When sent
        """
        sender_id: str
        receiver_id: Optional[str]
        message_type: MessageType
        content: Dict[str, Any]
        timestamp: datetime = field(default_factory=datetime.now)
    
    
    class CollaborativePlanner:
        """
        Collaborative planning system for multi-agent coordination.
        
        This planner enables agents to work together in planning and
        executing tasks toward shared goals.
        """
        
        def __init__(self, temperature: float = 0.4):
            """
            Initialize collaborative planner.
            
            Args:
                temperature: LLM temperature
            """
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.agents: Dict[str, CollaborativeAgent] = {}
            self.tasks: Dict[str, Task] = {}
            self.messages: List[Message] = []
            self.task_counter = 0
        
        def add_agent(
            self,
            agent_id: str,
            name: str,
            capabilities: List[str],
            capacity: float = 10.0
        ) -> CollaborativeAgent:
            """
            Add agent to the planning system.
            
            Args:
                agent_id: Agent identifier
                name: Agent name
                capabilities: Agent capabilities
                capacity: Work capacity
                
            Returns:
                Created agent
            """
            agent = CollaborativeAgent(
                agent_id=agent_id,
                name=name,
                capabilities=capabilities,
                capacity=capacity
            )
            self.agents[agent_id] = agent
            return agent
        
        def decompose_goal(self, goal: str, num_tasks: int = 5) -> List[Task]:
            """
            Decompose goal into tasks using LLM.
            
            Args:
                goal: Goal description
                num_tasks: Number of tasks to create
                
            Returns:
                List of tasks
            """
            prompt = ChatPromptTemplate.from_template(
                "Decompose this goal into {num_tasks} concrete tasks:\n\n"
                "Goal: {goal}\n\n"
                "For each task, provide:\n"
                "- Description\n"
                "- Required capabilities (e.g., analysis, coding, communication)\n"
                "- Estimated effort (1-5)\n\n"
                "Format as:\n"
                "Task N: [description]\n"
                "Capabilities: [cap1, cap2]\n"
                "Effort: [1-5]\n"
                "---\n"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            result = chain.invoke({"goal": goal, "num_tasks": num_tasks})
            
            # Parse tasks
            tasks = []
            task_blocks = result.split("---")
            
            for block in task_blocks:
                if not block.strip():
                    continue
                
                lines = [line.strip() for line in block.strip().split('\n')]
                description = ""
                capabilities = []
                effort = 1.0
                
                for line in lines:
                    if line.startswith("Task"):
                        description = line.split(":", 1)[1].strip() if ":" in line else line
                    elif line.startswith("Capabilities:"):
                        caps_text = line.split(":", 1)[1].strip()
                        capabilities = [c.strip() for c in caps_text.split(",")]
                    elif line.startswith("Effort:"):
                        try:
                            effort = float(line.split(":")[1].strip())
                        except:
                            effort = 1.0
                
                if description:
                    self.task_counter += 1
                    task = Task(
                        task_id=f"task_{self.task_counter}",
                        description=description,
                        required_capabilities=capabilities,
                        estimated_effort=effort
                    )
                    self.tasks[task.task_id] = task
                    tasks.append(task)
            
            return tasks
        
        def allocate_tasks_greedy(self) -> Dict[str, List[Task]]:
            """
            Allocate tasks to agents using greedy algorithm.
            
            Returns:
                Allocation map: agent_id -> tasks
            """
            allocation: Dict[str, List[Task]] = {aid: [] for aid in self.agents.keys()}
            
            # Sort tasks by priority (descending)
            unassigned = [t for t in self.tasks.values() if t.status == TaskStatus.UNASSIGNED]
            unassigned.sort(key=lambda t: t.priority, reverse=True)
            
            for task in unassigned:
                # Find capable agents
                capable_agents = [
                    agent for agent in self.agents.values()
                    if agent.can_handle(task)
                ]
                
                if not capable_agents:
                    continue
                
                # Assign to agent with most available capacity
                capable_agents.sort(key=lambda a: a.get_available_capacity(), reverse=True)
                best_agent = capable_agents[0]
                
                # Assign task
                task.assigned_to = best_agent.agent_id
                task.status = TaskStatus.ASSIGNED
                best_agent.assigned_tasks.append(task.task_id)
                best_agent.current_load += task.estimated_effort
                allocation[best_agent.agent_id].append(task)
            
            return allocation
        
        def allocate_tasks_auction(self) -> Dict[str, List[Task]]:
            """
            Allocate tasks using auction-based mechanism.
            
            Returns:
                Allocation map: agent_id -> tasks
            """
            allocation: Dict[str, List[Task]] = {aid: [] for aid in self.agents.keys()}
            
            unassigned = [t for t in self.tasks.values() if t.status == TaskStatus.UNASSIGNED]
            
            for task in unassigned:
                # Agents bid for task
                bids: List[tuple] = []
                
                for agent in self.agents.values():
                    if agent.can_handle(task):
                        # Bid value: higher if more capacity, lower if already loaded
                        bid_value = agent.get_available_capacity() / agent.capacity
                        bids.append((agent, bid_value))
                
                if bids:
                    # Award to highest bidder
                    bids.sort(key=lambda x: x[1], reverse=True)
                    winner = bids[0][0]
                    
                    task.assigned_to = winner.agent_id
                    task.status = TaskStatus.ASSIGNED
                    winner.assigned_tasks.append(task.task_id)
                    winner.current_load += task.estimated_effort
                    allocation[winner.agent_id].append(task)
                    
                    # Send message
                    self.messages.append(Message(
                        sender_id="coordinator",
                        receiver_id=winner.agent_id,
                        message_type=MessageType.TASK_ASSIGNMENT,
                        content={"task_id": task.task_id}
                    ))
            
            return allocation
        
        def identify_conflicts(self) -> List[Dict[str, Any]]:
            """
            Identify conflicts in task assignments.
            
            Returns:
                List of conflicts
            """
            conflicts = []
            
            # Resource conflicts: agents over capacity
            for agent in self.agents.values():
                if agent.current_load > agent.capacity:
                    conflicts.append({
                        "type": "capacity_exceeded",
                        "agent_id": agent.agent_id,
                        "load": agent.current_load,
                        "capacity": agent.capacity,
                        "excess": agent.current_load - agent.capacity
                    })
            
            # Dependency conflicts: tasks assigned before dependencies
            for task in self.tasks.values():
                if task.status == TaskStatus.ASSIGNED:
                    for dep_id in task.dependencies:
                        if dep_id in self.tasks:
                            dep_task = self.tasks[dep_id]
                            if dep_task.status == TaskStatus.UNASSIGNED:
                                conflicts.append({
                                    "type": "dependency_unassigned",
                                    "task_id": task.task_id,
                                    "dependency_id": dep_id
                                })
            
            return conflicts
        
        def resolve_capacity_conflict(self, agent_id: str):
            """
            Resolve capacity conflict by reassigning tasks.
            
            Args:
                agent_id: Agent with capacity issue
            """
            agent = self.agents[agent_id]
            
            # Sort agent's tasks by priority (keep high priority)
            agent_tasks = [
                self.tasks[tid] for tid in agent.assigned_tasks
                if tid in self.tasks
            ]
            agent_tasks.sort(key=lambda t: t.priority, reverse=True)
            
            # Remove lowest priority tasks until under capacity
            while agent.current_load > agent.capacity and agent_tasks:
                removed_task = agent_tasks.pop()
                removed_task.status = TaskStatus.UNASSIGNED
                removed_task.assigned_to = None
                agent.assigned_tasks.remove(removed_task.task_id)
                agent.current_load -= removed_task.estimated_effort
        
        def execute_tasks(self, agent_id: str, num_tasks: int = 1) -> List[Task]:
            """
            Simulate executing tasks for an agent.
            
            Args:
                agent_id: Agent executing tasks
                num_tasks: Number of tasks to execute
                
            Returns:
                Completed tasks
            """
            agent = self.agents[agent_id]
            completed = []
            
            # Get assigned tasks that are ready (dependencies met)
            ready_tasks = []
            for task_id in agent.assigned_tasks:
                if task_id not in self.tasks:
                    continue
                
                task = self.tasks[task_id]
                if task.status != TaskStatus.ASSIGNED:
                    continue
                
                # Check dependencies
                deps_met = all(
                    self.tasks[dep_id].status == TaskStatus.COMPLETED
                    for dep_id in task.dependencies
                    if dep_id in self.tasks
                )
                
                if deps_met:
                    ready_tasks.append(task)
            
            # Execute up to num_tasks
            for task in ready_tasks[:num_tasks]:
                task.status = TaskStatus.COMPLETED
                task.completed_at = datetime.now()
                agent.current_load -= task.estimated_effort
                completed.append(task)
                
                # Send status update
                self.messages.append(Message(
                    sender_id=agent_id,
                    receiver_id=None,  # Broadcast
                    message_type=MessageType.STATUS_UPDATE,
                    content={
                        "task_id": task.task_id,
                        "status": "completed"
                    }
                ))
            
            return completed
        
        def get_progress_summary(self) -> Dict[str, Any]:
            """Get collaborative planning progress."""
            total_tasks = len(self.tasks)
            completed = sum(1 for t in self.tasks.values() if t.status == TaskStatus.COMPLETED)
            in_progress = sum(1 for t in self.tasks.values() if t.status == TaskStatus.IN_PROGRESS)
            assigned = sum(1 for t in self.tasks.values() if t.status == TaskStatus.ASSIGNED)
            unassigned = sum(1 for t in self.tasks.values() if t.status == TaskStatus.UNASSIGNED)
            
            return {
                "total_tasks": total_tasks,
                "completed": completed,
                "in_progress": in_progress,
                "assigned": assigned,
                "unassigned": unassigned,
                "completion_rate": completed / total_tasks if total_tasks > 0 else 0,
                "total_agents": len(self.agents),
                "messages_sent": len(self.messages)
            }
        
        def get_agent_summary(self) -> str:
            """Get summary of agent workloads."""
            lines = ["AGENT WORKLOADS:"]
            
            for agent in self.agents.values():
                utilization = (agent.current_load / agent.capacity * 100) if agent.capacity > 0 else 0
                lines.append(
                    f"  {agent.name}: {len(agent.assigned_tasks)} tasks, "
                    f"{agent.current_load:.1f}/{agent.capacity:.1f} capacity ({utilization:.0f}%)"
                )
            
            return "\n".join(lines)
    
    
    def demonstrate_collaborative_planning():
        """Demonstrate collaborative planning pattern."""
        
        print("=" * 80)
        print("COLLABORATIVE PLANNING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic collaborative planning
        print("\n" + "=" * 80)
        print("Example 1: Multi-Agent Task Allocation")
        print("=" * 80)
        
        planner = CollaborativePlanner()
        
        # Add agents with different capabilities
        planner.add_agent("agent_1", "Alice", ["analysis", "coding"], capacity=10.0)
        planner.add_agent("agent_2", "Bob", ["coding", "testing"], capacity=10.0)
        planner.add_agent("agent_3", "Charlie", ["design", "communication"], capacity=10.0)
        
        print("\nAgents:")
        for agent in planner.agents.values():
            print(f"  • {agent.name}: {', '.join(agent.capabilities)}")
        
        # Decompose goal into tasks
        print("\nDecomposing goal...")
        tasks = planner.decompose_goal("Build a web application", num_tasks=5)
        
        print(f"\nCreated {len(tasks)} tasks:")
        for task in tasks:
            print(f"  {task.task_id}: {task.description}")
            print(f"    Capabilities: {', '.join(task.required_capabilities)}")
            print(f"    Effort: {task.estimated_effort}")
        
        # Allocate tasks
        print("\nAllocating tasks (greedy)...")
        allocation = planner.allocate_tasks_greedy()
        
        print("\nTask Allocation:")
        for agent_id, tasks in allocation.items():
            agent = planner.agents[agent_id]
            if tasks:
                print(f"  {agent.name}:")
                for task in tasks:
                    print(f"    - {task.description}")
        
        print("\n" + planner.get_agent_summary())
        
        # Example 2: Auction-based allocation
        print("\n" + "=" * 80)
        print("Example 2: Auction-Based Task Allocation")
        print("=" * 80)
        
        planner2 = CollaborativePlanner()
        
        planner2.add_agent("agent_a", "Agent A", ["coding"], capacity=8.0)
        planner2.add_agent("agent_b", "Agent B", ["coding"], capacity=12.0)
        planner2.add_agent("agent_c", "Agent C", ["coding"], capacity=6.0)
        
        # Create tasks manually
        for i in range(4):
            planner2.task_counter += 1
            task = Task(
                task_id=f"task_{planner2.task_counter}",
                description=f"Coding task {i+1}",
                required_capabilities=["coding"],
                estimated_effort=3.0
            )
            planner2.tasks[task.task_id] = task
        
        print("\nRunning auction-based allocation...")
        allocation2 = planner2.allocate_tasks_auction()
        
        print("\nAuction Results:")
        for agent_id, tasks in allocation2.items():
            agent = planner2.agents[agent_id]
            print(f"  {agent.name}: {len(tasks)} tasks "
                  f"(load: {agent.current_load}/{agent.capacity})")
        
        # Example 3: Conflict detection and resolution
        print("\n" + "=" * 80)
        print("Example 3: Conflict Detection and Resolution")
        print("=" * 80)
        
        planner3 = CollaborativePlanner()
        
        agent_d = planner3.add_agent("agent_d", "Agent D", ["task_skill"], capacity=5.0)
        
        # Create tasks that exceed capacity
        for i in range(4):
            planner3.task_counter += 1
            task = Task(
                task_id=f"task_{planner3.task_counter}",
                description=f"Task {i+1}",
                required_capabilities=["task_skill"],
                estimated_effort=2.0,
                status=TaskStatus.ASSIGNED,
                assigned_to="agent_d"
            )
            planner3.tasks[task.task_id] = task
            agent_d.assigned_tasks.append(task.task_id)
            agent_d.current_load += task.estimated_effort
        
        print(f"\nAgent D Load: {agent_d.current_load}/{agent_d.capacity}")
        print("Status: OVERLOADED")
        
        print("\nDetecting conflicts...")
        conflicts = planner3.identify_conflicts()
        
        for conflict in conflicts:
            print(f"  Conflict: {conflict['type']}")
            print(f"    Agent: {conflict['agent_id']}")
            print(f"    Excess load: {conflict['excess']:.1f}")
        
        print("\nResolving capacity conflict...")
        planner3.resolve_capacity_conflict("agent_d")
        
        print(f"\nAfter Resolution:")
        print(f"  Agent D Load: {agent_d.current_load}/{agent_d.capacity}")
        print(f"  Assigned tasks: {len(agent_d.assigned_tasks)}")
        unassigned = sum(1 for t in planner3.tasks.values() if t.status == TaskStatus.UNASSIGNED)
        print(f"  Unassigned tasks: {unassigned}")
        
        # Example 4: Dependency handling
        print("\n" + "=" * 80)
        print("Example 4: Task Dependencies")
        print("=" * 80)
        
        planner4 = CollaborativePlanner()
        
        planner4.add_agent("agent_e", "Agent E", ["any"], capacity=20.0)
        
        # Create tasks with dependencies
        task1 = Task(
            task_id="task_1",
            description="Design system",
            required_capabilities=["any"],
            estimated_effort=2.0,
            status=TaskStatus.ASSIGNED,
            assigned_to="agent_e"
        )
        
        task2 = Task(
            task_id="task_2",
            description="Implement system",
            required_capabilities=["any"],
            estimated_effort=3.0,
            dependencies=["task_1"],
            status=TaskStatus.ASSIGNED,
            assigned_to="agent_e"
        )
        
        task3 = Task(
            task_id="task_3",
            description="Test system",
            required_capabilities=["any"],
            estimated_effort=2.0,
            dependencies=["task_2"],
            status=TaskStatus.ASSIGNED,
            assigned_to="agent_e"
        )
        
        planner4.tasks.update({"task_1": task1, "task_2": task2, "task_3": task3})
        planner4.agents["agent_e"].assigned_tasks = ["task_1", "task_2", "task_3"]
        planner4.agents["agent_e"].current_load = 7.0
        
        print("\nTask Dependencies:")
        print("  task_1 (Design) → task_2 (Implement) → task_3 (Test)")
        
        print("\nExecuting tasks in order:")
        
        # Execute task 1
        completed = planner4.execute_tasks("agent_e", num_tasks=1)
        print(f"  ✓ Completed: {completed[0].description}")
        
        # Execute task 2
        completed = planner4.execute_tasks("agent_e", num_tasks=1)
        print(f"  ✓ Completed: {completed[0].description}")
        
        # Execute task 3
        completed = planner4.execute_tasks("agent_e", num_tasks=1)
        print(f"  ✓ Completed: {completed[0].description}")
        
        # Example 5: Progress tracking
        print("\n" + "=" * 80)
        print("Example 5: Collaborative Progress Tracking")
        print("=" * 80)
        
        planner5 = CollaborativePlanner()
        
        planner5.add_agent("agent_f", "Agent F", ["skill_a"], capacity=10.0)
        planner5.add_agent("agent_g", "Agent G", ["skill_b"], capacity=10.0)
        
        # Create and allocate tasks
        planner5.decompose_goal("Complete project", num_tasks=6)
        planner5.allocate_tasks_greedy()
        
        print("\nInitial Progress:")
        progress = planner5.get_progress_summary()
        print(f"  Total Tasks: {progress['total_tasks']}")
        print(f"  Assigned: {progress['assigned']}")
        print(f"  Unassigned: {progress['unassigned']}")
        print(f"  Completion Rate: {progress['completion_rate']:.0%}")
        
        # Simulate some work
        print("\nSimulating task execution...")
        for agent_id in planner5.agents.keys():
            planner5.execute_tasks(agent_id, num_tasks=2)
        
        print("\nUpdated Progress:")
        progress = planner5.get_progress_summary()
        print(f"  Completed: {progress['completed']}")
        print(f"  Completion Rate: {progress['completion_rate']:.0%}")
        print(f"  Messages Sent: {progress['messages_sent']}")
        
        # Example 6: Load balancing
        print("\n" + "=" * 80)
        print("Example 6: Load Balancing Across Agents")
        print("=" * 80)
        
        planner6 = CollaborativePlanner()
        
        planner6.add_agent("agent_h", "Agent H", ["general"], capacity=10.0)
        planner6.add_agent("agent_i", "Agent I", ["general"], capacity=15.0)
        planner6.add_agent("agent_j", "Agent J", ["general"], capacity=8.0)
        
        # Create uniform tasks
        for i in range(10):
            planner6.task_counter += 1
            task = Task(
                task_id=f"task_{planner6.task_counter}",
                description=f"Task {i+1}",
                required_capabilities=["general"],
                estimated_effort=2.0
            )
            planner6.tasks[task.task_id] = task
        
        print("\nBefore Allocation:")
        print(planner6.get_agent_summary())
        
        planner6.allocate_tasks_greedy()
        
        print("\nAfter Allocation:")
        print(planner6.get_agent_summary())
        
        # Example 7: Message-based coordination
        print("\n" + "=" * 80)
        print("Example 7: Message-Based Coordination")
        print("=" * 80)
        
        planner7 = CollaborativePlanner()
        
        planner7.add_agent("agent_k", "Agent K", ["skill"], capacity=10.0)
        planner7.add_agent("agent_l", "Agent L", ["skill"], capacity=10.0)
        
        # Create tasks and allocate
        for i in range(4):
            planner7.task_counter += 1
            task = Task(
                task_id=f"task_{planner7.task_counter}",
                description=f"Task {i+1}",
                required_capabilities=["skill"],
                estimated_effort=2.0
            )
            planner7.tasks[task.task_id] = task
        
        planner7.allocate_tasks_auction()
        
        print("\nCoordination Messages:")
        for msg in planner7.messages:
            receiver = msg.receiver_id or "ALL"
            print(f"  {msg.sender_id} → {receiver}: {msg.message_type.value}")
            if msg.message_type == MessageType.TASK_ASSIGNMENT:
                print(f"    Task: {msg.content['task_id']}")
        
        # Execute and send status updates
        for agent_id in planner7.agents.keys():
            planner7.execute_tasks(agent_id, num_tasks=1)
        
        print(f"\nTotal Messages: {len(planner7.messages)}")
        
        # Example 8: System summary
        print("\n" + "=" * 80)
        print("Example 8: Collaborative Planning Summary")
        print("=" * 80)
        
        planner8 = CollaborativePlanner()
        
        # Setup system
        planner8.add_agent("agent_m", "Specialist 1", ["analysis"], capacity=10.0)
        planner8.add_agent("agent_n", "Specialist 2", ["coding"], capacity=10.0)
        planner8.add_agent("agent_o", "Generalist", ["analysis", "coding"], capacity=12.0)
        
        planner8.decompose_goal("Build and deploy application", num_tasks=8)
        planner8.allocate_tasks_greedy()
        
        # Execute some tasks
        for agent_id in planner8.agents.keys():
            planner8.execute_tasks(agent_id, num_tasks=1)
        
        print("\nSYSTEM SUMMARY")
        print("=" * 60)
        print(planner8.get_agent_summary())
        
        print("\nPROGRESS:")
        progress = planner8.get_progress_summary()
        for key, value in progress.items():
            if isinstance(value, float):
                print(f"  {key}: {value:.2f}")
            else:
                print(f"  {key}: {value}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY: Collaborative Planning Pattern")
        print("=" * 80)
        
        summary = """
        The Collaborative Planning pattern demonstrated:
        
        1. MULTI-AGENT ALLOCATION (Example 1):
           - Goal decomposition
           - Agent capabilities matching
           - Greedy task allocation
           - Workload distribution
           - LLM-based task generation
        
        2. AUCTION MECHANISM (Example 2):
           - Bidding-based allocation
           - Capacity-based bidding
           - Winner selection
           - Fair task distribution
           - Market-based coordination
        
        3. CONFLICT RESOLUTION (Example 3):
           - Capacity conflict detection
           - Over-allocation handling
           - Task reassignment
           - Constraint satisfaction
           - Automatic resolution
        
        4. DEPENDENCY MANAGEMENT (Example 4):
           - Task dependencies
           - Sequential execution
           - Prerequisite checking
           - Ordered completion
           - Dependency-aware scheduling
        
        5. PROGRESS TRACKING (Example 5):
           - Status monitoring
           - Completion rates
           - Message counting
           - System metrics
           - Real-time updates
        
        6. LOAD BALANCING (Example 6):
           - Capacity-aware allocation
           - Even workload distribution
           - Utilization optimization
           - Resource efficiency
           - Balanced assignment
        
        7. MESSAGE COORDINATION (Example 7):
           - Assignment messages
           - Status updates
           - Broadcast communication
           - Coordination protocol
           - Event notification
        
        8. SYSTEM INTEGRATION (Example 8):
           - Comprehensive summary
           - Agent workloads
           - Progress metrics
           - System statistics
           - Complete view
        
        KEY BENEFITS:
        ✓ Leverages diverse agent capabilities
        ✓ Handles complex distributed tasks
        ✓ Load balancing across agents
        ✓ Fault tolerance through redundancy
        ✓ Parallel execution speedup
        ✓ Shared knowledge utilization
        ✓ Flexible task distribution
        ✓ Scalable coordination
        
        USE CASES:
        • Multi-robot coordination
        • Distributed project management
        • Supply chain planning
        • Military operations planning
        • Smart city management
        • Collaborative problem solving
        • Team-based game AI
        • Cloud resource orchestration
        
        COLLABORATION STRATEGIES:
        → Centralized: Coordinator assigns
        → Hierarchical: Tree of coordinators
        → Distributed: Peer negotiation
        → Auction-based: Competitive bidding
        → Consensus: Democratic voting
        
        BEST PRACTICES:
        1. Implement efficient communication protocols
        2. Use asynchronous messaging
        3. Handle agent failures gracefully
        4. Monitor collaboration overhead
        5. Implement timeout mechanisms
        6. Log all coordination decisions
        7. Support dynamic agent join/leave
        8. Enable human oversight
        
        TRADE-OFFS:
        • Communication overhead vs. coordination quality
        • Centralized vs. distributed control
        • Planning time vs. execution efficiency
        • Autonomy vs. coordination
        
        PRODUCTION CONSIDERATIONS:
        → Use message queues for async communication
        → Implement heartbeat monitoring
        → Handle network partitions
        → Cache agent capabilities
        → Support graceful degradation
        → Monitor communication latency
        → Log all agent interactions
        → Implement conflict resolution strategies
        → Support dynamic reconfiguration
        → Enable human intervention points
        
        This pattern enables multiple agents to work together effectively
        by coordinating their planning, sharing tasks based on capabilities,
        and synchronizing execution toward shared goals.
        """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_collaborative_planning()
    
        

    Agentic Design Pattern: Collaborative Planning

    Pattern 108: Collaborative Planning – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 108: Collaborative Planning

    Description: The Collaborative Planning pattern enables multiple agents to work together in creating and executing plans toward shared goals. Rather than planning in isolation, agents coordinate their planning efforts, share information, divide tasks, resolve conflicts, and synchronize their actions to achieve objectives that would be difficult or impossible for individual agents.

    Collaborative planning is essential for multi-agent systems where agents have
    complementary capabilities, shared resources, or interdependent goals. The
    pattern addresses challenges like distributed knowledge, conflicting preferences,
    resource contention, and coordination overhead.
    
    This pattern includes mechanisms for goal sharing, task decomposition and
    allocation, plan merging, conflict resolution, resource negotiation, and
    synchronized execution. Agents may use various collaboration strategies from
    centralized coordination to fully distributed consensus-based planning.
    

    Key Components: 1. Shared Goal Repository: Common objectives 2. Task Allocator: Assigns tasks to agents 3. Plan Merger: Combines individual plans 4. Conflict Resolver: Handles plan conflicts 5. Resource Negotiator: Allocates shared resources 6. Synchronization Manager: Coordinates timing 7. Communication Protocol: Agent interaction

    Collaboration Strategies: 1. Centralized: Single coordinator assigns tasks 2. Hierarchical: Tree of coordinators 3. Distributed: Peer-to-peer negotiation 4. Auction-based: Agents bid for tasks 5. Consensus: Democratic decision making 6. Blackboard: Shared workspace

    Planning Phases: 1. Goal Identification: Define shared objectives 2. Capability Sharing: Agents advertise skills 3. Task Decomposition: Break into subtasks 4. Task Allocation: Assign to appropriate agents 5. Plan Integration: Merge individual plans 6. Conflict Resolution: Resolve incompatibilities 7. Synchronized Execution: Coordinate timing

    Use Cases: – Multi-robot coordination – Distributed project management – Supply chain planning – Military operations planning – Smart city management – Collaborative problem solving – Team-based game AI

    Advantages: – Leverages diverse capabilities – Handles complex tasks beyond single agent – Load balancing across agents – Fault tolerance through redundancy – Parallel execution speedup – Shared knowledge utilization – Flexible task distribution

    Challenges: – Communication overhead – Conflict resolution complexity – Synchronization requirements – Trust and verification – Resource contention – Plan consistency maintenance – Scalability with agent count

    LangChain Implementation: This implementation uses LangChain for: – LLM-based task decomposition – Agent capability matching – Plan integration strategies – Natural language coordination

    Production Considerations: – Implement efficient communication protocols – Use asynchronous messaging – Handle agent failures gracefully – Monitor collaboration overhead – Implement timeout mechanisms – Log all coordination decisions – Support dynamic agent addition/removal – Enable human oversight


    File Statistics

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

    Module Documentation

    """
    Pattern 108: Collaborative Planning
    
    Description:
        The Collaborative Planning pattern enables multiple agents to work together
        in creating and executing plans toward shared goals. Rather than planning in
        isolation, agents coordinate their planning efforts, share information, divide
        tasks, resolve conflicts, and synchronize their actions to achieve objectives
        that would be difficult or impossible for individual agents.
    
        Collaborative planning is essential for multi-agent systems where agents have
        complementary capabilities, shared resources, or interdependent goals. The
        pattern addresses challenges like distributed knowledge, conflicting preferences,
        resource contention, and coordination overhead.
    
        This pattern includes mechanisms for goal sharing, task decomposition and
        allocation, plan merging, conflict resolution, resource negotiation, and
        synchronized execution. Agents may use various collaboration strategies from
        centralized coordination to fully distributed consensus-based planning.
    
    Key Components:
        1. Shared Goal Repository: Common objectives
        2. Task Allocator: Assigns tasks to agents
        3. Plan Merger: Combines individual plans
        4. Conflict Resolver: Handles plan conflicts
        5. Resource Negotiator: Allocates shared resources
        6. Synchronization Manager: Coordinates timing
        7. Communication Protocol: Agent interaction
    
    Collaboration Strategies:
        1. Centralized: Single coordinator assigns tasks
        2. Hierarchical: Tree of coordinators
        3. Distributed: Peer-to-peer negotiation
        4. Auction-based: Agents bid for tasks
        5. Consensus: Democratic decision making
        6. Blackboard: Shared workspace
    
    Planning Phases:
        1. Goal Identification: Define shared objectives
        2. Capability Sharing: Agents advertise skills
        3. Task Decomposition: Break into subtasks
        4. Task Allocation: Assign to appropriate agents
        5. Plan Integration: Merge individual plans
        6. Conflict Resolution: Resolve incompatibilities
        7. Synchronized Execution: Coordinate timing
    
    Use Cases:
        - Multi-robot coordination
        - Distributed project management
        - Supply chain planning
        - Military operations planning
        - Smart city management
        - Collaborative problem solving
        - Team-based game AI
    
    Advantages:
        - Leverages diverse capabilities
        - Handles complex tasks beyond single agent
        - Load balancing across agents
        - Fault tolerance through redundancy
        - Parallel execution speedup
        - Shared knowledge utilization
        - Flexible task distribution
    
    Challenges:
        - Communication overhead
        - Conflict resolution complexity
        - Synchronization requirements
        - Trust and verification
        - Resource contention
        - Plan consistency maintenance
        - Scalability with agent count
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based task decomposition
        - Agent capability matching
        - Plan integration strategies
        - Natural language coordination
    
    Production Considerations:
        - Implement efficient communication protocols
        - Use asynchronous messaging
        - Handle agent failures gracefully
        - Monitor collaboration overhead
        - Implement timeout mechanisms
        - Log all coordination decisions
        - Support dynamic agent addition/removal
        - Enable human oversight
    """
    

    Purpose: This module implements the Collaborative Planning 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 typing.Any
    import typing.Dict
    import typing.List
    import typing.Optional
    import typing.Set
    

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

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: TaskStatus

    Lines 106-112

    Inherits from: Enum

    Purpose:

    Status of a task.
    

    Class: MessageType

    Lines 115-123

    Inherits from: Enum

    Purpose:

    Types of coordination messages.
    

    Class: Task

    Lines 127-150

    Purpose:

    Represents a task in collaborative plan.
    
    Attributes:
        task_id: Unique identifier
        description: Task description
        required_capabilities: Capabilities needed
        estimated_effort: Effort estimate
        dependencies: Tasks that must complete first
        assigned_to: Agent ID assigned to
        status: Current status
        priority: Task priority
    

    Class: CollaborativeAgent

    Lines 154-185

    Purpose:

    Agent participating in collaborative planning.
    
    Attributes:
        agent_id: Unique identifier
        name: Agent name
        capabilities: List of capabilities
        capacity: Available work capacity
        current_load: Current workload
        assigned_tasks: Tasks assigned to this agent
    

    Methods:

    can_handle(self, task)

    Lines 173-181

    Purpose:

    Check if agent can handle a task.
    

    Parameters: – task: Task

    Returns: bool

    get_available_capacity(self)

    Lines 183-185

    Purpose:

    Get remaining capacity.
    

    Parameters:

    Returns: float


    Class: Message

    Lines 189-204

    Purpose:

    Communication message between agents.
    
    Attributes:
        sender_id: Sending agent ID
        receiver_id: Receiving agent ID (None for broadcast)
        message_type: Type of message
        content: Message payload
        timestamp: When sent
    

    Class: CollaborativePlanner

    Lines 207-543

    Purpose:

    Collaborative planning system for multi-agent coordination.
    
    This planner enables agents to work together in planning and
    executing tasks toward shared goals.
    

    Methods:

    __init__(self, temperature)

    Lines 215-226

    Purpose:

    Initialize collaborative planner.
    
    Args:
        temperature: LLM temperature
    

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

    add_agent(self, agent_id, name, capabilities, capacity)

    Lines 228-254

    Purpose:

    Add agent to the planning system.
    
    Args:
        agent_id: Agent identifier
        name: Agent name
        capabilities: Agent capabilities
        capacity: Work capacity
    
    Returns:
        Created agent
    

    Parameters: – agent_id: Agent id – name: Name – capabilities: Capabilities – capacity: Capacity

    Returns: CollaborativeAgent

    decompose_goal(self, goal, num_tasks)

    Lines 256-320

    Purpose:

    Decompose goal into tasks using LLM.
    
    Args:
        goal: Goal description
        num_tasks: Number of tasks to create
    
    Returns:
        List of tasks
    

    Parameters: – goal: Goal – num_tasks: Num tasks

    Returns: <ast.Subscript object at 0x000001A06D68E690>

    allocate_tasks_greedy(self)

    Lines 322-356

    Purpose:

    Allocate tasks to agents using greedy algorithm.
    
    Returns:
        Allocation map: agent_id -> tasks
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D888BD0>

    allocate_tasks_auction(self)

    Lines 358-398

    Purpose:

    Allocate tasks using auction-based mechanism.
    
    Returns:
        Allocation map: agent_id -> tasks
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D70C610>

    identify_conflicts(self)

    Lines 400-433

    Purpose:

    Identify conflicts in task assignments.
    
    Returns:
        List of conflicts
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D638B90>

    resolve_capacity_conflict(self, agent_id)

    Lines 435-457

    Purpose:

    Resolve capacity conflict by reassigning tasks.
    
    Args:
        agent_id: Agent with capacity issue
    

    Parameters: – agent_id: Agent id

    execute_tasks(self, agent_id, num_tasks)

    Lines 459-511

    Purpose:

    Simulate executing tasks for an agent.
    
    Args:
        agent_id: Agent executing tasks
        num_tasks: Number of tasks to execute
    
    Returns:
        Completed tasks
    

    Parameters: – agent_id: Agent id – num_tasks: Num tasks

    Returns: <ast.Subscript object at 0x000001A06D63D5D0>

    get_progress_summary(self)

    Lines 513-530

    Purpose:

    Get collaborative planning progress.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D7C82D0>

    get_agent_summary(self)

    Lines 532-543

    Purpose:

    Get summary of agent workloads.
    

    Parameters:

    Returns: str


    Functions

    demonstrate_collaborative_planning()

    Lines 546-981

    Purpose:

    Demonstrate collaborative planning pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Collaborative Planning 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 107
    • Pattern 109

    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 Collaborative Planning 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: 108_collaborative_planning.py
    Total Lines: 986
    Pattern Category: AI Agent Design Pattern #108


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Multi Objective Planning

    
        """
    Pattern 107: Multi-Objective Planning
    
    Description:
        The Multi-Objective Planning pattern enables agents to plan and make decisions
        when multiple, often conflicting objectives must be balanced. Instead of
        optimizing for a single goal, the agent must consider tradeoffs between
        competing objectives like cost vs. quality, speed vs. accuracy, or risk vs.
        reward.
        
        This pattern is essential for realistic decision-making where perfect solutions
        don't exist and every choice involves tradeoffs. The agent must evaluate plans
        across multiple dimensions, understand Pareto-optimal solutions, and make
        informed decisions about which tradeoffs are acceptable given the context.
        
        Multi-objective planning involves objective weighting, constraint satisfaction,
        Pareto optimization, preference elicitation, and tradeoff analysis. The agent
        can present multiple plan options with different tradeoff profiles, allowing
        humans or automated systems to select based on current priorities.
    
    Key Components:
        1. Objective Set: Multiple goals to optimize
        2. Objective Weights: Relative importance of each objective
        3. Plan Evaluator: Scores plans across all objectives
        4. Pareto Filter: Identifies non-dominated solutions
        5. Tradeoff Analyzer: Analyzes competing objectives
        6. Constraint Checker: Ensures hard constraints met
        7. Preference Engine: Incorporates user preferences
    
    Objective Types:
        1. Maximization: Quality, performance, completeness
        2. Minimization: Cost, time, risk, resource usage
        3. Target: Hit specific value (with tolerance)
        4. Constraint: Must satisfy (hard requirement)
        
    Common Objective Combinations:
        - Cost vs. Quality vs. Speed (iron triangle)
        - Risk vs. Reward
        - Precision vs. Recall
        - Exploration vs. Exploitation
        - Short-term vs. Long-term goals
        - Individual vs. Group benefit
    
    Planning Approaches:
        1. Weighted Sum: Combine objectives with weights
        2. Lexicographic: Prioritize objectives in order
        3. Pareto Optimal: Non-dominated solutions
        4. Satisficing: Good enough on all objectives
        5. Goal Programming: Meet targets with deviations
        6. Multi-Criteria Decision Making (MCDM)
    
    Use Cases:
        - Resource allocation with constraints
        - Project planning with multiple stakeholders
        - Route planning (time vs. cost vs. comfort)
        - Product design (features vs. complexity vs. cost)
        - Investment decisions (risk vs. return)
        - Scheduling (efficiency vs. fairness)
        - Machine learning (accuracy vs. interpretability)
    
    Advantages:
        - Realistic modeling of complex decisions
        - Explicit tradeoff consideration
        - Multiple solution alternatives
        - Stakeholder preference incorporation
        - Better than single-objective optimization
        - Transparent decision criteria
        - Flexible priority adjustment
    
    Challenges:
        - Objective quantification difficulty
        - Weight elicitation from users
        - Computational complexity
        - Visualizing multi-dimensional tradeoffs
        - Handling conflicting constraints
        - Determining Pareto frontier
        - Explaining tradeoff decisions
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based objective identification
        - Plan evaluation across dimensions
        - Tradeoff analysis and explanation
        - Natural language objective specification
        
    Production Considerations:
        - Define clear, measurable objectives
        - Implement efficient Pareto filtering
        - Provide tradeoff visualizations
        - Allow dynamic weight adjustment
        - Cache evaluated plans
        - Support constraint hierarchies
        - Enable sensitivity analysis
        - Log decision rationale
    """
    
    import os
    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 ObjectiveType(Enum):
        """Type of optimization objective."""
        MAXIMIZE = "maximize"
        MINIMIZE = "minimize"
        TARGET = "target"
        CONSTRAINT = "constraint"
    
    
    @dataclass
    class Objective:
        """
        Represents an optimization objective.
        
        Attributes:
            name: Objective name
            objective_type: Maximization, minimization, target, or constraint
            weight: Relative importance (0-1)
            target_value: Target value (for TARGET type)
            constraint_value: Threshold (for CONSTRAINT type)
            unit: Measurement unit
        """
        name: str
        objective_type: ObjectiveType
        weight: float = 0.5
        target_value: Optional[float] = None
        constraint_value: Optional[float] = None
        unit: str = ""
        
        def evaluate(self, value: float) -> float:
            """
            Evaluate objective given a value.
            
            Args:
                value: Actual value
                
            Returns:
                Normalized score (0-1), higher is better
            """
            if self.objective_type == ObjectiveType.MAXIMIZE:
                # Normalize to 0-1 (assuming reasonable range)
                return min(1.0, max(0.0, value / 100.0))
            
            elif self.objective_type == ObjectiveType.MINIMIZE:
                # Lower is better, invert score
                return max(0.0, 1.0 - (value / 100.0))
            
            elif self.objective_type == ObjectiveType.TARGET:
                # Closer to target is better
                if self.target_value is None:
                    return 0.5
                distance = abs(value - self.target_value)
                return max(0.0, 1.0 - (distance / 50.0))
            
            elif self.objective_type == ObjectiveType.CONSTRAINT:
                # Binary: constraint satisfied or not
                if self.constraint_value is None:
                    return 1.0
                return 1.0 if value  float:
            """Get normalized score for an objective."""
            value = self.objective_values.get(objective.name, 0.0)
            return objective.evaluate(value)
    
    
    class MultiObjectivePlanner:
        """
        Multi-objective planning system.
        
        This planner evaluates and compares plans across multiple objectives,
        identifies Pareto-optimal solutions, and analyzes tradeoffs.
        """
        
        def __init__(self, temperature: float = 0.4):
            """
            Initialize multi-objective planner.
            
            Args:
                temperature: LLM temperature
            """
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.objectives: List[Objective] = []
            self.plans: List[Plan] = []
            self.plan_counter = 0
        
        def add_objective(
            self,
            name: str,
            objective_type: ObjectiveType,
            weight: float = 0.5,
            target_value: Optional[float] = None,
            constraint_value: Optional[float] = None,
            unit: str = ""
        ) -> Objective:
            """
            Add an objective to optimize.
            
            Args:
                name: Objective name
                objective_type: Type of objective
                weight: Relative importance
                target_value: Target value (for TARGET)
                constraint_value: Constraint threshold
                unit: Measurement unit
                
            Returns:
                Created objective
            """
            objective = Objective(
                name=name,
                objective_type=objective_type,
                weight=weight,
                target_value=target_value,
                constraint_value=constraint_value,
                unit=unit
            )
            self.objectives.append(objective)
            return objective
        
        def create_plan(
            self,
            description: str,
            objective_values: Dict[str, float]
        ) -> Plan:
            """
            Create a plan with objective values.
            
            Args:
                description: Plan description
                objective_values: Values for each objective
                
            Returns:
                Created plan
            """
            self.plan_counter += 1
            
            # Check constraint satisfaction
            feasible = True
            for obj in self.objectives:
                if obj.objective_type == ObjectiveType.CONSTRAINT:
                    value = objective_values.get(obj.name, float('inf'))
                    if obj.constraint_value is not None and value > obj.constraint_value:
                        feasible = False
                        break
            
            plan = Plan(
                plan_id=f"plan_{self.plan_counter}",
                description=description,
                objective_values=objective_values,
                feasible=feasible
            )
            
            self.plans.append(plan)
            return plan
        
        def evaluate_plan(self, plan: Plan) -> Dict[str, Any]:
            """
            Evaluate plan across all objectives.
            
            Args:
                plan: Plan to evaluate
                
            Returns:
                Evaluation results
            """
            scores = {}
            weighted_sum = 0.0
            total_weight = 0.0
            
            for obj in self.objectives:
                score = plan.get_score(obj)
                scores[obj.name] = score
                
                if obj.objective_type != ObjectiveType.CONSTRAINT:
                    weighted_sum += score * obj.weight
                    total_weight += obj.weight
            
            overall_score = weighted_sum / total_weight if total_weight > 0 else 0.0
            
            return {
                "plan_id": plan.plan_id,
                "feasible": plan.feasible,
                "scores": scores,
                "overall_score": overall_score,
                "objective_values": plan.objective_values
            }
        
        def find_pareto_optimal(self) -> List[Plan]:
            """
            Find Pareto-optimal (non-dominated) plans.
            
            Returns:
                List of Pareto-optimal plans
            """
            feasible_plans = [p for p in self.plans if p.feasible]
            
            if not feasible_plans:
                return []
            
            # Mark dominated plans
            for plan in feasible_plans:
                plan.dominated = False
            
            for i, plan1 in enumerate(feasible_plans):
                for plan2 in feasible_plans[i+1:]:
                    # Check if plan1 dominates plan2
                    plan1_better = False
                    plan2_better = False
                    
                    for obj in self.objectives:
                        if obj.objective_type == ObjectiveType.CONSTRAINT:
                            continue
                        
                        score1 = plan1.get_score(obj)
                        score2 = plan2.get_score(obj)
                        
                        if score1 > score2:
                            plan1_better = True
                        elif score2 > score1:
                            plan2_better = True
                    
                    # Mark dominated plans
                    if plan1_better and not plan2_better:
                        plan2.dominated = True
                    elif plan2_better and not plan1_better:
                        plan1.dominated = True
            
            return [p for p in feasible_plans if not p.dominated]
        
        def analyze_tradeoffs(
            self,
            plan1: Plan,
            plan2: Plan
        ) -> Dict[str, Any]:
            """
            Analyze tradeoffs between two plans.
            
            Args:
                plan1: First plan
                plan2: Second plan
                
            Returns:
                Tradeoff analysis
            """
            tradeoffs = []
            
            for obj in self.objectives:
                if obj.objective_type == ObjectiveType.CONSTRAINT:
                    continue
                
                score1 = plan1.get_score(obj)
                score2 = plan2.get_score(obj)
                value1 = plan1.objective_values.get(obj.name, 0)
                value2 = plan2.objective_values.get(obj.name, 0)
                
                difference = score2 - score1
                
                if abs(difference) > 0.05:  # Significant difference
                    tradeoffs.append({
                        "objective": obj.name,
                        "plan1_value": value1,
                        "plan2_value": value2,
                        "plan1_score": score1,
                        "plan2_score": score2,
                        "difference": difference,
                        "winner": "plan2" if difference > 0 else "plan1"
                    })
            
            return {
                "plan1": plan1.plan_id,
                "plan2": plan2.plan_id,
                "tradeoffs": tradeoffs,
                "num_tradeoffs": len(tradeoffs)
            }
        
        def generate_plan_with_priorities(
            self,
            task: str,
            priority_objectives: List[str]
        ) -> Plan:
            """
            Generate plan optimizing for priority objectives.
            
            Args:
                task: Task description
                priority_objectives: High-priority objective names
                
            Returns:
                Generated plan
            """
            # Create prompt emphasizing priority objectives
            objectives_text = "\n".join([
                f"- {obj.name} ({obj.objective_type.value})"
                + (f" - PRIORITY" if obj.name in priority_objectives else "")
                for obj in self.objectives
            ])
            
            prompt = ChatPromptTemplate.from_template(
                "Create a plan for this task that optimizes the priority objectives:\n\n"
                "Task: {task}\n\n"
                "Objectives:\n{objectives}\n\n"
                "Provide a brief plan description (one sentence).\n"
                "Plan:"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            description = chain.invoke({
                "task": task,
                "objectives": objectives_text
            }).strip()
            
            # Generate estimated values (in practice, would come from simulation)
            objective_values = {}
            for obj in self.objectives:
                # Favor priority objectives
                if obj.name in priority_objectives:
                    if obj.objective_type == ObjectiveType.MAXIMIZE:
                        objective_values[obj.name] = 80.0
                    elif obj.objective_type == ObjectiveType.MINIMIZE:
                        objective_values[obj.name] = 20.0
                    else:
                        objective_values[obj.name] = 50.0
                else:
                    # Non-priority gets moderate values
                    objective_values[obj.name] = 50.0
            
            return self.create_plan(description, objective_values)
        
        def rank_plans(self) -> List[Tuple[Plan, float]]:
            """
            Rank plans by weighted overall score.
            
            Returns:
                List of (plan, score) tuples, sorted by score
            """
            ranked = []
            
            for plan in self.plans:
                if not plan.feasible:
                    continue
                
                eval_result = self.evaluate_plan(plan)
                ranked.append((plan, eval_result["overall_score"]))
            
            ranked.sort(key=lambda x: x[1], reverse=True)
            return ranked
        
        def get_summary(self) -> str:
            """Get summary of plans and objectives."""
            lines = ["MULTI-OBJECTIVE PLANNING SUMMARY"]
            lines.append("=" * 60)
            
            # Objectives
            lines.append("\nObjectives:")
            for obj in self.objectives:
                lines.append(
                    f"  • {obj.name} ({obj.objective_type.value})"
                    f" - weight: {obj.weight:.2f}"
                )
            
            # Plans
            lines.append(f"\nTotal Plans: {len(self.plans)}")
            lines.append(f"Feasible Plans: {sum(1 for p in self.plans if p.feasible)}")
            
            # Pareto optimal
            pareto = self.find_pareto_optimal()
            lines.append(f"Pareto-Optimal Plans: {len(pareto)}")
            
            return "\n".join(lines)
    
    
    def demonstrate_multi_objective_planning():
        """Demonstrate multi-objective planning pattern."""
        
        print("=" * 80)
        print("MULTI-OBJECTIVE PLANNING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic multi-objective planning
        print("\n" + "=" * 80)
        print("Example 1: Project Planning with Cost, Time, Quality")
        print("=" * 80)
        
        planner = MultiObjectivePlanner()
        
        # Define objectives
        planner.add_objective("cost", ObjectiveType.MINIMIZE, weight=0.4, unit="$")
        planner.add_objective("time", ObjectiveType.MINIMIZE, weight=0.3, unit="days")
        planner.add_objective("quality", ObjectiveType.MAXIMIZE, weight=0.3, unit="score")
        
        print("\nObjectives:")
        for obj in planner.objectives:
            print(f"  • {obj.name}: {obj.objective_type.value} (weight: {obj.weight})")
        
        # Create alternative plans
        plans_data = [
            ("Budget plan: Minimize costs, longer timeline", {"cost": 10000, "time": 60, "quality": 70}),
            ("Fast plan: Minimize time, higher costs", {"cost": 25000, "time": 30, "quality": 75}),
            ("Quality plan: Maximize quality, balanced cost/time", {"cost": 18000, "time": 45, "quality": 90}),
            ("Balanced plan: Moderate on all objectives", {"cost": 15000, "time": 45, "quality": 80}),
        ]
        
        print("\nCreating alternative plans...")
        for desc, values in plans_data:
            plan = planner.create_plan(desc, values)
            print(f"  ✓ {plan.plan_id}: {desc}")
        
        # Evaluate plans
        print("\nPlan Evaluations:")
        for plan in planner.plans:
            eval_result = planner.evaluate_plan(plan)
            print(f"\n  {plan.plan_id}: {plan.description}")
            print(f"    Overall Score: {eval_result['overall_score']:.3f}")
            for obj_name, score in eval_result['scores'].items():
                value = plan.objective_values[obj_name]
                print(f"    {obj_name}: {value} → score: {score:.3f}")
        
        # Example 2: Pareto optimal solutions
        print("\n" + "=" * 80)
        print("Example 2: Finding Pareto-Optimal Solutions")
        print("=" * 80)
        
        planner2 = MultiObjectivePlanner()
        
        planner2.add_objective("performance", ObjectiveType.MAXIMIZE, weight=0.5)
        planner2.add_objective("efficiency", ObjectiveType.MAXIMIZE, weight=0.5)
        
        # Create plans with different tradeoffs
        test_plans = [
            ("High performance, low efficiency", {"performance": 90, "efficiency": 40}),
            ("Low performance, high efficiency", {"performance": 40, "efficiency": 90}),
            ("Balanced", {"performance": 70, "efficiency": 70}),
            ("Dominated plan", {"performance": 50, "efficiency": 50}),
            ("Another balanced", {"performance": 75, "efficiency": 65}),
        ]
        
        for desc, values in test_plans:
            planner2.create_plan(desc, values)
        
        print("\nAll Plans:")
        for plan in planner2.plans:
            print(f"  {plan.plan_id}: {plan.description}")
            print(f"    Performance: {plan.objective_values['performance']}, "
                  f"Efficiency: {plan.objective_values['efficiency']}")
        
        pareto_plans = planner2.find_pareto_optimal()
        
        print(f"\nPareto-Optimal Plans ({len(pareto_plans)} found):")
        for plan in pareto_plans:
            print(f"  ✓ {plan.plan_id}: {plan.description}")
            print(f"    Performance: {plan.objective_values['performance']}, "
                  f"Efficiency: {plan.objective_values['efficiency']}")
        
        # Example 3: Tradeoff analysis
        print("\n" + "=" * 80)
        print("Example 3: Tradeoff Analysis Between Plans")
        print("=" * 80)
        
        planner3 = MultiObjectivePlanner()
        
        planner3.add_objective("speed", ObjectiveType.MAXIMIZE, weight=0.4)
        planner3.add_objective("accuracy", ObjectiveType.MAXIMIZE, weight=0.4)
        planner3.add_objective("cost", ObjectiveType.MINIMIZE, weight=0.2)
        
        plan_a = planner3.create_plan(
            "Fast but less accurate",
            {"speed": 90, "accuracy": 60, "cost": 20}
        )
        
        plan_b = planner3.create_plan(
            "Slow but very accurate",
            {"speed": 50, "accuracy": 95, "cost": 40}
        )
        
        print("\nComparing two plans:")
        print(f"  Plan A: {plan_a.description}")
        print(f"  Plan B: {plan_b.description}")
        
        tradeoff = planner3.analyze_tradeoffs(plan_a, plan_b)
        
        print(f"\nTradeoff Analysis ({tradeoff['num_tradeoffs']} significant differences):")
        for t in tradeoff['tradeoffs']:
            winner = "Plan A" if t['winner'] == 'plan1' else "Plan B"
            print(f"  • {t['objective']}:")
            print(f"    Plan A: {t['plan1_value']} (score: {t['plan1_score']:.3f})")
            print(f"    Plan B: {t['plan2_value']} (score: {t['plan2_score']:.3f})")
            print(f"    Winner: {winner}")
        
        # Example 4: Constraints
        print("\n" + "=" * 80)
        print("Example 4: Hard Constraints")
        print("=" * 80)
        
        planner4 = MultiObjectivePlanner()
        
        planner4.add_objective("profit", ObjectiveType.MAXIMIZE, weight=0.6)
        planner4.add_objective("customer_satisfaction", ObjectiveType.MAXIMIZE, weight=0.4)
        planner4.add_objective("budget", ObjectiveType.CONSTRAINT, constraint_value=50000)
        
        print("\nObjectives:")
        print("  • Maximize profit (weight: 0.6)")
        print("  • Maximize customer satisfaction (weight: 0.4)")
        print("  • Budget constraint: ≤ $50,000")
        
        constraint_plans = [
            ("Within budget", {"profit": 30000, "customer_satisfaction": 80, "budget": 45000}),
            ("Over budget", {"profit": 50000, "customer_satisfaction": 90, "budget": 60000}),
            ("Tight budget", {"profit": 25000, "customer_satisfaction": 75, "budget": 50000}),
        ]
        
        print("\nEvaluating plans:")
        for desc, values in constraint_plans:
            plan = planner4.create_plan(desc, values)
            status = "✓ FEASIBLE" if plan.feasible else "✗ INFEASIBLE"
            print(f"  {status}: {desc}")
            print(f"    Budget: ${values['budget']:,}")
        
        # Example 5: Ranking plans
        print("\n" + "=" * 80)
        print("Example 5: Ranking Plans by Overall Score")
        print("=" * 80)
        
        planner5 = MultiObjectivePlanner()
        
        planner5.add_objective("innovation", ObjectiveType.MAXIMIZE, weight=0.3)
        planner5.add_objective("feasibility", ObjectiveType.MAXIMIZE, weight=0.4)
        planner5.add_objective("risk", ObjectiveType.MINIMIZE, weight=0.3)
        
        ranking_plans = [
            ("Conservative approach", {"innovation": 40, "feasibility": 90, "risk": 10}),
            ("Innovative but risky", {"innovation": 90, "feasibility": 60, "risk": 70}),
            ("Moderate innovation", {"innovation": 70, "feasibility": 80, "risk": 30}),
            ("Safe and feasible", {"innovation": 50, "feasibility": 95, "risk": 15}),
        ]
        
        for desc, values in ranking_plans:
            planner5.create_plan(desc, values)
        
        ranked = planner5.rank_plans()
        
        print("\nPlans Ranked by Weighted Score:")
        for i, (plan, score) in enumerate(ranked, 1):
            print(f"  {i}. {plan.description}")
            print(f"     Overall Score: {score:.3f}")
            print(f"     Innovation: {plan.objective_values['innovation']}, "
                  f"Feasibility: {plan.objective_values['feasibility']}, "
                  f"Risk: {plan.objective_values['risk']}")
        
        # Example 6: Priority-based planning
        print("\n" + "=" * 80)
        print("Example 6: Priority-Based Plan Generation")
        print("=" * 80)
        
        planner6 = MultiObjectivePlanner()
        
        planner6.add_objective("reliability", ObjectiveType.MAXIMIZE, weight=0.3)
        planner6.add_objective("performance", ObjectiveType.MAXIMIZE, weight=0.3)
        planner6.add_objective("maintainability", ObjectiveType.MAXIMIZE, weight=0.2)
        planner6.add_objective("cost", ObjectiveType.MINIMIZE, weight=0.2)
        
        print("\nGenerating plans with different priorities:")
        
        # Priority on reliability
        plan1 = planner6.generate_plan_with_priorities(
            "Build production system",
            ["reliability"]
        )
        print(f"\n  Priority: Reliability")
        print(f"  Plan: {plan1.description}")
        
        # Priority on performance
        plan2 = planner6.generate_plan_with_priorities(
            "Build production system",
            ["performance"]
        )
        print(f"\n  Priority: Performance")
        print(f"  Plan: {plan2.description}")
        
        # Example 7: Weight sensitivity
        print("\n" + "=" * 80)
        print("Example 7: Objective Weight Sensitivity")
        print("=" * 80)
        
        planner7 = MultiObjectivePlanner()
        
        obj1 = planner7.add_objective("feature_count", ObjectiveType.MAXIMIZE)
        obj2 = planner7.add_objective("simplicity", ObjectiveType.MAXIMIZE)
        
        test_plan = planner7.create_plan(
            "Product design",
            {"feature_count": 70, "simplicity": 60}
        )
        
        print("\nTesting different weight combinations:")
        
        weight_combos = [
            (0.8, 0.2, "Feature-focused"),
            (0.5, 0.5, "Balanced"),
            (0.2, 0.8, "Simplicity-focused"),
        ]
        
        for w1, w2, label in weight_combos:
            obj1.weight = w1
            obj2.weight = w2
            
            eval_result = planner7.evaluate_plan(test_plan)
            print(f"\n  {label} (weights: {w1:.1f}, {w2:.1f}):")
            print(f"    Overall Score: {eval_result['overall_score']:.3f}")
        
        # Example 8: System summary
        print("\n" + "=" * 80)
        print("Example 8: Planning System Summary")
        print("=" * 80)
        
        planner8 = MultiObjectivePlanner()
        
        planner8.add_objective("throughput", ObjectiveType.MAXIMIZE, weight=0.35)
        planner8.add_objective("latency", ObjectiveType.MINIMIZE, weight=0.35)
        planner8.add_objective("resource_usage", ObjectiveType.MINIMIZE, weight=0.30)
        
        # Create several plans
        for i in range(5):
            planner8.create_plan(
                f"Design option {i+1}",
                {
                    "throughput": 50 + (i * 10),
                    "latency": 100 - (i * 15),
                    "resource_usage": 40 + (i * 5)
                }
            )
        
        print(planner8.get_summary())
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY: Multi-Objective Planning Pattern")
        print("=" * 80)
        
        summary = """
        The Multi-Objective Planning pattern demonstrated:
        
        1. MULTIPLE OBJECTIVES (Example 1):
           - Cost, time, quality tradeoffs
           - Weighted objective evaluation
           - Overall score calculation
           - Alternative plan comparison
           - Objective value normalization
        
        2. PARETO OPTIMALITY (Example 2):
           - Non-dominated solutions
           - Domination checking
           - Efficient frontier identification
           - Multiple optimal alternatives
           - Tradeoff visualization
        
        3. TRADEOFF ANALYSIS (Example 3):
           - Comparing plan pairs
           - Identifying differences
           - Winner determination
           - Quantifying tradeoffs
           - Decision support
        
        4. HARD CONSTRAINTS (Example 4):
           - Constraint satisfaction
           - Feasibility checking
           - Plan filtering
           - Requirement enforcement
           - Infeasible plan rejection
        
        5. PLAN RANKING (Example 5):
           - Weighted overall scoring
           - Sorted alternatives
           - Best plan identification
           - Multi-criteria comparison
           - Decision making
        
        6. PRIORITY OBJECTIVES (Example 6):
           - LLM-guided plan generation
           - Priority emphasis
           - Context-aware planning
           - Objective focus
           - Adaptive generation
        
        7. WEIGHT SENSITIVITY (Example 7):
           - Dynamic weight adjustment
           - Impact analysis
           - Preference tuning
           - Score variation
           - Stakeholder alignment
        
        8. SYSTEM INTEGRATION (Example 8):
           - Comprehensive summary
           - Statistics tracking
           - Pareto analysis
           - System overview
           - Decision support
        
        KEY BENEFITS:
        ✓ Realistic complex decision modeling
        ✓ Explicit tradeoff consideration
        ✓ Multiple solution alternatives
        ✓ Stakeholder preference integration
        ✓ Better than single-objective
        ✓ Transparent decision criteria
        ✓ Flexible priority adjustment
        ✓ Pareto-optimal solutions
        
        USE CASES:
        • Resource allocation with constraints
        • Project planning (cost/time/quality)
        • Route planning (time/cost/comfort)
        • Product design tradeoffs
        • Investment decisions (risk/return)
        • Scheduling (efficiency/fairness)
        • ML model selection (accuracy/interpretability)
        • System design (performance/cost/reliability)
        
        OBJECTIVE TYPES:
        → Maximize: Quality, performance, revenue
        → Minimize: Cost, time, risk, resource usage
        → Target: Hit specific value
        → Constraint: Must satisfy (hard requirement)
        
        PLANNING APPROACHES:
        → Weighted Sum: Combine with weights
        → Lexicographic: Priority ordering
        → Pareto Optimal: Non-dominated set
        → Satisficing: Good enough
        → Goal Programming: Target achievement
        
        BEST PRACTICES:
        1. Define clear, measurable objectives
        2. Implement efficient Pareto filtering
        3. Provide tradeoff visualizations
        4. Allow dynamic weight adjustment
        5. Cache evaluated plans for reuse
        6. Support constraint hierarchies
        7. Enable sensitivity analysis
        8. Log decision rationale
        
        TRADE-OFFS:
        • Thoroughness vs. computational cost
        • Number of objectives vs. complexity
        • Weight precision vs. simplicity
        • Exact vs. approximate solutions
        
        PRODUCTION CONSIDERATIONS:
        → Normalize objectives to comparable scales
        → Implement efficient Pareto algorithms
        → Provide interactive weight tuning
        → Visualize tradeoff spaces (2D/3D plots)
        → Cache evaluation results
        → Support what-if analysis
        → Enable constraint relaxation
        → Log all evaluation details
        → Provide explanation of tradeoffs
        → Support group decision making
        
        This pattern enables agents to make realistic decisions when
        multiple conflicting objectives must be balanced, providing
        transparency into tradeoffs and multiple solution alternatives.
        """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_multi_objective_planning()
    
        

    Agentic Design Pattern: Multi Objective Planning

    Pattern 107: Multi Objective Planning – Line-by-Line Explanation

    Overview

    This document provides a detailed line-by-line explanation of the Multi Objective Planning pattern implementation.


    Pattern Description

    Pattern 107: Multi-Objective Planning

    Description: The Multi-Objective Planning pattern enables agents to plan and make decisions when multiple, often conflicting objectives must be balanced. Instead of optimizing for a single goal, the agent must consider tradeoffs between competing objectives like cost vs. quality, speed vs. accuracy, or risk vs. reward.

    This pattern is essential for realistic decision-making where perfect solutions
    don't exist and every choice involves tradeoffs. The agent must evaluate plans
    across multiple dimensions, understand Pareto-optimal solutions, and make
    informed decisions about which tradeoffs are acceptable given the context.
    
    Multi-objective planning involves objective weighting, constraint satisfaction,
    Pareto optimization, preference elicitation, and tradeoff analysis. The agent
    can present multiple plan options with different tradeoff profiles, allowing
    humans or automated systems to select based on current priorities.
    

    Key Components: 1. Objective Set: Multiple goals to optimize 2. Objective Weights: Relative importance of each objective 3. Plan Evaluator: Scores plans across all objectives 4. Pareto Filter: Identifies non-dominated solutions 5. Tradeoff Analyzer: Analyzes competing objectives 6. Constraint Checker: Ensures hard constraints met 7. Preference Engine: Incorporates user preferences

    Objective Types: 1. Maximization: Quality, performance, completeness 2. Minimization: Cost, time, risk, resource usage 3. Target: Hit specific value (with tolerance) 4. Constraint: Must satisfy (hard requirement)

    Common Objective Combinations: – Cost vs. Quality vs. Speed (iron triangle) – Risk vs. Reward – Precision vs. Recall – Exploration vs. Exploitation – Short-term vs. Long-term goals – Individual vs. Group benefit

    Planning Approaches: 1. Weighted Sum: Combine objectives with weights 2. Lexicographic: Prioritize objectives in order 3. Pareto Optimal: Non-dominated solutions 4. Satisficing: Good enough on all objectives 5. Goal Programming: Meet targets with deviations 6. Multi-Criteria Decision Making (MCDM)

    Use Cases: – Resource allocation with constraints – Project planning with multiple stakeholders – Route planning (time vs. cost vs. comfort) – Product design (features vs. complexity vs. cost) – Investment decisions (risk vs. return) – Scheduling (efficiency vs. fairness) – Machine learning (accuracy vs. interpretability)

    Advantages: – Realistic modeling of complex decisions – Explicit tradeoff consideration – Multiple solution alternatives – Stakeholder preference incorporation – Better than single-objective optimization – Transparent decision criteria – Flexible priority adjustment

    Challenges: – Objective quantification difficulty – Weight elicitation from users – Computational complexity – Visualizing multi-dimensional tradeoffs – Handling conflicting constraints – Determining Pareto frontier – Explaining tradeoff decisions

    LangChain Implementation: This implementation uses LangChain for: – LLM-based objective identification – Plan evaluation across dimensions – Tradeoff analysis and explanation – Natural language objective specification

    Production Considerations: – Define clear, measurable objectives – Implement efficient Pareto filtering – Provide tradeoff visualizations – Allow dynamic weight adjustment – Cache evaluated plans – Support constraint hierarchies – Enable sensitivity analysis – Log decision rationale


    File Statistics

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

    Module Documentation

    """
    Pattern 107: Multi-Objective Planning
    
    Description:
        The Multi-Objective Planning pattern enables agents to plan and make decisions
        when multiple, often conflicting objectives must be balanced. Instead of
        optimizing for a single goal, the agent must consider tradeoffs between
        competing objectives like cost vs. quality, speed vs. accuracy, or risk vs.
        reward.
    
        This pattern is essential for realistic decision-making where perfect solutions
        don't exist and every choice involves tradeoffs. The agent must evaluate plans
        across multiple dimensions, understand Pareto-optimal solutions, and make
        informed decisions about which tradeoffs are acceptable given the context.
    
        Multi-objective planning involves objective weighting, constraint satisfaction,
        Pareto optimization, preference elicitation, and tradeoff analysis. The agent
        can present multiple plan options with different tradeoff profiles, allowing
        humans or automated systems to select based on current priorities.
    
    Key Components:
        1. Objective Set: Multiple goals to optimize
        2. Objective Weights: Relative importance of each objective
        3. Plan Evaluator: Scores plans across all objectives
        4. Pareto Filter: Identifies non-dominated solutions
        5. Tradeoff Analyzer: Analyzes competing objectives
        6. Constraint Checker: Ensures hard constraints met
        7. Preference Engine: Incorporates user preferences
    
    Objective Types:
        1. Maximization: Quality, performance, completeness
        2. Minimization: Cost, time, risk, resource usage
        3. Target: Hit specific value (with tolerance)
        4. Constraint: Must satisfy (hard requirement)
    
    Common Objective Combinations:
        - Cost vs. Quality vs. Speed (iron triangle)
        - Risk vs. Reward
        - Precision vs. Recall
        - Exploration vs. Exploitation
        - Short-term vs. Long-term goals
        - Individual vs. Group benefit
    
    Planning Approaches:
        1. Weighted Sum: Combine objectives with weights
        2. Lexicographic: Prioritize objectives in order
        3. Pareto Optimal: Non-dominated solutions
        4. Satisficing: Good enough on all objectives
        5. Goal Programming: Meet targets with deviations
        6. Multi-Criteria Decision Making (MCDM)
    
    Use Cases:
        - Resource allocation with constraints
        - Project planning with multiple stakeholders
        - Route planning (time vs. cost vs. comfort)
        - Product design (features vs. complexity vs. cost)
        - Investment decisions (risk vs. return)
        - Scheduling (efficiency vs. fairness)
        - Machine learning (accuracy vs. interpretability)
    
    Advantages:
        - Realistic modeling of complex decisions
        - Explicit tradeoff consideration
        - Multiple solution alternatives
        - Stakeholder preference incorporation
        - Better than single-objective optimization
        - Transparent decision criteria
        - Flexible priority adjustment
    
    Challenges:
        - Objective quantification difficulty
        - Weight elicitation from users
        - Computational complexity
        - Visualizing multi-dimensional tradeoffs
        - Handling conflicting constraints
        - Determining Pareto frontier
        - Explaining tradeoff decisions
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based objective identification
        - Plan evaluation across dimensions
        - Tradeoff analysis and explanation
        - Natural language objective specification
    
    Production Considerations:
        - Define clear, measurable objectives
        - Implement efficient Pareto filtering
        - Provide tradeoff visualizations
        - Allow dynamic weight adjustment
        - Cache evaluated plans
        - Support constraint hierarchies
        - Enable sensitivity analysis
        - Log decision rationale
    """
    

    Purpose: This module implements the Multi Objective Planning pattern, providing a structured approach to agent design and implementation.


    Import Statements

    Standard Library Imports

    import dataclasses.dataclass
    import dataclasses.field
    import enum.Enum
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import typing.Any
    import typing.Dict
    import typing.List
    import typing.Optional
    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 – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Tuple: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: ObjectiveType

    Lines 110-115

    Inherits from: Enum

    Purpose:

    Type of optimization objective.
    

    Class: Objective

    Lines 119-169

    Purpose:

    Represents an optimization objective.
    
    Attributes:
        name: Objective name
        objective_type: Maximization, minimization, target, or constraint
        weight: Relative importance (0-1)
        target_value: Target value (for TARGET type)
        constraint_value: Threshold (for CONSTRAINT type)
        unit: Measurement unit
    

    Methods:

    evaluate(self, value)

    Lines 138-169

    Purpose:

    Evaluate objective given a value.
    
    Args:
        value: Actual value
    
    Returns:
        Normalized score (0-1), higher is better
    

    Parameters: – value: Value

    Returns: float


    Class: Plan

    Lines 173-193

    Purpose:

    Represents a plan with multiple objective values.
    
    Attributes:
        plan_id: Unique identifier
        description: Plan description
        objective_values: Values for each objective
        feasible: Whether all constraints are satisfied
        dominated: Whether dominated by another plan
    

    Methods:

    get_score(self, objective)

    Lines 190-193

    Purpose:

    Get normalized score for an objective.
    

    Parameters: – objective: Objective

    Returns: float


    Class: MultiObjectivePlanner

    Lines 196-499

    Purpose:

    Multi-objective planning system.
    
    This planner evaluates and compares plans across multiple objectives,
    identifies Pareto-optimal solutions, and analyzes tradeoffs.
    

    Methods:

    __init__(self, temperature)

    Lines 204-214

    Purpose:

    Initialize multi-objective planner.
    
    Args:
        temperature: LLM temperature
    

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

    add_objective(self, name, objective_type, weight, target_value, constraint_value, unit)

    Lines 216-248

    Purpose:

    Add an objective to optimize.
    
    Args:
        name: Objective name
        objective_type: Type of objective
        weight: Relative importance
        target_value: Target value (for TARGET)
        constraint_value: Constraint threshold
        unit: Measurement unit
    
    Returns:
        Created objective
    

    Parameters: – name: Name – objective_type: Objective type – weight: Weight – target_value: Target value – constraint_value: Constraint value – unit: Unit

    Returns: Objective

    create_plan(self, description, objective_values)

    Lines 250-284

    Purpose:

    Create a plan with objective values.
    
    Args:
        description: Plan description
        objective_values: Values for each objective
    
    Returns:
        Created plan
    

    Parameters: – description: Description – objective_values: Objective values

    Returns: Plan

    evaluate_plan(self, plan)

    Lines 286-316

    Purpose:

    Evaluate plan across all objectives.
    
    Args:
        plan: Plan to evaluate
    
    Returns:
        Evaluation results
    

    Parameters: – plan: Plan

    Returns: <ast.Subscript object at 0x000001A06D8107D0>

    find_pareto_optimal(self)

    Lines 318-358

    Purpose:

    Find Pareto-optimal (non-dominated) plans.
    
    Returns:
        List of Pareto-optimal plans
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06E094690>

    analyze_tradeoffs(self, plan1, plan2)

    Lines 360-404

    Purpose:

    Analyze tradeoffs between two plans.
    
    Args:
        plan1: First plan
        plan2: Second plan
    
    Returns:
        Tradeoff analysis
    

    Parameters: – plan1: Plan1 – plan2: Plan2

    Returns: <ast.Subscript object at 0x000001A06D659890>

    generate_plan_with_priorities(self, task, priority_objectives)

    Lines 406-457

    Purpose:

    Generate plan optimizing for priority objectives.
    
    Args:
        task: Task description
        priority_objectives: High-priority objective names
    
    Returns:
        Generated plan
    

    Parameters: – task: Task – priority_objectives: Priority objectives

    Returns: Plan

    rank_plans(self)

    Lines 459-476

    Purpose:

    Rank plans by weighted overall score.
    
    Returns:
        List of (plan, score) tuples, sorted by score
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D7FF210>

    get_summary(self)

    Lines 478-499

    Purpose:

    Get summary of plans and objectives.
    

    Parameters:

    Returns: str


    Functions

    demonstrate_multi_objective_planning()

    Lines 502-894

    Purpose:

    Demonstrate multi-objective planning pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Multi Objective Planning 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 106
    • Pattern 108

    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 Multi Objective Planning 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: 107_multi_objective_planning.py
    Total Lines: 899
    Pattern Category: AI Agent Design Pattern #107


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Contingency Planning

    
        """
    Pattern 106: Contingency Planning
    
    Description:
        The Contingency Planning pattern prepares agents to handle unexpected events
        and failures by creating alternative plans before they're needed. This pattern
        involves anticipating potential problems, creating backup plans, and enabling
        quick adaptation when things don't go as expected.
        
        Contingency planning is crucial for reliable agent systems that operate in
        uncertain, dynamic environments. Rather than reacting to failures after they
        occur, agents proactively identify risks, assess their likelihood and impact,
        and prepare alternative strategies. This reduces downtime, improves robustness,
        and enables graceful degradation when primary plans fail.
        
        The pattern includes risk identification, scenario planning, plan B/C/D
        creation, trigger condition specification, and rapid plan switching. It
        balances the cost of planning for contingencies against the benefit of
        being prepared for failures.
    
    Key Components:
        1. Risk Identifier: Detects potential failure points
        2. Scenario Generator: Creates alternative scenarios
        3. Contingency Creator: Develops backup plans
        4. Trigger Monitor: Watches for contingency conditions
        5. Plan Switcher: Activates alternative plans
        6. Impact Assessor: Evaluates risk severity
        7. Recovery Planner: Plans return to primary plan
    
    Contingency Types:
        1. Alternative Action: Different way to achieve same goal
        2. Fallback Goal: Reduced/modified objective
        3. Resource Substitution: Use different resources
        4. Path Alternative: Different sequence of steps
        5. Graceful Degradation: Accept reduced functionality
        6. Abort and Recover: Abandon task, clean up
    
    Risk Assessment:
        1. Likelihood: Probability of occurrence (low/medium/high)
        2. Impact: Severity of consequences (minor/moderate/critical)
        3. Priority: Likelihood × Impact
        4. Mitigation Cost: Resources to prepare contingency
        
    Planning Phases:
        1. Risk Identification: What could go wrong?
        2. Impact Analysis: How bad would it be?
        3. Contingency Development: What's plan B?
        4. Trigger Specification: When to switch plans?
        5. Monitoring: Watch for triggers
        6. Activation: Switch to contingency
        7. Recovery: Return to normal operations
    
    Use Cases:
        - Mission-critical operations
        - Uncertain environments
        - Resource-constrained systems
        - High-stakes decision making
        - Autonomous systems
        - Real-time operations
        - Safety-critical applications
    
    Advantages:
        - Reduces response time to failures
        - Improves system reliability
        - Enables graceful degradation
        - Proactive vs reactive approach
        - Builds resilience
        - Reduces costly downtime
        - Increases confidence
    
    Challenges:
        - Computational cost of planning
        - Predicting all possible failures
        - Balancing thoroughness vs. efficiency
        - Maintaining multiple plans
        - Detecting when to switch
        - Avoiding over-planning
        - Resource allocation for contingencies
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based risk identification
        - Alternative plan generation
        - Scenario analysis
        - Natural language contingency description
        
    Production Considerations:
        - Focus on high-impact, high-probability risks
        - Use tiered contingency levels (B, C, D plans)
        - Implement fast plan switching
        - Monitor trigger conditions efficiently
        - Test contingency plans regularly
        - Document all contingencies
        - Balance planning cost vs. benefit
        - Enable human override when needed
    """
    
    import os
    from typing import List, Dict, Any, Optional
    from dataclasses import dataclass, field
    from datetime import datetime
    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 RiskLevel(Enum):
        """Risk likelihood levels."""
        LOW = "low"
        MEDIUM = "medium"
        HIGH = "high"
    
    
    class ImpactLevel(Enum):
        """Impact severity levels."""
        MINOR = "minor"
        MODERATE = "moderate"
        CRITICAL = "critical"
    
    
    class PlanStatus(Enum):
        """Status of a plan."""
        PRIMARY = "primary"
        CONTINGENCY = "contingency"
        ACTIVE = "active"
        FAILED = "failed"
        SUCCEEDED = "succeeded"
    
    
    @dataclass
    class Risk:
        """
        Represents a potential risk or failure point.
        
        Attributes:
            risk_id: Unique identifier
            description: Risk description
            likelihood: Probability of occurrence
            impact: Severity of consequences
            trigger_condition: Condition that indicates risk occurred
            affected_steps: Plan steps affected by this risk
        """
        risk_id: str
        description: str
        likelihood: RiskLevel
        impact: ImpactLevel
        trigger_condition: str
        affected_steps: List[str] = field(default_factory=list)
        
        @property
        def priority(self) -> int:
            """Calculate risk priority."""
            likelihood_scores = {RiskLevel.LOW: 1, RiskLevel.MEDIUM: 2, RiskLevel.HIGH: 3}
            impact_scores = {ImpactLevel.MINOR: 1, ImpactLevel.MODERATE: 2, ImpactLevel.CRITICAL: 3}
            return likelihood_scores[self.likelihood] * impact_scores[self.impact]
    
    
    @dataclass
    class Plan:
        """
        Represents a plan (primary or contingency).
        
        Attributes:
            plan_id: Unique identifier
            description: Plan description
            steps: List of action steps
            status: Plan status
            parent_plan_id: ID of plan this is contingency for
            triggers: Conditions that activate this plan
            cost: Estimated cost/resources
            success_criteria: How to know if plan succeeded
        """
        plan_id: str
        description: str
        steps: List[str]
        status: PlanStatus
        parent_plan_id: Optional[str] = None
        triggers: List[str] = field(default_factory=list)
        cost: float = 1.0
        success_criteria: List[str] = field(default_factory=list)
        activated_at: Optional[datetime] = None
        completed_at: Optional[datetime] = None
    
    
    class ContingencyPlanner:
        """
        Contingency planning system that creates and manages backup plans.
        
        This planner identifies risks, creates alternative plans, monitors
        for trigger conditions, and switches to contingencies when needed.
        """
        
        def __init__(self, temperature: float = 0.4):
            """
            Initialize contingency planner.
            
            Args:
                temperature: LLM temperature
            """
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.plans: Dict[str, Plan] = {}
            self.risks: Dict[str, Risk] = {}
            self.active_plan_id: Optional[str] = None
            self.plan_counter = 0
            self.risk_counter = 0
            self.event_log: List[Dict[str, Any]] = []
        
        def create_primary_plan(
            self,
            description: str,
            steps: List[str]
        ) -> Plan:
            """
            Create primary plan.
            
            Args:
                description: Plan description
                steps: Plan steps
                
            Returns:
                Created plan
            """
            self.plan_counter += 1
            plan = Plan(
                plan_id=f"plan_{self.plan_counter}",
                description=description,
                steps=steps,
                status=PlanStatus.PRIMARY
            )
            
            self.plans[plan.plan_id] = plan
            self.active_plan_id = plan.plan_id
            
            return plan
        
        def identify_risks(self, plan: Plan) -> List[Risk]:
            """
            Identify potential risks in a plan using LLM.
            
            Args:
                plan: Plan to analyze
                
            Returns:
                List of identified risks
            """
            prompt = ChatPromptTemplate.from_template(
                "Analyze this plan and identify 3-5 potential risks or failure points:\n\n"
                "Plan: {description}\n"
                "Steps:\n{steps}\n\n"
                "For each risk, provide:\n"
                "- Description of what could go wrong\n"
                "- Likelihood (low/medium/high)\n"
                "- Impact (minor/moderate/critical)\n"
                "- Trigger condition (how to detect it happened)\n\n"
                "Format each risk as:\n"
                "Risk: [description]\n"
                "Likelihood: [level]\n"
                "Impact: [level]\n"
                "Trigger: [condition]\n"
                "---\n"
            )
            
            steps_text = "\n".join(f"{i+1}. {step}" for i, step in enumerate(plan.steps))
            
            chain = prompt | self.llm | StrOutputParser()
            result = chain.invoke({
                "description": plan.description,
                "steps": steps_text
            })
            
            # Parse risks from LLM output
            risks = []
            risk_blocks = result.split("---")
            
            for block in risk_blocks:
                if not block.strip():
                    continue
                
                lines = [line.strip() for line in block.strip().split('\n')]
                risk_desc = ""
                likelihood = RiskLevel.MEDIUM
                impact = ImpactLevel.MODERATE
                trigger = ""
                
                for line in lines:
                    if line.startswith("Risk:"):
                        risk_desc = line[5:].strip()
                    elif line.startswith("Likelihood:"):
                        level = line[11:].strip().lower()
                        likelihood = RiskLevel(level) if level in [l.value for l in RiskLevel] else RiskLevel.MEDIUM
                    elif line.startswith("Impact:"):
                        level = line[7:].strip().lower()
                        impact = ImpactLevel(level) if level in [i.value for i in ImpactLevel] else ImpactLevel.MODERATE
                    elif line.startswith("Trigger:"):
                        trigger = line[8:].strip()
                
                if risk_desc and trigger:
                    self.risk_counter += 1
                    risk = Risk(
                        risk_id=f"risk_{self.risk_counter}",
                        description=risk_desc,
                        likelihood=likelihood,
                        impact=impact,
                        trigger_condition=trigger
                    )
                    self.risks[risk.risk_id] = risk
                    risks.append(risk)
            
            return risks
        
        def create_contingency_plan(
            self,
            primary_plan: Plan,
            risk: Risk,
            level: str = "B"
        ) -> Plan:
            """
            Create contingency plan for a specific risk.
            
            Args:
                primary_plan: Primary plan
                risk: Risk to mitigate
                level: Contingency level (B, C, D, etc.)
                
            Returns:
                Contingency plan
            """
            prompt = ChatPromptTemplate.from_template(
                "Create a contingency plan (Plan {level}) for this situation:\n\n"
                "Primary Plan: {plan_description}\n"
                "Risk: {risk_description}\n"
                "Trigger: {trigger}\n\n"
                "Design an alternative plan that:\n"
                "1. Achieves the same or similar goal\n"
                "2. Avoids or mitigates the identified risk\n"
                "3. Is practical and feasible\n\n"
                "Provide 3-5 concrete steps for the contingency plan.\n"
                "List each step on a new line, numbered.\n\n"
                "Steps:"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            result = chain.invoke({
                "level": level,
                "plan_description": primary_plan.description,
                "risk_description": risk.description,
                "trigger": risk.trigger_condition
            })
            
            # Parse steps
            lines = [line.strip() for line in result.strip().split('\n') if line.strip()]
            steps = []
            
            for line in lines:
                # Remove numbering
                step = line
                for prefix in ["1.", "2.", "3.", "4.", "5.", "1)", "2)", "3)", "4)", "5)", "-", "*"]:
                    if step.startswith(prefix):
                        step = step[len(prefix):].strip()
                        break
                if step:
                    steps.append(step)
            
            # Create contingency plan
            self.plan_counter += 1
            contingency = Plan(
                plan_id=f"plan_{self.plan_counter}",
                description=f"Plan {level}: {primary_plan.description} (contingency for {risk.description})",
                steps=steps,
                status=PlanStatus.CONTINGENCY,
                parent_plan_id=primary_plan.plan_id,
                triggers=[risk.trigger_condition]
            )
            
            self.plans[contingency.plan_id] = contingency
            
            return contingency
        
        def create_tiered_contingencies(
            self,
            primary_plan: Plan,
            num_tiers: int = 2
        ) -> List[Plan]:
            """
            Create multiple tiers of contingency plans.
            
            Args:
                primary_plan: Primary plan
                num_tiers: Number of contingency tiers
                
            Returns:
                List of contingency plans
            """
            # Identify risks
            risks = self.identify_risks(primary_plan)
            
            # Sort by priority
            risks.sort(key=lambda r: r.priority, reverse=True)
            
            # Create contingencies for top risks
            contingencies = []
            levels = ["B", "C", "D", "E", "F"]
            
            for i in range(min(num_tiers, len(risks), len(levels))):
                contingency = self.create_contingency_plan(
                    primary_plan,
                    risks[i],
                    level=levels[i]
                )
                contingencies.append(contingency)
            
            return contingencies
        
        def check_trigger_conditions(
            self,
            observed_events: List[str]
        ) -> Optional[Plan]:
            """
            Check if any contingency should be activated.
            
            Args:
                observed_events: List of observed events/conditions
                
            Returns:
                Contingency plan to activate, or None
            """
            # Check contingency plans for matching triggers
            contingency_plans = [
                p for p in self.plans.values()
                if p.status == PlanStatus.CONTINGENCY
            ]
            
            for plan in contingency_plans:
                for trigger in plan.triggers:
                    for event in observed_events:
                        if trigger.lower() in event.lower() or event.lower() in trigger.lower():
                            return plan
            
            return None
        
        def activate_contingency(
            self,
            contingency_plan: Plan,
            reason: str
        ):
            """
            Activate a contingency plan.
            
            Args:
                contingency_plan: Contingency to activate
                reason: Reason for activation
            """
            # Deactivate current plan
            if self.active_plan_id and self.active_plan_id in self.plans:
                self.plans[self.active_plan_id].status = PlanStatus.FAILED
            
            # Activate contingency
            contingency_plan.status = PlanStatus.ACTIVE
            contingency_plan.activated_at = datetime.now()
            self.active_plan_id = contingency_plan.plan_id
            
            # Log event
            self.event_log.append({
                "timestamp": datetime.now(),
                "event": "contingency_activated",
                "plan_id": contingency_plan.plan_id,
                "reason": reason
            })
        
        def simulate_execution(
            self,
            plan: Plan,
            simulate_failure: Optional[str] = None
        ) -> Dict[str, Any]:
            """
            Simulate plan execution.
            
            Args:
                plan: Plan to execute
                simulate_failure: Simulate failure at this step
                
            Returns:
                Execution result
            """
            results = []
            
            for i, step in enumerate(plan.steps):
                # Simulate failure
                if simulate_failure and simulate_failure.lower() in step.lower():
                    return {
                        "success": False,
                        "completed_steps": results,
                        "failed_at": step,
                        "reason": f"Simulated failure at: {step}"
                    }
                
                # Successful step
                results.append({
                    "step": step,
                    "status": "completed",
                    "timestamp": datetime.now()
                })
            
            plan.status = PlanStatus.SUCCEEDED
            plan.completed_at = datetime.now()
            
            return {
                "success": True,
                "completed_steps": results,
                "plan_id": plan.plan_id
            }
        
        def get_plan_summary(self) -> str:
            """Get summary of all plans."""
            lines = []
            
            # Primary plans
            primary = [p for p in self.plans.values() if p.status in [PlanStatus.PRIMARY, PlanStatus.ACTIVE]]
            if primary:
                lines.append("PRIMARY PLAN:")
                for plan in primary:
                    active = " (ACTIVE)" if plan.plan_id == self.active_plan_id else ""
                    lines.append(f"  {plan.plan_id}{active}: {plan.description}")
                    for i, step in enumerate(plan.steps, 1):
                        lines.append(f"    {i}. {step}")
            
            # Contingencies
            contingencies = [p for p in self.plans.values() if p.status == PlanStatus.CONTINGENCY]
            if contingencies:
                lines.append("\nCONTINGENCY PLANS:")
                for plan in contingencies:
                    lines.append(f"  {plan.plan_id}: {plan.description}")
                    if plan.triggers:
                        lines.append(f"    Triggers: {', '.join(plan.triggers[:2])}...")
                    for i, step in enumerate(plan.steps, 1):
                        lines.append(f"    {i}. {step}")
            
            return "\n".join(lines)
        
        def get_risk_summary(self) -> str:
            """Get summary of identified risks."""
            lines = ["IDENTIFIED RISKS (by priority):"]
            
            risks = sorted(self.risks.values(), key=lambda r: r.priority, reverse=True)
            
            for risk in risks:
                priority = risk.priority
                lines.append(
                    f"\n  [{priority}] {risk.description}\n"
                    f"      Likelihood: {risk.likelihood.value}, Impact: {risk.impact.value}\n"
                    f"      Trigger: {risk.trigger_condition}"
                )
            
            return "\n".join(lines)
    
    
    def demonstrate_contingency_planning():
        """Demonstrate contingency planning pattern."""
        
        print("=" * 80)
        print("CONTINGENCY PLANNING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic contingency planning
        print("\n" + "=" * 80)
        print("Example 1: Creating Primary and Contingency Plans")
        print("=" * 80)
        
        planner = ContingencyPlanner()
        
        # Create primary plan
        primary = planner.create_primary_plan(
            "Deploy new software version",
            [
                "Run automated tests",
                "Build production package",
                "Deploy to staging",
                "Run smoke tests",
                "Deploy to production"
            ]
        )
        
        print("\nPrimary Plan:")
        print(f"  {primary.description}")
        for i, step in enumerate(primary.steps, 1):
            print(f"  {i}. {step}")
        
        print("\nIdentifying risks...")
        risks = planner.identify_risks(primary)
        
        print(f"\nIdentified {len(risks)} risks:")
        for risk in risks:
            print(f"  • {risk.description}")
            print(f"    Likelihood: {risk.likelihood.value}, Impact: {risk.impact.value}")
            print(f"    Priority: {risk.priority}")
        
        # Create contingency for highest priority risk
        if risks:
            top_risk = max(risks, key=lambda r: r.priority)
            contingency = planner.create_contingency_plan(primary, top_risk, "B")
            
            print(f"\nContingency Plan B (for: {top_risk.description}):")
            for i, step in enumerate(contingency.steps, 1):
                print(f"  {i}. {step}")
        
        # Example 2: Tiered contingencies
        print("\n" + "=" * 80)
        print("Example 2: Multi-Tier Contingency Planning")
        print("=" * 80)
        
        planner2 = ContingencyPlanner()
        
        primary2 = planner2.create_primary_plan(
            "Complete important presentation",
            [
                "Prepare slides",
                "Rehearse presentation",
                "Set up projector",
                "Deliver presentation"
            ]
        )
        
        print("\nCreating tiered contingencies...")
        contingencies = planner2.create_tiered_contingencies(primary2, num_tiers=3)
        
        print(f"\nCreated {len(contingencies)} contingency tiers")
        print(planner2.get_plan_summary())
        
        # Example 3: Trigger detection and activation
        print("\n" + "=" * 80)
        print("Example 3: Contingency Activation")
        print("=" * 80)
        
        planner3 = ContingencyPlanner()
        
        primary3 = planner3.create_primary_plan(
            "Travel to meeting",
            [
                "Drive to meeting location",
                "Park car",
                "Enter building"
            ]
        )
        
        # Create simple contingency
        planner3.risk_counter += 1
        traffic_risk = Risk(
            risk_id=f"risk_{planner3.risk_counter}",
            description="Heavy traffic delays arrival",
            likelihood=RiskLevel.MEDIUM,
            impact=ImpactLevel.MODERATE,
            trigger_condition="traffic jam detected"
        )
        planner3.risks[traffic_risk.risk_id] = traffic_risk
        
        contingency3 = planner3.create_contingency_plan(primary3, traffic_risk, "B")
        
        print("\nPrimary Plan:")
        print(f"  {primary3.description}")
        
        print("\nContingency Plan B:")
        print(f"  {contingency3.description}")
        
        print("\nSimulating execution with problem...")
        observed_events = ["traffic jam detected", "delay expected"]
        
        triggered_plan = planner3.check_trigger_conditions(observed_events)
        
        if triggered_plan:
            print(f"\n⚠️  Trigger detected! Activating: {triggered_plan.plan_id}")
            planner3.activate_contingency(triggered_plan, "Traffic jam encountered")
            print(f"  Now executing: {triggered_plan.description}")
        
        # Example 4: Risk prioritization
        print("\n" + "=" * 80)
        print("Example 4: Risk Assessment and Prioritization")
        print("=" * 80)
        
        planner4 = ContingencyPlanner()
        
        primary4 = planner4.create_primary_plan(
            "Launch product campaign",
            [
                "Finalize marketing materials",
                "Schedule social media posts",
                "Send press releases",
                "Monitor campaign metrics"
            ]
        )
        
        print("\nAnalyzing risks...")
        risks4 = planner4.identify_risks(primary4)
        
        print(planner4.get_risk_summary())
        
        # Example 5: Execution with fallback
        print("\n" + "=" * 80)
        print("Example 5: Execution with Automatic Fallback")
        print("=" * 80)
        
        planner5 = ContingencyPlanner()
        
        primary5 = planner5.create_primary_plan(
            "Process data file",
            [
                "Download file from server",
                "Parse file contents",
                "Validate data",
                "Store in database"
            ]
        )
        
        # Create contingency
        planner5.risk_counter += 1
        download_risk = Risk(
            risk_id=f"risk_{planner5.risk_counter}",
            description="File download fails",
            likelihood=RiskLevel.MEDIUM,
            impact=ImpactLevel.MODERATE,
            trigger_condition="download fails"
        )
        
        contingency5 = Plan(
            plan_id="plan_backup",
            description="Plan B: Use cached file",
            steps=[
                "Retrieve file from cache",
                "Parse file contents",
                "Validate data",
                "Store in database"
            ],
            status=PlanStatus.CONTINGENCY,
            parent_plan_id=primary5.plan_id,
            triggers=["download fails"]
        )
        planner5.plans[contingency5.plan_id] = contingency5
        
        print("\nAttempting primary plan...")
        result = planner5.simulate_execution(primary5, simulate_failure="download")
        
        if not result["success"]:
            print(f"❌ Primary plan failed: {result['reason']}")
            
            # Check for contingency
            events = ["download fails"]
            triggered = planner5.check_trigger_conditions(events)
            
            if triggered:
                print(f"\n✓ Activating contingency: {triggered.plan_id}")
                planner5.activate_contingency(triggered, result["reason"])
                
                print("\nAttempting contingency plan...")
                result2 = planner5.simulate_execution(triggered)
                
                if result2["success"]:
                    print(f"✅ Contingency plan succeeded!")
        
        # Example 6: Graceful degradation
        print("\n" + "=" * 80)
        print("Example 6: Graceful Degradation Strategy")
        print("=" * 80)
        
        planner6 = ContingencyPlanner()
        
        primary6 = planner6.create_primary_plan(
            "Generate comprehensive report",
            [
                "Collect data from all sources",
                "Perform advanced analytics",
                "Create interactive visualizations",
                "Generate PDF report"
            ]
        )
        
        # Degraded plan
        degraded = Plan(
            plan_id="plan_degraded",
            description="Plan B: Generate basic report (degraded)",
            steps=[
                "Collect data from primary source only",
                "Perform basic statistics",
                "Create simple charts",
                "Generate text report"
            ],
            status=PlanStatus.CONTINGENCY,
            parent_plan_id=primary6.plan_id,
            triggers=["data source unavailable", "time constraint"]
        )
        planner6.plans[degraded.plan_id] = degraded
        
        print("\nPrimary Plan (Full Functionality):")
        for i, step in enumerate(primary6.steps, 1):
            print(f"  {i}. {step}")
        
        print("\nDegraded Plan (Reduced Functionality):")
        for i, step in enumerate(degraded.steps, 1):
            print(f"  {i}. {step}")
        
        print("\nStrategy: Accept reduced functionality to meet deadline")
        
        # Example 7: Multiple contingency levels
        print("\n" + "=" * 80)
        print("Example 7: Cascade of Contingencies")
        print("=" * 80)
        
        planner7 = ContingencyPlanner()
        
        primary7 = planner7.create_primary_plan(
            "Connect to service",
            ["Connect to primary server"]
        )
        
        plan_b = Plan(
            plan_id="plan_b",
            description="Plan B: Connect to backup server",
            steps=["Connect to backup server"],
            status=PlanStatus.CONTINGENCY,
            parent_plan_id=primary7.plan_id,
            triggers=["primary server down"]
        )
        
        plan_c = Plan(
            plan_id="plan_c",
            description="Plan C: Use cached data",
            steps=["Load cached data from local storage"],
            status=PlanStatus.CONTINGENCY,
            parent_plan_id=plan_b.plan_id,
            triggers=["backup server down"]
        )
        
        plan_d = Plan(
            plan_id="plan_d",
            description="Plan D: Operate in offline mode",
            steps=["Enable offline mode", "Queue operations"],
            status=PlanStatus.CONTINGENCY,
            parent_plan_id=plan_c.plan_id,
            triggers=["no cached data"]
        )
        
        planner7.plans.update({
            plan_b.plan_id: plan_b,
            plan_c.plan_id: plan_c,
            plan_d.plan_id: plan_d
        })
        
        print("\nContingency Cascade:")
        print("  Primary: Connect to primary server")
        print("  Plan B: Connect to backup server (if primary fails)")
        print("  Plan C: Use cached data (if backup fails)")
        print("  Plan D: Operate offline (if no cache)")
        print("\nStrategy: Progressive degradation with multiple fallback levels")
        
        # Example 8: Contingency metrics
        print("\n" + "=" * 80)
        print("Example 8: Contingency Planning Metrics")
        print("=" * 80)
        
        planner8 = ContingencyPlanner()
        
        primary8 = planner8.create_primary_plan(
            "Complete project milestone",
            [
                "Finish feature implementation",
                "Write documentation",
                "Conduct code review",
                "Merge to main branch"
            ]
        )
        
        # Create multiple contingencies
        planner8.create_tiered_contingencies(primary8, num_tiers=3)
        
        print("\nContingency Planning Statistics:")
        print(f"  Total Plans: {len(planner8.plans)}")
        print(f"  Primary Plans: {sum(1 for p in planner8.plans.values() if p.status == PlanStatus.PRIMARY)}")
        print(f"  Contingency Plans: {sum(1 for p in planner8.plans.values() if p.status == PlanStatus.CONTINGENCY)}")
        print(f"  Identified Risks: {len(planner8.risks)}")
        print(f"  High-Priority Risks: {sum(1 for r in planner8.risks.values() if r.priority >= 6)}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY: Contingency Planning Pattern")
        print("=" * 80)
        
        summary = """
        The Contingency Planning pattern demonstrated:
        
        1. BASIC CONTINGENCIES (Example 1):
           - Risk identification using LLM
           - Likelihood and impact assessment
           - Priority calculation
           - Alternative plan creation
           - Trigger condition specification
        
        2. TIERED PLANNING (Example 2):
           - Multiple contingency levels (B, C, D)
           - Risk-based prioritization
           - Comprehensive backup strategies
           - Structured alternatives
        
        3. TRIGGER ACTIVATION (Example 3):
           - Real-time monitoring
           - Condition matching
           - Automatic plan switching
           - Event-driven adaptation
           - Seamless transition
        
        4. RISK PRIORITIZATION (Example 4):
           - Priority scoring (likelihood × impact)
           - Risk ranking
           - Resource allocation guidance
           - Focus on critical risks
        
        5. AUTOMATIC FALLBACK (Example 5):
           - Failure detection
           - Contingency activation
           - Execution retry
           - Success tracking
           - Resilient operation
        
        6. GRACEFUL DEGRADATION (Example 6):
           - Reduced functionality plans
           - Partial goal achievement
           - Quality-time tradeoffs
           - Acceptable alternatives
        
        7. CONTINGENCY CASCADE (Example 7):
           - Multi-level fallbacks
           - Progressive degradation
           - Chain of alternatives
           - Comprehensive coverage
        
        8. METRICS TRACKING (Example 8):
           - Plan statistics
           - Risk counts
           - Priority distribution
           - Coverage assessment
        
        KEY BENEFITS:
        ✓ Reduces response time to failures
        ✓ Improves system reliability
        ✓ Enables graceful degradation
        ✓ Proactive preparation
        ✓ Builds resilience
        ✓ Reduces costly downtime
        ✓ Increases confidence
        ✓ Supports high-stakes operations
        
        USE CASES:
        • Mission-critical operations
        • Uncertain environments
        • Resource-constrained systems
        • High-stakes decision making
        • Autonomous systems
        • Real-time operations
        • Safety-critical applications
        • Production deployments
        
        CONTINGENCY TYPES:
        → Alternative Action: Different approach
        → Fallback Goal: Modified objective
        → Resource Substitution: Different resources
        → Path Alternative: Different sequence
        → Graceful Degradation: Reduced functionality
        → Abort and Recover: Clean exit
        
        BEST PRACTICES:
        1. Focus on high-impact, high-probability risks
        2. Use tiered contingency levels (B, C, D)
        3. Implement fast plan switching
        4. Monitor trigger conditions efficiently
        5. Test contingency plans regularly
        6. Document all contingencies clearly
        7. Balance planning cost vs. benefit
        8. Enable human override capability
        
        TRADE-OFFS:
        • Planning cost vs. preparedness benefit
        • Coverage thoroughness vs. efficiency
        • Multiple plans vs. maintenance burden
        • Proactive vs. reactive approach
        
        PRODUCTION CONSIDERATIONS:
        → Implement efficient trigger monitoring
        → Cache contingency plans for quick access
        → Test contingencies in simulation
        → Monitor plan activation frequency
        → Track contingency success rates
        → Update plans based on experience
        → Provide clear plan switching notifications
        → Enable rollback to primary plan
        → Log all plan transitions
        → Support human decision overrides
        
        This pattern enables agents to handle unexpected failures
        gracefully by preparing alternative plans before they're needed,
        monitoring for trigger conditions, and switching quickly when
        problems occur.
        """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_contingency_planning()
    
        

    Agentic Design Pattern: Contingency Planning

    Pattern 106: Contingency Planning – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 106: Contingency Planning

    Description: The Contingency Planning pattern prepares agents to handle unexpected events and failures by creating alternative plans before they’re needed. This pattern involves anticipating potential problems, creating backup plans, and enabling quick adaptation when things don’t go as expected.

    Contingency planning is crucial for reliable agent systems that operate in
    uncertain, dynamic environments. Rather than reacting to failures after they
    occur, agents proactively identify risks, assess their likelihood and impact,
    and prepare alternative strategies. This reduces downtime, improves robustness,
    and enables graceful degradation when primary plans fail.
    
    The pattern includes risk identification, scenario planning, plan B/C/D
    creation, trigger condition specification, and rapid plan switching. It
    balances the cost of planning for contingencies against the benefit of
    being prepared for failures.
    

    Key Components: 1. Risk Identifier: Detects potential failure points 2. Scenario Generator: Creates alternative scenarios 3. Contingency Creator: Develops backup plans 4. Trigger Monitor: Watches for contingency conditions 5. Plan Switcher: Activates alternative plans 6. Impact Assessor: Evaluates risk severity 7. Recovery Planner: Plans return to primary plan

    Contingency Types: 1. Alternative Action: Different way to achieve same goal 2. Fallback Goal: Reduced/modified objective 3. Resource Substitution: Use different resources 4. Path Alternative: Different sequence of steps 5. Graceful Degradation: Accept reduced functionality 6. Abort and Recover: Abandon task, clean up

    Risk Assessment: 1. Likelihood: Probability of occurrence (low/medium/high) 2. Impact: Severity of consequences (minor/moderate/critical) 3. Priority: Likelihood × Impact 4. Mitigation Cost: Resources to prepare contingency

    Planning Phases: 1. Risk Identification: What could go wrong? 2. Impact Analysis: How bad would it be? 3. Contingency Development: What’s plan B? 4. Trigger Specification: When to switch plans? 5. Monitoring: Watch for triggers 6. Activation: Switch to contingency 7. Recovery: Return to normal operations

    Use Cases: – Mission-critical operations – Uncertain environments – Resource-constrained systems – High-stakes decision making – Autonomous systems – Real-time operations – Safety-critical applications

    Advantages: – Reduces response time to failures – Improves system reliability – Enables graceful degradation – Proactive vs reactive approach – Builds resilience – Reduces costly downtime – Increases confidence

    Challenges: – Computational cost of planning – Predicting all possible failures – Balancing thoroughness vs. efficiency – Maintaining multiple plans – Detecting when to switch – Avoiding over-planning – Resource allocation for contingencies

    LangChain Implementation: This implementation uses LangChain for: – LLM-based risk identification – Alternative plan generation – Scenario analysis – Natural language contingency description

    Production Considerations: – Focus on high-impact, high-probability risks – Use tiered contingency levels (B, C, D plans) – Implement fast plan switching – Monitor trigger conditions efficiently – Test contingency plans regularly – Document all contingencies – Balance planning cost vs. benefit – Enable human override when needed


    File Statistics

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

    Module Documentation

    """
    Pattern 106: Contingency Planning
    
    Description:
        The Contingency Planning pattern prepares agents to handle unexpected events
        and failures by creating alternative plans before they're needed. This pattern
        involves anticipating potential problems, creating backup plans, and enabling
        quick adaptation when things don't go as expected.
    
        Contingency planning is crucial for reliable agent systems that operate in
        uncertain, dynamic environments. Rather than reacting to failures after they
        occur, agents proactively identify risks, assess their likelihood and impact,
        and prepare alternative strategies. This reduces downtime, improves robustness,
        and enables graceful degradation when primary plans fail.
    
        The pattern includes risk identification, scenario planning, plan B/C/D
        creation, trigger condition specification, and rapid plan switching. It
        balances the cost of planning for contingencies against the benefit of
        being prepared for failures.
    
    Key Components:
        1. Risk Identifier: Detects potential failure points
        2. Scenario Generator: Creates alternative scenarios
        3. Contingency Creator: Develops backup plans
        4. Trigger Monitor: Watches for contingency conditions
        5. Plan Switcher: Activates alternative plans
        6. Impact Assessor: Evaluates risk severity
        7. Recovery Planner: Plans return to primary plan
    
    Contingency Types:
        1. Alternative Action: Different way to achieve same goal
        2. Fallback Goal: Reduced/modified objective
        3. Resource Substitution: Use different resources
        4. Path Alternative: Different sequence of steps
        5. Graceful Degradation: Accept reduced functionality
        6. Abort and Recover: Abandon task, clean up
    
    Risk Assessment:
        1. Likelihood: Probability of occurrence (low/medium/high)
        2. Impact: Severity of consequences (minor/moderate/critical)
        3. Priority: Likelihood × Impact
        4. Mitigation Cost: Resources to prepare contingency
    
    Planning Phases:
        1. Risk Identification: What could go wrong?
        2. Impact Analysis: How bad would it be?
        3. Contingency Development: What's plan B?
        4. Trigger Specification: When to switch plans?
        5. Monitoring: Watch for triggers
        6. Activation: Switch to contingency
        7. Recovery: Return to normal operations
    
    Use Cases:
        - Mission-critical operations
        - Uncertain environments
        - Resource-constrained systems
        - High-stakes decision making
        - Autonomous systems
        - Real-time operations
        - Safety-critical applications
    
    Advantages:
        - Reduces response time to failures
        - Improves system reliability
        - Enables graceful degradation
        - Proactive vs reactive approach
        - Builds resilience
        - Reduces costly downtime
        - Increases confidence
    
    Challenges:
        - Computational cost of planning
        - Predicting all possible failures
        - Balancing thoroughness vs. efficiency
        - Maintaining multiple plans
        - Detecting when to switch
        - Avoiding over-planning
        - Resource allocation for contingencies
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based risk identification
        - Alternative plan generation
        - Scenario analysis
        - Natural language contingency description
    
    Production Considerations:
        - Focus on high-impact, high-probability risks
        - Use tiered contingency levels (B, C, D plans)
        - Implement fast plan switching
        - Monitor trigger conditions efficiently
        - Test contingency plans regularly
        - Document all contingencies
        - Balance planning cost vs. benefit
        - Enable human override when needed
    """
    

    Purpose: This module implements the Contingency Planning 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 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 – 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: RiskLevel

    Lines 112-116

    Inherits from: Enum

    Purpose:

    Risk likelihood levels.
    

    Class: ImpactLevel

    Lines 119-123

    Inherits from: Enum

    Purpose:

    Impact severity levels.
    

    Class: PlanStatus

    Lines 126-132

    Inherits from: Enum

    Purpose:

    Status of a plan.
    

    Class: Risk

    Lines 136-160

    Purpose:

    Represents a potential risk or failure point.
    
    Attributes:
        risk_id: Unique identifier
        description: Risk description
        likelihood: Probability of occurrence
        impact: Severity of consequences
        trigger_condition: Condition that indicates risk occurred
        affected_steps: Plan steps affected by this risk
    

    Methods:

    priority(self)

    Lines 156-160

    Decorators: property

    Purpose:

    Calculate risk priority.
    

    Parameters:

    Returns: int


    Class: Plan

    Lines 164-187

    Purpose:

    Represents a plan (primary or contingency).
    
    Attributes:
        plan_id: Unique identifier
        description: Plan description
        steps: List of action steps
        status: Plan status
        parent_plan_id: ID of plan this is contingency for
        triggers: Conditions that activate this plan
        cost: Estimated cost/resources
        success_criteria: How to know if plan succeeded
    

    Class: ContingencyPlanner

    Lines 190-559

    Purpose:

    Contingency planning system that creates and manages backup plans.
    
    This planner identifies risks, creates alternative plans, monitors
    for trigger conditions, and switches to contingencies when needed.
    

    Methods:

    __init__(self, temperature)

    Lines 198-211

    Purpose:

    Initialize contingency planner.
    
    Args:
        temperature: LLM temperature
    

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

    create_primary_plan(self, description, steps)

    Lines 213-239

    Purpose:

    Create primary plan.
    
    Args:
        description: Plan description
        steps: Plan steps
    
    Returns:
        Created plan
    

    Parameters: – description: Description – steps: Steps

    Returns: Plan

    identify_risks(self, plan)

    Lines 241-314

    Purpose:

    Identify potential risks in a plan using LLM.
    
    Args:
        plan: Plan to analyze
    
    Returns:
        List of identified risks
    

    Parameters: – plan: Plan

    Returns: <ast.Subscript object at 0x000001A06D6AB310>

    create_contingency_plan(self, primary_plan, risk, level)

    Lines 316-382

    Purpose:

    Create contingency plan for a specific risk.
    
    Args:
        primary_plan: Primary plan
        risk: Risk to mitigate
        level: Contingency level (B, C, D, etc.)
    
    Returns:
        Contingency plan
    

    Parameters: – primary_plan: Primary plan – risk: Risk – level: Level

    Returns: Plan

    create_tiered_contingencies(self, primary_plan, num_tiers)

    Lines 384-417

    Purpose:

    Create multiple tiers of contingency plans.
    
    Args:
        primary_plan: Primary plan
        num_tiers: Number of contingency tiers
    
    Returns:
        List of contingency plans
    

    Parameters: – primary_plan: Primary plan – num_tiers: Num tiers

    Returns: <ast.Subscript object at 0x000001A06D608C50>

    check_trigger_conditions(self, observed_events)

    Lines 419-444

    Purpose:

    Check if any contingency should be activated.
    
    Args:
        observed_events: List of observed events/conditions
    
    Returns:
        Contingency plan to activate, or None
    

    Parameters: – observed_events: Observed events

    Returns: <ast.Subscript object at 0x000001A06D83C910>

    activate_contingency(self, contingency_plan, reason)

    Lines 446-473

    Purpose:

    Activate a contingency plan.
    
    Args:
        contingency_plan: Contingency to activate
        reason: Reason for activation
    

    Parameters: – contingency_plan: Contingency plan – reason: Reason

    simulate_execution(self, plan, simulate_failure)

    Lines 475-516

    Purpose:

    Simulate plan execution.
    
    Args:
        plan: Plan to execute
        simulate_failure: Simulate failure at this step
    
    Returns:
        Execution result
    

    Parameters: – plan: Plan – simulate_failure: Simulate failure

    Returns: <ast.Subscript object at 0x000001A06D156C50>

    get_plan_summary(self)

    Lines 518-543

    Purpose:

    Get summary of all plans.
    

    Parameters:

    Returns: str

    get_risk_summary(self)

    Lines 545-559

    Purpose:

    Get summary of identified risks.
    

    Parameters:

    Returns: str


    Functions

    demonstrate_contingency_planning()

    Lines 562-1006

    Purpose:

    Demonstrate contingency planning pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Contingency Planning 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 105
    • Pattern 107

    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 Contingency Planning 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: 106_contingency_planning.py
    Total Lines: 1011
    Pattern Category: AI Agent Design Pattern #106


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Hierarchical Planning

    
        """
    Pattern 105: Hierarchical Planning
    
    Description:
        The Hierarchical Planning pattern breaks down complex goals into a hierarchy
        of sub-goals and actions at multiple levels of abstraction. This pattern
        enables agents to handle long-horizon tasks by decomposing them into manageable
        pieces, planning at different levels of granularity, and progressively refining
        high-level strategies into concrete actions.
        
        Hierarchical planning is inspired by hierarchical task networks (HTN) and
        goal decomposition techniques. It allows agents to reason about problems at
        multiple scales: strategic (what to achieve), tactical (how to achieve it),
        and operational (specific actions). Each level focuses on appropriate
        abstractions, making planning more tractable and interpretable.
        
        This pattern is particularly effective for complex, multi-step tasks where
        flat planning would result in intractably large search spaces. By organizing
        goals hierarchically, the agent can plan efficiently, adapt at different
        levels, and explain its reasoning at appropriate levels of detail.
    
    Key Components:
        1. Goal Hierarchy: Tree structure of goals and sub-goals
        2. Abstraction Levels: Strategic, tactical, operational layers
        3. Decomposition Rules: How to break goals into sub-goals
        4. Level Planner: Plans at each abstraction level
        5. Refinement Engine: Converts abstract plans to concrete actions
        6. Execution Monitor: Tracks progress at all levels
        7. Re-planning Trigger: Identifies when to replan at each level
    
    Abstraction Levels:
        1. Strategic Level: High-level goals and objectives
           - What needs to be achieved
           - Overall strategy and approach
           - Long-term planning horizon
        
        2. Tactical Level: Sub-goals and procedures
           - How to achieve strategic goals
           - Intermediate milestones
           - Medium-term planning
        
        3. Operational Level: Concrete actions
           - Specific actions to execute
           - Immediate next steps
           - Short-term execution
    
    Planning Phases:
        1. Goal Decomposition: Break high-level goal into sub-goals
        2. Level Planning: Plan at each abstraction level
        3. Plan Refinement: Refine abstract plans into detailed plans
        4. Execution: Execute lowest-level actions
        5. Monitoring: Track progress at all levels
        6. Re-planning: Adapt when needed at appropriate level
    
    Use Cases:
        - Complex project planning
        - Multi-step task execution
        - Robotic mission planning
        - Software development workflows
        - Travel itinerary planning
        - Research project organization
        - Business process automation
    
    Advantages:
        - Manages complexity through abstraction
        - More efficient than flat planning
        - Allows reasoning at appropriate granularity
        - Easier to explain and understand
        - Facilitates partial replanning
        - Modular and reusable sub-plans
        - Scales to long-horizon tasks
    
    Challenges:
        - Defining appropriate abstraction levels
        - Goal decomposition complexity
        - Ensuring consistency across levels
        - Handling dependencies between sub-goals
        - Determining when to replan at each level
        - Balancing planning depth vs. efficiency
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based goal decomposition
        - Multi-level plan generation
        - Plan refinement and detailing
        - Natural language planning
        
    Production Considerations:
        - Define clear abstraction levels for domain
        - Implement efficient goal decomposition
        - Track dependencies between sub-goals
        - Monitor execution at all levels
        - Enable replanning at appropriate level
        - Cache and reuse common sub-plans
        - Provide visualization of plan hierarchy
        - Handle failures at correct abstraction level
    """
    
    import os
    from typing import List, Dict, Any, Optional, Tuple
    from dataclasses import dataclass, field
    from datetime import datetime
    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 PlanLevel(Enum):
        """Planning abstraction levels."""
        STRATEGIC = "strategic"  # High-level goals
        TACTICAL = "tactical"    # Sub-goals and procedures
        OPERATIONAL = "operational"  # Concrete actions
    
    
    class GoalStatus(Enum):
        """Status of a goal."""
        PENDING = "pending"
        IN_PROGRESS = "in_progress"
        COMPLETED = "completed"
        FAILED = "failed"
        BLOCKED = "blocked"
    
    
    @dataclass
    class Goal:
        """
        Represents a goal at any level of the hierarchy.
        
        Attributes:
            goal_id: Unique identifier
            description: Goal description
            level: Abstraction level
            parent_id: Parent goal ID (None for root)
            children_ids: List of child goal IDs
            status: Current status
            priority: Priority (1-10)
            dependencies: IDs of goals that must complete first
            actions: Concrete actions (for operational level)
            metadata: Additional information
        """
        goal_id: str
        description: str
        level: PlanLevel
        parent_id: Optional[str] = None
        children_ids: List[str] = field(default_factory=list)
        status: GoalStatus = GoalStatus.PENDING
        priority: int = 5
        dependencies: List[str] = field(default_factory=list)
        actions: List[str] = field(default_factory=list)
        metadata: Dict[str, Any] = field(default_factory=dict)
        created_at: datetime = field(default_factory=datetime.now)
        completed_at: Optional[datetime] = None
    
    
    class HierarchicalPlanner:
        """
        Hierarchical planning system that plans at multiple abstraction levels.
        
        This planner decomposes high-level goals into hierarchies of sub-goals,
        plans at each level, and progressively refines abstract plans into
        concrete actions.
        """
        
        def __init__(self, temperature: float = 0.3):
            """
            Initialize hierarchical planner.
            
            Args:
                temperature: LLM temperature for planning
            """
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.goals: Dict[str, Goal] = {}
            self.goal_counter = 0
            self.execution_log: List[Dict[str, Any]] = []
        
        def create_goal(
            self,
            description: str,
            level: PlanLevel,
            parent_id: Optional[str] = None,
            priority: int = 5,
            dependencies: Optional[List[str]] = None
        ) -> Goal:
            """
            Create a new goal.
            
            Args:
                description: Goal description
                level: Abstraction level
                parent_id: Parent goal ID
                priority: Priority (1-10)
                dependencies: Goal dependencies
                
            Returns:
                Created goal
            """
            self.goal_counter += 1
            goal = Goal(
                goal_id=f"goal_{self.goal_counter}",
                description=description,
                level=level,
                parent_id=parent_id,
                priority=priority,
                dependencies=dependencies or []
            )
            
            self.goals[goal.goal_id] = goal
            
            # Update parent's children
            if parent_id and parent_id in self.goals:
                self.goals[parent_id].children_ids.append(goal.goal_id)
            
            return goal
        
        def decompose_goal(
            self,
            goal: Goal,
            target_level: PlanLevel
        ) -> List[Goal]:
            """
            Decompose a goal into sub-goals at lower abstraction level.
            
            Args:
                goal: Goal to decompose
                target_level: Target abstraction level for sub-goals
                
            Returns:
                List of sub-goals
            """
            # Use LLM to decompose goal
            prompt = ChatPromptTemplate.from_template(
                "You are a hierarchical planning assistant.\n\n"
                "Goal: {goal_description}\n"
                "Current Level: {current_level}\n"
                "Target Level: {target_level}\n\n"
                "Decompose this goal into 3-5 sub-goals at the {target_level} level.\n"
                "List each sub-goal on a new line, numbered.\n"
                "Sub-goals should be concrete, achievable, and collectively accomplish the parent goal.\n\n"
                "Sub-goals:"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            result = chain.invoke({
                "goal_description": goal.description,
                "current_level": goal.level.value,
                "target_level": target_level.value
            })
            
            # Parse sub-goals from LLM output
            lines = [line.strip() for line in result.strip().split('\n') if line.strip()]
            sub_goals = []
            
            for line in lines:
                # Remove numbering (e.g., "1. ", "1) ", etc.)
                description = line
                for prefix in ["1.", "2.", "3.", "4.", "5.", "1)", "2)", "3)", "4)", "5)", "-", "*"]:
                    if description.startswith(prefix):
                        description = description[len(prefix):].strip()
                        break
                
                if description:
                    sub_goal = self.create_goal(
                        description=description,
                        level=target_level,
                        parent_id=goal.goal_id,
                        priority=goal.priority
                    )
                    sub_goals.append(sub_goal)
            
            return sub_goals
        
        def plan_strategic(self, objective: str) -> Goal:
            """
            Create strategic-level plan.
            
            Args:
                objective: High-level objective
                
            Returns:
                Strategic goal
            """
            # Create strategic goal
            strategic_goal = self.create_goal(
                description=objective,
                level=PlanLevel.STRATEGIC,
                priority=10
            )
            
            return strategic_goal
        
        def plan_tactical(self, strategic_goal: Goal) -> List[Goal]:
            """
            Create tactical-level plans from strategic goal.
            
            Args:
                strategic_goal: Strategic goal to decompose
                
            Returns:
                List of tactical goals
            """
            tactical_goals = self.decompose_goal(strategic_goal, PlanLevel.TACTICAL)
            return tactical_goals
        
        def plan_operational(self, tactical_goal: Goal) -> List[Goal]:
            """
            Create operational-level plans from tactical goal.
            
            Args:
                tactical_goal: Tactical goal to decompose
                
            Returns:
                List of operational goals
            """
            operational_goals = self.decompose_goal(tactical_goal, PlanLevel.OPERATIONAL)
            
            # Generate concrete actions for operational goals
            for op_goal in operational_goals:
                op_goal.actions = self.generate_actions(op_goal)
            
            return operational_goals
        
        def generate_actions(self, operational_goal: Goal) -> List[str]:
            """
            Generate concrete actions for an operational goal.
            
            Args:
                operational_goal: Operational goal
                
            Returns:
                List of concrete actions
            """
            prompt = ChatPromptTemplate.from_template(
                "Generate 2-4 specific, concrete actions to accomplish this goal:\n\n"
                "Goal: {goal}\n\n"
                "List each action on a new line, numbered.\n"
                "Actions should be specific and executable.\n\n"
                "Actions:"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            result = chain.invoke({"goal": operational_goal.description})
            
            # Parse actions
            lines = [line.strip() for line in result.strip().split('\n') if line.strip()]
            actions = []
            
            for line in lines:
                # Remove numbering
                action = line
                for prefix in ["1.", "2.", "3.", "4.", "1)", "2)", "3)", "4)", "-", "*"]:
                    if action.startswith(prefix):
                        action = action[len(prefix):].strip()
                        break
                if action:
                    actions.append(action)
            
            return actions
        
        def create_full_hierarchy(self, objective: str) -> Goal:
            """
            Create complete hierarchical plan from objective.
            
            Args:
                objective: High-level objective
                
            Returns:
                Root strategic goal with full hierarchy
            """
            # Strategic level
            strategic_goal = self.plan_strategic(objective)
            
            # Tactical level
            tactical_goals = self.plan_tactical(strategic_goal)
            
            # Operational level
            for tactical_goal in tactical_goals:
                self.plan_operational(tactical_goal)
            
            return strategic_goal
        
        def get_next_action(self) -> Optional[Tuple[Goal, str]]:
            """
            Get next executable action from the plan.
            
            Returns:
                Tuple of (goal, action) or None if no actions available
            """
            # Find operational goals that are ready to execute
            operational_goals = [
                g for g in self.goals.values()
                if g.level == PlanLevel.OPERATIONAL and g.status == GoalStatus.PENDING
            ]
            
            # Sort by priority
            operational_goals.sort(key=lambda g: g.priority, reverse=True)
            
            for goal in operational_goals:
                # Check dependencies
                deps_satisfied = all(
                    self.goals[dep_id].status == GoalStatus.COMPLETED
                    for dep_id in goal.dependencies
                    if dep_id in self.goals
                )
                
                if deps_satisfied and goal.actions:
                    return (goal, goal.actions[0])
            
            return None
        
        def execute_action(self, goal: Goal, action: str) -> bool:
            """
            Execute an action (simulated).
            
            Args:
                goal: Goal being executed
                action: Action to execute
                
            Returns:
                Success status
            """
            # Mark goal as in progress
            if goal.status == GoalStatus.PENDING:
                goal.status = GoalStatus.IN_PROGRESS
            
            # Log execution
            self.execution_log.append({
                "timestamp": datetime.now(),
                "goal_id": goal.goal_id,
                "action": action,
                "level": goal.level.value
            })
            
            # Remove completed action
            if action in goal.actions:
                goal.actions.remove(action)
            
            # Mark goal complete if no more actions
            if not goal.actions:
                goal.status = GoalStatus.COMPLETED
                goal.completed_at = datetime.now()
                self.propagate_completion(goal)
            
            return True
        
        def propagate_completion(self, goal: Goal):
            """
            Propagate goal completion up the hierarchy.
            
            Args:
                goal: Completed goal
            """
            if not goal.parent_id or goal.parent_id not in self.goals:
                return
            
            parent = self.goals[goal.parent_id]
            
            # Check if all children are complete
            all_children_complete = all(
                self.goals[child_id].status == GoalStatus.COMPLETED
                for child_id in parent.children_ids
                if child_id in self.goals
            )
            
            if all_children_complete:
                parent.status = GoalStatus.COMPLETED
                parent.completed_at = datetime.now()
                self.propagate_completion(parent)
        
        def get_hierarchy_summary(self, goal_id: Optional[str] = None) -> str:
            """
            Get textual summary of goal hierarchy.
            
            Args:
                goal_id: Root goal ID (None for all roots)
                
            Returns:
                Formatted hierarchy string
            """
            def format_goal(goal: Goal, indent: int = 0) -> str:
                status_symbol = {
                    GoalStatus.PENDING: "⏳",
                    GoalStatus.IN_PROGRESS: "🔄",
                    GoalStatus.COMPLETED: "✅",
                    GoalStatus.FAILED: "❌",
                    GoalStatus.BLOCKED: "🚫"
                }
                
                symbol = status_symbol.get(goal.status, "❓")
                prefix = "  " * indent
                
                result = f"{prefix}{symbol} [{goal.level.value}] {goal.description}\n"
                
                # Add actions for operational level
                if goal.level == PlanLevel.OPERATIONAL and goal.actions:
                    for action in goal.actions:
                        result += f"{prefix}  → {action}\n"
                
                # Recursively add children
                for child_id in goal.children_ids:
                    if child_id in self.goals:
                        result += format_goal(self.goals[child_id], indent + 1)
                
                return result
            
            if goal_id:
                if goal_id in self.goals:
                    return format_goal(self.goals[goal_id])
                return "Goal not found"
            
            # Format all root goals
            root_goals = [g for g in self.goals.values() if g.parent_id is None]
            return "\n".join(format_goal(g) for g in root_goals)
        
        def get_statistics(self) -> Dict[str, Any]:
            """Get planning statistics."""
            goals_by_level = {level: 0 for level in PlanLevel}
            goals_by_status = {status: 0 for status in GoalStatus}
            
            for goal in self.goals.values():
                goals_by_level[goal.level] += 1
                goals_by_status[goal.status] += 1
            
            return {
                "total_goals": len(self.goals),
                "by_level": {level.value: count for level, count in goals_by_level.items()},
                "by_status": {status.value: count for status, count in goals_by_status.items()},
                "total_actions_executed": len(self.execution_log)
            }
    
    
    def demonstrate_hierarchical_planning():
        """Demonstrate hierarchical planning pattern."""
        
        print("=" * 80)
        print("HIERARCHICAL PLANNING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic three-level hierarchy
        print("\n" + "=" * 80)
        print("Example 1: Three-Level Planning Hierarchy")
        print("=" * 80)
        
        planner = HierarchicalPlanner()
        
        print("\nCreating hierarchical plan for: 'Build a web application'")
        
        # Strategic level
        strategic = planner.plan_strategic("Build a web application")
        print(f"\n[STRATEGIC] {strategic.description}")
        
        # Tactical level
        tactical = planner.plan_tactical(strategic)
        print(f"\n[TACTICAL] Decomposed into {len(tactical)} sub-goals:")
        for i, tac_goal in enumerate(tactical, 1):
            print(f"  {i}. {tac_goal.description}")
        
        # Operational level (for first tactical goal)
        if tactical:
            operational = planner.plan_operational(tactical[0])
            print(f"\n[OPERATIONAL] '{tactical[0].description}' decomposed into {len(operational)} goals:")
            for i, op_goal in enumerate(operational, 1):
                print(f"  {i}. {op_goal.description}")
                if op_goal.actions:
                    for action in op_goal.actions:
                        print(f"     → {action}")
        
        # Example 2: Complete hierarchy creation
        print("\n" + "=" * 80)
        print("Example 2: Complete Hierarchical Plan")
        print("=" * 80)
        
        planner2 = HierarchicalPlanner()
        
        objective = "Organize a team offsite event"
        print(f"\nObjective: {objective}\n")
        
        root = planner2.create_full_hierarchy(objective)
        
        print("Complete Hierarchy:")
        print(planner2.get_hierarchy_summary())
        
        # Example 3: Plan execution
        print("\n" + "=" * 80)
        print("Example 3: Executing Hierarchical Plan")
        print("=" * 80)
        
        planner3 = HierarchicalPlanner()
        
        # Create simpler plan for execution demo
        strategic = planner3.create_goal(
            "Complete project report",
            PlanLevel.STRATEGIC
        )
        
        tactical1 = planner3.create_goal(
            "Gather data and research",
            PlanLevel.TACTICAL,
            parent_id=strategic.goal_id
        )
        
        tactical2 = planner3.create_goal(
            "Write and format report",
            PlanLevel.TACTICAL,
            parent_id=strategic.goal_id,
            dependencies=[tactical1.goal_id]
        )
        
        # Operational goals for tactical1
        op1 = planner3.create_goal(
            "Collect data from databases",
            PlanLevel.OPERATIONAL,
            parent_id=tactical1.goal_id
        )
        op1.actions = ["Connect to database", "Run queries", "Export results"]
        
        op2 = planner3.create_goal(
            "Review literature",
            PlanLevel.OPERATIONAL,
            parent_id=tactical1.goal_id
        )
        op2.actions = ["Search papers", "Read abstracts"]
        
        print("Initial Plan:")
        print(planner3.get_hierarchy_summary())
        
        print("\nExecuting actions...")
        
        # Execute actions
        for i in range(5):
            next_item = planner3.get_next_action()
            if next_item:
                goal, action = next_item
                print(f"  {i+1}. Executing: {action} (for '{goal.description}')")
                planner3.execute_action(goal, action)
            else:
                break
        
        print("\nUpdated Plan:")
        print(planner3.get_hierarchy_summary())
        
        # Example 4: Priority-based planning
        print("\n" + "=" * 80)
        print("Example 4: Priority-Based Goal Selection")
        print("=" * 80)
        
        planner4 = HierarchicalPlanner()
        
        # Create goals with different priorities
        strategic = planner4.create_goal(
            "Launch product",
            PlanLevel.STRATEGIC,
            priority=10
        )
        
        high_priority = planner4.create_goal(
            "Fix critical bug",
            PlanLevel.OPERATIONAL,
            parent_id=strategic.goal_id,
            priority=9
        )
        high_priority.actions = ["Identify bug", "Write fix", "Test fix"]
        
        low_priority = planner4.create_goal(
            "Update documentation",
            PlanLevel.OPERATIONAL,
            parent_id=strategic.goal_id,
            priority=3
        )
        low_priority.actions = ["Review docs", "Make updates"]
        
        print("Goals by priority:")
        for goal in sorted(planner4.goals.values(), key=lambda g: g.priority, reverse=True):
            if goal.level == PlanLevel.OPERATIONAL:
                print(f"  Priority {goal.priority}: {goal.description}")
        
        print("\nExecution order (priority-based):")
        for i in range(5):
            next_item = planner4.get_next_action()
            if next_item:
                goal, action = next_item
                print(f"  {i+1}. [P{goal.priority}] {action}")
                planner4.execute_action(goal, action)
            else:
                break
        
        # Example 5: Dependency handling
        print("\n" + "=" * 80)
        print("Example 5: Goal Dependencies")
        print("=" * 80)
        
        planner5 = HierarchicalPlanner()
        
        strategic = planner5.create_goal("Deploy application", PlanLevel.STRATEGIC)
        
        goal_a = planner5.create_goal(
            "Run tests",
            PlanLevel.OPERATIONAL,
            parent_id=strategic.goal_id
        )
        goal_a.actions = ["Execute test suite"]
        
        goal_b = planner5.create_goal(
            "Build for production",
            PlanLevel.OPERATIONAL,
            parent_id=strategic.goal_id,
            dependencies=[goal_a.goal_id]
        )
        goal_b.actions = ["Compile code"]
        
        goal_c = planner5.create_goal(
            "Deploy to server",
            PlanLevel.OPERATIONAL,
            parent_id=strategic.goal_id,
            dependencies=[goal_b.goal_id]
        )
        goal_c.actions = ["Upload to server"]
        
        print("Dependency chain: Tests → Build → Deploy\n")
        print("Initial state:")
        for goal in [goal_a, goal_b, goal_c]:
            deps = ", ".join(goal.dependencies) if goal.dependencies else "none"
            print(f"  {goal.description}: dependencies=[{deps}], status={goal.status.value}")
        
        print("\nExecution respects dependencies:")
        for i in range(3):
            next_item = planner5.get_next_action()
            if next_item:
                goal, action = next_item
                print(f"  {i+1}. {action} (from '{goal.description}')")
                planner5.execute_action(goal, action)
        
        # Example 6: Progress monitoring
        print("\n" + "=" * 80)
        print("Example 6: Multi-Level Progress Monitoring")
        print("=" * 80)
        
        planner6 = HierarchicalPlanner()
        
        root = planner6.create_full_hierarchy("Write research paper")
        
        print("Initial Statistics:")
        stats = planner6.get_statistics()
        print(f"  Total Goals: {stats['total_goals']}")
        print(f"  By Level: {stats['by_level']}")
        print(f"  By Status: {stats['by_status']}")
        
        # Execute some actions
        print("\nExecuting 10 actions...")
        for _ in range(10):
            next_item = planner6.get_next_action()
            if next_item:
                goal, action = next_item
                planner6.execute_action(goal, action)
        
        print("\nUpdated Statistics:")
        stats = planner6.get_statistics()
        print(f"  Total Goals: {stats['total_goals']}")
        print(f"  By Status: {stats['by_status']}")
        print(f"  Actions Executed: {stats['total_actions_executed']}")
        
        # Example 7: Plan visualization
        print("\n" + "=" * 80)
        print("Example 7: Hierarchical Plan Visualization")
        print("=" * 80)
        
        planner7 = HierarchicalPlanner()
        
        # Create multi-level plan
        strategic = planner7.create_goal("Learn Python", PlanLevel.STRATEGIC)
        
        tac1 = planner7.create_goal("Master basics", PlanLevel.TACTICAL, parent_id=strategic.goal_id)
        tac2 = planner7.create_goal("Build projects", PlanLevel.TACTICAL, parent_id=strategic.goal_id)
        
        op1 = planner7.create_goal("Learn syntax", PlanLevel.OPERATIONAL, parent_id=tac1.goal_id)
        op1.actions = ["Study variables", "Practice loops"]
        op1.status = GoalStatus.COMPLETED  # Mark as done
        
        op2 = planner7.create_goal("Learn data structures", PlanLevel.OPERATIONAL, parent_id=tac1.goal_id)
        op2.actions = ["Study lists", "Study dictionaries"]
        op2.status = GoalStatus.IN_PROGRESS
        
        op3 = planner7.create_goal("Build web app", PlanLevel.OPERATIONAL, parent_id=tac2.goal_id)
        op3.actions = ["Choose framework", "Create app"]
        
        print("Hierarchical Plan with Status:")
        print(planner7.get_hierarchy_summary())
        
        # Example 8: Replanning scenario
        print("\n" + "=" * 80)
        print("Example 8: Adaptive Replanning")
        print("=" * 80)
        
        planner8 = HierarchicalPlanner()
        
        print("Original Plan:")
        strategic = planner8.create_goal("Travel to conference", PlanLevel.STRATEGIC)
        
        tac1 = planner8.create_goal("Book transportation", PlanLevel.TACTICAL, parent_id=strategic.goal_id)
        op1 = planner8.create_goal("Book flight", PlanLevel.OPERATIONAL, parent_id=tac1.goal_id)
        op1.actions = ["Search flights", "Purchase ticket"]
        
        print(planner8.get_hierarchy_summary())
        
        print("\nSimulating failure (flight cancelled)...")
        op1.status = GoalStatus.FAILED
        
        # Create alternative plan
        print("\nReplanning at tactical level:")
        op2 = planner8.create_goal("Book train", PlanLevel.OPERATIONAL, parent_id=tac1.goal_id)
        op2.actions = ["Search trains", "Purchase ticket"]
        
        print(planner8.get_hierarchy_summary())
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY: Hierarchical Planning Pattern")
        print("=" * 80)
        
        summary = """
        The Hierarchical Planning pattern demonstrated:
        
        1. THREE-LEVEL HIERARCHY (Example 1):
           - Strategic: High-level objectives
           - Tactical: Sub-goals and procedures
           - Operational: Concrete actions
           - Progressive decomposition
           - Abstraction management
        
        2. COMPLETE HIERARCHY (Example 2):
           - Full plan generation
           - Multi-level decomposition
           - Parent-child relationships
           - Structured goal tree
           - Hierarchical visualization
        
        3. PLAN EXECUTION (Example 3):
           - Sequential action execution
           - Status tracking at all levels
           - Progress propagation
           - Completion detection
           - Hierarchical updates
        
        4. PRIORITY HANDLING (Example 4):
           - Priority-based selection
           - Urgent task handling
           - Importance ordering
           - Resource allocation
           - Efficient scheduling
        
        5. DEPENDENCY MANAGEMENT (Example 5):
           - Goal dependencies
           - Execution ordering
           - Prerequisite checking
           - Constraint satisfaction
           - Sequential execution
        
        6. PROGRESS MONITORING (Example 6):
           - Multi-level tracking
           - Status aggregation
           - Statistics collection
           - Performance metrics
           - Completion monitoring
        
        7. PLAN VISUALIZATION (Example 7):
           - Hierarchical display
           - Status indicators
           - Tree structure
           - Action details
           - Clear representation
        
        8. ADAPTIVE REPLANNING (Example 8):
           - Failure handling
           - Alternative generation
           - Level-appropriate replanning
           - Plan modification
           - Resilience
        
        KEY BENEFITS:
        ✓ Manages complexity through abstraction
        ✓ Efficient planning for long horizons
        ✓ Reasoning at appropriate granularity
        ✓ Easy to understand and explain
        ✓ Facilitates partial replanning
        ✓ Modular and reusable
        ✓ Scales to complex tasks
        ✓ Clear progress tracking
        
        USE CASES:
        • Complex project planning
        • Multi-step task execution
        • Robotic mission planning
        • Software development workflows
        • Travel itinerary planning
        • Research project organization
        • Business process automation
        • Strategic decision-making
        
        ABSTRACTION LEVELS:
        → Strategic: What to achieve (objectives)
        → Tactical: How to achieve (procedures)
        → Operational: Specific actions (execution)
        
        BEST PRACTICES:
        1. Define clear abstraction levels for domain
        2. Decompose goals systematically
        3. Track dependencies between goals
        4. Monitor execution at all levels
        5. Enable replanning at appropriate level
        6. Cache and reuse common sub-plans
        7. Visualize plan hierarchy
        8. Handle failures at correct level
        
        TRADE-OFFS:
        • Planning overhead vs. execution efficiency
        • Abstraction depth vs. simplicity
        • Flexibility vs. structure
        • Decomposition detail vs. overview
        
        PRODUCTION CONSIDERATIONS:
        → Implement domain-specific decomposition rules
        → Use caching for common sub-plans
        → Monitor performance at each level
        → Enable interactive plan adjustment
        → Provide clear visualization
        → Track dependencies explicitly
        → Implement efficient replanning
        → Handle partial plan execution
        → Support concurrent goal execution
        → Log decisions at all levels
        
        This pattern enables agents to tackle complex, long-horizon tasks
        by organizing planning at multiple levels of abstraction, from
        high-level strategies to concrete actions.
        """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_hierarchical_planning()
    
        

    Agentic Design Pattern: Hierarchical Planning

    Pattern 105: Hierarchical Planning – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 105: Hierarchical Planning

    Description: The Hierarchical Planning pattern breaks down complex goals into a hierarchy of sub-goals and actions at multiple levels of abstraction. This pattern enables agents to handle long-horizon tasks by decomposing them into manageable pieces, planning at different levels of granularity, and progressively refining high-level strategies into concrete actions.

    Hierarchical planning is inspired by hierarchical task networks (HTN) and
    goal decomposition techniques. It allows agents to reason about problems at
    multiple scales: strategic (what to achieve), tactical (how to achieve it),
    and operational (specific actions). Each level focuses on appropriate
    abstractions, making planning more tractable and interpretable.
    
    This pattern is particularly effective for complex, multi-step tasks where
    flat planning would result in intractably large search spaces. By organizing
    goals hierarchically, the agent can plan efficiently, adapt at different
    levels, and explain its reasoning at appropriate levels of detail.
    

    Key Components: 1. Goal Hierarchy: Tree structure of goals and sub-goals 2. Abstraction Levels: Strategic, tactical, operational layers 3. Decomposition Rules: How to break goals into sub-goals 4. Level Planner: Plans at each abstraction level 5. Refinement Engine: Converts abstract plans to concrete actions 6. Execution Monitor: Tracks progress at all levels 7. Re-planning Trigger: Identifies when to replan at each level

    Abstraction Levels: 1. Strategic Level: High-level goals and objectives – What needs to be achieved – Overall strategy and approach – Long-term planning horizon

    2. Tactical Level: Sub-goals and procedures
       - How to achieve strategic goals
       - Intermediate milestones
       - Medium-term planning
    
    3. Operational Level: Concrete actions
       - Specific actions to execute
       - Immediate next steps
       - Short-term execution
    

    Planning Phases: 1. Goal Decomposition: Break high-level goal into sub-goals 2. Level Planning: Plan at each abstraction level 3. Plan Refinement: Refine abstract plans into detailed plans 4. Execution: Execute lowest-level actions 5. Monitoring: Track progress at all levels 6. Re-planning: Adapt when needed at appropriate level

    Use Cases: – Complex project planning – Multi-step task execution – Robotic mission planning – Software development workflows – Travel itinerary planning – Research project organization – Business process automation

    Advantages: – Manages complexity through abstraction – More efficient than flat planning – Allows reasoning at appropriate granularity – Easier to explain and understand – Facilitates partial replanning – Modular and reusable sub-plans – Scales to long-horizon tasks

    Challenges: – Defining appropriate abstraction levels – Goal decomposition complexity – Ensuring consistency across levels – Handling dependencies between sub-goals – Determining when to replan at each level – Balancing planning depth vs. efficiency

    LangChain Implementation: This implementation uses LangChain for: – LLM-based goal decomposition – Multi-level plan generation – Plan refinement and detailing – Natural language planning

    Production Considerations: – Define clear abstraction levels for domain – Implement efficient goal decomposition – Track dependencies between sub-goals – Monitor execution at all levels – Enable replanning at appropriate level – Cache and reuse common sub-plans – Provide visualization of plan hierarchy – Handle failures at correct abstraction level


    File Statistics

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

    Module Documentation

    """
    Pattern 105: Hierarchical Planning
    
    Description:
        The Hierarchical Planning pattern breaks down complex goals into a hierarchy
        of sub-goals and actions at multiple levels of abstraction. This pattern
        enables agents to handle long-horizon tasks by decomposing them into manageable
        pieces, planning at different levels of granularity, and progressively refining
        high-level strategies into concrete actions.
    
        Hierarchical planning is inspired by hierarchical task networks (HTN) and
        goal decomposition techniques. It allows agents to reason about problems at
        multiple scales: strategic (what to achieve), tactical (how to achieve it),
        and operational (specific actions). Each level focuses on appropriate
        abstractions, making planning more tractable and interpretable.
    
        This pattern is particularly effective for complex, multi-step tasks where
        flat planning would result in intractably large search spaces. By organizing
        goals hierarchically, the agent can plan efficiently, adapt at different
        levels, and explain its reasoning at appropriate levels of detail.
    
    Key Components:
        1. Goal Hierarchy: Tree structure of goals and sub-goals
        2. Abstraction Levels: Strategic, tactical, operational layers
        3. Decomposition Rules: How to break goals into sub-goals
        4. Level Planner: Plans at each abstraction level
        5. Refinement Engine: Converts abstract plans to concrete actions
        6. Execution Monitor: Tracks progress at all levels
        7. Re-planning Trigger: Identifies when to replan at each level
    
    Abstraction Levels:
        1. Strategic Level: High-level goals and objectives
           - What needs to be achieved
           - Overall strategy and approach
           - Long-term planning horizon
    
        2. Tactical Level: Sub-goals and procedures
           - How to achieve strategic goals
           - Intermediate milestones
           - Medium-term planning
    
        3. Operational Level: Concrete actions
           - Specific actions to execute
           - Immediate next steps
           - Short-term execution
    
    Planning Phases:
        1. Goal Decomposition: Break high-level goal into sub-goals
        2. Level Planning: Plan at each abstraction level
        3. Plan Refinement: Refine abstract plans into detailed plans
        4. Execution: Execute lowest-level actions
        5. Monitoring: Track progress at all levels
        6. Re-planning: Adapt when needed at appropriate level
    
    Use Cases:
        - Complex project planning
        - Multi-step task execution
        - Robotic mission planning
        - Software development workflows
        - Travel itinerary planning
        - Research project organization
        - Business process automation
    
    Advantages:
        - Manages complexity through abstraction
        - More efficient than flat planning
        - Allows reasoning at appropriate granularity
        - Easier to explain and understand
        - Facilitates partial replanning
        - Modular and reusable sub-plans
        - Scales to long-horizon tasks
    
    Challenges:
        - Defining appropriate abstraction levels
        - Goal decomposition complexity
        - Ensuring consistency across levels
        - Handling dependencies between sub-goals
        - Determining when to replan at each level
        - Balancing planning depth vs. efficiency
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based goal decomposition
        - Multi-level plan generation
        - Plan refinement and detailing
        - Natural language planning
    
    Production Considerations:
        - Define clear abstraction levels for domain
        - Implement efficient goal decomposition
        - Track dependencies between sub-goals
        - Monitor execution at all levels
        - Enable replanning at appropriate level
        - Cache and reuse common sub-plans
        - Provide visualization of plan hierarchy
        - Handle failures at correct abstraction level
    """
    

    Purpose: This module implements the Hierarchical Planning 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 typing.Any
    import typing.Dict
    import typing.List
    import typing.Optional
    import typing.Tuple
    

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

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: PlanLevel

    Lines 113-117

    Inherits from: Enum

    Purpose:

    Planning abstraction levels.
    

    Class: GoalStatus

    Lines 120-126

    Inherits from: Enum

    Purpose:

    Status of a goal.
    

    Class: Goal

    Lines 130-157

    Purpose:

    Represents a goal at any level of the hierarchy.
    
    Attributes:
        goal_id: Unique identifier
        description: Goal description
        level: Abstraction level
        parent_id: Parent goal ID (None for root)
        children_ids: List of child goal IDs
        status: Current status
        priority: Priority (1-10)
        dependencies: IDs of goals that must complete first
        actions: Concrete actions (for operational level)
        metadata: Additional information
    

    Class: HierarchicalPlanner

    Lines 160-533

    Purpose:

    Hierarchical planning system that plans at multiple abstraction levels.
    
    This planner decomposes high-level goals into hierarchies of sub-goals,
    plans at each level, and progressively refines abstract plans into
    concrete actions.
    

    Methods:

    __init__(self, temperature)

    Lines 169-179

    Purpose:

    Initialize hierarchical planner.
    
    Args:
        temperature: LLM temperature for planning
    

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

    create_goal(self, description, level, parent_id, priority, dependencies)

    Lines 181-218

    Purpose:

    Create a new goal.
    
    Args:
        description: Goal description
        level: Abstraction level
        parent_id: Parent goal ID
        priority: Priority (1-10)
        dependencies: Goal dependencies
    
    Returns:
        Created goal
    

    Parameters: – description: Description – level: Level – parent_id: Parent id – priority: Priority – dependencies: Dependencies

    Returns: Goal

    decompose_goal(self, goal, target_level)

    Lines 220-275

    Purpose:

    Decompose a goal into sub-goals at lower abstraction level.
    
    Args:
        goal: Goal to decompose
        target_level: Target abstraction level for sub-goals
    
    Returns:
        List of sub-goals
    

    Parameters: – goal: Goal – target_level: Target level

    Returns: <ast.Subscript object at 0x000001A06D6F6A50>

    plan_strategic(self, objective)

    Lines 277-294

    Purpose:

    Create strategic-level plan.
    
    Args:
        objective: High-level objective
    
    Returns:
        Strategic goal
    

    Parameters: – objective: Objective

    Returns: Goal

    plan_tactical(self, strategic_goal)

    Lines 296-307

    Purpose:

    Create tactical-level plans from strategic goal.
    
    Args:
        strategic_goal: Strategic goal to decompose
    
    Returns:
        List of tactical goals
    

    Parameters: – strategic_goal: Strategic goal

    Returns: <ast.Subscript object at 0x000001A06D605650>

    plan_operational(self, tactical_goal)

    Lines 309-325

    Purpose:

    Create operational-level plans from tactical goal.
    
    Args:
        tactical_goal: Tactical goal to decompose
    
    Returns:
        List of operational goals
    

    Parameters: – tactical_goal: Tactical goal

    Returns: <ast.Subscript object at 0x000001A06D718510>

    generate_actions(self, operational_goal)

    Lines 327-362

    Purpose:

    Generate concrete actions for an operational goal.
    
    Args:
        operational_goal: Operational goal
    
    Returns:
        List of concrete actions
    

    Parameters: – operational_goal: Operational goal

    Returns: <ast.Subscript object at 0x000001A06D866310>

    create_full_hierarchy(self, objective)

    Lines 364-384

    Purpose:

    Create complete hierarchical plan from objective.
    
    Args:
        objective: High-level objective
    
    Returns:
        Root strategic goal with full hierarchy
    

    Parameters: – objective: Objective

    Returns: Goal

    get_next_action(self)

    Lines 386-413

    Purpose:

    Get next executable action from the plan.
    
    Returns:
        Tuple of (goal, action) or None if no actions available
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D72C9D0>

    execute_action(self, goal, action)

    Lines 415-448

    Purpose:

    Execute an action (simulated).
    
    Args:
        goal: Goal being executed
        action: Action to execute
    
    Returns:
        Success status
    

    Parameters: – goal: Goal – action: Action

    Returns: bool

    propagate_completion(self, goal)

    Lines 450-472

    Purpose:

    Propagate goal completion up the hierarchy.
    
    Args:
        goal: Completed goal
    

    Parameters: – goal: Goal

    get_hierarchy_summary(self, goal_id)

    Lines 474-517

    Purpose:

    Get textual summary of goal hierarchy.
    
    Args:
        goal_id: Root goal ID (None for all roots)
    
    Returns:
        Formatted hierarchy string
    

    Parameters: – goal_id: Goal id

    Returns: str

    get_statistics(self)

    Lines 519-533

    Purpose:

    Get planning statistics.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D853BD0>


    Functions

    demonstrate_hierarchical_planning()

    Lines 536-941

    Purpose:

    Demonstrate hierarchical planning pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Hierarchical Planning 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 104
    • Pattern 106

    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 Hierarchical Planning 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: 105_hierarchical_planning.py
    Total Lines: 946
    Pattern Category: AI Agent Design Pattern #105


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Memory Replay

    
        """
    Pattern 104: Memory Replay & Rehearsal
    
    Description:
        The Memory Replay & Rehearsal pattern implements mechanisms for re-experiencing
        and strengthening memories through replay, similar to how the human brain
        consolidates memories during sleep and practice. This pattern is crucial for
        learning, skill retention, and memory consolidation in long-running agent systems.
        
        Memory replay involves re-activating stored experiences to strengthen neural
        patterns, transfer information from short-term to long-term storage, and discover
        new insights through recombination. Rehearsal can be exact (episodic replay) or
        generative (synthetic variations), and can occur during downtime, triggered by
        context, or scheduled periodically.
        
        This pattern supports various replay strategies including experience replay
        (reinforcement learning), prioritized replay (focus on important memories),
        interleaved replay (mixing old and new), and generative replay (creating
        variations). It implements replay scheduling, priority calculation, and
        consolidation effects.
    
    Key Components:
        1. Memory Buffer: Store of experiences to replay
        2. Replay Strategy: Algorithm for selecting memories
        3. Replay Scheduler: When to perform replay
        4. Priority Calculator: Importance scoring
        5. Consolidation Engine: Strengthening mechanism
        6. Synthetic Generator: Create variations
        7. Replay Metrics: Track effectiveness
    
    Replay Types:
        1. Experience Replay: Re-live past experiences
        2. Prioritized Replay: Focus on important/surprising
        3. Interleaved Replay: Mix old and new memories
        4. Generative Replay: Create synthetic variations
        5. Forward Replay: Chronological order
        6. Reverse Replay: Reverse chronological
        7. Random Replay: Random sampling
        
    Replay Timing:
        1. Offline Replay: During downtime/sleep
        2. Online Replay: During active tasks
        3. Triggered Replay: By context or cues
        4. Periodic Replay: Scheduled intervals
        5. Event-driven Replay: After significant events
    
    Use Cases:
        - Reinforcement learning agents
        - Skill learning and retention
        - Memory consolidation systems
        - Experience-based learning
        - Pattern recognition improvement
        - Knowledge transfer and generalization
        - Continuous learning systems
    
    Advantages:
        - Strengthens important memories
        - Improves learning efficiency
        - Enables offline learning
        - Prevents catastrophic forgetting
        - Discovers new patterns
        - Transfers knowledge
        - Simulates "sleep consolidation"
    
    Challenges:
        - Computational cost of replay
        - Selecting which memories to replay
        - Balancing old vs. new experiences
        - Avoiding overfitting to replayed memories
        - Determining replay frequency
        - Managing replay buffer size
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based memory importance assessment
        - Synthetic memory generation
        - Pattern extraction from replay
        - Learning from replayed experiences
        
    Production Considerations:
        - Schedule replay during low-activity periods
        - Implement priority-based sampling
        - Monitor replay effectiveness metrics
        - Limit replay buffer size
        - Balance replay with new experiences
        - Track memory strengthening
        - Implement incremental consolidation
        - Consider computational budget
    """
    
    import os
    import time
    from typing import List, Dict, Any, Optional, Callable
    from dataclasses import dataclass, field
    from datetime import datetime, timedelta
    from enum import Enum
    from collections import deque
    import random
    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 ReplayStrategy(Enum):
        """Strategy for replaying memories."""
        PRIORITIZED = "prioritized"  # High-priority first
        RANDOM = "random"  # Random sampling
        RECENT = "recent"  # Most recent first
        OLDEST = "oldest"  # Oldest first
        INTERLEAVED = "interleaved"  # Mix old and new
        REVERSE = "reverse"  # Reverse chronological
    
    
    class ReplayTrigger(Enum):
        """When replay is triggered."""
        MANUAL = "manual"
        PERIODIC = "periodic"
        THRESHOLD = "threshold"  # When buffer is full
        CONTEXTUAL = "contextual"  # By context match
        EVENT_DRIVEN = "event_driven"
    
    
    @dataclass
    class MemoryTrace:
        """
        Memory trace for replay.
        
        Attributes:
            trace_id: Unique identifier
            content: Memory content
            context: Context when created
            timestamp: Creation time
            importance: Importance score
            replay_count: Times replayed
            strength: Memory strength
            last_replayed: Last replay time
            success_metric: Performance metric
            metadata: Additional information
        """
        trace_id: str
        content: str
        context: Dict[str, Any]
        timestamp: datetime
        importance: float
        replay_count: int = 0
        strength: float = 0.5
        last_replayed: Optional[datetime] = None
        success_metric: float = 0.5
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    class MemoryReplaySystem:
        """
        Memory replay and rehearsal system.
        
        This system manages the replay of stored memories for consolidation,
        learning, and pattern discovery.
        """
        
        def __init__(
            self,
            buffer_size: int = 1000,
            replay_batch_size: int = 10,
            strategy: ReplayStrategy = ReplayStrategy.PRIORITIZED,
            strengthening_rate: float = 0.1,
            temperature: float = 0.3
        ):
            """
            Initialize memory replay system.
            
            Args:
                buffer_size: Maximum memories to store
                replay_batch_size: Memories per replay session
                strategy: Replay selection strategy
                strengthening_rate: How much to strengthen on replay
                temperature: LLM temperature
            """
            self.buffer_size = buffer_size
            self.replay_batch_size = replay_batch_size
            self.strategy = strategy
            self.strengthening_rate = strengthening_rate
            
            self.memory_buffer: deque = deque(maxlen=buffer_size)
            self.replay_history: List[Dict[str, Any]] = []
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.trace_counter = 0
        
        def store_memory(
            self,
            content: str,
            context: Dict[str, Any],
            importance: float = 0.5,
            success_metric: float = 0.5
        ) -> MemoryTrace:
            """
            Store memory for potential replay.
            
            Args:
                content: Memory content
                context: Context information
                importance: Importance score
                success_metric: Success/performance metric
                
            Returns:
                Created memory trace
            """
            self.trace_counter += 1
            trace = MemoryTrace(
                trace_id=f"trace_{self.trace_counter}",
                content=content,
                context=context,
                timestamp=datetime.now(),
                importance=importance,
                success_metric=success_metric
            )
            
            self.memory_buffer.append(trace)
            return trace
        
        def calculate_priority(self, trace: MemoryTrace) -> float:
            """
            Calculate replay priority for memory.
            
            Args:
                trace: Memory trace
                
            Returns:
                Priority score
            """
            # Factors: importance, recency, success, replay count
            now = datetime.now()
            
            # Recency (newer = higher priority)
            time_since = (now - trace.timestamp).total_seconds() / 3600
            recency_score = 1.0 / (1.0 + time_since)
            
            # Inverse replay count (less replayed = higher priority)
            replay_score = 1.0 / (1.0 + trace.replay_count)
            
            # Combine factors
            priority = (
                trace.importance * 0.4 +
                recency_score * 0.2 +
                trace.success_metric * 0.2 +
                replay_score * 0.2
            )
            
            return priority
        
        def select_memories_for_replay(
            self,
            n: Optional[int] = None
        ) -> List[MemoryTrace]:
            """
            Select memories to replay based on strategy.
            
            Args:
                n: Number of memories (default: replay_batch_size)
                
            Returns:
                List of memory traces to replay
            """
            if n is None:
                n = self.replay_batch_size
            
            if not self.memory_buffer:
                return []
            
            n = min(n, len(self.memory_buffer))
            memories = list(self.memory_buffer)
            
            if self.strategy == ReplayStrategy.PRIORITIZED:
                # Sort by priority
                priorities = [(trace, self.calculate_priority(trace)) for trace in memories]
                priorities.sort(key=lambda x: x[1], reverse=True)
                selected = [trace for trace, _ in priorities[:n]]
            
            elif self.strategy == ReplayStrategy.RANDOM:
                selected = random.sample(memories, n)
            
            elif self.strategy == ReplayStrategy.RECENT:
                selected = sorted(memories, key=lambda x: x.timestamp, reverse=True)[:n]
            
            elif self.strategy == ReplayStrategy.OLDEST:
                selected = sorted(memories, key=lambda x: x.timestamp)[:n]
            
            elif self.strategy == ReplayStrategy.INTERLEAVED:
                # Mix old and new
                sorted_by_time = sorted(memories, key=lambda x: x.timestamp)
                selected = []
                for i in range(n):
                    if i % 2 == 0 and i // 2 = 0:
                        selected.append(sorted_by_time[-(i // 2 + 1)])  # New
            
            elif self.strategy == ReplayStrategy.REVERSE:
                selected = sorted(memories, key=lambda x: x.timestamp, reverse=True)[:n]
            
            else:
                selected = memories[:n]
            
            return selected
        
        def replay(
            self,
            n: Optional[int] = None,
            consolidate: bool = True
        ) -> Dict[str, Any]:
            """
            Perform memory replay.
            
            Args:
                n: Number of memories to replay
                consolidate: Whether to strengthen memories
                
            Returns:
                Replay statistics
            """
            selected = self.select_memories_for_replay(n)
            
            if not selected:
                return {"replayed": 0, "strengthened": 0}
            
            replayed_count = 0
            strengthened_count = 0
            
            for trace in selected:
                # Update replay statistics
                trace.replay_count += 1
                trace.last_replayed = datetime.now()
                replayed_count += 1
                
                # Consolidate (strengthen) memory
                if consolidate:
                    old_strength = trace.strength
                    trace.strength = min(1.0, trace.strength + self.strengthening_rate)
                    if trace.strength > old_strength:
                        strengthened_count += 1
            
            # Record replay session
            session = {
                "timestamp": datetime.now(),
                "replayed_count": replayed_count,
                "strengthened_count": strengthened_count,
                "strategy": self.strategy.value,
                "traces": [t.trace_id for t in selected]
            }
            self.replay_history.append(session)
            
            return {
                "replayed": replayed_count,
                "strengthened": strengthened_count,
                "avg_strength_increase": self.strengthening_rate if consolidate else 0
            }
        
        def generate_synthetic_memory(
            self,
            base_memory: MemoryTrace,
            variation: str = "similar"
        ) -> MemoryTrace:
            """
            Generate synthetic memory variation.
            
            Args:
                base_memory: Memory to vary
                variation: Type of variation
                
            Returns:
                New synthetic memory
            """
            # Generate variation using LLM
            prompt = ChatPromptTemplate.from_template(
                "Given this memory: {content}\n\n"
                "Generate a {variation} variation of this memory. "
                "Keep the core concept but change details or perspective. "
                "Return only the new memory content.\n\n"
                "Variation:"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            synthetic_content = chain.invoke({
                "content": base_memory.content,
                "variation": variation
            })
            
            # Create synthetic memory
            synthetic = self.store_memory(
                content=synthetic_content.strip(),
                context={**base_memory.context, "synthetic": True, "base": base_memory.trace_id},
                importance=base_memory.importance * 0.8,
                success_metric=base_memory.success_metric
            )
            
            return synthetic
        
        def generative_replay(
            self,
            n_synthetic: int = 5,
            min_importance: float = 0.6
        ) -> List[MemoryTrace]:
            """
            Perform generative replay with synthetic variations.
            
            Args:
                n_synthetic: Number of synthetic memories to generate
                min_importance: Minimum importance for base memories
                
            Returns:
                List of generated synthetic memories
            """
            # Select high-importance memories as bases
            important_memories = [
                trace for trace in self.memory_buffer
                if trace.importance >= min_importance
            ]
            
            if not important_memories:
                return []
            
            # Generate synthetic variations
            synthetic_memories = []
            for _ in range(n_synthetic):
                base = random.choice(important_memories)
                synthetic = self.generate_synthetic_memory(base)
                synthetic_memories.append(synthetic)
            
            return synthetic_memories
        
        def get_statistics(self) -> Dict[str, Any]:
            """Get replay statistics."""
            if not self.memory_buffer:
                return {
                    "total_memories": 0,
                    "total_replays": 0,
                    "avg_strength": 0,
                    "avg_replay_count": 0
                }
            
            memories = list(self.memory_buffer)
            
            return {
                "total_memories": len(memories),
                "total_replays": len(self.replay_history),
                "avg_strength": sum(m.strength for m in memories) / len(memories),
                "avg_replay_count": sum(m.replay_count for m in memories) / len(memories),
                "buffer_usage": len(memories) / self.buffer_size,
                "strategy": self.strategy.value
            }
    
    
    class ReplayLearningAgent:
        """
        Agent that learns through memory replay.
        
        This agent uses replay to consolidate memories and improve
        performance through rehearsal.
        """
        
        def __init__(
            self,
            buffer_size: int = 500,
            replay_interval: int = 10,
            temperature: float = 0.5
        ):
            """
            Initialize replay learning agent.
            
            Args:
                buffer_size: Memory buffer size
                replay_interval: Interactions between replays
                temperature: LLM temperature
            """
            self.replay_system = MemoryReplaySystem(
                buffer_size=buffer_size,
                strategy=ReplayStrategy.PRIORITIZED
            )
            self.replay_interval = replay_interval
            self.interaction_count = 0
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
        
        def experience(
            self,
            content: str,
            context: Dict[str, Any],
            importance: float = 0.5,
            success: float = 0.5
        ):
            """
            Record an experience.
            
            Args:
                content: Experience content
                context: Context information
                importance: Importance score
                success: Success metric
            """
            self.replay_system.store_memory(content, context, importance, success)
            self.interaction_count += 1
            
            # Periodic replay
            if self.interaction_count % self.replay_interval == 0:
                self.replay_system.replay()
        
        def consolidate(self):
            """Perform memory consolidation through replay."""
            self.replay_system.replay(consolidate=True)
        
        def get_strong_memories(self, n: int = 10) -> List[MemoryTrace]:
            """
            Get strongest memories.
            
            Args:
                n: Number of memories
                
            Returns:
                List of strong memories
            """
            memories = list(self.replay_system.memory_buffer)
            memories.sort(key=lambda x: x.strength, reverse=True)
            return memories[:n]
    
    
    def demonstrate_memory_replay():
        """Demonstrate memory replay and rehearsal pattern."""
        
        print("=" * 80)
        print("MEMORY REPLAY & REHEARSAL PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic replay system
        print("\n" + "=" * 80)
        print("Example 1: Memory Storage and Replay")
        print("=" * 80)
        
        replay_system = MemoryReplaySystem(
            buffer_size=50,
            replay_batch_size=5,
            strategy=ReplayStrategy.PRIORITIZED
        )
        
        print("\nStoring experiences...")
        experiences = [
            ("Learned Python basics", {"topic": "python"}, 0.7, 0.8),
            ("Made a coding mistake", {"topic": "error"}, 0.9, 0.3),
            ("Successfully debugged issue", {"topic": "debug"}, 0.8, 0.9),
            ("Read documentation", {"topic": "learning"}, 0.5, 0.6),
            ("Completed project milestone", {"topic": "achievement"}, 0.9, 0.9),
        ]
        
        for content, context, importance, success in experiences:
            replay_system.store_memory(content, context, importance, success)
            print(f"  ✓ Stored: {content}")
        
        stats = replay_system.get_statistics()
        print(f"\nBuffer status: {stats['total_memories']} memories stored")
        
        # Perform replay
        print("\nPerforming prioritized replay...")
        result = replay_system.replay(n=3, consolidate=True)
        
        print(f"  Replayed: {result['replayed']} memories")
        print(f"  Strengthened: {result['strengthened']} memories")
        
        # Example 2: Different replay strategies
        print("\n" + "=" * 80)
        print("Example 2: Comparing Replay Strategies")
        print("=" * 80)
        
        strategies = [
            ReplayStrategy.PRIORITIZED,
            ReplayStrategy.RANDOM,
            ReplayStrategy.RECENT,
            ReplayStrategy.OLDEST
        ]
        
        for strategy in strategies:
            test_system = MemoryReplaySystem(
                buffer_size=20,
                replay_batch_size=3,
                strategy=strategy
            )
            
            # Add memories with timestamps
            for i in range(10):
                test_system.store_memory(
                    f"Memory {i}",
                    {"index": i},
                    importance=0.3 + (i * 0.05),
                    success_metric=0.5
                )
                time.sleep(0.01)
            
            print(f"\nStrategy: {strategy.value}")
            selected = test_system.select_memories_for_replay(3)
            for trace in selected:
                print(f"  → {trace.content} (importance: {trace.importance:.2f})")
        
        # Example 3: Strengthening through replay
        print("\n" + "=" * 80)
        print("Example 3: Memory Strengthening Through Replay")
        print("=" * 80)
        
        strengthen_system = MemoryReplaySystem(
            strengthening_rate=0.15
        )
        
        # Store important memory
        trace = strengthen_system.store_memory(
            "Critical information to remember",
            {"type": "important"},
            importance=0.8,
            success_metric=0.9
        )
        
        print(f"\nMemory: '{trace.content}'")
        print(f"Initial strength: {trace.strength:.2f}")
        print(f"\nReplaying memory multiple times...")
        
        for i in range(1, 6):
            strengthen_system.replay(n=1, consolidate=True)
            print(f"  After replay {i}: strength = {trace.strength:.2f}, "
                  f"replay_count = {trace.replay_count}")
        
        # Example 4: Priority-based replay
        print("\n" + "=" * 80)
        print("Example 4: Priority-Based Memory Selection")
        print("=" * 80)
        
        priority_system = MemoryReplaySystem(
            strategy=ReplayStrategy.PRIORITIZED
        )
        
        # Add memories with different priorities
        memory_data = [
            ("Low priority memory", 0.2, 0.3),
            ("Medium priority memory", 0.5, 0.5),
            ("High priority memory", 0.9, 0.8),
            ("Critical memory", 0.95, 0.9),
            ("Another low priority", 0.3, 0.4),
        ]
        
        traces = []
        for content, importance, success in memory_data:
            trace = priority_system.store_memory(
                content,
                {"type": "test"},
                importance,
                success
            )
            traces.append(trace)
            time.sleep(0.01)
        
        print("\nMemories and their priorities:")
        for trace in traces:
            priority = priority_system.calculate_priority(trace)
            print(f"  [{priority:.3f}] {trace.content}")
        
        print(f"\nSelecting top 3 for replay...")
        selected = priority_system.select_memories_for_replay(3)
        for trace in selected:
            print(f"  ✓ {trace.content}")
        
        # Example 5: Interleaved replay
        print("\n" + "=" * 80)
        print("Example 5: Interleaved Replay (Old + New)")
        print("=" * 80)
        
        interleaved_system = MemoryReplaySystem(
            strategy=ReplayStrategy.INTERLEAVED,
            replay_batch_size=6
        )
        
        # Add memories over time
        for i in range(10):
            interleaved_system.store_memory(
                f"Memory from time {i}",
                {"time": i},
                importance=0.5
            )
            time.sleep(0.01)
        
        print("\nInterleaved replay (mixing old and new):")
        selected = interleaved_system.select_memories_for_replay(6)
        for trace in selected:
            print(f"  → {trace.content}")
        
        # Example 6: Generative replay
        print("\n" + "=" * 80)
        print("Example 6: Generative Replay with Synthetic Memories")
        print("=" * 80)
        
        gen_system = MemoryReplaySystem()
        
        # Store base memories
        base_memories = [
            ("Python is good for data science", {"topic": "python"}, 0.8),
            ("Machine learning requires clean data", {"topic": "ml"}, 0.9),
        ]
        
        print("\nBase memories:")
        for content, context, importance in base_memories:
            trace = gen_system.store_memory(content, context, importance, 0.7)
            print(f"  • {trace.content}")
        
        print("\nGenerating synthetic variations...")
        synthetic = gen_system.generative_replay(n_synthetic=3, min_importance=0.7)
        
        print(f"\nGenerated {len(synthetic)} synthetic memories:")
        for trace in synthetic:
            base_id = trace.context.get("base", "unknown")
            print(f"  • {trace.content}")
            print(f"    (from: {base_id})")
        
        # Example 7: Replay learning agent
        print("\n" + "=" * 80)
        print("Example 7: Agent with Periodic Replay")
        print("=" * 80)
        
        agent = ReplayLearningAgent(
            buffer_size=50,
            replay_interval=5
        )
        
        print("\nAgent learning through experience...")
        
        experiences = [
            ("Learned concept A", {"subject": "learning"}, 0.7, 0.8),
            ("Applied concept A successfully", {"subject": "application"}, 0.8, 0.9),
            ("Struggled with concept B", {"subject": "challenge"}, 0.9, 0.4),
            ("Reviewed fundamentals", {"subject": "review"}, 0.6, 0.7),
            ("Mastered concept B", {"subject": "mastery"}, 0.9, 0.9),
            ("Completed practice exercises", {"subject": "practice"}, 0.7, 0.8),
            ("Taught concept to peer", {"subject": "teaching"}, 0.8, 0.9),
            ("Received positive feedback", {"subject": "feedback"}, 0.7, 0.8),
            ("Started new project", {"subject": "project"}, 0.8, 0.7),
            ("Made significant progress", {"subject": "progress"}, 0.8, 0.8),
        ]
        
        for i, (content, context, importance, success) in enumerate(experiences, 1):
            agent.experience(content, context, importance, success)
            print(f"  {i}. Experienced: {content}")
            
            if i % agent.replay_interval == 0:
                print(f"     → Automatic replay triggered at interaction {i}")
        
        # Check strongest memories
        print("\nStrongest consolidated memories:")
        strong = agent.get_strong_memories(5)
        for trace in strong:
            print(f"  [{trace.strength:.2f}] {trace.content} "
                  f"(replayed {trace.replay_count}x)")
        
        # Example 8: Replay statistics
        print("\n" + "=" * 80)
        print("Example 8: Replay System Statistics")
        print("=" * 80)
        
        stats_system = MemoryReplaySystem(buffer_size=100)
        
        # Simulate learning session
        print("\nSimulating learning session...")
        for i in range(50):
            stats_system.store_memory(
                f"Experience {i}",
                {"index": i},
                importance=random.uniform(0.3, 0.9),
                success_metric=random.uniform(0.4, 1.0)
            )
        
        # Multiple replay sessions
        for _ in range(5):
            stats_system.replay()
        
        stats = stats_system.get_statistics()
        
        print(f"\nReplay Statistics:")
        print(f"  Total Memories: {stats['total_memories']}")
        print(f"  Total Replay Sessions: {stats['total_replays']}")
        print(f"  Average Memory Strength: {stats['avg_strength']:.2f}")
        print(f"  Average Replay Count: {stats['avg_replay_count']:.2f}")
        print(f"  Buffer Usage: {stats['buffer_usage']:.1%}")
        print(f"  Strategy: {stats['strategy']}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY: Memory Replay & Rehearsal Pattern")
        print("=" * 80)
        
        summary = """
        The Memory Replay & Rehearsal pattern demonstrated:
        
        1. BASIC REPLAY (Example 1):
           - Memory storage in replay buffer
           - Selection and replay of memories
           - Memory strengthening through consolidation
           - Replay statistics tracking
        
        2. REPLAY STRATEGIES (Example 2):
           - Prioritized: High-importance first
           - Random: Random sampling
           - Recent: Newest memories
           - Oldest: Oldest memories
           - Strategy comparison and selection
        
        3. STRENGTHENING (Example 3):
           - Progressive strength increase
           - Multiple replay sessions
           - Replay count tracking
           - Consolidation effects
        
        4. PRIORITY CALCULATION (Example 4):
           - Importance weighting
           - Recency consideration
           - Success metric integration
           - Replay count balancing
           - Automatic priority ranking
        
        5. INTERLEAVED REPLAY (Example 5):
           - Mixing old and new memories
           - Balanced temporal distribution
           - Prevents recency bias
           - Catastrophic forgetting prevention
        
        6. GENERATIVE REPLAY (Example 6):
           - Synthetic memory generation
           - LLM-based variations
           - Knowledge expansion
           - Creative recombination
        
        7. AGENT INTEGRATION (Example 7):
           - Periodic automatic replay
           - Experience-based learning
           - Strongest memory identification
           - Continuous consolidation
        
        8. STATISTICS TRACKING (Example 8):
           - Buffer usage monitoring
           - Replay session counting
           - Strength progression
           - System performance metrics
        
        KEY BENEFITS:
        ✓ Strengthens important memories
        ✓ Improves learning efficiency
        ✓ Enables offline consolidation
        ✓ Prevents catastrophic forgetting
        ✓ Discovers new patterns
        ✓ Simulates sleep consolidation
        ✓ Supports continuous learning
        
        USE CASES:
        • Reinforcement learning agents
        • Skill learning and retention
        • Memory consolidation systems
        • Experience-based learning
        • Pattern recognition improvement
        • Knowledge transfer and generalization
        • Continuous learning systems
        
        BEST PRACTICES:
        1. Schedule replay during low-activity periods
        2. Use priority-based selection for efficiency
        3. Balance old and new experiences
        4. Monitor strengthening effectiveness
        5. Limit replay buffer size appropriately
        6. Track replay metrics for tuning
        7. Consider computational budget
        8. Implement gradual strengthening
        
        TRADE-OFFS:
        • Computational cost vs. learning benefit
        • Replay frequency vs. new experiences
        • Buffer size vs. memory usage
        • Exact vs. generative replay
        
        PRODUCTION CONSIDERATIONS:
        → Schedule replay during off-peak hours
        → Implement priority-based sampling for efficiency
        → Monitor replay effectiveness metrics
        → Set appropriate buffer size limits
        → Balance replay with new experience learning
        → Track memory strength progression
        → Implement incremental consolidation
        → Consider distributed replay for scale
        → Use batch processing for efficiency
        → Monitor computational costs
        
        This pattern enables efficient memory consolidation and learning
        through strategic replay and rehearsal, mimicking human memory
        consolidation during sleep and practice.
        """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_memory_replay()
    
        

    Agentic Design Pattern: Memory Replay

    Pattern 104: Memory Replay – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 104: Memory Replay & Rehearsal

    Description: The Memory Replay & Rehearsal pattern implements mechanisms for re-experiencing and strengthening memories through replay, similar to how the human brain consolidates memories during sleep and practice. This pattern is crucial for learning, skill retention, and memory consolidation in long-running agent systems.

    Memory replay involves re-activating stored experiences to strengthen neural
    patterns, transfer information from short-term to long-term storage, and discover
    new insights through recombination. Rehearsal can be exact (episodic replay) or
    generative (synthetic variations), and can occur during downtime, triggered by
    context, or scheduled periodically.
    
    This pattern supports various replay strategies including experience replay
    (reinforcement learning), prioritized replay (focus on important memories),
    interleaved replay (mixing old and new), and generative replay (creating
    variations). It implements replay scheduling, priority calculation, and
    consolidation effects.
    

    Key Components: 1. Memory Buffer: Store of experiences to replay 2. Replay Strategy: Algorithm for selecting memories 3. Replay Scheduler: When to perform replay 4. Priority Calculator: Importance scoring 5. Consolidation Engine: Strengthening mechanism 6. Synthetic Generator: Create variations 7. Replay Metrics: Track effectiveness

    Replay Types: 1. Experience Replay: Re-live past experiences 2. Prioritized Replay: Focus on important/surprising 3. Interleaved Replay: Mix old and new memories 4. Generative Replay: Create synthetic variations 5. Forward Replay: Chronological order 6. Reverse Replay: Reverse chronological 7. Random Replay: Random sampling

    Replay Timing: 1. Offline Replay: During downtime/sleep 2. Online Replay: During active tasks 3. Triggered Replay: By context or cues 4. Periodic Replay: Scheduled intervals 5. Event-driven Replay: After significant events

    Use Cases: – Reinforcement learning agents – Skill learning and retention – Memory consolidation systems – Experience-based learning – Pattern recognition improvement – Knowledge transfer and generalization – Continuous learning systems

    Advantages: – Strengthens important memories – Improves learning efficiency – Enables offline learning – Prevents catastrophic forgetting – Discovers new patterns – Transfers knowledge – Simulates “sleep consolidation”

    Challenges: – Computational cost of replay – Selecting which memories to replay – Balancing old vs. new experiences – Avoiding overfitting to replayed memories – Determining replay frequency – Managing replay buffer size

    LangChain Implementation: This implementation uses LangChain for: – LLM-based memory importance assessment – Synthetic memory generation – Pattern extraction from replay – Learning from replayed experiences

    Production Considerations: – Schedule replay during low-activity periods – Implement priority-based sampling – Monitor replay effectiveness metrics – Limit replay buffer size – Balance replay with new experiences – Track memory strengthening – Implement incremental consolidation – Consider computational budget


    File Statistics

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

    Module Documentation

    """
    Pattern 104: Memory Replay & Rehearsal
    
    Description:
        The Memory Replay & Rehearsal pattern implements mechanisms for re-experiencing
        and strengthening memories through replay, similar to how the human brain
        consolidates memories during sleep and practice. This pattern is crucial for
        learning, skill retention, and memory consolidation in long-running agent systems.
    
        Memory replay involves re-activating stored experiences to strengthen neural
        patterns, transfer information from short-term to long-term storage, and discover
        new insights through recombination. Rehearsal can be exact (episodic replay) or
        generative (synthetic variations), and can occur during downtime, triggered by
        context, or scheduled periodically.
    
        This pattern supports various replay strategies including experience replay
        (reinforcement learning), prioritized replay (focus on important memories),
        interleaved replay (mixing old and new), and generative replay (creating
        variations). It implements replay scheduling, priority calculation, and
        consolidation effects.
    
    Key Components:
        1. Memory Buffer: Store of experiences to replay
        2. Replay Strategy: Algorithm for selecting memories
        3. Replay Scheduler: When to perform replay
        4. Priority Calculator: Importance scoring
        5. Consolidation Engine: Strengthening mechanism
        6. Synthetic Generator: Create variations
        7. Replay Metrics: Track effectiveness
    
    Replay Types:
        1. Experience Replay: Re-live past experiences
        2. Prioritized Replay: Focus on important/surprising
        3. Interleaved Replay: Mix old and new memories
        4. Generative Replay: Create synthetic variations
        5. Forward Replay: Chronological order
        6. Reverse Replay: Reverse chronological
        7. Random Replay: Random sampling
    
    Replay Timing:
        1. Offline Replay: During downtime/sleep
        2. Online Replay: During active tasks
        3. Triggered Replay: By context or cues
        4. Periodic Replay: Scheduled intervals
        5. Event-driven Replay: After significant events
    
    Use Cases:
        - Reinforcement learning agents
        - Skill learning and retention
        - Memory consolidation systems
        - Experience-based learning
        - Pattern recognition improvement
        - Knowledge transfer and generalization
        - Continuous learning systems
    
    Advantages:
        - Strengthens important memories
        - Improves learning efficiency
        - Enables offline learning
        - Prevents catastrophic forgetting
        - Discovers new patterns
        - Transfers knowledge
        - Simulates "sleep consolidation"
    
    Challenges:
        - Computational cost of replay
        - Selecting which memories to replay
        - Balancing old vs. new experiences
        - Avoiding overfitting to replayed memories
        - Determining replay frequency
        - Managing replay buffer size
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based memory importance assessment
        - Synthetic memory generation
        - Pattern extraction from replay
        - Learning from replayed experiences
    
    Production Considerations:
        - Schedule replay during low-activity periods
        - Implement priority-based sampling
        - Monitor replay effectiveness metrics
        - Limit replay buffer size
        - Balance replay with new experiences
        - Track memory strengthening
        - Implement incremental consolidation
        - Consider computational budget
    """
    

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


    Import Statements

    Standard Library Imports

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

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

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: ReplayStrategy

    Lines 108-115

    Inherits from: Enum

    Purpose:

    Strategy for replaying memories.
    

    Class: ReplayTrigger

    Lines 118-124

    Inherits from: Enum

    Purpose:

    When replay is triggered.
    

    Class: MemoryTrace

    Lines 128-153

    Purpose:

    Memory trace for replay.
    
    Attributes:
        trace_id: Unique identifier
        content: Memory content
        context: Context when created
        timestamp: Creation time
        importance: Importance score
        replay_count: Times replayed
        strength: Memory strength
        last_replayed: Last replay time
        success_metric: Performance metric
        metadata: Additional information
    

    Class: MemoryReplaySystem

    Lines 156-453

    Purpose:

    Memory replay and rehearsal system.
    
    This system manages the replay of stored memories for consolidation,
    learning, and pattern discovery.
    

    Methods:

    __init__(self, buffer_size, replay_batch_size, strategy, strengthening_rate, temperature)

    Lines 164-190

    Purpose:

    Initialize memory replay system.
    
    Args:
        buffer_size: Maximum memories to store
        replay_batch_size: Memories per replay session
        strategy: Replay selection strategy
        strengthening_rate: How much to strengthen on replay
        temperature: LLM temperature
    

    Parameters: – buffer_size: Buffer size – replay_batch_size: Replay batch size – strategy: Strategy – strengthening_rate: Strengthening rate – temperature: Sampling temperature (0-1)

    store_memory(self, content, context, importance, success_metric)

    Lines 192-222

    Purpose:

    Store memory for potential replay.
    
    Args:
        content: Memory content
        context: Context information
        importance: Importance score
        success_metric: Success/performance metric
    
    Returns:
        Created memory trace
    

    Parameters: – content: Content – context: Context – importance: Importance – success_metric: Success metric

    Returns: MemoryTrace

    calculate_priority(self, trace)

    Lines 224-252

    Purpose:

    Calculate replay priority for memory.
    
    Args:
        trace: Memory trace
    
    Returns:
        Priority score
    

    Parameters: – trace: Trace

    Returns: float

    select_memories_for_replay(self, n)

    Lines 254-307

    Purpose:

    Select memories to replay based on strategy.
    
    Args:
        n: Number of memories (default: replay_batch_size)
    
    Returns:
        List of memory traces to replay
    

    Parameters: – n: N

    Returns: <ast.Subscript object at 0x000001A06D71A410>

    replay(self, n, consolidate)

    Lines 309-359

    Purpose:

    Perform memory replay.
    
    Args:
        n: Number of memories to replay
        consolidate: Whether to strengthen memories
    
    Returns:
        Replay statistics
    

    Parameters: – n: N – consolidate: Consolidate

    Returns: <ast.Subscript object at 0x000001A06D701710>

    generate_synthetic_memory(self, base_memory, variation)

    Lines 361-399

    Purpose:

    Generate synthetic memory variation.
    
    Args:
        base_memory: Memory to vary
        variation: Type of variation
    
    Returns:
        New synthetic memory
    

    Parameters: – base_memory: Memory store for conversation history – variation: Variation

    Returns: MemoryTrace

    generative_replay(self, n_synthetic, min_importance)

    Lines 401-432

    Purpose:

    Perform generative replay with synthetic variations.
    
    Args:
        n_synthetic: Number of synthetic memories to generate
        min_importance: Minimum importance for base memories
    
    Returns:
        List of generated synthetic memories
    

    Parameters: – n_synthetic: N synthetic – min_importance: Min importance

    Returns: <ast.Subscript object at 0x000001A06D6624D0>

    get_statistics(self)

    Lines 434-453

    Purpose:

    Get replay statistics.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D83CF50>


    Class: ReplayLearningAgent

    Lines 456-525

    Purpose:

    Agent that learns through memory replay.
    
    This agent uses replay to consolidate memories and improve
    performance through rehearsal.
    

    Methods:

    __init__(self, buffer_size, replay_interval, temperature)

    Lines 464-484

    Purpose:

    Initialize replay learning agent.
    
    Args:
        buffer_size: Memory buffer size
        replay_interval: Interactions between replays
        temperature: LLM temperature
    

    Parameters: – buffer_size: Buffer size – replay_interval: Replay interval – temperature: Sampling temperature (0-1)

    experience(self, content, context, importance, success)

    Lines 486-507

    Purpose:

    Record an experience.
    
    Args:
        content: Experience content
        context: Context information
        importance: Importance score
        success: Success metric
    

    Parameters: – content: Content – context: Context – importance: Importance – success: Success

    consolidate(self)

    Lines 509-511

    Purpose:

    Perform memory consolidation through replay.
    

    Parameters:

    get_strong_memories(self, n)

    Lines 513-525

    Purpose:

    Get strongest memories.
    
    Args:
        n: Number of memories
    
    Returns:
        List of strong memories
    

    Parameters: – n: N

    Returns: <ast.Subscript object at 0x000001A06D88A710>


    Functions

    demonstrate_memory_replay()

    Lines 528-898

    Purpose:

    Demonstrate memory replay and rehearsal pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Memory Replay 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 103
    • Pattern 105

    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 Memory Replay 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: 104_memory_replay.py
    Total Lines: 903
    Pattern Category: AI Agent Design Pattern #104


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Associative Networks

    
        """
    Pattern 103: Associative Memory Networks
    
    Description:
        The Associative Memory Networks pattern implements memory systems where memories
        are interconnected through associations, enabling retrieval through spreading
        activation and contextual relationships. This pattern mimics human associative
        memory where one memory can trigger recall of related memories through various
        types of connections (semantic, temporal, causal, episodic).
        
        Associative networks allow agents to discover indirect relationships, make
        creative connections, and retrieve relevant information through chains of
        associations rather than direct queries. Each memory node can be linked to
        others through typed relationships, and activation spreads through the network
        when memories are accessed, bringing related memories to attention.
        
        This pattern supports various association types including semantic similarity,
        temporal co-occurrence, causal relationships, part-whole hierarchies, and
        user-defined custom links. It implements spreading activation algorithms,
        association strength tracking, and network-based retrieval strategies.
    
    Key Components:
        1. Memory Nodes: Individual memory items in the network
        2. Association Links: Typed connections between memories
        3. Spreading Activation: Propagation of activation through network
        4. Association Strength: Weight of connections
        5. Activation Decay: Gradual decrease of activation
        6. Network Traversal: Algorithms for exploring associations
        7. Link Types: Different relationship types
    
    Association Types:
        1. Semantic: Similar meaning or concept
        2. Temporal: Occurred at similar times
        3. Causal: Cause-effect relationships
        4. Spatial: Co-location in space
        5. Episodic: Part of same experience
        6. Hierarchical: Part-whole, is-a relationships
        7. Functional: Used together or for similar purposes
        
    Spreading Activation:
        1. Initial Activation: Starting memory gets activation
        2. Propagation: Activation spreads to connected memories
        3. Decay: Activation weakens with distance
        4. Threshold: Minimum activation for retrieval
        5. Convergence: Multiple paths strengthen activation
        6. Inhibition: Negative associations reduce activation
    
    Use Cases:
        - Creative problem solving (analogies, brainstorming)
        - Context-aware information retrieval
        - Knowledge discovery and inference
        - Recommendation systems
        - Learning from experience
        - Concept mapping and knowledge graphs
        - Semantic search and exploration
    
    Advantages:
        - Flexible retrieval through associations
        - Discovers indirect relationships
        - Mimics human memory organization
        - Supports creative connections
        - Handles ambiguous queries
        - Learns from access patterns
        - Enables serendipitous discovery
    
    Challenges:
        - Computational complexity of spreading activation
        - Managing association strength decay
        - Preventing over-activation (too many results)
        - Determining appropriate association types
        - Balancing network density
        - Avoiding spurious associations
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based semantic similarity calculation
        - Association type inference
        - Context-aware link creation
        - Network-based reasoning
        
    Production Considerations:
        - Implement efficient graph traversal algorithms
        - Set appropriate activation thresholds
        - Monitor network size and complexity
        - Prune weak associations periodically
        - Cache frequently accessed paths
        - Consider distributed graph storage
        - Implement incremental updates
        - Balance precision vs. recall in retrieval
    """
    
    import os
    from typing import List, Dict, Any, Optional, Set, Tuple
    from dataclasses import dataclass, field
    from datetime import datetime
    from enum import Enum
    from collections import defaultdict, deque
    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 AssociationType(Enum):
        """Types of associations between memories."""
        SEMANTIC = "semantic"  # Similar meaning
        TEMPORAL = "temporal"  # Time-based
        CAUSAL = "causal"  # Cause-effect
        SPATIAL = "spatial"  # Location-based
        EPISODIC = "episodic"  # Same experience
        HIERARCHICAL = "hierarchical"  # Part-whole
        FUNCTIONAL = "functional"  # Used together
        CUSTOM = "custom"  # User-defined
    
    
    @dataclass
    class MemoryNode:
        """
        Node in associative memory network.
        
        Attributes:
            node_id: Unique identifier
            content: Memory content
            created_at: Creation timestamp
            activation: Current activation level
            base_activation: Base activation strength
            tags: Associated tags
            metadata: Additional information
        """
        node_id: str
        content: str
        created_at: datetime
        activation: float = 0.0
        base_activation: float = 0.5
        tags: List[str] = field(default_factory=list)
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class Association:
        """
        Link between memory nodes.
        
        Attributes:
            from_node: Source node ID
            to_node: Target node ID
            association_type: Type of association
            strength: Association strength (0-1)
            created_at: When association was created
            access_count: Number of times traversed
            metadata: Additional information
        """
        from_node: str
        to_node: str
        association_type: AssociationType
        strength: float
        created_at: datetime
        access_count: int = 0
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    class AssociativeMemoryNetwork:
        """
        Associative memory network with spreading activation.
        
        This class implements a network of interconnected memories with
        various association types and spreading activation for retrieval.
        """
        
        def __init__(
            self,
            activation_decay: float = 0.5,
            activation_threshold: float = 0.3,
            max_spread_depth: int = 3,
            temperature: float = 0.3
        ):
            """
            Initialize associative memory network.
            
            Args:
                activation_decay: Decay factor for spreading (0-1)
                activation_threshold: Minimum activation for retrieval
                max_spread_depth: Maximum depth for spreading activation
                temperature: LLM temperature
            """
            self.activation_decay = activation_decay
            self.activation_threshold = activation_threshold
            self.max_spread_depth = max_spread_depth
            
            self.nodes: Dict[str, MemoryNode] = {}
            self.associations: Dict[str, List[Association]] = defaultdict(list)
            self.reverse_associations: Dict[str, List[Association]] = defaultdict(list)
            
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.node_counter = 0
        
        def add_memory(
            self,
            content: str,
            tags: List[str] = None,
            base_activation: float = 0.5
        ) -> MemoryNode:
            """
            Add memory node to network.
            
            Args:
                content: Memory content
                tags: Associated tags
                base_activation: Base activation level
                
            Returns:
                Created memory node
            """
            self.node_counter += 1
            node_id = f"node_{self.node_counter}"
            
            node = MemoryNode(
                node_id=node_id,
                content=content,
                created_at=datetime.now(),
                base_activation=base_activation,
                tags=tags or []
            )
            
            self.nodes[node_id] = node
            return node
        
        def associate(
            self,
            from_node_id: str,
            to_node_id: str,
            association_type: AssociationType,
            strength: float = 0.5,
            bidirectional: bool = True
        ):
            """
            Create association between nodes.
            
            Args:
                from_node_id: Source node
                to_node_id: Target node
                association_type: Type of association
                strength: Association strength
                bidirectional: Create reverse link too
            """
            if from_node_id not in self.nodes or to_node_id not in self.nodes:
                return
            
            # Forward association
            assoc = Association(
                from_node=from_node_id,
                to_node=to_node_id,
                association_type=association_type,
                strength=strength,
                created_at=datetime.now()
            )
            self.associations[from_node_id].append(assoc)
            self.reverse_associations[to_node_id].append(assoc)
            
            # Bidirectional association
            if bidirectional:
                reverse_assoc = Association(
                    from_node=to_node_id,
                    to_node=from_node_id,
                    association_type=association_type,
                    strength=strength,
                    created_at=datetime.now()
                )
                self.associations[to_node_id].append(reverse_assoc)
                self.reverse_associations[from_node_id].append(reverse_assoc)
        
        def auto_associate(
            self,
            node_id: str,
            min_strength: float = 0.4
        ):
            """
            Automatically create associations based on similarity.
            
            Args:
                node_id: Node to associate
                min_strength: Minimum strength for association
            """
            if node_id not in self.nodes:
                return
            
            node = self.nodes[node_id]
            
            # Find similar nodes by tags
            for other_id, other_node in self.nodes.items():
                if other_id == node_id:
                    continue
                
                # Tag-based similarity
                common_tags = set(node.tags) & set(other_node.tags)
                if common_tags and len(node.tags) > 0:
                    strength = len(common_tags) / max(len(node.tags), len(other_node.tags))
                    
                    if strength >= min_strength:
                        self.associate(
                            node_id,
                            other_id,
                            AssociationType.SEMANTIC,
                            strength,
                            bidirectional=True
                        )
        
        def spread_activation(
            self,
            start_node_id: str,
            initial_activation: float = 1.0
        ) -> Dict[str, float]:
            """
            Spread activation from starting node through network.
            
            Args:
                start_node_id: Starting node
                initial_activation: Initial activation level
                
            Returns:
                Dictionary of node_id -> activation level
            """
            if start_node_id not in self.nodes:
                return {}
            
            # Reset all activations
            for node in self.nodes.values():
                node.activation = 0.0
            
            # Initialize with starting node
            activations = {start_node_id: initial_activation}
            self.nodes[start_node_id].activation = initial_activation
            
            # Breadth-first spreading
            queue = deque([(start_node_id, initial_activation, 0)])
            visited = set()
            
            while queue:
                current_id, current_activation, depth = queue.popleft()
                
                if current_id in visited or depth >= self.max_spread_depth:
                    continue
                
                visited.add(current_id)
                
                # Spread to associated nodes
                for assoc in self.associations.get(current_id, []):
                    target_id = assoc.to_node
                    
                    # Calculate activation with decay
                    decay = self.activation_decay ** depth
                    new_activation = current_activation * assoc.strength * decay
                    
                    # Update activation (cumulative)
                    if target_id in activations:
                        activations[target_id] += new_activation
                    else:
                        activations[target_id] = new_activation
                    
                    self.nodes[target_id].activation = activations[target_id]
                    
                    # Track association usage
                    assoc.access_count += 1
                    
                    # Continue spreading if above threshold
                    if new_activation > self.activation_threshold:
                        queue.append((target_id, new_activation, depth + 1))
            
            return activations
        
        def retrieve_associated(
            self,
            node_id: str,
            min_activation: Optional[float] = None,
            max_results: int = 10
        ) -> List[Tuple[MemoryNode, float]]:
            """
            Retrieve memories associated with given node.
            
            Args:
                node_id: Starting node
                min_activation: Minimum activation threshold
                max_results: Maximum number of results
                
            Returns:
                List of (node, activation) tuples
            """
            if min_activation is None:
                min_activation = self.activation_threshold
            
            # Spread activation
            activations = self.spread_activation(node_id)
            
            # Filter and sort by activation
            results = [
                (self.nodes[nid], activation)
                for nid, activation in activations.items()
                if nid != node_id and activation >= min_activation
            ]
            
            results.sort(key=lambda x: x[1], reverse=True)
            return results[:max_results]
        
        def find_path(
            self,
            from_node_id: str,
            to_node_id: str,
            max_depth: int = 5
        ) -> Optional[List[str]]:
            """
            Find path between two nodes.
            
            Args:
                from_node_id: Start node
                to_node_id: End node
                max_depth: Maximum path length
                
            Returns:
                List of node IDs in path, or None
            """
            if from_node_id not in self.nodes or to_node_id not in self.nodes:
                return None
            
            # BFS to find shortest path
            queue = deque([(from_node_id, [from_node_id])])
            visited = set()
            
            while queue:
                current_id, path = queue.popleft()
                
                if len(path) > max_depth:
                    continue
                
                if current_id == to_node_id:
                    return path
                
                if current_id in visited:
                    continue
                
                visited.add(current_id)
                
                # Explore neighbors
                for assoc in self.associations.get(current_id, []):
                    if assoc.to_node not in visited:
                        queue.append((assoc.to_node, path + [assoc.to_node]))
            
            return None
        
        def get_statistics(self) -> Dict[str, Any]:
            """Get network statistics."""
            total_associations = sum(len(assocs) for assocs in self.associations.values())
            
            assoc_types = defaultdict(int)
            for assocs in self.associations.values():
                for assoc in assocs:
                    assoc_types[assoc.association_type.value] += 1
            
            return {
                "total_nodes": len(self.nodes),
                "total_associations": total_associations,
                "avg_associations_per_node": total_associations / len(self.nodes) if self.nodes else 0,
                "association_types": dict(assoc_types)
            }
    
    
    class AssociativeMemoryAgent:
        """
        Agent with associative memory network.
        
        This agent uses associative memory for context-aware retrieval
        and creative problem-solving through memory associations.
        """
        
        def __init__(
            self,
            activation_decay: float = 0.5,
            temperature: float = 0.5
        ):
            """
            Initialize associative memory agent.
            
            Args:
                activation_decay: Activation decay factor
                temperature: LLM temperature
            """
            self.memory_network = AssociativeMemoryNetwork(
                activation_decay=activation_decay
            )
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
        
        def learn(
            self,
            content: str,
            tags: List[str] = None,
            related_to: List[str] = None
        ) -> MemoryNode:
            """
            Learn new information with associations.
            
            Args:
                content: Content to learn
                tags: Associated tags
                related_to: IDs of related memories
                
            Returns:
                Created memory node
            """
            # Add memory
            node = self.memory_network.add_memory(content, tags)
            
            # Auto-associate based on tags
            self.memory_network.auto_associate(node.node_id)
            
            # Explicit associations
            if related_to:
                for related_id in related_to:
                    self.memory_network.associate(
                        node.node_id,
                        related_id,
                        AssociationType.SEMANTIC,
                        0.7
                    )
            
            return node
        
        def recall(
            self,
            query: str,
            max_results: int = 5
        ) -> List[Tuple[MemoryNode, float, List[str]]]:
            """
            Recall memories through associative retrieval.
            
            Args:
                query: Query string
                max_results: Maximum results
                
            Returns:
                List of (node, activation, path) tuples
            """
            # Find direct matches
            matching_nodes = []
            for node in self.memory_network.nodes.values():
                if query.lower() in node.content.lower():
                    matching_nodes.append(node)
            
            if not matching_nodes:
                return []
            
            # Use first match as starting point for spreading activation
            start_node = matching_nodes[0]
            
            # Spread activation
            associated = self.memory_network.retrieve_associated(
                start_node.node_id,
                max_results=max_results * 2
            )
            
            # Build results with paths
            results = []
            for node, activation in associated:
                path = self.memory_network.find_path(start_node.node_id, node.node_id)
                results.append((node, activation, path or []))
            
            return results[:max_results]
    
    
    def demonstrate_associative_memory():
        """Demonstrate associative memory networks pattern."""
        
        print("=" * 80)
        print("ASSOCIATIVE MEMORY NETWORKS PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic network creation and associations
        print("\n" + "=" * 80)
        print("Example 1: Building Associative Memory Network")
        print("=" * 80)
        
        network = AssociativeMemoryNetwork(
            activation_decay=0.6,
            activation_threshold=0.2,
            max_spread_depth=3
        )
        
        print("\nAdding memories about animals...")
        
        # Add animal memories
        dog = network.add_memory("Dog is a pet animal", ["animal", "pet", "mammal"])
        cat = network.add_memory("Cat is a pet animal", ["animal", "pet", "mammal"])
        lion = network.add_memory("Lion is a wild animal", ["animal", "wild", "mammal"])
        bird = network.add_memory("Bird can fly", ["animal", "flying"])
        fish = network.add_memory("Fish lives in water", ["animal", "aquatic"])
        
        print(f"  Added {len(network.nodes)} memory nodes")
        
        # Create explicit associations
        print("\nCreating associations...")
        network.associate(dog.node_id, cat.node_id, AssociationType.SEMANTIC, 0.8)
        network.associate(dog.node_id, lion.node_id, AssociationType.HIERARCHICAL, 0.5)
        network.associate(cat.node_id, lion.node_id, AssociationType.HIERARCHICAL, 0.5)
        
        stats = network.get_statistics()
        print(f"  Total associations: {stats['total_associations']}")
        print(f"  Avg per node: {stats['avg_associations_per_node']:.1f}")
        
        # Example 2: Spreading activation
        print("\n" + "=" * 80)
        print("Example 2: Spreading Activation")
        print("=" * 80)
        
        print(f"\nActivating memory: '{dog.content}'")
        activations = network.spread_activation(dog.node_id, initial_activation=1.0)
        
        print("\nActivation levels:")
        sorted_activations = sorted(
            activations.items(),
            key=lambda x: x[1],
            reverse=True
        )
        for node_id, activation in sorted_activations:
            node = network.nodes[node_id]
            print(f"  [{activation:.3f}] {node.content}")
        
        # Example 3: Associative retrieval
        print("\n" + "=" * 80)
        print("Example 3: Associative Retrieval")
        print("=" * 80)
        
        print(f"\nRetrieving memories associated with 'Dog'...")
        associated_memories = network.retrieve_associated(dog.node_id, max_results=5)
        
        print(f"\nFound {len(associated_memories)} associated memories:")
        for node, activation in associated_memories:
            print(f"  [{activation:.3f}] {node.content}")
            print(f"           Tags: {node.tags}")
        
        # Example 4: Path finding
        print("\n" + "=" * 80)
        print("Example 4: Finding Association Paths")
        print("=" * 80)
        
        print(f"\nFinding path from 'Dog' to 'Lion'...")
        path = network.find_path(dog.node_id, lion.node_id)
        
        if path:
            print(f"  Path found ({len(path)} steps):")
            for i, node_id in enumerate(path):
                node = network.nodes[node_id]
                print(f"    {i+1}. {node.content}")
        
        # Example 5: Auto-association by tags
        print("\n" + "=" * 80)
        print("Example 5: Automatic Association by Similarity")
        print("=" * 80)
        
        auto_network = AssociativeMemoryNetwork()
        
        # Add memories with tags
        memories = [
            ("Python is a programming language", ["programming", "python", "language"]),
            ("Java is a programming language", ["programming", "java", "language"]),
            ("Machine learning uses Python", ["programming", "python", "ml"]),
            ("Data science involves statistics", ["data", "statistics", "analysis"]),
            ("Python is popular in data science", ["programming", "python", "data"]),
        ]
        
        nodes = []
        for content, tags in memories:
            node = auto_network.add_memory(content, tags)
            nodes.append(node)
            auto_network.auto_associate(node.node_id, min_strength=0.4)
        
        print(f"\nAdded {len(nodes)} memories")
        
        stats = auto_network.get_statistics()
        print(f"Auto-created associations: {stats['total_associations']}")
        print(f"Association types: {stats['association_types']}")
        
        # Show associations for first memory
        print(f"\nAssociations for: '{nodes[0].content}'")
        assocs = auto_network.associations[nodes[0].node_id]
        for assoc in assocs:
            target = auto_network.nodes[assoc.to_node]
            print(f"  → [{assoc.strength:.2f}] {target.content}")
        
        # Example 6: Multi-hop retrieval
        print("\n" + "=" * 80)
        print("Example 6: Multi-Hop Associative Retrieval")
        print("=" * 80)
        
        # Create network with multi-hop connections
        hop_network = AssociativeMemoryNetwork(
            max_spread_depth=4,
            activation_threshold=0.15
        )
        
        # Create a knowledge chain
        apple = hop_network.add_memory("Apple is a fruit", ["food", "fruit"])
        fruit = hop_network.add_memory("Fruit is healthy", ["food", "health"])
        health = hop_network.add_memory("Health requires exercise", ["health", "fitness"])
        exercise = hop_network.add_memory("Exercise burns calories", ["fitness", "energy"])
        
        # Chain associations
        hop_network.associate(apple.node_id, fruit.node_id, AssociationType.HIERARCHICAL, 0.8)
        hop_network.associate(fruit.node_id, health.node_id, AssociationType.CAUSAL, 0.7)
        hop_network.associate(health.node_id, exercise.node_id, AssociationType.CAUSAL, 0.6)
        
        print("\nKnowledge chain created:")
        print("  Apple → Fruit → Health → Exercise")
        
        print(f"\nStarting from 'Apple', spreading activation...")
        multi_hop = hop_network.retrieve_associated(apple.node_id, max_results=10)
        
        print(f"\nRetrieved {len(multi_hop)} memories through associations:")
        for node, activation in multi_hop:
            path = hop_network.find_path(apple.node_id, node.node_id)
            hops = len(path) - 1 if path else 0
            print(f"  [{activation:.3f}] ({hops} hops) {node.content}")
        
        # Example 7: Associative memory agent
        print("\n" + "=" * 80)
        print("Example 7: Agent with Associative Memory")
        print("=" * 80)
        
        agent = AssociativeMemoryAgent(activation_decay=0.6)
        
        print("\nTeaching agent about programming concepts...")
        
        # Teach related concepts
        python_node = agent.learn(
            "Python is a high-level programming language",
            tags=["programming", "python"]
        )
        
        django_node = agent.learn(
            "Django is a Python web framework",
            tags=["programming", "python", "web"],
            related_to=[python_node.node_id]
        )
        
        flask_node = agent.learn(
            "Flask is a lightweight Python framework",
            tags=["programming", "python", "web"],
            related_to=[python_node.node_id, django_node.node_id]
        )
        
        agent.learn(
            "NumPy is a Python library for numerical computing",
            tags=["programming", "python", "data"],
            related_to=[python_node.node_id]
        )
        
        agent.learn(
            "pandas is used for data analysis in Python",
            tags=["programming", "python", "data"],
            related_to=[python_node.node_id]
        )
        
        print(f"  Learned {len(agent.memory_network.nodes)} concepts")
        
        # Recall with associations
        print("\nRecalling memories about 'Python'...")
        recalled = agent.recall("Python", max_results=5)
        
        print(f"\nRecalled {len(recalled)} associated memories:")
        for node, activation, path in recalled:
            print(f"  [{activation:.3f}] {node.content}")
            if path and len(path) > 1:
                print(f"           Path: {' → '.join(path[:3])}")
        
        # Example 8: Network statistics and visualization
        print("\n" + "=" * 80)
        print("Example 8: Network Analysis")
        print("=" * 80)
        
        stats = agent.memory_network.get_statistics()
        
        print("\nNetwork Statistics:")
        print(f"  Total Nodes: {stats['total_nodes']}")
        print(f"  Total Associations: {stats['total_associations']}")
        print(f"  Avg Associations/Node: {stats['avg_associations_per_node']:.2f}")
        print(f"  Association Types:")
        for assoc_type, count in stats['association_types'].items():
            print(f"    - {assoc_type}: {count}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY: Associative Memory Networks Pattern")
        print("=" * 80)
        
        summary = """
        The Associative Memory Networks pattern demonstrated:
        
        1. NETWORK CONSTRUCTION (Example 1):
           - Memory nodes with content and tags
           - Typed associations between memories
           - Semantic, hierarchical, and custom links
           - Bidirectional and directional connections
        
        2. SPREADING ACTIVATION (Example 2):
           - Initial activation at source node
           - Propagation through network links
           - Activation decay with distance
           - Strength-weighted spreading
           - Cumulative activation from multiple paths
        
        3. ASSOCIATIVE RETRIEVAL (Example 3):
           - Activation-based memory retrieval
           - Threshold filtering
           - Ranked results by activation
           - Context-aware recall
        
        4. PATH FINDING (Example 4):
           - Shortest path between memories
           - Breadth-first search
           - Multi-hop connections
           - Relationship discovery
        
        5. AUTO-ASSOCIATION (Example 5):
           - Tag-based similarity
           - Automatic link creation
           - Strength calculation
           - Semantic relationships
        
        6. MULTI-HOP RETRIEVAL (Example 6):
           - Deep network traversal
           - Chain of associations
           - Indirect relationships
           - Knowledge inference
        
        7. AGENT INTEGRATION (Example 7):
           - Learning with associations
           - Context-aware recall
           - Path visualization
           - Association strengthening
        
        8. NETWORK ANALYSIS (Example 8):
           - Node and link statistics
           - Association type distribution
           - Network density metrics
           - Structure analysis
        
        KEY BENEFITS:
        ✓ Flexible context-aware retrieval
        ✓ Discovers indirect relationships
        ✓ Mimics human associative memory
        ✓ Supports creative connections
        ✓ Handles ambiguous queries
        ✓ Learns from access patterns
        ✓ Enables serendipitous discovery
        
        USE CASES:
        • Creative problem solving and brainstorming
        • Context-aware information retrieval
        • Knowledge discovery and inference
        • Recommendation systems
        • Learning from experience
        • Concept mapping and knowledge graphs
        • Semantic search and exploration
        
        BEST PRACTICES:
        1. Choose appropriate association types
        2. Set reasonable activation thresholds
        3. Limit spreading depth for performance
        4. Prune weak associations periodically
        5. Use tags for initial similarity
        6. Track and strengthen frequently used paths
        7. Balance network density
        8. Monitor spreading activation performance
        
        TRADE-OFFS:
        • Complexity vs. flexibility
        • Precision vs. recall
        • Network size vs. performance
        • Automatic vs. manual association
        
        PRODUCTION CONSIDERATIONS:
        → Implement efficient graph storage (Neo4j, NetworkX)
        → Cache frequently accessed activation patterns
        → Set appropriate activation thresholds
        → Monitor and prune weak associations
        → Consider distributed graph for scale
        → Implement incremental activation updates
        → Track association usage for strengthening
        → Balance exploration vs. exploitation
        → Use approximate algorithms for large networks
        → Consider temporal decay of associations
        
        This pattern enables flexible, context-aware memory retrieval through
        network-based associations, supporting creative problem-solving and
        knowledge discovery through spreading activation mechanisms.
        """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_associative_memory()
    
        

    Agentic Design Pattern: Associative Networks

    Pattern 103: Associative Networks – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 103: Associative Memory Networks

    Description: The Associative Memory Networks pattern implements memory systems where memories are interconnected through associations, enabling retrieval through spreading activation and contextual relationships. This pattern mimics human associative memory where one memory can trigger recall of related memories through various types of connections (semantic, temporal, causal, episodic).

    Associative networks allow agents to discover indirect relationships, make
    creative connections, and retrieve relevant information through chains of
    associations rather than direct queries. Each memory node can be linked to
    others through typed relationships, and activation spreads through the network
    when memories are accessed, bringing related memories to attention.
    
    This pattern supports various association types including semantic similarity,
    temporal co-occurrence, causal relationships, part-whole hierarchies, and
    user-defined custom links. It implements spreading activation algorithms,
    association strength tracking, and network-based retrieval strategies.
    

    Key Components: 1. Memory Nodes: Individual memory items in the network 2. Association Links: Typed connections between memories 3. Spreading Activation: Propagation of activation through network 4. Association Strength: Weight of connections 5. Activation Decay: Gradual decrease of activation 6. Network Traversal: Algorithms for exploring associations 7. Link Types: Different relationship types

    Association Types: 1. Semantic: Similar meaning or concept 2. Temporal: Occurred at similar times 3. Causal: Cause-effect relationships 4. Spatial: Co-location in space 5. Episodic: Part of same experience 6. Hierarchical: Part-whole, is-a relationships 7. Functional: Used together or for similar purposes

    Spreading Activation: 1. Initial Activation: Starting memory gets activation 2. Propagation: Activation spreads to connected memories 3. Decay: Activation weakens with distance 4. Threshold: Minimum activation for retrieval 5. Convergence: Multiple paths strengthen activation 6. Inhibition: Negative associations reduce activation

    Use Cases: – Creative problem solving (analogies, brainstorming) – Context-aware information retrieval – Knowledge discovery and inference – Recommendation systems – Learning from experience – Concept mapping and knowledge graphs – Semantic search and exploration

    Advantages: – Flexible retrieval through associations – Discovers indirect relationships – Mimics human memory organization – Supports creative connections – Handles ambiguous queries – Learns from access patterns – Enables serendipitous discovery

    Challenges: – Computational complexity of spreading activation – Managing association strength decay – Preventing over-activation (too many results) – Determining appropriate association types – Balancing network density – Avoiding spurious associations

    LangChain Implementation: This implementation uses LangChain for: – LLM-based semantic similarity calculation – Association type inference – Context-aware link creation – Network-based reasoning

    Production Considerations: – Implement efficient graph traversal algorithms – Set appropriate activation thresholds – Monitor network size and complexity – Prune weak associations periodically – Cache frequently accessed paths – Consider distributed graph storage – Implement incremental updates – Balance precision vs. recall in retrieval


    File Statistics

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

    Module Documentation

    """
    Pattern 103: Associative Memory Networks
    
    Description:
        The Associative Memory Networks pattern implements memory systems where memories
        are interconnected through associations, enabling retrieval through spreading
        activation and contextual relationships. This pattern mimics human associative
        memory where one memory can trigger recall of related memories through various
        types of connections (semantic, temporal, causal, episodic).
    
        Associative networks allow agents to discover indirect relationships, make
        creative connections, and retrieve relevant information through chains of
        associations rather than direct queries. Each memory node can be linked to
        others through typed relationships, and activation spreads through the network
        when memories are accessed, bringing related memories to attention.
    
        This pattern supports various association types including semantic similarity,
        temporal co-occurrence, causal relationships, part-whole hierarchies, and
        user-defined custom links. It implements spreading activation algorithms,
        association strength tracking, and network-based retrieval strategies.
    
    Key Components:
        1. Memory Nodes: Individual memory items in the network
        2. Association Links: Typed connections between memories
        3. Spreading Activation: Propagation of activation through network
        4. Association Strength: Weight of connections
        5. Activation Decay: Gradual decrease of activation
        6. Network Traversal: Algorithms for exploring associations
        7. Link Types: Different relationship types
    
    Association Types:
        1. Semantic: Similar meaning or concept
        2. Temporal: Occurred at similar times
        3. Causal: Cause-effect relationships
        4. Spatial: Co-location in space
        5. Episodic: Part of same experience
        6. Hierarchical: Part-whole, is-a relationships
        7. Functional: Used together or for similar purposes
    
    Spreading Activation:
        1. Initial Activation: Starting memory gets activation
        2. Propagation: Activation spreads to connected memories
        3. Decay: Activation weakens with distance
        4. Threshold: Minimum activation for retrieval
        5. Convergence: Multiple paths strengthen activation
        6. Inhibition: Negative associations reduce activation
    
    Use Cases:
        - Creative problem solving (analogies, brainstorming)
        - Context-aware information retrieval
        - Knowledge discovery and inference
        - Recommendation systems
        - Learning from experience
        - Concept mapping and knowledge graphs
        - Semantic search and exploration
    
    Advantages:
        - Flexible retrieval through associations
        - Discovers indirect relationships
        - Mimics human memory organization
        - Supports creative connections
        - Handles ambiguous queries
        - Learns from access patterns
        - Enables serendipitous discovery
    
    Challenges:
        - Computational complexity of spreading activation
        - Managing association strength decay
        - Preventing over-activation (too many results)
        - Determining appropriate association types
        - Balancing network density
        - Avoiding spurious associations
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based semantic similarity calculation
        - Association type inference
        - Context-aware link creation
        - Network-based reasoning
    
    Production Considerations:
        - Implement efficient graph traversal algorithms
        - Set appropriate activation thresholds
        - Monitor network size and complexity
        - Prune weak associations periodically
        - Cache frequently accessed paths
        - Consider distributed graph storage
        - Implement incremental updates
        - Balance precision vs. recall in retrieval
    """
    

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


    Import Statements

    Standard Library Imports

    import collections.defaultdict
    import collections.deque
    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 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 – 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.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Set: Type hints for better code documentation (List, Dict, Any, Optional) – 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: AssociationType

    Lines 107-116

    Inherits from: Enum

    Purpose:

    Types of associations between memories.
    

    Class: MemoryNode

    Lines 120-139

    Purpose:

    Node in associative memory network.
    
    Attributes:
        node_id: Unique identifier
        content: Memory content
        created_at: Creation timestamp
        activation: Current activation level
        base_activation: Base activation strength
        tags: Associated tags
        metadata: Additional information
    

    Class: Association

    Lines 143-162

    Purpose:

    Link between memory nodes.
    
    Attributes:
        from_node: Source node ID
        to_node: Target node ID
        association_type: Type of association
        strength: Association strength (0-1)
        created_at: When association was created
        access_count: Number of times traversed
        metadata: Additional information
    

    Class: AssociativeMemoryNetwork

    Lines 165-466

    Purpose:

    Associative memory network with spreading activation.
    
    This class implements a network of interconnected memories with
    various association types and spreading activation for retrieval.
    

    Methods:

    __init__(self, activation_decay, activation_threshold, max_spread_depth, temperature)

    Lines 173-198

    Purpose:

    Initialize associative memory network.
    
    Args:
        activation_decay: Decay factor for spreading (0-1)
        activation_threshold: Minimum activation for retrieval
        max_spread_depth: Maximum depth for spreading activation
        temperature: LLM temperature
    

    Parameters: – activation_decay: Activation decay – activation_threshold: Activation threshold – max_spread_depth: Max spread depth – temperature: Sampling temperature (0-1)

    add_memory(self, content, tags, base_activation)

    Lines 200-229

    Purpose:

    Add memory node to network.
    
    Args:
        content: Memory content
        tags: Associated tags
        base_activation: Base activation level
    
    Returns:
        Created memory node
    

    Parameters: – content: Content – tags: Tags – base_activation: Base activation

    Returns: MemoryNode

    associate(self, from_node_id, to_node_id, association_type, strength, bidirectional)

    Lines 231-273

    Purpose:

    Create association between nodes.
    
    Args:
        from_node_id: Source node
        to_node_id: Target node
        association_type: Type of association
        strength: Association strength
        bidirectional: Create reverse link too
    

    Parameters: – from_node_id: From node id – to_node_id: To node id – association_type: Association type – strength: Strength – bidirectional: Bidirectional

    auto_associate(self, node_id, min_strength)

    Lines 275-309

    Purpose:

    Automatically create associations based on similarity.
    
    Args:
        node_id: Node to associate
        min_strength: Minimum strength for association
    

    Parameters: – node_id: Node id – min_strength: Min strength

    spread_activation(self, start_node_id, initial_activation)

    Lines 311-372

    Purpose:

    Spread activation from starting node through network.
    
    Args:
        start_node_id: Starting node
        initial_activation: Initial activation level
    
    Returns:
        Dictionary of node_id -> activation level
    

    Parameters: – start_node_id: Start node id – initial_activation: Initial activation

    Returns: <ast.Subscript object at 0x000001A06D659590>

    retrieve_associated(self, node_id, min_activation, max_results)

    Lines 374-405

    Purpose:

    Retrieve memories associated with given node.
    
    Args:
        node_id: Starting node
        min_activation: Minimum activation threshold
        max_results: Maximum number of results
    
    Returns:
        List of (node, activation) tuples
    

    Parameters: – node_id: Node id – min_activation: Min activation – max_results: Max results

    Returns: <ast.Subscript object at 0x000001A06D685950>

    find_path(self, from_node_id, to_node_id, max_depth)

    Lines 407-450

    Purpose:

    Find path between two nodes.
    
    Args:
        from_node_id: Start node
        to_node_id: End node
        max_depth: Maximum path length
    
    Returns:
        List of node IDs in path, or None
    

    Parameters: – from_node_id: From node id – to_node_id: To node id – max_depth: Max depth

    Returns: <ast.Subscript object at 0x000001A06D7FC050>

    get_statistics(self)

    Lines 452-466

    Purpose:

    Get network statistics.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D7F7B50>


    Class: AssociativeMemoryAgent

    Lines 469-568

    Purpose:

    Agent with associative memory network.
    
    This agent uses associative memory for context-aware retrieval
    and creative problem-solving through memory associations.
    

    Methods:

    __init__(self, activation_decay, temperature)

    Lines 477-492

    Purpose:

    Initialize associative memory agent.
    
    Args:
        activation_decay: Activation decay factor
        temperature: LLM temperature
    

    Parameters: – activation_decay: Activation decay – temperature: Sampling temperature (0-1)

    learn(self, content, tags, related_to)

    Lines 494-527

    Purpose:

    Learn new information with associations.
    
    Args:
        content: Content to learn
        tags: Associated tags
        related_to: IDs of related memories
    
    Returns:
        Created memory node
    

    Parameters: – content: Content – tags: Tags – related_to: Related to

    Returns: MemoryNode

    recall(self, query, max_results)

    Lines 529-568

    Purpose:

    Recall memories through associative retrieval.
    
    Args:
        query: Query string
        max_results: Maximum results
    
    Returns:
        List of (node, activation, path) tuples
    

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

    Returns: <ast.Subscript object at 0x000001A06D72A2D0>


    Functions

    demonstrate_associative_memory()

    Lines 571-898

    Purpose:

    Demonstrate associative memory networks pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Associative Networks 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 102
    • Pattern 104

    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 Associative Networks 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: 103_associative_networks.py
    Total Lines: 903
    Pattern Category: AI Agent Design Pattern #103


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Hierarchical Memory

    
        """
    Pattern 102: Hierarchical Memory
    
    Description:
        The Hierarchical Memory pattern organizes agent memory into multiple levels with
        different characteristics, mirroring human memory systems that include working
        memory (immediate, limited capacity), short-term memory (temporary, accessible),
        and long-term memory (persistent, large capacity). This pattern enables efficient
        memory management by storing information at appropriate levels based on recency,
        importance, and access frequency.
        
        Each level in the hierarchy has distinct properties: working memory holds current
        context with fast access but very limited capacity; short-term memory maintains
        recent information with moderate capacity and access speed; long-term memory stores
        consolidated information with large capacity but slower access. Information flows
        between levels through promotion (consolidation) and retrieval mechanisms.
        
        This pattern supports both automatic and strategic memory movement between levels,
        implements forgetting at each level with different strategies, and enables
        efficient multi-level search for relevant information.
    
    Key Components:
        1. Working Memory: Current active context (very limited, fast)
        2. Short-Term Memory: Recent information (limited, moderate speed)
        3. Long-Term Memory: Consolidated knowledge (unlimited, slower)
        4. Promotion Mechanism: Move important info to higher levels
        5. Retrieval System: Search across all levels
        6. Consolidation: Strengthen and reorganize memories
        7. Level-specific Forgetting: Different strategies per level
    
    Memory Levels:
        1. Working Memory (L0):
           - Capacity: 5-9 items (Miller's Law)
           - Duration: Active task only
           - Access: Instant
           - Purpose: Current context and immediate operations
        
        2. Short-Term Memory (L1):
           - Capacity: 50-100 items
           - Duration: Recent session/day
           - Access: Very fast
           - Purpose: Recent interactions and temporary information
        
        3. Long-Term Memory (L2):
           - Capacity: Unlimited (practical limits apply)
           - Duration: Persistent across sessions
           - Access: Slower (requires search)
           - Purpose: Important knowledge and experiences
    
    Memory Operations:
        1. Store: Add new memory at appropriate level
        2. Retrieve: Search across levels
        3. Promote: Move important memories up
        4. Consolidate: Reorganize and strengthen
        5. Forget: Remove based on level-specific rules
        6. Rehearse: Strengthen through repetition
    
    Use Cases:
        - Conversational agents with context management
        - Learning systems with knowledge retention
        - Personal assistants with memory across sessions
        - Task-oriented agents with working context
        - Long-running autonomous agents
        - Multi-session applications
        - Knowledge management systems
    
    Advantages:
        - Efficient memory organization
        - Appropriate retention strategies per level
        - Fast access to recent/important information
        - Scalable to large memory requirements
        - Natural forgetting curves
        - Supports both short and long-term needs
        - Mimics human memory structure
    
    Challenges:
        - Determining appropriate level for memories
        - Managing transitions between levels
        - Balancing access speed vs. capacity
        - Consolidation strategy selection
        - Preventing important memory loss
        - Synchronization across levels
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based memory consolidation and summarization
        - Importance assessment for promotion decisions
        - Context-aware retrieval across levels
        - Memory linking and association
        
    Production Considerations:
        - Tune capacity limits for each level
        - Implement persistent storage for long-term memory
        - Monitor access patterns and adjust
        - Regular consolidation during low-activity periods
        - Backup critical memories before forgetting
        - Implement recovery mechanisms
        - Consider storage costs for long-term memory
        - Balance access speed with storage efficiency
    """
    
    import os
    from typing import List, Dict, Any, Optional, Set, Tuple
    from dataclasses import dataclass, field
    from datetime import datetime, timedelta
    from enum import Enum
    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
    
    load_dotenv()
    
    
    class MemoryLevel(Enum):
        """Hierarchical memory levels."""
        WORKING = "working"  # L0: Current context (5-9 items)
        SHORT_TERM = "short_term"  # L1: Recent memory (50-100 items)
        LONG_TERM = "long_term"  # L2: Persistent memory (unlimited)
    
    
    @dataclass
    class HierarchicalMemoryItem:
        """
        Memory item with hierarchical tracking.
        
        Attributes:
            memory_id: Unique identifier
            content: Memory content
            level: Current memory level
            created_at: Creation timestamp
            last_accessed: Last access timestamp
            access_count: Number of accesses
            importance: Importance score (0-1)
            strength: Memory strength (0-1)
            tags: Associated tags
            links: Links to related memories
            metadata: Additional information
        """
        memory_id: str
        content: str
        level: MemoryLevel
        created_at: datetime
        last_accessed: datetime
        access_count: int = 0
        importance: float = 0.5
        strength: float = 0.5
        tags: List[str] = field(default_factory=list)
        links: Set[str] = field(default_factory=set)
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    class HierarchicalMemorySystem:
        """
        Multi-level hierarchical memory system.
        
        This system manages memory across working, short-term, and long-term levels
        with automatic promotion, consolidation, and forgetting mechanisms.
        """
        
        def __init__(
            self,
            working_capacity: int = 7,
            short_term_capacity: int = 50,
            promotion_threshold: float = 0.7,
            temperature: float = 0.3
        ):
            """
            Initialize hierarchical memory system.
            
            Args:
                working_capacity: Max items in working memory
                short_term_capacity: Max items in short-term memory
                promotion_threshold: Threshold for promoting memories
                temperature: LLM temperature
            """
            self.working_capacity = working_capacity
            self.short_term_capacity = short_term_capacity
            self.promotion_threshold = promotion_threshold
            
            # Memory stores for each level
            self.working_memory: deque = deque(maxlen=working_capacity)
            self.short_term_memory: Dict[str, HierarchicalMemoryItem] = {}
            self.long_term_memory: Dict[str, HierarchicalMemoryItem] = {}
            
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.memory_counter = 0
        
        def store(
            self,
            content: str,
            level: MemoryLevel = MemoryLevel.WORKING,
            importance: float = 0.5,
            tags: List[str] = None
        ) -> HierarchicalMemoryItem:
            """
            Store memory at specified level.
            
            Args:
                content: Memory content
                level: Initial memory level
                importance: Importance score
                tags: Associated tags
                
            Returns:
                Created memory item
            """
            self.memory_counter += 1
            memory_id = f"mem_{self.memory_counter}"
            
            now = datetime.now()
            memory = HierarchicalMemoryItem(
                memory_id=memory_id,
                content=content,
                level=level,
                created_at=now,
                last_accessed=now,
                importance=importance,
                strength=importance,
                tags=tags or []
            )
            
            if level == MemoryLevel.WORKING:
                # Add to working memory (FIFO queue)
                if len(self.working_memory) >= self.working_capacity:
                    # Move oldest to short-term before removing
                    oldest = self.working_memory[0]
                    self._promote_to_short_term(oldest)
                self.working_memory.append(memory)
            
            elif level == MemoryLevel.SHORT_TERM:
                self.short_term_memory[memory_id] = memory
                # Check capacity
                if len(self.short_term_memory) > self.short_term_capacity:
                    self._consolidate_short_term()
            
            else:  # LONG_TERM
                self.long_term_memory[memory_id] = memory
            
            return memory
        
        def retrieve(
            self,
            query: str = None,
            tags: List[str] = None,
            level: Optional[MemoryLevel] = None,
            n: int = 5
        ) -> List[HierarchicalMemoryItem]:
            """
            Retrieve memories across levels.
            
            Args:
                query: Search query
                tags: Filter by tags
                level: Specific level to search (None for all)
                n: Number of results
                
            Returns:
                List of matching memories
            """
            results = []
            
            # Search working memory (always checked first)
            if level is None or level == MemoryLevel.WORKING:
                for mem in self.working_memory:
                    if self._matches(mem, query, tags):
                        mem.last_accessed = datetime.now()
                        mem.access_count += 1
                        results.append(mem)
            
            # Search short-term memory
            if level is None or level == MemoryLevel.SHORT_TERM:
                for mem in self.short_term_memory.values():
                    if self._matches(mem, query, tags):
                        mem.last_accessed = datetime.now()
                        mem.access_count += 1
                        results.append(mem)
            
            # Search long-term memory
            if level is None or level == MemoryLevel.LONG_TERM:
                for mem in self.long_term_memory.values():
                    if self._matches(mem, query, tags):
                        mem.last_accessed = datetime.now()
                        mem.access_count += 1
                        results.append(mem)
            
            # Sort by relevance (strength * recency)
            now = datetime.now()
            def score(mem):
                recency = 1.0 / (1 + (now - mem.last_accessed).total_seconds() / 3600)
                return mem.strength * 0.7 + recency * 0.3
            
            results.sort(key=score, reverse=True)
            return results[:n]
        
        def _matches(
            self,
            memory: HierarchicalMemoryItem,
            query: Optional[str],
            tags: Optional[List[str]]
        ) -> bool:
            """Check if memory matches search criteria."""
            if tags:
                if not any(tag in memory.tags for tag in tags):
                    return False
            
            if query:
                if query.lower() not in memory.content.lower():
                    return False
            
            return True
        
        def _promote_to_short_term(self, memory: HierarchicalMemoryItem):
            """Promote working memory item to short-term."""
            memory.level = MemoryLevel.SHORT_TERM
            self.short_term_memory[memory.memory_id] = memory
            
            # Check if should promote further to long-term
            if memory.strength >= self.promotion_threshold:
                self._promote_to_long_term(memory)
        
        def _promote_to_long_term(self, memory: HierarchicalMemoryItem):
            """Promote short-term memory to long-term."""
            memory.level = MemoryLevel.LONG_TERM
            
            # Remove from short-term
            if memory.memory_id in self.short_term_memory:
                del self.short_term_memory[memory.memory_id]
            
            # Add to long-term
            self.long_term_memory[memory.memory_id] = memory
        
        def _consolidate_short_term(self):
            """Consolidate short-term memories when capacity exceeded."""
            # Sort by strength and age
            memories = sorted(
                self.short_term_memory.values(),
                key=lambda m: (m.strength, m.access_count),
                reverse=True
            )
            
            # Promote top memories to long-term
            to_promote = memories[:int(self.short_term_capacity * 0.3)]
            for mem in to_promote:
                if mem.strength >= self.promotion_threshold * 0.8:
                    self._promote_to_long_term(mem)
            
            # Remove weakest memories
            to_remove = memories[self.short_term_capacity:]
            for mem in to_remove:
                del self.short_term_memory[mem.memory_id]
        
        def strengthen(self, memory_id: str, amount: float = 0.1):
            """
            Strengthen a memory.
            
            Args:
                memory_id: Memory to strengthen
                amount: Amount to increase strength
            """
            # Check all levels
            memory = None
            
            for mem in self.working_memory:
                if mem.memory_id == memory_id:
                    memory = mem
                    break
            
            if not memory and memory_id in self.short_term_memory:
                memory = self.short_term_memory[memory_id]
            
            if not memory and memory_id in self.long_term_memory:
                memory = self.long_term_memory[memory_id]
            
            if memory:
                memory.strength = min(1.0, memory.strength + amount)
                memory.importance = min(1.0, memory.importance + amount * 0.5)
                
                # Check for promotion
                if memory.level == MemoryLevel.SHORT_TERM and \
                   memory.strength >= self.promotion_threshold:
                    self._promote_to_long_term(memory)
        
        def consolidate_all(self):
            """
            Perform full memory consolidation.
            
            This simulates sleep/rest consolidation process.
            """
            # Move working memory to short-term
            while len(self.working_memory) > 0:
                mem = self.working_memory.popleft()
                self._promote_to_short_term(mem)
            
            # Consolidate short-term
            self._consolidate_short_term()
        
        def get_statistics(self) -> Dict[str, Any]:
            """Get memory statistics across all levels."""
            return {
                "working_memory": {
                    "count": len(self.working_memory),
                    "capacity": self.working_capacity,
                    "usage": len(self.working_memory) / self.working_capacity
                },
                "short_term_memory": {
                    "count": len(self.short_term_memory),
                    "capacity": self.short_term_capacity,
                    "usage": len(self.short_term_memory) / self.short_term_capacity
                },
                "long_term_memory": {
                    "count": len(self.long_term_memory),
                    "avg_strength": sum(m.strength for m in self.long_term_memory.values()) / len(self.long_term_memory) if self.long_term_memory else 0
                },
                "total_memories": len(self.working_memory) + len(self.short_term_memory) + len(self.long_term_memory)
            }
        
        def get_memory_at_level(self, level: MemoryLevel) -> List[HierarchicalMemoryItem]:
            """Get all memories at specific level."""
            if level == MemoryLevel.WORKING:
                return list(self.working_memory)
            elif level == MemoryLevel.SHORT_TERM:
                return list(self.short_term_memory.values())
            else:
                return list(self.long_term_memory.values())
    
    
    class HierarchicalMemoryAgent:
        """
        Agent with hierarchical memory system.
        
        This agent uses multi-level memory to maintain context while
        supporting long-term knowledge retention.
        """
        
        def __init__(
            self,
            working_capacity: int = 7,
            short_term_capacity: int = 50,
            temperature: float = 0.5
        ):
            """
            Initialize hierarchical memory agent.
            
            Args:
                working_capacity: Working memory capacity
                short_term_capacity: Short-term memory capacity
                temperature: LLM temperature
            """
            self.memory_system = HierarchicalMemorySystem(
                working_capacity=working_capacity,
                short_term_capacity=short_term_capacity
            )
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
        
        def process(
            self,
            user_input: str,
            context_tags: List[str] = None
        ) -> str:
            """
            Process input with hierarchical memory.
            
            Args:
                user_input: User input
                context_tags: Context tags for retrieval
                
            Returns:
                Response
            """
            # Retrieve relevant memories from all levels
            relevant_memories = self.memory_system.retrieve(
                query=user_input,
                tags=context_tags,
                n=5
            )
            
            # Build context from memories
            memory_context = ""
            if relevant_memories:
                memory_context = "Relevant memories:\n"
                for mem in relevant_memories[:3]:
                    memory_context += f"- [{mem.level.value}] {mem.content}\n"
            
            # Generate response
            prompt = ChatPromptTemplate.from_template(
                "You are an assistant with hierarchical memory.\n\n"
                "{memory_context}\n"
                "User: {user_input}\n\n"
                "Respond naturally, using relevant memories."
            )
            
            chain = prompt | self.llm | StrOutputParser()
            response = chain.invoke({
                "memory_context": memory_context if memory_context else "No relevant memories.",
                "user_input": user_input
            })
            
            # Store interaction in working memory
            self.memory_system.store(
                content=f"Q: {user_input[:50]} | A: {response[:50]}",
                level=MemoryLevel.WORKING,
                importance=0.5,
                tags=context_tags or []
            )
            
            # Strengthen accessed memories
            for mem in relevant_memories:
                self.memory_system.strengthen(mem.memory_id, 0.05)
            
            return response
        
        def memorize(
            self,
            content: str,
            importance: float = 0.7,
            level: MemoryLevel = MemoryLevel.SHORT_TERM,
            tags: List[str] = None
        ):
            """
            Explicitly memorize information.
            
            Args:
                content: Content to memorize
                importance: Importance score
                level: Initial memory level
                tags: Associated tags
            """
            self.memory_system.store(
                content=content,
                level=level,
                importance=importance,
                tags=tags or []
            )
        
        def consolidate(self):
            """Perform memory consolidation."""
            self.memory_system.consolidate_all()
    
    
    def demonstrate_hierarchical_memory():
        """Demonstrate hierarchical memory pattern."""
        
        print("=" * 80)
        print("HIERARCHICAL MEMORY PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic three-level memory
        print("\n" + "=" * 80)
        print("Example 1: Three-Level Memory Structure")
        print("=" * 80)
        
        memory_system = HierarchicalMemorySystem(
            working_capacity=5,
            short_term_capacity=10,
            promotion_threshold=0.7
        )
        
        print(f"\nMemory Configuration:")
        print(f"  Working Memory Capacity: {memory_system.working_capacity}")
        print(f"  Short-Term Memory Capacity: {memory_system.short_term_capacity}")
        print(f"  Promotion Threshold: {memory_system.promotion_threshold}")
        
        # Store at different levels
        print("\nStoring memories at different levels:")
        
        # Working memory
        memory_system.store("Current task: Analyze data", MemoryLevel.WORKING, 0.6, ["task"])
        memory_system.store("User asked about weather", MemoryLevel.WORKING, 0.4, ["context"])
        print("  ✓ Added to working memory: 2 items")
        
        # Short-term memory
        memory_system.store("User's name is John", MemoryLevel.SHORT_TERM, 0.8, ["personal"])
        memory_system.store("Meeting scheduled at 3pm", MemoryLevel.SHORT_TERM, 0.7, ["schedule"])
        print("  ✓ Added to short-term memory: 2 items")
        
        # Long-term memory
        memory_system.store("User allergic to peanuts", MemoryLevel.LONG_TERM, 0.9, ["health", "important"])
        memory_system.store("Company policy: No PII in logs", MemoryLevel.LONG_TERM, 0.9, ["policy"])
        print("  ✓ Added to long-term memory: 2 items")
        
        stats = memory_system.get_statistics()
        print(f"\nMemory Distribution:")
        print(f"  Working: {stats['working_memory']['count']}/{stats['working_memory']['capacity']}")
        print(f"  Short-term: {stats['short_term_memory']['count']}/{stats['short_term_memory']['capacity']}")
        print(f"  Long-term: {stats['long_term_memory']['count']}")
        
        # Example 2: Automatic promotion from working to short-term
        print("\n" + "=" * 80)
        print("Example 2: Automatic Memory Promotion")
        print("=" * 80)
        
        print("\nFilling working memory beyond capacity...")
        for i in range(8):
            memory_system.store(
                f"Working memory item {i}",
                MemoryLevel.WORKING,
                0.5,
                ["test"]
            )
        
        stats = memory_system.get_statistics()
        print(f"\nAfter adding 8 items to working memory:")
        print(f"  Working: {stats['working_memory']['count']}/{stats['working_memory']['capacity']}")
        print(f"  Short-term: {stats['short_term_memory']['count']}/{stats['short_term_memory']['capacity']}")
        print(f"  → Oldest items automatically promoted to short-term")
        
        # Example 3: Cross-level retrieval
        print("\n" + "=" * 80)
        print("Example 3: Cross-Level Memory Retrieval")
        print("=" * 80)
        
        # Add more memories for testing retrieval
        memory_system.store("Paris is the capital of France", MemoryLevel.LONG_TERM, 0.8, ["geography", "facts"])
        memory_system.store("Python is a programming language", MemoryLevel.SHORT_TERM, 0.7, ["programming", "facts"])
        memory_system.store("Today's weather is sunny", MemoryLevel.WORKING, 0.3, ["weather", "today"])
        
        queries = [
            ("facts", "Search by tag: 'facts'"),
            ("Python", "Search by query: 'Python'"),
            (None, "Retrieve top 5 memories (no filter)")
        ]
        
        for query, description in queries:
            print(f"\n{description}")
            if query == "facts":
                results = memory_system.retrieve(tags=["facts"], n=5)
            elif query:
                results = memory_system.retrieve(query=query, n=5)
            else:
                results = memory_system.retrieve(n=5)
            
            for i, mem in enumerate(results, 1):
                print(f"  {i}. [{mem.level.value}] {mem.content[:50]}")
        
        # Example 4: Memory strengthening and promotion
        print("\n" + "=" * 80)
        print("Example 4: Memory Strengthening & Promotion")
        print("=" * 80)
        
        # Add memory to short-term
        test_mem = memory_system.store(
            "Important repeated information",
            MemoryLevel.SHORT_TERM,
            0.6,
            ["important"]
        )
        
        print(f"\nInitial state:")
        print(f"  Level: {test_mem.level.value}")
        print(f"  Strength: {test_mem.strength:.2f}")
        print(f"  Importance: {test_mem.importance:.2f}")
        
        print(f"\nStrengthening memory 3 times...")
        for i in range(3):
            memory_system.strengthen(test_mem.memory_id, 0.1)
            # Re-fetch to get updated state
            results = memory_system.retrieve(query="Important repeated", n=1)
            if results:
                mem = results[0]
                print(f"  After strengthening {i+1}: strength={mem.strength:.2f}, level={mem.level.value}")
        
        # Example 5: Memory consolidation
        print("\n" + "=" * 80)
        print("Example 5: Memory Consolidation Process")
        print("=" * 80)
        
        consolidation_system = HierarchicalMemorySystem(
            working_capacity=5,
            short_term_capacity=15,
            promotion_threshold=0.7
        )
        
        # Simulate a day of interactions
        print("\nSimulating day of interactions...")
        interactions = [
            ("Morning: Check email", 0.4, ["morning", "routine"]),
            ("Meeting notes: Q1 planning", 0.8, ["work", "important"]),
            ("Lunch order: Pizza", 0.2, ["food"]),
            ("Code review completed", 0.7, ["work"]),
            ("User reported bug #123", 0.8, ["work", "bug"]),
            ("Evening: Workout schedule", 0.3, ["personal"]),
        ]
        
        for content, importance, tags in interactions:
            consolidation_system.store(content, MemoryLevel.WORKING, importance, tags)
        
        stats_before = consolidation_system.get_statistics()
        print(f"\nBefore consolidation:")
        print(f"  Working: {stats_before['working_memory']['count']}")
        print(f"  Short-term: {stats_before['short_term_memory']['count']}")
        print(f"  Long-term: {stats_before['long_term_memory']['count']}")
        
        print(f"\nPerforming consolidation (simulating sleep)...")
        consolidation_system.consolidate_all()
        
        stats_after = consolidation_system.get_statistics()
        print(f"\nAfter consolidation:")
        print(f"  Working: {stats_after['working_memory']['count']}")
        print(f"  Short-term: {stats_after['short_term_memory']['count']}")
        print(f"  Long-term: {stats_after['long_term_memory']['count']}")
        print(f"  → Important memories promoted to long-term")
        
        # Example 6: Hierarchical memory agent
        print("\n" + "=" * 80)
        print("Example 6: Agent with Hierarchical Memory")
        print("=" * 80)
        
        agent = HierarchicalMemoryAgent(
            working_capacity=5,
            short_term_capacity=20
        )
        
        # Teach agent some facts
        print("\nTeaching agent long-term facts...")
        agent.memorize(
            "User's favorite color is blue",
            importance=0.8,
            level=MemoryLevel.LONG_TERM,
            tags=["personal", "preferences"]
        )
        agent.memorize(
            "Company founded in 2020",
            importance=0.7,
            level=MemoryLevel.LONG_TERM,
            tags=["company", "facts"]
        )
        
        # Have conversation
        print("\nConversation with agent:")
        conversations = [
            ("What's my favorite color?", ["personal"]),
            ("Tell me about the company", ["company"]),
            ("What did we just talk about?", ["context"]),
        ]
        
        for user_input, tags in conversations:
            print(f"\nUser: {user_input}")
            response = agent.process(user_input, tags)
            print(f"Agent: {response[:200]}...")
        
        # Check memory distribution
        stats = agent.memory_system.get_statistics()
        print(f"\nAgent Memory State:")
        print(f"  Working: {stats['working_memory']['count']}")
        print(f"  Short-term: {stats['short_term_memory']['count']}")
        print(f"  Long-term: {stats['long_term_memory']['count']}")
        
        # Example 7: Memory level visualization
        print("\n" + "=" * 80)
        print("Example 7: Memory Level Visualization")
        print("=" * 80)
        
        viz_system = HierarchicalMemorySystem(working_capacity=5, short_term_capacity=10)
        
        # Populate all levels
        viz_system.store("Current context item 1", MemoryLevel.WORKING, 0.5, ["context"])
        viz_system.store("Current context item 2", MemoryLevel.WORKING, 0.5, ["context"])
        
        for i in range(5):
            viz_system.store(f"Recent memory {i}", MemoryLevel.SHORT_TERM, 0.6, ["recent"])
        
        for i in range(8):
            viz_system.store(f"Long-term knowledge {i}", MemoryLevel.LONG_TERM, 0.8, ["knowledge"])
        
        print("\nMemory Hierarchy Visualization:")
        print("\n┌─────────────────────────────────────────┐")
        print("│  WORKING MEMORY (Fast, Limited)        │")
        print("│  Capacity: 5 items                      │")
        print("└─────────────────────────────────────────┘")
        working_mems = viz_system.get_memory_at_level(MemoryLevel.WORKING)
        for mem in working_mems:
            print(f"    • {mem.content[:35]}")
        
        print("\n┌─────────────────────────────────────────┐")
        print("│  SHORT-TERM MEMORY (Recent)             │")
        print("│  Capacity: 10 items                     │")
        print("└─────────────────────────────────────────┘")
        short_mems = viz_system.get_memory_at_level(MemoryLevel.SHORT_TERM)
        for mem in short_mems[:3]:
            print(f"    • {mem.content[:35]}")
        print(f"    ... and {len(short_mems)-3} more")
        
        print("\n┌─────────────────────────────────────────┐")
        print("│  LONG-TERM MEMORY (Persistent)          │")
        print("│  Capacity: Unlimited                    │")
        print("└─────────────────────────────────────────┘")
        long_mems = viz_system.get_memory_at_level(MemoryLevel.LONG_TERM)
        for mem in long_mems[:3]:
            print(f"    • {mem.content[:35]}")
        print(f"    ... and {len(long_mems)-3} more")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY: Hierarchical Memory Pattern")
        print("=" * 80)
        
        summary = """
        The Hierarchical Memory pattern demonstrated:
        
        1. THREE-LEVEL STRUCTURE (Example 1):
           - Working Memory: Current context (5 items, instant access)
           - Short-Term Memory: Recent info (50 items, fast access)
           - Long-Term Memory: Persistent knowledge (unlimited, slower)
           - Different capacity and access characteristics per level
        
        2. AUTOMATIC PROMOTION (Example 2):
           - Working memory overflow triggers promotion
           - Oldest items move to short-term automatically
           - Maintains working memory within capacity
           - Seamless memory level transitions
        
        3. CROSS-LEVEL RETRIEVAL (Example 3):
           - Single search across all memory levels
           - Priority to recent/important memories
           - Tag-based and query-based filtering
           - Efficient multi-level search
        
        4. STRENGTHENING & PROMOTION (Example 4):
           - Repeated access strengthens memories
           - Strong memories promoted to higher levels
           - Short-term → Long-term promotion at threshold
           - Automatic importance adjustment
        
        5. CONSOLIDATION (Example 5):
           - Simulates sleep/rest consolidation
           - Working → Short-term → Long-term flow
           - Important memories promoted upward
           - Weak memories forgotten
        
        6. AGENT INTEGRATION (Example 6):
           - Hierarchical memory in agent workflow
           - Long-term facts retained across interactions
           - Working memory tracks conversation context
           - Context-aware responses using all levels
        
        7. VISUALIZATION (Example 7):
           - Clear separation of memory levels
           - Capacity tracking per level
           - Memory distribution visible
           - Hierarchical organization display
        
        KEY BENEFITS:
        ✓ Efficient memory organization
        ✓ Fast access to relevant information
        ✓ Scalable memory management
        ✓ Natural forgetting curves
        ✓ Supports both short and long-term needs
        ✓ Mimics human memory architecture
        ✓ Automatic capacity management
        
        USE CASES:
        • Conversational agents with context
        • Learning systems with retention
        • Personal assistants across sessions
        • Task-oriented agents with working context
        • Long-running autonomous agents
        • Multi-session applications
        • Knowledge management systems
        
        BEST PRACTICES:
        1. Set appropriate capacity for each level
        2. Tune promotion thresholds for use case
        3. Regular consolidation during low activity
        4. Strengthen frequently accessed memories
        5. Implement persistent storage for long-term
        6. Monitor memory distribution
        7. Use tags for efficient retrieval
        8. Balance access speed vs. capacity
        
        TRADE-OFFS:
        • Access speed vs. memory capacity
        • Simplicity vs. sophistication
        • Automatic vs. manual promotion
        • Storage cost vs. retention
        
        PRODUCTION CONSIDERATIONS:
        → Implement persistent storage for long-term memory
        → Tune capacities based on system resources
        → Monitor access patterns and adjust
        → Regular consolidation during off-peak
        → Backup critical memories before operations
        → Consider database for long-term storage
        → Implement recovery mechanisms
        → Balance storage costs with retention needs
        → Track memory usage metrics
        → Test promotion thresholds thoroughly
        
        This pattern provides efficient, scalable memory management by
        organizing information into levels with appropriate characteristics,
        enabling agents to maintain context while supporting long-term retention.
        """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_hierarchical_memory()
    
        

    Agentic Design Pattern: Hierarchical Memory

    Pattern 102: Hierarchical Memory – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 102: Hierarchical Memory

    Description: The Hierarchical Memory pattern organizes agent memory into multiple levels with different characteristics, mirroring human memory systems that include working memory (immediate, limited capacity), short-term memory (temporary, accessible), and long-term memory (persistent, large capacity). This pattern enables efficient memory management by storing information at appropriate levels based on recency, importance, and access frequency.

    Each level in the hierarchy has distinct properties: working memory holds current
    context with fast access but very limited capacity; short-term memory maintains
    recent information with moderate capacity and access speed; long-term memory stores
    consolidated information with large capacity but slower access. Information flows
    between levels through promotion (consolidation) and retrieval mechanisms.
    
    This pattern supports both automatic and strategic memory movement between levels,
    implements forgetting at each level with different strategies, and enables
    efficient multi-level search for relevant information.
    

    Key Components: 1. Working Memory: Current active context (very limited, fast) 2. Short-Term Memory: Recent information (limited, moderate speed) 3. Long-Term Memory: Consolidated knowledge (unlimited, slower) 4. Promotion Mechanism: Move important info to higher levels 5. Retrieval System: Search across all levels 6. Consolidation: Strengthen and reorganize memories 7. Level-specific Forgetting: Different strategies per level

    Memory Levels: 1. Working Memory (L0): – Capacity: 5-9 items (Miller’s Law) – Duration: Active task only – Access: Instant – Purpose: Current context and immediate operations

    2. Short-Term Memory (L1):
       - Capacity: 50-100 items
       - Duration: Recent session/day
       - Access: Very fast
       - Purpose: Recent interactions and temporary information
    
    3. Long-Term Memory (L2):
       - Capacity: Unlimited (practical limits apply)
       - Duration: Persistent across sessions
       - Access: Slower (requires search)
       - Purpose: Important knowledge and experiences
    

    Memory Operations: 1. Store: Add new memory at appropriate level 2. Retrieve: Search across levels 3. Promote: Move important memories up 4. Consolidate: Reorganize and strengthen 5. Forget: Remove based on level-specific rules 6. Rehearse: Strengthen through repetition

    Use Cases: – Conversational agents with context management – Learning systems with knowledge retention – Personal assistants with memory across sessions – Task-oriented agents with working context – Long-running autonomous agents – Multi-session applications – Knowledge management systems

    Advantages: – Efficient memory organization – Appropriate retention strategies per level – Fast access to recent/important information – Scalable to large memory requirements – Natural forgetting curves – Supports both short and long-term needs – Mimics human memory structure

    Challenges: – Determining appropriate level for memories – Managing transitions between levels – Balancing access speed vs. capacity – Consolidation strategy selection – Preventing important memory loss – Synchronization across levels

    LangChain Implementation: This implementation uses LangChain for: – LLM-based memory consolidation and summarization – Importance assessment for promotion decisions – Context-aware retrieval across levels – Memory linking and association

    Production Considerations: – Tune capacity limits for each level – Implement persistent storage for long-term memory – Monitor access patterns and adjust – Regular consolidation during low-activity periods – Backup critical memories before forgetting – Implement recovery mechanisms – Consider storage costs for long-term memory – Balance access speed with storage efficiency


    File Statistics

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

    Module Documentation

    """
    Pattern 102: Hierarchical Memory
    
    Description:
        The Hierarchical Memory pattern organizes agent memory into multiple levels with
        different characteristics, mirroring human memory systems that include working
        memory (immediate, limited capacity), short-term memory (temporary, accessible),
        and long-term memory (persistent, large capacity). This pattern enables efficient
        memory management by storing information at appropriate levels based on recency,
        importance, and access frequency.
    
        Each level in the hierarchy has distinct properties: working memory holds current
        context with fast access but very limited capacity; short-term memory maintains
        recent information with moderate capacity and access speed; long-term memory stores
        consolidated information with large capacity but slower access. Information flows
        between levels through promotion (consolidation) and retrieval mechanisms.
    
        This pattern supports both automatic and strategic memory movement between levels,
        implements forgetting at each level with different strategies, and enables
        efficient multi-level search for relevant information.
    
    Key Components:
        1. Working Memory: Current active context (very limited, fast)
        2. Short-Term Memory: Recent information (limited, moderate speed)
        3. Long-Term Memory: Consolidated knowledge (unlimited, slower)
        4. Promotion Mechanism: Move important info to higher levels
        5. Retrieval System: Search across all levels
        6. Consolidation: Strengthen and reorganize memories
        7. Level-specific Forgetting: Different strategies per level
    
    Memory Levels:
        1. Working Memory (L0):
           - Capacity: 5-9 items (Miller's Law)
           - Duration: Active task only
           - Access: Instant
           - Purpose: Current context and immediate operations
    
        2. Short-Term Memory (L1):
           - Capacity: 50-100 items
           - Duration: Recent session/day
           - Access: Very fast
           - Purpose: Recent interactions and temporary information
    
        3. Long-Term Memory (L2):
           - Capacity: Unlimited (practical limits apply)
           - Duration: Persistent across sessions
           - Access: Slower (requires search)
           - Purpose: Important knowledge and experiences
    
    Memory Operations:
        1. Store: Add new memory at appropriate level
        2. Retrieve: Search across levels
        3. Promote: Move important memories up
        4. Consolidate: Reorganize and strengthen
        5. Forget: Remove based on level-specific rules
        6. Rehearse: Strengthen through repetition
    
    Use Cases:
        - Conversational agents with context management
        - Learning systems with knowledge retention
        - Personal assistants with memory across sessions
        - Task-oriented agents with working context
        - Long-running autonomous agents
        - Multi-session applications
        - Knowledge management systems
    
    Advantages:
        - Efficient memory organization
        - Appropriate retention strategies per level
        - Fast access to recent/important information
        - Scalable to large memory requirements
        - Natural forgetting curves
        - Supports both short and long-term needs
        - Mimics human memory structure
    
    Challenges:
        - Determining appropriate level for memories
        - Managing transitions between levels
        - Balancing access speed vs. capacity
        - Consolidation strategy selection
        - Preventing important memory loss
        - Synchronization across levels
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based memory consolidation and summarization
        - Importance assessment for promotion decisions
        - Context-aware retrieval across levels
        - Memory linking and association
    
    Production Considerations:
        - Tune capacity limits for each level
        - Implement persistent storage for long-term memory
        - Monitor access patterns and adjust
        - Regular consolidation during low-activity periods
        - Backup critical memories before forgetting
        - Implement recovery mechanisms
        - Consider storage costs for long-term memory
        - Balance access speed with storage efficiency
    """
    

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


    Import Statements

    Standard Library Imports

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

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

    Lines 117-121

    Inherits from: Enum

    Purpose:

    Hierarchical memory levels.
    

    Class: HierarchicalMemoryItem

    Lines 125-152

    Purpose:

    Memory item with hierarchical tracking.
    
    Attributes:
        memory_id: Unique identifier
        content: Memory content
        level: Current memory level
        created_at: Creation timestamp
        last_accessed: Last access timestamp
        access_count: Number of accesses
        importance: Importance score (0-1)
        strength: Memory strength (0-1)
        tags: Associated tags
        links: Links to related memories
        metadata: Additional information
    

    Class: HierarchicalMemorySystem

    Lines 155-427

    Purpose:

    Multi-level hierarchical memory system.
    
    This system manages memory across working, short-term, and long-term levels
    with automatic promotion, consolidation, and forgetting mechanisms.
    

    Methods:

    __init__(self, working_capacity, short_term_capacity, promotion_threshold, temperature)

    Lines 163-189

    Purpose:

    Initialize hierarchical memory system.
    
    Args:
        working_capacity: Max items in working memory
        short_term_capacity: Max items in short-term memory
        promotion_threshold: Threshold for promoting memories
        temperature: LLM temperature
    

    Parameters: – working_capacity: Working capacity – short_term_capacity: Short term capacity – promotion_threshold: Promotion threshold – temperature: Sampling temperature (0-1)

    store(self, content, level, importance, tags)

    Lines 191-242

    Purpose:

    Store memory at specified level.
    
    Args:
        content: Memory content
        level: Initial memory level
        importance: Importance score
        tags: Associated tags
    
    Returns:
        Created memory item
    

    Parameters: – content: Content – level: Level – importance: Importance – tags: Tags

    Returns: HierarchicalMemoryItem

    retrieve(self, query, tags, level, n)

    Lines 244-296

    Purpose:

    Retrieve memories across levels.
    
    Args:
        query: Search query
        tags: Filter by tags
        level: Specific level to search (None for all)
        n: Number of results
    
    Returns:
        List of matching memories
    

    Parameters: – query: Input query or question – tags: Tags – level: Level – n: N

    Returns: <ast.Subscript object at 0x000001A06D6AAF10>

    _matches(self, memory, query, tags)

    Lines 298-313

    Purpose:

    Check if memory matches search criteria.
    

    Parameters: – memory: Memory store for conversation history – query: Input query or question – tags: Tags

    Returns: bool

    _promote_to_short_term(self, memory)

    Lines 315-322

    Purpose:

    Promote working memory item to short-term.
    

    Parameters: – memory: Memory store for conversation history

    _promote_to_long_term(self, memory)

    Lines 324-333

    Purpose:

    Promote short-term memory to long-term.
    

    Parameters: – memory: Memory store for conversation history

    _consolidate_short_term(self)

    Lines 335-353

    Purpose:

    Consolidate short-term memories when capacity exceeded.
    

    Parameters:

    strengthen(self, memory_id, amount)

    Lines 355-384

    Purpose:

    Strengthen a memory.
    
    Args:
        memory_id: Memory to strengthen
        amount: Amount to increase strength
    

    Parameters: – memory_id: Memory store for conversation history – amount: Amount

    consolidate_all(self)

    Lines 386-398

    Purpose:

    Perform full memory consolidation.
    
    This simulates sleep/rest consolidation process.
    

    Parameters:

    get_statistics(self)

    Lines 400-418

    Purpose:

    Get memory statistics across all levels.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D889ED0>

    get_memory_at_level(self, level)

    Lines 420-427

    Purpose:

    Get all memories at specific level.
    

    Parameters: – level: Level

    Returns: <ast.Subscript object at 0x000001A06D6614D0>


    Class: HierarchicalMemoryAgent

    Lines 430-540

    Purpose:

    Agent with hierarchical memory system.
    
    This agent uses multi-level memory to maintain context while
    supporting long-term knowledge retention.
    

    Methods:

    __init__(self, working_capacity, short_term_capacity, temperature)

    Lines 438-456

    Purpose:

    Initialize hierarchical memory agent.
    
    Args:
        working_capacity: Working memory capacity
        short_term_capacity: Short-term memory capacity
        temperature: LLM temperature
    

    Parameters: – working_capacity: Working capacity – short_term_capacity: Short term capacity – temperature: Sampling temperature (0-1)

    process(self, user_input, context_tags)

    Lines 458-513

    Purpose:

    Process input with hierarchical memory.
    
    Args:
        user_input: User input
        context_tags: Context tags for retrieval
    
    Returns:
        Response
    

    Parameters: – user_input: Input data – context_tags: Context tags

    Returns: str

    memorize(self, content, importance, level, tags)

    Lines 515-536

    Purpose:

    Explicitly memorize information.
    
    Args:
        content: Content to memorize
        importance: Importance score
        level: Initial memory level
        tags: Associated tags
    

    Parameters: – content: Content – importance: Importance – level: Level – tags: Tags

    consolidate(self)

    Lines 538-540

    Purpose:

    Perform memory consolidation.
    

    Parameters:


    Functions

    demonstrate_hierarchical_memory()

    Lines 543-896

    Purpose:

    Demonstrate hierarchical memory pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Hierarchical Memory 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 101
    • Pattern 103

    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 Hierarchical Memory 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: 102_hierarchical_memory.py
    Total Lines: 901
    Pattern Category: AI Agent Design Pattern #102


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Memory Prioritization

    
        """
    Pattern 101: Memory Prioritization & Forgetting
    
    Description:
        The Memory Prioritization & Forgetting pattern enables agents to manage limited
        memory resources by selectively retaining important information and forgetting
        less relevant memories. This pattern is crucial for long-running agents that
        accumulate vast amounts of information over time, as it prevents memory overflow,
        maintains system performance, and focuses attention on currently relevant data.
        
        Human memory naturally prioritizes and forgets information based on factors like
        recency, frequency, importance, and emotional significance. This pattern emulates
        these mechanisms to create more efficient and effective agent memory systems.
        
        The pattern implements various forgetting strategies including decay-based forgetting
        (memories fade over time), interference-based forgetting (new memories replace old),
        and strategic forgetting (explicit removal of irrelevant information). It uses
        prioritization schemes to rank memories and make retention decisions.
    
    Key Components:
        1. Memory Priority Scoring: Calculate importance of each memory
        2. Forgetting Mechanisms: Time decay, interference, strategic removal
        3. Retention Policies: Rules for what to keep/forget
        4. Memory Consolidation: Strengthen important memories
        5. Capacity Management: Maintain memory within limits
        6. Access Patterns: Track memory usage for prioritization
        7. Relevance Assessment: Determine current importance
    
    Prioritization Factors:
        1. Recency: How recently was memory accessed/created
        2. Frequency: How often has memory been accessed
        3. Importance: Explicit importance score
        4. Relevance: Similarity to current context
        5. Emotional Valence: Significance of memory content
        6. Utility: How useful has memory been
        
    Forgetting Strategies:
        1. Time-based Decay: Memories fade over time
        2. Capacity-based: Remove oldest when full
        3. LRU (Least Recently Used): Remove least accessed
        4. LFU (Least Frequently Used): Remove least frequent
        5. Relevance-based: Remove contextually irrelevant
        6. Strategic: Explicit removal decisions
    
    Use Cases:
        - Long-running conversational agents
        - Personal assistant agents
        - Learning agents with limited memory
        - Real-time systems with memory constraints
        - Adaptive agents in changing environments
        - Information filtering systems
        - Context-aware applications
    
    Advantages:
        - Efficient memory utilization
        - Improved performance (less to search)
        - Focus on relevant information
        - Prevents memory overflow
        - Adapts to changing contexts
        - Natural information lifecycle
        - Scalable for long-term operation
    
    Challenges:
        - Determining importance accurately
        - Risk of forgetting needed information
        - Balancing recency vs. importance
        - Context-dependent relevance
        - Computational overhead of scoring
        - Irreversible information loss
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based importance assessment
        - Context relevance evaluation
        - Memory summarization before forgetting
        - Strategic forgetting decisions
        
    Production Considerations:
        - Set appropriate memory capacity limits
        - Implement memory backups before forgetting
        - Log all forgetting operations for debugging
        - Monitor memory usage and hit rates
        - Tune priority weights for use case
        - Implement memory recovery mechanisms
        - Consider gradual degradation vs. deletion
        - Balance performance vs. retention
    """
    
    import os
    import time
    from typing import List, Dict, Any, Optional, Callable
    from dataclasses import dataclass, field
    from datetime import datetime, timedelta
    from enum import Enum
    from collections import defaultdict
    import math
    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 ForgettingStrategy(Enum):
        """Strategy for forgetting memories."""
        TIME_DECAY = "time_decay"
        LRU = "lru"  # Least Recently Used
        LFU = "lfu"  # Least Frequently Used
        PRIORITY_BASED = "priority_based"
        CAPACITY_BASED = "capacity_based"
        RELEVANCE_BASED = "relevance_based"
    
    
    @dataclass
    class Memory:
        """
        Represents a memory item with priority tracking.
        
        Attributes:
            memory_id: Unique identifier
            content: Memory content
            created_at: When memory was created
            last_accessed: When last accessed
            access_count: Number of times accessed
            importance: Base importance score (0-1)
            priority: Calculated priority score
            tags: Associated tags
            context: Context when created
            metadata: Additional information
        """
        memory_id: str
        content: str
        created_at: datetime
        last_accessed: datetime
        access_count: int = 0
        importance: float = 0.5
        priority: float = 0.5
        tags: List[str] = field(default_factory=list)
        context: Dict[str, Any] = field(default_factory=dict)
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    class MemoryPrioritizer:
        """
        Manages memory prioritization and forgetting.
        
        This class implements various strategies for calculating memory priority
        and deciding what to forget when memory capacity is reached.
        """
        
        def __init__(
            self,
            max_capacity: int = 100,
            strategy: ForgettingStrategy = ForgettingStrategy.PRIORITY_BASED,
            decay_rate: float = 0.1,
            recency_weight: float = 0.3,
            frequency_weight: float = 0.2,
            importance_weight: float = 0.5,
            temperature: float = 0.3
        ):
            """
            Initialize memory prioritizer.
            
            Args:
                max_capacity: Maximum number of memories to retain
                strategy: Forgetting strategy to use
                decay_rate: Rate of time-based decay (0-1)
                recency_weight: Weight for recency in priority calculation
                frequency_weight: Weight for frequency in priority calculation
                importance_weight: Weight for importance in priority calculation
                temperature: LLM temperature
            """
            self.max_capacity = max_capacity
            self.strategy = strategy
            self.decay_rate = decay_rate
            self.recency_weight = recency_weight
            self.frequency_weight = frequency_weight
            self.importance_weight = importance_weight
            self.memories: Dict[str, Memory] = {}
            self.forgotten_count = 0
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
        
        def calculate_priority(
            self,
            memory: Memory,
            current_context: Optional[Dict[str, Any]] = None
        ) -> float:
            """
            Calculate priority score for a memory.
            
            Args:
                memory: Memory to score
                current_context: Current context for relevance calculation
                
            Returns:
                Priority score (0-1)
            """
            now = datetime.now()
            
            # Recency score (exponential decay)
            time_since_access = (now - memory.last_accessed).total_seconds()
            recency_score = math.exp(-self.decay_rate * time_since_access / 3600)
            
            # Frequency score (normalized log scale)
            frequency_score = math.log(memory.access_count + 1) / math.log(100)
            frequency_score = min(1.0, frequency_score)
            
            # Importance score (base importance)
            importance_score = memory.importance
            
            # Combined priority
            priority = (
                self.recency_weight * recency_score +
                self.frequency_weight * frequency_score +
                self.importance_weight * importance_score
            )
            
            # Context relevance boost
            if current_context:
                relevance_boost = self._calculate_relevance(memory, current_context)
                priority = priority * (1 + relevance_boost * 0.5)
            
            return min(1.0, max(0.0, priority))
        
        def _calculate_relevance(
            self,
            memory: Memory,
            context: Dict[str, Any]
        ) -> float:
            """
            Calculate relevance of memory to current context.
            
            Args:
                memory: Memory to evaluate
                context: Current context
                
            Returns:
                Relevance score (0-1)
            """
            # Simple tag-based relevance
            context_tags = context.get("tags", [])
            if not context_tags:
                return 0.0
            
            common_tags = set(memory.tags) & set(context_tags)
            if not memory.tags:
                return 0.0
            
            return len(common_tags) / len(memory.tags)
        
        def add_memory(
            self,
            memory_id: str,
            content: str,
            importance: float = 0.5,
            tags: List[str] = None,
            context: Dict[str, Any] = None
        ) -> Memory:
            """
            Add new memory and apply forgetting if needed.
            
            Args:
                memory_id: Unique identifier
                content: Memory content
                importance: Base importance score
                tags: Associated tags
                context: Creation context
                
            Returns:
                Created memory
            """
            now = datetime.now()
            
            memory = Memory(
                memory_id=memory_id,
                content=content,
                created_at=now,
                last_accessed=now,
                importance=importance,
                tags=tags or [],
                context=context or {}
            )
            
            # Calculate initial priority
            memory.priority = self.calculate_priority(memory)
            
            self.memories[memory_id] = memory
            
            # Check capacity and forget if needed
            if len(self.memories) > self.max_capacity:
                self._forget_memories()
            
            return memory
        
        def access_memory(
            self,
            memory_id: str,
            current_context: Optional[Dict[str, Any]] = None
        ) -> Optional[Memory]:
            """
            Access a memory and update its statistics.
            
            Args:
                memory_id: Memory identifier
                current_context: Current context
                
            Returns:
                Memory if found, None otherwise
            """
            memory = self.memories.get(memory_id)
            if not memory:
                return None
            
            # Update access statistics
            memory.last_accessed = datetime.now()
            memory.access_count += 1
            
            # Recalculate priority
            memory.priority = self.calculate_priority(memory, current_context)
            
            return memory
        
        def _forget_memories(self):
            """Forget memories based on strategy."""
            
            if self.strategy == ForgettingStrategy.LRU:
                # Remove least recently used
                to_forget = sorted(
                    self.memories.values(),
                    key=lambda m: m.last_accessed
                )[:len(self.memories) - self.max_capacity + 1]
            
            elif self.strategy == ForgettingStrategy.LFU:
                # Remove least frequently used
                to_forget = sorted(
                    self.memories.values(),
                    key=lambda m: m.access_count
                )[:len(self.memories) - self.max_capacity + 1]
            
            elif self.strategy == ForgettingStrategy.PRIORITY_BASED:
                # Remove lowest priority
                to_forget = sorted(
                    self.memories.values(),
                    key=lambda m: m.priority
                )[:len(self.memories) - self.max_capacity + 1]
            
            elif self.strategy == ForgettingStrategy.TIME_DECAY:
                # Remove oldest
                to_forget = sorted(
                    self.memories.values(),
                    key=lambda m: m.created_at
                )[:len(self.memories) - self.max_capacity + 1]
            
            else:
                # Default to priority-based
                to_forget = sorted(
                    self.memories.values(),
                    key=lambda m: m.priority
                )[:len(self.memories) - self.max_capacity + 1]
            
            # Remove memories
            for memory in to_forget:
                del self.memories[memory.memory_id]
                self.forgotten_count += 1
        
        def get_top_memories(
            self,
            n: int,
            current_context: Optional[Dict[str, Any]] = None
        ) -> List[Memory]:
            """
            Get top N memories by priority.
            
            Args:
                n: Number of memories to return
                current_context: Current context for priority calculation
                
            Returns:
                List of top memories
            """
            # Recalculate priorities if context provided
            if current_context:
                for memory in self.memories.values():
                    memory.priority = self.calculate_priority(memory, current_context)
            
            sorted_memories = sorted(
                self.memories.values(),
                key=lambda m: m.priority,
                reverse=True
            )
            
            return sorted_memories[:n]
        
        def consolidate_memory(self, memory_id: str):
            """
            Consolidate memory by increasing its importance.
            
            Args:
                memory_id: Memory to consolidate
            """
            memory = self.memories.get(memory_id)
            if memory:
                # Increase importance
                memory.importance = min(1.0, memory.importance * 1.2)
                memory.priority = self.calculate_priority(memory)
        
        def get_statistics(self) -> Dict[str, Any]:
            """
            Get memory statistics.
            
            Returns:
                Dictionary of statistics
            """
            if not self.memories:
                return {
                    "total_memories": 0,
                    "forgotten_count": self.forgotten_count,
                    "capacity_used": 0.0
                }
            
            priorities = [m.priority for m in self.memories.values()]
            access_counts = [m.access_count for m in self.memories.values()]
            
            return {
                "total_memories": len(self.memories),
                "forgotten_count": self.forgotten_count,
                "capacity_used": len(self.memories) / self.max_capacity,
                "avg_priority": sum(priorities) / len(priorities),
                "avg_access_count": sum(access_counts) / len(access_counts),
                "strategy": self.strategy.value
            }
    
    
    class SmartMemoryAgent:
        """
        Agent with intelligent memory management.
        
        This agent uses memory prioritization to maintain relevant information
        while forgetting less important memories.
        """
        
        def __init__(
            self,
            max_memory: int = 50,
            strategy: ForgettingStrategy = ForgettingStrategy.PRIORITY_BASED,
            temperature: float = 0.5
        ):
            """
            Initialize smart memory agent.
            
            Args:
                max_memory: Maximum memory capacity
                strategy: Forgetting strategy
                temperature: LLM temperature
            """
            self.prioritizer = MemoryPrioritizer(
                max_capacity=max_memory,
                strategy=strategy
            )
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.interaction_count = 0
        
        def process_interaction(
            self,
            user_input: str,
            tags: List[str] = None,
            importance: float = 0.5
        ) -> str:
            """
            Process user interaction with memory management.
            
            Args:
                user_input: User input
                tags: Tags for this interaction
                importance: Importance score
                
            Returns:
                Response
            """
            self.interaction_count += 1
            memory_id = f"interaction_{self.interaction_count}"
            
            # Get relevant memories
            context = {"tags": tags or []}
            relevant_memories = self.prioritizer.get_top_memories(5, context)
            
            # Build context from memories
            memory_context = "\n".join([
                f"- {m.content} (priority: {m.priority:.2f})"
                for m in relevant_memories[:3]
            ])
            
            # Generate response using LLM
            prompt = ChatPromptTemplate.from_template(
                "You are a helpful assistant with selective memory. "
                "Here are your most relevant memories:\n{memory_context}\n\n"
                "User: {user_input}\n\n"
                "Respond naturally, incorporating relevant memories."
            )
            
            chain = prompt | self.llm | StrOutputParser()
            response = chain.invoke({
                "memory_context": memory_context if memory_context else "No relevant memories",
                "user_input": user_input
            })
            
            # Store interaction in memory
            self.prioritizer.add_memory(
                memory_id=memory_id,
                content=f"User: {user_input} | Response: {response[:100]}",
                importance=importance,
                tags=tags,
                context={"type": "interaction"}
            )
            
            return response
        
        def memorize_fact(
            self,
            fact: str,
            importance: float = 0.7,
            tags: List[str] = None
        ):
            """
            Explicitly memorize a fact.
            
            Args:
                fact: Fact to memorize
                importance: Importance score
                tags: Associated tags
            """
            self.interaction_count += 1
            memory_id = f"fact_{self.interaction_count}"
            
            self.prioritizer.add_memory(
                memory_id=memory_id,
                content=fact,
                importance=importance,
                tags=tags or [],
                context={"type": "fact"}
            )
        
        def get_memory_summary(self) -> str:
            """
            Get summary of memory state.
            
            Returns:
                Memory summary
            """
            stats = self.prioritizer.get_statistics()
            top_memories = self.prioritizer.get_top_memories(5)
            
            summary = f"Memory Statistics:\n"
            summary += f"  Total Memories: {stats['total_memories']}\n"
            summary += f"  Forgotten: {stats['forgotten_count']}\n"
            summary += f"  Capacity Used: {stats['capacity_used']:.1%}\n"
            summary += f"  Avg Priority: {stats.get('avg_priority', 0):.2f}\n\n"
            
            summary += "Top Memories:\n"
            for i, mem in enumerate(top_memories, 1):
                summary += f"  {i}. {mem.content[:60]}... "
                summary += f"(priority: {mem.priority:.2f}, "
                summary += f"accessed: {mem.access_count}x)\n"
            
            return summary
    
    
    def demonstrate_memory_prioritization():
        """Demonstrate memory prioritization and forgetting pattern."""
        
        print("=" * 80)
        print("MEMORY PRIORITIZATION & FORGETTING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic priority-based forgetting
        print("\n" + "=" * 80)
        print("Example 1: Priority-Based Memory Management")
        print("=" * 80)
        
        prioritizer = MemoryPrioritizer(
            max_capacity=10,
            strategy=ForgettingStrategy.PRIORITY_BASED,
            recency_weight=0.3,
            frequency_weight=0.2,
            importance_weight=0.5
        )
        
        print("\nAdding 15 memories with varying importance...")
        print(f"Max capacity: {prioritizer.max_capacity}")
        
        # Add memories with different importance levels
        memories_to_add = [
            ("mem_1", "User's name is Alice", 0.9, ["personal", "name"]),
            ("mem_2", "Likes pizza", 0.5, ["food", "preferences"]),
            ("mem_3", "Birthday is June 15", 0.8, ["personal", "date"]),
            ("mem_4", "Works in tech", 0.6, ["work"]),
            ("mem_5", "Random fact about weather", 0.2, ["misc"]),
            ("mem_6", "Allergic to peanuts", 0.9, ["health", "important"]),
            ("mem_7", "Favorite color is blue", 0.3, ["preferences"]),
            ("mem_8", "Lives in San Francisco", 0.7, ["personal", "location"]),
            ("mem_9", "Speaks English and Spanish", 0.6, ["language"]),
            ("mem_10", "Unimportant note", 0.1, ["misc"]),
            ("mem_11", "Has a dog named Max", 0.6, ["personal", "pets"]),
            ("mem_12", "Exercises regularly", 0.4, ["health"]),
            ("mem_13", "Another random fact", 0.1, ["misc"]),
            ("mem_14", "Emergency contact: Bob", 0.8, ["important", "contact"]),
            ("mem_15", "Likes science fiction", 0.4, ["entertainment"]),
        ]
        
        for mem_id, content, importance, tags in memories_to_add:
            prioritizer.add_memory(mem_id, content, importance, tags)
            time.sleep(0.01)  # Small delay to differentiate timestamps
        
        print(f"\nMemories after adding 15 (capacity: {prioritizer.max_capacity}):")
        print(f"  Retained: {len(prioritizer.memories)}")
        print(f"  Forgotten: {prioritizer.forgotten_count}")
        
        # Show retained memories
        print("\nRetained memories (sorted by priority):")
        retained = sorted(prioritizer.memories.values(), key=lambda m: m.priority, reverse=True)
        for i, mem in enumerate(retained, 1):
            print(f"  {i}. [{mem.memory_id}] {mem.content} "
                  f"(importance: {mem.importance:.1f}, priority: {mem.priority:.2f})")
        
        # Example 2: Impact of access patterns
        print("\n" + "=" * 80)
        print("Example 2: Access Patterns Affecting Priority")
        print("=" * 80)
        
        print("\nAccessing 'Likes pizza' memory multiple times...")
        for _ in range(5):
            prioritizer.access_memory("mem_2")
            time.sleep(0.01)
        
        mem_2 = prioritizer.memories.get("mem_2")
        if mem_2:
            print(f"  Access count: {mem_2.access_count}")
            print(f"  Updated priority: {mem_2.priority:.2f}")
        
        # Add more memories to trigger forgetting
        print("\nAdding 3 more low-importance memories...")
        for i in range(16, 19):
            prioritizer.add_memory(
                f"mem_{i}",
                f"Low importance memory {i}",
                0.2,
                ["misc"]
            )
        
        print(f"\nMemories after additions:")
        print(f"  Retained: {len(prioritizer.memories)}")
        print(f"  Forgotten: {prioritizer.forgotten_count}")
        print(f"  'Likes pizza' still retained: {'mem_2' in prioritizer.memories}")
        
        # Example 3: Context-based relevance
        print("\n" + "=" * 80)
        print("Example 3: Context-Based Memory Retrieval")
        print("=" * 80)
        
        contexts = [
            {"tags": ["personal"], "description": "Personal info"},
            {"tags": ["health"], "description": "Health-related"},
            {"tags": ["work"], "description": "Work-related"},
        ]
        
        for context in contexts:
            print(f"\nContext: {context['description']}")
            top_memories = prioritizer.get_top_memories(3, context)
            for i, mem in enumerate(top_memories, 1):
                print(f"  {i}. {mem.content[:40]}... "
                      f"(priority: {mem.priority:.2f}, tags: {mem.tags})")
        
        # Example 4: Different forgetting strategies
        print("\n" + "=" * 80)
        print("Example 4: Comparing Forgetting Strategies")
        print("=" * 80)
        
        strategies = [
            ForgettingStrategy.LRU,
            ForgettingStrategy.LFU,
            ForgettingStrategy.TIME_DECAY,
        ]
        
        for strategy in strategies:
            print(f"\nStrategy: {strategy.value}")
            test_prioritizer = MemoryPrioritizer(
                max_capacity=5,
                strategy=strategy
            )
            
            # Add 8 memories
            for i in range(8):
                test_prioritizer.add_memory(
                    f"test_mem_{i}",
                    f"Memory {i}",
                    0.5,
                    ["test"]
                )
                if i < 3:  # Access first 3 multiple times
                    for _ in range(3):
                        test_prioritizer.access_memory(f"test_mem_{i}")
                time.sleep(0.01)
            
            print(f"  Retained memories: {list(test_prioritizer.memories.keys())}")
            print(f"  Forgotten count: {test_prioritizer.forgotten_count}")
        
        # Example 5: Smart memory agent
        print("\n" + "=" * 80)
        print("Example 5: Smart Memory Agent with Forgetting")
        print("=" * 80)
        
        agent = SmartMemoryAgent(
            max_memory=15,
            strategy=ForgettingStrategy.PRIORITY_BASED
        )
        
        print("\nInteracting with agent (max 15 memories)...")
        
        # Store important facts
        agent.memorize_fact(
            "User's name is Sarah",
            importance=0.9,
            tags=["personal", "name"]
        )
        agent.memorize_fact(
            "Allergic to shellfish",
            importance=0.9,
            tags=["health", "important"]
        )
        
        # Multiple interactions
        interactions = [
            ("Tell me about healthy eating", ["health", "advice"], 0.5),
            ("What's my name?", ["personal"], 0.7),
            ("Random weather question", ["misc"], 0.2),
            ("What should I avoid eating?", ["health", "important"], 0.8),
            ("Tell me a joke", ["entertainment"], 0.3),
        ]
        
        for user_input, tags, importance in interactions:
            print(f"\nUser: {user_input}")
            response = agent.process_interaction(user_input, tags, importance)
            print(f"Agent: {response[:150]}...")
        
        # Add many low-importance interactions
        print("\nAdding 15 low-importance interactions...")
        for i in range(15):
            agent.process_interaction(
                f"Unimportant question {i}",
                tags=["misc"],
                importance=0.1
            )
        
        print("\n" + agent.get_memory_summary())
        
        # Verify important memories retained
        print("\nVerifying important memories retained:")
        important_memories = agent.prioritizer.get_top_memories(5)
        for mem in important_memories:
            if "Sarah" in mem.content or "shellfish" in mem.content:
                print(f"  ✓ Important memory retained: {mem.content[:50]}...")
        
        # Example 6: Memory consolidation
        print("\n" + "=" * 80)
        print("Example 6: Memory Consolidation (Strengthening)")
        print("=" * 80)
        
        consolidator = MemoryPrioritizer(max_capacity=10)
        
        # Add memory and show initial priority
        mem = consolidator.add_memory(
            "consolidate_me",
            "Important fact to consolidate",
            importance=0.5,
            tags=["test"]
        )
        print(f"Initial importance: {mem.importance:.2f}")
        print(f"Initial priority: {mem.priority:.2f}")
        
        # Consolidate (strengthen) the memory
        print("\nConsolidating memory 3 times...")
        for i in range(3):
            consolidator.consolidate_memory("consolidate_me")
            mem = consolidator.memories["consolidate_me"]
            print(f"  After consolidation {i+1}: "
                  f"importance={mem.importance:.2f}, priority={mem.priority:.2f}")
        
        # Example 7: Memory statistics over time
        print("\n" + "=" * 80)
        print("Example 7: Memory Statistics Analysis")
        print("=" * 80)
        
        tracker = MemoryPrioritizer(
            max_capacity=20,
            strategy=ForgettingStrategy.PRIORITY_BASED
        )
        
        print("\nAdding 50 memories over time with varying access patterns...")
        
        for i in range(50):
            importance = 0.3 + (i % 5) * 0.15  # Varying importance
            tracker.add_memory(
                f"mem_{i}",
                f"Memory content {i}",
                importance=importance,
                tags=[f"tag_{i % 3}"]
            )
            
            # Simulate some memories being accessed more
            if i % 5 == 0:
                tracker.access_memory(f"mem_{i}")
            
            if i % 10 == 0:
                stats = tracker.get_statistics()
                print(f"\nAfter {i+1} additions:")
                print(f"  Retained: {stats['total_memories']}")
                print(f"  Forgotten: {stats['forgotten_count']}")
                print(f"  Capacity: {stats['capacity_used']:.1%}")
                print(f"  Avg priority: {stats.get('avg_priority', 0):.2f}")
        
        # Final statistics
        final_stats = tracker.get_statistics()
        print(f"\nFinal Statistics:")
        print(f"  Total memories added: 50")
        print(f"  Currently retained: {final_stats['total_memories']}")
        print(f"  Total forgotten: {final_stats['forgotten_count']}")
        print(f"  Retention rate: {final_stats['total_memories']/50:.1%}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY: Memory Prioritization & Forgetting Pattern")
        print("=" * 80)
        
        summary = """
        The Memory Prioritization & Forgetting pattern demonstrated:
        
        1. PRIORITY-BASED FORGETTING (Example 1):
           - Automatic memory management within capacity limits
           - Importance-weighted retention decisions
           - Low-importance memories forgotten first
           - High-importance memories (name, allergies) retained
        
        2. ACCESS PATTERNS (Example 2):
           - Frequently accessed memories gain higher priority
           - Recency and frequency tracked automatically
           - "Likes pizza" retained despite lower base importance
           - Dynamic priority recalculation
        
        3. CONTEXT-BASED RETRIEVAL (Example 3):
           - Tag-based relevance calculation
           - Context-aware memory prioritization
           - Different top memories for different contexts
           - Personal, health, work contexts demonstrated
        
        4. FORGETTING STRATEGIES (Example 4):
           - LRU: Removes least recently used
           - LFU: Removes least frequently used
           - Time Decay: Removes oldest memories
           - Priority-Based: Removes lowest priority
        
        5. SMART AGENT (Example 5):
           - Integrated memory management in agent workflow
           - Important facts (name, allergies) retained
           - Low-importance interactions forgotten
           - Context-aware responses using relevant memories
        
        6. MEMORY CONSOLIDATION (Example 6):
           - Strengthening important memories
           - Importance increases with consolidation
           - Priority automatically recalculated
           - Simulates rehearsal/reinforcement
        
        7. STATISTICS TRACKING (Example 7):
           - Memory usage metrics over time
           - Retention rate monitoring
           - Forgetting rate analysis
           - Capacity utilization tracking
        
        KEY BENEFITS:
        ✓ Efficient memory utilization
        ✓ Focus on relevant information
        ✓ Scalable for long-term operation
        ✓ Automatic capacity management
        ✓ Context-aware prioritization
        ✓ Adaptive to usage patterns
        ✓ Prevents memory overflow
        
        USE CASES:
        • Long-running conversational agents
        • Personal assistant applications
        • Learning agents with memory constraints
        • Real-time systems with limited resources
        • Adaptive systems in changing environments
        • Information filtering and summarization
        • Context-aware applications
        
        BEST PRACTICES:
        1. Set appropriate capacity limits for use case
        2. Weight priority factors based on needs
        3. Use high importance for critical information
        4. Implement backup/logging before forgetting
        5. Monitor retention rates and adjust
        6. Consolidate frequently-used memories
        7. Consider context in retrieval
        8. Track statistics for tuning
        
        TRADE-OFFS:
        • Memory efficiency vs. information loss
        • Computational overhead vs. accuracy
        • Recency vs. importance weighting
        • Capacity limits vs. completeness
        
        PRODUCTION CONSIDERATIONS:
        → Set capacity based on system resources
        → Implement memory backup mechanisms
        → Log all forgetting operations for debugging
        → Monitor hit rates and adjust weights
        → Test forgetting strategies for domain
        → Consider reversible "archiving" vs. deletion
        → Balance performance with retention needs
        → Implement gradual priority decay
        → Track memory access patterns for optimization
        → Consider user-controlled importance overrides
        
        This pattern enables agents to operate efficiently over long periods
        by intelligently managing memory, focusing on relevant information,
        and gracefully handling capacity constraints through smart forgetting.
        """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_memory_prioritization()
    
        

    Agentic Design Pattern: Memory Prioritization

    Pattern 101: Memory Prioritization – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 101: Memory Prioritization & Forgetting

    Description: The Memory Prioritization & Forgetting pattern enables agents to manage limited memory resources by selectively retaining important information and forgetting less relevant memories. This pattern is crucial for long-running agents that accumulate vast amounts of information over time, as it prevents memory overflow, maintains system performance, and focuses attention on currently relevant data.

    Human memory naturally prioritizes and forgets information based on factors like
    recency, frequency, importance, and emotional significance. This pattern emulates
    these mechanisms to create more efficient and effective agent memory systems.
    
    The pattern implements various forgetting strategies including decay-based forgetting
    (memories fade over time), interference-based forgetting (new memories replace old),
    and strategic forgetting (explicit removal of irrelevant information). It uses
    prioritization schemes to rank memories and make retention decisions.
    

    Key Components: 1. Memory Priority Scoring: Calculate importance of each memory 2. Forgetting Mechanisms: Time decay, interference, strategic removal 3. Retention Policies: Rules for what to keep/forget 4. Memory Consolidation: Strengthen important memories 5. Capacity Management: Maintain memory within limits 6. Access Patterns: Track memory usage for prioritization 7. Relevance Assessment: Determine current importance

    Prioritization Factors: 1. Recency: How recently was memory accessed/created 2. Frequency: How often has memory been accessed 3. Importance: Explicit importance score 4. Relevance: Similarity to current context 5. Emotional Valence: Significance of memory content 6. Utility: How useful has memory been

    Forgetting Strategies: 1. Time-based Decay: Memories fade over time 2. Capacity-based: Remove oldest when full 3. LRU (Least Recently Used): Remove least accessed 4. LFU (Least Frequently Used): Remove least frequent 5. Relevance-based: Remove contextually irrelevant 6. Strategic: Explicit removal decisions

    Use Cases: – Long-running conversational agents – Personal assistant agents – Learning agents with limited memory – Real-time systems with memory constraints – Adaptive agents in changing environments – Information filtering systems – Context-aware applications

    Advantages: – Efficient memory utilization – Improved performance (less to search) – Focus on relevant information – Prevents memory overflow – Adapts to changing contexts – Natural information lifecycle – Scalable for long-term operation

    Challenges: – Determining importance accurately – Risk of forgetting needed information – Balancing recency vs. importance – Context-dependent relevance – Computational overhead of scoring – Irreversible information loss

    LangChain Implementation: This implementation uses LangChain for: – LLM-based importance assessment – Context relevance evaluation – Memory summarization before forgetting – Strategic forgetting decisions

    Production Considerations: – Set appropriate memory capacity limits – Implement memory backups before forgetting – Log all forgetting operations for debugging – Monitor memory usage and hit rates – Tune priority weights for use case – Implement memory recovery mechanisms – Consider gradual degradation vs. deletion – Balance performance vs. retention


    File Statistics

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

    Module Documentation

    """
    Pattern 101: Memory Prioritization & Forgetting
    
    Description:
        The Memory Prioritization & Forgetting pattern enables agents to manage limited
        memory resources by selectively retaining important information and forgetting
        less relevant memories. This pattern is crucial for long-running agents that
        accumulate vast amounts of information over time, as it prevents memory overflow,
        maintains system performance, and focuses attention on currently relevant data.
    
        Human memory naturally prioritizes and forgets information based on factors like
        recency, frequency, importance, and emotional significance. This pattern emulates
        these mechanisms to create more efficient and effective agent memory systems.
    
        The pattern implements various forgetting strategies including decay-based forgetting
        (memories fade over time), interference-based forgetting (new memories replace old),
        and strategic forgetting (explicit removal of irrelevant information). It uses
        prioritization schemes to rank memories and make retention decisions.
    
    Key Components:
        1. Memory Priority Scoring: Calculate importance of each memory
        2. Forgetting Mechanisms: Time decay, interference, strategic removal
        3. Retention Policies: Rules for what to keep/forget
        4. Memory Consolidation: Strengthen important memories
        5. Capacity Management: Maintain memory within limits
        6. Access Patterns: Track memory usage for prioritization
        7. Relevance Assessment: Determine current importance
    
    Prioritization Factors:
        1. Recency: How recently was memory accessed/created
        2. Frequency: How often has memory been accessed
        3. Importance: Explicit importance score
        4. Relevance: Similarity to current context
        5. Emotional Valence: Significance of memory content
        6. Utility: How useful has memory been
    
    Forgetting Strategies:
        1. Time-based Decay: Memories fade over time
        2. Capacity-based: Remove oldest when full
        3. LRU (Least Recently Used): Remove least accessed
        4. LFU (Least Frequently Used): Remove least frequent
        5. Relevance-based: Remove contextually irrelevant
        6. Strategic: Explicit removal decisions
    
    Use Cases:
        - Long-running conversational agents
        - Personal assistant agents
        - Learning agents with limited memory
        - Real-time systems with memory constraints
        - Adaptive agents in changing environments
        - Information filtering systems
        - Context-aware applications
    
    Advantages:
        - Efficient memory utilization
        - Improved performance (less to search)
        - Focus on relevant information
        - Prevents memory overflow
        - Adapts to changing contexts
        - Natural information lifecycle
        - Scalable for long-term operation
    
    Challenges:
        - Determining importance accurately
        - Risk of forgetting needed information
        - Balancing recency vs. importance
        - Context-dependent relevance
        - Computational overhead of scoring
        - Irreversible information loss
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based importance assessment
        - Context relevance evaluation
        - Memory summarization before forgetting
        - Strategic forgetting decisions
    
    Production Considerations:
        - Set appropriate memory capacity limits
        - Implement memory backups before forgetting
        - Log all forgetting operations for debugging
        - Monitor memory usage and hit rates
        - Tune priority weights for use case
        - Implement memory recovery mechanisms
        - Consider gradual degradation vs. deletion
        - Balance performance vs. retention
    """
    

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


    Import Statements

    Standard Library Imports

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

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

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from dotenv.load_dotenv
    from math
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: ForgettingStrategy

    Lines 106-113

    Inherits from: Enum

    Purpose:

    Strategy for forgetting memories.
    

    Class: Memory

    Lines 117-142

    Purpose:

    Represents a memory item with priority tracking.
    
    Attributes:
        memory_id: Unique identifier
        content: Memory content
        created_at: When memory was created
        last_accessed: When last accessed
        access_count: Number of times accessed
        importance: Base importance score (0-1)
        priority: Calculated priority score
        tags: Associated tags
        context: Context when created
        metadata: Additional information
    

    Class: MemoryPrioritizer

    Lines 145-433

    Purpose:

    Manages memory prioritization and forgetting.
    
    This class implements various strategies for calculating memory priority
    and deciding what to forget when memory capacity is reached.
    

    Methods:

    __init__(self, max_capacity, strategy, decay_rate, recency_weight, frequency_weight, importance_weight, temperature)

    Lines 153-183

    Purpose:

    Initialize memory prioritizer.
    
    Args:
        max_capacity: Maximum number of memories to retain
        strategy: Forgetting strategy to use
        decay_rate: Rate of time-based decay (0-1)
        recency_weight: Weight for recency in priority calculation
        frequency_weight: Weight for frequency in priority calculation
        importance_weight: Weight for importance in priority calculation
        temperature: LLM temperature
    

    Parameters: – max_capacity: Max capacity – strategy: Strategy – decay_rate: Decay rate – recency_weight: Recency weight – frequency_weight: Frequency weight – importance_weight: Importance weight – temperature: Sampling temperature (0-1)

    calculate_priority(self, memory, current_context)

    Lines 185-225

    Purpose:

    Calculate priority score for a memory.
    
    Args:
        memory: Memory to score
        current_context: Current context for relevance calculation
    
    Returns:
        Priority score (0-1)
    

    Parameters: – memory: Memory store for conversation history – current_context: Current context

    Returns: float

    _calculate_relevance(self, memory, context)

    Lines 227-251

    Purpose:

    Calculate relevance of memory to current context.
    
    Args:
        memory: Memory to evaluate
        context: Current context
    
    Returns:
        Relevance score (0-1)
    

    Parameters: – memory: Memory store for conversation history – context: Context

    Returns: float

    add_memory(self, memory_id, content, importance, tags, context)

    Lines 253-295

    Purpose:

    Add new memory and apply forgetting if needed.
    
    Args:
        memory_id: Unique identifier
        content: Memory content
        importance: Base importance score
        tags: Associated tags
        context: Creation context
    
    Returns:
        Created memory
    

    Parameters: – memory_id: Memory store for conversation history – content: Content – importance: Importance – tags: Tags – context: Context

    Returns: Memory

    access_memory(self, memory_id, current_context)

    Lines 297-323

    Purpose:

    Access a memory and update its statistics.
    
    Args:
        memory_id: Memory identifier
        current_context: Current context
    
    Returns:
        Memory if found, None otherwise
    

    Parameters: – memory_id: Memory store for conversation history – current_context: Current context

    Returns: <ast.Subscript object at 0x000001A06D72E490>

    _forget_memories(self)

    Lines 325-366

    Purpose:

    Forget memories based on strategy.
    

    Parameters:

    get_top_memories(self, n, current_context)

    Lines 368-394

    Purpose:

    Get top N memories by priority.
    
    Args:
        n: Number of memories to return
        current_context: Current context for priority calculation
    
    Returns:
        List of top memories
    

    Parameters: – n: N – current_context: Current context

    Returns: <ast.Subscript object at 0x000001A06D16AFD0>

    consolidate_memory(self, memory_id)

    Lines 396-407

    Purpose:

    Consolidate memory by increasing its importance.
    
    Args:
        memory_id: Memory to consolidate
    

    Parameters: – memory_id: Memory store for conversation history

    get_statistics(self)

    Lines 409-433

    Purpose:

    Get memory statistics.
    
    Returns:
        Dictionary of statistics
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D7F4990>


    Class: SmartMemoryAgent

    Lines 436-567

    Purpose:

    Agent with intelligent memory management.
    
    This agent uses memory prioritization to maintain relevant information
    while forgetting less important memories.
    

    Methods:

    __init__(self, max_memory, strategy, temperature)

    Lines 444-463

    Purpose:

    Initialize smart memory agent.
    
    Args:
        max_memory: Maximum memory capacity
        strategy: Forgetting strategy
        temperature: LLM temperature
    

    Parameters: – max_memory: Memory store for conversation history – strategy: Strategy – temperature: Sampling temperature (0-1)

    process_interaction(self, user_input, tags, importance)

    Lines 465-518

    Purpose:

    Process user interaction with memory management.
    
    Args:
        user_input: User input
        tags: Tags for this interaction
        importance: Importance score
    
    Returns:
        Response
    

    Parameters: – user_input: Input data – tags: Tags – importance: Importance

    Returns: str

    memorize_fact(self, fact, importance, tags)

    Lines 520-543

    Purpose:

    Explicitly memorize a fact.
    
    Args:
        fact: Fact to memorize
        importance: Importance score
        tags: Associated tags
    

    Parameters: – fact: Fact – importance: Importance – tags: Tags

    get_memory_summary(self)

    Lines 545-567

    Purpose:

    Get summary of memory state.
    
    Returns:
        Memory summary
    

    Parameters:

    Returns: str


    Functions

    demonstrate_memory_prioritization()

    Lines 570-932

    Purpose:

    Demonstrate memory prioritization and forgetting pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Memory Prioritization 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 100
    • Pattern 102

    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 Memory Prioritization 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: 101_memory_prioritization.py
    Total Lines: 937
    Pattern Category: AI Agent Design Pattern #101


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Hierarchical Communication

    
        """
    Pattern 100: Hierarchical Communication
    
    Description:
        The Hierarchical Communication pattern establishes structured communication channels
        between agents organized in a hierarchical structure. This pattern is essential for
        large-scale multi-agent systems where flat communication would be inefficient or
        chaotic. It defines clear lines of authority, responsibility, and communication flow.
        
        Hierarchical structures enable scalable coordination by organizing agents into levels:
        executives/managers at higher levels provide strategic direction and oversight, while
        workers at lower levels handle operational tasks. Communication flows both top-down
        (commands, directives, goals) and bottom-up (reports, escalations, feedback).
        
        This pattern supports various organizational structures including strict hierarchies,
        matrix organizations, and hybrid models. It handles delegation, escalation, reporting,
        and cross-level communication while maintaining clear accountability and reducing
        communication overhead through proper routing and filtering.
    
    Key Components:
        1. Hierarchical Levels: Organization into layers (executive, manager, worker)
        2. Authority Structure: Who can command whom
        3. Communication Channels: Allowed communication paths
        4. Delegation: Task assignment down the hierarchy
        5. Escalation: Problem raising up the hierarchy
        6. Reporting: Status updates to superiors
        7. Coordination: Cross-branch communication
        8. Information Filtering: Aggregation at each level
    
    Organizational Structures:
        1. Strict Hierarchy: Tree structure, single reporting line
        2. Matrix Organization: Multiple reporting lines
        3. Flat Hierarchy: Few levels, wide spans
        4. Deep Hierarchy: Many levels, narrow spans
        5. Hybrid: Mix of hierarchical and flat structures
        
    Communication Types:
        1. Top-Down: Commands, goals, policies from above
        2. Bottom-Up: Reports, feedback, escalations from below
        3. Lateral: Peer-to-peer at same level
        4. Cross-Level: Skip-level communication (exceptional)
        5. Broadcast: Announcements to entire subtree
    
    Use Cases:
        - Large-scale multi-agent systems
        - Enterprise workflow automation
        - Military/command structures
        - Corporate organizational modeling
        - Distributed task management
        - Crisis management systems
        - Project management hierarchies
        - Robotic swarm coordination
    
    Advantages:
        - Scalable communication structure
        - Clear lines of authority
        - Reduced communication overhead
        - Organized task delegation
        - Efficient escalation paths
        - Information aggregation
        - Accountability tracking
        - Span of control management
    
    Challenges:
        - Communication delays through levels
        - Information loss in aggregation
        - Rigidity in dynamic environments
        - Single points of failure
        - Bureaucratic overhead
        - Potential bottlenecks at higher levels
        - Difficulty with cross-functional needs
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based delegation and task breakdown
        - Status report generation and summarization
        - Escalation decision-making
        - Command interpretation
        - Cross-level communication reasoning
        
    Production Considerations:
        - Design appropriate span of control (3-10 subordinates typical)
        - Implement timeout handling for non-responsive nodes
        - Log all communications for audit trails
        - Balance hierarchy depth vs. communication overhead
        - Support emergency bypass channels
        - Monitor for bottlenecks at key levels
        - Implement redundancy for critical nodes
        - Handle dynamic reorganization gracefully
    """
    
    import os
    from typing import List, Dict, Any, Optional, Set
    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 AgentLevel(Enum):
        """Hierarchical level of an agent."""
        EXECUTIVE = "executive"
        MANAGER = "manager"
        WORKER = "worker"
    
    
    class MessageDirection(Enum):
        """Direction of communication in hierarchy."""
        DOWN = "down"  # Superior to subordinate
        UP = "up"      # Subordinate to superior
        LATERAL = "lateral"  # Peer to peer
        BROADCAST = "broadcast"  # To entire subtree
    
    
    class MessageType(Enum):
        """Type of hierarchical message."""
        COMMAND = "command"
        DELEGATION = "delegation"
        REPORT = "report"
        ESCALATION = "escalation"
        QUERY = "query"
        RESPONSE = "response"
        ANNOUNCEMENT = "announcement"
    
    
    @dataclass
    class HierarchicalMessage:
        """
        Message in hierarchical communication system.
        
        Attributes:
            message_id: Unique identifier
            sender: Sending agent
            recipients: List of recipient agents
            message_type: Type of message
            direction: Communication direction
            content: Message content
            priority: Priority level (1-5, 5 highest)
            timestamp: When message was sent
            requires_response: Whether response is expected
            parent_message_id: Reference to parent message
            metadata: Additional information
        """
        message_id: str
        sender: str
        recipients: List[str]
        message_type: MessageType
        direction: MessageDirection
        content: Dict[str, Any]
        priority: int = 3
        timestamp: datetime = field(default_factory=datetime.now)
        requires_response: bool = False
        parent_message_id: Optional[str] = None
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class AgentNode:
        """
        Node in hierarchical agent structure.
        
        Attributes:
            agent_id: Unique identifier
            level: Hierarchical level
            superior: Agent's supervisor (None for root)
            subordinates: List of direct reports
            capabilities: Agent's capabilities
            current_tasks: Active tasks
            metadata: Additional information
        """
        agent_id: str
        level: AgentLevel
        superior: Optional[str] = None
        subordinates: List[str] = field(default_factory=list)
        capabilities: List[str] = field(default_factory=list)
        current_tasks: List[Dict[str, Any]] = field(default_factory=list)
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    class HierarchicalAgent:
        """
        Agent that operates within hierarchical communication structure.
        
        This agent can send commands to subordinates, report to superiors,
        escalate issues, delegate tasks, and communicate with peers.
        """
        
        def __init__(
            self,
            agent_id: str,
            level: AgentLevel,
            capabilities: List[str],
            temperature: float = 0.4
        ):
            """
            Initialize hierarchical agent.
            
            Args:
                agent_id: Agent's unique identifier
                level: Hierarchical level
                capabilities: List of capabilities
                temperature: LLM temperature
            """
            self.agent_id = agent_id
            self.level = level
            self.capabilities = capabilities
            self.superior: Optional[str] = None
            self.subordinates: List[str] = []
            self.peers: Set[str] = set()
            self.tasks: List[Dict[str, Any]] = []
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.message_history: List[HierarchicalMessage] = []
        
        def delegate_task(
            self,
            task: Dict[str, Any],
            subordinate_id: str
        ) -> HierarchicalMessage:
            """
            Delegate task to subordinate.
            
            Args:
                task: Task to delegate
                subordinate_id: ID of subordinate to delegate to
                
            Returns:
                Delegation message
            """
            # Generate delegation message using LLM
            prompt = ChatPromptTemplate.from_template(
                "You are {agent_id}, a {level} agent delegating a task. "
                "Task: {task_description}. "
                "Write clear, concise delegation instructions (2-3 sentences) "
                "for your subordinate, including objectives and expectations.\n\n"
                "Instructions:"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            instructions = chain.invoke({
                "agent_id": self.agent_id,
                "level": self.level.value,
                "task_description": task.get("description", "Unknown task")
            })
            
            message = HierarchicalMessage(
                message_id=f"{self.agent_id}_del_{len(self.message_history)}",
                sender=self.agent_id,
                recipients=[subordinate_id],
                message_type=MessageType.DELEGATION,
                direction=MessageDirection.DOWN,
                content={
                    "task": task,
                    "instructions": instructions.strip(),
                    "deadline": task.get("deadline"),
                    "priority": task.get("priority", 3)
                },
                priority=task.get("priority", 3),
                requires_response=True
            )
            
            self.message_history.append(message)
            return message
        
        def report_status(
            self,
            status: str,
            details: Dict[str, Any]
        ) -> HierarchicalMessage:
            """
            Report status to superior.
            
            Args:
                status: Status summary
                details: Detailed information
                
            Returns:
                Status report message
            """
            # Generate summary using LLM
            prompt = ChatPromptTemplate.from_template(
                "You are {agent_id}, a {level} agent reporting to your superior. "
                "Status: {status}. "
                "Details: {details}. "
                "Write a professional 2-3 sentence status report.\n\n"
                "Report:"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            report = chain.invoke({
                "agent_id": self.agent_id,
                "level": self.level.value,
                "status": status,
                "details": details
            })
            
            message = HierarchicalMessage(
                message_id=f"{self.agent_id}_rep_{len(self.message_history)}",
                sender=self.agent_id,
                recipients=[self.superior] if self.superior else [],
                message_type=MessageType.REPORT,
                direction=MessageDirection.UP,
                content={
                    "status": status,
                    "report": report.strip(),
                    "details": details,
                    "tasks_completed": len([t for t in self.tasks if t.get("completed")]),
                    "tasks_active": len([t for t in self.tasks if not t.get("completed")])
                },
                priority=2
            )
            
            self.message_history.append(message)
            return message
        
        def escalate_issue(
            self,
            issue: str,
            severity: int,
            context: Dict[str, Any]
        ) -> HierarchicalMessage:
            """
            Escalate issue to superior.
            
            Args:
                issue: Issue description
                severity: Severity level (1-5)
                context: Additional context
                
            Returns:
                Escalation message
            """
            # Generate escalation using LLM
            prompt = ChatPromptTemplate.from_template(
                "You are {agent_id} escalating an issue to your superior. "
                "Issue: {issue}. "
                "Severity: {severity}/5. "
                "Write a clear 2-3 sentence escalation explaining the problem "
                "and why it needs higher-level attention.\n\n"
                "Escalation:"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            escalation_text = chain.invoke({
                "agent_id": self.agent_id,
                "issue": issue,
                "severity": severity
            })
            
            message = HierarchicalMessage(
                message_id=f"{self.agent_id}_esc_{len(self.message_history)}",
                sender=self.agent_id,
                recipients=[self.superior] if self.superior else [],
                message_type=MessageType.ESCALATION,
                direction=MessageDirection.UP,
                content={
                    "issue": issue,
                    "severity": severity,
                    "escalation": escalation_text.strip(),
                    "context": context
                },
                priority=min(5, severity + 1),
                requires_response=True
            )
            
            self.message_history.append(message)
            return message
        
        def broadcast_announcement(
            self,
            announcement: str
        ) -> HierarchicalMessage:
            """
            Broadcast announcement to all subordinates.
            
            Args:
                announcement: Announcement text
                
            Returns:
                Broadcast message
            """
            message = HierarchicalMessage(
                message_id=f"{self.agent_id}_bcast_{len(self.message_history)}",
                sender=self.agent_id,
                recipients=self.subordinates.copy(),
                message_type=MessageType.ANNOUNCEMENT,
                direction=MessageDirection.BROADCAST,
                content={
                    "announcement": announcement,
                    "scope": "all_subordinates"
                },
                priority=3
            )
            
            self.message_history.append(message)
            return message
    
    
    class HierarchicalOrganization:
        """
        Manages hierarchical organization of agents.
        
        This class maintains the organizational structure, routes messages,
        handles escalations, and provides organizational queries.
        """
        
        def __init__(self):
            """Initialize hierarchical organization."""
            self.agents: Dict[str, AgentNode] = {}
            self.root: Optional[str] = None
            self.llm = ChatOpenAI(temperature=0.3, model="gpt-3.5-turbo")
            self.message_log: List[HierarchicalMessage] = []
        
        def add_agent(
            self,
            agent_id: str,
            level: AgentLevel,
            superior: Optional[str] = None,
            capabilities: List[str] = None
        ):
            """
            Add agent to organization.
            
            Args:
                agent_id: Agent identifier
                level: Hierarchical level
                superior: Superior agent (None for root)
                capabilities: Agent capabilities
            """
            node = AgentNode(
                agent_id=agent_id,
                level=level,
                superior=superior,
                capabilities=capabilities or []
            )
            
            self.agents[agent_id] = node
            
            if superior is None:
                self.root = agent_id
            else:
                if superior in self.agents:
                    self.agents[superior].subordinates.append(agent_id)
        
        def get_reporting_chain(self, agent_id: str) -> List[str]:
            """
            Get chain of command from agent to root.
            
            Args:
                agent_id: Starting agent
                
            Returns:
                List of agent IDs in reporting chain
            """
            chain = []
            current = agent_id
            
            while current:
                chain.append(current)
                node = self.agents.get(current)
                if not node or not node.superior:
                    break
                current = node.superior
            
            return chain
        
        def get_subordinate_tree(self, agent_id: str) -> List[str]:
            """
            Get all subordinates in tree.
            
            Args:
                agent_id: Root agent
                
            Returns:
                List of all subordinate agent IDs
            """
            subordinates = []
            node = self.agents.get(agent_id)
            
            if not node:
                return subordinates
            
            for sub_id in node.subordinates:
                subordinates.append(sub_id)
                subordinates.extend(self.get_subordinate_tree(sub_id))
            
            return subordinates
        
        def route_message(self, message: HierarchicalMessage) -> bool:
            """
            Route message through hierarchy.
            
            Args:
                message: Message to route
                
            Returns:
                True if routing successful
            """
            self.message_log.append(message)
            
            # Validate sender exists
            if message.sender not in self.agents:
                return False
            
            # Validate recipients exist
            for recipient in message.recipients:
                if recipient not in self.agents:
                    return False
            
            # Validate direction
            sender_node = self.agents[message.sender]
            
            if message.direction == MessageDirection.DOWN:
                # Must send to subordinates
                for recipient in message.recipients:
                    if recipient not in sender_node.subordinates:
                        return False
            
            elif message.direction == MessageDirection.UP:
                # Must send to superior
                if message.recipients and message.recipients[0] != sender_node.superior:
                    return False
            
            return True
        
        def aggregate_reports(
            self,
            manager_id: str
        ) -> Dict[str, Any]:
            """
            Aggregate reports from subordinates.
            
            Args:
                manager_id: Manager agent ID
                
            Returns:
                Aggregated report data
            """
            manager_node = self.agents.get(manager_id)
            if not manager_node:
                return {}
            
            # Collect reports from subordinates
            reports = []
            for msg in self.message_log[-50:]:  # Last 50 messages
                if (msg.message_type == MessageType.REPORT and
                    msg.sender in manager_node.subordinates and
                    manager_id in msg.recipients):
                    reports.append(msg.content)
            
            # Aggregate statistics
            total_completed = sum(r.get("tasks_completed", 0) for r in reports)
            total_active = sum(r.get("tasks_active", 0) for r in reports)
            
            return {
                "subordinate_count": len(manager_node.subordinates),
                "reports_received": len(reports),
                "total_tasks_completed": total_completed,
                "total_tasks_active": total_active,
                "subordinate_status": {
                    r.get("status", "unknown"): r.get("status", "unknown")
                    for r in reports
                }
            }
        
        def visualize_hierarchy(self) -> str:
            """
            Create text visualization of hierarchy.
            
            Returns:
                Tree structure as text
            """
            if not self.root:
                return "Empty hierarchy"
            
            def build_tree(agent_id: str, level: int = 0) -> str:
                node = self.agents.get(agent_id)
                if not node:
                    return ""
                
                indent = "  " * level
                tree = f"{indent}├─ {agent_id} ({node.level.value})\n"
                
                for sub_id in node.subordinates:
                    tree += build_tree(sub_id, level + 1)
                
                return tree
            
            root_node = self.agents[self.root]
            tree = f"{self.root} ({root_node.level.value})\n"
            
            for sub_id in root_node.subordinates:
                tree += build_tree(sub_id, 1)
            
            return tree
    
    
    def demonstrate_hierarchical_communication():
        """Demonstrate hierarchical communication pattern."""
        
        print("=" * 80)
        print("HIERARCHICAL COMMUNICATION PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Basic hierarchical structure
        print("\n" + "=" * 80)
        print("Example 1: Three-Level Organizational Hierarchy")
        print("=" * 80)
        
        # Create organization
        org = HierarchicalOrganization()
        
        # Add executive
        org.add_agent(
            "CEO",
            AgentLevel.EXECUTIVE,
            capabilities=["strategy", "resource_allocation"]
        )
        
        # Add managers
        org.add_agent(
            "Engineering_Manager",
            AgentLevel.MANAGER,
            superior="CEO",
            capabilities=["technical_oversight", "team_management"]
        )
        
        org.add_agent(
            "Operations_Manager",
            AgentLevel.MANAGER,
            superior="CEO",
            capabilities=["operations", "logistics"]
        )
        
        # Add workers
        org.add_agent(
            "Engineer_1",
            AgentLevel.WORKER,
            superior="Engineering_Manager",
            capabilities=["coding", "testing"]
        )
        
        org.add_agent(
            "Engineer_2",
            AgentLevel.WORKER,
            superior="Engineering_Manager",
            capabilities=["coding", "deployment"]
        )
        
        org.add_agent(
            "Operations_1",
            AgentLevel.WORKER,
            superior="Operations_Manager",
            capabilities=["monitoring", "maintenance"]
        )
        
        print("\nOrganizational Structure:")
        print(org.visualize_hierarchy())
        
        # Create agent instances
        ceo = HierarchicalAgent("CEO", AgentLevel.EXECUTIVE, ["strategy"])
        eng_mgr = HierarchicalAgent("Engineering_Manager", AgentLevel.MANAGER, ["technical"])
        eng_mgr.superior = "CEO"
        eng_mgr.subordinates = ["Engineer_1", "Engineer_2"]
        
        engineer1 = HierarchicalAgent("Engineer_1", AgentLevel.WORKER, ["coding"])
        engineer1.superior = "Engineering_Manager"
        
        # Example 2: Top-down communication (delegation)
        print("\n" + "=" * 80)
        print("Example 2: Top-Down Communication (Task Delegation)")
        print("=" * 80)
        
        print("\nCEO delegates to Engineering Manager:")
        task = {
            "description": "Implement new authentication system",
            "priority": 4,
            "deadline": "2 weeks"
        }
        
        delegation_msg = ceo.delegate_task(task, "Engineering_Manager")
        org.route_message(delegation_msg)
        
        print(f"  From: {delegation_msg.sender}")
        print(f"  To: {delegation_msg.recipients}")
        print(f"  Type: {delegation_msg.message_type.value}")
        print(f"  Priority: {delegation_msg.priority}")
        print(f"  Instructions: {delegation_msg.content['instructions']}")
        
        print("\nEngineering Manager delegates to Engineer:")
        sub_task = {
            "description": "Design authentication API",
            "priority": 4,
            "deadline": "1 week"
        }
        
        sub_delegation = eng_mgr.delegate_task(sub_task, "Engineer_1")
        org.route_message(sub_delegation)
        
        print(f"  From: {sub_delegation.sender}")
        print(f"  To: {sub_delegation.recipients}")
        print(f"  Instructions: {sub_delegation.content['instructions']}")
        
        # Example 3: Bottom-up communication (reporting)
        print("\n" + "=" * 80)
        print("Example 3: Bottom-Up Communication (Status Reporting)")
        print("=" * 80)
        
        # Worker reports to manager
        engineer1.tasks = [
            {"task": "API design", "completed": True},
            {"task": "Unit tests", "completed": False}
        ]
        
        print("\nEngineer reports to Engineering Manager:")
        status_msg = engineer1.report_status(
            status="In Progress",
            details={
                "completed_work": "API design completed",
                "current_work": "Writing unit tests",
                "blockers": "None"
            }
        )
        org.route_message(status_msg)
        
        print(f"  From: {status_msg.sender}")
        print(f"  To: {status_msg.recipients}")
        print(f"  Status: {status_msg.content['status']}")
        print(f"  Report: {status_msg.content['report']}")
        print(f"  Tasks Completed: {status_msg.content['tasks_completed']}")
        print(f"  Tasks Active: {status_msg.content['tasks_active']}")
        
        # Manager aggregates and reports up
        eng_mgr.tasks = [
            {"task": "Auth system", "completed": False},
            {"task": "Code review", "completed": True}
        ]
        
        print("\nEngineering Manager reports to CEO:")
        mgr_status = eng_mgr.report_status(
            status="On Track",
            details={
                "team_progress": "Authentication system 40% complete",
                "resources": "Team fully allocated",
                "risks": "No major risks identified"
            }
        )
        org.route_message(mgr_status)
        
        print(f"  From: {mgr_status.sender}")
        print(f"  To: {mgr_status.recipients}")
        print(f"  Report: {mgr_status.content['report']}")
        
        # Example 4: Escalation
        print("\n" + "=" * 80)
        print("Example 4: Issue Escalation")
        print("=" * 80)
        
        print("\nEngineer escalates blocking issue:")
        escalation = engineer1.escalate_issue(
            issue="Critical security vulnerability discovered in dependency",
            severity=4,
            context={
                "dependency": "auth-library v2.1",
                "cve": "CVE-2024-1234",
                "impact": "All users affected"
            }
        )
        org.route_message(escalation)
        
        print(f"  From: {escalation.sender}")
        print(f"  To: {escalation.recipients}")
        print(f"  Severity: {escalation.content['severity']}/5")
        print(f"  Issue: {escalation.content['issue']}")
        print(f"  Escalation: {escalation.content['escalation']}")
        print(f"  Priority: {escalation.priority}")
        
        # Manager can escalate further
        print("\nEngineering Manager escalates to CEO:")
        further_escalation = eng_mgr.escalate_issue(
            issue="Critical security vulnerability requires immediate action",
            severity=5,
            context={
                "original_reporter": "Engineer_1",
                "required_action": "Emergency patch deployment",
                "estimated_downtime": "2 hours"
            }
        )
        org.route_message(further_escalation)
        
        print(f"  From: {further_escalation.sender}")
        print(f"  To: {further_escalation.recipients}")
        print(f"  Severity: {further_escalation.content['severity']}/5")
        print(f"  Escalation: {further_escalation.content['escalation']}")
        
        # Example 5: Broadcast communication
        print("\n" + "=" * 80)
        print("Example 5: Broadcast Communication")
        print("=" * 80)
        
        print("\nCEO broadcasts announcement to all managers:")
        announcement = ceo.broadcast_announcement(
            "Company-wide security review initiated. All projects on hold for assessment."
        )
        # Manually set recipients for demo
        announcement.recipients = ["Engineering_Manager", "Operations_Manager"]
        org.route_message(announcement)
        
        print(f"  From: {announcement.sender}")
        print(f"  To: {', '.join(announcement.recipients)}")
        print(f"  Announcement: {announcement.content['announcement']}")
        
        print("\nEngineering Manager broadcasts to team:")
        team_announcement = eng_mgr.broadcast_announcement(
            "Security review starting today. Please document all dependencies."
        )
        team_announcement.recipients = ["Engineer_1", "Engineer_2"]
        org.route_message(team_announcement)
        
        print(f"  From: {team_announcement.sender}")
        print(f"  To: {', '.join(team_announcement.recipients)}")
        print(f"  Announcement: {team_announcement.content['announcement']}")
        
        # Example 6: Report aggregation
        print("\n" + "=" * 80)
        print("Example 6: Report Aggregation")
        print("=" * 80)
        
        print("\nEngineering Manager aggregates team reports:")
        aggregated = org.aggregate_reports("Engineering_Manager")
        
        print(f"  Subordinates: {aggregated['subordinate_count']}")
        print(f"  Reports Received: {aggregated['reports_received']}")
        print(f"  Total Tasks Completed: {aggregated['total_tasks_completed']}")
        print(f"  Total Tasks Active: {aggregated['total_tasks_active']}")
        
        # Example 7: Organizational queries
        print("\n" + "=" * 80)
        print("Example 7: Organizational Queries")
        print("=" * 80)
        
        print("\nReporting chain for Engineer_1:")
        chain = org.get_reporting_chain("Engineer_1")
        print(f"  {' → '.join(chain)}")
        
        print("\nAll subordinates under CEO:")
        subordinates = org.get_subordinate_tree("CEO")
        print(f"  Total: {len(subordinates)} agents")
        print(f"  {', '.join(subordinates)}")
        
        print("\nAll subordinates under Engineering_Manager:")
        eng_subs = org.get_subordinate_tree("Engineering_Manager")
        print(f"  Total: {len(eng_subs)} agents")
        print(f"  {', '.join(eng_subs)}")
        
        # Communication statistics
        print("\n" + "=" * 80)
        print("Communication Statistics")
        print("=" * 80)
        
        msg_types = defaultdict(int)
        msg_directions = defaultdict(int)
        
        for msg in org.message_log:
            msg_types[msg.message_type.value] += 1
            msg_directions[msg.direction.value] += 1
        
        print("\nMessage Types:")
        for msg_type, count in sorted(msg_types.items()):
            print(f"  {msg_type}: {count}")
        
        print("\nMessage Directions:")
        for direction, count in sorted(msg_directions.items()):
            print(f"  {direction}: {count}")
        
        print(f"\nTotal Messages: {len(org.message_log)}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY: Hierarchical Communication Pattern")
        print("=" * 80)
        
        summary = """
        The Hierarchical Communication pattern demonstrated:
        
        1. ORGANIZATIONAL STRUCTURE (Example 1):
           - Three-level hierarchy: Executive → Managers → Workers
           - Clear reporting lines and spans of control
           - Tree-based organizational model
           - Visualization of hierarchical relationships
        
        2. TOP-DOWN COMMUNICATION (Example 2):
           - Task delegation from superiors to subordinates
           - Multi-level delegation cascade
           - Clear instructions and expectations
           - Priority and deadline tracking
        
        3. BOTTOM-UP COMMUNICATION (Example 3):
           - Status reporting from workers to managers
           - Report aggregation at manager level
           - Consolidated reporting to executives
           - Progress and metrics tracking
        
        4. ESCALATION MECHANISM (Example 4):
           - Issue escalation through reporting chain
           - Severity-based prioritization
           - Multi-level escalation for critical issues
           - Context preservation through escalation
        
        5. BROADCAST COMMUNICATION (Example 5):
           - Top-down announcements to entire subtrees
           - Manager-to-team broadcasts
           - Efficient one-to-many communication
           - Cascading announcements through levels
        
        6. REPORT AGGREGATION (Example 6):
           - Automated aggregation of subordinate reports
           - Statistical summaries for managers
           - Workload and progress visibility
           - Data-driven decision support
        
        7. ORGANIZATIONAL QUERIES (Example 7):
           - Reporting chain traversal
           - Subordinate tree enumeration
           - Span of control analysis
           - Organizational structure navigation
        
        KEY BENEFITS:
        ✓ Scalable communication structure
        ✓ Clear authority and accountability
        ✓ Efficient information flow
        ✓ Reduced communication overhead
        ✓ Organized task delegation
        ✓ Systematic escalation paths
        ✓ Information aggregation at each level
        ✓ Traceable decision-making
        
        USE CASES:
        • Large-scale multi-agent systems
        • Enterprise workflow automation
        • Military/command structures
        • Corporate organizational modeling
        • Distributed task management
        • Crisis management systems
        • Project management hierarchies
        • Robotic swarm coordination
        
        BEST PRACTICES:
        1. Keep span of control reasonable (3-10 subordinates)
        2. Define clear roles and responsibilities
        3. Establish escalation thresholds
        4. Aggregate reports at each level
        5. Allow emergency bypass when needed
        6. Log all communications for audit
        7. Monitor for bottlenecks at key levels
        8. Support dynamic reorganization
        
        TRADE-OFFS:
        • Communication speed vs. organizational control
        • Hierarchy depth vs. span of control
        • Autonomy vs. oversight
        • Flexibility vs. structure
        
        PRODUCTION CONSIDERATIONS:
        → Design appropriate organizational depth
        → Implement timeout handling for non-responsive nodes
        → Support redundancy for critical positions
        → Monitor communication bottlenecks
        → Log all messages for compliance and audit
        → Handle node failures gracefully
        → Support emergency communication channels
        → Balance delegation with oversight
        → Implement report aggregation efficiently
        → Consider matrix structures for complex needs
        
        This pattern enables scalable, organized communication in large
        multi-agent systems through clear hierarchical structure, defined
        communication channels, and systematic information flow.
        """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_hierarchical_communication()
    
        

    Agentic Design Pattern: Hierarchical Communication

    Pattern 100: Hierarchical Communication – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 100: Hierarchical Communication

    Description: The Hierarchical Communication pattern establishes structured communication channels between agents organized in a hierarchical structure. This pattern is essential for large-scale multi-agent systems where flat communication would be inefficient or chaotic. It defines clear lines of authority, responsibility, and communication flow.

    Hierarchical structures enable scalable coordination by organizing agents into levels:
    executives/managers at higher levels provide strategic direction and oversight, while
    workers at lower levels handle operational tasks. Communication flows both top-down
    (commands, directives, goals) and bottom-up (reports, escalations, feedback).
    
    This pattern supports various organizational structures including strict hierarchies,
    matrix organizations, and hybrid models. It handles delegation, escalation, reporting,
    and cross-level communication while maintaining clear accountability and reducing
    communication overhead through proper routing and filtering.
    

    Key Components: 1. Hierarchical Levels: Organization into layers (executive, manager, worker) 2. Authority Structure: Who can command whom 3. Communication Channels: Allowed communication paths 4. Delegation: Task assignment down the hierarchy 5. Escalation: Problem raising up the hierarchy 6. Reporting: Status updates to superiors 7. Coordination: Cross-branch communication 8. Information Filtering: Aggregation at each level

    Organizational Structures: 1. Strict Hierarchy: Tree structure, single reporting line 2. Matrix Organization: Multiple reporting lines 3. Flat Hierarchy: Few levels, wide spans 4. Deep Hierarchy: Many levels, narrow spans 5. Hybrid: Mix of hierarchical and flat structures

    Communication Types: 1. Top-Down: Commands, goals, policies from above 2. Bottom-Up: Reports, feedback, escalations from below 3. Lateral: Peer-to-peer at same level 4. Cross-Level: Skip-level communication (exceptional) 5. Broadcast: Announcements to entire subtree

    Use Cases: – Large-scale multi-agent systems – Enterprise workflow automation – Military/command structures – Corporate organizational modeling – Distributed task management – Crisis management systems – Project management hierarchies – Robotic swarm coordination

    Advantages: – Scalable communication structure – Clear lines of authority – Reduced communication overhead – Organized task delegation – Efficient escalation paths – Information aggregation – Accountability tracking – Span of control management

    Challenges: – Communication delays through levels – Information loss in aggregation – Rigidity in dynamic environments – Single points of failure – Bureaucratic overhead – Potential bottlenecks at higher levels – Difficulty with cross-functional needs

    LangChain Implementation: This implementation uses LangChain for: – LLM-based delegation and task breakdown – Status report generation and summarization – Escalation decision-making – Command interpretation – Cross-level communication reasoning

    Production Considerations: – Design appropriate span of control (3-10 subordinates typical) – Implement timeout handling for non-responsive nodes – Log all communications for audit trails – Balance hierarchy depth vs. communication overhead – Support emergency bypass channels – Monitor for bottlenecks at key levels – Implement redundancy for critical nodes – Handle dynamic reorganization gracefully


    File Statistics

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

    Module Documentation

    """
    Pattern 100: Hierarchical Communication
    
    Description:
        The Hierarchical Communication pattern establishes structured communication channels
        between agents organized in a hierarchical structure. This pattern is essential for
        large-scale multi-agent systems where flat communication would be inefficient or
        chaotic. It defines clear lines of authority, responsibility, and communication flow.
    
        Hierarchical structures enable scalable coordination by organizing agents into levels:
        executives/managers at higher levels provide strategic direction and oversight, while
        workers at lower levels handle operational tasks. Communication flows both top-down
        (commands, directives, goals) and bottom-up (reports, escalations, feedback).
    
        This pattern supports various organizational structures including strict hierarchies,
        matrix organizations, and hybrid models. It handles delegation, escalation, reporting,
        and cross-level communication while maintaining clear accountability and reducing
        communication overhead through proper routing and filtering.
    
    Key Components:
        1. Hierarchical Levels: Organization into layers (executive, manager, worker)
        2. Authority Structure: Who can command whom
        3. Communication Channels: Allowed communication paths
        4. Delegation: Task assignment down the hierarchy
        5. Escalation: Problem raising up the hierarchy
        6. Reporting: Status updates to superiors
        7. Coordination: Cross-branch communication
        8. Information Filtering: Aggregation at each level
    
    Organizational Structures:
        1. Strict Hierarchy: Tree structure, single reporting line
        2. Matrix Organization: Multiple reporting lines
        3. Flat Hierarchy: Few levels, wide spans
        4. Deep Hierarchy: Many levels, narrow spans
        5. Hybrid: Mix of hierarchical and flat structures
    
    Communication Types:
        1. Top-Down: Commands, goals, policies from above
        2. Bottom-Up: Reports, feedback, escalations from below
        3. Lateral: Peer-to-peer at same level
        4. Cross-Level: Skip-level communication (exceptional)
        5. Broadcast: Announcements to entire subtree
    
    Use Cases:
        - Large-scale multi-agent systems
        - Enterprise workflow automation
        - Military/command structures
        - Corporate organizational modeling
        - Distributed task management
        - Crisis management systems
        - Project management hierarchies
        - Robotic swarm coordination
    
    Advantages:
        - Scalable communication structure
        - Clear lines of authority
        - Reduced communication overhead
        - Organized task delegation
        - Efficient escalation paths
        - Information aggregation
        - Accountability tracking
        - Span of control management
    
    Challenges:
        - Communication delays through levels
        - Information loss in aggregation
        - Rigidity in dynamic environments
        - Single points of failure
        - Bureaucratic overhead
        - Potential bottlenecks at higher levels
        - Difficulty with cross-functional needs
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based delegation and task breakdown
        - Status report generation and summarization
        - Escalation decision-making
        - Command interpretation
        - Cross-level communication reasoning
    
    Production Considerations:
        - Design appropriate span of control (3-10 subordinates typical)
        - Implement timeout handling for non-responsive nodes
        - Log all communications for audit trails
        - Balance hierarchy depth vs. communication overhead
        - Support emergency bypass channels
        - Monitor for bottlenecks at key levels
        - Implement redundancy for critical nodes
        - Handle dynamic reorganization gracefully
    """
    

    Purpose: This module implements the Hierarchical Communication 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 typing.Any
    import typing.Dict
    import typing.List
    import typing.Optional
    import typing.Set
    

    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 – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Set: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: AgentLevel

    Lines 107-111

    Inherits from: Enum

    Purpose:

    Hierarchical level of an agent.
    

    Class: MessageDirection

    Lines 114-119

    Inherits from: Enum

    Purpose:

    Direction of communication in hierarchy.
    

    Class: MessageType

    Lines 122-130

    Inherits from: Enum

    Purpose:

    Type of hierarchical message.
    

    Class: HierarchicalMessage

    Lines 134-161

    Purpose:

    Message in hierarchical communication system.
    
    Attributes:
        message_id: Unique identifier
        sender: Sending agent
        recipients: List of recipient agents
        message_type: Type of message
        direction: Communication direction
        content: Message content
        priority: Priority level (1-5, 5 highest)
        timestamp: When message was sent
        requires_response: Whether response is expected
        parent_message_id: Reference to parent message
        metadata: Additional information
    

    Class: AgentNode

    Lines 165-184

    Purpose:

    Node in hierarchical agent structure.
    
    Attributes:
        agent_id: Unique identifier
        level: Hierarchical level
        superior: Agent's supervisor (None for root)
        subordinates: List of direct reports
        capabilities: Agent's capabilities
        current_tasks: Active tasks
        metadata: Additional information
    

    Class: HierarchicalAgent

    Lines 187-402

    Purpose:

    Agent that operates within hierarchical communication structure.
    
    This agent can send commands to subordinates, report to superiors,
    escalate issues, delegate tasks, and communicate with peers.
    

    Methods:

    __init__(self, agent_id, level, capabilities, temperature)

    Lines 195-219

    Purpose:

    Initialize hierarchical agent.
    
    Args:
        agent_id: Agent's unique identifier
        level: Hierarchical level
        capabilities: List of capabilities
        temperature: LLM temperature
    

    Parameters: – agent_id: Agent id – level: Level – capabilities: Capabilities – temperature: Sampling temperature (0-1)

    delegate_task(self, task, subordinate_id)

    Lines 221-269

    Purpose:

    Delegate task to subordinate.
    
    Args:
        task: Task to delegate
        subordinate_id: ID of subordinate to delegate to
    
    Returns:
        Delegation message
    

    Parameters: – task: Task – subordinate_id: Subordinate id

    Returns: HierarchicalMessage

    report_status(self, status, details)

    Lines 271-320

    Purpose:

    Report status to superior.
    
    Args:
        status: Status summary
        details: Detailed information
    
    Returns:
        Status report message
    

    Parameters: – status: Status – details: Details

    Returns: HierarchicalMessage

    escalate_issue(self, issue, severity, context)

    Lines 322-373

    Purpose:

    Escalate issue to superior.
    
    Args:
        issue: Issue description
        severity: Severity level (1-5)
        context: Additional context
    
    Returns:
        Escalation message
    

    Parameters: – issue: Issue – severity: Severity – context: Context

    Returns: HierarchicalMessage

    broadcast_announcement(self, announcement)

    Lines 375-402

    Purpose:

    Broadcast announcement to all subordinates.
    
    Args:
        announcement: Announcement text
    
    Returns:
        Broadcast message
    

    Parameters: – announcement: Announcement

    Returns: HierarchicalMessage


    Class: HierarchicalOrganization

    Lines 405-601

    Purpose:

    Manages hierarchical organization of agents.
    
    This class maintains the organizational structure, routes messages,
    handles escalations, and provides organizational queries.
    

    Methods:

    __init__(self)

    Lines 413-418

    Purpose:

    Initialize hierarchical organization.
    

    Parameters:

    add_agent(self, agent_id, level, superior, capabilities)

    Lines 420-449

    Purpose:

    Add agent to organization.
    
    Args:
        agent_id: Agent identifier
        level: Hierarchical level
        superior: Superior agent (None for root)
        capabilities: Agent capabilities
    

    Parameters: – agent_id: Agent id – level: Level – superior: Superior – capabilities: Capabilities

    get_reporting_chain(self, agent_id)

    Lines 451-471

    Purpose:

    Get chain of command from agent to root.
    
    Args:
        agent_id: Starting agent
    
    Returns:
        List of agent IDs in reporting chain
    

    Parameters: – agent_id: Agent id

    Returns: <ast.Subscript object at 0x000001A06D83F690>

    get_subordinate_tree(self, agent_id)

    Lines 473-493

    Purpose:

    Get all subordinates in tree.
    
    Args:
        agent_id: Root agent
    
    Returns:
        List of all subordinate agent IDs
    

    Parameters: – agent_id: Agent id

    Returns: <ast.Subscript object at 0x000001A06D83E190>

    route_message(self, message)

    Lines 495-530

    Purpose:

    Route message through hierarchy.
    
    Args:
        message: Message to route
    
    Returns:
        True if routing successful
    

    Parameters: – message: Message

    Returns: bool

    aggregate_reports(self, manager_id)

    Lines 532-570

    Purpose:

    Aggregate reports from subordinates.
    
    Args:
        manager_id: Manager agent ID
    
    Returns:
        Aggregated report data
    

    Parameters: – manager_id: Manager id

    Returns: <ast.Subscript object at 0x000001A06D88BA10>

    visualize_hierarchy(self)

    Lines 572-601

    Purpose:

    Create text visualization of hierarchy.
    
    Returns:
        Tree structure as text
    

    Parameters:

    Returns: str


    Functions

    demonstrate_hierarchical_communication()

    Lines 604-987

    Purpose:

    Demonstrate hierarchical communication pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Hierarchical Communication 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 099
    • Pattern 101

    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 Hierarchical Communication 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: 100_hierarchical_communication.py
    Total Lines: 992
    Pattern Category: AI Agent Design Pattern #100


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Negotiation Protocol

    
        """
    Pattern 099: Negotiation Protocol
    
    Description:
        The Negotiation Protocol pattern enables multiple agents to reach agreements through
        structured negotiation processes. This pattern is essential for multi-agent systems
        where agents have different goals, preferences, or constraints and need to find
        mutually acceptable solutions through iterative proposal and counter-proposal exchanges.
        
        Negotiation is a fundamental aspect of multi-agent coordination, allowing agents to
        resolve conflicts, allocate resources, coordinate actions, and make collective decisions.
        This pattern provides mechanisms for proposal generation, evaluation, counter-offers,
        concession strategies, and agreement finalization.
        
        The pattern supports various negotiation strategies including competitive (zero-sum),
        cooperative (win-win), and mixed approaches. It handles single-issue and multi-issue
        negotiations, bilateral and multilateral negotiations, and includes mechanisms for
        handling deadlocks and timeouts.
    
    Key Components:
        1. Proposal: Structured offer from one agent to others
        2. Counter-Proposal: Modified proposal in response to original
        3. Utility Function: Evaluates value of proposals for agent
        4. Concession Strategy: Determines how much to compromise
        5. Negotiation Protocol: Rules governing negotiation process
        6. Agreement: Final mutually accepted terms
        7. Mediation: Optional third-party facilitation
    
    Negotiation Types:
        1. Distributive: Fixed resources, competitive
        2. Integrative: Value creation, cooperative
        3. Multi-issue: Multiple attributes to negotiate
        4. Sequential: Issues addressed one at a time
        5. Package: All issues negotiated together
        
    Strategies:
        1. Conceding: Gradually reduce demands
        2. Hardball: Maintain firm position
        3. Tit-for-tat: Mirror opponent's behavior
        4. Time-based: Adjust based on deadline
        5. Information-based: Learn and adapt
    
    Use Cases:
        - Resource allocation in multi-agent systems
        - Task assignment and coordination
        - Conflict resolution between agents
        - Service level agreements
        - Coalition formation
        - Price negotiation in marketplaces
        - Scheduling and planning coordination
        - Policy consensus building
    
    Advantages:
        - Flexible conflict resolution
        - Fair resource distribution
        - Autonomous decision-making
        - Adaptable to agent preferences
        - Handles diverse objectives
        - Enables cooperation
        - Supports complex agreements
    
    Challenges:
        - Computational complexity
        - Strategic manipulation
        - Information asymmetry
        - Deadlock situations
        - Time constraints
        - Preference elicitation
        - Trust issues
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based proposal generation and evaluation
        - Utility calculation with reasoning
        - Counter-proposal generation
        - Agreement evaluation
        - Negotiation strategy implementation
        
    Production Considerations:
        - Set negotiation timeouts and round limits
        - Implement deadlock breaking mechanisms
        - Log all negotiation steps for transparency
        - Validate proposals against constraints
        - Handle malformed proposals gracefully
        - Monitor for strategic manipulation
        - Implement fair mediation when needed
        - Consider privacy of agent preferences
    """
    
    import os
    from typing import List, Dict, Any, Optional, 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 ProposalStatus(Enum):
        """Status of a negotiation proposal."""
        PENDING = "pending"
        ACCEPTED = "accepted"
        REJECTED = "rejected"
        COUNTER = "counter"
        WITHDRAWN = "withdrawn"
    
    
    class NegotiationOutcome(Enum):
        """Outcome of a negotiation."""
        AGREEMENT = "agreement"
        DEADLOCK = "deadlock"
        TIMEOUT = "timeout"
        WITHDRAWN = "withdrawn"
    
    
    @dataclass
    class Proposal:
        """
        Represents a proposal in a negotiation.
        
        Attributes:
            proposal_id: Unique identifier
            proposer: Agent making the proposal
            recipient: Agent(s) receiving proposal
            round_number: Negotiation round
            terms: Dictionary of proposed terms
            utility: Utility value for proposer
            status: Current status of proposal
            timestamp: When proposal was made
            justification: Reasoning for proposal
            metadata: Additional information
        """
        proposal_id: str
        proposer: str
        recipient: str
        round_number: int
        terms: Dict[str, Any]
        utility: float
        status: ProposalStatus
        timestamp: datetime
        justification: str = ""
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    @dataclass
    class NegotiationState:
        """
        Tracks the state of an ongoing negotiation.
        
        Attributes:
            negotiation_id: Unique identifier
            participants: Agents involved
            issue: What is being negotiated
            proposals: History of proposals
            current_round: Current round number
            max_rounds: Maximum allowed rounds
            start_time: When negotiation started
            end_time: When negotiation ended
            outcome: Final outcome
            agreement: Final agreed terms
            metadata: Additional information
        """
        negotiation_id: str
        participants: List[str]
        issue: str
        proposals: List[Proposal] = field(default_factory=list)
        current_round: int = 1
        max_rounds: int = 10
        start_time: datetime = field(default_factory=datetime.now)
        end_time: Optional[datetime] = None
        outcome: Optional[NegotiationOutcome] = None
        agreement: Optional[Dict[str, Any]] = None
        metadata: Dict[str, Any] = field(default_factory=dict)
    
    
    class NegotiationAgent:
        """
        Agent that participates in negotiations.
        
        This agent can generate proposals, evaluate offers, make counter-proposals,
        and decide when to accept or reject proposals based on its utility function
        and negotiation strategy.
        """
        
        def __init__(
            self,
            name: str,
            preferences: Dict[str, Any],
            reservation_utility: float = 0.3,
            concession_rate: float = 0.1,
            temperature: float = 0.5
        ):
            """
            Initialize negotiation agent.
            
            Args:
                name: Agent's name
                preferences: Agent's preferences for negotiation issues
                reservation_utility: Minimum acceptable utility (walk-away point)
                concession_rate: How quickly agent makes concessions
                temperature: LLM temperature for generation
            """
            self.name = name
            self.preferences = preferences
            self.reservation_utility = reservation_utility
            self.concession_rate = concession_rate
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.negotiation_history: List[Proposal] = []
        
        def calculate_utility(self, terms: Dict[str, Any]) -> float:
            """
            Calculate utility of proposed terms.
            
            Args:
                terms: Proposed terms
                
            Returns:
                Utility value between 0 and 1
            """
            # Simple weighted sum utility calculation
            total_utility = 0.0
            total_weight = 0.0
            
            for key, value in terms.items():
                if key in self.preferences:
                    pref = self.preferences[key]
                    weight = pref.get("weight", 1.0)
                    ideal = pref.get("ideal", value)
                    minimum = pref.get("minimum", 0)
                    
                    # Normalize value
                    if ideal != minimum:
                        normalized = (value - minimum) / (ideal - minimum)
                        normalized = max(0.0, min(1.0, normalized))
                    else:
                        normalized = 1.0 if value == ideal else 0.0
                    
                    total_utility += weight * normalized
                    total_weight += weight
            
            return total_utility / total_weight if total_weight > 0 else 0.0
        
        def generate_initial_proposal(
            self,
            issue: str,
            recipient: str,
            round_number: int = 1
        ) -> Proposal:
            """
            Generate initial proposal.
            
            Args:
                issue: Issue being negotiated
                recipient: Recipient agent
                round_number: Round number
                
            Returns:
                Initial proposal
            """
            # Start with terms close to ideal preferences
            terms = {}
            for key, pref in self.preferences.items():
                # Start at 90% of ideal for initial proposal
                ideal = pref.get("ideal", 100)
                minimum = pref.get("minimum", 0)
                initial_value = ideal * 0.9 + minimum * 0.1
                terms[key] = initial_value
            
            utility = self.calculate_utility(terms)
            
            # Generate justification using LLM
            prompt = ChatPromptTemplate.from_template(
                "You are {agent_name} negotiating about {issue}. "
                "You propose the following terms: {terms}. "
                "Write a brief 2-3 sentence justification for this proposal, "
                "emphasizing why it's fair and reasonable.\n\n"
                "Justification:"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            justification = chain.invoke({
                "agent_name": self.name,
                "issue": issue,
                "terms": terms
            })
            
            proposal = Proposal(
                proposal_id=f"{self.name}_r{round_number}",
                proposer=self.name,
                recipient=recipient,
                round_number=round_number,
                terms=terms,
                utility=utility,
                status=ProposalStatus.PENDING,
                timestamp=datetime.now(),
                justification=justification.strip()
            )
            
            self.negotiation_history.append(proposal)
            return proposal
        
        def evaluate_proposal(
            self,
            proposal: Proposal,
            current_round: int
        ) -> Tuple[bool, str]:
            """
            Evaluate whether to accept a proposal.
            
            Args:
                proposal: Proposal to evaluate
                current_round: Current negotiation round
                
            Returns:
                Tuple of (accept: bool, reasoning: str)
            """
            utility = self.calculate_utility(proposal.terms)
            
            # Accept if utility meets threshold
            # Threshold decreases over rounds (concession)
            round_factor = 1.0 - (current_round * self.concession_rate)
            threshold = max(
                self.reservation_utility,
                self.reservation_utility + (1.0 - self.reservation_utility) * round_factor
            )
            
            accept = utility >= threshold
            
            # Generate reasoning using LLM
            prompt = ChatPromptTemplate.from_template(
                "You are {agent_name} evaluating a proposal about {issue}. "
                "The proposed terms are: {terms}. "
                "The utility you would gain is {utility:.2f} "
                "(your minimum acceptable is {threshold:.2f}). "
                "Decision: {'ACCEPT' if accept else 'REJECT'}. "
                "Write a brief 2-3 sentence explanation for this decision.\n\n"
                "Explanation:"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            reasoning = chain.invoke({
                "agent_name": self.name,
                "issue": proposal.metadata.get("issue", "the issue"),
                "terms": proposal.terms,
                "utility": utility,
                "threshold": threshold,
                "accept": accept
            })
            
            return accept, reasoning.strip()
        
        def generate_counter_proposal(
            self,
            previous_proposal: Proposal,
            issue: str,
            round_number: int
        ) -> Proposal:
            """
            Generate counter-proposal.
            
            Args:
                previous_proposal: Proposal to counter
                issue: Issue being negotiated
                round_number: Current round number
                
            Returns:
                Counter-proposal
            """
            # Make concessions based on previous proposal and round number
            terms = {}
            for key, pref in self.preferences.items():
                ideal = pref.get("ideal", 100)
                minimum = pref.get("minimum", 0)
                previous_value = previous_proposal.terms.get(key, minimum)
                
                # Move toward middle ground with concession
                concession = self.concession_rate * round_number
                my_target = ideal * (1 - concession) + minimum * concession
                counter_value = (my_target + previous_value) / 2
                
                # Ensure within bounds
                counter_value = max(minimum, min(ideal, counter_value))
                terms[key] = counter_value
            
            utility = self.calculate_utility(terms)
            
            # Generate justification using LLM
            prompt = ChatPromptTemplate.from_template(
                "You are {agent_name} making a counter-proposal about {issue}. "
                "The previous proposal was: {previous_terms}. "
                "Your counter-proposal is: {terms}. "
                "Write a brief 2-3 sentence justification explaining "
                "how this is a reasonable compromise.\n\n"
                "Justification:"
            )
            
            chain = prompt | self.llm | StrOutputParser()
            justification = chain.invoke({
                "agent_name": self.name,
                "issue": issue,
                "previous_terms": previous_proposal.terms,
                "terms": terms
            })
            
            proposal = Proposal(
                proposal_id=f"{self.name}_r{round_number}_counter",
                proposer=self.name,
                recipient=previous_proposal.proposer,
                round_number=round_number,
                terms=terms,
                utility=utility,
                status=ProposalStatus.PENDING,
                timestamp=datetime.now(),
                justification=justification.strip(),
                metadata={"counter_to": previous_proposal.proposal_id}
            )
            
            self.negotiation_history.append(proposal)
            return proposal
    
    
    class NegotiationMediator:
        """
        Mediator that facilitates negotiation between agents.
        
        The mediator manages the negotiation protocol, enforces rules,
        tracks state, and can suggest compromises when negotiations stall.
        """
        
        def __init__(self, temperature: float = 0.3):
            """
            Initialize negotiation mediator.
            
            Args:
                temperature: LLM temperature for generation
            """
            self.llm = ChatOpenAI(temperature=temperature, model="gpt-3.5-turbo")
            self.negotiations: Dict[str, NegotiationState] = {}
        
        def start_negotiation(
            self,
            negotiation_id: str,
            participants: List[str],
            issue: str,
            max_rounds: int = 10
        ) -> NegotiationState:
            """
            Start a new negotiation.
            
            Args:
                negotiation_id: Unique identifier
                participants: List of participating agents
                issue: Issue being negotiated
                max_rounds: Maximum rounds allowed
                
            Returns:
                Initial negotiation state
            """
            state = NegotiationState(
                negotiation_id=negotiation_id,
                participants=participants,
                issue=issue,
                max_rounds=max_rounds
            )
            self.negotiations[negotiation_id] = state
            return state
        
        def process_round(
            self,
            negotiation_id: str,
            proposal: Proposal,
            response_accept: bool,
            response_reasoning: str,
            counter_proposal: Optional[Proposal] = None
        ) -> NegotiationState:
            """
            Process a negotiation round.
            
            Args:
                negotiation_id: Negotiation identifier
                proposal: Current proposal
                response_accept: Whether proposal was accepted
                response_reasoning: Reasoning for response
                counter_proposal: Counter-proposal if not accepted
                
            Returns:
                Updated negotiation state
            """
            state = self.negotiations[negotiation_id]
            
            # Add proposal to history
            proposal.metadata["response_reasoning"] = response_reasoning
            state.proposals.append(proposal)
            
            if response_accept:
                # Negotiation successful
                proposal.status = ProposalStatus.ACCEPTED
                state.outcome = NegotiationOutcome.AGREEMENT
                state.agreement = proposal.terms
                state.end_time = datetime.now()
            elif counter_proposal:
                # Counter-proposal made
                proposal.status = ProposalStatus.COUNTER
                state.proposals.append(counter_proposal)
                state.current_round += 1
            else:
                # Proposal rejected without counter
                proposal.status = ProposalStatus.REJECTED
                state.current_round += 1
            
            # Check for timeout
            if state.current_round > state.max_rounds and not state.outcome:
                state.outcome = NegotiationOutcome.TIMEOUT
                state.end_time = datetime.now()
            
            return state
        
        def suggest_compromise(
            self,
            negotiation_id: str
        ) -> Optional[Dict[str, Any]]:
            """
            Suggest compromise when negotiation stalls.
            
            Args:
                negotiation_id: Negotiation identifier
                
            Returns:
                Suggested compromise terms or None
            """
            state = self.negotiations.get(negotiation_id)
            if not state or len(state.proposals)  str:
            """
            Generate summary of negotiation.
            
            Args:
                negotiation_id: Negotiation identifier
                
            Returns:
                Summary text
            """
            state = self.negotiations.get(negotiation_id)
            if not state:
                return "Negotiation not found."
            
            summary = f"Negotiation: {state.issue}\n"
            summary += f"Participants: {', '.join(state.participants)}\n"
            summary += f"Rounds: {state.current_round}/{state.max_rounds}\n"
            summary += f"Outcome: {state.outcome.value if state.outcome else 'ongoing'}\n"
            
            if state.agreement:
                summary += f"\nAgreed Terms:\n"
                for key, value in state.agreement.items():
                    summary += f"  - {key}: {value}\n"
            
            summary += f"\nProposal History ({len(state.proposals)} proposals):\n"
            for i, prop in enumerate(state.proposals[-5:], 1):  # Last 5
                summary += f"  {i}. Round {prop.round_number}: "
                summary += f"{prop.proposer} -> {prop.recipient} "
                summary += f"[{prop.status.value}]\n"
            
            return summary
    
    
    def demonstrate_negotiation_protocol():
        """Demonstrate negotiation protocol pattern."""
        
        print("=" * 80)
        print("NEGOTIATION PROTOCOL PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Example 1: Simple bilateral negotiation
        print("\n" + "=" * 80)
        print("Example 1: Bilateral Price Negotiation")
        print("=" * 80)
        
        # Create buyer and seller agents
        buyer = NegotiationAgent(
            name="Buyer",
            preferences={
                "price": {"ideal": 100, "minimum": 100, "weight": 1.0},
                "delivery_time": {"ideal": 1, "minimum": 1, "weight": 0.5}
            },
            reservation_utility=0.4,
            concession_rate=0.08
        )
        
        seller = NegotiationAgent(
            name="Seller",
            preferences={
                "price": {"ideal": 150, "minimum": 120, "weight": 1.0},
                "delivery_time": {"ideal": 7, "minimum": 1, "weight": 0.3}
            },
            reservation_utility=0.3,
            concession_rate=0.1
        )
        
        mediator = NegotiationMediator()
        
        # Start negotiation
        issue = "Product Sale Agreement"
        state = mediator.start_negotiation(
            negotiation_id="negotiation_001",
            participants=["Buyer", "Seller"],
            issue=issue,
            max_rounds=8
        )
        
        print(f"\nStarting negotiation: {issue}")
        print(f"Participants: {', '.join(state.participants)}")
        print(f"Maximum rounds: {state.max_rounds}")
        
        # Negotiation rounds
        current_proposer = buyer
        current_recipient = seller
        current_proposal = buyer.generate_initial_proposal(issue, "Seller")
        
        print(f"\n{'Round':<8} {'Proposer':<12} {'Action':<15} {'Price':<10} {'Delivery':<10} {'Utility':<10}")
        print("-" * 80)
        
        while state.current_round <= state.max_rounds and not state.outcome:
            # Display proposal
            print(f"{state.current_round:<8} {current_proposal.proposer:<12} "
                  f"{'Proposes':<15} "
                  f"{current_proposal.terms.get('price', 0):<10.1f} "
                  f"{current_proposal.terms.get('delivery_time', 0):<10.1f} "
                  f"{current_proposal.utility:<10.2f}")
            
            # Recipient evaluates proposal
            accept, reasoning = current_recipient.evaluate_proposal(
                current_proposal,
                state.current_round
            )
            
            if accept:
                # Agreement reached
                state = mediator.process_round(
                    "negotiation_001",
                    current_proposal,
                    True,
                    reasoning
                )
                print(f"{'':<8} {current_recipient.name:<12} {'✓ ACCEPTS':<15}")
                break
            else:
                # Generate counter-proposal
                counter = current_recipient.generate_counter_proposal(
                    current_proposal,
                    issue,
                    state.current_round
                )
                
                print(f"{'':<8} {current_recipient.name:<12} {'✗ Rejects':<15}")
                
                state = mediator.process_round(
                    "negotiation_001",
                    current_proposal,
                    False,
                    reasoning,
                    counter
                )
                
                # Switch roles
                current_proposal = counter
                current_proposer, current_recipient = current_recipient, current_proposer
        
        # Print outcome
        print("\n" + "-" * 80)
        print(f"Outcome: {state.outcome.value.upper()}")
        if state.agreement:
            print("\nFinal Agreement:")
            for key, value in state.agreement.items():
                print(f"  - {key}: {value:.2f}")
            
            print(f"\nUtilities:")
            print(f"  Buyer utility: {buyer.calculate_utility(state.agreement):.2f}")
            print(f"  Seller utility: {seller.calculate_utility(state.agreement):.2f}")
        
        # Example 2: Multi-issue negotiation
        print("\n" + "=" * 80)
        print("Example 2: Multi-Issue Resource Allocation")
        print("=" * 80)
        
        agent_a = NegotiationAgent(
            name="Agent_A",
            preferences={
                "cpu_cores": {"ideal": 16, "minimum": 4, "weight": 1.0},
                "memory_gb": {"ideal": 64, "minimum": 16, "weight": 0.8},
                "storage_tb": {"ideal": 10, "minimum": 2, "weight": 0.6}
            },
            reservation_utility=0.35,
            concession_rate=0.12
        )
        
        agent_b = NegotiationAgent(
            name="Agent_B",
            preferences={
                "cpu_cores": {"ideal": 12, "minimum": 4, "weight": 0.9},
                "memory_gb": {"ideal": 48, "minimum": 16, "weight": 1.0},
                "storage_tb": {"ideal": 8, "minimum": 2, "weight": 0.5}
            },
            reservation_utility=0.3,
            concession_rate=0.1
        )
        
        mediator2 = NegotiationMediator()
        
        issue2 = "Server Resource Allocation"
        state2 = mediator2.start_negotiation(
            negotiation_id="negotiation_002",
            participants=["Agent_A", "Agent_B"],
            issue=issue2,
            max_rounds=6
        )
        
        print(f"\nNegotiating: {issue2}")
        print(f"Available resources to allocate between agents")
        
        # Run negotiation
        proposer = agent_a
        recipient = agent_b
        proposal = agent_a.generate_initial_proposal(issue2, "Agent_B")
        
        print(f"\n{'Round':<8} {'Proposer':<12} {'CPU':<10} {'Memory':<12} {'Storage':<12} {'Decision':<15}")
        print("-" * 80)
        
        for round_num in range(1, state2.max_rounds + 1):
            print(f"{round_num:<8} {proposal.proposer:<12} "
                  f"{proposal.terms.get('cpu_cores', 0):<10.1f} "
                  f"{proposal.terms.get('memory_gb', 0):<12.1f} "
                  f"{proposal.terms.get('storage_tb', 0):<12.1f}", end=" ")
            
            accept, reasoning = recipient.evaluate_proposal(proposal, round_num)
            
            if accept:
                state2 = mediator2.process_round(
                    "negotiation_002",
                    proposal,
                    True,
                    reasoning
                )
                print("✓ ACCEPTED")
                break
            else:
                print("✗ Counter")
                counter = recipient.generate_counter_proposal(proposal, issue2, round_num)
                state2 = mediator2.process_round(
                    "negotiation_002",
                    proposal,
                    False,
                    reasoning,
                    counter
                )
                proposal = counter
                proposer, recipient = recipient, proposer
        
        print("\n" + "-" * 80)
        if state2.agreement:
            print("Agreement Reached!")
            print(f"\nFinal Resource Allocation:")
            for key, value in state2.agreement.items():
                print(f"  {key}: {value:.1f}")
        
        # Example 3: Negotiation with mediation
        print("\n" + "=" * 80)
        print("Example 3: Mediated Negotiation with Compromise")
        print("=" * 80)
        
        # Stubborn agents with conflicting preferences
        stubborn_a = NegotiationAgent(
            name="Stubborn_A",
            preferences={
                "budget": {"ideal": 50000, "minimum": 40000, "weight": 1.0}
            },
            reservation_utility=0.7,  # Very high reservation
            concession_rate=0.02  # Very slow concessions
        )
        
        stubborn_b = NegotiationAgent(
            name="Stubborn_B",
            preferences={
                "budget": {"ideal": 80000, "minimum": 70000, "weight": 1.0}
            },
            reservation_utility=0.7,
            concession_rate=0.02
        )
        
        mediator3 = NegotiationMediator()
        
        state3 = mediator3.start_negotiation(
            negotiation_id="negotiation_003",
            participants=["Stubborn_A", "Stubborn_B"],
            issue="Project Budget Allocation",
            max_rounds=5
        )
        
        print("\nNegotiation between stubborn agents (likely to deadlock)")
        
        # Run negotiation
        proposer = stubborn_a
        recipient = stubborn_b
        proposal = stubborn_a.generate_initial_proposal(
            "Project Budget Allocation",
            "Stubborn_B"
        )
        
        for round_num in range(1, 6):
            print(f"\nRound {round_num}:")
            print(f"  Proposal from {proposal.proposer}: Budget = ${proposal.terms['budget']:,.0f}")
            
            accept, reasoning = recipient.evaluate_proposal(proposal, round_num)
            
            if accept:
                state3 = mediator3.process_round(
                    "negotiation_003",
                    proposal,
                    True,
                    reasoning
                )
                print(f"  {recipient.name} accepts!")
                break
            else:
                print(f"  {recipient.name} rejects")
                counter = recipient.generate_counter_proposal(
                    proposal,
                    "Project Budget Allocation",
                    round_num
                )
                state3 = mediator3.process_round(
                    "negotiation_003",
                    proposal,
                    False,
                    reasoning,
                    counter
                )
                proposal = counter
                proposer, recipient = recipient, proposer
        
        if not state3.outcome:
            print("\nDeadlock detected! Mediator suggests compromise...")
            compromise = mediator3.suggest_compromise("negotiation_003")
            if compromise:
                print(f"Suggested compromise: Budget = ${compromise['budget']:,.0f}")
                print(f"  Utility for Stubborn_A: {stubborn_a.calculate_utility(compromise):.2f}")
                print(f"  Utility for Stubborn_B: {stubborn_b.calculate_utility(compromise):.2f}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY: Negotiation Protocol Pattern")
        print("=" * 80)
        
        summary = """
        The Negotiation Protocol pattern demonstrated:
        
        1. BILATERAL NEGOTIATION (Example 1):
           - Two agents negotiating price and delivery
           - Iterative proposal and counter-proposal
           - Utility-based evaluation
           - Gradual concessions leading to agreement
           - Both agents achieve acceptable utility
        
        2. MULTI-ISSUE NEGOTIATION (Example 2):
           - Resource allocation across multiple dimensions
           - Weighted preferences for different attributes
           - Trade-offs between competing interests
           - Efficient agreement through compromise
        
        3. MEDIATED NEGOTIATION (Example 3):
           - Stubborn agents with conflicting goals
           - Deadlock detection
           - Mediator-suggested compromise
           - Breaking negotiation impasses
        
        KEY BENEFITS:
        ✓ Autonomous conflict resolution
        ✓ Fair resource distribution
        ✓ Adaptable to agent preferences
        ✓ Handles complex multi-issue negotiations
        ✓ Supports cooperative and competitive scenarios
        ✓ Transparent reasoning through justifications
        ✓ Mediation for difficult negotiations
        
        USE CASES:
        • Resource allocation in distributed systems
        • Task assignment and scheduling
        • Service level agreement negotiation
        • Price negotiation in marketplaces
        • Coalition formation
        • Conflict resolution between agents
        • Policy consensus building
        
        BEST PRACTICES:
        1. Set clear reservation utilities (walk-away points)
        2. Implement gradual concession strategies
        3. Use utility functions to evaluate proposals
        4. Track negotiation history for transparency
        5. Set round limits to prevent infinite loops
        6. Provide justifications for proposals
        7. Use mediation for deadlock situations
        8. Consider fairness and efficiency
        
        TRADE-OFFS:
        • Computational cost vs. negotiation quality
        • Speed of agreement vs. optimal outcome
        • Information sharing vs. strategic advantage
        • Autonomy vs. mediation intervention
        
        PRODUCTION CONSIDERATIONS:
        → Monitor negotiation metrics (rounds, success rate)
        → Log all proposals for audit trails
        → Implement timeout and deadlock detection
        → Validate proposals against constraints
        → Handle edge cases (withdrawal, malformed proposals)
        → Consider privacy of agent preferences
        → Test with diverse agent strategies
        → Implement fair mediation mechanisms
        
        This pattern enables sophisticated multi-agent coordination through
        structured negotiation, allowing agents to autonomously reach agreements
        while respecting individual preferences and constraints.
        """
        
        print(summary)
    
    
    if __name__ == "__main__":
        demonstrate_negotiation_protocol()
    
        

    Agentic Design Pattern: Negotiation Protocol

    Pattern 099: Negotiation Protocol – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 099: Negotiation Protocol

    Description: The Negotiation Protocol pattern enables multiple agents to reach agreements through structured negotiation processes. This pattern is essential for multi-agent systems where agents have different goals, preferences, or constraints and need to find mutually acceptable solutions through iterative proposal and counter-proposal exchanges.

    Negotiation is a fundamental aspect of multi-agent coordination, allowing agents to
    resolve conflicts, allocate resources, coordinate actions, and make collective decisions.
    This pattern provides mechanisms for proposal generation, evaluation, counter-offers,
    concession strategies, and agreement finalization.
    
    The pattern supports various negotiation strategies including competitive (zero-sum),
    cooperative (win-win), and mixed approaches. It handles single-issue and multi-issue
    negotiations, bilateral and multilateral negotiations, and includes mechanisms for
    handling deadlocks and timeouts.
    

    Key Components: 1. Proposal: Structured offer from one agent to others 2. Counter-Proposal: Modified proposal in response to original 3. Utility Function: Evaluates value of proposals for agent 4. Concession Strategy: Determines how much to compromise 5. Negotiation Protocol: Rules governing negotiation process 6. Agreement: Final mutually accepted terms 7. Mediation: Optional third-party facilitation

    Negotiation Types: 1. Distributive: Fixed resources, competitive 2. Integrative: Value creation, cooperative 3. Multi-issue: Multiple attributes to negotiate 4. Sequential: Issues addressed one at a time 5. Package: All issues negotiated together

    Strategies: 1. Conceding: Gradually reduce demands 2. Hardball: Maintain firm position 3. Tit-for-tat: Mirror opponent’s behavior 4. Time-based: Adjust based on deadline 5. Information-based: Learn and adapt

    Use Cases: – Resource allocation in multi-agent systems – Task assignment and coordination – Conflict resolution between agents – Service level agreements – Coalition formation – Price negotiation in marketplaces – Scheduling and planning coordination – Policy consensus building

    Advantages: – Flexible conflict resolution – Fair resource distribution – Autonomous decision-making – Adaptable to agent preferences – Handles diverse objectives – Enables cooperation – Supports complex agreements

    Challenges: – Computational complexity – Strategic manipulation – Information asymmetry – Deadlock situations – Time constraints – Preference elicitation – Trust issues

    LangChain Implementation: This implementation uses LangChain for: – LLM-based proposal generation and evaluation – Utility calculation with reasoning – Counter-proposal generation – Agreement evaluation – Negotiation strategy implementation

    Production Considerations: – Set negotiation timeouts and round limits – Implement deadlock breaking mechanisms – Log all negotiation steps for transparency – Validate proposals against constraints – Handle malformed proposals gracefully – Monitor for strategic manipulation – Implement fair mediation when needed – Consider privacy of agent preferences


    File Statistics

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

    Module Documentation

    """
    Pattern 099: Negotiation Protocol
    
    Description:
        The Negotiation Protocol pattern enables multiple agents to reach agreements through
        structured negotiation processes. This pattern is essential for multi-agent systems
        where agents have different goals, preferences, or constraints and need to find
        mutually acceptable solutions through iterative proposal and counter-proposal exchanges.
    
        Negotiation is a fundamental aspect of multi-agent coordination, allowing agents to
        resolve conflicts, allocate resources, coordinate actions, and make collective decisions.
        This pattern provides mechanisms for proposal generation, evaluation, counter-offers,
        concession strategies, and agreement finalization.
    
        The pattern supports various negotiation strategies including competitive (zero-sum),
        cooperative (win-win), and mixed approaches. It handles single-issue and multi-issue
        negotiations, bilateral and multilateral negotiations, and includes mechanisms for
        handling deadlocks and timeouts.
    
    Key Components:
        1. Proposal: Structured offer from one agent to others
        2. Counter-Proposal: Modified proposal in response to original
        3. Utility Function: Evaluates value of proposals for agent
        4. Concession Strategy: Determines how much to compromise
        5. Negotiation Protocol: Rules governing negotiation process
        6. Agreement: Final mutually accepted terms
        7. Mediation: Optional third-party facilitation
    
    Negotiation Types:
        1. Distributive: Fixed resources, competitive
        2. Integrative: Value creation, cooperative
        3. Multi-issue: Multiple attributes to negotiate
        4. Sequential: Issues addressed one at a time
        5. Package: All issues negotiated together
    
    Strategies:
        1. Conceding: Gradually reduce demands
        2. Hardball: Maintain firm position
        3. Tit-for-tat: Mirror opponent's behavior
        4. Time-based: Adjust based on deadline
        5. Information-based: Learn and adapt
    
    Use Cases:
        - Resource allocation in multi-agent systems
        - Task assignment and coordination
        - Conflict resolution between agents
        - Service level agreements
        - Coalition formation
        - Price negotiation in marketplaces
        - Scheduling and planning coordination
        - Policy consensus building
    
    Advantages:
        - Flexible conflict resolution
        - Fair resource distribution
        - Autonomous decision-making
        - Adaptable to agent preferences
        - Handles diverse objectives
        - Enables cooperation
        - Supports complex agreements
    
    Challenges:
        - Computational complexity
        - Strategic manipulation
        - Information asymmetry
        - Deadlock situations
        - Time constraints
        - Preference elicitation
        - Trust issues
    
    LangChain Implementation:
        This implementation uses LangChain for:
        - LLM-based proposal generation and evaluation
        - Utility calculation with reasoning
        - Counter-proposal generation
        - Agreement evaluation
        - Negotiation strategy implementation
    
    Production Considerations:
        - Set negotiation timeouts and round limits
        - Implement deadlock breaking mechanisms
        - Log all negotiation steps for transparency
        - Validate proposals against constraints
        - Handle malformed proposals gracefully
        - Monitor for strategic manipulation
        - Implement fair mediation when needed
        - Consider privacy of agent preferences
    """
    

    Purpose: This module implements the Negotiation Protocol 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 typing.Any
    import typing.Dict
    import typing.List
    import typing.Optional
    import typing.Tuple
    

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

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: ProposalStatus

    Lines 104-110

    Inherits from: Enum

    Purpose:

    Status of a negotiation proposal.
    

    Class: NegotiationOutcome

    Lines 113-118

    Inherits from: Enum

    Purpose:

    Outcome of a negotiation.
    

    Class: Proposal

    Lines 122-147

    Purpose:

    Represents a proposal in a negotiation.
    
    Attributes:
        proposal_id: Unique identifier
        proposer: Agent making the proposal
        recipient: Agent(s) receiving proposal
        round_number: Negotiation round
        terms: Dictionary of proposed terms
        utility: Utility value for proposer
        status: Current status of proposal
        timestamp: When proposal was made
        justification: Reasoning for proposal
        metadata: Additional information
    

    Class: NegotiationState

    Lines 151-178

    Purpose:

    Tracks the state of an ongoing negotiation.
    
    Attributes:
        negotiation_id: Unique identifier
        participants: Agents involved
        issue: What is being negotiated
        proposals: History of proposals
        current_round: Current round number
        max_rounds: Maximum allowed rounds
        start_time: When negotiation started
        end_time: When negotiation ended
        outcome: Final outcome
        agreement: Final agreed terms
        metadata: Additional information
    

    Class: NegotiationAgent

    Lines 181-424

    Purpose:

    Agent that participates in negotiations.
    
    This agent can generate proposals, evaluate offers, make counter-proposals,
    and decide when to accept or reject proposals based on its utility function
    and negotiation strategy.
    

    Methods:

    __init__(self, name, preferences, reservation_utility, concession_rate, temperature)

    Lines 190-213

    Purpose:

    Initialize negotiation agent.
    
    Args:
        name: Agent's name
        preferences: Agent's preferences for negotiation issues
        reservation_utility: Minimum acceptable utility (walk-away point)
        concession_rate: How quickly agent makes concessions
        temperature: LLM temperature for generation
    

    Parameters: – name: Name – preferences: Preferences – reservation_utility: Reservation utility – concession_rate: Concession rate – temperature: Sampling temperature (0-1)

    calculate_utility(self, terms)

    Lines 215-246

    Purpose:

    Calculate utility of proposed terms.
    
    Args:
        terms: Proposed terms
    
    Returns:
        Utility value between 0 and 1
    

    Parameters: – terms: Terms

    Returns: float

    generate_initial_proposal(self, issue, recipient, round_number)

    Lines 248-305

    Purpose:

    Generate initial proposal.
    
    Args:
        issue: Issue being negotiated
        recipient: Recipient agent
        round_number: Round number
    
    Returns:
        Initial proposal
    

    Parameters: – issue: Issue – recipient: Recipient – round_number: Round number

    Returns: Proposal

    evaluate_proposal(self, proposal, current_round)

    Lines 307-355

    Purpose:

    Evaluate whether to accept a proposal.
    
    Args:
        proposal: Proposal to evaluate
        current_round: Current negotiation round
    
    Returns:
        Tuple of (accept: bool, reasoning: str)
    

    Parameters: – proposal: Proposal – current_round: Current round

    Returns: <ast.Subscript object at 0x000001A06D658650>

    generate_counter_proposal(self, previous_proposal, issue, round_number)

    Lines 357-424

    Purpose:

    Generate counter-proposal.
    
    Args:
        previous_proposal: Proposal to counter
        issue: Issue being negotiated
        round_number: Current round number
    
    Returns:
        Counter-proposal
    

    Parameters: – previous_proposal: Previous proposal – issue: Issue – round_number: Round number

    Returns: Proposal


    Class: NegotiationMediator

    Lines 427-584

    Purpose:

    Mediator that facilitates negotiation between agents.
    
    The mediator manages the negotiation protocol, enforces rules,
    tracks state, and can suggest compromises when negotiations stall.
    

    Methods:

    __init__(self, temperature)

    Lines 435-443

    Purpose:

    Initialize negotiation mediator.
    
    Args:
        temperature: LLM temperature for generation
    

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

    start_negotiation(self, negotiation_id, participants, issue, max_rounds)

    Lines 445-471

    Purpose:

    Start a new negotiation.
    
    Args:
        negotiation_id: Unique identifier
        participants: List of participating agents
        issue: Issue being negotiated
        max_rounds: Maximum rounds allowed
    
    Returns:
        Initial negotiation state
    

    Parameters: – negotiation_id: Negotiation id – participants: Participants – issue: Issue – max_rounds: Max rounds

    Returns: NegotiationState

    process_round(self, negotiation_id, proposal, response_accept, response_reasoning, counter_proposal)

    Lines 473-521

    Purpose:

    Process a negotiation round.
    
    Args:
        negotiation_id: Negotiation identifier
        proposal: Current proposal
        response_accept: Whether proposal was accepted
        response_reasoning: Reasoning for response
        counter_proposal: Counter-proposal if not accepted
    
    Returns:
        Updated negotiation state
    

    Parameters: – negotiation_id: Negotiation id – proposal: Proposal – response_accept: Response accept – response_reasoning: Response reasoning – counter_proposal: Counter proposal

    Returns: NegotiationState

    suggest_compromise(self, negotiation_id)

    Lines 523-549

    Purpose:

    Suggest compromise when negotiation stalls.
    
    Args:
        negotiation_id: Negotiation identifier
    
    Returns:
        Suggested compromise terms or None
    

    Parameters: – negotiation_id: Negotiation id

    Returns: <ast.Subscript object at 0x000001A06D7FF2D0>

    get_negotiation_summary(self, negotiation_id)

    Lines 551-584

    Purpose:

    Generate summary of negotiation.
    
    Args:
        negotiation_id: Negotiation identifier
    
    Returns:
        Summary text
    

    Parameters: – negotiation_id: Negotiation id

    Returns: str


    Functions

    demonstrate_negotiation_protocol()

    Lines 587-945

    Purpose:

    Demonstrate negotiation protocol pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Negotiation Protocol 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 098
    • Pattern 100

    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 Negotiation Protocol 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: 099_negotiation_protocol.py
    Total Lines: 950
    Pattern Category: AI Agent Design Pattern #099


    Generated from actual implementation code
    For educational and reference purposes