### Index # Welcome to Atomic Agents Documentation ```{toctree} :maxdepth: 2 :caption: Documentation guides/index api/index examples/index contributing ``` # A Lightweight and Modular Framework for Building AI Agents ```{admonition} AI Assistant Resources :class: tip šŸ“„ **Download Documentation for AI Assistants and LLMs** Choose the resource that best fits your needs: - **{download}`šŸ“š Full Package <_static/llms-full.txt>`** - Complete documentation, source code, and examples in one file - **{download}`šŸ“– Documentation Only <_static/llms-docs.txt>`** - API documentation, guides, and references - **{download}`šŸ’» Source Code Only <_static/llms-source.txt>`** - Complete atomic-agents framework source code - **{download}`šŸŽÆ Examples Only <_static/llms-examples.txt>`** - All example implementations with READMEs All files are optimized for AI assistants and Large Language Models, with clear structure and formatting for easy parsing. ``` The Atomic Agents framework is designed around the concept of atomicity to be an extremely lightweight and modular framework for building Agentic AI pipelines and applications without sacrificing developer experience and maintainability. The framework provides a set of tools and agents that can be combined to create powerful applications. It is built on top of [Instructor](https://github.com/jxnl/instructor) and leverages the power of [Pydantic](https://docs.pydantic.dev/latest/) for data and schema validation and serialization. All logic and control flows are written in Python, enabling developers to apply familiar best practices and workflows from traditional software development without compromising flexibility or clarity. ## Key Features - **Modularity**: Build AI applications by combining small, reusable components - **Predictability**: Define clear input and output schemas using Pydantic - **Extensibility**: Easily swap out components or integrate new ones - **Control**: Fine-tune each part of the system individually - **Provider Agnostic**: Works with various LLM providers through Instructor - **Built for Production**: Robust error handling and async support ## Installation You can install Atomic Agents using pip: ```bash pip install atomic-agents ``` Or using uv (recommended): ```bash uv add atomic-agents ``` Make sure you also install the provider you want to use. Provider SDKs are available as instructor extras: ```bash pip install instructor[groq] # for Groq pip install instructor[anthropic] # for Anthropic pip install instructor[google-genai] # for Gemini ``` OpenAI is included by default. This also installs the CLI *Atomic Assembler*, which can be used to download Tools (and soon also Agents and Pipelines). ```{note} The framework supports multiple providers through Instructor, including **OpenAI**, **Anthropic**, **Groq**, **Ollama** (local models), **Gemini**, and more! For a full list of all supported providers and their setup instructions, have a look at the [Instructor Integrations documentation](https://python.useinstructor.com/integrations/). ``` ## Quick Example Here's a glimpse of how easy it is to create an agent: ```python import instructor import openai from atomic_agents.context import ChatHistory from atomic_agents import AtomicAgent, AgentConfig, BasicChatInputSchema, BasicChatOutputSchema # Set up your API key (either in environment or pass directly) # os.environ["OPENAI_API_KEY"] = "your-api-key" # or pass it to the client: openai.OpenAI(api_key="your-api-key") # Initialize agent with history history = ChatHistory() # Set up client with your preferred provider client = instructor.from_openai(openai.OpenAI()) # Pass your API key here if not in environment # Create an agent agent = AtomicAgent[BasicChatInputSchema, BasicChatOutputSchema]( config=AgentConfig( client=client, model="gpt-5-mini", # Use your provider's model history=history ) ) # Interact with your agent (using the agent's input schema) response = agent.run(agent.input_schema(chat_message="Tell me about quantum computing")) # Or more explicitly: response = agent.run( BasicChatInputSchema(chat_message="Tell me about quantum computing") ) print(response) ``` ## Example Projects Check out our example projects in our [GitHub repository](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples): - [Quickstart Examples](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/quickstart): Simple examples to get started - [Hooks System](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/hooks-example): Comprehensive monitoring, error handling, and performance metrics - [Basic Multimodal](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/basic-multimodal): Analyze images with text - [RAG Chatbot](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/rag-chatbot): Build context-aware chatbots - [Web Search Agent](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/web-search-agent): Create agents that perform web searches - [Deep Research](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/deep-research): Perform deep research tasks - [YouTube Summarizer](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/youtube-summarizer): Extract knowledge from videos - [YouTube to Recipe](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/youtube-to-recipe): Convert cooking videos into structured recipes - [Orchestration Agent](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/orchestration-agent): Coordinate multiple agents for complex tasks ## Community & Support - [GitHub Repository](https://github.com/eigenwise/atomic-agents) - [Issue Tracker](https://github.com/eigenwise/atomic-agents/issues) - [Reddit Community](https://www.reddit.com/r/AtomicAgents/) ## Indices and References * {ref}`genindex` * {ref}`modindex` * {ref}`search` --- ### Api/Agents # Agents ## Schema Hierarchy The Atomic Agents framework uses Pydantic for schema validation and serialization. All input and output schemas follow this inheritance pattern: ```PlainText pydantic.BaseModel └── BaseIOSchema ā”œā”€ā”€ BasicChatInputSchema └── BasicChatOutputSchema ``` ### BaseIOSchema The base schema class that all agent input/output schemas inherit from. ```{eval-rst} .. py:class:: BaseIOSchema Base schema class for all agent input/output schemas. Inherits from :class:`pydantic.BaseModel`. All agent schemas must inherit from this class to ensure proper serialization and validation. **Inheritance:** - :class:`pydantic.BaseModel` ``` ### BasicChatInputSchema The default input schema for agents. ```{eval-rst} .. py:class:: BasicChatInputSchema Default input schema for agent interactions. **Inheritance:** - :class:`BaseIOSchema` → :class:`pydantic.BaseModel` .. py:attribute:: chat_message :type: str The message to send to the agent. Example: >>> input_schema = BasicChatInputSchema(chat_message="Hello, agent!") >>> agent.run(input_schema) ``` ### BasicChatOutputSchema The default output schema for agents. ```{eval-rst} .. py:class:: BasicChatOutputSchema Default output schema for agent responses. **Inheritance:** - :class:`BaseIOSchema` → :class:`pydantic.BaseModel` .. py:attribute:: chat_message :type: str The response message from the agent. Example: >>> response = agent.run(input_schema) >>> print(response.chat_message) ``` ### Creating Custom Schemas You can create custom input/output schemas by inheriting from `BaseIOSchema`: ```python from pydantic import Field from typing import List from atomic_agents import BaseIOSchema class CustomInputSchema(BaseIOSchema): chat_message: str = Field(..., description="User's message") context: str = Field(None, description="Optional context for the agent") class CustomOutputSchema(BaseIOSchema): chat_message: str = Field(..., description="Agent's response") follow_up_questions: List[str] = Field( default_factory=list, description="Suggested follow-up questions" ) confidence: float = Field( ..., description="Confidence score for the response", ge=0.0, le=1.0 ) ``` ## Base Agent The `AtomicAgent` class is the foundation for building AI agents in the Atomic Agents framework. It handles chat interactions, history management, system prompts, and responses from language models. ```python from atomic_agents import AtomicAgent, AgentConfig from atomic_agents.context import ChatHistory, SystemPromptGenerator # Create agent with basic configuration agent = AtomicAgent[BasicChatInputSchema, BasicChatOutputSchema]( config=AgentConfig( client=instructor.from_openai(OpenAI()), model="gpt-4-turbo-preview", history=ChatHistory(), system_prompt_generator=SystemPromptGenerator() ) ) # Run the agent response = agent.run(user_input) # Stream responses async for partial_response in agent.run_async(user_input): print(partial_response) ``` ### Configuration The `AgentConfig` class provides configuration options: ```python class AgentConfig: client: instructor.Instructor # Client for interacting with the language model model: str = "gpt-4-turbo-preview" # Model to use history: Optional[ChatHistory] = None # History component system_prompt_generator: Optional[SystemPromptGenerator] = None # Prompt generator input_schema: Optional[Type[BaseModel]] = None # Custom input schema output_schema: Optional[Type[BaseModel]] = None # Custom output schema model_api_parameters: Optional[dict] = None # Additional API parameters ``` ### Input/Output Schemas Default schemas for basic chat interactions: ```python class BasicChatInputSchema(BaseIOSchema): """Input from the user to the AI agent.""" chat_message: str = Field( ..., description="The chat message sent by the user." ) class BasicChatOutputSchema(BaseIOSchema): """Response generated by the chat agent.""" chat_message: str = Field( ..., description="The markdown-enabled response generated by the chat agent." ) ``` ### Key Methods - `run(user_input: Optional[BaseIOSchema] = None) -> BaseIOSchema`: Process user input and get response - `run_async(user_input: Optional[BaseIOSchema] = None)`: Stream responses asynchronously - `get_response(response_model=None) -> Type[BaseModel]`: Get direct model response - `reset_history()`: Reset history to initial state - `get_context_provider(provider_name: str)`: Get a registered context provider - `register_context_provider(provider_name: str, provider: BaseDynamicContextProvider)`: Register a new context provider - `unregister_context_provider(provider_name: str)`: Remove a context provider - `get_context_token_count() -> TokenCountResult`: Get token count for current context (system prompt + history) ### Context Providers Context providers can be used to inject dynamic information into the system prompt: ```python from atomic_agents.context import BaseDynamicContextProvider class SearchResultsProvider(BaseDynamicContextProvider): def __init__(self, title: str): super().__init__(title=title) self.results = [] def get_info(self) -> str: return "\n\n".join([ f"Result {idx}:\n{result}" for idx, result in enumerate(self.results, 1) ]) # Register with agent agent.register_context_provider( "search_results", SearchResultsProvider("Search Results") ) ``` ### Streaming Support The agent supports streaming responses for more interactive experiences: ```python async def chat(): async for partial_response in agent.run_async(user_input): # Handle each chunk of the response print(partial_response.chat_message) ``` ### History Management The agent automatically manages conversation history through the `ChatHistory` component: ```python # Access history history = agent.history.get_history() # Reset to initial state agent.reset_history() # Save/load history state serialized = agent.history.dump() agent.history.load(serialized) ``` ### Token Counting Monitor context usage with the `get_context_token_count()` method. Token counts are computed accurately on-demand by serializing the context exactly as Instructor does, including the output schema overhead. This works with any provider (OpenAI, Anthropic, Google, etc.) and supports multimodal content: ```python # Get accurate token count at any time - always returns a result token_info = agent.get_context_token_count() print(f"Total tokens: {token_info.total}") print(f"System prompt (with schema): {token_info.system_prompt} tokens") print(f"History: {token_info.history} tokens") print(f"Model: {token_info.model}") # Check context utilization if max tokens is known if token_info.max_tokens: print(f"Max context: {token_info.max_tokens} tokens") if token_info.utilization: print(f"Context utilization: {token_info.utilization:.1%}") ``` The `TokenCountResult` contains: - `total`: Total tokens in context (system + history + schema overhead) - `system_prompt`: Tokens used by system prompt and output schema - `history`: Tokens used by conversation history (including multimodal content) - `model`: The model name used for counting - `max_tokens`: Maximum context window (if known) - `utilization`: Percentage of context used (if max_tokens known) ### Custom Schemas You can use custom input/output schemas for structured interactions: ```python from pydantic import BaseModel, Field from typing import List class CustomInput(BaseIOSchema): """Custom input with specific fields""" question: str = Field(..., description="User's question") context: str = Field(..., description="Additional context") class CustomOutput(BaseIOSchema): """Custom output with structured data""" answer: str = Field(..., description="Answer to the question") sources: List[str] = Field(..., description="Source references") # Create agent with custom schemas agent = AtomicAgent[CustomInput, CustomOutput]( config=AgentConfig( client=client, model=model, ) ) ``` For full API details: ```{eval-rst} .. automodule:: atomic_agents.agents.atomic_agent :members: :undoc-members: :show-inheritance: ``` --- ### Api/Context # Context ```{seealso} For a comprehensive guide on memory management, multi-agent patterns, and best practices, see the **[Memory and Context Guide](/guides/memory)**. ``` ## Agent History The `ChatHistory` class manages conversation history and state for AI agents. It implements `BaseChatHistory`, an interface-only abstract base class that declares the memory contract `AtomicAgent` depends on (the type `AgentConfig.history` accepts). Implement `BaseChatHistory` directly, or subclass `ChatHistory`, to plug in your own persistent or backend-specific memory store. See the [Memory guide's "Writing a Custom Memory Backend" section](/guides/memory#writing-a-custom-memory-backend) for the full contract and a recommended pattern. ```python from atomic_agents.context import ChatHistory from atomic_agents import BaseIOSchema # Initialize history with optional max messages history = ChatHistory(max_messages=10) # Add messages history.add_message( role="user", content=BaseIOSchema(...) ) # Initialize a new turn history.initialize_turn() turn_id = history.get_current_turn_id() # Access history history = history.get_history() # Manage history history.get_message_count() # Get number of messages history.delete_turn_id(turn_id) # Delete messages by turn # Persistence serialized = history.dump() # Save to string history.load(serialized) # Load from string # Create copy new_history = history.copy() ``` Key features: - Message history management with role-based messages - Turn-based conversation tracking - Support for multimodal content (images, etc.) - Serialization and persistence - History size management - Deep copy functionality ### Message Structure Messages in history are structured as: ```python class Message(BaseModel): role: str # e.g., 'user', 'assistant', 'system' content: BaseIOSchema # Message content following schema turn_id: Optional[str] # Unique ID for grouping messages ``` ### Multimodal Support The history system automatically handles multimodal content: ```python # For content with images history = history.get_history() for message in history: if isinstance(message.content, list): text_content = message.content[0] # JSON string images = message.content[1:] # List of images ``` ## System Prompt Generator The `SystemPromptGenerator` creates structured system prompts for AI agents: ```python from atomic_agents.context import ( SystemPromptGenerator, BaseDynamicContextProvider ) # Create generator with static content generator = SystemPromptGenerator( background=[ "You are a helpful AI assistant.", "You specialize in technical support." ], steps=[ "1. Understand the user's request", "2. Analyze available information", "3. Provide clear solutions" ], output_instructions=[ "Use clear, concise language", "Include step-by-step instructions", "Cite relevant documentation" ] ) # Generate prompt prompt = generator.generate_prompt() ``` ### Custom System Prompt Generator If you require finer control over system prompt construction, subclass `BaseSystemPromptGenerator` and implement `generate_prompt()`. This approach is useful when prompt content should be maintained in a human-readable format (e.g., Markdown or text file) to allow review or editing by non-developers. ```python from pathlib import Path from typing import Dict, Optional, Union from atomic_agents.context import ( BaseDynamicContextProvider, BaseSystemPromptGenerator ) class MarkdownFileSystemPromptGenerator(BaseSystemPromptGenerator): def __init__( self, md_file: Union[Path, str], context_providers: Optional[Dict[str, BaseDynamicContextProvider]] = None, ): super().__init__(context_providers=context_providers) path = Path(md_file) if not path.exists(): raise FileNotFoundError(f"System prompt file not found: {md_file}") self.system_prompt = path.read_text(encoding="utf-8") def generate_prompt(self) -> str: return f"{self.system_prompt}\n\n{self._build_context_string()}" def _build_context_string(self) -> str: if not self.context_providers: return "" context_sections = ["# Additional Context"] for provider in self.context_providers.values(): info = provider.get_info() if info: context_sections.append(f"## {provider.title}") context_sections.append(info) context_sections.append("") return "\n".join(context_sections).strip() generator = MarkdownFileSystemPromptGenerator("path/to/system_prompt.md") prompt = generator.generate_prompt() ``` ### Dynamic Context Providers Context providers inject dynamic information into prompts: ```python from dataclasses import dataclass from typing import List @dataclass class SearchResult: content: str metadata: dict class SearchResultsProvider(BaseDynamicContextProvider): def __init__(self, title: str): super().__init__(title=title) self.results: List[SearchResult] = [] def get_info(self) -> str: """Format search results for the prompt""" if not self.results: return "No search results available." return "\n\n".join([ f"Result {idx}:\nMetadata: {result.metadata}\nContent:\n{result.content}\n{'-' * 80}" for idx, result in enumerate(self.results, 1) ]) # Use with generator generator = SystemPromptGenerator( background=["You answer based on search results."], context_providers={ "search_results": SearchResultsProvider("Search Results") } ) ``` The generated prompt will include: 1. Background information 2. Processing steps (if provided) 3. Dynamic context from providers 4. Output instructions ## Base Components ### BaseIOSchema Base class for all input/output schemas: ```python from atomic_agents import BaseIOSchema from pydantic import Field class CustomSchema(BaseIOSchema): """Schema description (required)""" field: str = Field(..., description="Field description") ``` Key features: - Requires docstring description - Rich representation support - Automatic schema validation - JSON serialization ### BaseTool Base class for creating tools: ```python from atomic_agents import BaseTool, BaseToolConfig from pydantic import Field class MyToolConfig(BaseToolConfig): """Tool configuration""" api_key: str = Field( default=os.getenv("API_KEY"), description="API key for the service" ) class MyTool(BaseTool[MyToolInputSchema, MyToolOutputSchema]): """Tool implementation""" input_schema = MyToolInputSchema output_schema = MyToolOutputSchema def __init__(self, config: MyToolConfig = MyToolConfig()): super().__init__(config) self.api_key = config.api_key def run(self, params: MyToolInputSchema) -> MyToolOutputSchema: # Implement tool logic pass ``` Key features: - Structured input/output schemas - Configuration management - Title and description overrides - Error handling For full API details: ```{eval-rst} .. automodule:: atomic_agents.context.chat_history :members: :undoc-members: :show-inheritance: .. automodule:: atomic_agents.context.base_chat_history :members: :undoc-members: :show-inheritance: .. automodule:: atomic_agents.context.system_prompt_generator :members: :undoc-members: :show-inheritance: .. automodule:: atomic_agents.base.base_io_schema :members: :undoc-members: :show-inheritance: .. automodule:: atomic_agents.base.base_tool :members: :undoc-members: :show-inheritance: ``` --- ### Api/Index # API Reference This section contains the API reference for all public modules and classes in Atomic Agents. ```{toctree} :maxdepth: 2 :caption: API Reference agents context utils ``` ## Core Components The Atomic Agents framework is built around several core components that work together to provide a flexible and powerful system for building AI agents. ### Agents The agents module provides the base classes for creating AI agents: - `AtomicAgent`: The foundational agent class that handles interactions with LLMs - `AgentConfig`: Configuration class for customizing agent behavior - `BasicChatInputSchema`: Standard input schema for agent interactions - `BasicChatOutputSchema`: Standard output schema for agent responses [Learn more about agents](agents.md) ### Context Components The context module contains essential building blocks: - `ChatHistory`: Manages conversation history and state with support for: - Message history with role-based messages - Turn-based conversation tracking - Multimodal content - Serialization and persistence - History size management - `SystemPromptGenerator`: Creates structured system prompts with: - Background information - Processing steps - Output instructions - Dynamic context through context providers - `BaseDynamicContextProvider`: Base class for creating custom context providers that can inject dynamic information into system prompts [Learn more about context components](context.md) ### Utils The utils module provides helper functions and utilities: - Message formatting - Tool response handling - Schema validation - Error handling [Learn more about utilities](utils.md) ## Getting Started For practical examples and guides on using these components, see: - [Quickstart Guide](../guides/quickstart.md) - [Tools Guide](../guides/tools.md) --- ### Api/Utils # Utilities ## Token Counting The `TokenCounter` utility provides provider-agnostic token counting for any model supported by LiteLLM. This allows you to monitor context usage regardless of whether you're using OpenAI, Anthropic, Google, or any other supported provider. ### TokenCountResult A named tuple containing token count information: ```{eval-rst} .. py:class:: TokenCountResult Named tuple containing token count information. .. py:attribute:: total :type: int Total tokens in the context (system prompt + history + schema overhead). .. py:attribute:: system_prompt :type: int Tokens used by the system prompt and output schema. .. py:attribute:: history :type: int Tokens used by conversation history (including multimodal content). .. py:attribute:: model :type: str The model used for token counting. .. py:attribute:: max_tokens :type: Optional[int] Maximum context window for the model (if known). .. py:attribute:: utilization :type: Optional[float] Context utilization percentage (0.0 to 1.0) if max_tokens is known. ``` ### TokenCounter The main utility class for counting tokens: ```{eval-rst} .. py:class:: TokenCounter Utility class for counting tokens in messages using LiteLLM. .. py:method:: count_messages(model: str, messages: List[Dict[str, Any]]) -> int Count tokens in a list of messages. :param model: The model name (e.g., "gpt-4", "claude-3-opus-20240229") :param messages: List of message dictionaries with "role" and "content" keys :return: Number of tokens .. py:method:: count_text(model: str, text: str) -> int Count tokens in a text string. :param model: The model name :param text: The text to count tokens for :return: Number of tokens .. py:method:: get_max_tokens(model: str) -> Optional[int] Get the maximum context window for a model. :param model: The model name :return: Maximum tokens, or None if unknown .. py:method:: count_context(model: str, system_messages: List[Dict], history_messages: List[Dict]) -> TokenCountResult Count tokens for a complete context (system prompt + history). :param model: The model name :param system_messages: System prompt messages :param history_messages: Conversation history messages :return: TokenCountResult with detailed breakdown ``` ### Usage Example ```python from atomic_agents.utils import TokenCounter, TokenCountResult # Direct usage counter = TokenCounter() messages = [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"}, {"role": "assistant", "content": "Hi there! How can I help?"}, ] # Count tokens in messages token_count = counter.count_messages("gpt-4", messages) # Get max context window max_tokens = counter.get_max_tokens("gpt-4") # Count complete context with breakdown result = counter.count_context( model="gpt-4", system_messages=[{"role": "system", "content": "You are helpful."}], history_messages=[{"role": "user", "content": "Hello!"}], ) print(f"Total: {result.total}, System: {result.system_prompt}, History: {result.history}") if result.utilization: print(f"Context utilization: {result.utilization:.1%}") ``` ### Using with AtomicAgent The easiest way to get token counts is through the agent's `get_context_token_count()` method. The agent computes accurate token counts on-demand by serializing the context exactly as Instructor does, including output schema overhead and multimodal content: ```python # Get accurate token count at any time - always returns a result token_info = agent.get_context_token_count() print(f"Total tokens: {token_info.total}") print(f"System prompt (with schema): {token_info.system_prompt} tokens") print(f"History: {token_info.history} tokens") if token_info.utilization: print(f"Context utilization: {token_info.utilization:.1%}") ``` The token count includes: - System prompt content - Output schema overhead (the JSON schema Instructor sends for structured output) - Conversation history (including multimodal content like images, PDFs, audio) This gives you an accurate count that matches what would be sent to the API. ## Tool Message Formatting ```{eval-rst} .. automodule:: atomic_agents.utils.format_tool_message :members: :undoc-members: :show-inheritance: ``` --- ### Examples/Index # Example Projects This section contains detailed examples of using Atomic Agents in various scenarios. ```{note} All examples are available in optimized formats for AI assistants: - **{download}`Examples with documentation <../_static/llms-examples.txt>`** - All examples with source code and READMEs - **{download}`Full framework package <../_static/llms-full.txt>`** - Complete documentation, source, and examples ``` ## Quickstart Examples Simple examples to get started with the framework: - Basic chatbot with history - Custom chatbot with personality - Streaming responses - Custom input/output schemas - Multiple provider support šŸ“‚ **[View on GitHub](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/quickstart)** - Browse the complete source code and run the examples ## Hooks System Comprehensive monitoring and error handling with the AtomicAgent hook system: - Parse error handling and validation - API call monitoring and metrics - Response time tracking and performance analysis - Intelligent retry mechanisms - Production-ready error isolation - Real-time performance dashboards šŸ“‚ **[View on GitHub](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/hooks-example)** - Browse the complete source code and run the examples ## Basic Multimodal Examples of working with images and text: - Image analysis with text descriptions - Image-based question answering - Visual content generation - Multi-image comparisons šŸ“‚ **[View on GitHub](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/basic-multimodal)** - Browse the complete source code and run the examples ## RAG Chatbot Build context-aware chatbots with retrieval-augmented generation: - Document indexing and embedding - Semantic search integration - Context-aware responses - Source attribution - Follow-up suggestions šŸ“‚ **[View on GitHub](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/rag-chatbot)** - Browse the complete source code and run the examples ## Web Search Agent Create agents that can search and analyze web content: - Web search integration - Content extraction - Result synthesis - Multi-source research - Citation tracking šŸ“‚ **[View on GitHub](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/web-search-agent)** - Browse the complete source code and run the examples ## Deep Research Perform comprehensive research tasks: - Multi-step research workflows - Information synthesis - Source validation - Structured output generation - Citation management šŸ“‚ **[View on GitHub](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/deep-research)** - Browse the complete source code and run the examples ## YouTube Summarizer Extract and analyze information from videos: - Transcript extraction - Content summarization - Key point identification - Timestamp linking - Chapter generation šŸ“‚ **[View on GitHub](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/youtube-summarizer)** - Browse the complete source code and run the examples ## YouTube to Recipe Convert cooking videos into structured recipes: - Video analysis - Recipe extraction - Ingredient parsing - Step-by-step instructions - Time and temperature conversion šŸ“‚ **[View on GitHub](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/youtube-to-recipe)** - Browse the complete source code and run the examples ## Orchestration Agent Coordinate multiple agents for complex tasks: - Agent coordination - Task decomposition - Progress tracking - Error handling - Result aggregation šŸ“‚ **[View on GitHub](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/orchestration-agent)** - Browse the complete source code and run the examples ## MCP Agent Build intelligent agents using the Model Context Protocol: - Server implementation with multiple transport methods - Dynamic tool discovery and registration - Natural language query processing - Stateful conversation handling - Extensible tool architecture [View MCP Agent Documentation](mcp_agent.md) šŸ“‚ **[View on GitHub](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/mcp-agent)** - Browse the complete source code and run the examples --- ### Examples/Mcp Agent # MCP Server and Client Example This guide provides a detailed overview of the Model Context Protocol (MCP) server and client example implementation in the Atomic Agents framework. ## Overview The MCP example demonstrates how to build an intelligent agent system using the Model Context Protocol, showcasing both server and client implementations. The example supports three transport methods: STDIO, Server-Sent Events (SSE), and HTTP Stream. ## Architecture ### MCP Server The server component (`example-mcp-server`) is built using: - FastMCP: A high-performance MCP server implementation - Starlette: A lightweight ASGI framework - Uvicorn: An ASGI server implementation Key components: 1. **Transport Layers**: - `server_stdio.py`: Implements STDIO-based communication - `server_sse.py`: Implements SSE-based HTTP communication - `server_http.py`: Implements unified HTTP streaming transport 2. **Tools Service**: - Manages registration and execution of MCP tools - Handles tool discovery and metadata 3. **Resource Service**: - Manages static resources - Handles resource discovery and access 4. **Built-in Tools**: - `AddNumbersTool`: Performs addition - `SubtractNumbersTool`: Performs subtraction - `MultiplyNumbersTool`: Performs multiplication - `DivideNumbersTool`: Performs division ### MCP Client The client component (`example-client`) is an intelligent agent that: 1. **Tool Discovery**: - Dynamically discovers available tools from the MCP server - Builds a schema-based tool registry 2. **Query Processing**: - Uses GPT models for natural language understanding - Extracts parameters from user queries - Selects appropriate tools based on intent 3. **Execution Flow**: - Maintains conversation context - Handles tool execution results - Provides conversational responses ## Implementation Details ### Server Implementation The server supports three transport methods: 1. **SSE Transport** (`server_sse.py`): ```python # Initialize FastMCP server mcp = FastMCP("example-mcp-server") # Register tools and resources tool_service.register_tools(get_available_tools()) resource_service.register_resources(get_available_resources()) # Create Starlette app with CORS support app = create_starlette_app(mcp_server) ``` 2. **STDIO Transport** (`server_stdio.py`): - Runs as a subprocess - Communicates through standard input/output - Ideal for local development 3. **HTTP Stream Transport** (`server_http.py`): - Single `/mcp` endpoint for JSON-RPC and SSE-style streaming - Handles session via `Mcp-Session-Id` header; allows resumable and cancelable streams ### Client Implementation The client uses a sophisticated orchestration system: 1. **Tool Management**: ```python # Fetch available tools (synchronous) tools = fetch_mcp_tools( mcp_endpoint=config.mcp_server_url, transport_type=MCPTransportType.HTTP_STREAM, ) # Or fetch tools asynchronously (must be called within async context) tools = await fetch_mcp_tools_async( mcp_endpoint=config.mcp_server_command, transport_type=MCPTransportType.STDIO, client_session=session, # Optional pre-initialized session ) # Build tool schema mapping tool_schema_to_class_map = { ToolClass.input_schema: ToolClass for ToolClass in tools if hasattr(ToolClass, "input_schema") } ``` 2. **Query Processing**: - Uses an orchestrator agent to analyze queries - Extracts parameters and selects appropriate tools - Maintains conversation context through ChatHistory 3. **Async vs Sync Tool Fetching**: ```python # Synchronous fetching (suitable for HTTP/SSE) tools = fetch_mcp_tools( mcp_endpoint="http://localhost:6969", transport_type=MCPTransportType.HTTP_STREAM ) # Asynchronous fetching (optimized for STDIO with persistent sessions) async def setup_tools(): tools = await fetch_mcp_tools_async( transport_type=MCPTransportType.STDIO, client_session=session # Reuse existing session ) return tools ``` ## MCP Transport Methods The example implements three distinct transport methods via the `MCPTransportType` enum, each with its own advantages: ```python from atomic_agents.connectors.mcp.mcp_definition_service import MCPTransportType # Available transport types MCPTransportType.STDIO # Standard input/output transport MCPTransportType.SSE # Server-Sent Events transport MCPTransportType.HTTP_STREAM # HTTP Stream transport ``` ### 1. STDIO Transport STDIO transport uses standard input/output streams for communication between the client and server: ```python # Client-side STDIO setup (from main_stdio.py) async def _bootstrap_stdio(): stdio_exit_stack = AsyncExitStack() command_parts = shlex.split(config.mcp_stdio_server_command) server_params = StdioServerParameters(command=command_parts[0], args=command_parts[1:], env=None) read_stream, write_stream = await stdio_exit_stack.enter_async_context(stdio_client(server_params)) session = await stdio_exit_stack.enter_async_context(ClientSession(read_stream, write_stream)) await session.initialize() return session ``` **Key advantages**: - No network configuration required - Simple local setup - Direct process communication - Lower latency for local usage **Use cases**: - Development and testing - Single-user environments - Embedded agent applications - Offline operation ### 2. SSE Transport Server-Sent Events (SSE) transport uses HTTP long-polling for real-time, one-way communication: ```python # Server-side SSE setup (from server_sse.py) async def handle_sse(request: Request) -> None: async with sse.connect_sse( request.scope, request.receive, request._send, # noqa: SLF001 ) as (read_stream, write_stream): await mcp_server.run( read_stream, write_stream, mcp_server.create_initialization_options(), ) ``` **Key advantages**: - Multiple clients can connect to a single server - Network-based communication - Stateless server architecture - Suitable for distributed systems **Use cases**: - Production deployments - Multi-user environments - Scalable agent infrastructure - Cross-network operation ### 3. HTTP Stream Transport HTTP Stream transport uses a single `/mcp` endpoint for JSON-RPC and streaming communication: ```python # Client-side HTTP Stream setup tools = fetch_mcp_tools( mcp_endpoint="http://localhost:6969", transport_type=MCPTransportType.HTTP_STREAM, ) ``` **Key advantages**: - Single endpoint for all MCP operations - Session management via headers - Resumable and cancelable streams - Modern HTTP-based architecture **Use cases**: - Modern web applications - Cloud-native deployments - Microservice architectures - API gateway integration ## Interfaces ### Tool Interface The MCP server defines a standardized tool interface that all tools must implement: ```python class Tool(ABC): """Abstract base class for all tools.""" name: ClassVar[str] description: ClassVar[str] input_model: ClassVar[Type[BaseToolInput]] output_model: ClassVar[Optional[Type[BaseModel]]] = None @abstractmethod async def execute(self, input_data: BaseToolInput) -> ToolResponse: """Execute the tool with given arguments.""" pass def get_schema(self) -> Dict[str, Any]: """Get JSON schema for the tool.""" schema = { "name": self.name, "description": self.description, "input": self.input_model.model_json_schema(), } if self.output_model: schema["output"] = self.output_model.model_json_schema() return schema ``` The tool interface consists of: 1. **Class Variables**: - `name`: Tool identifier used in MCP communications - `description`: Human-readable tool description - `input_model`: Pydantic model defining input parameters - `output_model`: Pydantic model defining output structure (optional) 2. **Execute Method**: - Asynchronous method that performs the tool's functionality - Takes strongly-typed input data - Returns a structured ToolResponse 3. **Schema Method**: - Provides JSON Schema for tool discovery - Enables automatic documentation generation - Facilitates client-side validation ### Resource Interface The MCP server defines a standardized resource interface that all resources must implement: ```python class Resource(ABC): """Abstract base class for all resources.""" name: ClassVar[str] description: ClassVar[str] uri: ClassVar[str] mime_type: ClassVar[str] input_model: ClassVar[Type[BaseResourceInput]] output_model: ClassVar[Optional[Type[BaseModel]]] = None @abstractmethod async def read(self, input_data: BaseResourceInput) -> ResourceResponse: """Read data from the resource.""" pass def get_schema(self) -> Dict[str, Any]: """Get JSON schema for the resource.""" schema = { "name": self.name, "description": self.description, "uri": self.uri, "mime_type": self.mime_type, "input": self.input_model.model_json_schema(), } if self.output_model: schema["output"] = self.output_model.model_json_schema() return schema ``` The resource interface consists of: 1. **Class Variables**: - `name`: Resource identifier used in MCP communications - `description`: Human-readable resource description - `uri`: URI pattern for accessing the resource - `mime_type`: MIME type of the resource content - `input_model`: Pydantic model defining input parameters - `output_model`: Pydantic model defining output structure (optional) 2. **Read Method**: - Asynchronous method that retrieves data from the resource - Takes strongly-typed input data - Returns a structured ResourceResponse 3. **Schema Method**: - Provides URI Template for resource discovery - Enables automatic documentation generation ### Prompt Interface The MCP client uses a standardized prompt interface for managing prompts: ```python class Prompt(ABC): """Abstract base class for all prompts.""" name: ClassVar[str] description: ClassVar[str] input_model: ClassVar[Type[BasePromptInput]] output_model: ClassVar[Optional[Type[BaseModel]]] = None @abstractmethod async def generate(self, input_data: BasePromptInput) -> PromptResponse: """Generate the prompt with given arguments.""" pass def get_schema(self) -> Dict[str, Any]: """Get JSON schema for the prompt.""" schema = { "name": self.name, "description": self.description, "input": self.input_model.model_json_schema(), } if self.output_model: schema["output"] = self.output_model.model_json_schema() return schema ``` The prompt interface consists of: 1. **Class Variables**: - `name`: Prompt identifier used in MCP communications - `description`: Human-readable prompt description - `input_model`: Pydantic model defining input parameters - `output_model`: Pydantic model defining output structure (optional) 2. **Generate Method**: - Asynchronous method that generates the prompt - Takes strongly-typed input data - Returns a structured PromptResponse 3. **Schema Method**: - Provides JSON Schema for prompt discovery - Enables automatic documentation generation ## Configuration ### Server Configuration The server can be configured through command-line arguments: ```bash uv run example-mcp-server --mode=sse --host=0.0.0.0 --port=6969 --reload uv run example-mcp-server --mode=stdio uv run example-mcp-server --mode=http_stream --host=0.0.0.0 --port=6969 ``` Options: - `--mode`: Transport mode (sse/stdio/http_stream) - `--host`: Host to bind to - `--port`: Port to listen on - `--reload`: Enable auto-reload for development ### Client Configuration The client uses a configuration class: ```python @dataclass class MCPConfig: mcp_server_url: str = "http://localhost:6969" openai_model: str = "gpt-5-mini" openai_api_key: str = os.getenv("OPENAI_API_KEY") ``` For STDIO transport, additional options are available: ```python @dataclass class MCPConfig: openai_model: str = "gpt-5-mini" openai_api_key: str = os.getenv("OPENAI_API_KEY") mcp_stdio_server_command: str = "uv run example-mcp-server --mode stdio" ``` ## Usage Examples 1. **Start the Server (SSE mode)**: ```bash cd example-mcp-server uv run example-mcp-server --mode=sse ``` 2. **Run the Client**: Using the main launcher with transport selection: ```bash cd example-client uv run python -m example_client.main --transport sse ``` Directly calling the SSE client: ```bash cd example-client uv run python -m example_client.main_sse ``` Directly calling the STDIO client: ```bash cd example-client uv run python -m example_client.main_stdio ``` 3. **Example Queries**: ``` You: What is 2+2? You: Calculate the square root of 144 You: Generate a random number between 1 and 100 ``` ## Best Practices 1. **Development**: - Use STDIO transport for local development - Enable server auto-reload during development - Implement proper error handling 2. **Production**: - Use SSE transport for production deployments - Configure appropriate CORS settings - Implement authentication if needed 3. **Tool Development**: - Follow the Tool interface contract - Provide clear input/output schemas - Include comprehensive documentation ## Extending the Example ### Adding New Tools To add new tools: 1. Create a new tool class implementing the Tool interface 2. Register the tool in the server's tool service 3. The client will automatically discover and use the new tool Example tool structure: ```python class MyNewTool(Tool): name = "my_new_tool" description = "This tool performs a custom operation" input_model = create_model( "MyNewToolInput", param1=(str, Field(..., description="First parameter")), param2=(int, Field(..., description="Second parameter")), __base__=BaseToolInput ) async def execute(self, input_data: BaseToolInput) -> ToolResponse: # Access params with input_data.param1, input_data.param2 result = f"Processed {input_data.param1} with {input_data.param2}" return ToolResponse.from_text(result) ``` Then register the tool in the server: ```python def get_available_tools() -> List[Tool]: return [ # ... existing tools ... MyNewTool(), ] ``` ### Adding New Resources To add new resources: 1. Create a new resource class implementing the Resource interface 2. Register the resource in the server's resource service 3. The client can access the new resource via its URI Example resource structure: ```python class MyNewResource(Resource): name = "my_new_resource" description = "This resource provides custom data" uri = "resource://my_new_resource/{param1}" mime_type = "application/json" input_model = create_model( "MyNewResourceInput", param1=(str, Field(..., description="Resource parameter")), __base__=BaseResourceInput ) async def read(self, input_data: BaseResourceInput) -> ResourceResponse: # Access params with input_data.param1 data = {"message": f"Data for {input_data.param1}"} return ResourceResponse.from_data(data, self.mime_type) ``` Then register the resource in the server: ```python def get_available_resources() -> List[Resource]: return [ # ... existing resources ... MyNewResource(), ] ``` ### Adding New Prompts To add new prompts: 1. Create a new prompt class implementing the Prompt interface 2. Register the prompt in the server's prompt service 3. The client will automatically discover and use the new prompt Example prompt structure: ```python class MyNewPrompt(Prompt): name = "my_new_prompt" description = "This prompt generates a custom response" input_model = create_model( "MyNewPromptInput", param1=(str, Field(..., description="First parameter")), param2=(int, Field(..., description="Second parameter")), __base__=BasePromptInput ) async def generate(self, input_data: BasePromptInput) -> PromptResponse: # Access params with input_data.param1, input_data.param2 result = f"Generated response for {input_data.param1} with {input_data.param2}" return PromptResponse.from_text(result) ``` Then register the prompt in the server: ```python def get_available_prompts() -> List[Prompt]: return [ # ... existing prompts ... MyNewPrompt(), ] ``` --- ### Guides/Cookbook # Cookbook Practical recipes for common Atomic Agents use cases. ## Quick Reference | Recipe | Description | |--------|-------------| | [Basic Chatbot](#basic-chatbot) | Simple conversational agent | | [Chatbot with Memory](#chatbot-with-memory) | Agent that remembers context | | [Custom Output Schema](#custom-output-schema) | Structured responses | | [Multi-Provider Agent](#multi-provider-agent) | Switch between LLM providers | | [Agent with Tools](#agent-with-tools) | Agent using external tools | | [Streaming Chatbot](#streaming-chatbot) | Real-time response streaming | | [Research Agent](#research-agent) | Multi-step research workflow | | [RAG Agent](#rag-agent) | Retrieval-augmented generation | ## Basic Chatbot A minimal chatbot implementation. ```python """ Basic Chatbot Recipe A simple conversational agent that responds to user messages. Requirements: - pip install atomic-agents openai - Set OPENAI_API_KEY environment variable """ import os import instructor import openai from atomic_agents import AtomicAgent, AgentConfig, BasicChatInputSchema, BasicChatOutputSchema from atomic_agents.context import ChatHistory def create_basic_chatbot(): """Create a basic chatbot agent.""" client = instructor.from_openai(openai.OpenAI()) agent = AtomicAgent[BasicChatInputSchema, BasicChatOutputSchema]( config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory() ) ) return agent def chat_loop(agent): """Interactive chat loop.""" print("Chatbot ready! Type 'quit' to exit.\n") while True: user_input = input("You: ").strip() if user_input.lower() in ['quit', 'exit', 'q']: print("Goodbye!") break if not user_input: continue response = agent.run(BasicChatInputSchema(chat_message=user_input)) print(f"Bot: {response.chat_message}\n") if __name__ == "__main__": agent = create_basic_chatbot() chat_loop(agent) ``` ## Chatbot with Memory Agent that maintains conversation history across turns. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Custom Output Schema Agent with structured output including metadata. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Multi-Provider Agent Switch between different LLM providers dynamically. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Agent with Tools Agent that uses tools to extend capabilities. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Streaming Chatbot Real-time streaming responses. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Research Agent Multi-step research workflow. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## RAG Agent Retrieval-augmented generation pattern. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Summary These recipes demonstrate common patterns: | Pattern | Key Components | Use Case | |---------|---------------|----------| | Basic Chatbot | AtomicAgent, ChatHistory | Simple Q&A | | Memory | ChatHistory persistence | Context retention | | Custom Schema | BaseIOSchema subclass | Structured output | | Multi-Provider | Provider switching | Flexibility | | Tools | BaseTool | Extended capabilities | | Streaming | run_async_stream | Real-time UX | | Research | Multiple agents | Complex workflows | | RAG | Context providers | Knowledge-augmented | Combine these patterns to build sophisticated AI applications. --- ### Guides/Deployment # Deployment Guide This guide covers best practices for deploying Atomic Agents applications to production environments. ## Overview Deploying AI agents requires attention to: - **Configuration Management**: Environment-specific settings - **API Key Security**: Secure credential handling - **Scaling**: Handling concurrent requests - **Monitoring**: Observability and alerting - **Error Handling**: Graceful degradation ## Environment Configuration ### Using Environment Variables Store configuration in environment variables: ```python import os from dataclasses import dataclass from typing import Optional @dataclass class AgentDeploymentConfig: """Production configuration for agents.""" # Required openai_api_key: str model: str # Optional with defaults max_tokens: int = 2048 temperature: float = 0.7 timeout: float = 30.0 max_retries: int = 3 @classmethod def from_env(cls) -> "AgentDeploymentConfig": """Load configuration from environment variables.""" api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise ValueError("OPENAI_API_KEY environment variable is required") return cls( openai_api_key=api_key, model=os.getenv("AGENT_MODEL", "gpt-4o-mini"), max_tokens=int(os.getenv("AGENT_MAX_TOKENS", "2048")), temperature=float(os.getenv("AGENT_TEMPERATURE", "0.7")), timeout=float(os.getenv("AGENT_TIMEOUT", "30.0")), max_retries=int(os.getenv("AGENT_MAX_RETRIES", "3")), ) # Usage config = AgentDeploymentConfig.from_env() ``` ### Configuration File Pattern For complex deployments, use configuration files: ```python import os import json from pathlib import Path def load_config(env: str = None) -> dict: """Load environment-specific configuration.""" env = env or os.getenv("DEPLOYMENT_ENV", "development") config_path = Path(f"config/{env}.json") if not config_path.exists(): raise FileNotFoundError(f"Config not found: {config_path}") with open(config_path) as f: config = json.load(f) # Override with environment variables if os.getenv("OPENAI_API_KEY"): config["openai_api_key"] = os.getenv("OPENAI_API_KEY") return config # config/production.json example: # { # "model": "gpt-4o", # "max_tokens": 4096, # "timeout": 60, # "rate_limit": { # "requests_per_minute": 100, # "tokens_per_minute": 100000 # } # } ``` ## Creating Production-Ready Agents ### Agent Factory Pattern Create agents with production configuration: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## FastAPI Integration Deploy agents as REST APIs: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Docker Deployment ### Dockerfile ```dockerfile FROM python:3.12-slim WORKDIR /app # Install uv for faster dependency installation RUN pip install uv # Copy dependency files COPY pyproject.toml uv.lock ./ # Install dependencies RUN uv sync --frozen --no-dev # Copy application code COPY . . # Set environment variables ENV PYTHONUNBUFFERED=1 ENV DEPLOYMENT_ENV=production # Expose port EXPOSE 8000 # Run the application CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] ``` ### Docker Compose ```yaml version: '3.8' services: agent-api: build: . ports: - "8000:8000" environment: - OPENAI_API_KEY=${OPENAI_API_KEY} - AGENT_MODEL=gpt-4o-mini - DEPLOYMENT_ENV=production healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 10s retries: 3 deploy: replicas: 3 resources: limits: memory: 512M redis: image: redis:7-alpine ports: - "6379:6379" ``` ## Rate Limiting Implement rate limiting to control API costs: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Graceful Shutdown Handle shutdown signals properly: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Health Checks Implement comprehensive health checks: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Best Practices Summary | Area | Recommendation | |------|----------------| | Configuration | Use environment variables, never hardcode secrets | | API Keys | Store in secrets manager (AWS Secrets Manager, Vault) | | Scaling | Use async clients, implement connection pooling | | Monitoring | Add health checks, log request/response metrics | | Error Handling | Implement retries, circuit breakers, fallbacks | | Rate Limiting | Respect API limits, implement client-side limiting | | Shutdown | Handle signals, drain connections gracefully | ## Deployment Checklist - [ ] Environment variables configured - [ ] API keys stored securely - [ ] Health check endpoint implemented - [ ] Rate limiting configured - [ ] Error handling and retries implemented - [ ] Logging and monitoring set up - [ ] Graceful shutdown handling - [ ] Docker/container configuration - [ ] Load balancing configured (if scaling) - [ ] Backup/fallback providers configured --- ### Guides/Error Handling # Error Handling Guide This guide covers best practices for handling errors in Atomic Agents applications, including validation errors, API failures, and custom error handling patterns. ## Overview Atomic Agents provides multiple layers of error handling: 1. **Schema Validation** - Pydantic validates input/output at runtime 2. **API Error Handling** - Handle LLM provider errors gracefully 3. **Hook System** - Monitor and respond to errors via hooks 4. **Custom Exception Handling** - Build robust error recovery patterns ## Schema Validation Errors Pydantic schemas catch invalid data before it reaches the LLM. ### Basic Validation ```python import os from typing import List from pydantic import Field, field_validator import instructor import openai from atomic_agents import AtomicAgent, AgentConfig, BaseIOSchema from atomic_agents.context import ChatHistory class ValidatedInputSchema(BaseIOSchema): """Input schema with validation rules.""" query: str = Field(..., description="User query", min_length=1, max_length=1000) max_results: int = Field(default=10, ge=1, le=100, description="Maximum results to return") @field_validator('query') @classmethod def query_not_empty(cls, v: str) -> str: if not v.strip(): raise ValueError("Query cannot be empty or whitespace only") return v.strip() class ValidatedOutputSchema(BaseIOSchema): """Output schema with validation.""" answer: str = Field(..., description="The response") confidence: float = Field(..., ge=0.0, le=1.0, description="Confidence score 0-1") sources: List[str] = Field(default_factory=list, description="Source references") # Initialize client and agent client = instructor.from_openai(openai.OpenAI()) agent = AtomicAgent[ValidatedInputSchema, ValidatedOutputSchema]( config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory() ) ) # Handle validation errors try: response = agent.run(ValidatedInputSchema(query="", max_results=5)) except ValueError as e: print(f"Validation error: {e}") ``` ### Custom Validators ```python from pydantic import Field, field_validator, model_validator from typing import Optional from atomic_agents import BaseIOSchema class SearchInputSchema(BaseIOSchema): """Search input with complex validation.""" query: str = Field(..., description="Search query") category: Optional[str] = Field(None, description="Category filter") date_from: Optional[str] = Field(None, description="Start date YYYY-MM-DD") date_to: Optional[str] = Field(None, description="End date YYYY-MM-DD") @field_validator('category') @classmethod def validate_category(cls, v: Optional[str]) -> Optional[str]: valid_categories = ['technology', 'science', 'business', 'health'] if v is not None and v.lower() not in valid_categories: raise ValueError(f"Category must be one of: {valid_categories}") return v.lower() if v else None @model_validator(mode='after') def validate_dates(self): if self.date_from and self.date_to: if self.date_from > self.date_to: raise ValueError("date_from must be before date_to") return self ``` ## API Error Handling Handle LLM provider errors gracefully with retry logic. ### Basic Retry Pattern ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Using the Hook System for Error Handling The Atomic Agents hook system provides powerful error monitoring capabilities. ### Error Logging Hook ```python import os import logging from datetime import datetime from typing import Any, Optional import instructor import openai from atomic_agents import AtomicAgent, AgentConfig, BasicChatInputSchema, BasicChatOutputSchema from atomic_agents.context import ChatHistory # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) def on_error_hook(error: Exception, context: dict) -> None: """Hook called when an error occurs during agent execution.""" logger.error(f"Agent error: {type(error).__name__}: {error}") logger.error(f"Context: {context}") def on_completion_hook(response: Any, duration_ms: float) -> None: """Hook called on successful completion.""" logger.info(f"Agent completed in {duration_ms:.2f}ms") # Create agent with hooks using Instructor's hook system client = instructor.from_openai(openai.OpenAI()) # Register hooks with the instructor client client.on("completion", lambda *args: on_completion_hook(*args)) agent = AtomicAgent[BasicChatInputSchema, BasicChatOutputSchema]( config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory() ) ) ``` ### Comprehensive Error Handler ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Graceful Degradation Implement fallback behavior when the primary agent fails. ### Fallback Agent Pattern ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Best Practices ### 1. Always Validate Input ```python from pydantic import Field, field_validator from atomic_agents import BaseIOSchema class SafeInputSchema(BaseIOSchema): """Input schema with comprehensive validation.""" message: str = Field(..., min_length=1, max_length=10000) @field_validator('message') @classmethod def sanitize_message(cls, v: str) -> str: # Remove potential prompt injection attempts dangerous_patterns = ['ignore previous', 'disregard instructions'] for pattern in dangerous_patterns: if pattern.lower() in v.lower(): raise ValueError("Invalid input detected") return v.strip() ``` ### 2. Log All Errors ```python import logging from functools import wraps logger = logging.getLogger(__name__) def log_errors(func): """Decorator to log all errors from agent operations.""" @wraps(func) def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: logger.exception(f"Error in {func.__name__}: {e}") raise return wrapper ``` ### 3. Set Timeouts ```python import os import instructor import openai from atomic_agents import AtomicAgent, AgentConfig from atomic_agents.context import ChatHistory # Configure timeout at client level client = instructor.from_openai( openai.OpenAI(timeout=30.0) # 30 second timeout ) agent = AtomicAgent( config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory(), model_api_parameters={ "max_tokens": 500 # Limit response length } ) ) ``` ### 4. Implement Circuit Breaker ```python import time from typing import Optional, Callable from dataclasses import dataclass @dataclass class CircuitBreaker: """Simple circuit breaker for agent calls.""" failure_threshold: int = 5 reset_timeout: float = 60.0 _failure_count: int = 0 _last_failure_time: float = 0 _state: str = "closed" # closed, open, half-open def call(self, func: Callable, *args, **kwargs): """Execute function with circuit breaker protection.""" if self._state == "open": if time.time() - self._last_failure_time > self.reset_timeout: self._state = "half-open" else: raise Exception("Circuit breaker is open") try: result = func(*args, **kwargs) self._on_success() return result except Exception as e: self._on_failure() raise def _on_success(self): self._failure_count = 0 self._state = "closed" def _on_failure(self): self._failure_count += 1 self._last_failure_time = time.time() if self._failure_count >= self.failure_threshold: self._state = "open" # Usage circuit_breaker = CircuitBreaker(failure_threshold=3, reset_timeout=30.0) def safe_agent_call(agent, input_data): return circuit_breaker.call(agent.run, input_data) ``` ## Summary Key error handling strategies in Atomic Agents: | Strategy | Use Case | Implementation | |----------|----------|----------------| | Schema Validation | Prevent invalid inputs | Pydantic validators | | Retry Logic | Transient failures | Exponential backoff | | Hook System | Monitoring & logging | Instructor hooks | | Fallback Chain | High availability | Multiple agents | | Circuit Breaker | Prevent cascade failures | State machine | Always combine multiple strategies for robust production applications. --- ### Guides/Faq # Frequently Asked Questions Common questions and answers about using Atomic Agents. ## Installation & Setup ### How do I install Atomic Agents? Install using pip: ```bash pip install atomic-agents ``` Or using uv (recommended): ```bash uv add atomic-agents ``` You also need to install your LLM provider. OpenAI is included by default. For other providers, use instructor extras: ```bash # For Anthropic pip install instructor[anthropic] # For Groq pip install instructor[groq] # For Gemini pip install instructor[google-genai] ``` ### What Python version is required? Atomic Agents requires **Python 3.12 or higher**. ```bash # Check your Python version python --version ``` ### How do I set up my API key? Set your API key as an environment variable: ```bash # OpenAI export OPENAI_API_KEY="your-api-key" # Anthropic export ANTHROPIC_API_KEY="your-api-key" # Or use a .env file with python-dotenv ``` In your code: ```python import os from dotenv import load_dotenv load_dotenv() # Load from .env file # Keys are read from environment api_key = os.getenv("OPENAI_API_KEY") ``` ## Agent Configuration ### How do I create a basic agent? ```python import instructor import openai from atomic_agents import AtomicAgent, AgentConfig, BasicChatInputSchema, BasicChatOutputSchema from atomic_agents.context import ChatHistory # Create instructor client client = instructor.from_openai(openai.OpenAI()) # Create agent agent = AtomicAgent[BasicChatInputSchema, BasicChatOutputSchema]( config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory() ) ) # Use the agent response = agent.run(BasicChatInputSchema(chat_message="Hello!")) print(response.chat_message) ``` ### How do I use different LLM providers? Atomic Agents works with any provider supported by Instructor: **OpenAI:** ```python import instructor import openai client = instructor.from_openai(openai.OpenAI()) ``` **Anthropic:** ```python import instructor from anthropic import Anthropic client = instructor.from_anthropic(Anthropic()) ``` **Groq:** ```python import instructor from groq import Groq client = instructor.from_groq(Groq(), mode=instructor.Mode.JSON) ``` **Ollama (local models):** ```python import instructor from openai import OpenAI client = instructor.from_openai( OpenAI( base_url="http://localhost:11434/v1", api_key="ollama" ), mode=instructor.Mode.JSON ) ``` **Google Gemini:** ```python import instructor from openai import OpenAI import os client = instructor.from_openai( OpenAI( api_key=os.getenv("GEMINI_API_KEY"), base_url="https://generativelanguage.googleapis.com/v1beta/openai/" ), mode=instructor.Mode.JSON ) ``` ### How do I customize the system prompt? Use `SystemPromptGenerator` to define agent behavior: ```python from atomic_agents.context import SystemPromptGenerator system_prompt = SystemPromptGenerator( background=[ "You are a helpful coding assistant.", "You specialize in Python programming." ], steps=[ "Analyze the user's question.", "Provide clear, working code examples.", "Explain the code step by step." ], output_instructions=[ "Always include code examples.", "Use markdown formatting.", "Keep explanations concise." ] ) agent = AtomicAgent[BasicChatInputSchema, BasicChatOutputSchema]( config=AgentConfig( client=client, model="gpt-5-mini", system_prompt_generator=system_prompt ) ) ``` ### How do I add memory/conversation history? Use `ChatHistory` to maintain conversation context: ```python from atomic_agents.context import ChatHistory # Create history history = ChatHistory() # Create agent with history agent = AtomicAgent[BasicChatInputSchema, BasicChatOutputSchema]( config=AgentConfig( client=client, model="gpt-5-mini", history=history ) ) # Conversation is automatically maintained agent.run(BasicChatInputSchema(chat_message="My name is Alice")) agent.run(BasicChatInputSchema(chat_message="What's my name?")) # Will remember "Alice" # Reset history when needed agent.reset_history() ``` ## Custom Schemas ### How do I create custom input/output schemas? Inherit from `BaseIOSchema`: ```python from typing import List, Optional from pydantic import Field from atomic_agents import BaseIOSchema class CustomInputSchema(BaseIOSchema): """Custom input with additional fields.""" question: str = Field(..., description="The user's question") context: Optional[str] = Field(None, description="Additional context") max_length: int = Field(default=500, description="Max response length") class CustomOutputSchema(BaseIOSchema): """Custom output with structured data.""" answer: str = Field(..., description="The answer to the question") confidence: float = Field(..., ge=0.0, le=1.0, description="Confidence score") sources: List[str] = Field(default_factory=list, description="Source references") follow_up_questions: List[str] = Field(default_factory=list, description="Suggested follow-ups") # Use with agent agent = AtomicAgent[CustomInputSchema, CustomOutputSchema]( config=AgentConfig(client=client, model="gpt-5-mini") ) response = agent.run(CustomInputSchema(question="What is Python?")) print(f"Answer: {response.answer}") print(f"Confidence: {response.confidence}") ``` ### How do I add validation to schemas? Use Pydantic validators: ```python from pydantic import Field, field_validator, model_validator from atomic_agents import BaseIOSchema class ValidatedInputSchema(BaseIOSchema): """Input with validation rules.""" query: str = Field(..., min_length=1, max_length=1000) category: str = Field(...) @field_validator('category') @classmethod def validate_category(cls, v: str) -> str: valid = ['tech', 'science', 'business'] if v.lower() not in valid: raise ValueError(f"Category must be one of: {valid}") return v.lower() @field_validator('query') @classmethod def sanitize_query(cls, v: str) -> str: return v.strip() @model_validator(mode='after') def validate_combination(self): # Cross-field validation if self.category == 'tech' and len(self.query) < 10: raise ValueError("Tech queries must be at least 10 characters") return self ``` ## Tools ### How do I create a custom tool? Inherit from `BaseTool`: ```python import os from pydantic import Field from atomic_agents import BaseTool, BaseToolConfig, BaseIOSchema class WeatherInputSchema(BaseIOSchema): """Input for weather tool.""" city: str = Field(..., description="City name to get weather for") class WeatherOutputSchema(BaseIOSchema): """Output from weather tool.""" temperature: float = Field(..., description="Temperature in Celsius") condition: str = Field(..., description="Weather condition") humidity: int = Field(..., description="Humidity percentage") class WeatherToolConfig(BaseToolConfig): """Configuration for weather tool.""" api_key: str = Field(default_factory=lambda: os.getenv("WEATHER_API_KEY")) class WeatherTool(BaseTool[WeatherInputSchema, WeatherOutputSchema]): """Tool to fetch current weather.""" def __init__(self, config: WeatherToolConfig = None): super().__init__(config or WeatherToolConfig()) self.api_key = self.config.api_key def run(self, params: WeatherInputSchema) -> WeatherOutputSchema: # Implement your tool logic here # This is a mock implementation return WeatherOutputSchema( temperature=22.5, condition="Sunny", humidity=45 ) # Use the tool tool = WeatherTool() result = tool.run(WeatherInputSchema(city="London")) print(f"Temperature: {result.temperature}°C") ``` ### How do I use the built-in tools? Use the Atomic Assembler CLI to download tools: ```bash atomic ``` Then import and use them: ```python from calculator.tool.calculator import CalculatorTool, CalculatorInputSchema calculator = CalculatorTool() result = calculator.run(CalculatorInputSchema(expression="2 + 2 * 3")) print(result.value) # 8.0 ``` ## Streaming & Async ### How do I stream responses? Use `run_stream()` for synchronous streaming: ```python from atomic_agents import AtomicAgent, AgentConfig, BasicChatInputSchema, BasicChatOutputSchema # Synchronous streaming for partial in agent.run_stream(BasicChatInputSchema(chat_message="Write a poem")): print(partial.chat_message, end='', flush=True) print() # Newline at end ``` ### How do I use async methods? Use `run_async()` for async operations: ```python import asyncio from openai import AsyncOpenAI import instructor from atomic_agents import AtomicAgent, AgentConfig, BasicChatInputSchema, BasicChatOutputSchema from atomic_agents.context import ChatHistory async def main(): # Use async client client = instructor.from_openai(AsyncOpenAI()) agent = AtomicAgent[BasicChatInputSchema, BasicChatOutputSchema]( config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory() ) ) # Non-streaming async response = await agent.run_async(BasicChatInputSchema(chat_message="Hello")) print(response.chat_message) # Streaming async async for partial in agent.run_async_stream(BasicChatInputSchema(chat_message="Write a story")): print(partial.chat_message, end='', flush=True) asyncio.run(main()) ``` ## Context Providers ### How do I inject dynamic context? Create a custom context provider: ```python from typing import List from atomic_agents.context import BaseDynamicContextProvider class SearchResultsProvider(BaseDynamicContextProvider): """Provides search results as context.""" def __init__(self, title: str = "Search Results"): super().__init__(title=title) self.results: List[str] = [] def add_result(self, result: str): self.results.append(result) def clear(self): self.results = [] def get_info(self) -> str: if not self.results: return "No search results available." return "\n".join(f"- {r}" for r in self.results) # Register with agent provider = SearchResultsProvider() provider.add_result("Python is a programming language") provider.add_result("Python was created by Guido van Rossum") agent.register_context_provider("search_results", provider) # The context is now included in the system prompt response = agent.run(BasicChatInputSchema(chat_message="Tell me about Python")) ``` ## Common Issues ### Why am I getting validation errors? Check that your input matches the schema: ```python from pydantic import ValidationError try: response = agent.run(BasicChatInputSchema(chat_message="")) except ValidationError as e: print("Validation errors:") for error in e.errors(): print(f" {error['loc']}: {error['msg']}") ``` ### How do I handle API rate limits? Implement retry logic: ```python import time from openai import RateLimitError def run_with_retry(agent, input_data, max_retries=3): for attempt in range(max_retries): try: return agent.run(input_data) except RateLimitError: if attempt < max_retries - 1: wait = 2 ** attempt # Exponential backoff print(f"Rate limited. Waiting {wait}s...") time.sleep(wait) else: raise ``` ### How do I debug agent behavior? 1. **Check the system prompt:** ```python print(agent.system_prompt_generator.generate_prompt()) ``` 2. **Inspect history:** ```python for msg in agent.history.get_history(): print(f"{msg['role']}: {msg['content']}") ``` 3. **Enable logging:** ```python import logging logging.basicConfig(level=logging.DEBUG) ``` ## MCP Integration ### How do I connect to an MCP server? ```python from atomic_agents.connectors.mcp import fetch_mcp_tools_async, MCPTransportType async def setup_mcp_tools(): tools = await fetch_mcp_tools_async( server_url="http://localhost:8000", transport_type=MCPTransportType.HTTP_STREAM ) return tools # Use tools with your agent tools = asyncio.run(setup_mcp_tools()) ``` ## Migration ### How do I upgrade from v1.x to v2.0? Key changes: 1. **Import paths:** ```python # Old from atomic_agents.lib.base.base_io_schema import BaseIOSchema # New from atomic_agents import BaseIOSchema ``` 2. **Class names:** ```python # Old from atomic_agents.agents.base_agent import BaseAgent, BaseAgentConfig # New from atomic_agents import AtomicAgent, AgentConfig ``` 3. **Schemas as type parameters:** ```python # Old agent = BaseAgent(BaseAgentConfig( client=client, model="gpt-5-mini", input_schema=MyInput, output_schema=MyOutput )) # New agent = AtomicAgent[MyInput, MyOutput]( AgentConfig(client=client, model="gpt-5-mini") ) ``` See the [Upgrade Guide](../UPGRADE_DOC.md) for complete migration instructions. --- ### Guides/Hooks # Hooks Guide This guide covers the hook system in Atomic Agents, enabling comprehensive monitoring, error handling, and intelligent retry mechanisms. ## Overview The Atomic Agents hook system integrates with Instructor's event system to provide: - **Comprehensive Monitoring**: Track all aspects of agent execution - **Robust Error Handling**: Graceful handling of validation and completion errors - **Intelligent Retry Patterns**: Implement smart retry logic based on error context - **Performance Metrics**: Monitor response times, success rates, and error patterns - **Zero Overhead**: Hooks only execute when registered and enabled ## Supported Hook Events | Event | Description | When Triggered | |-------|-------------|----------------| | `parse:error` | Pydantic validation failures | When LLM output doesn't match schema | | `completion:kwargs` | Before API calls | Just before sending request to LLM | | `completion:response` | After API responses | When LLM returns a response | | `completion:error` | API or network errors | On connection failures, timeouts, etc. | ## Basic Hook Registration Register hooks using the `register_hook` method on any `AtomicAgent`: ```python import os import instructor import openai from atomic_agents import AtomicAgent, AgentConfig, BasicChatInputSchema, BasicChatOutputSchema from atomic_agents.context import ChatHistory def on_parse_error(error): """Handle validation errors.""" print(f"Validation failed: {error}") def on_completion_kwargs(**kwargs): """Log API call details before request.""" model = kwargs.get("model", "unknown") print(f"Calling model: {model}") def on_completion_response(response, **kwargs): """Process successful responses.""" if hasattr(response, "usage"): print(f"Tokens used: {response.usage.total_tokens}") def on_completion_error(error, **kwargs): """Handle API errors.""" print(f"API error: {type(error).__name__}: {error}") # Create agent client = instructor.from_openai(openai.OpenAI()) agent = AtomicAgent[BasicChatInputSchema, BasicChatOutputSchema]( config=AgentConfig( client=client, model="gpt-4o-mini", history=ChatHistory() ) ) # Register hooks agent.register_hook("parse:error", on_parse_error) agent.register_hook("completion:kwargs", on_completion_kwargs) agent.register_hook("completion:response", on_completion_response) agent.register_hook("completion:error", on_completion_error) # Use the agent normally - hooks are called automatically response = agent.run(BasicChatInputSchema(chat_message="Hello!")) ``` ## Performance Monitoring Track request metrics for performance analysis: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Detailed Validation Error Handling Extract detailed information from validation errors: ```python from pydantic import ValidationError def detailed_parse_error_handler(error): """Extract detailed validation error information.""" if isinstance(error, ValidationError): print("Validation Error Details:") for err in error.errors(): # Get field path (e.g., "confidence" or "nested.field") field_path = " -> ".join(str(x) for x in err["loc"]) error_type = err["type"] message = err["msg"] print(f" Field: {field_path}") print(f" Type: {error_type}") print(f" Message: {message}") # Access input value if available if "input" in err: print(f" Invalid Value: {err['input']}") else: print(f"Parse Error: {error}") agent.register_hook("parse:error", detailed_parse_error_handler) ``` ## Retry Strategies with Hooks Implement intelligent retry logic based on error context: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Managing Hooks ### Enable/Disable Hooks Temporarily disable hooks without unregistering: ```python # Disable all hooks agent.disable_hooks() # Run without hook overhead response = agent.run(input_data) # Re-enable hooks agent.enable_hooks() # Check if hooks are enabled if agent.hooks_enabled(): print("Hooks are active") ``` ### Unregister Hooks Remove specific hooks or clear all: ```python # Unregister a specific hook agent.unregister_hook("parse:error", on_parse_error) # Clear all hooks agent.clear_hooks() ``` ## Production Logging Pattern A complete production-ready logging setup: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Best Practices ### 1. Keep Hooks Lightweight Hooks run synchronously - avoid heavy operations: ```python # Good: Quick logging def on_response(response, **kwargs): logger.info(f"Response received") # Avoid: Heavy processing in hooks def on_response_slow(response, **kwargs): # Don't do this - blocks the response save_to_database(response) send_to_analytics(response) generate_report(response) ``` ### 2. Handle Hook Exceptions Wrap hook logic to prevent failures from disrupting the agent: ```python def safe_hook(func): """Decorator to catch hook exceptions.""" @wraps(func) def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: logger.error(f"Hook error in {func.__name__}: {e}") return wrapper @safe_hook def on_completion_response(response, **kwargs): # If this fails, the agent continues working process_response(response) ``` ### 3. Use Hooks for Cross-Cutting Concerns Hooks are ideal for: - Logging and monitoring - Metrics collection - Error tracking - Performance profiling - Audit trails ### 4. Don't Modify Responses in Hooks Hooks are for observation, not transformation: ```python # Good: Observe and log def on_response(response, **kwargs): logger.info(f"Got response: {response}") # Avoid: Trying to modify response def on_response_bad(response, **kwargs): response.chat_message = "Modified" # Don't do this ``` ## Summary | Feature | Method | Description | |---------|--------|-------------| | Register hook | `agent.register_hook(event, callback)` | Add a hook callback | | Unregister hook | `agent.unregister_hook(event, callback)` | Remove specific hook | | Clear all hooks | `agent.clear_hooks()` | Remove all hooks | | Enable hooks | `agent.enable_hooks()` | Activate hook system | | Disable hooks | `agent.disable_hooks()` | Deactivate hook system | | Check status | `agent.hooks_enabled()` | Check if hooks active | Use hooks to add monitoring and error handling to your agents without modifying core business logic. --- ### Guides/Index # User Guide This section contains detailed guides for working with Atomic Agents. ```{toctree} :maxdepth: 2 :caption: Guides quickstart memory tools hooks orchestration cookbook error-handling testing deployment performance security logging faq ``` ## Implementation Patterns The framework supports various implementation patterns and use cases: ### Chatbots and Assistants - Basic chat interfaces with any LLM provider - Streaming responses - Custom response schemas - Suggested follow-up questions - History management and context retention - Multi-turn conversations ### RAG Systems - Query generation and optimization - Context-aware responses - Document Q&A with source tracking - Information synthesis and summarization - Custom embedding and retrieval strategies - Hybrid search approaches ### Specialized Agents - YouTube video summarization and analysis - Web search and deep research - Recipe generation from various sources - Multimodal interactions (text, images, etc.) - Custom tool integration - Custom MCP integration to support tools, resources, and prompts - Task orchestration ## Provider Integration Guide Atomic Agents is designed to be provider-agnostic. Here's how to work with different providers: ### Provider Selection - Choose any provider supported by Instructor - Configure provider-specific settings - Handle rate limits and quotas - Implement fallback strategies ### Local Development - Use Ollama for local testing - Mock responses for development - Debug provider interactions - Test provider switching ### Production Deployment - Load balancing between providers - Failover configurations - Cost optimization strategies - Performance monitoring ### Custom Provider Integration - Extend Instructor for new providers - Implement custom client wrappers - Add provider-specific features - Handle unique response formats ## Best Practices ### Error Handling - Implement proper exception handling - Add retry mechanisms - Log provider errors - Handle rate limits gracefully ### Performance Optimization - Use streaming for long responses - Implement caching strategies - Optimize prompt lengths - Batch operations when possible ### Security - Secure API key management - Input validation and sanitization - Output filtering - Rate limiting and quotas ## Getting Help If you need help, you can: 1. Check our [GitHub Issues](https://github.com/eigenwise/atomic-agents/issues) 2. Join our [Reddit community](https://www.reddit.com/r/AtomicAgents/) 3. Read through our examples in the repository 4. Review the example projects in `atomic-examples/` **See also**: - [API Reference](/api/index) - Browse the API reference - [Main Documentation](/index) - Return to main documentation --- ### Guides/Logging # Logging and Monitoring Guide This guide covers logging, monitoring, and observability best practices for Atomic Agents applications. ## Overview Effective logging and monitoring enables: - **Debugging**: Trace issues in agent behavior - **Performance Tracking**: Identify bottlenecks - **Cost Monitoring**: Track API usage and costs - **Alerting**: Detect anomalies and failures - **Auditing**: Maintain records for compliance ## Basic Logging Setup ### Configure Python Logging Set up structured logging for agents: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Agent Logging with Hooks ### Comprehensive Request Logging Use hooks to log all agent interactions: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Metrics Collection ### Token and Cost Tracking Track API usage and costs: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Monitoring Dashboard ### FastAPI Metrics Endpoint Expose metrics via HTTP: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Distributed Tracing ### OpenTelemetry Integration Add distributed tracing for complex systems: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Alerting ### Alert Conditions Define alert conditions for monitoring: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Log Analysis Patterns ### Structured Log Queries Design logs for easy querying: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Best Practices ### Logging Guidelines | What to Log | Why | Example | |-------------|-----|---------| | Request IDs | Trace requests | `request_id: "abc123"` | | Timestamps | Timeline analysis | `timestamp: "2024-01-15T10:30:00Z"` | | Model used | Cost attribution | `model: "gpt-4o-mini"` | | Token counts | Usage tracking | `tokens: {"prompt": 100, "completion": 50}` | | Latency | Performance monitoring | `duration_ms: 1523` | | Error types | Debugging | `error_type: "ValidationError"` | | User IDs | Audit trails | `user_id: "user456"` | ### What NOT to Log - Full request/response content (privacy) - API keys or secrets - Personal identifiable information (PII) - Sensitive business data ## Summary | Component | Purpose | Tools | |-----------|---------|-------| | Logging | Debug & audit | Python logging, structured JSON | | Metrics | Performance tracking | Custom collectors, Prometheus | | Tracing | Request flow | OpenTelemetry, Jaeger | | Alerting | Issue detection | Custom rules, PagerDuty | | Dashboards | Visualization | Grafana, custom endpoints | Implement logging and monitoring from the start - it's much harder to add later. --- ### Guides/Memory # Memory and Context Management This guide covers everything you need to know about managing conversation memory and dynamic context in Atomic Agents. Whether you're building a simple chatbot or orchestrating complex multi-agent systems, understanding memory management is essential. ```{contents} :local: :depth: 2 ``` ## Introduction ### What You'll Learn - How conversation history works in Atomic Agents - What "turns" are and how they're tracked - How messages are automatically managed during agent execution - How to persist and restore conversation state - How to use context providers for dynamic information injection - Advanced multi-agent memory patterns ### Prerequisites - Basic familiarity with Atomic Agents ([Quickstart Guide](quickstart.md)) - Understanding of Python classes and async/await ### The Problem This Solves A common question from developers (see [GitHub Issue #58](https://github.com/eigenwise/atomic-agents/issues/58)): > "In most of the examples only the initial message is added, not any subsequent runs. Is this automatic?" **Yes, it is automatic!** When you call `agent.run(user_input)`, the framework automatically: 1. Adds your input to the conversation history 2. Sends the full history to the LLM 3. Adds the LLM's response to history This guide explains exactly how this works and how to leverage it for complex use cases. --- ## Understanding Memory in Atomic Agents ### The Conversation Model Atomic Agents uses a **turn-based conversation model** where each interaction between user and assistant forms a "turn". The `ChatHistory` class manages this conversation state. ```{mermaid} flowchart LR subgraph Turn1["Turn 1 (turn_id: abc-123)"] U1[User Message] A1[Assistant Response] end subgraph Turn2["Turn 2 (turn_id: def-456)"] U2[User Message] A2[Assistant Response] end subgraph Turn3["Turn 3 (turn_id: ghi-789)"] U3[User Message] A3[Assistant Response] end U1 --> A1 A1 -.-> U2 U2 --> A2 A2 -.-> U3 U3 --> A3 ``` **Key Concepts:** - **Message**: A single piece of content with a role (user, assistant, system) - **Turn**: A logical grouping of related messages (typically user input + assistant response) - **Turn ID**: A UUID that links messages belonging to the same turn - **History**: The complete sequence of messages in a conversation ### Messages and Turns Each message in the history has three components: ```python from atomic_agents.context import Message # Message structure message = Message( role="user", # "user", "assistant", or "system" content=some_schema, # Must be a BaseIOSchema instance turn_id="abc-123" # UUID linking related messages ) ``` **Why Turn IDs Matter:** - Group related messages together - Enable deletion of complete turns (user message + response) - Track conversation flow for debugging - Support conversation branching patterns --- ## ChatHistory Fundamentals ### Creating and Configuring History ```python from atomic_agents.context import ChatHistory # Basic history (unlimited messages) history = ChatHistory() # History with message limit (oldest messages removed when exceeded) history = ChatHistory(max_messages=50) ``` ### Using History with an Agent ```python import instructor import openai from atomic_agents import AtomicAgent, AgentConfig, BaseIOSchema from atomic_agents.context import ChatHistory from pydantic import Field # Define schemas class ChatInput(BaseIOSchema): """User chat message""" message: str = Field(..., description="The user's message") class ChatOutput(BaseIOSchema): """Assistant response""" response: str = Field(..., description="The assistant's response") # Create history and agent history = ChatHistory(max_messages=100) client = instructor.from_openai(openai.OpenAI()) agent = AtomicAgent[ChatInput, ChatOutput]( config=AgentConfig( client=client, model="gpt-5-mini", history=history, ) ) # Each run automatically manages history response1 = agent.run(ChatInput(message="Hello!")) response2 = agent.run(ChatInput(message="What did I just say?")) # The agent remembers the previous message! ``` ### The Turn Lifecycle ```{mermaid} stateDiagram-v2 [*] --> NoTurn: ChatHistory created NoTurn --> ActiveTurn: initialize_turn() called NoTurn --> ActiveTurn: add_message() called ActiveTurn --> ActiveTurn: add_message() same turn ActiveTurn --> NewTurn: initialize_turn() called NewTurn --> ActiveTurn: Generates new UUID ActiveTurn --> NoTurn: All turns deleted note right of ActiveTurn: current_turn_id = UUID note right of NoTurn: current_turn_id = None ``` **Turn Lifecycle Methods:** ```python # Initialize a new turn (generates new UUID) history.initialize_turn() # Get the current turn ID turn_id = history.get_current_turn_id() print(f"Current turn: {turn_id}") # e.g., "abc-123-def-456" # Add a message to the current turn history.add_message("user", ChatInput(message="Hello")) # Messages added without initialize_turn() use the existing turn # or auto-initialize if no turn exists ``` --- ## Automatic Memory Management This section addresses the core question from GitHub Issue #58: **How does automatic message management work?** ### How .run() Manages Memory When you call `agent.run(user_input)`, here's exactly what happens: ```{mermaid} flowchart TD A["agent.run(user_input)"] --> B{user_input
provided?} B -->|Yes| C["history.initialize_turn()
Creates new UUID"] C --> D["history.add_message('user', user_input)
Stores user message"] B -->|No| E["Skip turn initialization
Use existing history"] D --> F["_prepare_messages()
Build message list"] E --> F F --> G["System prompt + history"] G --> H["LLM API call"] H --> I["Receive response"] I --> J["history.add_message('assistant', response)
Stores response"] J --> K["_manage_overflow()
Trim if needed"] K --> L["Return response"] style C fill:#e1f5fe style D fill:#e1f5fe style J fill:#e1f5fe ``` ### Step-by-Step Trace Let's trace through a complete conversation: ```python from atomic_agents import AtomicAgent, AgentConfig, BaseIOSchema from atomic_agents.context import ChatHistory from pydantic import Field class Input(BaseIOSchema): """Input""" text: str = Field(...) class Output(BaseIOSchema): """Output""" reply: str = Field(...) # Create agent with history history = ChatHistory() agent = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=history )) # --- TURN 1 --- print(f"Before run: {history.get_message_count()} messages") # 0 messages response1 = agent.run(Input(text="Hi, my name is Alice")) # Internally: # 1. history.initialize_turn() -> turn_id = "abc-123" # 2. history.add_message("user", Input(text="Hi...")) # 3. LLM called with history # 4. history.add_message("assistant", Output(reply="Hello Alice!")) print(f"After run 1: {history.get_message_count()} messages") # 2 messages print(f"Turn ID: {history.get_current_turn_id()}") # "abc-123" # --- TURN 2 --- response2 = agent.run(Input(text="What's my name?")) # Internally: # 1. history.initialize_turn() -> turn_id = "def-456" (NEW turn) # 2. history.add_message("user", Input(text="What's...")) # 3. LLM called with FULL history (all 4 messages) # 4. history.add_message("assistant", Output(reply="Your name is Alice!")) print(f"After run 2: {history.get_message_count()} messages") # 4 messages print(f"Turn ID: {history.get_current_turn_id()}") # "def-456" ``` ### Running Without Input You can call `.run()` without input to continue within the same turn: ```python # First call with input - starts new turn response = agent.run(Input(text="Start a story")) # Subsequent call without input - same turn continues # Useful for: tool follow-ups, multi-step reasoning continuation = agent.run() # No new turn created, uses existing history ``` ### Streaming and Async Behavior All execution methods handle memory the same way: | Method | Memory Behavior | |--------|-----------------| | `agent.run(input)` | Automatic turn init + message add | | `agent.run_stream(input)` | Same as run(), streams response | | `agent.run_async(input)` | Same as run(), async execution | | `agent.run_async_stream(input)` | Same as run(), async + streaming | ```python # Streaming example - memory works identically async for chunk in agent.run_async_stream(Input(text="Hello")): print(chunk.reply, end="", flush=True) # History is updated with complete response after stream finishes ``` --- ## History Persistence and Management ### Serialization: Saving Conversations Save conversation history to disk or database: ```python from atomic_agents.context import ChatHistory # ... after some conversation ... # Serialize to JSON string serialized = history.dump() # Save to file with open("conversation.json", "w") as f: f.write(serialized) # Save to database db.save_conversation(user_id=123, data=serialized) ``` ### Deserialization: Restoring Conversations ```python # Load from file with open("conversation.json", "r") as f: serialized = f.read() # Create new history and load history = ChatHistory() history.load(serialized) # Use with agent agent = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=history, # Restored history! )) # Continue the conversation where it left off response = agent.run(Input(text="Where were we?")) ``` ```{warning} Only load serialized data from trusted sources. The `load()` method reconstructs Python classes from the serialized data. ``` ### Overflow Management Control memory usage with `max_messages`: ```python # Keep only last 20 messages history = ChatHistory(max_messages=20) # When 21st message is added, oldest message is removed # This is FIFO (First In, First Out) - oldest messages go first ``` **Strategy for Long Conversations:** ```python # Option 1: Simple limit history = ChatHistory(max_messages=50) # Option 2: Monitor and handle manually if history.get_message_count() > 40: # Maybe summarize old messages before they're lost old_messages = history.get_history()[:10] summary = summarize_messages(old_messages) # Store summary in context provider instead ``` ### History Manipulation **Copying History:** ```python # Create independent copy (deep copy) history_copy = history.copy() # Modifications don't affect original history_copy.add_message("user", Input(text="This only goes in copy")) ``` **Deleting Turns:** ```python # Get the turn ID you want to delete turn_id = history.get_current_turn_id() # Delete all messages with that turn ID history.delete_turn_id(turn_id) # Useful for: removing failed attempts, undo functionality ``` **Resetting History:** ```python # Clear all messages, start fresh agent.reset_history() # or history = ChatHistory() # Create new instance ``` --- ## Writing a Custom Memory Backend Memory is intentionally kept outside the framework core. `ChatHistory` is a solid in-memory default, but if you want conversations to survive a restart, live in Redis, Postgres, or some hosted memory service, that integration belongs in **your** codebase, not as a framework dependency. `BaseChatHistory` is the stable seam that makes this possible: build against it and `AtomicAgent` doesn't care what's underneath. ### The Contract `BaseChatHistory` is an interface-only abstract base class (`ABC`). It declares no state and no behavior of its own, just the methods a history implementation must provide: - `initialize_turn(self) -> None` - `add_message(self, role: str, content: BaseIOSchema) -> None` - `get_history(self) -> List[Dict]` - `get_current_turn_id(self) -> Optional[str]` - `delete_turn_id(self, turn_id: str)` - `get_message_count(self) -> int` - `dump(self) -> str` - `load(self, serialized_data: str) -> None` - `copy(self) -> "BaseChatHistory"` Every implementation also has to maintain two attributes, because `AtomicAgent` reads them directly: - `history: List[Message]` - the in-memory working list of messages. `AtomicAgent._trim_context` reads this list (via each message's `turn_id`) to decide what to trim when a context-length limit is hit. - `current_turn_id: Optional[str]` - the ID of the current turn, or `None` if no turn has started yet. Set by `initialize_turn()`. ### Recommended Pattern: Subclass ChatHistory Don't implement `BaseChatHistory` from scratch unless you have a good reason to. Subclassing `ChatHistory` gets you turn handling and serialization for free, you only need to override the couple of methods that touch your storage layer: ```python from atomic_agents.context import ChatHistory class PersistentHistory(ChatHistory): def __init__(self, session_id, store, max_messages=None): super().__init__(max_messages=max_messages) self.session_id = session_id self.store = store saved = store.get(session_id) if saved: self.load(saved) def add_message(self, role, content): super().add_message(role, content) self.store.put(self.session_id, self.dump()) def copy(self): # AtomicAgent calls copy() for its initial_history snapshot and reset_history(). # Without this override, copy() returns a plain ChatHistory and your backend is # silently dropped after reset_history() (later writes stop reaching the store). new_history = PersistentHistory(self.session_id, self.store, max_messages=self.max_messages) new_history.load(self.dump()) new_history.current_turn_id = self.current_turn_id return new_history ``` The core of the pattern is small: load on init if there's saved state, persist on every `add_message`. `dump()`/`load()` already round-trip the full history, so there's no serialization logic to write yourself. ```{important} Override `copy()` too. `AtomicAgent` snapshots `initial_history` with `copy()` at construction and restores it in `reset_history()`. The built-in `ChatHistory.copy()` hard-codes a plain `ChatHistory`, so a subclass that carries extra state (a store handle, a `session_id`) that forgets to override `copy()` will be **silently replaced by a non-persistent in-memory history** the first time `reset_history()` runs. Any subclass with its own state must override `copy()` to return its own type. ``` ```{note} This pattern re-serializes and rewrites the **entire** history on every message (`self.dump()`), which is fine for a demo but O(n²) over a long conversation. A production backend against Redis/Postgres/etc. should persist incrementally (append the new message) rather than dumping the whole history each write. ``` ### From-Scratch Alternative If you don't want the in-memory list at all (say your backend is a query, not a cache), implement `BaseChatHistory` directly. You give up the free turn tracking and `get_history()` serialization that `ChatHistory` provides, so you own all of it: generating turn IDs, building the wire-format list of dicts, and keeping `history` / `current_turn_id` in sync with whatever `AtomicAgent` expects to read. ### Try It There's a full runnable example of this pattern (a dependency-free SQLite-backed history that survives across process runs) at [`atomic-examples/persistent-memory`](https://github.com/eigenwise/atomic-agents/tree/main/atomic-examples/persistent-memory). --- ## Multimodal Content in History ChatHistory supports images, PDFs, and audio through Instructor's multimodal types, plus video through the framework's own `VideoURL` type. ### Adding Multimodal Messages ```python from instructor import Image, PDF, Audio from atomic_agents import BaseIOSchema from pydantic import Field from typing import List class ImageAnalysisInput(BaseIOSchema): """Input with images for analysis""" question: str = Field(..., description="Question about the images") images: List[Image] = Field(..., description="Images to analyze") # Create input with images input_with_images = ImageAnalysisInput( question="What's in these images?", images=[ Image.from_path("photo1.jpg"), Image.from_path("photo2.png"), ] ) # Run agent - images are stored in history response = agent.run(input_with_images) ``` ### Multimodal Message Structure When history contains multimodal content, `get_history()` returns a special structure: ```python history_data = history.get_history() for message in history_data: if isinstance(message["content"], list): # Multimodal message json_content = message["content"][0] # Text/JSON data multimodal_objects = message["content"][1:] # Images, PDFs, etc. else: # Text-only message json_content = message["content"] ``` ### Video Instructor has no video type yet, so Atomic Agents ships its own `VideoURL` for providers that accept OpenAI-compatible `video_url` content parts (MiniMax, Qwen-VL, ...): ```python from atomic_agents import BaseIOSchema, VideoURL from pydantic import Field class VideoAnalysisInput(BaseIOSchema): """Input with a video for analysis""" question: str = Field(..., description="Question about the video") video: VideoURL = Field(..., description="Video to analyze") input_with_video = VideoAnalysisInput( question="What happens in this clip?", video=VideoURL(url="https://example.com/clip.mp4", fps=1.0), ) ``` `get_history()` emits the video as a `{"type": "video_url", ...}` content part, which Instructor passes through to the provider unchanged. ### Serialization with Multimodal ```{note} Multimodal content with file paths is serialized by path. Ensure files exist at the same paths when loading. ``` ```python # Serialize (file paths are preserved) serialized = history.dump() # When loading, files must be accessible at original paths history.load(serialized) ``` --- ## Dynamic Context with Providers Context providers inject dynamic information into agent system prompts at runtime, complementing the static conversation history. ### Understanding the Difference | Aspect | ChatHistory (Memory) | Context Providers | |--------|---------------------|-------------------| | **Purpose** | Store conversation turns | Inject dynamic context | | **Location** | Message history | System prompt | | **Persistence** | Saved with history | Regenerated each call | | **Use Case** | Conversation continuity | Real-time data (RAG, user info, time) | ```{mermaid} flowchart TB subgraph SystemPrompt["System Prompt (sent to LLM)"] BG[Background Instructions] ST[Steps] subgraph DC["Dynamic Context"] CP1[Context Provider 1] CP2[Context Provider 2] CP3[Context Provider 3] end OI[Output Instructions] end subgraph Messages["Conversation Messages"] H[ChatHistory Messages] end SystemPrompt --> LLM Messages --> LLM LLM --> Response ``` ### Creating Custom Context Providers ```python from atomic_agents.context import BaseDynamicContextProvider class UserContextProvider(BaseDynamicContextProvider): """Provides current user information to the agent.""" def __init__(self): super().__init__(title="Current User") self.user_name: str = "" self.user_role: str = "" self.preferences: dict = {} def get_info(self) -> str: """Called every time the agent runs.""" if not self.user_name: return "No user logged in." info = f"User: {self.user_name} (Role: {self.user_role})" if self.preferences: prefs = ", ".join(f"{k}: {v}" for k, v in self.preferences.items()) info += f"\nPreferences: {prefs}" return info ``` ### Registering Context Providers ```python from atomic_agents import AtomicAgent, AgentConfig from atomic_agents.context import SystemPromptGenerator # Create provider user_provider = UserContextProvider() # Option 1: Register with SystemPromptGenerator system_prompt = SystemPromptGenerator( background=["You are a helpful assistant."], context_providers={"user": user_provider} ) agent = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", system_prompt_generator=system_prompt, )) # Option 2: Register after agent creation agent.register_context_provider("user", user_provider) # Update provider state before running user_provider.user_name = "Alice" user_provider.user_role = "Admin" # Now the agent knows about Alice! response = agent.run(Input(text="What can I do?")) ``` ### Common Context Provider Patterns **RAG (Retrieval-Augmented Generation):** ```python class RAGContextProvider(BaseDynamicContextProvider): """Injects retrieved documents into the prompt.""" def __init__(self, vector_db): super().__init__(title="Relevant Documents") self.vector_db = vector_db self.current_query: str = "" self._cached_results: list = [] def search(self, query: str, top_k: int = 3): """Call before agent.run() to update context.""" self.current_query = query self._cached_results = self.vector_db.search(query, top_k=top_k) def get_info(self) -> str: if not self._cached_results: return "No relevant documents found." docs = [] for i, doc in enumerate(self._cached_results, 1): docs.append(f"Document {i}:\n{doc['content']}\nSource: {doc['source']}") return "\n\n".join(docs) # Usage rag_provider = RAGContextProvider(vector_db) agent.register_context_provider("documents", rag_provider) # Before each query user_query = "How do I reset my password?" rag_provider.search(user_query) # Update context response = agent.run(Input(text=user_query)) ``` **Time-Aware Context:** ```python from datetime import datetime class TimeContextProvider(BaseDynamicContextProvider): """Provides current time information.""" def __init__(self): super().__init__(title="Current Time") def get_info(self) -> str: now = datetime.now() return f"Current date/time: {now.strftime('%Y-%m-%d %H:%M:%S %Z')}" ``` **Session Context:** ```python class SessionContextProvider(BaseDynamicContextProvider): """Tracks session-specific state.""" def __init__(self): super().__init__(title="Session State") self.data: dict = {} def set(self, key: str, value: str): self.data[key] = value def get_info(self) -> str: if not self.data: return "No session data." return "\n".join(f"- {k}: {v}" for k, v in self.data.items()) ``` --- ## Multi-Agent Memory Patterns This section addresses the question from GitHub Issue #58: > "How do I handle a scenario where one agent performs an action, a second agent evaluates it, and then passes results back to the first agent's memory?" Here are five patterns for managing memory across multiple agents. ### Pattern 1: Shared History Multiple agents share the same `ChatHistory` instance, seeing each other's messages. ```{mermaid} flowchart LR subgraph SharedHistory["Shared ChatHistory"] M1[Message 1] M2[Message 2] M3[Message 3] M4[Message 4] end A1[Agent A] --> SharedHistory A2[Agent B] --> SharedHistory A3[Agent C] --> SharedHistory ``` **Use Case:** Agents that need full conversation context (e.g., specialist + generalist). ```python from atomic_agents import AtomicAgent, AgentConfig from atomic_agents.context import ChatHistory # One history shared by all shared_history = ChatHistory() # Agent A - Technical Expert technical_agent = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=shared_history, # Same history system_prompt_generator=SystemPromptGenerator( background=["You are a technical expert."] ), )) # Agent B - Communication Expert communication_agent = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=shared_history, # Same history! system_prompt_generator=SystemPromptGenerator( background=["You simplify technical explanations."] ), )) # Conversation flow user_input = Input(text="Explain quantum computing") # Technical agent adds to shared history technical_response = technical_agent.run(user_input) # Communication agent sees technical response in history simple_response = communication_agent.run( Input(text="Simplify the above explanation for a child") ) ``` ### Pattern 2: Independent Histories Each agent maintains its own isolated history. ```{mermaid} flowchart TB subgraph Agent_A["Agent A"] HA[History A] end subgraph Agent_B["Agent B"] HB[History B] end subgraph Agent_C["Agent C"] HC[History C] end User --> Agent_A User --> Agent_B User --> Agent_C ``` **Use Case:** Parallel processing, independent tasks, privacy isolation. ```python # Each agent has its own history agent_a = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory(), # Independent )) agent_b = AtomicAgent[Input, Output](config=AgentConfig( client=client, model="gpt-5-mini", history=ChatHistory(), # Independent )) # They don't see each other's conversations response_a = agent_a.run(Input(text="Research topic A")) response_b = agent_b.run(Input(text="Research topic B")) ``` (pattern-3-agent-to-agent-messaging)= ### Pattern 3: Agent-to-Agent Messaging Manually transfer outputs between agent memories. **This directly addresses Issue #58.** ```{mermaid} sequenceDiagram participant U as User participant O as Orchestrator participant A as Agent A participant B as Agent B U->>O: Initial request O->>A: run(user_input) Note over A: Turn 1: User + Response
added to A.history A-->>O: Result A O->>O: Manual transfer Note over O: B.history.add_message(
"user", Result A) O->>B: run(None) Note over B: Uses existing history
Turn 2: Response added B-->>O: Result B O->>O: Manual transfer Note over O: A.history.add_message(
"user", Result B) O->>A: run(None) Note over A: Continues with
B's feedback in context A-->>O: Final Result ``` **Use Case:** Agent loops, evaluation cycles, iterative refinement. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ### Pattern 4: Supervisor-Worker with Context Providers Use context providers to share state between supervisor and worker agents. ```{mermaid} flowchart TB subgraph SharedContext["Shared Context Provider"] SC[Task State & Results] end SUP[Supervisor Agent] --> SharedContext W1[Worker 1] --> SharedContext W2[Worker 2] --> SharedContext W3[Worker 3] --> SharedContext SUP -->|Delegates| W1 SUP -->|Delegates| W2 SUP -->|Delegates| W3 W1 -->|Updates| SharedContext W2 -->|Updates| SharedContext W3 -->|Updates| SharedContext ``` ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ### Pattern 5: Memory-Augmented Loops Combine conversation history with external memory for long-running processes. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` --- ## Best Practices ### When to Use Each Pattern | Scenario | Recommended Pattern | |----------|-------------------| | Single agent chatbot | Basic ChatHistory | | Multi-turn with context | ChatHistory + Context Providers | | Parallel independent tasks | Independent Histories | | Sequential pipeline | Agent-to-Agent Messaging | | Iterative refinement loops | Agent-to-Agent Messaging | | Supervisor-worker | Shared Context Providers | | Long-running processes | Memory-Augmented Loops | ### Managing Context Window Limits ```python from atomic_agents.utils import get_context_token_count # Monitor token usage token_info = agent.get_context_token_count() print(f"Total tokens: {token_info.total}") print(f"System prompt: {token_info.system_prompt}") print(f"History: {token_info.history}") print(f"Utilization: {token_info.utilization:.1%}") # Set appropriate limits if token_info.utilization > 0.8: # Consider trimming history or summarizing pass ``` ### Testing Agents with Memory ```python import pytest from atomic_agents.context import ChatHistory @pytest.fixture def fresh_history(): """Provide clean history for each test.""" return ChatHistory() @pytest.fixture def agent_with_history(fresh_history): """Agent with clean history.""" return AtomicAgent[Input, Output](config=AgentConfig( client=mock_client, model="gpt-5-mini", history=fresh_history, )) def test_conversation_continuity(agent_with_history): """Test that agent remembers previous messages.""" agent_with_history.run(Input(text="My name is Bob")) response = agent_with_history.run(Input(text="What's my name?")) assert "Bob" in response.response def test_history_persistence(agent_with_history): """Test serialization/deserialization.""" agent_with_history.run(Input(text="Remember: secret=42")) # Serialize serialized = agent_with_history.history.dump() # Create new history and load new_history = ChatHistory() new_history.load(serialized) assert new_history.get_message_count() == 2 ``` ### Debugging Memory Issues ```python # Inspect current history for msg in history.history: print(f"[{msg.role}] Turn: {msg.turn_id}") print(f" Content: {msg.content.model_dump_json()[:100]}...") print() # Check turn state print(f"Current turn ID: {history.get_current_turn_id()}") print(f"Message count: {history.get_message_count()}") print(f"Max messages: {history.max_messages}") ``` --- ## Troubleshooting ### "Messages aren't being added to history" **Cause:** Calling `run()` without input after resetting history. ```python # Wrong - no messages to work with agent.reset_history() agent.run() # Nothing in history! # Correct agent.reset_history() agent.run(Input(text="Start fresh")) # Provides input ``` ### "Agent doesn't remember previous conversation" **Cause:** Creating new agent instances instead of reusing. ```python # Wrong - new agent = new history each time def handle_message(text): agent = AtomicAgent[Input, Output](config=config) # New instance! return agent.run(Input(text=text)) # Correct - reuse agent instance agent = AtomicAgent[Input, Output](config=config) # Create once def handle_message(text): return agent.run(Input(text=text)) # Reuse ``` ### "How do I pass memory between agents?" See [Pattern 3: Agent-to-Agent Messaging](#pattern-3-agent-to-agent-messaging). ```python # Transfer output to another agent's memory agent_b.history.add_message("user", agent_a_output) agent_b.run() # Now has context from agent A ``` ### "What exactly is a 'turn'?" A **turn** is a logical unit of conversation, typically containing: - One user message - One assistant response - Both sharing the same `turn_id` (UUID) ```python # This is ONE turn: response = agent.run(Input(text="Hello")) # turn_id "abc-123" assigned to both user message and response # This starts a NEW turn: response2 = agent.run(Input(text="Next question")) # turn_id "def-456" assigned to new pair ``` ### "History is too large / context overflow" ```python # Option 1: Limit history size history = ChatHistory(max_messages=30) # Option 2: Monitor and handle if history.get_message_count() > 40: # Summarize or archive old messages pass # Option 3: Use context providers for persistent data # instead of relying on conversation history ``` --- ## API Quick Reference ### ChatHistory | Method | Description | |--------|-------------| | `ChatHistory(max_messages=None)` | Create history with optional limit | | `add_message(role, content)` | Add message to current turn | | `initialize_turn()` | Start new turn with new UUID | | `get_current_turn_id()` | Get current turn's UUID | | `get_history()` | Get all messages as list of dicts | | `get_message_count()` | Get number of messages | | `delete_turn_id(turn_id)` | Delete all messages in a turn | | `dump()` | Serialize to JSON string | | `load(data)` | Deserialize from JSON string | | `copy()` | Create deep copy | ### BaseChatHistory The abstract base class `ChatHistory` implements. Subclass it (directly, or via `ChatHistory`) to plug in a custom memory backend. See [Writing a Custom Memory Backend](#writing-a-custom-memory-backend). | Method | Description | |--------|-------------| | `initialize_turn()` | Start new turn with new UUID | | `add_message(role, content)` | Add message to current turn | | `get_history()` | Get all messages as list of dicts | | `get_current_turn_id()` | Get current turn's UUID | | `delete_turn_id(turn_id)` | Delete all messages in a turn | | `get_message_count()` | Get number of messages | | `dump()` | Serialize to JSON string | | `load(data)` | Deserialize from JSON string | | `copy()` | Create deep copy | Every implementation must also maintain a `history: List[Message]` attribute and a `current_turn_id: Optional[str]` attribute. ### Message | Field | Type | Description | |-------|------|-------------| | `role` | str | "user", "assistant", or "system" | | `content` | BaseIOSchema | Message content | | `turn_id` | Optional[str] | UUID linking related messages | ### BaseDynamicContextProvider | Method | Description | |--------|-------------| | `__init__(title)` | Create with display title | | `get_info() -> str` | Return context string (override this) | --- ## Next Steps - [Quickstart Guide](quickstart.md) - Get started with Atomic Agents - [Tools Guide](tools.md) - Add capabilities to your agents - [Orchestration Guide](orchestration.md) - Coordinate multiple agents - [Hooks Guide](hooks.md) - Monitor and customize agent behavior - [API Reference](/api/context) - Full API documentation --- ## Summary Key takeaways: 1. **Automatic Memory**: `agent.run(input)` automatically manages history - you don't need to manually add messages 2. **Turns**: A turn groups user input + assistant response with a shared UUID 3. **Persistence**: Use `dump()`/`load()` to save and restore conversations 4. **Context Providers**: Inject dynamic information (RAG, user data, time) into system prompts 5. **Multi-Agent**: Use shared history, agent-to-agent messaging, or context providers depending on your needs For questions or issues, visit our [GitHub repository](https://github.com/eigenwise/atomic-agents) or [Reddit community](https://www.reddit.com/r/AtomicAgents/). --- ### Guides/Orchestration # Orchestration and Multi-Agent Patterns This guide covers patterns for building multi-agent systems and orchestrating complex workflows with Atomic Agents. ## Overview Orchestration in Atomic Agents enables: - **Tool Selection**: Agents that choose appropriate tools based on input - **Multi-Agent Pipelines**: Chain agents for complex workflows - **Dynamic Routing**: Route queries to specialized agents - **Parallel Execution**: Run multiple agents concurrently - **Agent Composition**: Combine agents for sophisticated behavior ## Tool Orchestration Pattern The most common pattern: an orchestrator agent that selects and invokes tools. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Sequential Pipeline Pattern Chain multiple agents where each agent's output feeds the next: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Parallel Execution Pattern Run multiple agents concurrently for independent tasks: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Router Pattern Route queries to specialized agents based on classification: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Context Sharing Between Agents Share information between agents using context providers: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Supervisor Pattern A supervisor agent that manages and validates worker agents: ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ## Best Practices ### 1. Design Clear Interfaces Define explicit input/output schemas for each agent: ```python # Good: Clear, typed interfaces class AgentAOutput(BaseIOSchema): data: str metadata: dict class AgentBInput(BaseIOSchema): data: str # Explicitly matches AgentAOutput.data ``` ### 2. Handle Failures Gracefully Implement fallbacks and error handling: ```python def execute_with_fallback(primary_agent, fallback_agent, input_data): try: return primary_agent.run(input_data) except Exception as e: print(f"Primary failed: {e}, using fallback") return fallback_agent.run(input_data) ``` ### 3. Monitor Agent Interactions Log inter-agent communication: ```python def logged_handoff(from_agent: str, to_agent: str, data): print(f"[{from_agent}] -> [{to_agent}]: {type(data).__name__}") return data ``` ### 4. Keep Agents Focused Each agent should have a single responsibility: ```python # Good: Single responsibility query_generator = AtomicAgent[...] # Only generates queries analyzer = AtomicAgent[...] # Only analyzes # Avoid: Multiple responsibilities in one agent do_everything_agent = AtomicAgent[...] # Too complex ``` ## Summary | Pattern | Use Case | Key Benefit | |---------|----------|-------------| | Tool Orchestration | Dynamic tool selection | Flexible routing | | Sequential Pipeline | Multi-step processing | Clear data flow | | Parallel Execution | Independent analyses | Performance | | Router Pattern | Query classification | Specialization | | Context Sharing | Knowledge accumulation | Collaboration | | Supervisor Pattern | Quality assurance | Validation | Choose patterns based on your workflow requirements and combine them for sophisticated agent systems. ---