Agentic Architecture Pattern: Online Learning

Written by

in


    """
Pattern 55: Online Learning

Purpose: Learn continuously from streaming data in real-time.
Updates model incrementally as new data arrives, adapting to changing patterns.
"""

from typing import Dict, Any, List, Optional, Deque
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from collections import deque
import random
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI


class UpdateStrategy(Enum):
    """Strategies for online model updates."""
    IMMEDIATE = "immediate"  # Update after each example
    MINI_BATCH = "mini_batch"  # Update after small batches
    SLIDING_WINDOW = "sliding_window"  # Maintain recent examples only
    ADAPTIVE = "adaptive"  # Adjust based on performance


@dataclass
class StreamingExample:
    """A single streaming data example."""
    id: str
    features: Dict[str, Any]
    label: Optional[str] = None
    timestamp: datetime = field(default_factory=datetime.now)
    weight: float = 1.0
    metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass
class ModelSnapshot:
    """Snapshot of model state at a point in time."""
    timestamp: datetime
    examples_seen: int
    accuracy: float
    parameters: Dict[str, Any]
    metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass
class PerformanceMetrics:
    """Real-time performance metrics."""
    accuracy: float
    recent_accuracy: float  # Last N examples
    examples_processed: int
    updates_performed: int
    avg_confidence: float
    timestamp: datetime = field(default_factory=datetime.now)


class OnlineLearningAgent:
    """
    Online Learning Agent - Learn continuously from streaming data.
    
    Adapts in real-time as new examples arrive, maintaining performance
    on non-stationary data streams. Uses LLM for pattern recognition.
    """
    
    def __init__(self, 
                 name: str,
                 task_description: str,
                 strategy: UpdateStrategy = UpdateStrategy.IMMEDIATE,
                 window_size: int = 100,
                 batch_size: int = 10):
        self.name = name
        self.task_description = task_description
        self.strategy = strategy
        self.window_size = window_size
        self.batch_size = batch_size
        self.llm = ChatOpenAI(model="gpt-4", temperature=0.3)
        
        # Streaming data buffer
        self.data_buffer: Deque[StreamingExample] = deque(maxlen=window_size)
        self.batch_buffer: List[StreamingExample] = []
        
        # Model state
        self.learned_patterns: Dict[str, Any] = {}
        self.examples_seen: int = 0
        self.updates_performed: int = 0
        
        # Performance tracking
        self.predictions: List[Dict[str, Any]] = []
        self.snapshots: List[ModelSnapshot] = []
        
        # Statistics
        self.statistics: Dict[str, Any] = {
            "correct_predictions": 0,
            "total_predictions": 0,
            "recent_correct": 0,
            "recent_total": 0
        }
    
    def process_stream(self, example: StreamingExample):
        """Process a single streaming example."""
        self.examples_seen += 1
        
        # Add to buffer
        if self.strategy == UpdateStrategy.SLIDING_WINDOW:
            self.data_buffer.append(example)
        else:
            self.data_buffer.append(example)
            self.batch_buffer.append(example)
        
        # Decide when to update
        should_update = False
        
        if self.strategy == UpdateStrategy.IMMEDIATE:
            should_update = True
        elif self.strategy == UpdateStrategy.MINI_BATCH:
            should_update = len(self.batch_buffer) >= self.batch_size
        elif self.strategy == UpdateStrategy.ADAPTIVE:
            # Update when performance drops
            recent_accuracy = self._calculate_recent_accuracy()
            should_update = recent_accuracy = self.batch_size
        
        # Perform update
        if should_update:
            self._update_model()
            if self.strategy != UpdateStrategy.SLIDING_WINDOW:
                self.batch_buffer.clear()
        
        print(f"   {self.name}: Processed example {example.id} (total: {self.examples_seen})")
    
    def _update_model(self):
        """Update model with current buffer."""
        if not self.data_buffer:
            return
        
        self.updates_performed += 1
        
        # Get recent examples for context
        recent_examples = list(self.data_buffer)[-min(20, len(self.data_buffer)):]
        
        # Extract patterns using LLM
        patterns = self._extract_patterns(recent_examples)
        
        # Update learned patterns
        for pattern_id, pattern_data in patterns.items():
            if pattern_id in self.learned_patterns:
                # Merge with existing pattern
                self.learned_patterns[pattern_id]["confidence"] = (
                    0.7 * self.learned_patterns[pattern_id]["confidence"] + 
                    0.3 * pattern_data["confidence"]
                )
                self.learned_patterns[pattern_id]["occurrences"] += 1
            else:
                self.learned_patterns[pattern_id] = pattern_data
                self.learned_patterns[pattern_id]["occurrences"] = 1
        
        # Take snapshot periodically
        if self.updates_performed % 10 == 0:
            self._take_snapshot()
        
        print(f"   {self.name}: Model updated (update #{self.updates_performed}, "
              f"{len(self.learned_patterns)} patterns)")
    
    def _extract_patterns(self, examples: List[StreamingExample]) -> Dict[str, Any]:
        """Extract patterns from examples using LLM."""
        if not examples:
            return {}
        
        # Prepare examples summary
        examples_text = ""
        for ex in examples[:5]:
            label_text = f"→ {ex.label}" if ex.label else "→ ?"
            examples_text += f"  - {ex.features} {label_text}\n"
        
        prompt = ChatPromptTemplate.from_messages([
            ("system", "You are analyzing streaming data patterns. "
                      "Identify key patterns that could help with prediction."),
            ("human", f"Task: {self.task_description}\n\n"
                     f"Recent examples ({len(examples)} total):\n{examples_text}\n"
                     f"Identify 2-3 key patterns. Format each as: PATTERN_ID|DESCRIPTION|CONFIDENCE\n"
                     f"Example: pattern_1|High feature X correlates with label A|0.8")
        ])
        
        chain = prompt | self.llm
        patterns = {}
        
        try:
            response = chain.invoke({})
            content = response.content if isinstance(response.content, str) else str(response.content)
            lines = content.strip().split('\n')
            
            for line in lines[:3]:
                try:
                    parts = line.split('|')
                    if len(parts) >= 3:
                        pattern_id = parts[0].strip()
                        description = parts[1].strip()
                        confidence = float(parts[2].strip())
                        
                        patterns[pattern_id] = {
                            "description": description,
                            "confidence": confidence
                        }
                except:
                    continue
        except:
            pass
        
        # Add simple statistical patterns
        if examples and examples[0].label:
            # Label distribution
            label_counts = {}
            for ex in examples:
                if ex.label:
                    label_counts[ex.label] = label_counts.get(ex.label, 0) + 1
            
            if label_counts:
                most_common = max(label_counts.items(), key=lambda x: x[1])
                patterns["label_distribution"] = {
                    "description": f"Most common label: {most_common[0]}",
                    "confidence": most_common[1] / len(examples)
                }
        
        return patterns
    
    def predict(self, example: StreamingExample) -> Dict[str, Any]:
        """Make prediction on streaming example."""
        # Create prompt with learned patterns
        patterns_text = ""
        for pattern_id, pattern_data in list(self.learned_patterns.items())[:5]:
            patterns_text += f"  - {pattern_data['description']} (confidence: {pattern_data['confidence']:.2f})\n"
        
        if not patterns_text:
            patterns_text = "  - No patterns learned yet\n"
        
        prompt = ChatPromptTemplate.from_messages([
            ("system", f"You are performing {self.task_description}. "
                      f"Use the learned patterns to make predictions."),
            ("human", f"Learned patterns:\n{patterns_text}\n"
                     f"New example: {example.features}\n\n"
                     f"Predict the label and confidence (0.0-1.0).\n"
                     f"Format: LABEL|CONFIDENCE")
        ])
        
        chain = prompt | self.llm
        
        try:
            response = chain.invoke({})
            content = response.content if isinstance(response.content, str) else str(response.content)
            parts = content.strip().split('|')
            
            predicted_label = parts[0].strip()
            confidence = float(parts[1].strip()) if len(parts) > 1 else 0.5
        except:
            # Fallback: use most common pattern
            if self.learned_patterns and "label_distribution" in self.learned_patterns:
                predicted_label = self.learned_patterns["label_distribution"]["description"].split(": ")[1]
                confidence = 0.5
            else:
                predicted_label = "unknown"
                confidence = 0.3
        
        # Track prediction
        is_correct = example.label and predicted_label == example.label
        
        prediction = {
            "example_id": example.id,
            "predicted_label": predicted_label,
            "actual_label": example.label,
            "confidence": confidence,
            "correct": is_correct,
            "timestamp": datetime.now()
        }
        
        self.predictions.append(prediction)
        
        # Update statistics
        self.statistics["total_predictions"] += 1
        self.statistics["recent_total"] += 1
        
        if is_correct:
            self.statistics["correct_predictions"] += 1
            self.statistics["recent_correct"] += 1
        
        # Keep only recent stats (last 50)
        if self.statistics["recent_total"] > 50:
            self.statistics["recent_total"] = 50
            self.statistics["recent_correct"] = sum(
                1 for p in self.predictions[-50:] if p["correct"]
            )
        
        return prediction
    
    def _calculate_recent_accuracy(self) -> float:
        """Calculate accuracy on recent predictions."""
        if self.statistics["recent_total"] == 0:
            return 0.0
        return self.statistics["recent_correct"] / self.statistics["recent_total"]
    
    def _take_snapshot(self):
        """Take snapshot of current model state."""
        accuracy = (self.statistics["correct_predictions"] / 
                   self.statistics["total_predictions"] 
                   if self.statistics["total_predictions"] > 0 else 0.0)
        
        snapshot = ModelSnapshot(
            timestamp=datetime.now(),
            examples_seen=self.examples_seen,
            accuracy=accuracy,
            parameters={
                "num_patterns": len(self.learned_patterns),
                "updates": self.updates_performed
            }
        )
        self.snapshots.append(snapshot)
    
    def get_performance_metrics(self) -> PerformanceMetrics:
        """Get current performance metrics."""
        accuracy = (self.statistics["correct_predictions"] / 
                   self.statistics["total_predictions"] 
                   if self.statistics["total_predictions"] > 0 else 0.0)
        
        recent_accuracy = self._calculate_recent_accuracy()
        
        avg_confidence = (sum(p["confidence"] for p in self.predictions[-50:]) / 
                         min(50, len(self.predictions))
                         if self.predictions else 0.0)
        
        return PerformanceMetrics(
            accuracy=accuracy,
            recent_accuracy=recent_accuracy,
            examples_processed=self.examples_seen,
            updates_performed=self.updates_performed,
            avg_confidence=avg_confidence
        )
    
    def get_statistics(self) -> Dict[str, Any]:
        """Get comprehensive statistics."""
        metrics = self.get_performance_metrics()
        
        return {
            "name": self.name,
            "strategy": self.strategy.value,
            "examples_seen": self.examples_seen,
            "updates_performed": self.updates_performed,
            "accuracy": metrics.accuracy,
            "recent_accuracy": metrics.recent_accuracy,
            "patterns_learned": len(self.learned_patterns),
            "snapshots": len(self.snapshots)
        }


class OnlineLearningPattern:
    """
    Online Learning Pattern - Continuous learning from data streams.
    
    Coordinates multiple agents learning from streaming data with
    different update strategies.
    """
    
    def __init__(self):
        self.agents: Dict[str, OnlineLearningAgent] = {}
        self.stream_history: List[StreamingExample] = []
    
    def register_agent(self, agent: OnlineLearningAgent):
        """Register online learning agent."""
        self.agents[agent.name] = agent
        print(f"   Registered agent: {agent.name} ({agent.strategy.value})")
    
    def simulate_stream(self, 
                       examples: List[StreamingExample],
                       test_interval: int = 10):
        """Simulate streaming data with online learning."""
        print("\n   Simulating data stream...")
        
        for i, example in enumerate(examples):
            self.stream_history.append(example)
            
            # Each agent processes the stream
            for agent in self.agents.values():
                agent.process_stream(example)
                
                # Test prediction every N examples
                if (i + 1) % test_interval == 0:
                    prediction = agent.predict(example)
            
            # Progress update
            if (i + 1) % 20 == 0:
                print(f"   Processed {i + 1}/{len(examples)} examples...")
    
    def run(self, examples: List[StreamingExample]) -> Dict[str, Any]:
        """Run online learning simulation."""
        print("=== Online Learning Pattern Demo ===\n")
        
        print("1. Simulating data stream...")
        self.simulate_stream(examples)
        
        print("\n2. Collecting performance metrics...")
        metrics = {name: agent.get_performance_metrics() 
                  for name, agent in self.agents.items()}
        
        stats = {name: agent.get_statistics() 
                for name, agent in self.agents.items()}
        
        return {
            "pattern": "OnlineLearning",
            "agents": len(self.agents),
            "stream_length": len(examples),
            "metrics": metrics,
            "statistics": stats
        }


# Example usage
if __name__ == "__main__":
    print("=== Online Learning Pattern Demo ===\n")
    
    # Create pattern
    pattern = OnlineLearningPattern()
    
    # Create agents with different strategies
    print("1. Creating agents...")
    immediate_agent = OnlineLearningAgent(
        name="ImmediateAgent",
        task_description="classify sentiment as positive or negative",
        strategy=UpdateStrategy.IMMEDIATE
    )
    pattern.register_agent(immediate_agent)
    
    batch_agent = OnlineLearningAgent(
        name="BatchAgent",
        task_description="classify sentiment as positive or negative",
        strategy=UpdateStrategy.MINI_BATCH,
        batch_size=10
    )
    pattern.register_agent(batch_agent)
    
    adaptive_agent = OnlineLearningAgent(
        name="AdaptiveAgent",
        task_description="classify sentiment as positive or negative",
        strategy=UpdateStrategy.ADAPTIVE
    )
    pattern.register_agent(adaptive_agent)
    
    # Generate streaming data
    print("\n2. Generating streaming data...")
    stream = []
    
    # Initial positive examples
    for i in range(30):
        stream.append(StreamingExample(
            id=f"ex_{i}",
            features={"text": f"positive_text_{i}", "score": random.uniform(0.6, 1.0)},
            label="positive"
        ))
    
    # Shift to negative examples (concept drift)
    for i in range(30, 60):
        stream.append(StreamingExample(
            id=f"ex_{i}",
            features={"text": f"negative_text_{i}", "score": random.uniform(0.0, 0.4)},
            label="negative"
        ))
    
    # Mixed examples
    for i in range(60, 100):
        label = random.choice(["positive", "negative"])
        score = random.uniform(0.6, 1.0) if label == "positive" else random.uniform(0.0, 0.4)
        stream.append(StreamingExample(
            id=f"ex_{i}",
            features={"text": f"{label}_text_{i}", "score": score},
            label=label
        ))
    
    # Run online learning
    print("\n3. Running online learning...")
    summary = pattern.run(stream)
    
    print(f"\n4. Results:")
    print(f"   Agents: {summary['agents']}")
    print(f"   Stream Length: {summary['stream_length']}")
    
    print(f"\n5. Performance Metrics:")
    for agent_name, metrics in summary['metrics'].items():
        print(f"\n   {agent_name}:")
        print(f"     Accuracy: {metrics.accuracy:.2%}")
        print(f"     Recent Accuracy: {metrics.recent_accuracy:.2%}")
        print(f"     Examples Processed: {metrics.examples_processed}")
        print(f"     Updates Performed: {metrics.updates_performed}")
        print(f"     Avg Confidence: {metrics.avg_confidence:.2f}")
    
    print(f"\n6. Statistics:")
    for agent_name, stats in summary['statistics'].items():
        print(f"\n   {agent_name}:")
        for key, value in stats.items():
            if key != "name":
                print(f"     {key}: {value}")
    
    print("\n✓ Online learning adapted to streaming data in real-time!")

    

Agentic Architecture Pattern: Online Learning

Pattern 55: 55 Online Learning – Line-by-Line Explanation

Overview

Pattern 55: Online Learning

Purpose: Learn continuously from streaming data in real-time. Updates model incrementally as new data arrives, adapting to changing patterns.


Code Structure Analysis

File Information

  • Pattern Number: 55
  • Pattern Name: 55 Online Learning
  • File Path: patterns\learning_adaptation\55_online_learning.py
  • Total Lines: 500

Detailed Code Explanation

Module Docstring

"""
Pattern 55: Online Learning

Purpose: Learn continuously from streaming data in real-time.
Updates model incrementally as new data arrives, adapting to changing patterns.
"""

Explanation: This module docstring describes the purpose and key features of the 55 Online Learning pattern.


Imports Section

from typing import Dict, Any, List, Optional, Deque
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from collections import deque
import random
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI  # First 20 imports shown

Explanation: The module imports necessary dependencies: – from typing import Dict, Any, List, Optional, Deque: Type hints for better code documentation – from dataclasses import dataclass, field: Data class decorators for clean data structures – from datetime import datetime, timedelta: Date and time utilities – from enum import Enum: Enumeration support for fixed sets of values – from langchain_core.prompts import ChatPromptTemplate: LangChain framework integration – from langchain_openai import ChatOpenAI: LangChain framework integration


Enumerations

UpdateStrategy (Line 18)

class UpdateStrategy(Enum):
    IMMEDIATE
    MINI_BATCH
    SLIDING_WINDOW
    ADAPTIVE

Purpose: Strategies for online model updates.

Values: – IMMEDIATE: Represents immediate – MINI_BATCH: Represents mini batch – SLIDING_WINDOW: Represents sliding window – ADAPTIVE: Represents adaptive


Classes

UpdateStrategy (Line 18)

Description: Strategies for online model updates.

StreamingExample (Line 27)

Description: A single streaming data example.

ModelSnapshot (Line 38)

Description: Snapshot of model state at a point in time.

PerformanceMetrics (Line 48)

Description: Real-time performance metrics.

OnlineLearningAgent (Line 58)

Description: Online Learning Agent – Learn continuously from streaming data.

Adapts in real-time as new examples arrive, maintaining performance on non-stationary data streams. Uses LLM for pattern recognition.

Methods:

__init__ (Line 66)
def __init__(self, name, task_description, strategy, window_size, batch_size):
    """No method docstring."""

Purpose: No method docstring.

Parameters: – self: Instance reference – name: Identifier or label – task_description: Parameter for task_description – strategy: Parameter for strategy – window_size: Parameter for window_size – batch_size: Parameter for batch_size

process_stream (Line 100)
def process_stream(self, example):
    """Process a single streaming example."""

Purpose: Process a single streaming example.

Parameters: – self: Instance reference – example: Parameter for example

_update_model (Line 131)
def _update_model(self):
    """Update model with current buffer."""

Purpose: Update model with current buffer.

Parameters: – self: Instance reference

_extract_patterns (Line 164)
def _extract_patterns(self, examples):
    """Extract patterns from examples using LLM."""

Purpose: Extract patterns from examples using LLM.

Parameters: – self: Instance reference – examples: Parameter for examples

predict (Line 226)
def predict(self, example):
    """Make prediction on streaming example."""

Purpose: Make prediction on streaming example.

Parameters: – self: Instance reference – example: Parameter for example

_calculate_recent_accuracy (Line 294)
def _calculate_recent_accuracy(self):
    """Calculate accuracy on recent predictions."""

Purpose: Calculate accuracy on recent predictions.

Parameters: – self: Instance reference

_take_snapshot (Line 300)
def _take_snapshot(self):
    """Take snapshot of current model state."""

Purpose: Take snapshot of current model state.

Parameters: – self: Instance reference

get_performance_metrics (Line 317)
def get_performance_metrics(self):
    """Get current performance metrics."""

Purpose: Get current performance metrics.

Parameters: – self: Instance reference

get_statistics (Line 337)
def get_statistics(self):
    """Get comprehensive statistics."""

Purpose: Get comprehensive statistics.

Parameters: – self: Instance reference

OnlineLearningPattern (Line 353)

Description: Online Learning Pattern – Continuous learning from data streams.

Coordinates multiple agents learning from streaming data with different update strategies.

Methods:

__init__ (Line 361)
def __init__(self):
    """No method docstring."""

Purpose: No method docstring.

Parameters: – self: Instance reference

register_agent (Line 365)
def register_agent(self, agent):
    """Register online learning agent."""

Purpose: Register online learning agent.

Parameters: – self: Instance reference – agent: Parameter for agent

simulate_stream (Line 370)
def simulate_stream(self, examples, test_interval):
    """Simulate streaming data with online learning."""

Purpose: Simulate streaming data with online learning.

Parameters: – self: Instance reference – examples: Parameter for examples – test_interval: Parameter for test_interval

run (Line 391)
def run(self, examples):
    """Run online learning simulation."""

Purpose: Run online learning simulation.

Parameters: – self: Instance reference – examples: Parameter for examples


Design Patterns Used

  • Strategy Pattern: Different execution strategies for various scenarios
  • State Pattern: Managing different states in the workflow
  • Chain of Responsibility: Sequential processing through workflow nodes
  • Observer Pattern: Tracking and monitoring pattern execution
  • Factory Pattern: Creating instances with proper configuration

Usage Example

# Initialize the pattern
pattern = UpdateStrategy(name="example", model_name="gpt-4")

# Configure and execute
result = pattern.execute(data={"key": "value"})

# Check results
print(result)

Key Concepts

  • LLM Integration: Uses LangChain/OpenAI for intelligent decision-making
  • Workflow Orchestration: LangGraph manages execution flow through nodes
  • State Management: TypedDict defines workflow state structure
  • Error Handling: Comprehensive error handling and fallback mechanisms
  • Extensibility: Designed for easy customization and extension

Extension Points

This pattern can be extended by: 1. Adding custom workflow nodes 2. Implementing additional validation logic 3. Integrating with external services 4. Customizing LLM prompts and responses 5. Adding monitoring and logging capabilities

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *