Agentic Design Pattern: Curiosity Driven Exploration

Written by

in


    """
Pattern 118: Curiosity-Driven Exploration

Description:
    Agent explores environment based on intrinsic motivation driven by
    novelty, information gain, and prediction error.

Components:
    - Novelty detection
    - Information gain calculation
    - Exploration strategy
    - Intrinsic reward

Use Cases:
    - Open-ended learning
    - Exploration tasks
    - Discovery systems

LangChain Implementation:
    Uses LLM to generate curious questions and exploration strategies based on
    knowledge gaps and novelty.
"""

import os
from typing import List, Dict, Any, Set
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 CuriosityDrivenAgent:
    """Agent that explores based on intrinsic curiosity."""
    
    def __init__(self, model_name: str = "gpt-4"):
        self.llm = ChatOpenAI(model=model_name, temperature=0.8)
        self.knowledge_base = set()
        self.explored_topics = []
        self.curiosity_log = []
        
    def assess_novelty(self, topic: str) -> float:
        """Assess how novel a topic is based on existing knowledge."""
        novelty_prompt = ChatPromptTemplate.from_messages([
            ("system", "Rate the novelty of this topic given existing knowledge. Return a score from 0.0 (completely known) to 1.0 (completely novel)."),
            ("user", """Topic: {topic}

Known Knowledge:
{knowledge}

Novelty score (0.0-1.0):""")
        ])
        
        knowledge_str = "\n".join([f"- {k}" for k in list(self.knowledge_base)[:20]]) or "Empty"
        
        chain = novelty_prompt | self.llm | StrOutputParser()
        response = chain.invoke({"topic": topic, "knowledge": knowledge_str})
        
        try:
            score = float(response.strip())
            return max(0.0, min(1.0, score))
        except:
            return 0.5
    
    def generate_curious_questions(self, context: str) -> List[str]:
        """Generate questions driven by curiosity about knowledge gaps."""
        curiosity_prompt = ChatPromptTemplate.from_messages([
            ("system", """You are a curious agent exploring knowledge.
Generate 3-5 interesting questions that would expand understanding in unexplored directions.
Focus on:
- Novel connections
- Surprising implications
- Unexplored aspects
- Deep mechanisms"""),
            ("user", """Current Context: {context}

Known Knowledge:
{knowledge}

Generate curious questions:""")
        ])
        
        knowledge_str = "\n".join([f"- {k}" for k in list(self.knowledge_base)[-10:]]) or "Empty"
        
        chain = curiosity_prompt | self.llm | StrOutputParser()
        response = chain.invoke({"context": context, "knowledge": knowledge_str})
        
        # Parse questions
        questions = [line.strip('- ').strip() for line in response.split('\n') if line.strip()]
        return [q for q in questions if '?' in q][:5]
    
    def calculate_information_gain(self, potential_exploration: str) -> float:
        """Calculate expected information gain from exploring a topic."""
        gain_prompt = ChatPromptTemplate.from_messages([
            ("system", "Estimate information gain (0.0-1.0) from exploring this topic given current knowledge."),
            ("user", """Potential Exploration: {exploration}

Current Knowledge:
{knowledge}

Information gain score (0.0-1.0):""")
        ])
        
        knowledge_str = "\n".join([f"- {k}" for k in list(self.knowledge_base)[-15:]]) or "Empty"
        
        chain = gain_prompt | self.llm | StrOutputParser()
        response = chain.invoke({"exploration": potential_exploration, "knowledge": knowledge_str})
        
        try:
            score = float(response.strip())
            return max(0.0, min(1.0, score))
        except:
            return 0.5
    
    def explore(self, topic: str) -> Dict[str, Any]:
        """Explore a topic and gain knowledge."""
        print(f"\nšŸ” Exploring: {topic}")
        
        # Assess novelty
        novelty = self.assess_novelty(topic)
        print(f"  Novelty: {novelty:.2f}")
        
        # Calculate information gain
        info_gain = self.calculate_information_gain(topic)
        print(f"  Information Gain: {info_gain:.2f}")
        
        # Intrinsic reward
        intrinsic_reward = (novelty + info_gain) / 2
        print(f"  Intrinsic Reward: {intrinsic_reward:.2f}")
        
        # Explore the topic
        exploration_prompt = ChatPromptTemplate.from_messages([
            ("system", "Explore this topic deeply and discover interesting insights."),
            ("user", "Topic: {topic}\n\nProvide interesting discoveries:")
        ])
        
        chain = exploration_prompt | self.llm | StrOutputParser()
        discoveries = chain.invoke({"topic": topic})
        
        # Update knowledge base
        self.knowledge_base.add(topic)
        self.explored_topics.append(topic)
        
        result = {
            "topic": topic,
            "novelty": novelty,
            "information_gain": info_gain,
            "intrinsic_reward": intrinsic_reward,
            "discoveries": discoveries
        }
        
        self.curiosity_log.append(result)
        
        print(f"\n  Discoveries:")
        for line in discoveries.split('\n')[:3]:
            if line.strip():
                print(f"    • {line.strip()}")
        
        return result
    
    def choose_next_exploration(self, candidates: List[str]) -> str:
        """Choose next exploration target based on curiosity."""
        print("\nšŸ“Š Evaluating exploration candidates...")
        
        scores = []
        for candidate in candidates:
            novelty = self.assess_novelty(candidate)
            info_gain = self.calculate_information_gain(candidate)
            curiosity_score = (novelty + info_gain) / 2
            scores.append((candidate, curiosity_score))
            print(f"  {candidate}: {curiosity_score:.2f}")
        
        # Choose highest curiosity score
        best_candidate = max(scores, key=lambda x: x[1])
        print(f"\n✨ Selected: {best_candidate[0]} (score: {best_candidate[1]:.2f})")
        
        return best_candidate[0]
    
    def get_exploration_summary(self) -> str:
        """Summarize exploration journey."""
        summary_prompt = ChatPromptTemplate.from_messages([
            ("system", "Summarize this curiosity-driven exploration journey."),
            ("user", """Explored Topics:
{topics}

Curiosity Log:
{log}

Provide a summary of:
1. Key discoveries
2. Exploration patterns
3. Knowledge growth
4. Most rewarding explorations""")
        ])
        
        topics = "\n".join([f"{i+1}. {t}" for i, t in enumerate(self.explored_topics)])
        log = "\n".join([
            f"Topic: {entry['topic']}, Reward: {entry['intrinsic_reward']:.2f}"
            for entry in self.curiosity_log
        ])
        
        chain = summary_prompt | self.llm | StrOutputParser()
        return chain.invoke({"topics": topics, "log": log})


def demonstrate_curiosity_driven_exploration():
    """Demonstrate curiosity-driven exploration pattern."""
    print("=== Curiosity-Driven Exploration Pattern ===\n")
    
    agent = CuriosityDrivenAgent()
    
    # Initial topic
    print("1. Starting Exploration from Initial Topic")
    print("-" * 50)
    agent.explore("Quantum Computing")
    
    # Generate curious questions
    print("\n2. Generating Curious Questions")
    print("-" * 50)
    questions = agent.generate_curious_questions("Quantum Computing")
    print("Curious questions generated:")
    for i, q in enumerate(questions, 1):
        print(f"{i}. {q}")
    
    # Explore based on curiosity
    print("\n3. Curiosity-Driven Exploration")
    print("-" * 50)
    
    # Candidate topics
    candidates = [
        "Quantum Entanglement Applications",
        "Quantum Error Correction",
        "Superconducting Qubits",
        "Quantum Algorithms for Chemistry"
    ]
    
    # Choose and explore
    for i in range(3):
        print(f"\n--- Exploration Round {i+1} ---")
        next_topic = agent.choose_next_exploration(candidates)
        agent.explore(next_topic)
        
        # Remove explored topic and add related ones
        candidates.remove(next_topic)
        
        # Generate new candidates based on discoveries
        if i < 2:
            new_candidates_prompt = ChatPromptTemplate.from_messages([
                ("system", "Suggest 2 related unexplored topics based on recent discovery."),
                ("user", "Recent exploration: {topic}\n\nSuggest new topics:")
            ])
            chain = new_candidates_prompt | agent.llm | StrOutputParser()
            new_suggestions = chain.invoke({"topic": next_topic})
            new_topics = [line.strip('- ').strip() for line in new_suggestions.split('\n') if line.strip()][:2]
            candidates.extend(new_topics)
    
    # Summary
    print("\n4. Exploration Journey Summary")
    print("-" * 50)
    summary = agent.get_exploration_summary()
    print(summary)
    
    print("\n=== Summary ===")
    print(f"Total explorations: {len(agent.explored_topics)}")
    print(f"Knowledge base size: {len(agent.knowledge_base)}")
    print(f"Average intrinsic reward: {sum(e['intrinsic_reward'] for e in agent.curiosity_log) / len(agent.curiosity_log):.2f}")
    print("\nCuriosity-driven exploration demonstrated with:")
    print("- Novelty assessment")
    print("- Information gain calculation")
    print("- Intrinsic reward-based decisions")
    print("- Autonomous exploration strategy")
    print("- Continuous knowledge expansion")


if __name__ == "__main__":
    demonstrate_curiosity_driven_exploration()

    

Agentic Design Pattern: Curiosity Driven Exploration

Pattern 118: Curiosity Driven Exploration – Line-by-Line Explanation

Overview

This document provides a detailed line-by-line explanation of the Curiosity Driven Exploration pattern implementation.


Pattern Description

Pattern 118: Curiosity-Driven Exploration

Description: Agent explores environment based on intrinsic motivation driven by novelty, information gain, and prediction error.

Components: – Novelty detection – Information gain calculation – Exploration strategy – Intrinsic reward

Use Cases: – Open-ended learning – Exploration tasks – Discovery systems

LangChain Implementation: Uses LLM to generate curious questions and exploration strategies based on knowledge gaps and novelty.


File Statistics

  • Total Lines: 279
  • Classes: 1
  • Functions: 1
  • Constants: 0

Module Documentation

"""
Pattern 118: Curiosity-Driven Exploration

Description:
    Agent explores environment based on intrinsic motivation driven by
    novelty, information gain, and prediction error.

Components:
    - Novelty detection
    - Information gain calculation
    - Exploration strategy
    - Intrinsic reward

Use Cases:
    - Open-ended learning
    - Exploration tasks
    - Discovery systems

LangChain Implementation:
    Uses LLM to generate curious questions and exploration strategies based on
    knowledge gaps and novelty.
"""

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


Import Statements

Standard Library Imports

import langchain_core.output_parsers.StrOutputParser
import langchain_core.prompts.ChatPromptTemplate
import os
import typing.Any
import typing.Dict
import typing.List
import typing.Set

Purpose: Core Python functionality: – 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.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: CuriosityDrivenAgent

Lines 35-205

Purpose:

Agent that explores based on intrinsic curiosity.

Methods:

__init__(self, model_name)

Lines 38-42

Purpose: Initialize the class instance with required parameters.

Parameters: – model_name: Language model instance

assess_novelty(self, topic)

Lines 44-65

Purpose:

Assess how novel a topic is based on existing knowledge.

Parameters: – topic: Topic

Returns: float

generate_curious_questions(self, context)

Lines 67-92

Purpose:

Generate questions driven by curiosity about knowledge gaps.

Parameters: – context: Context

Returns: <ast.Subscript object at 0x000001A06D7CC0D0>

calculate_information_gain(self, potential_exploration)

Lines 94-115

Purpose:

Calculate expected information gain from exploring a topic.

Parameters: – potential_exploration: Potential exploration

Returns: float

explore(self, topic)

Lines 117-161

Purpose:

Explore a topic and gain knowledge.

Parameters: – topic: Topic

Returns: <ast.Subscript object at 0x000001A06D88B210>

choose_next_exploration(self, candidates)

Lines 163-179

Purpose:

Choose next exploration target based on curiosity.

Parameters: – candidates: Candidates

Returns: str

get_exploration_summary(self)

Lines 181-205

Purpose:

Summarize exploration journey.

Parameters:

Returns: str


Functions

demonstrate_curiosity_driven_exploration()

Lines 208-274

Purpose:

Demonstrate curiosity-driven exploration pattern.

Key Concepts Summary

Pattern Overview

The Curiosity Driven Exploration 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 117
  • Pattern 119

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 Curiosity Driven Exploration 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: 118_curiosity_driven_exploration.py
Total Lines: 279
Pattern Category: AI Agent Design Pattern #118


Generated from actual implementation code
For educational and reference purposes

Comments

Leave a Reply

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