Tag: design-pattern

  • Agentic Design Pattern: Episodic Memory Retrieval

    
        """
    Pattern 030: Episodic Memory & Retrieval
    
    Description:
        Episodic memory stores and retrieves specific experiences or events with temporal
        and contextual information. This pattern enables agents to recall relevant past
        experiences based on similarity, temporal proximity, and contextual relevance,
        supporting experience-based reasoning and learning.
    
    Components:
        - Episode: Time-stamped experience with context, actions, and outcomes
        - Episode Store: Storage system for episodes with indexing
        - Retrieval System: Similarity-based and temporal filtering
        - Context Matcher: Identifies relevant episodes for current situation
        - Experience Reconstructor: Rebuilds complete episode details
    
    Use Cases:
        - Learning from past interactions
        - Experience-based decision making
        - Personalization through history
        - Debugging and analysis
        - Temporal reasoning
    
    LangChain Implementation:
        Uses vector similarity search for content-based retrieval combined with
        temporal and contextual filtering to find relevant past experiences.
    """
    
    import os
    from typing import List, Dict, Any, Optional, Tuple
    from dataclasses import dataclass, field
    from datetime import datetime, timedelta
    from enum import Enum
    import json
    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 EpisodeType(Enum):
        """Types of episodes that can be stored."""
        CONVERSATION = "conversation"
        TASK_EXECUTION = "task_execution"
        DECISION = "decision"
        ERROR = "error"
        SUCCESS = "success"
        LEARNING = "learning"
        INTERACTION = "interaction"
    
    
    @dataclass
    class Episode:
        """
        Represents a single episodic memory.
        
        An episode captures a complete experience including context,
        actions taken, outcomes, and temporal information.
        """
        id: str
        type: EpisodeType
        timestamp: datetime
        context: Dict[str, Any]  # Situational context
        content: str  # Main content/description
        actions: List[str] = field(default_factory=list)  # Actions taken
        outcome: Optional[str] = None  # Result of the episode
        metadata: Dict[str, Any] = field(default_factory=dict)
        tags: List[str] = field(default_factory=list)
        importance: float = 0.5  # 0-1 scale
        access_count: int = 0
        last_accessed: Optional[datetime] = None
        
        def to_text(self) -> str:
            """Convert episode to searchable text representation."""
            parts = [
                f"Type: {self.type.value}",
                f"Content: {self.content}",
            ]
            
            if self.context:
                parts.append(f"Context: {json.dumps(self.context)}")
            
            if self.actions:
                parts.append(f"Actions: {', '.join(self.actions)}")
            
            if self.outcome:
                parts.append(f"Outcome: {self.outcome}")
            
            if self.tags:
                parts.append(f"Tags: {', '.join(self.tags)}")
            
            return " | ".join(parts)
        
        def mark_accessed(self):
            """Mark episode as accessed."""
            self.access_count += 1
            self.last_accessed = datetime.now()
    
    
    @dataclass
    class RetrievalQuery:
        """Query for retrieving episodes."""
        query: str
        episode_types: Optional[List[EpisodeType]] = None
        time_range: Optional[Tuple[datetime, datetime]] = None
        min_importance: float = 0.0
        max_results: int = 5
        tags: Optional[List[str]] = None
    
    
    @dataclass
    class RetrievalResult:
        """Result of episode retrieval with relevance scoring."""
        episode: Episode
        relevance_score: float  # Overall relevance (0-1)
        similarity_score: float  # Content similarity (0-1)
        recency_score: float  # Temporal relevance (0-1)
        importance_score: float  # Importance weight (0-1)
    
    
    class EpisodicMemoryStore:
        """
        Stores and retrieves episodic memories.
        
        Supports multiple retrieval strategies:
        - Similarity-based (content matching)
        - Temporal filtering (time-based)
        - Importance weighting
        - Tag-based filtering
        """
        
        def __init__(self, recency_decay_hours: float = 168.0):  # Default 1 week
            self.episodes: Dict[str, Episode] = {}
            self.recency_decay_hours = recency_decay_hours
            self._next_id = 1
        
        def add_episode(self, episode: Episode) -> str:
            """Add an episode to memory."""
            if not episode.id:
                episode.id = f"ep_{self._next_id:04d}"
                self._next_id += 1
            
            self.episodes[episode.id] = episode
            return episode.id
        
        def get_episode(self, episode_id: str) -> Optional[Episode]:
            """Retrieve a specific episode by ID."""
            episode = self.episodes.get(episode_id)
            if episode:
                episode.mark_accessed()
            return episode
        
        def retrieve(self, query: RetrievalQuery) -> List[RetrievalResult]:
            """
            Retrieve relevant episodes based on query.
            
            Combines multiple scoring factors:
            - Similarity to query content
            - Recency (temporal decay)
            - Importance weighting
            """
            candidates = self._filter_episodes(query)
            
            if not candidates:
                return []
            
            # Score each candidate
            results = []
            current_time = datetime.now()
            
            for episode in candidates:
                # Similarity score (simple keyword matching)
                similarity_score = self._compute_similarity(query.query, episode)
                
                # Recency score (exponential decay)
                recency_score = self._compute_recency(episode.timestamp, current_time)
                
                # Importance score
                importance_score = episode.importance
                
                # Combined relevance score (weighted average)
                relevance_score = (
                    0.5 * similarity_score +
                    0.3 * recency_score +
                    0.2 * importance_score
                )
                
                result = RetrievalResult(
                    episode=episode,
                    relevance_score=relevance_score,
                    similarity_score=similarity_score,
                    recency_score=recency_score,
                    importance_score=importance_score
                )
                results.append(result)
            
            # Sort by relevance and limit results
            results.sort(key=lambda x: x.relevance_score, reverse=True)
            
            # Mark accessed
            for result in results[:query.max_results]:
                result.episode.mark_accessed()
            
            return results[:query.max_results]
        
        def _filter_episodes(self, query: RetrievalQuery) -> List[Episode]:
            """Apply filters to episodes."""
            filtered = []
            
            for episode in self.episodes.values():
                # Type filter
                if query.episode_types and episode.type not in query.episode_types:
                    continue
                
                # Importance filter
                if episode.importance < query.min_importance:
                    continue
                
                # Time range filter
                if query.time_range:
                    start_time, end_time = query.time_range
                    if not (start_time <= episode.timestamp  float:
            """
            Compute similarity between query and episode.
            Simple keyword-based similarity (can be replaced with embeddings).
            """
            query_words = set(query.lower().split())
            episode_text = episode.to_text().lower()
            episode_words = set(episode_text.split())
            
            if not query_words:
                return 0.0
            
            # Jaccard similarity
            intersection = query_words & episode_words
            union = query_words | episode_words
            
            if not union:
                return 0.0
            
            return len(intersection) / len(union)
        
        def _compute_recency(self, timestamp: datetime, current_time: datetime) -> float:
            """
            Compute recency score with exponential decay.
            More recent episodes score higher.
            """
            hours_ago = (current_time - timestamp).total_seconds() / 3600
            
            if hours_ago  Dict[str, Any]:
            """Get statistics about episodic memory."""
            if not self.episodes:
                return {
                    "total_episodes": 0,
                    "episode_types": {},
                    "avg_importance": 0.0,
                    "most_accessed": None
                }
            
            type_counts = {}
            for episode in self.episodes.values():
                type_counts[episode.type.value] = type_counts.get(episode.type.value, 0) + 1
            
            avg_importance = sum(e.importance for e in self.episodes.values()) / len(self.episodes)
            
            most_accessed = max(self.episodes.values(), key=lambda e: e.access_count)
            
            return {
                "total_episodes": len(self.episodes),
                "episode_types": type_counts,
                "avg_importance": avg_importance,
                "most_accessed": {
                    "id": most_accessed.id,
                    "access_count": most_accessed.access_count,
                    "content": most_accessed.content[:50] + "..."
                }
            }
    
    
    class EpisodicMemoryAgent:
        """
        Agent that uses episodic memory to inform its responses.
        
        Capabilities:
        - Store experiences as episodes
        - Retrieve relevant past experiences
        - Learn from past successes and failures
        - Provide context-aware responses
        """
        
        def __init__(self, temperature: float = 0.7):
            self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=temperature)
            self.memory_store = EpisodicMemoryStore()
            
            # Prompt for responding with episodic context
            self.response_prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an AI assistant with episodic memory.
    You can remember past experiences and use them to inform your responses.
    
    Relevant past experiences:
    {episodes}
    
    Use these experiences to provide context-aware, informed responses.
    Reference past experiences when relevant."""),
                ("user", "{query}")
            ])
            
            # Prompt for extracting learnings
            self.learning_prompt = ChatPromptTemplate.from_messages([
                ("system", """Analyze this experience and extract key learnings.
    
    Experience: {experience}
    
    What are the key takeaways? What worked? What didn't?
    Provide 2-3 concise learnings."""),
                ("user", "Extract learnings")
            ])
        
        def record_episode(
            self,
            episode_type: EpisodeType,
            content: str,
            context: Optional[Dict[str, Any]] = None,
            actions: Optional[List[str]] = None,
            outcome: Optional[str] = None,
            importance: float = 0.5,
            tags: Optional[List[str]] = None
        ) -> str:
            """Record a new episodic memory."""
            episode = Episode(
                id="",  # Will be auto-generated
                type=episode_type,
                timestamp=datetime.now(),
                context=context or {},
                content=content,
                actions=actions or [],
                outcome=outcome,
                importance=importance,
                tags=tags or []
            )
            
            return self.memory_store.add_episode(episode)
        
        def respond_with_memory(self, query: str) -> Tuple[str, List[Episode]]:
            """
            Generate response using relevant episodic memories.
            
            Returns:
                Tuple of (response, relevant_episodes)
            """
            # Retrieve relevant episodes
            retrieval_query = RetrievalQuery(
                query=query,
                max_results=3
            )
            
            results = self.memory_store.retrieve(retrieval_query)
            
            # Format episodes for context
            if results:
                episodes_text = "\n\n".join([
                    f"Episode {i+1} (relevance: {r.relevance_score:.2f}):\n"
                    f"Type: {r.episode.type.value}\n"
                    f"Time: {r.episode.timestamp.strftime('%Y-%m-%d %H:%M')}\n"
                    f"Content: {r.episode.content}\n"
                    f"Outcome: {r.episode.outcome or 'N/A'}"
                    for i, r in enumerate(results)
                ])
            else:
                episodes_text = "No relevant past experiences found."
            
            # Generate response
            chain = self.response_prompt | self.llm | StrOutputParser()
            response = chain.invoke({
                "episodes": episodes_text,
                "query": query
            })
            
            # Extract episodes
            episodes = [r.episode for r in results]
            
            return response, episodes
        
        def learn_from_experience(self, episode_id: str) -> Optional[str]:
            """
            Extract learnings from a specific episode.
            """
            episode = self.memory_store.get_episode(episode_id)
            
            if not episode:
                return None
            
            # Format experience
            experience_text = (
                f"Type: {episode.type.value}\n"
                f"Context: {json.dumps(episode.context)}\n"
                f"Content: {episode.content}\n"
                f"Actions: {', '.join(episode.actions)}\n"
                f"Outcome: {episode.outcome or 'N/A'}"
            )
            
            # Extract learnings
            chain = self.learning_prompt | self.llm | StrOutputParser()
            learnings = chain.invoke({"experience": experience_text})
            
            return learnings
        
        def get_similar_experiences(
            self,
            reference_episode_id: str,
            max_results: int = 5
        ) -> List[RetrievalResult]:
            """
            Find episodes similar to a reference episode.
            """
            reference = self.memory_store.get_episode(reference_episode_id)
            
            if not reference:
                return []
            
            query = RetrievalQuery(
                query=reference.content,
                episode_types=[reference.type],
                max_results=max_results
            )
            
            results = self.memory_store.retrieve(query)
            
            # Exclude the reference episode itself
            return [r for r in results if r.episode.id != reference_episode_id]
        
        def get_recent_episodes(
            self,
            hours: int = 24,
            episode_type: Optional[EpisodeType] = None
        ) -> List[Episode]:
            """Get episodes from recent time period."""
            current_time = datetime.now()
            start_time = current_time - timedelta(hours=hours)
            
            query = RetrievalQuery(
                query="",  # Match all
                episode_types=[episode_type] if episode_type else None,
                time_range=(start_time, current_time),
                max_results=100  # Get all recent
            )
            
            results = self.memory_store.retrieve(query)
            return [r.episode for r in results]
    
    
    def demonstrate_episodic_memory():
        """
        Demonstrates episodic memory with storage, retrieval,
        and experience-based reasoning.
        """
        print("=" * 80)
        print("EPISODIC MEMORY & RETRIEVAL DEMONSTRATION")
        print("=" * 80)
        
        # Create agent
        agent = EpisodicMemoryAgent()
        
        # Test 1: Record various episodes
        print("\n" + "=" * 80)
        print("Test 1: Recording Episodes")
        print("=" * 80)
        
        episodes_to_record = [
            {
                "type": EpisodeType.CONVERSATION,
                "content": "User asked about Python programming best practices",
                "context": {"topic": "programming", "language": "Python"},
                "actions": ["Provided advice on PEP 8", "Recommended tools"],
                "outcome": "User found the advice helpful",
                "importance": 0.7,
                "tags": ["programming", "python", "advice"]
            },
            {
                "type": EpisodeType.TASK_EXECUTION,
                "content": "Helped user debug a database connection issue",
                "context": {"topic": "debugging", "database": "PostgreSQL"},
                "actions": ["Checked connection string", "Verified credentials", "Tested connection"],
                "outcome": "Successfully resolved the connection issue",
                "importance": 0.8,
                "tags": ["debugging", "database", "postgresql"]
            },
            {
                "type": EpisodeType.ERROR,
                "content": "Failed to parse user's malformed JSON input",
                "context": {"error_type": "JSONDecodeError"},
                "actions": ["Attempted to parse JSON", "Provided error message"],
                "outcome": "User corrected the JSON format",
                "importance": 0.6,
                "tags": ["error", "json", "parsing"]
            },
            {
                "type": EpisodeType.SUCCESS,
                "content": "Successfully explained machine learning concepts to beginner",
                "context": {"topic": "machine learning", "user_level": "beginner"},
                "actions": ["Used simple analogies", "Provided examples", "Answered follow-up questions"],
                "outcome": "User understood the concepts clearly",
                "importance": 0.9,
                "tags": ["teaching", "machine_learning", "success"]
            },
            {
                "type": EpisodeType.CONVERSATION,
                "content": "Discussed best practices for API design",
                "context": {"topic": "api design", "focus": "RESTful APIs"},
                "actions": ["Explained REST principles", "Discussed versioning", "Covered authentication"],
                "outcome": "User appreciated the comprehensive explanation",
                "importance": 0.75,
                "tags": ["api", "design", "rest"]
            }
        ]
        
        episode_ids = []
        for ep_data in episodes_to_record:
            ep_id = agent.record_episode(**ep_data)
            episode_ids.append(ep_id)
            print(f"✓ Recorded episode: {ep_data['content'][:50]}...")
        
        # Show statistics
        stats = agent.memory_store.get_statistics()
        print(f"\nMemory statistics:")
        print(f"  Total episodes: {stats['total_episodes']}")
        print(f"  Episode types: {stats['episode_types']}")
        print(f"  Average importance: {stats['avg_importance']:.2f}")
        
        # Test 2: Query with episodic memory
        print("\n" + "=" * 80)
        print("Test 2: Responding with Episodic Memory")
        print("=" * 80)
        
        queries = [
            "How should I approach learning programming?",
            "I'm having database connection problems, any advice?",
            "Can you explain machine learning to me?"
        ]
        
        for query in queries:
            print(f"\nUser: {query}")
            response, relevant_episodes = agent.respond_with_memory(query)
            print(f"\nAgent: {response}")
            print(f"\nRelevant past experiences: {len(relevant_episodes)}")
            for ep in relevant_episodes:
                print(f"  - {ep.type.value}: {ep.content[:60]}...")
        
        # Test 3: Learn from experiences
        print("\n" + "=" * 80)
        print("Test 3: Learning from Past Experiences")
        print("=" * 80)
        
        print(f"\nAnalyzing episode: {episode_ids[1]}")
        learnings = agent.learn_from_experience(episode_ids[1])
        if learnings:
            print(f"Learnings extracted:\n{learnings}")
        
        # Test 4: Find similar experiences
        print("\n" + "=" * 80)
        print("Test 4: Finding Similar Experiences")
        print("=" * 80)
        
        reference_id = episode_ids[0]
        print(f"\nFinding experiences similar to: {episode_ids[0]}")
        similar = agent.get_similar_experiences(reference_id, max_results=3)
        
        if similar:
            print(f"Found {len(similar)} similar experiences:")
            for result in similar:
                print(f"  - Relevance: {result.relevance_score:.2f}")
                print(f"    {result.episode.content[:70]}...")
        else:
            print("No similar experiences found")
        
        # Test 5: Recent activity
        print("\n" + "=" * 80)
        print("Test 5: Recent Activity Review")
        print("=" * 80)
        
        recent = agent.get_recent_episodes(hours=24)
        print(f"\nEpisodes from last 24 hours: {len(recent)}")
        for ep in recent[:3]:
            print(f"  - [{ep.timestamp.strftime('%H:%M')}] {ep.type.value}: {ep.content[:60]}...")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        print("""
    Episodic Memory & Retrieval provides:
    ✓ Time-stamped experience storage
    ✓ Multi-factor relevance scoring
    ✓ Context-aware retrieval
    ✓ Learning from past experiences
    ✓ Temporal and semantic filtering
    
    This pattern excels at:
    - Personalizing responses based on history
    - Learning from successes and failures
    - Providing context-aware recommendations
    - Supporting temporal reasoning
    - Building experiential knowledge
    
    Key advantages:
    1. Rich contextual information
    2. Temporal awareness
    3. Experience-based learning
    4. Flexible retrieval strategies
    5. Automatic relevance scoring
    
    Retrieval factors:
    - Similarity: Content matching (50% weight)
    - Recency: Temporal decay (30% weight)
    - Importance: Episode significance (20% weight)
    
    Use episodic memory when you need to:
    - Remember specific past interactions
    - Learn from historical outcomes
    - Provide personalized experiences
    - Support case-based reasoning
    - Track agent development over time
    """)
    
    
    if __name__ == "__main__":
        demonstrate_episodic_memory()
    
        

    Agentic Design Pattern: Episodic Memory Retrieval

    Pattern 030: Episodic Memory Retrieval – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 030: Episodic Memory & Retrieval

    Description: Episodic memory stores and retrieves specific experiences or events with temporal and contextual information. This pattern enables agents to recall relevant past experiences based on similarity, temporal proximity, and contextual relevance, supporting experience-based reasoning and learning.

    Components: – Episode: Time-stamped experience with context, actions, and outcomes – Episode Store: Storage system for episodes with indexing – Retrieval System: Similarity-based and temporal filtering – Context Matcher: Identifies relevant episodes for current situation – Experience Reconstructor: Rebuilds complete episode details

    Use Cases: – Learning from past interactions – Experience-based decision making – Personalization through history – Debugging and analysis – Temporal reasoning

    LangChain Implementation: Uses vector similarity search for content-based retrieval combined with temporal and contextual filtering to find relevant past experiences.


    File Statistics

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

    Module Documentation

    """
    Pattern 030: Episodic Memory & Retrieval
    
    Description:
        Episodic memory stores and retrieves specific experiences or events with temporal
        and contextual information. This pattern enables agents to recall relevant past
        experiences based on similarity, temporal proximity, and contextual relevance,
        supporting experience-based reasoning and learning.
    
    Components:
        - Episode: Time-stamped experience with context, actions, and outcomes
        - Episode Store: Storage system for episodes with indexing
        - Retrieval System: Similarity-based and temporal filtering
        - Context Matcher: Identifies relevant episodes for current situation
        - Experience Reconstructor: Rebuilds complete episode details
    
    Use Cases:
        - Learning from past interactions
        - Experience-based decision making
        - Personalization through history
        - Debugging and analysis
        - Temporal reasoning
    
    LangChain Implementation:
        Uses vector similarity search for content-based retrieval combined with
        temporal and contextual filtering to find relevant past experiences.
    """
    

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


    Import Statements

    Standard Library Imports

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

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

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from dotenv.load_dotenv
    from math
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: EpisodeType

    Lines 45-53

    Inherits from: Enum

    Purpose:

    Types of episodes that can be stored.
    

    Class: Episode

    Lines 57-101

    Purpose:

    Represents a single episodic memory.
    
    An episode captures a complete experience including context,
    actions taken, outcomes, and temporal information.
    

    Methods:

    to_text(self)

    Lines 77-96

    Purpose:

    Convert episode to searchable text representation.
    

    Parameters:

    Returns: str

    mark_accessed(self)

    Lines 98-101

    Purpose:

    Mark episode as accessed.
    

    Parameters:


    Class: RetrievalQuery

    Lines 105-112

    Purpose:

    Query for retrieving episodes.
    

    Class: RetrievalResult

    Lines 116-122

    Purpose:

    Result of episode retrieval with relevance scoring.
    

    Class: EpisodicMemoryStore

    Lines 125-300

    Purpose:

    Stores and retrieves episodic memories.
    
    Supports multiple retrieval strategies:
    - Similarity-based (content matching)
    - Temporal filtering (time-based)
    - Importance weighting
    - Tag-based filtering
    

    Methods:

    __init__(self, recency_decay_hours)

    Lines 136-139

    Purpose: Initialize the class instance with required parameters.

    Parameters: – recency_decay_hours: Recency decay hours

    add_episode(self, episode)

    Lines 141-148

    Purpose:

    Add an episode to memory.
    

    Parameters: – episode: Episode

    Returns: str

    get_episode(self, episode_id)

    Lines 150-155

    Purpose:

    Retrieve a specific episode by ID.
    

    Parameters: – episode_id: Episode id

    Returns: <ast.Subscript object at 0x000001A06D6A9950>

    retrieve(self, query)

    Lines 157-208

    Purpose:

    Retrieve relevant episodes based on query.
    
    Combines multiple scoring factors:
    - Similarity to query content
    - Recency (temporal decay)
    - Importance weighting
    

    Parameters: – query: Input query or question

    Returns: <ast.Subscript object at 0x000001A06D6F6AD0>

    _filter_episodes(self, query)

    Lines 210-236

    Purpose:

    Apply filters to episodes.
    

    Parameters: – query: Input query or question

    Returns: <ast.Subscript object at 0x000001A06D62B6D0>

    _compute_similarity(self, query, episode)

    Lines 238-257

    Purpose:

    Compute similarity between query and episode.
    Simple keyword-based similarity (can be replaced with embeddings).
    

    Parameters: – query: Input query or question – episode: Episode

    Returns: float

    _compute_recency(self, timestamp, current_time)

    Lines 259-271

    Purpose:

    Compute recency score with exponential decay.
    More recent episodes score higher.
    

    Parameters: – timestamp: Timestamp – current_time: Current time

    Returns: float

    get_statistics(self)

    Lines 273-300

    Purpose:

    Get statistics about episodic memory.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D6066D0>


    Class: EpisodicMemoryAgent

    Lines 303-472

    Purpose:

    Agent that uses episodic memory to inform its responses.
    
    Capabilities:
    - Store experiences as episodes
    - Retrieve relevant past experiences
    - Learn from past successes and failures
    - Provide context-aware responses
    

    Methods:

    __init__(self, temperature)

    Lines 314-340

    Purpose: Initialize the class instance with required parameters.

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

    record_episode(self, episode_type, content, context, actions, outcome, importance, tags)

    Lines 342-365

    Purpose:

    Record a new episodic memory.
    

    Parameters: – episode_type: Episode type – content: Content – context: Context – actions: Actions – outcome: Outcome – importance: Importance – tags: Tags

    Returns: str

    respond_with_memory(self, query)

    Lines 367-405

    Purpose:

    Generate response using relevant episodic memories.
    
    Returns:
        Tuple of (response, relevant_episodes)
    

    Parameters: – query: Input query or question

    Returns: <ast.Subscript object at 0x000001A06D156E50>

    learn_from_experience(self, episode_id)

    Lines 407-429

    Purpose:

    Extract learnings from a specific episode.
    

    Parameters: – episode_id: Episode id

    Returns: <ast.Subscript object at 0x000001A06D72A890>

    get_similar_experiences(self, reference_episode_id, max_results)

    Lines 431-453

    Purpose:

    Find episodes similar to a reference episode.
    

    Parameters: – reference_episode_id: Reference episode id – max_results: Max results

    Returns: <ast.Subscript object at 0x000001A06D72B850>

    get_recent_episodes(self, hours, episode_type)

    Lines 455-472

    Purpose:

    Get episodes from recent time period.
    

    Parameters: – hours: Hours – episode_type: Episode type

    Returns: <ast.Subscript object at 0x000001A06D169750>


    Functions

    demonstrate_episodic_memory()

    Lines 475-646

    Purpose:

    Demonstrates episodic memory with storage, retrieval,
    and experience-based reasoning.
    

    Key Concepts Summary

    Pattern Overview

    The Episodic Memory Retrieval 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 029
    • Pattern 031

    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 Episodic Memory Retrieval 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: 030_episodic_memory_retrieval.py
    Total Lines: 651
    Pattern Category: AI Agent Design Pattern #030


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Semantic Memory Networks

    
        """
    Pattern 029: Semantic Memory Networks
    
    Description:
        Semantic memory networks represent structured knowledge as graphs with entities,
        relationships, and attributes. This pattern enables sophisticated reasoning over
        interconnected concepts, supports graph-based queries, and facilitates knowledge
        discovery through relationship traversal.
    
    Components:
        - Entity: Nodes representing concepts, objects, or individuals
        - Relationship: Edges connecting entities with typed connections
        - Attributes: Properties associated with entities
        - Graph Store: Storage and indexing of the knowledge graph
        - Query Engine: Traversal and pattern matching over the graph
        - Reasoning Module: Inference and relationship discovery
    
    Use Cases:
        - Knowledge management systems
        - Question answering over structured knowledge
        - Relationship discovery and analysis
        - Semantic search and retrieval
        - Expert systems and decision support
    
    LangChain Implementation:
        Uses graph data structures, relationship modeling, and LLM-based query
        interpretation to create a semantic network that supports both explicit
        knowledge storage and implicit reasoning.
    """
    
    import os
    from typing import List, Dict, Any, Optional, Set, Tuple
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from collections import defaultdict
    import json
    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 RelationType(Enum):
        """Types of relationships between entities."""
        IS_A = "is_a"  # Inheritance/taxonomy
        PART_OF = "part_of"  # Composition
        RELATED_TO = "related_to"  # General association
        CAUSES = "causes"  # Causal relationship
        PRECEDES = "precedes"  # Temporal relationship
        LOCATED_IN = "located_in"  # Spatial relationship
        OWNS = "owns"  # Ownership
        CREATED_BY = "created_by"  # Authorship
        USES = "uses"  # Functional relationship
        SIMILAR_TO = "similar_to"  # Similarity
    
    
    @dataclass
    class Entity:
        """Represents a node in the semantic network."""
        id: str
        name: str
        type: str
        attributes: Dict[str, Any] = field(default_factory=dict)
        created_at: datetime = field(default_factory=datetime.now)
        
        def __hash__(self):
            return hash(self.id)
        
        def __eq__(self, other):
            return isinstance(other, Entity) and self.id == other.id
    
    
    @dataclass
    class Relationship:
        """Represents an edge in the semantic network."""
        source_id: str
        target_id: str
        type: RelationType
        attributes: Dict[str, Any] = field(default_factory=dict)
        weight: float = 1.0
        created_at: datetime = field(default_factory=datetime.now)
    
    
    @dataclass
    class QueryResult:
        """Result of a semantic query."""
        entities: List[Entity]
        relationships: List[Relationship]
        paths: List[List[str]] = field(default_factory=list)
        explanation: str = ""
    
    
    class SemanticGraph:
        """
        Graph data structure for storing semantic knowledge.
        
        Supports efficient entity lookup, relationship traversal,
        and pattern matching operations.
        """
        
        def __init__(self):
            self.entities: Dict[str, Entity] = {}
            self.relationships: List[Relationship] = []
            # Adjacency lists for efficient traversal
            self.outgoing: Dict[str, List[Relationship]] = defaultdict(list)
            self.incoming: Dict[str, List[Relationship]] = defaultdict(list)
        
        def add_entity(self, entity: Entity) -> None:
            """Add an entity to the graph."""
            self.entities[entity.id] = entity
        
        def add_relationship(self, relationship: Relationship) -> None:
            """Add a relationship to the graph."""
            # Validate entities exist
            if relationship.source_id not in self.entities:
                raise ValueError(f"Source entity {relationship.source_id} not found")
            if relationship.target_id not in self.entities:
                raise ValueError(f"Target entity {relationship.target_id} not found")
            
            self.relationships.append(relationship)
            self.outgoing[relationship.source_id].append(relationship)
            self.incoming[relationship.target_id].append(relationship)
        
        def get_entity(self, entity_id: str) -> Optional[Entity]:
            """Retrieve an entity by ID."""
            return self.entities.get(entity_id)
        
        def get_related_entities(
            self,
            entity_id: str,
            relation_type: Optional[RelationType] = None,
            direction: str = "outgoing"
        ) -> List[Entity]:
            """
            Get entities related to the given entity.
            
            Args:
                entity_id: Source entity ID
                relation_type: Optional filter by relationship type
                direction: "outgoing", "incoming", or "both"
            """
            related = []
            
            if direction in ["outgoing", "both"]:
                for rel in self.outgoing[entity_id]:
                    if relation_type is None or rel.type == relation_type:
                        related.append(self.entities[rel.target_id])
            
            if direction in ["incoming", "both"]:
                for rel in self.incoming[entity_id]:
                    if relation_type is None or rel.type == relation_type:
                        related.append(self.entities[rel.source_id])
            
            return related
        
        def find_path(
            self,
            start_id: str,
            end_id: str,
            max_depth: int = 5
        ) -> Optional[List[str]]:
            """
            Find a path between two entities using BFS.
            
            Returns list of entity IDs forming the path, or None if no path exists.
            """
            if start_id not in self.entities or end_id not in self.entities:
                return None
            
            if start_id == end_id:
                return [start_id]
            
            # BFS
            queue = [(start_id, [start_id])]
            visited = {start_id}
            
            while queue:
                current_id, path = queue.pop(0)
                
                if len(path) > max_depth:
                    continue
                
                # Explore neighbors
                for rel in self.outgoing[current_id]:
                    neighbor_id = rel.target_id
                    
                    if neighbor_id == end_id:
                        return path + [neighbor_id]
                    
                    if neighbor_id not in visited:
                        visited.add(neighbor_id)
                        queue.append((neighbor_id, path + [neighbor_id]))
            
            return None
        
        def get_subgraph(
            self,
            entity_ids: List[str],
            depth: int = 1
        ) -> Tuple[List[Entity], List[Relationship]]:
            """
            Extract a subgraph around given entities up to specified depth.
            """
            included_entities = set(entity_ids)
            
            # Expand to neighbors
            for _ in range(depth):
                new_entities = set()
                for entity_id in included_entities:
                    neighbors = self.get_related_entities(entity_id, direction="both")
                    new_entities.update(e.id for e in neighbors)
                included_entities.update(new_entities)
            
            # Get all entities
            entities = [self.entities[eid] for eid in included_entities if eid in self.entities]
            
            # Get relationships between included entities
            relationships = [
                rel for rel in self.relationships
                if rel.source_id in included_entities and rel.target_id in included_entities
            ]
            
            return entities, relationships
        
        def search_entities(
            self,
            query: str,
            entity_type: Optional[str] = None
        ) -> List[Entity]:
            """
            Search entities by name or attributes.
            Simple keyword-based search.
            """
            query_lower = query.lower()
            results = []
            
            for entity in self.entities.values():
                # Filter by type if specified
                if entity_type and entity.type != entity_type:
                    continue
                
                # Check name
                if query_lower in entity.name.lower():
                    results.append(entity)
                    continue
                
                # Check attributes
                for value in entity.attributes.values():
                    if isinstance(value, str) and query_lower in value.lower():
                        results.append(entity)
                        break
            
            return results
    
    
    class SemanticMemoryAgent:
        """
        Agent that maintains and reasons over a semantic memory network.
        
        Capabilities:
        - Extract entities and relationships from text
        - Store knowledge in graph structure
        - Answer questions using graph traversal
        - Discover implicit relationships
        """
        
        def __init__(self, temperature: float = 0.1):
            self.llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=temperature)
            self.graph = SemanticGraph()
            
            # Prompt for entity extraction
            self.extraction_prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert at extracting structured knowledge from text.
    Extract entities and relationships from the following text.
    
    Format your response as JSON:
    {{
      "entities": [
        {{"id": "unique_id", "name": "Entity Name", "type": "entity_type", "attributes": {{}}}},
        ...
      ],
      "relationships": [
        {{"source": "source_id", "target": "target_id", "type": "relationship_type"}},
        ...
      ]
    }}
    
    Valid relationship types: is_a, part_of, related_to, causes, precedes, located_in, owns, created_by, uses, similar_to
    
    Be specific and extract all meaningful entities and relationships."""),
                ("user", "{text}")
            ])
            
            # Prompt for query understanding
            self.query_prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert at answering questions using a knowledge graph.
    
    Given the following knowledge graph elements:
    {graph_context}
    
    Answer this question: {question}
    
    Provide a clear, concise answer based on the graph information."""),
                ("user", "{question}")
            ])
        
        def add_knowledge(self, text: str) -> Dict[str, Any]:
            """
            Extract and add knowledge from text to the semantic network.
            """
            # Extract entities and relationships
            chain = self.extraction_prompt | self.llm | StrOutputParser()
            result = chain.invoke({"text": text})
            
            # Parse JSON response
            try:
                # Try to extract JSON from response
                start_idx = result.find('{')
                end_idx = result.rfind('}') + 1
                if start_idx != -1 and end_idx != 0:
                    json_str = result[start_idx:end_idx]
                    knowledge = json.loads(json_str)
                else:
                    knowledge = json.loads(result)
            except json.JSONDecodeError:
                # If parsing fails, return error
                return {
                    "success": False,
                    "error": "Failed to parse extracted knowledge",
                    "raw_response": result
                }
            
            # Add entities
            entities_added = 0
            for entity_data in knowledge.get("entities", []):
                entity = Entity(
                    id=entity_data["id"],
                    name=entity_data["name"],
                    type=entity_data["type"],
                    attributes=entity_data.get("attributes", {})
                )
                self.graph.add_entity(entity)
                entities_added += 1
            
            # Add relationships
            relationships_added = 0
            for rel_data in knowledge.get("relationships", []):
                try:
                    # Map string to enum
                    rel_type_str = rel_data["type"].upper()
                    rel_type = RelationType[rel_type_str]
                    
                    relationship = Relationship(
                        source_id=rel_data["source"],
                        target_id=rel_data["target"],
                        type=rel_type
                    )
                    self.graph.add_relationship(relationship)
                    relationships_added += 1
                except (KeyError, ValueError) as e:
                    # Skip invalid relationships
                    continue
            
            return {
                "success": True,
                "entities_added": entities_added,
                "relationships_added": relationships_added
            }
        
        def query(self, question: str) -> str:
            """
            Answer a question using the semantic network.
            """
            # Search for relevant entities in the question
            words = question.lower().split()
            relevant_entities = set()
            
            for word in words:
                entities = self.graph.search_entities(word)
                relevant_entities.update(e.id for e in entities)
            
            if not relevant_entities:
                return "I don't have enough knowledge to answer that question."
            
            # Get subgraph around relevant entities
            entities, relationships = self.graph.get_subgraph(list(relevant_entities), depth=2)
            
            # Format graph context
            graph_context = self._format_graph_context(entities, relationships)
            
            # Generate answer
            chain = self.query_prompt | self.llm | StrOutputParser()
            answer = chain.invoke({
                "graph_context": graph_context,
                "question": question
            })
            
            return answer
        
        def find_relationship(
            self,
            entity1: str,
            entity2: str
        ) -> Optional[List[str]]:
            """
            Find how two entities are related through the graph.
            """
            # Find entities by name
            entities1 = self.graph.search_entities(entity1)
            entities2 = self.graph.search_entities(entity2)
            
            if not entities1 or not entities2:
                return None
            
            # Find shortest path
            path = self.graph.find_path(entities1[0].id, entities2[0].id)
            
            if path:
                # Convert IDs to names
                return [self.graph.get_entity(eid).name for eid in path]
            
            return None
        
        def get_related_concepts(
            self,
            concept: str,
            relation_type: Optional[str] = None
        ) -> List[str]:
            """
            Get concepts related to the given concept.
            """
            entities = self.graph.search_entities(concept)
            
            if not entities:
                return []
            
            # Convert relation type string to enum if provided
            rel_type_enum = None
            if relation_type:
                try:
                    rel_type_enum = RelationType[relation_type.upper()]
                except KeyError:
                    pass
            
            # Get related entities
            related = self.graph.get_related_entities(
                entities[0].id,
                relation_type=rel_type_enum,
                direction="both"
            )
            
            return [e.name for e in related]
        
        def _format_graph_context(
            self,
            entities: List[Entity],
            relationships: List[Relationship]
        ) -> str:
            """Format graph elements as readable context."""
            lines = ["Entities:"]
            for entity in entities[:10]:  # Limit to avoid token overflow
                attrs_str = ", ".join(f"{k}={v}" for k, v in entity.attributes.items())
                lines.append(f"  - {entity.name} ({entity.type})" + (f": {attrs_str}" if attrs_str else ""))
            
            lines.append("\nRelationships:")
            for rel in relationships[:20]:  # Limit relationships
                source = self.graph.get_entity(rel.source_id)
                target = self.graph.get_entity(rel.target_id)
                if source and target:
                    lines.append(f"  - {source.name} {rel.type.value} {target.name}")
            
            return "\n".join(lines)
        
        def get_statistics(self) -> Dict[str, Any]:
            """Get statistics about the semantic network."""
            entity_types = defaultdict(int)
            relation_types = defaultdict(int)
            
            for entity in self.graph.entities.values():
                entity_types[entity.type] += 1
            
            for rel in self.graph.relationships:
                relation_types[rel.type.value] += 1
            
            return {
                "total_entities": len(self.graph.entities),
                "total_relationships": len(self.graph.relationships),
                "entity_types": dict(entity_types),
                "relation_types": dict(relation_types)
            }
    
    
    def demonstrate_semantic_memory_networks():
        """
        Demonstrates semantic memory networks with knowledge extraction,
        graph-based queries, and relationship discovery.
        """
        print("=" * 80)
        print("SEMANTIC MEMORY NETWORKS DEMONSTRATION")
        print("=" * 80)
        
        # Create agent
        agent = SemanticMemoryAgent()
        
        # Test 1: Build knowledge base
        print("\n" + "=" * 80)
        print("Test 1: Building Knowledge Base")
        print("=" * 80)
        
        knowledge_texts = [
            """
            Python is a programming language created by Guido van Rossum.
            Python is used for web development, data analysis, and artificial intelligence.
            Python is similar to Ruby and JavaScript in terms of ease of use.
            """,
            """
            Machine Learning is a part of Artificial Intelligence.
            Machine Learning uses algorithms to analyze data and make predictions.
            TensorFlow and PyTorch are popular Machine Learning frameworks.
            TensorFlow was created by Google.
            """,
            """
            San Francisco is located in California.
            California is part of the United States.
            San Francisco is known for its technology industry.
            Many tech companies are located in San Francisco.
            """
        ]
        
        for i, text in enumerate(knowledge_texts, 1):
            print(f"\nAdding knowledge batch {i}...")
            result = agent.add_knowledge(text)
            if result["success"]:
                print(f"✓ Added {result['entities_added']} entities and {result['relationships_added']} relationships")
            else:
                print(f"✗ Error: {result.get('error', 'Unknown error')}")
        
        # Show statistics
        stats = agent.get_statistics()
        print(f"\nKnowledge base statistics:")
        print(f"  Total entities: {stats['total_entities']}")
        print(f"  Total relationships: {stats['total_relationships']}")
        print(f"  Entity types: {stats['entity_types']}")
        print(f"  Relation types: {stats['relation_types']}")
        
        # Test 2: Query knowledge
        print("\n" + "=" * 80)
        print("Test 2: Querying Knowledge Base")
        print("=" * 80)
        
        questions = [
            "What is Python used for?",
            "Who created Python?",
            "What are some Machine Learning frameworks?",
            "Where is San Francisco located?"
        ]
        
        for question in questions:
            print(f"\nQuestion: {question}")
            answer = agent.query(question)
            print(f"Answer: {answer}")
        
        # Test 3: Relationship discovery
        print("\n" + "=" * 80)
        print("Test 3: Discovering Relationships")
        print("=" * 80)
        
        concept_pairs = [
            ("Python", "Artificial Intelligence"),
            ("TensorFlow", "Google"),
            ("San Francisco", "United States")
        ]
        
        for concept1, concept2 in concept_pairs:
            print(f"\nFinding relationship between '{concept1}' and '{concept2}':")
            path = agent.find_relationship(concept1, concept2)
            if path:
                print(f"  Path: {' → '.join(path)}")
            else:
                print("  No direct relationship found")
        
        # Test 4: Related concepts
        print("\n" + "=" * 80)
        print("Test 4: Finding Related Concepts")
        print("=" * 80)
        
        concepts = ["Python", "Machine Learning", "San Francisco"]
        
        for concept in concepts:
            print(f"\nConcepts related to '{concept}':")
            related = agent.get_related_concepts(concept)
            if related:
                for rel_concept in related[:5]:  # Show top 5
                    print(f"  - {rel_concept}")
            else:
                print("  No related concepts found")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        print("""
    Semantic Memory Networks provide:
    ✓ Structured knowledge representation as graphs
    ✓ Entity and relationship extraction from text
    ✓ Graph-based querying and reasoning
    ✓ Relationship discovery through path finding
    ✓ Concept exploration through traversal
    
    This pattern excels at:
    - Knowledge management and organization
    - Question answering over structured data
    - Discovering implicit connections
    - Semantic search and retrieval
    - Building expert systems
    
    Key advantages:
    1. Explicit relationship modeling
    2. Efficient graph traversal
    3. Multi-hop reasoning support
    4. Scalable knowledge storage
    5. Interpretable knowledge representation
    
    Use semantic networks when you need to:
    - Maintain structured knowledge bases
    - Answer complex relational queries
    - Discover connections between concepts
    - Build domain-specific knowledge systems
    - Support inference and reasoning tasks
    """)
    
    
    if __name__ == "__main__":
        demonstrate_semantic_memory_networks()
    
        

    Agentic Design Pattern: Semantic Memory Networks

    Pattern 029: Semantic Memory Networks – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 029: Semantic Memory Networks

    Description: Semantic memory networks represent structured knowledge as graphs with entities, relationships, and attributes. This pattern enables sophisticated reasoning over interconnected concepts, supports graph-based queries, and facilitates knowledge discovery through relationship traversal.

    Components: – Entity: Nodes representing concepts, objects, or individuals – Relationship: Edges connecting entities with typed connections – Attributes: Properties associated with entities – Graph Store: Storage and indexing of the knowledge graph – Query Engine: Traversal and pattern matching over the graph – Reasoning Module: Inference and relationship discovery

    Use Cases: – Knowledge management systems – Question answering over structured knowledge – Relationship discovery and analysis – Semantic search and retrieval – Expert systems and decision support

    LangChain Implementation: Uses graph data structures, relationship modeling, and LLM-based query interpretation to create a semantic network that supports both explicit knowledge storage and implicit reasoning.


    File Statistics

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

    Module Documentation

    """
    Pattern 029: Semantic Memory Networks
    
    Description:
        Semantic memory networks represent structured knowledge as graphs with entities,
        relationships, and attributes. This pattern enables sophisticated reasoning over
        interconnected concepts, supports graph-based queries, and facilitates knowledge
        discovery through relationship traversal.
    
    Components:
        - Entity: Nodes representing concepts, objects, or individuals
        - Relationship: Edges connecting entities with typed connections
        - Attributes: Properties associated with entities
        - Graph Store: Storage and indexing of the knowledge graph
        - Query Engine: Traversal and pattern matching over the graph
        - Reasoning Module: Inference and relationship discovery
    
    Use Cases:
        - Knowledge management systems
        - Question answering over structured knowledge
        - Relationship discovery and analysis
        - Semantic search and retrieval
        - Expert systems and decision support
    
    LangChain Implementation:
        Uses graph data structures, relationship modeling, and LLM-based query
        interpretation to create a semantic network that supports both explicit
        knowledge storage and implicit reasoning.
    """
    

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


    Import Statements

    Standard Library Imports

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

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

    Lines 47-58

    Inherits from: Enum

    Purpose:

    Types of relationships between entities.
    

    Class: Entity

    Lines 62-74

    Purpose:

    Represents a node in the semantic network.
    

    Methods:

    __hash__(self)

    Lines 70-71

    Purpose: Hash operation.

    Parameters:

    __eq__(self, other)

    Lines 73-74

    Purpose: Eq operation.

    Parameters: – other: Other


    Class: Relationship

    Lines 78-85

    Purpose:

    Represents an edge in the semantic network.
    

    Class: QueryResult

    Lines 89-94

    Purpose:

    Result of a semantic query.
    

    Class: SemanticGraph

    Lines 97-257

    Purpose:

    Graph data structure for storing semantic knowledge.
    
    Supports efficient entity lookup, relationship traversal,
    and pattern matching operations.
    

    Methods:

    __init__(self)

    Lines 105-110

    Purpose: Initialize the class instance with required parameters.

    Parameters:

    add_entity(self, entity)

    Lines 112-114

    Purpose:

    Add an entity to the graph.
    

    Parameters: – entity: Entity

    Returns: None

    add_relationship(self, relationship)

    Lines 116-126

    Purpose:

    Add a relationship to the graph.
    

    Parameters: – relationship: Relationship

    Returns: None

    get_entity(self, entity_id)

    Lines 128-130

    Purpose:

    Retrieve an entity by ID.
    

    Parameters: – entity_id: Entity id

    Returns: <ast.Subscript object at 0x000001A06D749A10>

    get_related_entities(self, entity_id, relation_type, direction)

    Lines 132-158

    Purpose:

    Get entities related to the given entity.
    
    Args:
        entity_id: Source entity ID
        relation_type: Optional filter by relationship type
        direction: "outgoing", "incoming", or "both"
    

    Parameters: – entity_id: Entity id – relation_type: Relation type – direction: Direction

    Returns: <ast.Subscript object at 0x000001A06D74BC50>

    find_path(self, start_id, end_id, max_depth)

    Lines 160-198

    Purpose:

    Find a path between two entities using BFS.
    
    Returns list of entity IDs forming the path, or None if no path exists.
    

    Parameters: – start_id: Start id – end_id: End id – max_depth: Max depth

    Returns: <ast.Subscript object at 0x000001A06D751A10>

    get_subgraph(self, entity_ids, depth)

    Lines 200-227

    Purpose:

    Extract a subgraph around given entities up to specified depth.
    

    Parameters: – entity_ids: Entity ids – depth: Depth

    Returns: <ast.Subscript object at 0x000001A06D615850>

    search_entities(self, query, entity_type)

    Lines 229-257

    Purpose:

    Search entities by name or attributes.
    Simple keyword-based search.
    

    Parameters: – query: Input query or question – entity_type: Entity type

    Returns: <ast.Subscript object at 0x000001A06D620F90>


    Class: SemanticMemoryAgent

    Lines 260-494

    Purpose:

    Agent that maintains and reasons over a semantic memory network.
    
    Capabilities:
    - Extract entities and relationships from text
    - Store knowledge in graph structure
    - Answer questions using graph traversal
    - Discover implicit relationships
    

    Methods:

    __init__(self, temperature)

    Lines 271-309

    Purpose: Initialize the class instance with required parameters.

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

    add_knowledge(self, text)

    Lines 311-372

    Purpose:

    Extract and add knowledge from text to the semantic network.
    

    Parameters: – text: Text

    Returns: <ast.Subscript object at 0x000001A06D64E590>

    query(self, question)

    Lines 374-402

    Purpose:

    Answer a question using the semantic network.
    

    Parameters: – question: Question to answer

    Returns: str

    find_relationship(self, entity1, entity2)

    Lines 404-426

    Purpose:

    Find how two entities are related through the graph.
    

    Parameters: – entity1: Entity1 – entity2: Entity2

    Returns: <ast.Subscript object at 0x000001A06D15B2D0>

    get_related_concepts(self, concept, relation_type)

    Lines 428-456

    Purpose:

    Get concepts related to the given concept.
    

    Parameters: – concept: Concept – relation_type: Relation type

    Returns: <ast.Subscript object at 0x000001A06D703450>

    _format_graph_context(self, entities, relationships)

    Lines 458-476

    Purpose:

    Format graph elements as readable context.
    

    Parameters: – entities: Entities – relationships: Relationships

    Returns: str

    get_statistics(self)

    Lines 478-494

    Purpose:

    Get statistics about the semantic network.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D15CD50>


    Functions

    demonstrate_semantic_memory_networks()

    Lines 497-634

    Purpose:

    Demonstrates semantic memory networks with knowledge extraction,
    graph-based queries, and relationship discovery.
    

    Key Concepts Summary

    Pattern Overview

    The Semantic Memory 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 028
    • Pattern 030

    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 Semantic Memory 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: 029_semantic_memory_networks.py
    Total Lines: 639
    Pattern Category: AI Agent Design Pattern #029


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Working Memory

    
        """
    Pattern 028: Working Memory
    
    Description:
        The Working Memory pattern provides an active workspace for the agent's current
        task processing. Unlike short-term memory (conversation history) or long-term
        memory (persistent storage), working memory holds intermediate results, active
        goals, and context needed for the current cognitive task. It's dynamically
        updated as the agent progresses through multi-step reasoning.
    
    Components:
        - Active Goals: Current objectives being pursued
        - Intermediate Results: Partial solutions and computations
        - Task Context: Relevant information for current task
        - Attention Focus: What the agent is currently processing
        - Scratch Space: Temporary storage for calculations
    
    Use Cases:
        - Multi-step problem solving
        - Complex reasoning tasks
        - Task decomposition and execution
        - Mathematical computations
        - Planning and execution monitoring
    
    LangChain Implementation:
        Uses structured state management to track active goals, intermediate
        results, and task progress. Implements focus mechanisms to maintain
        relevant context during complex operations.
    
    Key Features:
        - Task-scoped memory
        - Goal tracking
        - Intermediate result storage
        - Dynamic updates
        - Context relevance filtering
    """
    
    import os
    from typing import List, Dict, Any, Optional, Set
    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 GoalStatus(Enum):
        """Status of a goal in working memory."""
        ACTIVE = "active"
        IN_PROGRESS = "in_progress"
        COMPLETED = "completed"
        BLOCKED = "blocked"
        ABANDONED = "abandoned"
    
    
    @dataclass
    class Goal:
        """Represents a goal in working memory."""
        id: str
        description: str
        status: GoalStatus = GoalStatus.ACTIVE
        parent_goal_id: Optional[str] = None
        sub_goals: List[str] = field(default_factory=list)
        result: Optional[Any] = None
        metadata: Dict[str, Any] = field(default_factory=dict)
        created_at: datetime = field(default_factory=datetime.now)
        
        def to_dict(self) -> Dict[str, Any]:
            """Convert to dictionary."""
            return {
                "id": self.id,
                "description": self.description,
                "status": self.status.value,
                "parent_goal_id": self.parent_goal_id,
                "sub_goals": self.sub_goals,
                "result": str(self.result) if self.result is not None else None,
                "metadata": self.metadata
            }
    
    
    @dataclass
    class IntermediateResult:
        """Stores intermediate computation results."""
        id: str
        description: str
        value: Any
        goal_id: str
        timestamp: datetime = field(default_factory=datetime.now)
        
        def to_dict(self) -> Dict[str, Any]:
            """Convert to dictionary."""
            return {
                "id": self.id,
                "description": self.description,
                "value": str(self.value),
                "goal_id": self.goal_id,
                "timestamp": self.timestamp.isoformat()
            }
    
    
    class WorkingMemorySpace:
        """
        Working memory workspace for active task processing.
        """
        
        def __init__(self, capacity: int = 7):
            """
            Initialize working memory.
            
            Args:
                capacity: Maximum number of active items (Miller's Law: 7±2)
            """
            self.capacity = capacity
            self.goals: Dict[str, Goal] = {}
            self.intermediate_results: Dict[str, IntermediateResult] = {}
            self.task_context: Dict[str, Any] = {}
            self.attention_focus: Optional[str] = None  # Current goal ID
            self.scratch_space: Dict[str, Any] = {}
            
        def add_goal(
            self,
            goal_id: str,
            description: str,
            parent_goal_id: Optional[str] = None
        ) -> Goal:
            """
            Add a new goal to working memory.
            
            Args:
                goal_id: Unique goal identifier
                description: Goal description
                parent_goal_id: Parent goal if this is a sub-goal
                
            Returns:
                Created goal
            """
            goal = Goal(
                id=goal_id,
                description=description,
                parent_goal_id=parent_goal_id
            )
            
            self.goals[goal_id] = goal
            
            # Link to parent
            if parent_goal_id and parent_goal_id in self.goals:
                self.goals[parent_goal_id].sub_goals.append(goal_id)
            
            # Set as focus if first goal
            if self.attention_focus is None:
                self.attention_focus = goal_id
            
            return goal
        
        def update_goal_status(self, goal_id: str, status: GoalStatus, result: Any = None):
            """Update goal status and optionally store result."""
            if goal_id in self.goals:
                self.goals[goal_id].status = status
                if result is not None:
                    self.goals[goal_id].result = result
        
        def store_intermediate(
            self,
            result_id: str,
            description: str,
            value: Any,
            goal_id: str
        ):
            """Store an intermediate result."""
            result = IntermediateResult(
                id=result_id,
                description=description,
                value=value,
                goal_id=goal_id
            )
            self.intermediate_results[result_id] = result
        
        def get_active_goals(self) -> List[Goal]:
            """Get all active goals."""
            return [
                goal for goal in self.goals.values()
                if goal.status in [GoalStatus.ACTIVE, GoalStatus.IN_PROGRESS]
            ]
        
        def get_current_focus(self) -> Optional[Goal]:
            """Get currently focused goal."""
            if self.attention_focus and self.attention_focus in self.goals:
                return self.goals[self.attention_focus]
            return None
        
        def shift_focus(self, goal_id: str):
            """Shift attention to a different goal."""
            if goal_id in self.goals:
                self.attention_focus = goal_id
        
        def get_goal_context(self, goal_id: str) -> Dict[str, Any]:
            """Get all relevant context for a goal."""
            if goal_id not in self.goals:
                return {}
            
            goal = self.goals[goal_id]
            context = {
                "goal": goal.to_dict(),
                "intermediate_results": [
                    ir.to_dict() for ir in self.intermediate_results.values()
                    if ir.goal_id == goal_id
                ],
                "sub_goals": [
                    self.goals[sg_id].to_dict()
                    for sg_id in goal.sub_goals
                    if sg_id in self.goals
                ]
            }
            
            return context
        
        def get_capacity_usage(self) -> float:
            """Get percentage of working memory capacity used."""
            active_items = len(self.get_active_goals()) + len(self.intermediate_results)
            return min(100.0, (active_items / self.capacity) * 100)
        
        def clear(self):
            """Clear working memory."""
            self.goals.clear()
            self.intermediate_results.clear()
            self.task_context.clear()
            self.attention_focus = None
            self.scratch_space.clear()
        
        def get_memory_state(self) -> Dict[str, Any]:
            """Get complete state of working memory."""
            return {
                "capacity": self.capacity,
                "capacity_usage": self.get_capacity_usage(),
                "total_goals": len(self.goals),
                "active_goals": len(self.get_active_goals()),
                "intermediate_results": len(self.intermediate_results),
                "current_focus": self.attention_focus,
                "goals": [g.to_dict() for g in self.goals.values()],
                "results": [ir.to_dict() for ir in self.intermediate_results.values()]
            }
    
    
    class WorkingMemoryAgent:
        """
        Agent with working memory for complex task processing.
        """
        
        def __init__(self, model: str = "gpt-3.5-turbo"):
            """
            Initialize agent with working memory.
            
            Args:
                model: LLM model to use
            """
            self.llm = ChatOpenAI(model=model, temperature=0.3)
            self.working_memory = WorkingMemorySpace()
            self.task_counter = 0
        
        def solve_problem(self, problem: str) -> Dict[str, Any]:
            """
            Solve a problem using working memory.
            
            Args:
                problem: Problem description
                
            Returns:
                Solution with working memory trace
            """
            print(f"\n[Agent] Solving problem: {problem}")
            print(f"[Agent] Initializing working memory...\n")
            
            # Clear working memory for new task
            self.working_memory.clear()
            
            # Create main goal
            main_goal_id = "goal_0"
            self.working_memory.add_goal(main_goal_id, problem)
            
            # Decompose problem into sub-goals
            print("[Step 1] Decomposing problem into sub-goals...")
            sub_goals = self._decompose_problem(problem)
            
            for i, sub_goal in enumerate(sub_goals):
                sub_goal_id = f"goal_{i+1}"
                self.working_memory.add_goal(sub_goal_id, sub_goal, main_goal_id)
                print(f"  Sub-goal {i+1}: {sub_goal}")
            
            print(f"\n[Working Memory] Capacity: {self.working_memory.get_capacity_usage():.1f}%\n")
            
            # Process each sub-goal
            for i, sub_goal in enumerate(sub_goals):
                sub_goal_id = f"goal_{i+1}"
                
                # Shift focus to this sub-goal
                self.working_memory.shift_focus(sub_goal_id)
                current_focus = self.working_memory.get_current_focus()
                
                print(f"[Step {i+2}] Processing: {current_focus.description}")
                
                # Mark as in progress
                self.working_memory.update_goal_status(sub_goal_id, GoalStatus.IN_PROGRESS)
                
                # Solve sub-goal
                result = self._solve_sub_goal(sub_goal)
                
                # Store result
                self.working_memory.update_goal_status(
                    sub_goal_id,
                    GoalStatus.COMPLETED,
                    result
                )
                
                # Store as intermediate result
                self.working_memory.store_intermediate(
                    f"result_{i}",
                    f"Solution to: {sub_goal}",
                    result,
                    sub_goal_id
                )
                
                print(f"  ✓ Result: {result}")
                print(f"  [Working Memory] Capacity: {self.working_memory.get_capacity_usage():.1f}%\n")
            
            # Synthesize final solution
            print("[Final Step] Synthesizing solution from sub-goals...")
            final_solution = self._synthesize_solution(problem)
            
            # Mark main goal as completed
            self.working_memory.update_goal_status(
                main_goal_id,
                GoalStatus.COMPLETED,
                final_solution
            )
            
            print(f"\n✓ Solution: {final_solution}\n")
            
            # Get memory state
            memory_state = self.working_memory.get_memory_state()
            
            return {
                "problem": problem,
                "solution": final_solution,
                "sub_goals": sub_goals,
                "working_memory_trace": memory_state
            }
        
        def _decompose_problem(self, problem: str) -> List[str]:
            """Decompose problem into sub-goals."""
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert at problem decomposition. Break down
    the given problem into 3-5 clear, actionable sub-goals."""),
                ("user", """Problem: {problem}
    
    List 3-5 sub-goals needed to solve this problem. Be specific and actionable.
    Respond with one sub-goal per line.""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            response = chain.invoke({"problem": problem})
            
            # Parse sub-goals
            lines = response.strip().split("\n")
            sub_goals = []
            for line in lines:
                line = line.strip()
                # Remove numbering
                for prefix in ["1.", "2.", "3.", "4.", "5.", "-", "*", "•"]:
                    if line.startswith(prefix):
                        line = line[len(prefix):].strip()
                if line:
                    sub_goals.append(line)
            
            return sub_goals[:5]  # Limit to 5 sub-goals
        
        def _solve_sub_goal(self, sub_goal: str) -> str:
            """Solve a single sub-goal."""
            # Get context from working memory
            focus = self.working_memory.get_current_focus()
            context = self.working_memory.get_goal_context(focus.id) if focus else {}
            
            # Get previously completed results
            previous_results = [
                ir for ir in self.working_memory.intermediate_results.values()
            ]
            
            previous_context = ""
            if previous_results:
                previous_context = "\n".join([
                    f"- {ir.description}: {ir.value}"
                    for ir in previous_results
                ])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "You solve sub-goals clearly and concisely."),
                ("user", """Sub-goal: {sub_goal}
    
    Previous Results:
    {previous_context}
    
    Provide a clear, concise solution:""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            result = chain.invoke({
                "sub_goal": sub_goal,
                "previous_context": previous_context if previous_context else "None yet"
            })
            
            return result.strip()
        
        def _synthesize_solution(self, problem: str) -> str:
            """Synthesize final solution from sub-goal results."""
            # Get all intermediate results
            results_text = "\n".join([
                f"{i+1}. {ir.description}\n   {ir.value}"
                for i, ir in enumerate(self.working_memory.intermediate_results.values())
            ])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "You synthesize complete solutions from sub-goal results."),
                ("user", """Original Problem: {problem}
    
    Sub-goal Results:
    {results_text}
    
    Provide a complete, coherent solution to the original problem:""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            solution = chain.invoke({
                "problem": problem,
                "results_text": results_text
            })
            
            return solution.strip()
    
    
    def demonstrate_working_memory():
        """Demonstrate the Working Memory pattern."""
        
        print("=" * 80)
        print("WORKING MEMORY PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = WorkingMemoryAgent()
        
        # Test 1: Multi-step math problem
        print("\n" + "=" * 80)
        print("TEST 1: Multi-Step Mathematical Problem")
        print("=" * 80)
        
        problem1 = """A store sells apples for $2 each and oranges for $3 each. 
    If you buy 5 apples and 4 oranges, get a 10% discount, and pay 8% tax, 
    what is the final price?"""
        
        result1 = agent.solve_problem(problem1)
        
        print("=" * 80)
        print("WORKING MEMORY TRACE")
        print("=" * 80)
        print(f"Goals tracked: {result1['working_memory_trace']['total_goals']}")
        print(f"Active computations: {result1['working_memory_trace']['intermediate_results']}")
        print(f"Capacity used: {result1['working_memory_trace']['capacity_usage']:.1f}%")
        
        # Test 2: Planning task
        print("\n\n" + "=" * 80)
        print("TEST 2: Planning Task")
        print("=" * 80)
        
        problem2 = """Plan a weekend trip to a nearby city including:
    transportation, accommodation, activities, and budget estimation."""
        
        result2 = agent.solve_problem(problem2)
        
        print("=" * 80)
        print("WORKING MEMORY TRACE")
        print("=" * 80)
        print(f"Goals tracked: {result2['working_memory_trace']['total_goals']}")
        print(f"Sub-goals completed: {len(result2['sub_goals'])}")
        
        # Test 3: Analytical task
        print("\n\n" + "=" * 80)
        print("TEST 3: Analytical Reasoning")
        print("=" * 80)
        
        problem3 = """Analyze the pros and cons of remote work and provide
    a recommendation for a tech company."""
        
        result3 = agent.solve_problem(problem3)
        
        print("=" * 80)
        print("WORKING MEMORY TRACE")
        print("=" * 80)
        for goal in result3['working_memory_trace']['goals']:
            print(f"\nGoal: {goal['description']}")
            print(f"  Status: {goal['status']}")
            if goal['result']:
                preview = goal['result'][:100] + "..." if len(goal['result']) > 100 else goal['result']
                print(f"  Result: {preview}")
        
        # Summary
        print("\n\n" + "=" * 80)
        print("PATTERN SUMMARY")
        print("=" * 80)
        print("""
    The Working Memory pattern demonstrates:
    
    1. **Active Workspace**: Maintains current task context
    2. **Goal Tracking**: Monitors progress on objectives
    3. **Intermediate Storage**: Stores partial results
    4. **Attention Focus**: Tracks current processing target
    5. **Capacity Management**: Limited capacity (Miller's Law)
    
    Key Components:
    
    Goals:
    - Hierarchical goal structure (main goal → sub-goals)
    - Status tracking (active, in-progress, completed, blocked)
    - Result storage
    - Parent-child relationships
    
    Intermediate Results:
    - Temporary computation storage
    - Linked to specific goals
    - Time-stamped for ordering
    - Available for later steps
    
    Attention Focus:
    - Single goal actively processed
    - Context switching between goals
    - Maintains processing coherence
    
    Working Memory Characteristics:
    
    Capacity Limitation (Miller's Law):
    - 7±2 items simultaneously
    - Prevents cognitive overload
    - Forces prioritization
    - Matches human working memory
    
    Dynamic Updates:
    - Content changes as task progresses
    - Old items removed when complete
    - New items added as needed
    - Maintains relevance
    
    Task-Scoped:
    - Cleared between tasks
    - Fresh workspace for each problem
    - No cross-task contamination
    
    Differences from Other Memory Types:
    
    vs Short-Term Memory:
    - Working: Active processing workspace
    - Short-term: Conversation history buffer
    - Working is goal-oriented, short-term is sequential
    
    vs Long-Term Memory:
    - Working: Temporary, task-scoped
    - Long-term: Permanent, cross-session
    - Working is volatile, long-term persists
    
    Use Cases:
    - Multi-step problem solving
    - Complex calculations
    - Task planning and execution
    - Reasoning chains
    - Resource allocation
    
    Benefits:
    - **Organized Processing**: Structured approach to complexity
    - **Progress Tracking**: Monitor goal completion
    - **Context Maintenance**: Relevant information available
    - **Intermediate Access**: Use partial results
    - **Cognitive Modeling**: Mimics human working memory
    
    This pattern is essential for agents tackling complex, multi-step
    tasks that require maintaining and manipulating intermediate state.
    """)
    
    
    if __name__ == "__main__":
        demonstrate_working_memory()
    
        

    Agentic Design Pattern: Working Memory

    Pattern 028: Working Memory – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 028: Working Memory

    Description: The Working Memory pattern provides an active workspace for the agent’s current task processing. Unlike short-term memory (conversation history) or long-term memory (persistent storage), working memory holds intermediate results, active goals, and context needed for the current cognitive task. It’s dynamically updated as the agent progresses through multi-step reasoning.

    Components: – Active Goals: Current objectives being pursued – Intermediate Results: Partial solutions and computations – Task Context: Relevant information for current task – Attention Focus: What the agent is currently processing – Scratch Space: Temporary storage for calculations

    Use Cases: – Multi-step problem solving – Complex reasoning tasks – Task decomposition and execution – Mathematical computations – Planning and execution monitoring

    LangChain Implementation: Uses structured state management to track active goals, intermediate results, and task progress. Implements focus mechanisms to maintain relevant context during complex operations.

    Key Features: – Task-scoped memory – Goal tracking – Intermediate result storage – Dynamic updates – Context relevance filtering


    File Statistics

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

    Module Documentation

    """
    Pattern 028: Working Memory
    
    Description:
        The Working Memory pattern provides an active workspace for the agent's current
        task processing. Unlike short-term memory (conversation history) or long-term
        memory (persistent storage), working memory holds intermediate results, active
        goals, and context needed for the current cognitive task. It's dynamically
        updated as the agent progresses through multi-step reasoning.
    
    Components:
        - Active Goals: Current objectives being pursued
        - Intermediate Results: Partial solutions and computations
        - Task Context: Relevant information for current task
        - Attention Focus: What the agent is currently processing
        - Scratch Space: Temporary storage for calculations
    
    Use Cases:
        - Multi-step problem solving
        - Complex reasoning tasks
        - Task decomposition and execution
        - Mathematical computations
        - Planning and execution monitoring
    
    LangChain Implementation:
        Uses structured state management to track active goals, intermediate
        results, and task progress. Implements focus mechanisms to maintain
        relevant context during complex operations.
    
    Key Features:
        - Task-scoped memory
        - Goal tracking
        - Intermediate result storage
        - Dynamic updates
        - Context relevance filtering
    """
    

    Purpose: This module implements the Working Memory 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: GoalStatus

    Lines 52-58

    Inherits from: Enum

    Purpose:

    Status of a goal in working memory.
    

    Class: Goal

    Lines 62-83

    Purpose:

    Represents a goal in working memory.
    

    Methods:

    to_dict(self)

    Lines 73-83

    Purpose:

    Convert to dictionary.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D66AB90>


    Class: IntermediateResult

    Lines 87-103

    Purpose:

    Stores intermediate computation results.
    

    Methods:

    to_dict(self)

    Lines 95-103

    Purpose:

    Convert to dictionary.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D6A3390>


    Class: WorkingMemorySpace

    Lines 106-246

    Purpose:

    Working memory workspace for active task processing.
    

    Methods:

    __init__(self, capacity)

    Lines 111-123

    Purpose:

    Initialize working memory.
    
    Args:
        capacity: Maximum number of active items (Miller's Law: 7±2)
    

    Parameters: – capacity: Capacity

    add_goal(self, goal_id, description, parent_goal_id)

    Lines 125-158

    Purpose:

    Add a new goal to working memory.
    
    Args:
        goal_id: Unique goal identifier
        description: Goal description
        parent_goal_id: Parent goal if this is a sub-goal
    
    Returns:
        Created goal
    

    Parameters: – goal_id: Goal id – description: Description – parent_goal_id: Parent goal id

    Returns: Goal

    update_goal_status(self, goal_id, status, result)

    Lines 160-165

    Purpose:

    Update goal status and optionally store result.
    

    Parameters: – goal_id: Goal id – status: Status – result: Result

    store_intermediate(self, result_id, description, value, goal_id)

    Lines 167-181

    Purpose:

    Store an intermediate result.
    

    Parameters: – result_id: Result id – description: Description – value: Value – goal_id: Goal id

    get_active_goals(self)

    Lines 183-188

    Purpose:

    Get all active goals.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D156A50>

    get_current_focus(self)

    Lines 190-194

    Purpose:

    Get currently focused goal.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D658710>

    shift_focus(self, goal_id)

    Lines 196-199

    Purpose:

    Shift attention to a different goal.
    

    Parameters: – goal_id: Goal id

    get_goal_context(self, goal_id)

    Lines 201-220

    Purpose:

    Get all relevant context for a goal.
    

    Parameters: – goal_id: Goal id

    Returns: <ast.Subscript object at 0x000001A06D739FD0>

    get_capacity_usage(self)

    Lines 222-225

    Purpose:

    Get percentage of working memory capacity used.
    

    Parameters:

    Returns: float

    clear(self)

    Lines 227-233

    Purpose:

    Clear working memory.
    

    Parameters:

    get_memory_state(self)

    Lines 235-246

    Purpose:

    Get complete state of working memory.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D62ABD0>


    Class: WorkingMemoryAgent

    Lines 249-444

    Purpose:

    Agent with working memory for complex task processing.
    

    Methods:

    __init__(self, model)

    Lines 254-263

    Purpose:

    Initialize agent with working memory.
    
    Args:
        model: LLM model to use
    

    Parameters: – model: Language model instance

    solve_problem(self, problem)

    Lines 265-351

    Purpose:

    Solve a problem using working memory.
    
    Args:
        problem: Problem description
    
    Returns:
        Solution with working memory trace
    

    Parameters: – problem: Problem

    Returns: <ast.Subscript object at 0x000001A06D60A890>

    _decompose_problem(self, problem)

    Lines 353-380

    Purpose:

    Decompose problem into sub-goals.
    

    Parameters: – problem: Problem

    Returns: <ast.Subscript object at 0x000001A06D5F7C50>

    _solve_sub_goal(self, sub_goal)

    Lines 382-417

    Purpose:

    Solve a single sub-goal.
    

    Parameters: – sub_goal: Sub goal

    Returns: str

    _synthesize_solution(self, problem)

    Lines 419-444

    Purpose:

    Synthesize final solution from sub-goal results.
    

    Parameters: – problem: Problem

    Returns: str


    Functions

    demonstrate_working_memory()

    Lines 447-589

    Purpose:

    Demonstrate the Working Memory pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Working 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 027
    • Pattern 029

    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 Working 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: 028_working_memory.py
    Total Lines: 594
    Pattern Category: AI Agent Design Pattern #028


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Long Term Memory

    
        """
    Pattern 027: Long-Term Memory
    
    Description:
        The Long-Term Memory pattern enables agents to store and retrieve information
        across multiple sessions and conversations. Unlike short-term memory that exists
        only within a session, long-term memory persists indefinitely, allowing agents
        to remember user preferences, past interactions, learned facts, and accumulated
        knowledge over time.
    
    Components:
        - Persistent Storage: Vector databases, traditional databases, file systems
        - Memory Indexing: Efficient retrieval mechanisms
        - Memory Types: Episodic (events), Semantic (facts), Procedural (skills)
        - Retrieval System: Similarity search, keyword matching
        - Memory Consolidation: Organizing and optimizing stored memories
    
    Use Cases:
        - Personalized user experiences
        - Learning from past interactions
        - Knowledge accumulation
        - User preference tracking
        - Long-term relationship building
    
    LangChain Implementation:
        Uses vector stores (Chroma, FAISS) for semantic memory storage,
        implements retrieval strategies, and combines with conversation
        context for personalized interactions.
    
    Key Features:
        - Cross-session persistence
        - Semantic similarity search
        - Memory categorization
        - Efficient retrieval
        - Memory aging and importance
    """
    
    import os
    from typing import List, Dict, Any, Optional, Tuple
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    import json
    from pathlib import Path
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI, OpenAIEmbeddings
    
    load_dotenv()
    
    
    class MemoryType(Enum):
        """Types of long-term memories."""
        EPISODIC = "episodic"  # Specific events and experiences
        SEMANTIC = "semantic"  # Facts and knowledge
        PROCEDURAL = "procedural"  # Skills and procedures
        PREFERENCE = "preference"  # User preferences
        INTERACTION = "interaction"  # Past interactions
    
    
    @dataclass
    class Memory:
        """Represents a long-term memory."""
        id: str
        content: str
        memory_type: MemoryType
        timestamp: datetime
        importance: float = 1.0  # 0-1 scale
        metadata: Dict[str, Any] = field(default_factory=dict)
        access_count: int = 0
        last_accessed: Optional[datetime] = None
        
        def to_dict(self) -> Dict[str, Any]:
            """Convert to dictionary."""
            return {
                "id": self.id,
                "content": self.content,
                "memory_type": self.memory_type.value,
                "timestamp": self.timestamp.isoformat(),
                "importance": self.importance,
                "metadata": self.metadata,
                "access_count": self.access_count,
                "last_accessed": self.last_accessed.isoformat() if self.last_accessed else None
            }
        
        @classmethod
        def from_dict(cls, data: Dict[str, Any]) -> 'Memory':
            """Create from dictionary."""
            return cls(
                id=data["id"],
                content=data["content"],
                memory_type=MemoryType(data["memory_type"]),
                timestamp=datetime.fromisoformat(data["timestamp"]),
                importance=data.get("importance", 1.0),
                metadata=data.get("metadata", {}),
                access_count=data.get("access_count", 0),
                last_accessed=datetime.fromisoformat(data["last_accessed"]) if data.get("last_accessed") else None
            )
    
    
    class SimpleVectorStore:
        """
        Simple vector store for semantic memory storage (mock implementation).
        In production, would use Chroma, FAISS, or Pinecone.
        """
        
        def __init__(self):
            """Initialize vector store."""
            self.memories: List[Memory] = []
            self.embeddings_model = None  # Would use OpenAIEmbeddings() in production
        
        def add_memory(self, memory: Memory):
            """Add a memory to the store."""
            self.memories.append(memory)
        
        def search(
            self,
            query: str,
            memory_type: Optional[MemoryType] = None,
            k: int = 5
        ) -> List[Memory]:
            """
            Search for relevant memories.
            
            Args:
                query: Search query
                memory_type: Filter by memory type
                k: Number of results
                
            Returns:
                List of relevant memories
            """
            # Filter by type if specified
            candidates = self.memories
            if memory_type:
                candidates = [m for m in candidates if m.memory_type == memory_type]
            
            # Simple keyword matching (in production, would use vector similarity)
            query_lower = query.lower()
            scored_memories = []
            
            for memory in candidates:
                # Simple scoring based on keyword matches
                content_lower = memory.content.lower()
                score = 0.0
                
                for word in query_lower.split():
                    if word in content_lower:
                        score += 1.0
                
                # Boost by importance and recency
                score *= memory.importance
                
                days_old = (datetime.now() - memory.timestamp).days
                recency_factor = 1.0 / (1.0 + days_old * 0.01)
                score *= recency_factor
                
                if score > 0:
                    scored_memories.append((score, memory))
            
            # Sort by score and return top k
            scored_memories.sort(reverse=True, key=lambda x: x[0])
            results = [memory for _, memory in scored_memories[:k]]
            
            # Update access statistics
            for memory in results:
                memory.access_count += 1
                memory.last_accessed = datetime.now()
            
            return results
        
        def get_all_memories(self) -> List[Memory]:
            """Get all memories."""
            return self.memories.copy()
        
        def delete_memory(self, memory_id: str):
            """Delete a memory by ID."""
            self.memories = [m for m in self.memories if m.id != memory_id]
    
    
    class PersistentMemoryStore:
        """
        Persistent storage for long-term memory (file-based mock).
        In production, would use proper database.
        """
        
        def __init__(self, storage_path: str = "memory_store.json"):
            """
            Initialize persistent store.
            
            Args:
                storage_path: Path to storage file
            """
            self.storage_path = Path(storage_path)
            self.vector_store = SimpleVectorStore()
            self._load()
        
        def _load(self):
            """Load memories from disk."""
            if self.storage_path.exists():
                try:
                    with open(self.storage_path, 'r') as f:
                        data = json.load(f)
                        for memory_data in data:
                            memory = Memory.from_dict(memory_data)
                            self.vector_store.add_memory(memory)
                except Exception as e:
                    print(f"Warning: Could not load memories: {e}")
        
        def _save(self):
            """Save memories to disk."""
            try:
                data = [m.to_dict() for m in self.vector_store.get_all_memories()]
                with open(self.storage_path, 'w') as f:
                    json.dump(data, f, indent=2)
            except Exception as e:
                print(f"Warning: Could not save memories: {e}")
        
        def add_memory(self, memory: Memory):
            """Add and persist a memory."""
            self.vector_store.add_memory(memory)
            self._save()
        
        def search(
            self,
            query: str,
            memory_type: Optional[MemoryType] = None,
            k: int = 5
        ) -> List[Memory]:
            """Search for relevant memories."""
            return self.vector_store.search(query, memory_type, k)
        
        def get_all_memories(self) -> List[Memory]:
            """Get all stored memories."""
            return self.vector_store.get_all_memories()
        
        def clear(self):
            """Clear all memories."""
            self.vector_store.memories.clear()
            self._save()
    
    
    class LongTermMemoryAgent:
        """
        Agent with long-term memory capabilities.
        """
        
        def __init__(
            self,
            user_id: str,
            storage_path: Optional[str] = None,
            model: str = "gpt-3.5-turbo"
        ):
            """
            Initialize agent with long-term memory.
            
            Args:
                user_id: Unique user identifier
                storage_path: Path to memory storage
                model: LLM model to use
            """
            self.user_id = user_id
            self.llm = ChatOpenAI(model=model, temperature=0.7)
            
            # Initialize memory store
            if storage_path is None:
                storage_path = f"memory_{user_id}.json"
            self.memory_store = PersistentMemoryStore(storage_path)
            
            self.interaction_count = 0
        
        def store_memory(
            self,
            content: str,
            memory_type: MemoryType,
            importance: float = 1.0,
            metadata: Optional[Dict] = None
        ):
            """
            Store a new memory.
            
            Args:
                content: Memory content
                memory_type: Type of memory
                importance: Importance score (0-1)
                metadata: Additional metadata
            """
            memory = Memory(
                id=f"{self.user_id}_{datetime.now().timestamp()}",
                content=content,
                memory_type=memory_type,
                timestamp=datetime.now(),
                importance=importance,
                metadata=metadata or {}
            )
            self.memory_store.add_memory(memory)
        
        def recall_memories(
            self,
            query: str,
            memory_type: Optional[MemoryType] = None,
            k: int = 5
        ) -> List[Memory]:
            """
            Recall relevant memories.
            
            Args:
                query: Query for memory retrieval
                memory_type: Filter by type
                k: Number of memories to retrieve
                
            Returns:
                List of relevant memories
            """
            return self.memory_store.search(query, memory_type, k)
        
        def chat(self, user_message: str) -> str:
            """
            Chat with memory-augmented responses.
            
            Args:
                user_message: User's message
                
            Returns:
                Agent's response
            """
            self.interaction_count += 1
            
            # Recall relevant memories
            relevant_memories = self.recall_memories(user_message, k=3)
            
            # Format memories for context
            memory_context = ""
            if relevant_memories:
                memory_lines = []
                for mem in relevant_memories:
                    memory_lines.append(
                        f"[{mem.memory_type.value}] {mem.content} "
                        f"(from {mem.timestamp.strftime('%Y-%m-%d')})"
                    )
                memory_context = "\n".join(memory_lines)
            
            # Create prompt with memory context
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a helpful AI assistant with long-term memory.
    Use the relevant memories to personalize your responses and maintain continuity
    across conversations."""),
                ("user", """Relevant Memories:
    {memory_context}
    
    Current Message: {message}
    
    Provide a helpful, personalized response:""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            response = chain.invoke({
                "memory_context": memory_context if memory_context else "No relevant memories found.",
                "message": user_message
            })
            
            # Store this interaction
            self.store_memory(
                content=f"User: {user_message} | AI: {response}",
                memory_type=MemoryType.INTERACTION,
                importance=0.5
            )
            
            # Extract and store important information
            self._extract_and_store_info(user_message)
            
            return response.strip()
        
        def _extract_and_store_info(self, message: str):
            """Extract and store important information from message."""
            # Simple extraction logic (in production, would use NER or LLM)
            message_lower = message.lower()
            
            # Detect preferences
            if any(word in message_lower for word in ["like", "prefer", "favorite", "love"]):
                self.store_memory(
                    content=message,
                    memory_type=MemoryType.PREFERENCE,
                    importance=0.8
                )
            
            # Detect facts
            if any(word in message_lower for word in ["my name is", "i am", "i work", "i live"]):
                self.store_memory(
                    content=message,
                    memory_type=MemoryType.SEMANTIC,
                    importance=0.9
                )
        
        def get_memory_summary(self) -> Dict[str, Any]:
            """Get summary of stored memories."""
            all_memories = self.memory_store.get_all_memories()
            
            memory_by_type = {}
            for mem_type in MemoryType:
                memory_by_type[mem_type.value] = sum(
                    1 for m in all_memories if m.memory_type == mem_type
                )
            
            total_accesses = sum(m.access_count for m in all_memories)
            
            return {
                "user_id": self.user_id,
                "total_memories": len(all_memories),
                "by_type": memory_by_type,
                "total_accesses": total_accesses,
                "interactions": self.interaction_count
            }
    
    
    def demonstrate_long_term_memory():
        """Demonstrate the Long-Term Memory pattern."""
        
        print("=" * 80)
        print("LONG-TERM MEMORY PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Create agent for user
        user_id = "alice_demo"
        agent = LongTermMemoryAgent(user_id=user_id, storage_path=f"memory_{user_id}_demo.json")
        
        # Clear any existing memories
        agent.memory_store.clear()
        
        # Session 1: Initial interaction
        print("\n" + "=" * 80)
        print("SESSION 1: Initial Interaction")
        print("=" * 80)
        
        messages_session1 = [
            "Hi, my name is Alice and I'm a software engineer.",
            "I really love Python programming and machine learning.",
            "My favorite food is sushi."
        ]
        
        for msg in messages_session1:
            print(f"\n👤 User: {msg}")
            response = agent.chat(msg)
            print(f"🤖 Agent: {response}")
        
        print("\n📊 Memory Summary after Session 1:")
        summary = agent.get_memory_summary()
        print(json.dumps(summary, indent=2))
        
        # Simulate new session (create new agent instance)
        print("\n\n" + "=" * 80)
        print("SESSION 2: Later Conversation (Testing Persistence)")
        print("=" * 80)
        print("(Simulating new session - creating fresh agent instance)")
        
        agent2 = LongTermMemoryAgent(user_id=user_id, storage_path=f"memory_{user_id}_demo.json")
        
        messages_session2 = [
            "What's my name?",
            "What do I do for work?",
            "What are my interests?",
            "What food do I like?"
        ]
        
        for msg in messages_session2:
            print(f"\n👤 User: {msg}")
            response = agent2.chat(msg)
            print(f"🤖 Agent: {response}")
        
        print("\n📊 Memory Summary after Session 2:")
        summary2 = agent2.get_memory_summary()
        print(json.dumps(summary2, indent=2))
        
        # Session 3: Memory accumulation
        print("\n\n" + "=" * 80)
        print("SESSION 3: Adding More Memories")
        print("=" * 80)
        
        agent3 = LongTermMemoryAgent(user_id=user_id, storage_path=f"memory_{user_id}_demo.json")
        
        messages_session3 = [
            "I recently started learning about transformers.",
            "I'm working on a project about recommendation systems.",
            "Can you suggest something based on my interests?"
        ]
        
        for msg in messages_session3:
            print(f"\n👤 User: {msg}")
            response = agent3.chat(msg)
            print(f"🤖 Agent: {response}")
        
        # Show memory details
        print("\n\n" + "=" * 80)
        print("STORED MEMORIES ANALYSIS")
        print("=" * 80)
        
        all_memories = agent3.memory_store.get_all_memories()
        
        print(f"\nTotal memories: {len(all_memories)}")
        
        for mem_type in MemoryType:
            type_memories = [m for m in all_memories if m.memory_type == mem_type]
            if type_memories:
                print(f"\n{mem_type.value.upper()} Memories ({len(type_memories)}):")
                for mem in type_memories[:3]:  # Show first 3
                    print(f"  - {mem.content[:80]}...")
                    print(f"    Importance: {mem.importance:.1f}, Accessed: {mem.access_count} times")
        
        # Clean up demo file
        try:
            os.remove(f"memory_{user_id}_demo.json")
        except:
            pass
        
        # Summary
        print("\n\n" + "=" * 80)
        print("PATTERN SUMMARY")
        print("=" * 80)
        print("""
    The Long-Term Memory pattern demonstrates:
    
    1. **Cross-Session Persistence**: Memories survive across sessions
    2. **Memory Categorization**: Different types (episodic, semantic, preference)
    3. **Semantic Retrieval**: Find relevant memories via similarity
    4. **Memory Statistics**: Track access patterns and importance
    5. **Personalization**: Use past memories for context-aware responses
    
    Memory Types:
    
    Episodic Memory:
    - Stores specific events and experiences
    - "When I visited Paris..."
    - Time and context-stamped
    
    Semantic Memory:
    - Stores facts and knowledge
    - "Paris is the capital of France"
    - General information
    
    Procedural Memory:
    - Stores skills and procedures
    - "How to make coffee"
    - Step-by-step knowledge
    
    Preference Memory:
    - Stores user preferences
    - "I prefer dark mode"
    - Likes and dislikes
    
    Interaction Memory:
    - Stores past conversations
    - Complete interaction history
    - Enables continuity
    
    Key Benefits:
    - **Personalization**: Tailored responses based on history
    - **Continuity**: Maintains context across sessions
    - **Learning**: Accumulates knowledge over time
    - **Relationship**: Builds long-term user relationships
    - **Efficiency**: Doesn't repeat questions
    
    Implementation Considerations:
    - Vector databases for semantic search (Chroma, Pinecone, FAISS)
    - Memory importance and decay
    - Privacy and data retention
    - Storage optimization
    - Cross-device synchronization
    
    Use Cases:
    - Personal assistants with memory of user
    - Educational tutors tracking student progress
    - Healthcare agents remembering patient history
    - Customer service with account context
    - Long-term collaborative projects
    
    This pattern transforms agents from stateless responders to
    persistent entities with accumulated knowledge and relationships.
    """)
    
    
    if __name__ == "__main__":
        demonstrate_long_term_memory()
    
        

    Agentic Design Pattern: Long Term Memory

    Pattern 027: Long Term Memory – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 027: Long-Term Memory

    Description: The Long-Term Memory pattern enables agents to store and retrieve information across multiple sessions and conversations. Unlike short-term memory that exists only within a session, long-term memory persists indefinitely, allowing agents to remember user preferences, past interactions, learned facts, and accumulated knowledge over time.

    Components: – Persistent Storage: Vector databases, traditional databases, file systems – Memory Indexing: Efficient retrieval mechanisms – Memory Types: Episodic (events), Semantic (facts), Procedural (skills) – Retrieval System: Similarity search, keyword matching – Memory Consolidation: Organizing and optimizing stored memories

    Use Cases: – Personalized user experiences – Learning from past interactions – Knowledge accumulation – User preference tracking – Long-term relationship building

    LangChain Implementation: Uses vector stores (Chroma, FAISS) for semantic memory storage, implements retrieval strategies, and combines with conversation context for personalized interactions.

    Key Features: – Cross-session persistence – Semantic similarity search – Memory categorization – Efficient retrieval – Memory aging and importance


    File Statistics

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

    Module Documentation

    """
    Pattern 027: Long-Term Memory
    
    Description:
        The Long-Term Memory pattern enables agents to store and retrieve information
        across multiple sessions and conversations. Unlike short-term memory that exists
        only within a session, long-term memory persists indefinitely, allowing agents
        to remember user preferences, past interactions, learned facts, and accumulated
        knowledge over time.
    
    Components:
        - Persistent Storage: Vector databases, traditional databases, file systems
        - Memory Indexing: Efficient retrieval mechanisms
        - Memory Types: Episodic (events), Semantic (facts), Procedural (skills)
        - Retrieval System: Similarity search, keyword matching
        - Memory Consolidation: Organizing and optimizing stored memories
    
    Use Cases:
        - Personalized user experiences
        - Learning from past interactions
        - Knowledge accumulation
        - User preference tracking
        - Long-term relationship building
    
    LangChain Implementation:
        Uses vector stores (Chroma, FAISS) for semantic memory storage,
        implements retrieval strategies, and combines with conversation
        context for personalized interactions.
    
    Key Features:
        - Cross-session persistence
        - Semantic similarity search
        - Memory categorization
        - Efficient retrieval
        - Memory aging and importance
    """
    

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


    Import Statements

    Standard Library Imports

    import dataclasses.dataclass
    import dataclasses.field
    import datetime.datetime
    import enum.Enum
    import json
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import os
    import pathlib.Path
    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 – json: JSON serialization/deserialization – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – os: Environment variable access and system operations – pathlib.Path: Object-oriented filesystem paths – 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
    from langchain_openai.OpenAIEmbeddings
    

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

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: MemoryType

    Lines 54-60

    Inherits from: Enum

    Purpose:

    Types of long-term memories.
    

    Class: Memory

    Lines 64-100

    Purpose:

    Represents a long-term memory.
    

    Methods:

    to_dict(self)

    Lines 75-86

    Purpose:

    Convert to dictionary.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D64A3D0>

    from_dict(cls, data)

    Lines 89-100

    Decorators: classmethod

    Purpose:

    Create from dictionary.
    

    Parameters: – cls: Cls – data: Data

    Returns: Memory


    Class: SimpleVectorStore

    Lines 103-180

    Purpose:

    Simple vector store for semantic memory storage (mock implementation).
    In production, would use Chroma, FAISS, or Pinecone.
    

    Methods:

    __init__(self)

    Lines 109-112

    Purpose:

    Initialize vector store.
    

    Parameters:

    add_memory(self, memory)

    Lines 114-116

    Purpose:

    Add a memory to the store.
    

    Parameters: – memory: Memory store for conversation history

    search(self, query, memory_type, k)

    Lines 118-172

    Purpose:

    Search for relevant memories.
    
    Args:
        query: Search query
        memory_type: Filter by memory type
        k: Number of results
    
    Returns:
        List of relevant memories
    

    Parameters: – query: Input query or question – memory_type: Memory store for conversation history – k: K

    Returns: <ast.Subscript object at 0x000001A06D6EA550>

    get_all_memories(self)

    Lines 174-176

    Purpose:

    Get all memories.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D6E9950>

    delete_memory(self, memory_id)

    Lines 178-180

    Purpose:

    Delete a memory by ID.
    

    Parameters: – memory_id: Memory store for conversation history


    Class: PersistentMemoryStore

    Lines 183-242

    Purpose:

    Persistent storage for long-term memory (file-based mock).
    In production, would use proper database.
    

    Methods:

    __init__(self, storage_path)

    Lines 189-198

    Purpose:

    Initialize persistent store.
    
    Args:
        storage_path: Path to storage file
    

    Parameters: – storage_path: Storage path

    _load(self)

    Lines 200-210

    Purpose:

    Load memories from disk.
    

    Parameters:

    _save(self)

    Lines 212-219

    Purpose:

    Save memories to disk.
    

    Parameters:

    add_memory(self, memory)

    Lines 221-224

    Purpose:

    Add and persist a memory.
    

    Parameters: – memory: Memory store for conversation history

    search(self, query, memory_type, k)

    Lines 226-233

    Purpose:

    Search for relevant memories.
    

    Parameters: – query: Input query or question – memory_type: Memory store for conversation history – k: K

    Returns: <ast.Subscript object at 0x000001A06D70CD50>

    get_all_memories(self)

    Lines 235-237

    Purpose:

    Get all stored memories.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D70E550>

    clear(self)

    Lines 239-242

    Purpose:

    Clear all memories.
    

    Parameters:


    Class: LongTermMemoryAgent

    Lines 245-416

    Purpose:

    Agent with long-term memory capabilities.
    

    Methods:

    __init__(self, user_id, storage_path, model)

    Lines 250-272

    Purpose:

    Initialize agent with long-term memory.
    
    Args:
        user_id: Unique user identifier
        storage_path: Path to memory storage
        model: LLM model to use
    

    Parameters: – user_id: User id – storage_path: Storage path – model: Language model instance

    store_memory(self, content, memory_type, importance, metadata)

    Lines 274-298

    Purpose:

    Store a new memory.
    
    Args:
        content: Memory content
        memory_type: Type of memory
        importance: Importance score (0-1)
        metadata: Additional metadata
    

    Parameters: – content: Content – memory_type: Memory store for conversation history – importance: Importance – metadata: Metadata

    recall_memories(self, query, memory_type, k)

    Lines 300-317

    Purpose:

    Recall relevant memories.
    
    Args:
        query: Query for memory retrieval
        memory_type: Filter by type
        k: Number of memories to retrieve
    
    Returns:
        List of relevant memories
    

    Parameters: – query: Input query or question – memory_type: Memory store for conversation history – k: K

    Returns: <ast.Subscript object at 0x000001A06D15B6D0>

    chat(self, user_message)

    Lines 319-375

    Purpose:

    Chat with memory-augmented responses.
    
    Args:
        user_message: User's message
    
    Returns:
        Agent's response
    

    Parameters: – user_message: User message

    Returns: str

    _extract_and_store_info(self, message)

    Lines 377-396

    Purpose:

    Extract and store important information from message.
    

    Parameters: – message: Message

    get_memory_summary(self)

    Lines 398-416

    Purpose:

    Get summary of stored memories.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D659D90>


    Functions

    demonstrate_long_term_memory()

    Lines 419-581

    Purpose:

    Demonstrate the Long-Term Memory pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Long Term 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 026
    • Pattern 028

    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 Long Term 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: 027_long_term_memory.py
    Total Lines: 586
    Pattern Category: AI Agent Design Pattern #027


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Short Term Memory

    
        """
    Pattern 026: Short-Term Memory
    
    Description:
        The Short-Term Memory pattern enables agents to maintain context within the current
        conversation or task session. It provides immediate access to recent interactions,
        allowing the agent to reference previous messages, maintain conversation flow, and
        build upon earlier context without requiring persistent storage.
    
    Components:
        - Conversation Buffer: Stores recent message history
        - Context Window Manager: Manages token limits
        - Message Formatter: Structures messages for LLM
        - Relevance Filter: Keeps most relevant context
        - Memory Summarizer: Condenses long conversations
    
    Use Cases:
        - Multi-turn conversations
        - Task continuity within session
        - Context-aware responses
        - Follow-up questions
        - Anaphora resolution
    
    LangChain Implementation:
        Uses LangChain's memory abstractions including ConversationBufferMemory,
        ConversationBufferWindowMemory, and ConversationSummaryMemory for
        different short-term memory strategies.
    
    Key Features:
        - Automatic message history tracking
        - Context window management
        - Memory clearing and reset
        - Selective memory retention
        - Token-aware buffering
    """
    
    import os
    from typing import List, Dict, Any, Optional
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
    from langchain_core.output_parsers import StrOutputParser
    from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class MemoryStrategy(Enum):
        """Strategy for managing short-term memory."""
        BUFFER = "buffer"  # Keep all messages
        WINDOW = "window"  # Keep last N messages
        TOKEN_LIMIT = "token_limit"  # Keep within token limit
        SUMMARY = "summary"  # Summarize older messages
    
    
    @dataclass
    class Message:
        """Represents a conversation message."""
        role: str  # "human", "ai", "system"
        content: str
        timestamp: datetime = field(default_factory=datetime.now)
        metadata: Dict[str, Any] = field(default_factory=dict)
        
        def to_langchain_message(self):
            """Convert to LangChain message format."""
            if self.role == "human":
                return HumanMessage(content=self.content)
            elif self.role == "ai":
                return AIMessage(content=self.content)
            else:
                return SystemMessage(content=self.content)
    
    
    @dataclass
    class ConversationContext:
        """Context for current conversation."""
        messages: List[Message] = field(default_factory=list)
        metadata: Dict[str, Any] = field(default_factory=dict)
        started_at: datetime = field(default_factory=datetime.now)
        
        def add_message(self, role: str, content: str, metadata: Optional[Dict] = None):
            """Add a message to the conversation."""
            message = Message(
                role=role,
                content=content,
                metadata=metadata or {}
            )
            self.messages.append(message)
        
        def get_recent_messages(self, n: int) -> List[Message]:
            """Get the N most recent messages."""
            return self.messages[-n:] if n > 0 else []
        
        def get_messages_by_role(self, role: str) -> List[Message]:
            """Get all messages from a specific role."""
            return [msg for msg in self.messages if msg.role == role]
        
        def clear(self):
            """Clear all messages."""
            self.messages.clear()
            self.started_at = datetime.now()
    
    
    class BufferMemory:
        """
        Simple buffer that stores all conversation messages.
        """
        
        def __init__(self, max_messages: Optional[int] = None):
            """
            Initialize buffer memory.
            
            Args:
                max_messages: Maximum number of messages to store (None = unlimited)
            """
            self.max_messages = max_messages
            self.context = ConversationContext()
        
        def add_interaction(self, human_message: str, ai_message: str):
            """Add a human-AI interaction pair."""
            self.context.add_message("human", human_message)
            self.context.add_message("ai", ai_message)
            
            # Trim if exceeding max
            if self.max_messages and len(self.context.messages) > self.max_messages:
                excess = len(self.context.messages) - self.max_messages
                self.context.messages = self.context.messages[excess:]
        
        def get_context_string(self) -> str:
            """Get conversation history as formatted string."""
            lines = []
            for msg in self.context.messages:
                role = msg.role.capitalize()
                lines.append(f"{role}: {msg.content}")
            return "\n".join(lines)
        
        def get_langchain_messages(self) -> List:
            """Get messages in LangChain format."""
            return [msg.to_langchain_message() for msg in self.context.messages]
        
        def clear(self):
            """Clear memory."""
            self.context.clear()
    
    
    class WindowMemory:
        """
        Sliding window memory that keeps only the last N messages.
        """
        
        def __init__(self, window_size: int = 10):
            """
            Initialize window memory.
            
            Args:
                window_size: Number of recent messages to keep
            """
            self.window_size = window_size
            self.context = ConversationContext()
        
        def add_interaction(self, human_message: str, ai_message: str):
            """Add interaction and maintain window."""
            self.context.add_message("human", human_message)
            self.context.add_message("ai", ai_message)
            
            # Keep only window_size messages
            if len(self.context.messages) > self.window_size:
                self.context.messages = self.context.messages[-self.window_size:]
        
        def get_context_string(self) -> str:
            """Get recent conversation history."""
            lines = []
            for msg in self.context.messages:
                role = msg.role.capitalize()
                lines.append(f"{role}: {msg.content}")
            return "\n".join(lines)
        
        def get_langchain_messages(self) -> List:
            """Get messages in LangChain format."""
            return [msg.to_langchain_message() for msg in self.context.messages]
    
    
    class TokenLimitMemory:
        """
        Memory that keeps messages within a token budget.
        """
        
        def __init__(self, max_tokens: int = 2000):
            """
            Initialize token-limit memory.
            
            Args:
                max_tokens: Maximum tokens to keep in memory
            """
            self.max_tokens = max_tokens
            self.context = ConversationContext()
        
        def _estimate_tokens(self, text: str) -> int:
            """Estimate token count (rough approximation)."""
            # Simple estimation: ~4 characters per token
            return len(text) // 4
        
        def add_interaction(self, human_message: str, ai_message: str):
            """Add interaction and maintain token limit."""
            self.context.add_message("human", human_message)
            self.context.add_message("ai", ai_message)
            
            # Remove oldest messages until under token limit
            while self._get_total_tokens() > self.max_tokens and len(self.context.messages) > 2:
                self.context.messages.pop(0)
        
        def _get_total_tokens(self) -> int:
            """Calculate total tokens in memory."""
            total = 0
            for msg in self.context.messages:
                total += self._estimate_tokens(msg.content)
            return total
        
        def get_context_string(self) -> str:
            """Get conversation history."""
            lines = []
            for msg in self.context.messages:
                role = msg.role.capitalize()
                lines.append(f"{role}: {msg.content}")
            return "\n".join(lines)
        
        def get_token_count(self) -> int:
            """Get current token count."""
            return self._get_total_tokens()
    
    
    class SummaryMemory:
        """
        Memory that summarizes older messages to save space.
        """
        
        def __init__(
            self,
            recent_messages: int = 4,
            model: str = "gpt-3.5-turbo"
        ):
            """
            Initialize summary memory.
            
            Args:
                recent_messages: Number of recent messages to keep verbatim
                model: LLM model for summarization
            """
            self.recent_messages = recent_messages
            self.llm = ChatOpenAI(model=model, temperature=0.3)
            self.context = ConversationContext()
            self.summary = ""
        
        def add_interaction(self, human_message: str, ai_message: str):
            """Add interaction and update summary if needed."""
            self.context.add_message("human", human_message)
            self.context.add_message("ai", ai_message)
            
            # If we have more than recent_messages, summarize older ones
            if len(self.context.messages) > self.recent_messages:
                messages_to_summarize = self.context.messages[:-self.recent_messages]
                self.summary = self._create_summary(messages_to_summarize)
                self.context.messages = self.context.messages[-self.recent_messages:]
        
        def _create_summary(self, messages: List[Message]) -> str:
            """Create summary of messages."""
            if not messages:
                return self.summary
            
            # Format messages
            formatted = "\n".join([
                f"{msg.role.capitalize()}: {msg.content}"
                for msg in messages
            ])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """Summarize the following conversation, preserving key information
    and context. Be concise but comprehensive."""),
                ("user", "Previous summary: {summary}\n\nNew messages:\n{messages}\n\nProvide updated summary:")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            new_summary = chain.invoke({
                "summary": self.summary if self.summary else "None",
                "messages": formatted
            })
            
            return new_summary.strip()
        
        def get_context_string(self) -> str:
            """Get conversation context with summary."""
            parts = []
            
            if self.summary:
                parts.append(f"[Summary of earlier conversation: {self.summary}]\n")
            
            for msg in self.context.messages:
                role = msg.role.capitalize()
                parts.append(f"{role}: {msg.content}")
            
            return "\n".join(parts)
    
    
    class ShortTermMemoryAgent:
        """
        Agent with short-term memory for conversations.
        """
        
        def __init__(
            self,
            memory_strategy: MemoryStrategy = MemoryStrategy.WINDOW,
            model: str = "gpt-3.5-turbo",
            **memory_kwargs
        ):
            """
            Initialize agent with short-term memory.
            
            Args:
                memory_strategy: Strategy for memory management
                model: LLM model to use
                **memory_kwargs: Arguments for memory initialization
            """
            self.llm = ChatOpenAI(model=model, temperature=0.7)
            self.memory_strategy = memory_strategy
            
            # Initialize appropriate memory type
            if memory_strategy == MemoryStrategy.BUFFER:
                self.memory = BufferMemory(**memory_kwargs)
            elif memory_strategy == MemoryStrategy.WINDOW:
                self.memory = WindowMemory(**memory_kwargs)
            elif memory_strategy == MemoryStrategy.TOKEN_LIMIT:
                self.memory = TokenLimitMemory(**memory_kwargs)
            elif memory_strategy == MemoryStrategy.SUMMARY:
                self.memory = SummaryMemory(**memory_kwargs)
        
        def chat(self, user_message: str) -> str:
            """
            Chat with the agent.
            
            Args:
                user_message: User's message
                
            Returns:
                Agent's response
            """
            # Get conversation context
            context = self.memory.get_context_string()
            
            # Create prompt with context
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a helpful AI assistant. Use the conversation history
    to provide context-aware responses. Reference previous messages when relevant."""),
                ("user", "{context}\n\nHuman: {message}\n\nProvide a helpful response:")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            # Generate response
            response = chain.invoke({
                "context": context if context else "No previous conversation.",
                "message": user_message
            })
            
            # Store interaction
            self.memory.add_interaction(user_message, response)
            
            return response.strip()
        
        def get_conversation_summary(self) -> Dict[str, Any]:
            """Get summary of current conversation."""
            message_count = len(self.memory.context.messages)
            
            summary = {
                "strategy": self.memory_strategy.value,
                "message_count": message_count,
                "human_messages": len(self.memory.context.get_messages_by_role("human")),
                "ai_messages": len(self.memory.context.get_messages_by_role("ai")),
                "started_at": self.memory.context.started_at.isoformat()
            }
            
            # Add strategy-specific info
            if hasattr(self.memory, 'get_token_count'):
                summary["token_count"] = self.memory.get_token_count()
            
            if hasattr(self.memory, 'summary') and self.memory.summary:
                summary["has_summary"] = True
                summary["summary_length"] = len(self.memory.summary)
            
            return summary
        
        def clear_memory(self):
            """Clear conversation memory."""
            self.memory.context.clear()
    
    
    def demonstrate_short_term_memory():
        """Demonstrate the Short-Term Memory pattern."""
        
        print("=" * 80)
        print("SHORT-TERM MEMORY PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Test 1: Buffer Memory
        print("\n" + "=" * 80)
        print("TEST 1: Buffer Memory (Unlimited)")
        print("=" * 80)
        
        agent1 = ShortTermMemoryAgent(
            memory_strategy=MemoryStrategy.BUFFER,
            max_messages=None
        )
        
        conversation1 = [
            "Hi, my name is Alice.",
            "What's my name?",
            "I'm interested in learning about AI.",
            "What did I say I was interested in?"
        ]
        
        for msg in conversation1:
            print(f"\n👤 Human: {msg}")
            response = agent1.chat(msg)
            print(f"🤖 AI: {response}")
        
        summary1 = agent1.get_conversation_summary()
        print(f"\n📊 Conversation Summary:")
        print(f"   Messages: {summary1['message_count']}")
        print(f"   Strategy: {summary1['strategy']}")
        
        # Test 2: Window Memory
        print("\n\n" + "=" * 80)
        print("TEST 2: Window Memory (Last 4 Messages)")
        print("=" * 80)
        
        agent2 = ShortTermMemoryAgent(
            memory_strategy=MemoryStrategy.WINDOW,
            window_size=4
        )
        
        conversation2 = [
            "My favorite color is blue.",
            "I live in New York.",
            "I work as a teacher.",
            "I like pizza.",
            "What's my favorite color?",  # This should be forgotten
            "Where do I live?",  # This should be remembered
        ]
        
        for msg in conversation2:
            print(f"\n👤 Human: {msg}")
            response = agent2.chat(msg)
            print(f"🤖 AI: {response}")
        
        summary2 = agent2.get_conversation_summary()
        print(f"\n📊 Conversation Summary:")
        print(f"   Messages in window: {summary2['message_count']}")
        print(f"   Strategy: {summary2['strategy']}")
        
        # Test 3: Token Limit Memory
        print("\n\n" + "=" * 80)
        print("TEST 3: Token Limit Memory (2000 tokens)")
        print("=" * 80)
        
        agent3 = ShortTermMemoryAgent(
            memory_strategy=MemoryStrategy.TOKEN_LIMIT,
            max_tokens=2000
        )
        
        conversation3 = [
            "Let me tell you about my day. " * 20,  # Long message
            "What did I just tell you about?",
            "Here's another long story. " * 20,  # Another long message
            "Can you remember both stories?"
        ]
        
        for msg in conversation3:
            msg_preview = msg[:60] + "..." if len(msg) > 60 else msg
            print(f"\n👤 Human: {msg_preview}")
            response = agent3.chat(msg)
            response_preview = response[:100] + "..." if len(response) > 100 else response
            print(f"🤖 AI: {response_preview}")
        
        summary3 = agent3.get_conversation_summary()
        print(f"\n📊 Conversation Summary:")
        print(f"   Messages: {summary3['message_count']}")
        print(f"   Token count: {summary3['token_count']}")
        print(f"   Strategy: {summary3['strategy']}")
        
        # Summary
        print("\n\n" + "=" * 80)
        print("PATTERN SUMMARY")
        print("=" * 80)
        print("""
    The Short-Term Memory pattern demonstrates:
    
    1. **Context Continuity**: Maintains conversation flow
    2. **Multiple Strategies**: Different approaches for different needs
    3. **Resource Management**: Controls memory usage
    4. **Context Awareness**: References previous messages
    5. **Session Scoped**: Memory persists within session
    
    Memory Strategies Compared:
    
    Buffer Memory:
    - ✓ Complete conversation history
    - ✓ Perfect recall
    - ✗ Unbounded growth
    - Use: Short conversations, debugging
    
    Window Memory:
    - ✓ Fixed memory size
    - ✓ Recent context preserved
    - ✗ Forgets older messages
    - Use: Long conversations, resource constraints
    
    Token Limit Memory:
    - ✓ Token budget control
    - ✓ Adapts to message length
    - ✗ May lose context unpredictably
    - Use: API token limits, cost control
    
    Summary Memory:
    - ✓ Compact representation
    - ✓ Preserves key information
    - ✗ Lossy compression
    - Use: Very long conversations
    
    Key Benefits:
    - **Natural Conversations**: Agents remember context
    - **Anaphora Resolution**: Handle "it", "that", "they" references
    - **Follow-up Questions**: Build on previous topics
    - **Resource Control**: Manage memory/token usage
    - **Session Management**: Clear boundaries
    
    Use Cases:
    - Customer service chatbots
    - Personal assistants
    - Tutorial/teaching agents
    - Technical support
    - Conversational interfaces
    
    This pattern is fundamental for any conversational AI system,
    enabling natural multi-turn interactions with context awareness.
    """)
    
    
    if __name__ == "__main__":
        demonstrate_short_term_memory()
    
        

    Agentic Design Pattern: Short Term Memory

    Pattern 026: Short Term Memory – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 026: Short-Term Memory

    Description: The Short-Term Memory pattern enables agents to maintain context within the current conversation or task session. It provides immediate access to recent interactions, allowing the agent to reference previous messages, maintain conversation flow, and build upon earlier context without requiring persistent storage.

    Components: – Conversation Buffer: Stores recent message history – Context Window Manager: Manages token limits – Message Formatter: Structures messages for LLM – Relevance Filter: Keeps most relevant context – Memory Summarizer: Condenses long conversations

    Use Cases: – Multi-turn conversations – Task continuity within session – Context-aware responses – Follow-up questions – Anaphora resolution

    LangChain Implementation: Uses LangChain’s memory abstractions including ConversationBufferMemory, ConversationBufferWindowMemory, and ConversationSummaryMemory for different short-term memory strategies.

    Key Features: – Automatic message history tracking – Context window management – Memory clearing and reset – Selective memory retention – Token-aware buffering


    File Statistics

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

    Module Documentation

    """
    Pattern 026: Short-Term Memory
    
    Description:
        The Short-Term Memory pattern enables agents to maintain context within the current
        conversation or task session. It provides immediate access to recent interactions,
        allowing the agent to reference previous messages, maintain conversation flow, and
        build upon earlier context without requiring persistent storage.
    
    Components:
        - Conversation Buffer: Stores recent message history
        - Context Window Manager: Manages token limits
        - Message Formatter: Structures messages for LLM
        - Relevance Filter: Keeps most relevant context
        - Memory Summarizer: Condenses long conversations
    
    Use Cases:
        - Multi-turn conversations
        - Task continuity within session
        - Context-aware responses
        - Follow-up questions
        - Anaphora resolution
    
    LangChain Implementation:
        Uses LangChain's memory abstractions including ConversationBufferMemory,
        ConversationBufferWindowMemory, and ConversationSummaryMemory for
        different short-term memory strategies.
    
    Key Features:
        - Automatic message history tracking
        - Context window management
        - Memory clearing and reset
        - Selective memory retention
        - Token-aware buffering
    """
    

    Purpose: This module implements the Short Term Memory 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.messages.AIMessage
    import langchain_core.messages.HumanMessage
    import langchain_core.messages.SystemMessage
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import langchain_core.prompts.MessagesPlaceholder
    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.messages.AIMessage: Regular expression operations – langchain_core.messages.HumanMessage: Regular expression operations – langchain_core.messages.SystemMessage: Regular expression operations – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – langchain_core.prompts.MessagesPlaceholder: 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: MemoryStrategy

    Lines 52-57

    Inherits from: Enum

    Purpose:

    Strategy for managing short-term memory.
    

    Class: Message

    Lines 61-75

    Purpose:

    Represents a conversation message.
    

    Methods:

    to_langchain_message(self)

    Lines 68-75

    Purpose:

    Convert to LangChain message format.
    

    Parameters:


    Class: ConversationContext

    Lines 79-105

    Purpose:

    Context for current conversation.
    

    Methods:

    add_message(self, role, content, metadata)

    Lines 85-92

    Purpose:

    Add a message to the conversation.
    

    Parameters: – role: Role – content: Content – metadata: Metadata

    get_recent_messages(self, n)

    Lines 94-96

    Purpose:

    Get the N most recent messages.
    

    Parameters: – n: N

    Returns: <ast.Subscript object at 0x000001A06D17A5D0>

    get_messages_by_role(self, role)

    Lines 98-100

    Purpose:

    Get all messages from a specific role.
    

    Parameters: – role: Role

    Returns: <ast.Subscript object at 0x000001A06D1789D0>

    clear(self)

    Lines 102-105

    Purpose:

    Clear all messages.
    

    Parameters:


    Class: BufferMemory

    Lines 108-147

    Purpose:

    Simple buffer that stores all conversation messages.
    

    Methods:

    __init__(self, max_messages)

    Lines 113-121

    Purpose:

    Initialize buffer memory.
    
    Args:
        max_messages: Maximum number of messages to store (None = unlimited)
    

    Parameters: – max_messages: Max messages

    add_interaction(self, human_message, ai_message)

    Lines 123-131

    Purpose:

    Add a human-AI interaction pair.
    

    Parameters: – human_message: Human message – ai_message: Ai message

    get_context_string(self)

    Lines 133-139

    Purpose:

    Get conversation history as formatted string.
    

    Parameters:

    Returns: str

    get_langchain_messages(self)

    Lines 141-143

    Purpose:

    Get messages in LangChain format.
    

    Parameters:

    Returns: List

    clear(self)

    Lines 145-147

    Purpose:

    Clear memory.
    

    Parameters:


    Class: WindowMemory

    Lines 150-184

    Purpose:

    Sliding window memory that keeps only the last N messages.
    

    Methods:

    __init__(self, window_size)

    Lines 155-163

    Purpose:

    Initialize window memory.
    
    Args:
        window_size: Number of recent messages to keep
    

    Parameters: – window_size: Window size

    add_interaction(self, human_message, ai_message)

    Lines 165-172

    Purpose:

    Add interaction and maintain window.
    

    Parameters: – human_message: Human message – ai_message: Ai message

    get_context_string(self)

    Lines 174-180

    Purpose:

    Get recent conversation history.
    

    Parameters:

    Returns: str

    get_langchain_messages(self)

    Lines 182-184

    Purpose:

    Get messages in LangChain format.
    

    Parameters:

    Returns: List


    Class: TokenLimitMemory

    Lines 187-233

    Purpose:

    Memory that keeps messages within a token budget.
    

    Methods:

    __init__(self, max_tokens)

    Lines 192-200

    Purpose:

    Initialize token-limit memory.
    
    Args:
        max_tokens: Maximum tokens to keep in memory
    

    Parameters: – max_tokens: Maximum tokens to generate

    _estimate_tokens(self, text)

    Lines 202-205

    Purpose:

    Estimate token count (rough approximation).
    

    Parameters: – text: Text

    Returns: int

    add_interaction(self, human_message, ai_message)

    Lines 207-214

    Purpose:

    Add interaction and maintain token limit.
    

    Parameters: – human_message: Human message – ai_message: Ai message

    _get_total_tokens(self)

    Lines 216-221

    Purpose:

    Calculate total tokens in memory.
    

    Parameters:

    Returns: int

    get_context_string(self)

    Lines 223-229

    Purpose:

    Get conversation history.
    

    Parameters:

    Returns: str

    get_token_count(self)

    Lines 231-233

    Purpose:

    Get current token count.
    

    Parameters:

    Returns: int


    Class: SummaryMemory

    Lines 236-306

    Purpose:

    Memory that summarizes older messages to save space.
    

    Methods:

    __init__(self, recent_messages, model)

    Lines 241-256

    Purpose:

    Initialize summary memory.
    
    Args:
        recent_messages: Number of recent messages to keep verbatim
        model: LLM model for summarization
    

    Parameters: – recent_messages: Recent messages – model: Language model instance

    add_interaction(self, human_message, ai_message)

    Lines 258-267

    Purpose:

    Add interaction and update summary if needed.
    

    Parameters: – human_message: Human message – ai_message: Ai message

    _create_summary(self, messages)

    Lines 269-293

    Purpose:

    Create summary of messages.
    

    Parameters: – messages: Messages

    Returns: str

    get_context_string(self)

    Lines 295-306

    Purpose:

    Get conversation context with summary.
    

    Parameters:

    Returns: str


    Class: ShortTermMemoryAgent

    Lines 309-398

    Purpose:

    Agent with short-term memory for conversations.
    

    Methods:

    __init__(self, memory_strategy, model)

    Lines 314-339

    Purpose:

    Initialize agent with short-term memory.
    
    Args:
        memory_strategy: Strategy for memory management
        model: LLM model to use
        **memory_kwargs: Arguments for memory initialization
    

    Parameters: – memory_strategy: Memory store for conversation history – model: Language model instance

    chat(self, user_message)

    Lines 341-372

    Purpose:

    Chat with the agent.
    
    Args:
        user_message: User's message
    
    Returns:
        Agent's response
    

    Parameters: – user_message: User message

    Returns: str

    get_conversation_summary(self)

    Lines 374-394

    Purpose:

    Get summary of current conversation.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D165C50>

    clear_memory(self)

    Lines 396-398

    Purpose:

    Clear conversation memory.
    

    Parameters:


    Functions

    demonstrate_short_term_memory()

    Lines 401-549

    Purpose:

    Demonstrate the Short-Term Memory pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Short Term 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 025
    • Pattern 027

    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 Short Term 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: 026_short_term_memory.py
    Total Lines: 554
    Pattern Category: AI Agent Design Pattern #026


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Action Sequence Planning

    
        """
    Pattern 025: Action Sequence Planning
    
    Description:
        The Action Sequence Planning pattern enables agents to decompose complex goals
        into ordered sequences of actions with preconditions, effects, and dependencies.
        The agent analyzes the goal, identifies required actions, determines ordering
        constraints, validates feasibility, and executes the plan step-by-step with
        monitoring and replanning capabilities.
    
    Components:
        - Action Library: Catalog of available actions with specifications
        - Planner: Generates action sequences to achieve goals
        - Dependency Analyzer: Identifies action dependencies and constraints
        - Feasibility Checker: Validates plan viability
        - Executor: Executes plans with monitoring and error handling
    
    Use Cases:
        - Multi-step task automation
        - Workflow orchestration
        - Robot task planning
        - Complex problem decomposition
        - Project planning and execution
    
    LangChain Implementation:
        Uses LLM for action selection and ordering, implements STRIPS-like
        planning with preconditions and effects, handles dynamic replanning.
    
    Key Features:
        - Action dependency analysis
        - Precondition and effect modeling
        - Parallel action detection
        - Plan validation
        - Dynamic replanning on failures
    """
    
    import os
    from typing import List, Dict, Any, Optional, Set
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class ActionStatus(Enum):
        """Status of action execution."""
        PENDING = "pending"
        READY = "ready"
        EXECUTING = "executing"
        COMPLETED = "completed"
        FAILED = "failed"
        SKIPPED = "skipped"
    
    
    @dataclass
    class ActionDefinition:
        """Definition of an available action."""
        name: str
        description: str
        preconditions: List[str]
        effects: List[str]
        duration: float = 1.0  # Estimated duration
        
        def to_dict(self) -> Dict[str, Any]:
            """Convert to dictionary for LLM context."""
            return {
                "name": self.name,
                "description": self.description,
                "preconditions": self.preconditions,
                "effects": self.effects
            }
    
    
    @dataclass
    class PlannedAction:
        """An action in a plan."""
        action: ActionDefinition
        step_number: int
        dependencies: List[int] = field(default_factory=list)
        status: ActionStatus = ActionStatus.PENDING
        result: Optional[str] = None
        error: Optional[str] = None
        start_time: Optional[datetime] = None
        end_time: Optional[datetime] = None
    
    
    @dataclass
    class ActionPlan:
        """Complete action plan."""
        goal: str
        actions: List[PlannedAction]
        initial_state: Set[str]
        final_state: Set[str]
        timestamp: datetime = field(default_factory=datetime.now)
        
        def get_ready_actions(self) -> List[PlannedAction]:
            """Get actions that are ready to execute."""
            ready = []
            for action in self.actions:
                if action.status == ActionStatus.PENDING:
                    # Check if all dependencies completed
                    dependencies_met = all(
                        self.actions[dep].status == ActionStatus.COMPLETED
                        for dep in action.dependencies
                    )
                    if dependencies_met:
                        ready.append(action)
            return ready
        
        def is_complete(self) -> bool:
            """Check if plan is complete."""
            return all(
                action.status in [ActionStatus.COMPLETED, ActionStatus.SKIPPED]
                for action in self.actions
            )
    
    
    class ActionLibrary:
        """
        Library of available actions.
        """
        
        def __init__(self):
            """Initialize action library with common actions."""
            self.actions: Dict[str, ActionDefinition] = {}
            self._register_builtin_actions()
        
        def _register_builtin_actions(self):
            """Register built-in actions."""
            
            # Data collection actions
            self.register_action(ActionDefinition(
                name="gather_requirements",
                description="Collect and document project requirements",
                preconditions=["project_defined"],
                effects=["requirements_gathered"]
            ))
            
            self.register_action(ActionDefinition(
                name="research_topic",
                description="Research and gather information on a topic",
                preconditions=[],
                effects=["research_completed", "information_gathered"]
            ))
            
            # Analysis actions
            self.register_action(ActionDefinition(
                name="analyze_data",
                description="Analyze collected data or information",
                preconditions=["data_available"],
                effects=["analysis_completed", "insights_generated"]
            ))
            
            self.register_action(ActionDefinition(
                name="identify_risks",
                description="Identify and assess potential risks",
                preconditions=["requirements_gathered"],
                effects=["risks_identified"]
            ))
            
            # Design actions
            self.register_action(ActionDefinition(
                name="create_design",
                description="Create system or solution design",
                preconditions=["requirements_gathered", "research_completed"],
                effects=["design_created"]
            ))
            
            self.register_action(ActionDefinition(
                name="review_design",
                description="Review and validate design",
                preconditions=["design_created"],
                effects=["design_reviewed"]
            ))
            
            # Implementation actions
            self.register_action(ActionDefinition(
                name="implement_solution",
                description="Implement the designed solution",
                preconditions=["design_reviewed"],
                effects=["solution_implemented"]
            ))
            
            self.register_action(ActionDefinition(
                name="write_documentation",
                description="Write technical documentation",
                preconditions=["solution_implemented"],
                effects=["documentation_written"]
            ))
            
            # Testing actions
            self.register_action(ActionDefinition(
                name="create_test_plan",
                description="Create comprehensive test plan",
                preconditions=["design_created"],
                effects=["test_plan_created"]
            ))
            
            self.register_action(ActionDefinition(
                name="execute_tests",
                description="Execute test cases and validate solution",
                preconditions=["solution_implemented", "test_plan_created"],
                effects=["tests_executed", "quality_validated"]
            ))
            
            # Deployment actions
            self.register_action(ActionDefinition(
                name="prepare_deployment",
                description="Prepare solution for deployment",
                preconditions=["tests_executed", "documentation_written"],
                effects=["deployment_ready"]
            ))
            
            self.register_action(ActionDefinition(
                name="deploy_solution",
                description="Deploy solution to production",
                preconditions=["deployment_ready"],
                effects=["solution_deployed"]
            ))
        
        def register_action(self, action: ActionDefinition):
            """Register a new action."""
            self.actions[action.name] = action
        
        def get_action(self, name: str) -> Optional[ActionDefinition]:
            """Get action by name."""
            return self.actions.get(name)
        
        def get_all_actions_description(self) -> str:
            """Get formatted description of all actions."""
            descriptions = []
            for action in self.actions.values():
                precond = ", ".join(action.preconditions) if action.preconditions else "none"
                effects = ", ".join(action.effects)
                descriptions.append(
                    f"- {action.name}: {action.description}\n"
                    f"  Preconditions: {precond}\n"
                    f"  Effects: {effects}"
                )
            return "\n\n".join(descriptions)
    
    
    class ActionPlanner:
        """
        Plans action sequences to achieve goals.
        """
        
        def __init__(
            self,
            action_library: ActionLibrary,
            model: str = "gpt-3.5-turbo"
        ):
            """
            Initialize action planner.
            
            Args:
                action_library: Library of available actions
                model: LLM model to use
            """
            self.action_library = action_library
            self.llm = ChatOpenAI(model=model, temperature=0.3)
        
        def create_plan(
            self,
            goal: str,
            initial_state: Optional[Set[str]] = None
        ) -> ActionPlan:
            """
            Create action plan to achieve goal.
            
            Args:
                goal: Goal to achieve
                initial_state: Initial world state
                
            Returns:
                Action plan
            """
            if initial_state is None:
                initial_state = set()
            
            actions_desc = self.action_library.get_all_actions_description()
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert action planner. Create a sequence of actions
    to achieve the given goal, respecting preconditions and dependencies.
    
    Available Actions:
    {actions_description}
    
    Your response should list actions in order, one per line, using exact action names.
    Only use actions from the available list."""),
                ("user", """Goal: {goal}
    
    Initial State: {initial_state}
    
    Create an action plan listing actions in execution order:""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            initial_state_str = ", ".join(initial_state) if initial_state else "none"
            
            response = chain.invoke({
                "actions_description": actions_desc,
                "goal": goal,
                "initial_state": initial_state_str
            })
            
            # Parse action names
            action_names = self._parse_action_names(response)
            
            # Build plan with dependencies
            planned_actions = self._build_plan_with_dependencies(
                action_names,
                initial_state
            )
            
            # Calculate final state
            final_state = self._calculate_final_state(planned_actions, initial_state)
            
            return ActionPlan(
                goal=goal,
                actions=planned_actions,
                initial_state=initial_state,
                final_state=final_state
            )
        
        def _parse_action_names(self, response: str) -> List[str]:
            """Parse action names from LLM response."""
            lines = response.strip().split("\n")
            action_names = []
            
            for line in lines:
                line = line.strip()
                # Remove numbering, bullets, etc.
                for prefix in ["1.", "2.", "3.", "4.", "5.", "6.", "7.", "8.", "9.", "-", "*", "•"]:
                    if line.startswith(prefix):
                        line = line[len(prefix):].strip()
                
                # Check if this is a valid action name
                if self.action_library.get_action(line):
                    action_names.append(line)
            
            return action_names
        
        def _build_plan_with_dependencies(
            self,
            action_names: List[str],
            initial_state: Set[str]
        ) -> List[PlannedAction]:
            """Build plan with dependency analysis."""
            planned_actions = []
            current_state = initial_state.copy()
            
            for step_num, action_name in enumerate(action_names):
                action_def = self.action_library.get_action(action_name)
                if not action_def:
                    continue
                
                # Find dependencies (actions that provide required preconditions)
                dependencies = []
                for precond in action_def.preconditions:
                    if precond not in current_state:
                        # Find which previous action provides this
                        for i, prev_action in enumerate(planned_actions):
                            if precond in prev_action.action.effects:
                                dependencies.append(i)
                                break
                
                planned_action = PlannedAction(
                    action=action_def,
                    step_number=step_num,
                    dependencies=dependencies
                )
                
                planned_actions.append(planned_action)
                
                # Update state with effects
                current_state.update(action_def.effects)
            
            return planned_actions
        
        def _calculate_final_state(
            self,
            planned_actions: List[PlannedAction],
            initial_state: Set[str]
        ) -> Set[str]:
            """Calculate final state after all actions."""
            final_state = initial_state.copy()
            
            for planned_action in planned_actions:
                final_state.update(planned_action.action.effects)
            
            return final_state
    
    
    class PlanExecutor:
        """
        Executes action plans.
        """
        
        def __init__(self):
            """Initialize plan executor."""
            pass
        
        def execute_plan(self, plan: ActionPlan) -> Dict[str, Any]:
            """
            Execute action plan.
            
            Args:
                plan: Action plan to execute
                
            Returns:
                Execution results
            """
            print(f"\n[Executor] Executing plan for goal: {plan.goal}")
            print(f"[Executor] Total actions: {len(plan.actions)}\n")
            
            completed = 0
            failed = 0
            
            while not plan.is_complete():
                # Get ready actions
                ready_actions = plan.get_ready_actions()
                
                if not ready_actions:
                    # Check if we have pending actions that can't proceed
                    pending = [a for a in plan.actions if a.status == ActionStatus.PENDING]
                    if pending:
                        print("[Executor] ✗ Deadlock: Actions have unmet dependencies")
                        for action in pending:
                            action.status = ActionStatus.FAILED
                            action.error = "Unmet dependencies"
                            failed += 1
                    break
                
                # Execute ready actions
                for action in ready_actions:
                    self._execute_action(action)
                    
                    if action.status == ActionStatus.COMPLETED:
                        completed += 1
                    elif action.status == ActionStatus.FAILED:
                        failed += 1
            
            success_rate = completed / len(plan.actions) * 100 if plan.actions else 0
            
            return {
                "goal": plan.goal,
                "total_actions": len(plan.actions),
                "completed": completed,
                "failed": failed,
                "success_rate": success_rate,
                "plan": plan
            }
        
        def _execute_action(self, action: PlannedAction):
            """Execute a single action."""
            action.status = ActionStatus.EXECUTING
            action.start_time = datetime.now()
            
            print(f"[Step {action.step_number + 1}] Executing: {action.action.name}")
            print(f"  Description: {action.action.description}")
            
            if action.dependencies:
                deps_str = ", ".join([str(d + 1) for d in action.dependencies])
                print(f"  Dependencies: Steps {deps_str}")
            
            # Simulate action execution (in real system, would call actual function)
            try:
                # Simulate success
                action.result = f"Successfully completed {action.action.name}"
                action.status = ActionStatus.COMPLETED
                print(f"  ✓ Completed")
                print(f"  Effects: {', '.join(action.action.effects)}")
                
            except Exception as e:
                action.error = str(e)
                action.status = ActionStatus.FAILED
                print(f"  ✗ Failed: {action.error}")
            
            action.end_time = datetime.now()
            print()
    
    
    class ActionSequencePlanningAgent:
        """
        Agent that plans and executes action sequences.
        """
        
        def __init__(self, model: str = "gpt-3.5-turbo"):
            """
            Initialize action sequence planning agent.
            
            Args:
                model: LLM model to use
            """
            self.action_library = ActionLibrary()
            self.planner = ActionPlanner(self.action_library, model)
            self.executor = PlanExecutor()
        
        def achieve_goal(
            self,
            goal: str,
            initial_state: Optional[Set[str]] = None
        ) -> Dict[str, Any]:
            """
            Plan and execute actions to achieve goal.
            
            Args:
                goal: Goal to achieve
                initial_state: Initial world state
                
            Returns:
                Execution results
            """
            print(f"\n{'=' * 80}")
            print(f"GOAL: {goal}")
            print("=" * 80)
            
            # Create plan
            print("\n[Agent] Creating action plan...")
            plan = self.planner.create_plan(goal, initial_state)
            
            print(f"\n[Agent] Plan created with {len(plan.actions)} actions")
            print("\nACTION PLAN:")
            print("-" * 80)
            
            for action in plan.actions:
                deps = ""
                if action.dependencies:
                    deps = f" (depends on: {[d+1 for d in action.dependencies]})"
                print(f"{action.step_number + 1}. {action.action.name}{deps}")
                print(f"   {action.action.description}")
            
            print("-" * 80)
            print(f"\nInitial state: {', '.join(plan.initial_state) if plan.initial_state else 'none'}")
            print(f"Expected final state: {', '.join(plan.final_state)}")
            
            # Execute plan
            print(f"\n{'=' * 80}")
            print("EXECUTION")
            print("=" * 80)
            
            result = self.executor.execute_plan(plan)
            
            # Summary
            print("=" * 80)
            print("EXECUTION SUMMARY")
            print("=" * 80)
            print(f"Goal: {result['goal']}")
            print(f"Total actions: {result['total_actions']}")
            print(f"Completed: {result['completed']}")
            print(f"Failed: {result['failed']}")
            print(f"Success rate: {result['success_rate']:.0f}%")
            
            return result
    
    
    def demonstrate_action_sequence_planning():
        """Demonstrate the Action Sequence Planning pattern."""
        
        print("=" * 80)
        print("ACTION SEQUENCE PLANNING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = ActionSequencePlanningAgent()
        
        # Show available actions
        print("\n" + "=" * 80)
        print("AVAILABLE ACTIONS")
        print("=" * 80)
        print(agent.action_library.get_all_actions_description())
        
        # Test 1: Software project workflow
        print("\n\n" + "=" * 80)
        print("TEST 1: Software Development Project")
        print("=" * 80)
        
        goal1 = "Complete a software development project from requirements to deployment"
        initial_state1 = {"project_defined"}
        
        result1 = agent.achieve_goal(goal1, initial_state1)
        
        # Test 2: Research and analysis task
        print("\n\n" + "=" * 80)
        print("TEST 2: Research and Analysis Task")
        print("=" * 80)
        
        goal2 = "Research a topic, analyze findings, and create a report"
        initial_state2 = set()
        
        result2 = agent.achieve_goal(goal2, initial_state2)
        
        # Test 3: Design and implementation
        print("\n\n" + "=" * 80)
        print("TEST 3: Design and Implementation")
        print("=" * 80)
        
        goal3 = "Design, implement, and test a new feature"
        initial_state3 = {"requirements_gathered", "research_completed"}
        
        result3 = agent.achieve_goal(goal3, initial_state3)
        
        # Overall summary
        print("\n\n" + "=" * 80)
        print("PATTERN SUMMARY")
        print("=" * 80)
        print("""
    The Action Sequence Planning pattern demonstrates:
    
    1. **Goal Decomposition**: Breaks complex goals into action sequences
    2. **Dependency Analysis**: Identifies action dependencies and ordering
    3. **Precondition Checking**: Ensures actions are feasible
    4. **Effect Modeling**: Tracks state changes from actions
    5. **Sequential Execution**: Executes plan respecting dependencies
    
    Key Benefits:
    - **Structured Approach**: Systematic goal achievement
    - **Dependency Management**: Proper action ordering
    - **State Tracking**: Monitors world state changes
    - **Validation**: Ensures plan feasibility
    - **Transparency**: Clear action sequences and reasoning
    
    Use Cases:
    - Multi-step task automation
    - Workflow orchestration
    - Project planning and execution
    - Robot task planning
    - Business process automation
    
    Planning Principles:
    - Actions have clear preconditions and effects
    - Dependencies are automatically inferred
    - Plans respect causal relationships
    - State evolves through action effects
    - Parallel execution possible when no dependencies
    
    This pattern enables agents to tackle complex, multi-step goals
    through structured planning and systematic execution.
    """)
    
    
    if __name__ == "__main__":
        demonstrate_action_sequence_planning()
    
        

    Agentic Design Pattern: Action Sequence Planning

    Pattern 025: Action Sequence Planning – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 025: Action Sequence Planning

    Description: The Action Sequence Planning pattern enables agents to decompose complex goals into ordered sequences of actions with preconditions, effects, and dependencies. The agent analyzes the goal, identifies required actions, determines ordering constraints, validates feasibility, and executes the plan step-by-step with monitoring and replanning capabilities.

    Components: – Action Library: Catalog of available actions with specifications – Planner: Generates action sequences to achieve goals – Dependency Analyzer: Identifies action dependencies and constraints – Feasibility Checker: Validates plan viability – Executor: Executes plans with monitoring and error handling

    Use Cases: – Multi-step task automation – Workflow orchestration – Robot task planning – Complex problem decomposition – Project planning and execution

    LangChain Implementation: Uses LLM for action selection and ordering, implements STRIPS-like planning with preconditions and effects, handles dynamic replanning.

    Key Features: – Action dependency analysis – Precondition and effect modeling – Parallel action detection – Plan validation – Dynamic replanning on failures


    File Statistics

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

    Module Documentation

    """
    Pattern 025: Action Sequence Planning
    
    Description:
        The Action Sequence Planning pattern enables agents to decompose complex goals
        into ordered sequences of actions with preconditions, effects, and dependencies.
        The agent analyzes the goal, identifies required actions, determines ordering
        constraints, validates feasibility, and executes the plan step-by-step with
        monitoring and replanning capabilities.
    
    Components:
        - Action Library: Catalog of available actions with specifications
        - Planner: Generates action sequences to achieve goals
        - Dependency Analyzer: Identifies action dependencies and constraints
        - Feasibility Checker: Validates plan viability
        - Executor: Executes plans with monitoring and error handling
    
    Use Cases:
        - Multi-step task automation
        - Workflow orchestration
        - Robot task planning
        - Complex problem decomposition
        - Project planning and execution
    
    LangChain Implementation:
        Uses LLM for action selection and ordering, implements STRIPS-like
        planning with preconditions and effects, handles dynamic replanning.
    
    Key Features:
        - Action dependency analysis
        - Precondition and effect modeling
        - Parallel action detection
        - Plan validation
        - Dynamic replanning on failures
    """
    

    Purpose: This module implements the Action Sequence 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: ActionStatus

    Lines 51-58

    Inherits from: Enum

    Purpose:

    Status of action execution.
    

    Class: ActionDefinition

    Lines 62-77

    Purpose:

    Definition of an available action.
    

    Methods:

    to_dict(self)

    Lines 70-77

    Purpose:

    Convert to dictionary for LLM context.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D60F5D0>


    Class: PlannedAction

    Lines 81-90

    Purpose:

    An action in a plan.
    

    Class: ActionPlan

    Lines 94-121

    Purpose:

    Complete action plan.
    

    Methods:

    get_ready_actions(self)

    Lines 102-114

    Purpose:

    Get actions that are ready to execute.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D645450>

    is_complete(self)

    Lines 116-121

    Purpose:

    Check if plan is complete.
    

    Parameters:

    Returns: bool


    Class: ActionLibrary

    Lines 124-246

    Purpose:

    Library of available actions.
    

    Methods:

    __init__(self)

    Lines 129-132

    Purpose:

    Initialize action library with common actions.
    

    Parameters:

    _register_builtin_actions(self)

    Lines 134-225

    Purpose:

    Register built-in actions.
    

    Parameters:

    register_action(self, action)

    Lines 227-229

    Purpose:

    Register a new action.
    

    Parameters: – action: Action

    get_action(self, name)

    Lines 231-233

    Purpose:

    Get action by name.
    

    Parameters: – name: Name

    Returns: <ast.Subscript object at 0x000001A06D7512D0>

    get_all_actions_description(self)

    Lines 235-246

    Purpose:

    Get formatted description of all actions.
    

    Parameters:

    Returns: str


    Class: ActionPlanner

    Lines 249-400

    Purpose:

    Plans action sequences to achieve goals.
    

    Methods:

    __init__(self, action_library, model)

    Lines 254-267

    Purpose:

    Initialize action planner.
    
    Args:
        action_library: Library of available actions
        model: LLM model to use
    

    Parameters: – action_library: Action library – model: Language model instance

    create_plan(self, goal, initial_state)

    Lines 269-332

    Purpose:

    Create action plan to achieve goal.
    
    Args:
        goal: Goal to achieve
        initial_state: Initial world state
    
    Returns:
        Action plan
    

    Parameters: – goal: Goal – initial_state: Initial state

    Returns: ActionPlan

    _parse_action_names(self, response)

    Lines 334-350

    Purpose:

    Parse action names from LLM response.
    

    Parameters: – response: Response

    Returns: <ast.Subscript object at 0x000001A06D648550>

    _build_plan_with_dependencies(self, action_names, initial_state)

    Lines 352-387

    Purpose:

    Build plan with dependency analysis.
    

    Parameters: – action_names: Action names – initial_state: Initial state

    Returns: <ast.Subscript object at 0x000001A06D63C510>

    _calculate_final_state(self, planned_actions, initial_state)

    Lines 389-400

    Purpose:

    Calculate final state after all actions.
    

    Parameters: – planned_actions: Planned actions – initial_state: Initial state

    Returns: <ast.Subscript object at 0x000001A06D6B2710>


    Class: PlanExecutor

    Lines 403-489

    Purpose:

    Executes action plans.
    

    Methods:

    __init__(self)

    Lines 408-410

    Purpose:

    Initialize plan executor.
    

    Parameters:

    execute_plan(self, plan)

    Lines 412-461

    Purpose:

    Execute action plan.
    
    Args:
        plan: Action plan to execute
    
    Returns:
        Execution results
    

    Parameters: – plan: Plan

    Returns: <ast.Subscript object at 0x000001A06D64FF10>

    _execute_action(self, action)

    Lines 463-489

    Purpose:

    Execute a single action.
    

    Parameters: – action: Action


    Class: ActionSequencePlanningAgent

    Lines 492-563

    Purpose:

    Agent that plans and executes action sequences.
    

    Methods:

    __init__(self, model)

    Lines 497-506

    Purpose:

    Initialize action sequence planning agent.
    
    Args:
        model: LLM model to use
    

    Parameters: – model: Language model instance

    achieve_goal(self, goal, initial_state)

    Lines 508-563

    Purpose:

    Plan and execute actions to achieve goal.
    
    Args:
        goal: Goal to achieve
        initial_state: Initial world state
    
    Returns:
        Execution results
    

    Parameters: – goal: Goal – initial_state: Initial state

    Returns: <ast.Subscript object at 0x000001A06D6A8650>


    Functions

    demonstrate_action_sequence_planning()

    Lines 566-647

    Purpose:

    Demonstrate the Action Sequence Planning pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Action Sequence 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 024
    • Pattern 026
    • Other Tool Use Patterns (020-025)

    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 Action Sequence 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: 025_action_sequence_planning.py
    Total Lines: 652
    Pattern Category: AI Agent Design Pattern #025


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Iterative Refinement

    
        """
    Pattern 024: Iterative Refinement
    
    Description:
        The Iterative Refinement pattern enables agents to progressively improve outputs
        through cycles of generation, evaluation, and refinement. The agent generates an
        initial solution, evaluates it against criteria, receives feedback, and refines
        the solution iteratively until quality thresholds are met or iteration limits reached.
    
    Components:
        - Generator: Creates initial and refined solutions
        - Evaluator: Assesses solution quality against criteria
        - Feedback Generator: Produces actionable improvement suggestions
        - Refinement Loop: Orchestrates iterative improvement cycles
        - Convergence Checker: Determines when to stop iterating
    
    Use Cases:
        - Content creation and editing
        - Code optimization and debugging
        - Design iteration and improvement
        - Answer quality enhancement
        - Complex problem solving
    
    LangChain Implementation:
        Uses separate LLM instances for generation and evaluation,
        implements feedback loops with structured criteria, and tracks
        improvement metrics across iterations.
    
    Key Features:
        - Multi-criteria evaluation
        - Targeted feedback generation
        - Iterative improvement tracking
        - Convergence detection
        - Quality metrics monitoring
    """
    
    import os
    from typing import List, Dict, Any, Optional, Callable
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    from dotenv import load_dotenv
    
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_openai import ChatOpenAI
    
    load_dotenv()
    
    
    class EvaluationCriterion(Enum):
        """Criteria for evaluating solutions."""
        ACCURACY = "accuracy"
        COMPLETENESS = "completeness"
        CLARITY = "clarity"
        EFFICIENCY = "efficiency"
        CREATIVITY = "creativity"
        RELEVANCE = "relevance"
        COHERENCE = "coherence"
        STYLE = "style"
    
    
    @dataclass
    class EvaluationScore:
        """Score for a single criterion."""
        criterion: EvaluationCriterion
        score: float  # 0-10
        feedback: str
        
        def to_dict(self) -> Dict[str, Any]:
            """Convert to dictionary."""
            return {
                "criterion": self.criterion.value,
                "score": self.score,
                "feedback": self.feedback
            }
    
    
    @dataclass
    class Evaluation:
        """Complete evaluation of a solution."""
        scores: List[EvaluationScore]
        overall_score: float
        summary: str
        suggestions: List[str]
        timestamp: datetime = field(default_factory=datetime.now)
        
        def get_score(self, criterion: EvaluationCriterion) -> Optional[float]:
            """Get score for specific criterion."""
            for score in self.scores:
                if score.criterion == criterion:
                    return score.score
            return None
        
        def get_feedback(self, criterion: EvaluationCriterion) -> Optional[str]:
            """Get feedback for specific criterion."""
            for score in self.scores:
                if score.criterion == criterion:
                    return score.feedback
            return None
    
    
    @dataclass
    class Solution:
        """A solution with metadata."""
        content: str
        iteration: int
        evaluation: Optional[Evaluation] = None
        timestamp: datetime = field(default_factory=datetime.now)
    
    
    class SolutionGenerator:
        """
        Generates and refines solutions.
        """
        
        def __init__(self, model: str = "gpt-3.5-turbo"):
            """
            Initialize solution generator.
            
            Args:
                model: LLM model to use
            """
            self.llm = ChatOpenAI(model=model, temperature=0.7)
        
        def generate_initial(
            self,
            task: str,
            requirements: Optional[str] = None
        ) -> Solution:
            """
            Generate initial solution.
            
            Args:
                task: Task description
                requirements: Optional requirements
                
            Returns:
                Initial solution
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert problem solver. Generate a high-quality
    solution to the given task."""),
                ("user", "{task}{requirements_str}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            requirements_str = f"\n\nRequirements:\n{requirements}" if requirements else ""
            
            content = chain.invoke({
                "task": task,
                "requirements_str": requirements_str
            })
            
            return Solution(
                content=content.strip(),
                iteration=0
            )
        
        def refine(
            self,
            task: str,
            current_solution: str,
            evaluation: Evaluation,
            iteration: int
        ) -> Solution:
            """
            Refine solution based on feedback.
            
            Args:
                task: Original task
                current_solution: Current solution
                evaluation: Evaluation with feedback
                iteration: Current iteration number
                
            Returns:
                Refined solution
            """
            # Format feedback
            feedback_items = [
                f"- {score.criterion.value}: {score.score}/10 - {score.feedback}"
                for score in evaluation.scores
            ]
            feedback_text = "\n".join(feedback_items)
            
            suggestions_text = "\n".join([
                f"- {suggestion}"
                for suggestion in evaluation.suggestions
            ])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are refining a solution based on evaluation feedback.
    Improve the solution by addressing the feedback while maintaining its strengths."""),
                ("user", """Task: {task}
    
    Current Solution:
    {current_solution}
    
    Evaluation Feedback:
    {feedback_text}
    
    Suggestions for Improvement:
    {suggestions_text}
    
    Overall Score: {overall_score}/10
    
    Provide an improved version that addresses the feedback:""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            content = chain.invoke({
                "task": task,
                "current_solution": current_solution,
                "feedback_text": feedback_text,
                "suggestions_text": suggestions_text,
                "overall_score": evaluation.overall_score
            })
            
            return Solution(
                content=content.strip(),
                iteration=iteration
            )
    
    
    class SolutionEvaluator:
        """
        Evaluates solutions against criteria.
        """
        
        def __init__(self, model: str = "gpt-3.5-turbo"):
            """
            Initialize solution evaluator.
            
            Args:
                model: LLM model to use
            """
            self.llm = ChatOpenAI(model=model, temperature=0.2)
        
        def evaluate(
            self,
            task: str,
            solution: Solution,
            criteria: List[EvaluationCriterion]
        ) -> Evaluation:
            """
            Evaluate solution against criteria.
            
            Args:
                task: Original task
                solution: Solution to evaluate
                criteria: Evaluation criteria
                
            Returns:
                Evaluation with scores and feedback
            """
            criteria_text = ", ".join([c.value for c in criteria])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert evaluator. Assess the solution against
    each criterion on a scale of 0-10, provide specific feedback, and suggest improvements."""),
                ("user", """Task: {task}
    
    Solution:
    {solution}
    
    Evaluation Criteria: {criteria_text}
    
    For each criterion, provide:
    1. Score (0-10)
    2. Specific feedback
    3. Suggestions for improvement
    
    Format your response as:
    CRITERION: [criterion name]
    SCORE: [0-10]
    FEEDBACK: [specific feedback]
    
    [Repeat for each criterion]
    
    OVERALL:
    SCORE: [average score]
    SUMMARY: [brief overall assessment]
    SUGGESTIONS:
    - [suggestion 1]
    - [suggestion 2]
    - [etc.]""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            response = chain.invoke({
                "task": task,
                "solution": solution.content,
                "criteria_text": criteria_text
            })
            
            # Parse evaluation
            return self._parse_evaluation(response, criteria)
        
        def _parse_evaluation(
            self,
            response: str,
            criteria: List[EvaluationCriterion]
        ) -> Evaluation:
            """Parse evaluation from LLM response."""
            lines = response.split("\n")
            
            scores = []
            overall_score = 0.0
            summary = ""
            suggestions = []
            
            current_criterion = None
            current_score = 0.0
            current_feedback = ""
            in_suggestions = False
            
            for line in lines:
                line = line.strip()
                
                if line.startswith("CRITERION:"):
                    if current_criterion:
                        scores.append(EvaluationScore(
                            criterion=current_criterion,
                            score=current_score,
                            feedback=current_feedback
                        ))
                    criterion_name = line.split(":", 1)[1].strip()
                    for c in criteria:
                        if c.value in criterion_name.lower():
                            current_criterion = c
                            break
                    current_feedback = ""
                    
                elif line.startswith("SCORE:"):
                    score_text = line.split(":", 1)[1].strip()
                    try:
                        if "/" in score_text:
                            current_score = float(score_text.split("/")[0])
                        else:
                            current_score = float(score_text)
                    except:
                        current_score = 5.0
                        
                elif line.startswith("FEEDBACK:"):
                    current_feedback = line.split(":", 1)[1].strip()
                    
                elif line.startswith("OVERALL:"):
                    if current_criterion:
                        scores.append(EvaluationScore(
                            criterion=current_criterion,
                            score=current_score,
                            feedback=current_feedback
                        ))
                        current_criterion = None
                        
                elif line.startswith("SUMMARY:"):
                    summary = line.split(":", 1)[1].strip()
                    
                elif line.startswith("SUGGESTIONS:"):
                    in_suggestions = True
                    
                elif in_suggestions and line.startswith("-"):
                    suggestion = line[1:].strip()
                    if suggestion:
                        suggestions.append(suggestion)
            
            # Add last criterion if exists
            if current_criterion:
                scores.append(EvaluationScore(
                    criterion=current_criterion,
                    score=current_score,
                    feedback=current_feedback
                ))
            
            # Calculate overall score
            if scores:
                overall_score = sum(s.score for s in scores) / len(scores)
            else:
                # Fallback parsing
                for line in lines:
                    if "SCORE:" in line and "OVERALL" in response[max(0, response.find(line)-50):response.find(line)]:
                        try:
                            score_text = line.split(":", 1)[1].strip()
                            if "/" in score_text:
                                overall_score = float(score_text.split("/")[0])
                            else:
                                overall_score = float(score_text)
                            break
                        except:
                            pass
            
            if not summary:
                summary = "Evaluation complete"
            
            if not suggestions:
                suggestions = ["Continue refining based on feedback"]
            
            return Evaluation(
                scores=scores if scores else [
                    EvaluationScore(c, overall_score, "See overall feedback")
                    for c in criteria
                ],
                overall_score=overall_score,
                summary=summary,
                suggestions=suggestions
            )
    
    
    class IterativeRefinementAgent:
        """
        Agent that iteratively refines solutions.
        """
        
        def __init__(
            self,
            model: str = "gpt-3.5-turbo",
            max_iterations: int = 3,
            quality_threshold: float = 8.0
        ):
            """
            Initialize iterative refinement agent.
            
            Args:
                model: LLM model to use
                max_iterations: Maximum refinement iterations
                quality_threshold: Minimum quality score to stop iterating
            """
            self.generator = SolutionGenerator(model)
            self.evaluator = SolutionEvaluator(model)
            self.max_iterations = max_iterations
            self.quality_threshold = quality_threshold
            self.history: List[Solution] = []
        
        def solve(
            self,
            task: str,
            criteria: List[EvaluationCriterion],
            requirements: Optional[str] = None
        ) -> Dict[str, Any]:
            """
            Solve task with iterative refinement.
            
            Args:
                task: Task description
                criteria: Evaluation criteria
                requirements: Optional requirements
                
            Returns:
                Solution with refinement history
            """
            print(f"\n[Agent] Starting iterative refinement")
            print(f"[Agent] Task: {task}")
            print(f"[Agent] Criteria: {[c.value for c in criteria]}")
            print(f"[Agent] Max iterations: {self.max_iterations}")
            print(f"[Agent] Quality threshold: {self.quality_threshold}/10\n")
            
            # Generate initial solution
            print("=" * 80)
            print("ITERATION 0: Initial Generation")
            print("=" * 80)
            
            current = self.generator.generate_initial(task, requirements)
            
            print("\nGenerated Solution:")
            print("-" * 80)
            print(current.content)
            print("-" * 80)
            
            # Evaluate initial solution
            print("\nEvaluating...")
            evaluation = self.evaluator.evaluate(task, current, criteria)
            current.evaluation = evaluation
            self.history.append(current)
            
            print(f"\nOverall Score: {evaluation.overall_score:.1f}/10")
            print("\nCriterion Scores:")
            for score in evaluation.scores:
                print(f"  - {score.criterion.value}: {score.score:.1f}/10")
            
            # Refinement loop
            iteration = 1
            while iteration = self.quality_threshold:
                    print(f"\n[Agent] ✓ Quality threshold met ({current.evaluation.overall_score:.1f} >= {self.quality_threshold})")
                    break
                
                print(f"\n{'=' * 80}")
                print(f"ITERATION {iteration}: Refinement")
                print("=" * 80)
                
                print("\nKey Suggestions:")
                for suggestion in current.evaluation.suggestions[:3]:
                    print(f"  - {suggestion}")
                
                # Refine solution
                print("\nRefining solution...")
                current = self.generator.refine(
                    task,
                    current.content,
                    current.evaluation,
                    iteration
                )
                
                print("\nRefined Solution:")
                print("-" * 80)
                print(current.content)
                print("-" * 80)
                
                # Evaluate refined solution
                print("\nEvaluating...")
                evaluation = self.evaluator.evaluate(task, current, criteria)
                current.evaluation = evaluation
                self.history.append(current)
                
                print(f"\nOverall Score: {evaluation.overall_score:.1f}/10")
                
                # Show improvement
                if len(self.history) > 1:
                    prev_score = self.history[-2].evaluation.overall_score
                    improvement = evaluation.overall_score - prev_score
                    print(f"Improvement: {improvement:+.1f}")
                
                print("\nCriterion Scores:")
                for score in evaluation.scores:
                    print(f"  - {score.criterion.value}: {score.score:.1f}/10")
                
                iteration += 1
            
            # Summary
            print(f"\n{'=' * 80}")
            print("REFINEMENT SUMMARY")
            print("=" * 80)
            
            final_solution = self.history[-1]
            initial_score = self.history[0].evaluation.overall_score
            final_score = final_solution.evaluation.overall_score
            
            print(f"\nIterations completed: {len(self.history) - 1}")
            print(f"Initial score: {initial_score:.1f}/10")
            print(f"Final score: {final_score:.1f}/10")
            print(f"Total improvement: {final_score - initial_score:+.1f}")
            print(f"Success: {final_score >= self.quality_threshold}")
            
            return {
                "task": task,
                "final_solution": final_solution,
                "history": self.history,
                "iterations": len(self.history) - 1,
                "initial_score": initial_score,
                "final_score": final_score,
                "improvement": final_score - initial_score,
                "threshold_met": final_score >= self.quality_threshold
            }
    
    
    def demonstrate_iterative_refinement():
        """Demonstrate the Iterative Refinement pattern."""
        
        print("=" * 80)
        print("ITERATIVE REFINEMENT PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = IterativeRefinementAgent(max_iterations=3, quality_threshold=7.5)
        
        # Test 1: Short essay
        print("\n" + "=" * 80)
        print("TEST 1: Essay Writing with Refinement")
        print("=" * 80)
        
        task1 = "Write a concise essay (150-200 words) about the importance of artificial intelligence in healthcare"
        criteria1 = [
            EvaluationCriterion.ACCURACY,
            EvaluationCriterion.CLARITY,
            EvaluationCriterion.COHERENCE
        ]
        
        result1 = agent.solve(task1, criteria1)
        
        # Test 2: Problem solution
        print("\n\n" + "=" * 80)
        print("TEST 2: Problem Solving with Multiple Criteria")
        print("=" * 80)
        
        agent2 = IterativeRefinementAgent(max_iterations=2, quality_threshold=8.0)
        
        task2 = "Explain how to reduce carbon emissions in urban areas"
        criteria2 = [
            EvaluationCriterion.COMPLETENESS,
            EvaluationCriterion.RELEVANCE,
            EvaluationCriterion.CREATIVITY
        ]
        
        result2 = agent2.solve(task2, criteria2)
        
        # Overall summary
        print("\n\n" + "=" * 80)
        print("PATTERN SUMMARY")
        print("=" * 80)
        print("""
    The Iterative Refinement pattern demonstrates:
    
    1. **Progressive Improvement**: Solutions improve through feedback cycles
    2. **Multi-Criteria Evaluation**: Assesses multiple quality dimensions
    3. **Targeted Feedback**: Provides specific, actionable suggestions
    4. **Convergence Detection**: Stops when quality threshold met
    5. **Improvement Tracking**: Monitors progress across iterations
    
    Key Benefits:
    - **Higher Quality**: Iterative refinement produces better solutions
    - **Transparency**: Shows improvement process and reasoning
    - **Flexibility**: Works with any evaluation criteria
    - **Efficiency**: Stops early if threshold met
    - **Debuggability**: Complete history of refinements
    
    Use Cases:
    - Content creation and editing
    - Code optimization
    - Design iteration
    - Answer improvement
    - Strategy development
    
    Refinement Patterns Observed:
    - Initial solutions often score 5-7/10
    - Each iteration typically improves by 0.5-1.5 points
    - Convergence usually achieved in 2-3 iterations
    - Specific feedback drives targeted improvements
    
    This pattern is essential for producing high-quality outputs in
    production agentic systems where initial responses may be insufficient.
    """)
    
    
    if __name__ == "__main__":
        demonstrate_iterative_refinement()
    
        

    Agentic Design Pattern: Iterative Refinement

    Pattern 024: Iterative Refinement – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 024: Iterative Refinement

    Description: The Iterative Refinement pattern enables agents to progressively improve outputs through cycles of generation, evaluation, and refinement. The agent generates an initial solution, evaluates it against criteria, receives feedback, and refines the solution iteratively until quality thresholds are met or iteration limits reached.

    Components: – Generator: Creates initial and refined solutions – Evaluator: Assesses solution quality against criteria – Feedback Generator: Produces actionable improvement suggestions – Refinement Loop: Orchestrates iterative improvement cycles – Convergence Checker: Determines when to stop iterating

    Use Cases: – Content creation and editing – Code optimization and debugging – Design iteration and improvement – Answer quality enhancement – Complex problem solving

    LangChain Implementation: Uses separate LLM instances for generation and evaluation, implements feedback loops with structured criteria, and tracks improvement metrics across iterations.

    Key Features: – Multi-criteria evaluation – Targeted feedback generation – Iterative improvement tracking – Convergence detection – Quality metrics monitoring


    File Statistics

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

    Module Documentation

    """
    Pattern 024: Iterative Refinement
    
    Description:
        The Iterative Refinement pattern enables agents to progressively improve outputs
        through cycles of generation, evaluation, and refinement. The agent generates an
        initial solution, evaluates it against criteria, receives feedback, and refines
        the solution iteratively until quality thresholds are met or iteration limits reached.
    
    Components:
        - Generator: Creates initial and refined solutions
        - Evaluator: Assesses solution quality against criteria
        - Feedback Generator: Produces actionable improvement suggestions
        - Refinement Loop: Orchestrates iterative improvement cycles
        - Convergence Checker: Determines when to stop iterating
    
    Use Cases:
        - Content creation and editing
        - Code optimization and debugging
        - Design iteration and improvement
        - Answer quality enhancement
        - Complex problem solving
    
    LangChain Implementation:
        Uses separate LLM instances for generation and evaluation,
        implements feedback loops with structured criteria, and tracks
        improvement metrics across iterations.
    
    Key Features:
        - Multi-criteria evaluation
        - Targeted feedback generation
        - Iterative improvement tracking
        - Convergence detection
        - Quality metrics monitoring
    """
    

    Purpose: This module implements the Iterative Refinement 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.Callable
    import typing.Dict
    import typing.List
    import typing.Optional
    

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

    Lines 51-60

    Inherits from: Enum

    Purpose:

    Criteria for evaluating solutions.
    

    Class: EvaluationScore

    Lines 64-76

    Purpose:

    Score for a single criterion.
    

    Methods:

    to_dict(self)

    Lines 70-76

    Purpose:

    Convert to dictionary.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D677AD0>


    Class: Evaluation

    Lines 80-100

    Purpose:

    Complete evaluation of a solution.
    

    Methods:

    get_score(self, criterion)

    Lines 88-93

    Purpose:

    Get score for specific criterion.
    

    Parameters: – criterion: Criterion

    Returns: <ast.Subscript object at 0x000001A06D676DD0>

    get_feedback(self, criterion)

    Lines 95-100

    Purpose:

    Get feedback for specific criterion.
    

    Parameters: – criterion: Criterion

    Returns: <ast.Subscript object at 0x000001A06D72AF10>


    Class: Solution

    Lines 104-109

    Purpose:

    A solution with metadata.
    

    Class: SolutionGenerator

    Lines 112-224

    Purpose:

    Generates and refines solutions.
    

    Methods:

    __init__(self, model)

    Lines 117-124

    Purpose:

    Initialize solution generator.
    
    Args:
        model: LLM model to use
    

    Parameters: – model: Language model instance

    generate_initial(self, task, requirements)

    Lines 126-159

    Purpose:

    Generate initial solution.
    
    Args:
        task: Task description
        requirements: Optional requirements
    
    Returns:
        Initial solution
    

    Parameters: – task: Task – requirements: Requirements

    Returns: Solution

    refine(self, task, current_solution, evaluation, iteration)

    Lines 161-224

    Purpose:

    Refine solution based on feedback.
    
    Args:
        task: Original task
        current_solution: Current solution
        evaluation: Evaluation with feedback
        iteration: Current iteration number
    
    Returns:
        Refined solution
    

    Parameters: – task: Task – current_solution: Current solution – evaluation: Evaluation – iteration: Iteration

    Returns: Solution


    Class: SolutionEvaluator

    Lines 227-409

    Purpose:

    Evaluates solutions against criteria.
    

    Methods:

    __init__(self, model)

    Lines 232-239

    Purpose:

    Initialize solution evaluator.
    
    Args:
        model: LLM model to use
    

    Parameters: – model: Language model instance

    evaluate(self, task, solution, criteria)

    Lines 241-300

    Purpose:

    Evaluate solution against criteria.
    
    Args:
        task: Original task
        solution: Solution to evaluate
        criteria: Evaluation criteria
    
    Returns:
        Evaluation with scores and feedback
    

    Parameters: – task: Task – solution: Solution – criteria: Criteria

    Returns: Evaluation

    _parse_evaluation(self, response, criteria)

    Lines 302-409

    Purpose:

    Parse evaluation from LLM response.
    

    Parameters: – response: Response – criteria: Criteria

    Returns: Evaluation


    Class: IterativeRefinementAgent

    Lines 412-557

    Purpose:

    Agent that iteratively refines solutions.
    

    Methods:

    __init__(self, model, max_iterations, quality_threshold)

    Lines 417-435

    Purpose:

    Initialize iterative refinement agent.
    
    Args:
        model: LLM model to use
        max_iterations: Maximum refinement iterations
        quality_threshold: Minimum quality score to stop iterating
    

    Parameters: – model: Language model instance – max_iterations: Max iterations – quality_threshold: Quality threshold

    solve(self, task, criteria, requirements)

    Lines 437-557

    Purpose:

    Solve task with iterative refinement.
    
    Args:
        task: Task description
        criteria: Evaluation criteria
        requirements: Optional requirements
    
    Returns:
        Solution with refinement history
    

    Parameters: – task: Task – criteria: Criteria – requirements: Requirements

    Returns: <ast.Subscript object at 0x000001A06D633A90>


    Functions

    demonstrate_iterative_refinement()

    Lines 560-634

    Purpose:

    Demonstrate the Iterative Refinement pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Iterative Refinement 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 023
    • Pattern 025
    • Other Tool Use Patterns (020-025)

    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 Iterative Refinement 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: 024_iterative_refinement.py
    Total Lines: 639
    Pattern Category: AI Agent Design Pattern #024


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Rag

    
        """
    Pattern 023: Retrieval-Augmented Generation (RAG)
    
    Description:
        Agent retrieves relevant information from a knowledge base before generating responses.
        RAG combines the power of retrieval systems with generative models to provide
        grounded, factual responses based on external knowledge.
    
    Components:
        - Document Loader: Loads documents into the system
        - Text Splitter: Chunks documents into manageable pieces
        - Embedding Model: Converts text to vector representations
        - Vector Store: Stores and retrieves embeddings efficiently
        - Retriever: Finds relevant documents
        - LLM: Generates response based on retrieved context
    
    Use Cases:
        - Question answering over documents
        - Knowledge-intensive tasks
        - Chat with your data applications
        - Customer support with documentation
    
    LangChain Implementation:
        Uses LangChain's built-in RAG components with vector stores and retrievers.
    """
    
    import os
    from typing import List, Dict, Any
    from dotenv import load_dotenv
    
    from langchain_community.document_loaders import TextLoader
    from langchain_core.documents import Document
    from langchain.text_splitter import RecursiveCharacterTextSplitter
    from langchain_openai import OpenAIEmbeddings, ChatOpenAI
    from langchain_community.vectorstores import Chroma
    from langchain.chains import RetrievalQA
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.output_parsers import StrOutputParser
    from langchain_core.runnables import RunnablePassthrough
    
    # Load environment variables
    load_dotenv()
    
    
    # Sample documents for demonstration
    SAMPLE_DOCUMENTS = [
        {
            "content": """LangChain is a framework for developing applications powered by language models. 
    It enables applications that are context-aware and can reason. LangChain provides modular 
    components for working with language models, as well as pre-built chains and agents.""",
            "metadata": {"source": "langchain_intro", "topic": "framework"}
        },
        {
            "content": """Vector databases are specialized databases designed to store and query 
    high-dimensional vectors efficiently. They use techniques like approximate nearest neighbor 
    search to find similar vectors quickly. Popular vector databases include Pinecone, Weaviate, 
    and Chroma.""",
            "metadata": {"source": "vector_db_guide", "topic": "databases"}
        },
        {
            "content": """Embeddings are numerical representations of text that capture semantic meaning. 
    Words or sentences with similar meanings have similar embeddings. Common embedding models include 
    OpenAI's text-embedding-ada-002 and open-source alternatives like Sentence Transformers.""",
            "metadata": {"source": "embeddings_explained", "topic": "embeddings"}
        },
        {
            "content": """Retrieval-Augmented Generation (RAG) combines retrieval systems with language models. 
    First, relevant documents are retrieved from a knowledge base. Then, these documents are provided 
    as context to the language model, which generates a response grounded in the retrieved information.""",
            "metadata": {"source": "rag_overview", "topic": "rag"}
        },
        {
            "content": """Semantic search finds results based on meaning rather than exact keyword matches. 
    It uses embeddings to represent queries and documents in a vector space, then finds the most 
    semantically similar documents. This enables finding relevant information even when different 
    words are used.""",
            "metadata": {"source": "semantic_search_guide", "topic": "search"}
        }
    ]
    
    
    class RAGAgent:
        """Agent that uses Retrieval-Augmented Generation."""
        
        def __init__(
            self,
            model_name: str = "gpt-3.5-turbo",
            embedding_model: str = "text-embedding-ada-002",
            chunk_size: int = 500,
            chunk_overlap: int = 50,
            k_documents: int = 3
        ):
            """
            Initialize the RAG agent.
            
            Args:
                model_name: LLM model to use for generation
                embedding_model: Embedding model for vector representations
                chunk_size: Size of text chunks
                chunk_overlap: Overlap between chunks
                k_documents: Number of documents to retrieve
            """
            self.llm = ChatOpenAI(
                model=model_name,
                temperature=0,
                api_key=os.getenv("OPENAI_API_KEY")
            )
            
            self.embeddings = OpenAIEmbeddings(
                model=embedding_model,
                api_key=os.getenv("OPENAI_API_KEY")
            )
            
            self.text_splitter = RecursiveCharacterTextSplitter(
                chunk_size=chunk_size,
                chunk_overlap=chunk_overlap,
                length_function=len
            )
            
            self.k_documents = k_documents
            self.vectorstore = None
            self.retriever = None
        
        def load_documents(self, documents: List[Dict[str, Any]]) -> None:
            """
            Load documents into the vector store.
            
            Args:
                documents: List of dictionaries with 'content' and 'metadata' keys
            """
            # Convert to Document objects
            docs = [
                Document(page_content=doc["content"], metadata=doc["metadata"])
                for doc in documents
            ]
            
            # Split documents into chunks
            splits = self.text_splitter.split_documents(docs)
            
            print(f"Loaded {len(docs)} documents, split into {len(splits)} chunks")
            
            # Create vector store
            self.vectorstore = Chroma.from_documents(
                documents=splits,
                embedding=self.embeddings,
                collection_name="rag_demo"
            )
            
            # Create retriever
            self.retriever = self.vectorstore.as_retriever(
                search_kwargs={"k": self.k_documents}
            )
        
        def retrieve_documents(self, query: str) -> List[Document]:
            """
            Retrieve relevant documents for a query.
            
            Args:
                query: Search query
                
            Returns:
                List of relevant documents
            """
            if not self.retriever:
                raise ValueError("No documents loaded. Call load_documents first.")
            
            return self.retriever.get_relevant_documents(query)
        
        def query_with_retrieval_qa(self, query: str) -> Dict[str, Any]:
            """
            Query using RetrievalQA chain (simple approach).
            
            Args:
                query: Question to answer
                
            Returns:
                Dictionary with answer and source documents
            """
            if not self.retriever:
                raise ValueError("No documents loaded. Call load_documents first.")
            
            # Create RetrievalQA chain
            qa_chain = RetrievalQA.from_chain_type(
                llm=self.llm,
                chain_type="stuff",  # "stuff" puts all docs into prompt
                retriever=self.retriever,
                return_source_documents=True
            )
            
            result = qa_chain.invoke({"query": query})
            
            return {
                "query": query,
                "answer": result["result"],
                "source_documents": result["source_documents"]
            }
        
        def query_with_custom_prompt(self, query: str) -> Dict[str, Any]:
            """
            Query using custom prompt template (more control).
            
            Args:
                query: Question to answer
                
            Returns:
                Dictionary with answer and retrieved documents
            """
            if not self.retriever:
                raise ValueError("No documents loaded. Call load_documents first.")
            
            # Custom prompt template
            template = """Answer the question based on the following context. 
    If you cannot answer the question based on the context, say so clearly.
    
    Context:
    {context}
    
    Question: {question}
    
    Answer:"""
            
            prompt = ChatPromptTemplate.from_template(template)
            
            # Create RAG chain using LCEL (LangChain Expression Language)
            def format_docs(docs):
                return "\n\n".join(doc.page_content for doc in docs)
            
            rag_chain = (
                {"context": self.retriever | format_docs, "question": RunnablePassthrough()}
                | prompt
                | self.llm
                | StrOutputParser()
            )
            
            # Get retrieved documents for transparency
            retrieved_docs = self.retrieve_documents(query)
            
            # Generate answer
            answer = rag_chain.invoke(query)
            
            return {
                "query": query,
                "answer": answer,
                "retrieved_documents": retrieved_docs,
                "num_retrieved": len(retrieved_docs)
            }
        
        def query_with_sources(self, query: str) -> Dict[str, Any]:
            """
            Query with explicit source citations.
            
            Args:
                query: Question to answer
                
            Returns:
                Dictionary with answer and cited sources
            """
            if not self.retriever:
                raise ValueError("No documents loaded. Call load_documents first.")
            
            # Retrieve documents
            docs = self.retrieve_documents(query)
            
            # Create context with source labels
            context_with_sources = ""
            sources = []
            
            for i, doc in enumerate(docs, 1):
                source_label = f"[Source {i}: {doc.metadata.get('source', 'unknown')}]"
                context_with_sources += f"{source_label}\n{doc.page_content}\n\n"
                sources.append({
                    "id": i,
                    "source": doc.metadata.get('source', 'unknown'),
                    "topic": doc.metadata.get('topic', 'unknown'),
                    "content": doc.page_content
                })
            
            # Generate answer with citations
            template = """Answer the question based on the following sources. 
    Cite your sources using [Source N] notation where applicable.
    
    Sources:
    {context}
    
    Question: {question}
    
    Answer (with citations):"""
            
            prompt = ChatPromptTemplate.from_template(template)
            chain = prompt | self.llm | StrOutputParser()
            
            answer = chain.invoke({
                "context": context_with_sources,
                "question": query
            })
            
            return {
                "query": query,
                "answer": answer,
                "sources": sources
            }
        
        def cleanup(self):
            """Clean up resources."""
            if self.vectorstore:
                try:
                    self.vectorstore.delete_collection()
                except:
                    pass
    
    
    def demonstrate_rag_pattern():
        """Demonstrates the RAG pattern with various approaches."""
        
        print("=" * 80)
        print("PATTERN 023: Retrieval-Augmented Generation (RAG)")
        print("=" * 80)
        print()
        
        # Create RAG agent
        agent = RAGAgent(k_documents=3)
        
        # Load sample documents
        print("Loading documents into vector store...")
        agent.load_documents(SAMPLE_DOCUMENTS)
        print()
        
        # Test queries
        queries = [
            "What is LangChain?",
            "How do vector databases work?",
            "What is RAG and how does it combine retrieval with generation?",
        ]
        
        # Approach 1: RetrievalQA chain
        print("\n" + "=" * 80)
        print("APPROACH 1: RetrievalQA Chain (Simple)")
        print("=" * 80)
        
        for i, query in enumerate(queries[:2], 1):
            print(f"\n{'- ' * 40}")
            print(f"Query {i}: {query}")
            print('- ' * 40)
            
            try:
                result = agent.query_with_retrieval_qa(query)
                print(f"\nAnswer:\n{result['answer']}")
                print(f"\nSources used: {len(result['source_documents'])} documents")
            except Exception as e:
                print(f"\n✗ Error: {str(e)}")
        
        # Approach 2: Custom prompt with LCEL
        print("\n\n" + "=" * 80)
        print("APPROACH 2: Custom Prompt with LCEL")
        print("=" * 80)
        
        query = queries[1]
        print(f"\n{'- ' * 40}")
        print(f"Query: {query}")
        print('- ' * 40)
        
        try:
            result = agent.query_with_custom_prompt(query)
            print(f"\nAnswer:\n{result['answer']}")
            print(f"\nRetrieved {result['num_retrieved']} documents")
            print("\nRetrieved document topics:")
            for doc in result['retrieved_documents']:
                print(f"  - {doc.metadata.get('topic', 'unknown')}")
        except Exception as e:
            print(f"\n✗ Error: {str(e)}")
        
        # Approach 3: With source citations
        print("\n\n" + "=" * 80)
        print("APPROACH 3: Answer with Source Citations")
        print("=" * 80)
        
        query = queries[2]
        print(f"\n{'- ' * 40}")
        print(f"Query: {query}")
        print('- ' * 40)
        
        try:
            result = agent.query_with_sources(query)
            print(f"\nAnswer:\n{result['answer']}")
            print(f"\nSources:")
            for source in result['sources']:
                print(f"  [{source['id']}] {source['source']} (Topic: {source['topic']})")
        except Exception as e:
            print(f"\n✗ Error: {str(e)}")
        
        # Cleanup
        agent.cleanup()
        
        # Summary
        print("\n\n" + "=" * 80)
        print("RAG PATTERN DEMONSTRATION COMPLETE")
        print("=" * 80)
        print()
        print("RAG Components Demonstrated:")
        print("1. Document loading and chunking")
        print("2. Vector embeddings and storage")
        print("3. Semantic retrieval")
        print("4. Context-aware generation")
        print("5. Source attribution")
        print()
        print("Key Benefits:")
        print("- Grounded responses based on actual data")
        print("- Reduced hallucinations")
        print("- Up-to-date information")
        print("- Source traceability")
        print("- Scalable knowledge base")
        print()
        print("LangChain Components Used:")
        print("- RecursiveCharacterTextSplitter: Chunks documents")
        print("- OpenAIEmbeddings: Creates vector embeddings")
        print("- Chroma: Vector store for efficient retrieval")
        print("- RetrievalQA: Pre-built RAG chain")
        print("- LCEL: Custom chain composition")
        print("- Document retriever: Finds relevant documents")
        print()
    
    
    if __name__ == "__main__":
        # Check for API key
        if not os.getenv("OPENAI_API_KEY"):
            print("Error: OPENAI_API_KEY not found in environment variables.")
            print("Please set it in your .env file or environment.")
            exit(1)
        
        demonstrate_rag_pattern()
    
        

    Agentic Design Pattern: Rag

    Pattern 023: Rag – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 023: Retrieval-Augmented Generation (RAG)

    Description: Agent retrieves relevant information from a knowledge base before generating responses. RAG combines the power of retrieval systems with generative models to provide grounded, factual responses based on external knowledge.

    Components: – Document Loader: Loads documents into the system – Text Splitter: Chunks documents into manageable pieces – Embedding Model: Converts text to vector representations – Vector Store: Stores and retrieves embeddings efficiently – Retriever: Finds relevant documents – LLM: Generates response based on retrieved context

    Use Cases: – Question answering over documents – Knowledge-intensive tasks – Chat with your data applications – Customer support with documentation

    LangChain Implementation: Uses LangChain’s built-in RAG components with vector stores and retrievers.


    File Statistics

    • Total Lines: 431
    • Classes: 1
    • Functions: 1
    • Constants: 1

    Module Documentation

    """
    Pattern 023: Retrieval-Augmented Generation (RAG)
    
    Description:
        Agent retrieves relevant information from a knowledge base before generating responses.
        RAG combines the power of retrieval systems with generative models to provide
        grounded, factual responses based on external knowledge.
    
    Components:
        - Document Loader: Loads documents into the system
        - Text Splitter: Chunks documents into manageable pieces
        - Embedding Model: Converts text to vector representations
        - Vector Store: Stores and retrieves embeddings efficiently
        - Retriever: Finds relevant documents
        - LLM: Generates response based on retrieved context
    
    Use Cases:
        - Question answering over documents
        - Knowledge-intensive tasks
        - Chat with your data applications
        - Customer support with documentation
    
    LangChain Implementation:
        Uses LangChain's built-in RAG components with vector stores and retrievers.
    """
    

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


    Import Statements

    Standard Library Imports

    import langchain_community.vectorstores.Chroma
    import langchain_core.documents.Document
    import langchain_core.output_parsers.StrOutputParser
    import langchain_core.prompts.ChatPromptTemplate
    import langchain_core.runnables.RunnablePassthrough
    import os
    import typing.Any
    import typing.Dict
    import typing.List
    

    Purpose: Core Python functionality: – langchain_community.vectorstores.Chroma: Regular expression operations – langchain_core.documents.Document: Regular expression operations – langchain_core.output_parsers.StrOutputParser: Regular expression operations – langchain_core.prompts.ChatPromptTemplate: Regular expression operations – langchain_core.runnables.RunnablePassthrough: Regular expression operations – os: Environment variable access and system operations – 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)

    LangChain Imports

    from langchain.chains.RetrievalQA
    from langchain.text_splitter.RecursiveCharacterTextSplitter
    from langchain_community.document_loaders.TextLoader
    from langchain_openai.ChatOpenAI
    from langchain_openai.OpenAIEmbeddings
    

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

    Other Imports

    from dotenv.load_dotenv
    

    Purpose: Additional dependencies for specialized functionality.


    Module Constants

    SAMPLE_DOCUMENTS

    SAMPLE_DOCUMENTS = [
    

    Line 46: Module-level constant definition.


    Classes

    Class: RAGAgent

    Lines 82-309

    Purpose:

    Agent that uses Retrieval-Augmented Generation.
    

    Methods:

    __init__(self, model_name, embedding_model, chunk_size, chunk_overlap, k_documents)

    Lines 85-122

    Purpose:

    Initialize the RAG agent.
    
    Args:
        model_name: LLM model to use for generation
        embedding_model: Embedding model for vector representations
        chunk_size: Size of text chunks
        chunk_overlap: Overlap between chunks
        k_documents: Number of documents to retrieve
    

    Parameters: – model_name: Language model instance – embedding_model: Language model instance – chunk_size: Chunk size – chunk_overlap: Chunk overlap – k_documents: K documents

    load_documents(self, documents)

    Lines 124-152

    Purpose:

    Load documents into the vector store.
    
    Args:
        documents: List of dictionaries with 'content' and 'metadata' keys
    

    Parameters: – documents: Documents

    Returns: None

    retrieve_documents(self, query)

    Lines 154-167

    Purpose:

    Retrieve relevant documents for a query.
    
    Args:
        query: Search query
    
    Returns:
        List of relevant documents
    

    Parameters: – query: Input query or question

    Returns: <ast.Subscript object at 0x000001A06D6F7E90>

    query_with_retrieval_qa(self, query)

    Lines 169-196

    Purpose:

    Query using RetrievalQA chain (simple approach).
    
    Args:
        query: Question to answer
    
    Returns:
        Dictionary with answer and source documents
    

    Parameters: – query: Input query or question

    Returns: <ast.Subscript object at 0x000001A06D702390>

    query_with_custom_prompt(self, query)

    Lines 198-246

    Purpose:

    Query using custom prompt template (more control).
    
    Args:
        query: Question to answer
    
    Returns:
        Dictionary with answer and retrieved documents
    

    Parameters: – query: Input query or question

    Returns: <ast.Subscript object at 0x000001A06D702990>

    query_with_sources(self, query)

    Lines 248-301

    Purpose:

    Query with explicit source citations.
    
    Args:
        query: Question to answer
    
    Returns:
        Dictionary with answer and cited sources
    

    Parameters: – query: Input query or question

    Returns: <ast.Subscript object at 0x000001A06D64EB90>

    cleanup(self)

    Lines 303-309

    Purpose:

    Clean up resources.
    

    Parameters:


    Functions

    demonstrate_rag_pattern()

    Lines 312-420

    Purpose:

    Demonstrates the RAG pattern with various approaches.
    

    Key Concepts Summary

    Pattern Overview

    The Rag 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 022
    • Pattern 024
    • Other Tool Use Patterns (020-025)

    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 Rag 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: 023_rag.py
    Total Lines: 431
    Pattern Category: AI Agent Design Pattern #023


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Code Generation

    
        """
    Pattern 022: Code Generation & Execution
    
    Description:
        The Code Generation & Execution pattern enables agents to generate executable code
        in response to user requests, safely execute the code in a sandboxed environment,
        and return results. This pattern combines LLM code generation capabilities with
        secure execution environments to solve computational problems programmatically.
    
    Components:
        - Code Generator: LLM generates code from natural language
        - Syntax Validator: Checks code for syntax errors
        - Sandbox Environment: Isolated execution context
        - Result Processor: Formats and returns execution results
        - Error Handler: Captures and explains execution errors
    
    Use Cases:
        - Data analysis and visualization
        - Mathematical problem solving
        - Text processing and transformation
        - Algorithm implementation
        - Quick prototyping and testing
    
    LangChain Implementation:
        Uses LLM for code generation with specific prompting strategies,
        implements safe execution with restricted environments, and handles
        multiple programming languages.
    
    Key Features:
        - Multi-language support (Python, JavaScript, etc.)
        - Sandboxed execution for security
        - Syntax and runtime error handling
        - Result capturing and formatting
        - Iterative refinement on errors
    """
    
    import os
    import sys
    from typing import List, Dict, Any, Optional
    from dataclasses import dataclass, field
    from enum import Enum
    from datetime import datetime
    import io
    import contextlib
    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 Language(Enum):
        """Supported programming languages."""
        PYTHON = "python"
        JAVASCRIPT = "javascript"
    
    
    class ExecutionStatus(Enum):
        """Status of code execution."""
        SUCCESS = "success"
        SYNTAX_ERROR = "syntax_error"
        RUNTIME_ERROR = "runtime_error"
        TIMEOUT = "timeout"
        FORBIDDEN = "forbidden"
    
    
    @dataclass
    class CodeSnippet:
        """Generated code snippet."""
        language: Language
        code: str
        description: str
        timestamp: datetime = field(default_factory=datetime.now)
    
    
    @dataclass
    class ExecutionResult:
        """Result of code execution."""
        code: str
        status: ExecutionStatus
        output: Optional[str] = None
        error: Optional[str] = None
        duration: Optional[float] = None
        timestamp: datetime = field(default_factory=datetime.now)
    
    
    class CodeGenerator:
        """
        Generates code from natural language descriptions.
        """
        
        def __init__(self, model: str = "gpt-3.5-turbo"):
            """
            Initialize code generator.
            
            Args:
                model: LLM model to use
            """
            self.llm = ChatOpenAI(model=model, temperature=0.2)
        
        def generate_python(
            self,
            task: str,
            context: Optional[str] = None
        ) -> CodeSnippet:
            """
            Generate Python code for a task.
            
            Args:
                task: Task description
                context: Optional context or constraints
                
            Returns:
                Generated code snippet
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert Python programmer. Generate clean, efficient,
    and well-commented Python code to accomplish the given task.
    
    Guidelines:
    - Write complete, runnable code
    - Use standard library when possible
    - Include helpful comments
    - Handle edge cases
    - Use proper error handling
    - Print or return results clearly
    
    Respond with ONLY the Python code, no explanations before or after."""),
                ("user", "{task}{context_str}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            context_str = f"\n\nContext: {context}" if context else ""
            
            response = chain.invoke({
                "task": task,
                "context_str": context_str
            })
            
            # Extract code from response
            code = self._extract_code(response)
            
            return CodeSnippet(
                language=Language.PYTHON,
                code=code,
                description=task
            )
        
        def generate_javascript(
            self,
            task: str,
            context: Optional[str] = None
        ) -> CodeSnippet:
            """
            Generate JavaScript code for a task.
            
            Args:
                task: Task description
                context: Optional context
                
            Returns:
                Generated code snippet
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are an expert JavaScript programmer. Generate clean, efficient
    JavaScript code to accomplish the given task.
    
    Guidelines:
    - Write complete, runnable Node.js code
    - Use modern ES6+ syntax
    - Include helpful comments
    - Handle edge cases
    - Use console.log for output
    
    Respond with ONLY the JavaScript code, no explanations."""),
                ("user", "{task}{context_str}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            context_str = f"\n\nContext: {context}" if context else ""
            
            response = chain.invoke({
                "task": task,
                "context_str": context_str
            })
            
            code = self._extract_code(response)
            
            return CodeSnippet(
                language=Language.JAVASCRIPT,
                code=code,
                description=task
            )
        
        def refine_code(
            self,
            original_code: str,
            error: str,
            language: Language
        ) -> str:
            """
            Refine code based on error feedback.
            
            Args:
                original_code: Original code that failed
                error: Error message
                language: Programming language
                
            Returns:
                Refined code
            """
            prompt = ChatPromptTemplate.from_messages([
                ("system", f"""You are debugging {language.value} code. Fix the error in the
    provided code and return the corrected version.
    
    Respond with ONLY the corrected code, no explanations."""),
                ("user", """Original Code:
    ```{language}
    {code}
    ```
    
    Error:
    {error}
    
    Provide the corrected code:""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            response = chain.invoke({
                "language": language.value,
                "code": original_code,
                "error": error
            })
            
            return self._extract_code(response)
        
        def _extract_code(self, response: str) -> str:
            """Extract code from LLM response."""
            # Remove markdown code blocks
            lines = response.split("\n")
            code_lines = []
            in_code_block = False
            
            for line in lines:
                if line.strip().startswith("```"):
                    in_code_block = not in_code_block
                    continue
                if in_code_block or not line.strip().startswith("```"):
                    code_lines.append(line)
            
            code = "\n".join(code_lines).strip()
            
            # If no code blocks found, return entire response
            if not code:
                code = response.strip()
            
            return code
    
    
    class SandboxExecutor:
        """
        Safely executes code in sandboxed environment.
        """
        
        def __init__(self):
            """Initialize sandbox executor."""
            # Restricted builtins for Python execution
            self.safe_builtins = {
                'abs': abs, 'all': all, 'any': any, 'ascii': ascii,
                'bin': bin, 'bool': bool, 'chr': chr, 'dict': dict,
                'divmod': divmod, 'enumerate': enumerate, 'filter': filter,
                'float': float, 'format': format, 'hex': hex, 'int': int,
                'len': len, 'list': list, 'map': map, 'max': max,
                'min': min, 'oct': oct, 'ord': ord, 'pow': pow,
                'range': range, 'reversed': reversed, 'round': round,
                'set': set, 'slice': slice, 'sorted': sorted, 'str': str,
                'sum': sum, 'tuple': tuple, 'zip': zip,
                'print': print, 'isinstance': isinstance, 'type': type
            }
        
        def execute_python(self, code: str) -> ExecutionResult:
            """
            Execute Python code safely.
            
            Args:
                code: Python code to execute
                
            Returns:
                Execution result
            """
            import time
            
            # Check for forbidden operations
            forbidden_keywords = ['import os', 'import sys', 'eval', 'exec', '__import__', 
                                'open', 'file', 'compile']
            for keyword in forbidden_keywords:
                if keyword in code:
                    return ExecutionResult(
                        code=code,
                        status=ExecutionStatus.FORBIDDEN,
                        error=f"Forbidden operation: {keyword}"
                    )
            
            # Syntax check
            try:
                compile(code, '', 'exec')
            except SyntaxError as e:
                return ExecutionResult(
                    code=code,
                    status=ExecutionStatus.SYNTAX_ERROR,
                    error=str(e)
                )
            
            # Execute in sandbox
            output_buffer = io.StringIO()
            
            try:
                start_time = time.time()
                
                # Redirect stdout
                with contextlib.redirect_stdout(output_buffer):
                    # Create restricted namespace
                    namespace = {'__builtins__': self.safe_builtins}
                    
                    # Execute code
                    exec(code, namespace)
                
                duration = time.time() - start_time
                output = output_buffer.getvalue()
                
                return ExecutionResult(
                    code=code,
                    status=ExecutionStatus.SUCCESS,
                    output=output if output else "Code executed successfully (no output)",
                    duration=duration
                )
                
            except Exception as e:
                return ExecutionResult(
                    code=code,
                    status=ExecutionStatus.RUNTIME_ERROR,
                    error=f"{type(e).__name__}: {str(e)}"
                )
        
        def execute_javascript(self, code: str) -> ExecutionResult:
            """
            Execute JavaScript code (simulated - would need Node.js).
            
            Args:
                code: JavaScript code
                
            Returns:
                Execution result
            """
            # In real implementation, would execute with Node.js
            # For demonstration, we simulate execution
            
            return ExecutionResult(
                code=code,
                status=ExecutionStatus.SUCCESS,
                output="[Simulated] JavaScript execution would require Node.js runtime",
                duration=0.0
            )
    
    
    class CodeGenerationAgent:
        """
        Agent that generates and executes code to solve problems.
        """
        
        def __init__(
            self,
            model: str = "gpt-3.5-turbo",
            max_retries: int = 2
        ):
            """
            Initialize code generation agent.
            
            Args:
                model: LLM model to use
                max_retries: Maximum refinement attempts on errors
            """
            self.generator = CodeGenerator(model)
            self.executor = SandboxExecutor()
            self.max_retries = max_retries
            self.execution_history: List[ExecutionResult] = []
        
        def solve_with_code(
            self,
            task: str,
            language: Language = Language.PYTHON,
            context: Optional[str] = None
        ) -> Dict[str, Any]:
            """
            Solve a task by generating and executing code.
            
            Args:
                task: Task description
                language: Programming language to use
                context: Optional context
                
            Returns:
                Solution with code and results
            """
            print(f"\n[Agent] Solving task: {task}")
            print(f"[Agent] Language: {language.value}\n")
            
            # Generate initial code
            print("[Agent] Generating code...")
            if language == Language.PYTHON:
                snippet = self.generator.generate_python(task, context)
            else:
                snippet = self.generator.generate_javascript(task, context)
            
            print(f"\n[Agent] Generated code:\n")
            print("=" * 60)
            print(snippet.code)
            print("=" * 60)
            
            # Execute with retries
            attempts = []
            result = None
            
            for attempt in range(self.max_retries + 1):
                print(f"\n[Agent] Execution attempt {attempt + 1}...")
                
                # Execute code
                if language == Language.PYTHON:
                    result = self.executor.execute_python(snippet.code)
                else:
                    result = self.executor.execute_javascript(snippet.code)
                
                self.execution_history.append(result)
                attempts.append(result)
                
                # Check if successful
                if result.status == ExecutionStatus.SUCCESS:
                    print(f"[Agent] ✓ Success!")
                    print(f"\n[Agent] Output:")
                    print("-" * 60)
                    print(result.output)
                    print("-" * 60)
                    break
                
                # Handle error
                print(f"[Agent] ✗ {result.status.value}: {result.error}")
                
                if attempt < self.max_retries:
                    print(f"[Agent] Refining code...")
                    snippet.code = self.generator.refine_code(
                        snippet.code,
                        result.error,
                        language
                    )
                    print(f"\n[Agent] Refined code:\n")
                    print("=" * 60)
                    print(snippet.code)
                    print("=" * 60)
            
            return {
                "task": task,
                "language": language.value,
                "final_code": snippet.code,
                "result": result,
                "attempts": len(attempts),
                "success": result.status == ExecutionStatus.SUCCESS
            }
    
    
    def demonstrate_code_generation():
        """Demonstrate the Code Generation & Execution pattern."""
        
        print("=" * 80)
        print("CODE GENERATION & EXECUTION PATTERN DEMONSTRATION")
        print("=" * 80)
        
        agent = CodeGenerationAgent()
        
        # Test 1: Simple calculation
        print("\n" + "=" * 80)
        print("TEST 1: Mathematical Calculation")
        print("=" * 80)
        
        task1 = "Calculate the factorial of 10 and print the result"
        result1 = agent.solve_with_code(task1, Language.PYTHON)
        
        print("\n" + "=" * 80)
        print("SOLUTION SUMMARY")
        print("=" * 80)
        print(f"Success: {result1['success']}")
        print(f"Attempts: {result1['attempts']}")
        
        # Test 2: Data processing
        print("\n" + "=" * 80)
        print("TEST 2: Data Processing")
        print("=" * 80)
        
        task2 = """Create a list of numbers from 1 to 20, filter only even numbers,
    square each one, and print the sum of all squared even numbers"""
        result2 = agent.solve_with_code(task2, Language.PYTHON)
        
        print("\n" + "=" * 80)
        print("SOLUTION SUMMARY")
        print("=" * 80)
        print(f"Success: {result2['success']}")
        print(f"Attempts: {result2['attempts']}")
        
        # Test 3: String manipulation
        print("\n" + "=" * 80)
        print("TEST 3: String Manipulation")
        print("=" * 80)
        
        task3 = """Create a function that takes a string and returns a dictionary
    counting the frequency of each character. Test it with 'hello world' and print the result."""
        result3 = agent.solve_with_code(task3, Language.PYTHON)
        
        print("\n" + "=" * 80)
        print("SOLUTION SUMMARY")
        print("=" * 80)
        print(f"Success: {result3['success']}")
        print(f"Attempts: {result3['attempts']}")
        
        # Test 4: Algorithm implementation
        print("\n" + "=" * 80)
        print("TEST 4: Algorithm Implementation")
        print("=" * 80)
        
        task4 = """Implement the Fibonacci sequence up to the 15th number using iteration
    (not recursion) and print all numbers"""
        result4 = agent.solve_with_code(task4, Language.PYTHON)
        
        print("\n" + "=" * 80)
        print("SOLUTION SUMMARY")
        print("=" * 80)
        print(f"Success: {result4['success']}")
        print(f"Attempts: {result4['attempts']}")
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        
        successful = sum(1 for r in agent.execution_history if r.status == ExecutionStatus.SUCCESS)
        total = len(agent.execution_history)
        
        print(f"""
    Total tasks: 4
    Total execution attempts: {total}
    Successful executions: {successful}
    Success rate: {successful/total*100:.0f}%
    
    Execution Breakdown:
    """)
        
        status_counts = {}
        for result in agent.execution_history:
            status_counts[result.status.value] = status_counts.get(result.status.value, 0) + 1
        
        for status, count in status_counts.items():
            print(f"  {status}: {count}")
        
        print("""
    The Code Generation & Execution pattern demonstrates:
    
    1. **Natural Language to Code**: Converts descriptions to executable code
    2. **Safe Execution**: Sandboxed environment prevents harmful operations
    3. **Error Recovery**: Automatically refines code on failures
    4. **Multi-Language**: Supports multiple programming languages
    5. **Result Capture**: Captures and formats execution output
    
    Key Benefits:
    - **Programmatic Problem Solving**: Handles complex computations
    - **Safety**: Restricted execution environment
    - **Self-Correction**: Learns from execution errors
    - **Flexibility**: Works across languages and domains
    - **Verifiable**: Actual code execution provides concrete results
    
    Use Cases:
    - Data analysis and statistics
    - Mathematical problem solving
    - Algorithm implementation and testing
    - Text processing pipelines
    - Quick prototyping and experimentation
    
    Limitations:
    - Restricted operations for security
    - Limited library access in sandbox
    - Execution timeouts for safety
    - Language-specific constraints
    
    This pattern enables agents to leverage the full power of programming
    to solve computational problems that are difficult for pure LLMs.
    """)
    
    
    if __name__ == "__main__":
        demonstrate_code_generation()
    
        

    Agentic Design Pattern: Code Generation

    Pattern 022: Code Generation – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 022: Code Generation & Execution

    Description: The Code Generation & Execution pattern enables agents to generate executable code in response to user requests, safely execute the code in a sandboxed environment, and return results. This pattern combines LLM code generation capabilities with secure execution environments to solve computational problems programmatically.

    Components: – Code Generator: LLM generates code from natural language – Syntax Validator: Checks code for syntax errors – Sandbox Environment: Isolated execution context – Result Processor: Formats and returns execution results – Error Handler: Captures and explains execution errors

    Use Cases: – Data analysis and visualization – Mathematical problem solving – Text processing and transformation – Algorithm implementation – Quick prototyping and testing

    LangChain Implementation: Uses LLM for code generation with specific prompting strategies, implements safe execution with restricted environments, and handles multiple programming languages.

    Key Features: – Multi-language support (Python, JavaScript, etc.) – Sandboxed execution for security – Syntax and runtime error handling – Result capturing and formatting – Iterative refinement on errors


    File Statistics

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

    Module Documentation

    """
    Pattern 022: Code Generation & Execution
    
    Description:
        The Code Generation & Execution pattern enables agents to generate executable code
        in response to user requests, safely execute the code in a sandboxed environment,
        and return results. This pattern combines LLM code generation capabilities with
        secure execution environments to solve computational problems programmatically.
    
    Components:
        - Code Generator: LLM generates code from natural language
        - Syntax Validator: Checks code for syntax errors
        - Sandbox Environment: Isolated execution context
        - Result Processor: Formats and returns execution results
        - Error Handler: Captures and explains execution errors
    
    Use Cases:
        - Data analysis and visualization
        - Mathematical problem solving
        - Text processing and transformation
        - Algorithm implementation
        - Quick prototyping and testing
    
    LangChain Implementation:
        Uses LLM for code generation with specific prompting strategies,
        implements safe execution with restricted environments, and handles
        multiple programming languages.
    
    Key Features:
        - Multi-language support (Python, JavaScript, etc.)
        - Sandboxed execution for security
        - Syntax and runtime error handling
        - Result capturing and formatting
        - Iterative refinement on errors
    """
    

    Purpose: This module implements the Code Generation 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 sys
    import time
    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 – time: Time-related functions – typing.Any: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Dict: Type hints for better code documentation (List, Dict, Any, Optional) – typing.List: Type hints for better code documentation (List, Dict, Any, Optional) – typing.Optional: Type hints for better code documentation (List, Dict, Any, Optional)

    LangChain Imports

    from langchain_openai.ChatOpenAI
    

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

    Other Imports

    from contextlib
    from dotenv.load_dotenv
    from io
    

    Purpose: Additional dependencies for specialized functionality.


    Classes

    Class: Language

    Lines 54-57

    Inherits from: Enum

    Purpose:

    Supported programming languages.
    

    Class: ExecutionStatus

    Lines 60-66

    Inherits from: Enum

    Purpose:

    Status of code execution.
    

    Class: CodeSnippet

    Lines 70-75

    Purpose:

    Generated code snippet.
    

    Class: ExecutionResult

    Lines 79-86

    Purpose:

    Result of code execution.
    

    Class: CodeGenerator

    Lines 89-262

    Purpose:

    Generates code from natural language descriptions.
    

    Methods:

    __init__(self, model)

    Lines 94-101

    Purpose:

    Initialize code generator.
    
    Args:
        model: LLM model to use
    

    Parameters: – model: Language model instance

    generate_python(self, task, context)

    Lines 103-150

    Purpose:

    Generate Python code for a task.
    
    Args:
        task: Task description
        context: Optional context or constraints
    
    Returns:
        Generated code snippet
    

    Parameters: – task: Task – context: Context

    Returns: CodeSnippet

    generate_javascript(self, task, context)

    Lines 152-197

    Purpose:

    Generate JavaScript code for a task.
    
    Args:
        task: Task description
        context: Optional context
    
    Returns:
        Generated code snippet
    

    Parameters: – task: Task – context: Context

    Returns: CodeSnippet

    refine_code(self, original_code, error, language)

    Lines 199-240

    Purpose:

    Refine code based on error feedback.
    
    Args:
        original_code: Original code that failed
        error: Error message
        language: Programming language
    
    Returns:
        Refined code
    

    Parameters: – original_code: Original code – error: Error – language: Language

    Returns: str

    _extract_code(self, response)

    Lines 242-262

    Purpose:

    Extract code from LLM response.
    

    Parameters: – response: Response

    Returns: str


    Class: SandboxExecutor

    Lines 265-368

    Purpose:

    Safely executes code in sandboxed environment.
    

    Methods:

    __init__(self)

    Lines 270-284

    Purpose:

    Initialize sandbox executor.
    

    Parameters:

    execute_python(self, code)

    Lines 286-348

    Purpose:

    Execute Python code safely.
    
    Args:
        code: Python code to execute
    
    Returns:
        Execution result
    

    Parameters: – code: Code

    Returns: ExecutionResult

    execute_javascript(self, code)

    Lines 350-368

    Purpose:

    Execute JavaScript code (simulated - would need Node.js).
    
    Args:
        code: JavaScript code
    
    Returns:
        Execution result
    

    Parameters: – code: Code

    Returns: ExecutionResult


    Class: CodeGenerationAgent

    Lines 371-472

    Purpose:

    Agent that generates and executes code to solve problems.
    

    Methods:

    __init__(self, model, max_retries)

    Lines 376-391

    Purpose:

    Initialize code generation agent.
    
    Args:
        model: LLM model to use
        max_retries: Maximum refinement attempts on errors
    

    Parameters: – model: Language model instance – max_retries: Max retries

    solve_with_code(self, task, language, context)

    Lines 393-472

    Purpose:

    Solve a task by generating and executing code.
    
    Args:
        task: Task description
        language: Programming language to use
        context: Optional context
    
    Returns:
        Solution with code and results
    

    Parameters: – task: Task – language: Language – context: Context

    Returns: <ast.Subscript object at 0x000001A06D608890>


    Functions

    demonstrate_code_generation()

    Lines 475-598

    Purpose:

    Demonstrate the Code Generation & Execution pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Code Generation 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 021
    • Pattern 023
    • Other Tool Use Patterns (020-025)

    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 Code Generation 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: 022_code_generation.py
    Total Lines: 603
    Pattern Category: AI Agent Design Pattern #022


    Generated from actual implementation code
    For educational and reference purposes

  • Agentic Design Pattern: Function Calling

    
        """
    Pattern 021: Function Calling
    
    Description:
        The Function Calling pattern enables LLMs to invoke structured functions with
        type-safe parameters. Unlike general tool use, function calling emphasizes strong
        typing, structured schemas, and direct integration with code functions. The LLM
        analyzes user intent, selects appropriate functions, extracts typed parameters,
        and invokes functions with validated inputs.
    
    Components:
        - Function Schema: Structured definitions with parameter types
        - Parameter Validation: Type checking and constraint enforcement
        - Function Binding: Direct mapping to callable functions
        - Response Formatting: Structured outputs from function calls
        - Multi-turn Handling: Multiple function calls in sequence
    
    Use Cases:
        - Database queries with typed parameters
        - API calls with strict schemas
        - System operations requiring validation
        - Structured data processing
        - Multi-step function orchestration
    
    LangChain Implementation:
        Uses LangChain's function calling with OpenAI's function calling API,
        implements type-safe parameter extraction, and handles multi-turn conversations.
    
    Key Features:
        - Type-safe parameter extraction
        - JSON schema validation
        - Multiple function selection
        - Error handling and retries
        - Conversation context preservation
    """
    
    import os
    from typing import List, Dict, Any, Optional, Union, Callable
    from dataclasses import dataclass, field
    from enum import Enum
    import json
    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 ParameterType(Enum):
        """Types of function parameters."""
        STRING = "string"
        INTEGER = "integer"
        NUMBER = "number"
        BOOLEAN = "boolean"
        ARRAY = "array"
        OBJECT = "object"
    
    
    @dataclass
    class FunctionParameter:
        """Definition of a function parameter."""
        name: str
        type: ParameterType
        description: str
        required: bool = True
        enum: Optional[List[Any]] = None
        default: Optional[Any] = None
        
        def to_schema(self) -> Dict[str, Any]:
            """Convert to JSON schema format."""
            schema = {
                "type": self.type.value,
                "description": self.description
            }
            if self.enum:
                schema["enum"] = self.enum
            return schema
    
    
    @dataclass
    class FunctionDefinition:
        """Complete function definition with schema."""
        name: str
        description: str
        parameters: List[FunctionParameter]
        function: Callable
        returns: str = "object"
        
        def to_openai_schema(self) -> Dict[str, Any]:
            """Convert to OpenAI function calling schema."""
            required_params = [p.name for p in self.parameters if p.required]
            
            properties = {
                param.name: param.to_schema()
                for param in self.parameters
            }
            
            return {
                "name": self.name,
                "description": self.description,
                "parameters": {
                    "type": "object",
                    "properties": properties,
                    "required": required_params
                }
            }
        
        def validate_parameters(self, params: Dict[str, Any]) -> tuple[bool, Optional[str]]:
            """Validate parameters against schema."""
            # Check required parameters
            for param in self.parameters:
                if param.required and param.name not in params:
                    return False, f"Missing required parameter: {param.name}"
            
            # Check types (basic validation)
            for param in self.parameters:
                if param.name in params:
                    value = params[param.name]
                    
                    # Type checking
                    if param.type == ParameterType.STRING and not isinstance(value, str):
                        return False, f"Parameter {param.name} must be string"
                    elif param.type == ParameterType.INTEGER and not isinstance(value, int):
                        return False, f"Parameter {param.name} must be integer"
                    elif param.type == ParameterType.NUMBER and not isinstance(value, (int, float)):
                        return False, f"Parameter {param.name} must be number"
                    elif param.type == ParameterType.BOOLEAN and not isinstance(value, bool):
                        return False, f"Parameter {param.name} must be boolean"
                    elif param.type == ParameterType.ARRAY and not isinstance(value, list):
                        return False, f"Parameter {param.name} must be array"
                    
                    # Enum validation
                    if param.enum and value not in param.enum:
                        return False, f"Parameter {param.name} must be one of {param.enum}"
            
            return True, None
    
    
    @dataclass
    class FunctionCall:
        """Record of a function call."""
        function_name: str
        arguments: Dict[str, Any]
        result: Any
        success: bool
        timestamp: datetime = field(default_factory=datetime.now)
        error: Optional[str] = None
    
    
    class FunctionRegistry:
        """
        Registry of callable functions with schemas.
        """
        
        def __init__(self):
            """Initialize function registry."""
            self.functions: Dict[str, FunctionDefinition] = {}
            self._register_builtin_functions()
        
        def _register_builtin_functions(self):
            """Register built-in functions."""
            
            # Get user info function
            self.register_function(FunctionDefinition(
                name="get_user_info",
                description="Retrieves information about a user by their ID",
                parameters=[
                    FunctionParameter("user_id", ParameterType.INTEGER, "The user's ID"),
                    FunctionParameter("fields", ParameterType.ARRAY, "Fields to retrieve", required=False)
                ],
                function=self._get_user_info
            ))
            
            # Create order function
            self.register_function(FunctionDefinition(
                name="create_order",
                description="Creates a new order in the system",
                parameters=[
                    FunctionParameter("product_id", ParameterType.STRING, "Product identifier"),
                    FunctionParameter("quantity", ParameterType.INTEGER, "Number of items"),
                    FunctionParameter("priority", ParameterType.STRING, "Order priority", 
                                    enum=["low", "normal", "high", "urgent"])
                ],
                function=self._create_order
            ))
            
            # Search database function
            self.register_function(FunctionDefinition(
                name="search_database",
                description="Searches the database with filters",
                parameters=[
                    FunctionParameter("table", ParameterType.STRING, "Table name"),
                    FunctionParameter("query", ParameterType.STRING, "Search query"),
                    FunctionParameter("limit", ParameterType.INTEGER, "Max results", required=False, default=10)
                ],
                function=self._search_database
            ))
            
            # Send notification function
            self.register_function(FunctionDefinition(
                name="send_notification",
                description="Sends a notification to a user",
                parameters=[
                    FunctionParameter("user_id", ParameterType.INTEGER, "Recipient user ID"),
                    FunctionParameter("message", ParameterType.STRING, "Notification message"),
                    FunctionParameter("urgent", ParameterType.BOOLEAN, "Is urgent", required=False, default=False)
                ],
                function=self._send_notification
            ))
            
            # Calculate price function
            self.register_function(FunctionDefinition(
                name="calculate_price",
                description="Calculates total price with tax and discount",
                parameters=[
                    FunctionParameter("base_price", ParameterType.NUMBER, "Base price"),
                    FunctionParameter("tax_rate", ParameterType.NUMBER, "Tax rate (0-1)", required=False, default=0.1),
                    FunctionParameter("discount", ParameterType.NUMBER, "Discount amount", required=False, default=0)
                ],
                function=self._calculate_price
            ))
        
        def register_function(self, function_def: FunctionDefinition):
            """Register a new function."""
            self.functions[function_def.name] = function_def
        
        def get_function(self, name: str) -> Optional[FunctionDefinition]:
            """Get function by name."""
            return self.functions.get(name)
        
        def get_all_schemas(self) -> List[Dict[str, Any]]:
            """Get all function schemas in OpenAI format."""
            return [func.to_openai_schema() for func in self.functions.values()]
        
        # Built-in function implementations (mock)
        
        def _get_user_info(self, user_id: int, fields: Optional[List[str]] = None) -> Dict[str, Any]:
            """Mock user info retrieval."""
            users = {
                1: {"name": "Alice", "email": "alice@example.com", "age": 30, "status": "active"},
                2: {"name": "Bob", "email": "bob@example.com", "age": 25, "status": "active"},
                3: {"name": "Charlie", "email": "charlie@example.com", "age": 35, "status": "inactive"}
            }
            
            user = users.get(user_id, {"error": "User not found"})
            
            if fields and "error" not in user:
                user = {k: v for k, v in user.items() if k in fields}
            
            return user
        
        def _create_order(self, product_id: str, quantity: int, priority: str) -> Dict[str, Any]:
            """Mock order creation."""
            import random
            return {
                "order_id": random.randint(1000, 9999),
                "product_id": product_id,
                "quantity": quantity,
                "priority": priority,
                "status": "pending",
                "created_at": datetime.now().isoformat()
            }
        
        def _search_database(self, table: str, query: str, limit: int = 10) -> Dict[str, Any]:
            """Mock database search."""
            mock_results = {
                "products": [
                    {"id": "P001", "name": "Laptop", "price": 999.99},
                    {"id": "P002", "name": "Mouse", "price": 29.99}
                ],
                "users": [
                    {"id": 1, "name": "Alice"},
                    {"id": 2, "name": "Bob"}
                ]
            }
            
            results = mock_results.get(table, [])
            return {
                "table": table,
                "query": query,
                "results": results[:limit],
                "total": len(results)
            }
        
        def _send_notification(self, user_id: int, message: str, urgent: bool = False) -> Dict[str, Any]:
            """Mock notification sending."""
            return {
                "notification_id": f"N{user_id}{datetime.now().microsecond}",
                "user_id": user_id,
                "message": message,
                "urgent": urgent,
                "sent": True,
                "timestamp": datetime.now().isoformat()
            }
        
        def _calculate_price(self, base_price: float, tax_rate: float = 0.1, discount: float = 0) -> Dict[str, Any]:
            """Calculate total price."""
            tax = base_price * tax_rate
            total = base_price + tax - discount
            
            return {
                "base_price": base_price,
                "tax": tax,
                "discount": discount,
                "total": total
            }
    
    
    class FunctionCallingAgent:
        """
        Agent that uses function calling to accomplish tasks.
        """
        
        def __init__(
            self,
            function_registry: FunctionRegistry,
            model: str = "gpt-3.5-turbo"
        ):
            """
            Initialize function calling agent.
            
            Args:
                function_registry: Registry of available functions
                model: LLM model to use
            """
            self.function_registry = function_registry
            self.llm = ChatOpenAI(model=model, temperature=0.1)
            self.call_history: List[FunctionCall] = []
        
        def select_and_extract(
            self,
            user_request: str,
            context: Optional[str] = None
        ) -> List[Dict[str, Any]]:
            """
            Select functions and extract parameters from user request.
            
            Args:
                user_request: User's request
                context: Optional context from previous turns
                
            Returns:
                List of function calls to make
            """
            # Get all function schemas
            schemas = self.function_registry.get_all_schemas()
            schemas_text = json.dumps(schemas, indent=2)
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", """You are a function calling expert. Analyze user requests and
    determine which functions to call with what parameters.
    
    Available functions:
    {schemas}
    
    Respond with a JSON array of function calls. Each call should have:
    - "function": function name
    - "arguments": object with parameter values
    
    If multiple functions are needed, include them all in the array.
    If no function is needed, respond with an empty array []."""),
                ("user", "{request}")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            full_request = user_request
            if context:
                full_request = f"Context: {context}\n\nRequest: {user_request}"
            
            response = chain.invoke({
                "schemas": schemas_text,
                "request": full_request
            })
            
            # Parse function calls
            try:
                json_start = response.find("[")
                json_end = response.rfind("]") + 1
                if json_start != -1 and json_end > json_start:
                    json_str = response[json_start:json_end]
                    function_calls = json.loads(json_str)
                    return function_calls if isinstance(function_calls, list) else []
                else:
                    return []
            except Exception as e:
                print(f"Error parsing function calls: {e}")
                return []
        
        def execute_function(
            self,
            function_name: str,
            arguments: Dict[str, Any]
        ) -> FunctionCall:
            """
            Execute a function with given arguments.
            
            Args:
                function_name: Name of function to call
                arguments: Arguments for the function
                
            Returns:
                FunctionCall record
            """
            func_def = self.function_registry.get_function(function_name)
            
            if func_def is None:
                call = FunctionCall(
                    function_name=function_name,
                    arguments=arguments,
                    result=None,
                    success=False,
                    error=f"Function '{function_name}' not found"
                )
                self.call_history.append(call)
                return call
            
            # Validate parameters
            valid, error = func_def.validate_parameters(arguments)
            if not valid:
                call = FunctionCall(
                    function_name=function_name,
                    arguments=arguments,
                    result=None,
                    success=False,
                    error=error
                )
                self.call_history.append(call)
                return call
            
            # Execute function
            try:
                result = func_def.function(**arguments)
                call = FunctionCall(
                    function_name=function_name,
                    arguments=arguments,
                    result=result,
                    success=True
                )
            except Exception as e:
                call = FunctionCall(
                    function_name=function_name,
                    arguments=arguments,
                    result=None,
                    success=False,
                    error=str(e)
                )
            
            self.call_history.append(call)
            return call
        
        def process_request(
            self,
            user_request: str,
            context: Optional[str] = None
        ) -> Dict[str, Any]:
            """
            Process user request with function calling.
            
            Args:
                user_request: User's request
                context: Optional context
                
            Returns:
                Results including function calls and response
            """
            print(f"\n[Agent] Processing request: {user_request}\n")
            
            # Select and extract function calls
            print("[Agent] Analyzing request and selecting functions...")
            function_calls = self.select_and_extract(user_request, context)
            
            if not function_calls:
                print("[Agent] No function calls needed\n")
                return {
                    "request": user_request,
                    "function_calls": [],
                    "response": "No functions needed for this request"
                }
            
            print(f"[Agent] Planning {len(function_calls)} function call(s)\n")
            
            # Execute function calls
            results = []
            for fc in function_calls:
                function_name = fc.get("function", "")
                arguments = fc.get("arguments", {})
                
                print(f"[Agent] Calling: {function_name}")
                print(f"[Agent] Arguments: {json.dumps(arguments, indent=2)}")
                
                call = self.execute_function(function_name, arguments)
                results.append(call)
                
                if call.success:
                    print(f"[Agent] Success: {json.dumps(call.result, indent=2)}\n")
                else:
                    print(f"[Agent] Error: {call.error}\n")
            
            # Generate final response
            response = self._generate_response(user_request, results)
            
            return {
                "request": user_request,
                "function_calls": results,
                "response": response
            }
        
        def _generate_response(
            self,
            user_request: str,
            function_calls: List[FunctionCall]
        ) -> str:
            """Generate natural language response from function results."""
            results_text = "\n".join([
                f"Function: {call.function_name}\n"
                f"Result: {json.dumps(call.result, indent=2) if call.success else f'Error: {call.error}'}"
                for call in function_calls
            ])
            
            prompt = ChatPromptTemplate.from_messages([
                ("system", "You generate natural responses from function call results."),
                ("user", """User Request: {request}
    
    Function Results:
    {results}
    
    Provide a natural, helpful response incorporating these results.""")
            ])
            
            chain = prompt | self.llm | StrOutputParser()
            
            response = chain.invoke({
                "request": user_request,
                "results": results_text
            })
            
            return response.strip()
    
    
    def demonstrate_function_calling():
        """Demonstrate the Function Calling pattern."""
        
        print("=" * 80)
        print("FUNCTION CALLING PATTERN DEMONSTRATION")
        print("=" * 80)
        
        # Initialize registry and agent
        registry = FunctionRegistry()
        agent = FunctionCallingAgent(registry)
        
        # Show available functions
        print("\n" + "=" * 80)
        print("AVAILABLE FUNCTIONS")
        print("=" * 80)
        schemas = registry.get_all_schemas()
        for schema in schemas:
            print(f"\n{schema['name']}:")
            print(f"  Description: {schema['description']}")
            print(f"  Parameters: {list(schema['parameters']['properties'].keys())}")
        
        # Test 1: Single function call
        print("\n" + "=" * 80)
        print("TEST 1: Single Function Call")
        print("=" * 80)
        
        request1 = "Get information about user ID 1"
        result1 = agent.process_request(request1)
        
        print("=" * 80)
        print("FINAL RESPONSE:")
        print("=" * 80)
        print(result1["response"])
        
        # Test 2: Multiple function calls
        print("\n" + "=" * 80)
        print("TEST 2: Multiple Function Calls")
        print("=" * 80)
        
        request2 = "Create an order for product P001 with quantity 5 and high priority, then send an urgent notification to user 1 about the order"
        result2 = agent.process_request(request2)
        
        print("=" * 80)
        print("FINAL RESPONSE:")
        print("=" * 80)
        print(result2["response"])
        
        # Test 3: Function with optional parameters
        print("\n" + "=" * 80)
        print("TEST 3: Optional Parameters")
        print("=" * 80)
        
        request3 = "Calculate the price for a $100 item with 15% tax and a $10 discount"
        result3 = agent.process_request(request3)
        
        print("=" * 80)
        print("FINAL RESPONSE:")
        print("=" * 80)
        print(result3["response"])
        
        # Test 4: Database search
        print("\n" + "=" * 80)
        print("TEST 4: Database Query")
        print("=" * 80)
        
        request4 = "Search the products table for laptops, limit to 5 results"
        result4 = agent.process_request(request4)
        
        print("=" * 80)
        print("FINAL RESPONSE:")
        print("=" * 80)
        print(result4["response"])
        
        # Summary
        print("\n" + "=" * 80)
        print("SUMMARY")
        print("=" * 80)
        print(f"""
    Total requests processed: 4
    Total function calls made: {len(agent.call_history)}
    Success rate: {sum(1 for call in agent.call_history if call.success) / len(agent.call_history) * 100:.0f}%
    
    Function Call Breakdown:
    """)
        
        for call in agent.call_history:
            status = "✓" if call.success else "✗"
            print(f"  {status} {call.function_name}({list(call.arguments.keys())})")
        
        print("""
    The Function Calling pattern demonstrates:
    
    1. **Type-Safe Invocation**: Strong parameter typing and validation
    2. **Schema-Driven**: JSON schemas define function contracts
    3. **Multi-Function**: Can orchestrate multiple function calls
    4. **Parameter Extraction**: LLM extracts typed parameters from text
    5. **Error Handling**: Validates inputs before execution
    
    Key Benefits:
    - **Type Safety**: Prevents invalid function calls
    - **Clear Contracts**: Well-defined function interfaces
    - **Composable**: Multiple functions work together
    - **Reliable**: Validation before execution
    - **Structured**: Predictable input/output formats
    
    Use Cases:
    - Database operations with typed queries
    - API integrations with strict schemas
    - System commands requiring validation
    - Multi-step workflows with dependencies
    - Enterprise integrations
    
    This pattern is essential for production agentic systems that need
    reliable, type-safe interactions with external systems and APIs.
    """)
    
    
    if __name__ == "__main__":
        demonstrate_function_calling()
    
        

    Agentic Design Pattern: Function Calling

    Pattern 021: Function Calling – Line-by-Line Explanation

    Overview

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


    Pattern Description

    Pattern 021: Function Calling

    Description: The Function Calling pattern enables LLMs to invoke structured functions with type-safe parameters. Unlike general tool use, function calling emphasizes strong typing, structured schemas, and direct integration with code functions. The LLM analyzes user intent, selects appropriate functions, extracts typed parameters, and invokes functions with validated inputs.

    Components: – Function Schema: Structured definitions with parameter types – Parameter Validation: Type checking and constraint enforcement – Function Binding: Direct mapping to callable functions – Response Formatting: Structured outputs from function calls – Multi-turn Handling: Multiple function calls in sequence

    Use Cases: – Database queries with typed parameters – API calls with strict schemas – System operations requiring validation – Structured data processing – Multi-step function orchestration

    LangChain Implementation: Uses LangChain’s function calling with OpenAI’s function calling API, implements type-safe parameter extraction, and handles multi-turn conversations.

    Key Features: – Type-safe parameter extraction – JSON schema validation – Multiple function selection – Error handling and retries – Conversation context preservation


    File Statistics

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

    Module Documentation

    """
    Pattern 021: Function Calling
    
    Description:
        The Function Calling pattern enables LLMs to invoke structured functions with
        type-safe parameters. Unlike general tool use, function calling emphasizes strong
        typing, structured schemas, and direct integration with code functions. The LLM
        analyzes user intent, selects appropriate functions, extracts typed parameters,
        and invokes functions with validated inputs.
    
    Components:
        - Function Schema: Structured definitions with parameter types
        - Parameter Validation: Type checking and constraint enforcement
        - Function Binding: Direct mapping to callable functions
        - Response Formatting: Structured outputs from function calls
        - Multi-turn Handling: Multiple function calls in sequence
    
    Use Cases:
        - Database queries with typed parameters
        - API calls with strict schemas
        - System operations requiring validation
        - Structured data processing
        - Multi-step function orchestration
    
    LangChain Implementation:
        Uses LangChain's function calling with OpenAI's function calling API,
        implements type-safe parameter extraction, and handles multi-turn conversations.
    
    Key Features:
        - Type-safe parameter extraction
        - JSON schema validation
        - Multiple function selection
        - Error handling and retries
        - Conversation context preservation
    """
    

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


    Import Statements

    Standard Library Imports

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

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

    Lines 52-59

    Inherits from: Enum

    Purpose:

    Types of function parameters.
    

    Class: FunctionParameter

    Lines 63-80

    Purpose:

    Definition of a function parameter.
    

    Methods:

    to_schema(self)

    Lines 72-80

    Purpose:

    Convert to JSON schema format.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D622310>


    Class: FunctionDefinition

    Lines 84-139

    Purpose:

    Complete function definition with schema.
    

    Methods:

    to_openai_schema(self)

    Lines 92-109

    Purpose:

    Convert to OpenAI function calling schema.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D70EF90>

    validate_parameters(self, params)

    Lines 111-139

    Purpose:

    Validate parameters against schema.
    

    Parameters: – params: Params

    Returns: <ast.Subscript object at 0x000001A06D6EA090>


    Class: FunctionCall

    Lines 143-150

    Purpose:

    Record of a function call.
    

    Class: FunctionRegistry

    Lines 153-309

    Purpose:

    Registry of callable functions with schemas.
    

    Methods:

    __init__(self)

    Lines 158-161

    Purpose:

    Initialize function registry.
    

    Parameters:

    _register_builtin_functions(self)

    Lines 163-224

    Purpose:

    Register built-in functions.
    

    Parameters:

    register_function(self, function_def)

    Lines 226-228

    Purpose:

    Register a new function.
    

    Parameters: – function_def: Function def

    get_function(self, name)

    Lines 230-232

    Purpose:

    Get function by name.
    

    Parameters: – name: Name

    Returns: <ast.Subscript object at 0x000001A06D15A090>

    get_all_schemas(self)

    Lines 234-236

    Purpose:

    Get all function schemas in OpenAI format.
    

    Parameters:

    Returns: <ast.Subscript object at 0x000001A06D1588D0>

    _get_user_info(self, user_id, fields)

    Lines 240-253

    Purpose:

    Mock user info retrieval.
    

    Parameters: – user_id: User id – fields: Fields

    Returns: <ast.Subscript object at 0x000001A06D701B90>

    _create_order(self, product_id, quantity, priority)

    Lines 255-265

    Purpose:

    Mock order creation.
    

    Parameters: – product_id: Product id – quantity: Quantity – priority: Priority

    Returns: <ast.Subscript object at 0x000001A06D6F7810>

    _search_database(self, table, query, limit)

    Lines 267-286

    Purpose:

    Mock database search.
    

    Parameters: – table: Table – query: Input query or question – limit: Limit

    Returns: <ast.Subscript object at 0x000001A06D15FC50>

    _send_notification(self, user_id, message, urgent)

    Lines 288-297

    Purpose:

    Mock notification sending.
    

    Parameters: – user_id: User id – message: Message – urgent: Urgent

    Returns: <ast.Subscript object at 0x000001A06D15E950>

    _calculate_price(self, base_price, tax_rate, discount)

    Lines 299-309

    Purpose:

    Calculate total price.
    

    Parameters: – base_price: Base price – tax_rate: Tax rate – discount: Discount

    Returns: <ast.Subscript object at 0x000001A06D676D10>


    Class: FunctionCallingAgent

    Lines 312-541

    Purpose:

    Agent that uses function calling to accomplish tasks.
    

    Methods:

    __init__(self, function_registry, model)

    Lines 317-331

    Purpose:

    Initialize function calling agent.
    
    Args:
        function_registry: Registry of available functions
        model: LLM model to use
    

    Parameters: – function_registry: Function registry – model: Language model instance

    select_and_extract(self, user_request, context)

    Lines 333-391

    Purpose:

    Select functions and extract parameters from user request.
    
    Args:
        user_request: User's request
        context: Optional context from previous turns
    
    Returns:
        List of function calls to make
    

    Parameters: – user_request: User request – context: Context

    Returns: <ast.Subscript object at 0x000001A06D73B6D0>

    execute_function(self, function_name, arguments)

    Lines 393-453

    Purpose:

    Execute a function with given arguments.
    
    Args:
        function_name: Name of function to call
        arguments: Arguments for the function
    
    Returns:
        FunctionCall record
    

    Parameters: – function_name: Function name – arguments: Arguments

    Returns: FunctionCall

    process_request(self, user_request, context)

    Lines 455-510

    Purpose:

    Process user request with function calling.
    
    Args:
        user_request: User's request
        context: Optional context
    
    Returns:
        Results including function calls and response
    

    Parameters: – user_request: User request – context: Context

    Returns: <ast.Subscript object at 0x000001A06D178AD0>

    _generate_response(self, user_request, function_calls)

    Lines 512-541

    Purpose:

    Generate natural language response from function results.
    

    Parameters: – user_request: User request – function_calls: Function calls

    Returns: str


    Functions

    demonstrate_function_calling()

    Lines 544-658

    Purpose:

    Demonstrate the Function Calling pattern.
    

    Key Concepts Summary

    Pattern Overview

    The Function Calling 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 020
    • Pattern 022
    • Other Tool Use Patterns (020-025)

    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 Function Calling 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: 021_function_calling.py
    Total Lines: 663
    Pattern Category: AI Agent Design Pattern #021


    Generated from actual implementation code
    For educational and reference purposes