# Contribute Source: https://framework.beeai.dev/community/contribute # MCP Slackbot Source: https://framework.beeai.dev/guides/mcp-slackbot ## Creating Slack Bot Agent with BeeAI Framework and MCP This tutorial guides you through creating an AI agent that can post messages to a Slack channel using the Model Context Protocol (MCP). *** ## Table of Contents * [Slack Agent Prerequisites](#slack-agent-prerequisites) * [Slack Configuration](#slack-configuration) * [Implementing the Slack Agent](#implementing-the-slack-agent) * [Running the Slack Agent](#running-the-slack-agent) *** ### Slack agent prerequisites * **[Python](https://www.python.org/)**: Version 3.11 or higher * **[Ollama](https://ollama.com/)**: Installed with the `granite3.3:8b` model pulled * **BeeAI framework** installed with `pip install beeai-framework` * Project setup: * Create project directory: `mkdir beeai-slack-agent && cd beeai-slack-agent` * Set up Python virtual environment: `python -m venv venv && source venv/bin/activate` * Create environment file: `echo -e "SLACK_BOT_TOKEN=\nSLACK_TEAM_ID=" >> .env` * Create agent module: `mkdir my_agents && touch my_agents/slack_agent.py` Once you've completed these prerequisites, you'll be ready to implement your Slack agent. ### Slack configuration To configure the Slack API integration: 1. Create a Slack app * Visit [https://api.slack.com/apps](https://api.slack.com/apps) and click "Create New App" > "From scratch" * Name your app (e.g., `Bee`) and select a workspace to develop your app in 2. Configure bot permissions * Navigate to `OAuth & Permissions` in the sidebar * Under "Bot Token Scopes", add the `chat:write` scope * Click "Install to \[Workspace]" and authorize the app 3. Gather credentials * Copy the "Bot User OAuth Token" and add it to your `.env` file as `SLACK_BOT_TOKEN=xoxb-your-token` * Get your Slack Team ID from your workspace URL `(https://app.slack.com/client/TXXXXXXX/...)` * Tip: Visit `https://.slack.com`, after redirect, your URL will change to `https://app.slack.com/client/TXXXXXXX/CXXXXXXX`, pick the segment starting with `TXXXXXXX` * Add the Team ID to your `.env` file as `SLACK_TEAM_ID=TXXXXXXX` 4. Create a channel * Create a public channel named `bee-playground` in your Slack workspace * Invite your bot to the channel by typing `/invite @Bee` in the channel ### Implementing the Slack agent The framework doesn't have any specialized tools for using Slack API. However, it supports tools exposed via Model Context Protocol (MCP) and performs automatic tool discovery. We will use that to give our agent the capability to post Slack messages. Now, copy and paste the following code into `slack_agent.py` module. Then, follow along with the comments for an explanation. ```py Python [expandable] theme={null} import asyncio import os import sys import traceback from typing import Any from dotenv import load_dotenv from mcp import StdioServerParameters from mcp.client.stdio import stdio_client from beeai_framework.agents.tool_calling import ToolCallingAgent from beeai_framework.backend import ChatModel, ChatModelParameters from beeai_framework.emitter import EventMeta from beeai_framework.errors import FrameworkError from beeai_framework.memory import UnconstrainedMemory from beeai_framework.tools.mcp import MCPClient, MCPTool from beeai_framework.tools.weather import OpenMeteoTool # Load environment variables load_dotenv() # Create server parameters for stdio connection server_params = StdioServerParameters( command="npx", args=["-y", "@modelcontextprotocol/server-slack"], env={ "SLACK_BOT_TOKEN": os.environ["SLACK_BOT_TOKEN"], "SLACK_TEAM_ID": os.environ["SLACK_TEAM_ID"], "PATH": os.getenv("PATH", default=""), }, ) async def slack_tool(client: MCPClient) -> MCPTool: # Discover Slack tools via MCP client slacktools = await MCPTool.from_client(client) filter_tool = filter(lambda tool: tool.name == "slack_post_message", slacktools) slack = list(filter_tool) return slack[0] async def create_agent() -> ToolCallingAgent: """Create and configure the agent with tools and LLM""" # Other models to try: # "llama3.1" # "deepseek-r1" # ensure the model is pulled before running llm = ChatModel.from_name( "ollama:llama3.1", ChatModelParameters(temperature=0), ) # Configure tools slack = await slack_tool(stdio_client(server_params)) weather = OpenMeteoTool() # Create agent with memory and tools and custom system prompt template agent = ToolCallingAgent( llm=llm, tools=[slack, weather], memory=UnconstrainedMemory(), templates={ "system": lambda template: template.update( defaults={ "instructions": """IMPORTANT: When the user mentions Slack, you must interact with the Slack tool before sending the final answer.""", } ) }, ) return agent def print_events(data: Any, event: EventMeta) -> None: """Print agent events""" if event.name in ["start", "retry", "update", "success", "error"]: print(f"\n** Event ({event.name}): {event.path} **\n{data}") async def main() -> None: """Main application loop""" # Create agent agent = await create_agent() # Run agent with the prompt response = await agent.run( "Post the current temperature in Prague to the '#bee-playground-xxx' Slack channel.", max_retries_per_step=3, total_max_retries=10, max_iterations=20, ).on("*", print_events) print("Agent 🤖 : ", response.last_message.text) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` *Source: [python/examples/tools/mcp\_slack\_agent.py](https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/tools/mcp_slack_agent.py)* ### Running the Slack agent Execute your agent with: ```bash theme={null} python my_agents/slack_agent.py ``` You will observe the agent: * Analyze the task * Determine it needs to check the weather in Boston * Use the OpenMeteo tool to get the current temperature * Use the `slack_post_message` tool to post to the #bee-playground Slack channel As you might have noticed, we made some restrictions to make the agent work with smaller models so that it can be executed locally. With larger LLMs, we could further simplify the code, use more tools, and create simpler prompts. This tutorial can be easily generalized to any MCP server with tools capability. Just plug it into Bee and execute. # A2A Source: https://framework.beeai.dev/integrations/a2a The **[Agent2Agent (A2A) Protocol](https://a2a-protocol.org/)** is the open standard for AI agent communication. Developed under the Linux Foundation, A2A makes it possible for agents to work together seamlessly across platforms, frameworks, and ecosystems. Supported in Python only. *** ## Prerequisites * **BeeAI Framework** installed with `pip install beeai-framework` * **BeeAI Framework extension for A2A** installed with `pip install 'beeai-framework[a2a]'` A2A Client The `A2AAgent` lets you easily connect with external agents using the A2A protocol. ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.adapters.a2a.agents import A2AAgent, A2AAgentUpdateEvent from beeai_framework.emitter import EventMeta from beeai_framework.errors import FrameworkError from beeai_framework.memory.unconstrained_memory import UnconstrainedMemory from examples.helpers.io import ConsoleReader async def main() -> None: reader = ConsoleReader() agent = A2AAgent(url="http://127.0.0.1:9999", memory=UnconstrainedMemory()) for prompt in reader: # Run the agent and observe events def print_update(data: A2AAgentUpdateEvent, event: EventMeta) -> None: value = data.value debug_info = value[1] if isinstance(value, tuple) else value reader.write("Agent 🤖 (debug) : ", str(debug_info)) response = await agent.run(prompt).on("update", print_update) reader.write("Agent 🤖 : ", response.last_message.text) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config.js"; import { A2AAgent } from "beeai-framework/adapters/a2a/agents/agent"; import { createConsoleReader } from "examples/helpers/io.js"; import { FrameworkError } from "beeai-framework/errors"; import { TokenMemory } from "beeai-framework/memory/tokenMemory"; const agent = new A2AAgent({ url: "http://127.0.0.1:9999", memory: new TokenMemory(), }); const reader = createConsoleReader(); try { for await (const { prompt } of reader) { const result = await agent.run({ input: prompt }).observe((emitter) => { emitter.on("update", (data) => { reader.write(`Agent (received progress) 🤖 : `, JSON.stringify(data.value, null, 2)); }); emitter.on("error", (data) => { reader.write(`Agent (error) 🤖 : `, data.message); }); }); reader.write(`Agent 🤖 : `, result.result.text); } } catch (error) { reader.write("Agent (error) 🤖", FrameworkError.ensure(error).dump()); } ``` A2A Server `A2AServer` lets you expose agents built in the BeeAI framework via A2A protocol. A2A supports only one agent per server. ```py Python [expandable] theme={null} # pyrefly: ignore [missing-module-attribute] from beeai_framework.adapters.a2a import A2AServer, A2AServerConfig from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.memory import UnconstrainedMemory from beeai_framework.serve.utils import LRUMemoryManager from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool from beeai_framework.tools.weather import OpenMeteoTool def main() -> None: llm = ChatModel.from_name("ollama:granite4:micro") agent = RequirementAgent( llm=llm, tools=[DuckDuckGoSearchTool(), OpenMeteoTool()], memory=UnconstrainedMemory(), ) # Register the agent with the A2A server and run the HTTP server # For the ToolCallingAgent, we don't need to specify A2AAgent factory method # because it is already registered in the A2AServer # we use LRU memory manager to keep limited amount of sessions in the memory A2AServer( config=A2AServerConfig(port=9999, protocol="jsonrpc"), memory_manager=LRUMemoryManager(maxsize=100) ).register(agent, send_trajectory=True).serve() if __name__ == "__main__": main() ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config.js"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; import { ToolCallingAgent } from "beeai-framework/agents/toolCalling/agent"; import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory"; import { A2AServer } from "beeai-framework/adapters/a2a/serve/server"; // ensure the model is pulled before running const llm = new OllamaChatModel("granite4:micro"); const agent = new ToolCallingAgent({ llm, memory: new UnconstrainedMemory(), tools: [ new OpenMeteoTool(), // weather tool ], }); await new A2AServer().register(agent).serve(); ``` # Agent Stack Source: https://framework.beeai.dev/integrations/agent-stack [Agent Stack](https://beeai.dev/) is an open platform to help you discover, run, and compose AI agents from any framework. This tutorial demonstrates how to consume agents from the Agent Stack and expose agents built in BeeAI Framework to the Agent Stack. **Prerequisites** * **[Agent Stack](https://beeai.dev/)** installed and running locally * **BeeAI Framework** installed with `pip install beeai-framework[agentstack]` *** ## Consuming from the platform (client) The `AgentStackAgent` class allows you to connect to any agent hosted on the Agent Stack. This means that you can interact with agents built from any framework! Use `AgentStackAgent` when: * You're connecting specifically to the Agent Stack services. * You want forward compatibility for the Agent Stack, no matter which protocol it is based on. Here's a simple example that uses the built-in `chat` agent: ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.adapters.agentstack.agents import AgentStackAgent from beeai_framework.errors import FrameworkError from beeai_framework.memory import UnconstrainedMemory from examples.helpers.io import ConsoleReader async def main() -> None: reader = ConsoleReader() agents = await AgentStackAgent.from_agent_stack(url="http://127.0.0.1:8333", memory=UnconstrainedMemory()) if len(agents) > 1: reader.write("Prompt: ", "Select one of the available agents:\n") while True: for index, agent in enumerate(agents): reader.write("AgentStack: ", f"{index}) {agent.name} - {agent.meta.description}") agents_index = reader.ask_single_question("Write agent's number: ") try: agent = agents[int(agents_index)] if agent: break except (ValueError, IndexError): reader.write( "AgentStack (error) : ", f"Invalid selection: `{agents_index}`. Please enter a valid agent number.\n", ) elif len(agents) == 1: agent = agents[0] else: reader.write("AgentStack (error) : ", "No agent registered within the agent stack.\n") exit(0) reader.write("AgentStack: ", f"Selected {agent.name}:\n") for prompt in reader: # Run the agent and observe events response = await agent.run(prompt).on( "update", lambda data, event: (reader.write(f"{agent.name} 🤖 (debug) : ", data)), ) reader.write(f"{agent.name} Agent 🤖 : ", response.last_message.text) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config.js"; import { AgentStackAgent } from "beeai-framework/adapters/agentstack/agents/agent"; import { createConsoleReader } from "examples/helpers/io.js"; import { FrameworkError } from "beeai-framework/errors"; import { TokenMemory } from "beeai-framework/memory/tokenMemory"; //////////////////////////////////////////////////////// /// Supports only BeeAI platform version v0.2.xx /// //////////////////////////////////////////////////////// const agentName = "chat"; const instance = new AgentStackAgent({ url: "http://127.0.0.1:8333/api/v1/acp", agentName, memory: new TokenMemory(), }); const reader = createConsoleReader(); try { for await (const { prompt } of reader) { const result = await instance.run({ input: prompt }).observe((emitter) => { emitter.on("update", (data) => { reader.write(`Agent (received progress) 🤖 : `, JSON.stringify(data.value, null, 2)); }); emitter.on("error", (data) => { reader.write(`Agent (error) 🤖 : `, data.message); }); }); reader.write(`Agent (${agentName}) 🤖 : `, result.result.text); } } catch (error) { reader.write("Agent (error) 🤖", FrameworkError.ensure(error).dump()); } ``` **Usage in Workflow** You can compose multiple Agent Stack agents into advanced workflows using the BeeAI framework's workflow capabilities. This example demonstrates a research and content creation pipeline: In this example, the `GPT Researcher` agent researches a topic, and the `Podcast creator` takes the research report and produces a podcast transcript. You can adjust or expand this pattern to orchestrate more complex multi-agent workflows. ```py Python [expandable] theme={null} import asyncio import sys import traceback from pydantic import BaseModel # pyrefly: ignore [missing-module-attribute] from beeai_framework.adapters.agentstack import AgentStackAgent from beeai_framework.errors import FrameworkError from beeai_framework.memory.unconstrained_memory import UnconstrainedMemory from beeai_framework.workflows import Workflow from examples.helpers.io import ConsoleReader async def main() -> None: reader = ConsoleReader() class State(BaseModel): topic: str research: str | None = None output: str | None = None agents = await AgentStackAgent.from_agent_stack(url="http://127.0.0.1:8333", memory=UnconstrainedMemory()) async def research(state: State) -> None: # Run the agent and observe events try: research_agent = next(agent for agent in agents if agent.name == "GPT Researcher") except StopIteration: raise ValueError("Agent 'GPT Researcher' not found") from None response = await research_agent.run(state.topic).on( "update", lambda data, _: (reader.write("Agent 🤖 (debug) : ", data)), ) state.research = response.last_message.text async def podcast(state: State) -> None: # Run the agent and observe events try: podcast_agent = next(agent for agent in agents if agent.name == "Podcast creator") except StopIteration: raise ValueError("Agent 'Podcast creator' not found") from None response = await podcast_agent.run(state.research or "").on( "update", lambda data, _: (reader.write("Agent 🤖 (debug) : ", data)), ) state.output = response.last_message.text # Define the structure of the workflow graph workflow = Workflow(State) workflow.add_step("research", research) workflow.add_step("podcast", podcast) # Execute the workflow result = await workflow.run(State(topic="Connemara")) print("\n*********************") print("Topic: ", result.state.topic) print("Research: ", result.state.research) print("Output: ", result.state.output) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` **Use agent from the remote Agent Stack** If you want to integrate an agent from a remote server with authorization, you must first obtain a JWT auth token, and then create a remote agent and set its required parameters. ```py Python theme={null} agents = await AgentStackAgent.from_agent_stack(url="https://my-agentstack-server.com/", auth_token="ey***") ``` or create a custom client: ```py Python theme={null} from agentstack_sdk.platform import PlatformClient from beeai_framework.adapters.agentstack.agents import AgentStackAgent async with PlatformClient(auth_token="ey***", base_url="https://my-agentstack-server.com/") as client: agents = await AgentStackAgent.from_agent_stack(url=client) ``` ## Exposing to the platform (server) The `AgentStackServer` class exposes an agent or any other runnable (tool/chat model, ...) to the **Agent Stack**. It gets automatically registered to the platform and allows you to access and use the agents directly in the platform. Key Features: * easy to expose (deploy) the current application to the production-ready environment * built-in trajectory, forms integration, LLM inference support, ... * easy to extend and debug ```py Python [expandable] theme={null} from beeai_framework.adapters.agentstack.backend.chat import AgentStackChatModel from beeai_framework.adapters.agentstack.serve.server import AgentStackMemoryManager, AgentStackServer from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModelParameters from beeai_framework.memory import UnconstrainedMemory from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool from beeai_framework.tools.weather import OpenMeteoTool try: from agentstack_sdk.a2a.extensions.ui.agent_detail import AgentDetail except ModuleNotFoundError as e: raise ModuleNotFoundError( "Optional module [agentstack] not found.\nRun 'pip install \"beeai-framework[agentstack]\"' to install." ) from e def main() -> None: llm = AgentStackChatModel( preferred_models=["openai:gpt-4o", "ollama:llama3.1:8b"], parameters=ChatModelParameters(stream=True), ) agent = RequirementAgent( llm=llm, tools=[DuckDuckGoSearchTool(), OpenMeteoTool()], memory=UnconstrainedMemory(), middlewares=[GlobalTrajectoryMiddleware()], ) # Runs HTTP server that registers to Agent Stack server = AgentStackServer(memory_manager=AgentStackMemoryManager()) server.register( agent, name="Framework chat agent", # (optional) description="Simple chat agent", # (optional) detail=AgentDetail(interaction_mode="multi-turn"), # default is multi-turn (optional) ) server.serve() if __name__ == "__main__": main() ``` ```ts TypeScript [expandable] theme={null} // COMING SOON ``` Agent Stack supports only one entry per server. To register more, you need to spawn more servers. You can use the platform to do LLM inference by using the `AgentStackChatModel(...)` class. ### Configuration **Server** The server's behavior can be influenced via attributes listed in `AgentStackServerConfig` class (host, port, self registration, ...). Internally the server preserves every conversation, the custom strategy can be used by implementing the base `MemoryManager` class. ```py Python [expandable] theme={null} from beeai_framework.adapters.agentstack.serve.server import AgentStackServer, AgentStackServerConfig from beeai_framework.serve.utils import LRUMemoryManager memory_manager = LRUMemoryManager(maxsize=64) # keeps max 64 conversations config = AgentStackServerConfig(host="127.0.0.1", port=9999, run_limit=3600, limit_concurrency=10) server = AgentStackServer(config=config, memory_manager=memory_manager) ``` **Agent** The agent’s meta information is inferred from its metadata (the `agent.meta` property). However, this information can be overridden during agent registration. See the following example. ```py Python [expandable] theme={null} from beeai_framework.adapters.agentstack.serve.server import AgentStackServer from beeai_framework.adapters.agentstack.backend.chat import AgentStackChatModel from agentstack_sdk.a2a.extensions.ui.agent_detail import AgentDetail server = AgentStackServer() agent = RequirementAgent(llm=AgentStackChatModel()) server.register(agent, name="SmartAgent", description="Knows everything!", url="https://example.com", version="1.0.0", default_input_modes=["text", "text/plain"], default_output_modes=["text"], detail=AgentDetail(interaction_mode="multi-turn", user_greeting="What can I do for you?") ) ``` ### Customization The Agent Stack has a concept of [extensions](https://docs.beeai.dev/concepts/extensions) that enable access to external services and UI components via dependency injection. The framework internally uses the following extensions: * **Form Extension:** for displaying prompts and other checks (for instance when using `AskPermissionRequirement`).. * **Trajectory Extension:** for showing agent's intermediate steps throughout the execution The current entries are listed in the `BaseAgentStackExtensions` class. **Custom Extensions** The following implementation demonstrates an agent that conducts an internet search and provides an answer to the given question, with inline citations. It does so by leveraging the [Citation Extension](https://docs.beeai.dev/build-agents/citations), which is managed by the `PlatformCitationMiddleware` class. Learn more about the [Middleware](/modules/middleware) concept. ```py Python [expandable] theme={null} # Copyright 2025 © BeeAI a Series of LF Projects, LLC # SPDX-License-Identifier: Apache-2.0 import re import sys import traceback from typing import Annotated from beeai_framework.adapters.agentstack.backend.chat import AgentStackChatModel from beeai_framework.adapters.agentstack.context import AgentStackContext from beeai_framework.adapters.agentstack.serve.server import AgentStackMemoryManager, AgentStackServer from beeai_framework.adapters.agentstack.serve.types import BaseAgentStackExtensions from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.agents.requirement.events import RequirementAgentSuccessEvent from beeai_framework.agents.requirement.requirements.conditional import ConditionalRequirement from beeai_framework.backend import AssistantMessage from beeai_framework.context import RunContext, RunMiddlewareProtocol from beeai_framework.emitter import EmitterOptions, EventMeta from beeai_framework.errors import FrameworkError from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool from beeai_framework.tools.search.wikipedia import WikipediaTool from beeai_framework.tools.think import ThinkTool try: from agentstack_sdk.a2a.extensions import Citation, CitationExtensionServer, CitationExtensionSpec from agentstack_sdk.a2a.extensions.ui.agent_detail import AgentDetail from agentstack_sdk.a2a.types import AgentMessage except ModuleNotFoundError as e: raise ModuleNotFoundError( "Optional module [agentstack] not found.\nRun 'pip install \"beeai-framework[agentstack]\"' to install." ) from e class CitationMiddleware(RunMiddlewareProtocol): def __init__(self) -> None: self._context: AgentStackContext | None = None def bind(self, ctx: RunContext) -> None: self._context = AgentStackContext.get() # add emitter with the highest priority to ensure citations are sent before any other event handling ctx.emitter.on("success", self._handle_success, options=EmitterOptions(priority=10, is_blocking=True)) async def _handle_success(self, data: RequirementAgentSuccessEvent, meta: EventMeta) -> None: assert self._context is not None citation_ext = self._context.extensions.get("citation") # check it is the final step if data.state.answer is not None: citations, clean_text = extract_citations(data.state.answer.text) if citations: await self._context.context.yield_async( AgentMessage(metadata=citation_ext.citation_metadata(citations=citations)) # type: ignore[attr-defined] ) # replace an assistant message with an updated text without citation links data.state.answer = AssistantMessage(content=clean_text) # define custom extensions class CustomExtensions(BaseAgentStackExtensions): citation: Annotated[CitationExtensionServer, CitationExtensionSpec()] def main() -> None: agent = RequirementAgent( llm=AgentStackChatModel(preferred_models=["openai/gpt-4o"]), tools=[WikipediaTool(), ThinkTool(), DuckDuckGoSearchTool()], instructions=( "You are an AI assistant focused on retrieving information from online sources. " "Mandatory Search: Always search for the topic on Wikipedia and always search for related current news. " "Mandatory Output Structure: Return the result in two separate sections with headings: " " 1. Basic Information (primarily utilizing data from Wikipedia, if relevant). " " 2. News (primarily utilizing current news results). " "Mandatory Citation: Always include a source link for all given information, especially news." ), requirements=[ ConditionalRequirement(ThinkTool, force_at_step=1, consecutive_allowed=False), ConditionalRequirement(WikipediaTool, min_invocations=1), ConditionalRequirement(DuckDuckGoSearchTool, min_invocations=1), ], description="Search for information based on a given phrase.", middlewares=[ GlobalTrajectoryMiddleware(), CitationMiddleware(), ], # add platform middleware to get citations from the platform ) # Runs HTTP server that registers to Agent Stack server = AgentStackServer(memory_manager=AgentStackMemoryManager()) # use platform memory server.register( agent, name="Information retrieval", detail=AgentDetail(interaction_mode="single-turn", user_greeting="What can I search for you?"), extensions=CustomExtensions, ) server.serve() # function to extract citations from text and return clean text without citation links def extract_citations(text: str) -> tuple[list[Citation], str]: citations, offset = [], 0 pattern = r"\[([^\]]+)\]\(([^)]+)\)" for match in re.finditer(pattern, text): content, url = match.groups() start = match.start() - offset citations.append( Citation( url=url, title=url.split("/")[-1].replace("-", " ").title() or content[:50], description=content[:100] + ("..." if len(content) > 100 else ""), start_index=start, end_index=start + len(content), ) ) offset += len(match.group(0)) - len(content) return citations, re.sub(pattern, r"\1", text) if __name__ == "__main__": try: main() except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} // COMING SOON ``` The `AgentStackContext.get()` can be called from anywhere if the code is running inside the given context. ### Platform RAG Agent You can use the [Vector Store Search Tool](/modules/tools#vector-store-search) to query the platform-native [vector store service](https://agentstack.beeai.dev/stable/agent-integration/rag#store) and leverage the platform-provided [embedding service](https://agentstack.beeai.dev/stable/agent-integration/rag#embedding). ```py Python [expandable] theme={null} from typing import Annotated from beeai_framework.adapters.agentstack.backend.chat import AgentStackChatModel from beeai_framework.adapters.agentstack.backend.embedding import AgentstackEmbeddingModel from beeai_framework.adapters.agentstack.backend.vector_store import NativeVectorStore from beeai_framework.adapters.agentstack.serve.server import AgentStackMemoryManager, AgentStackServer from beeai_framework.adapters.agentstack.serve.types import BaseAgentStackExtensions from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModelParameters from beeai_framework.backend.types import Document from beeai_framework.context import RunContext, RunContextStartEvent, RunMiddlewareProtocol from beeai_framework.emitter import EventMeta from beeai_framework.emitter.utils import create_internal_event_matcher from beeai_framework.memory import UnconstrainedMemory from beeai_framework.tools.search.retrieval import VectorStoreSearchTool try: from agentstack_sdk.a2a.extensions import EmbeddingServiceExtensionServer, EmbeddingServiceExtensionSpec except ModuleNotFoundError as e: raise ModuleNotFoundError( "Optional module [agentstack] not found.\nRun 'pip install \"beeai-framework[agentstack]\"' to install." ) from e # The middleware is necessary since the embedding service's initialization occurs after the client's call to the agent. class RAGMiddleware(RunMiddlewareProtocol): def __init__(self, vector_store: NativeVectorStore) -> None: self._vector_store = vector_store def bind(self, ctx: RunContext) -> None: # pyrefly: ignore [bad-override] # Only insert the documents during initialization. ctx.emitter.on(create_internal_event_matcher("start"), self._on_start) async def _on_start(self, _: RunContextStartEvent, meta: EventMeta) -> None: if not self._vector_store.is_initialized: print("debug: initializing vector store") await self._vector_store.add_documents( [ Document(content="My name is John.", metadata={}), Document(content="I am a python programmer.", metadata={}), Document(content="I am 30 years old.", metadata={}), ] ) def main() -> None: llm = AgentStackChatModel( preferred_models=["openai:gpt-4o", "ollama:llama3.1:8b"], parameters=ChatModelParameters(stream=True), ) # Initialize the embedding model from the Agent Stack. embedding_model = AgentstackEmbeddingModel(preferred_models=["ollama:nomic-embed-text:latest"]) vector_store = NativeVectorStore(embedding_model) # vector_store = VectorStore.from_name("AgentStack:NativeVectorStore", embedding_model=embedding_model) agent = RequirementAgent( llm=llm, tools=[VectorStoreSearchTool(vector_store)], memory=UnconstrainedMemory(), middlewares=[RAGMiddleware(vector_store)], # add middleware to initialize vector store ) # define custom extensions class CustomExtensions(BaseAgentStackExtensions): # "The property name must be 'embedding'. embedding: Annotated[ EmbeddingServiceExtensionServer, EmbeddingServiceExtensionSpec.single_demand(suggested=tuple(embedding_model.preferred_models)), ] # Runs HTTP server that registers to Agent Stack server = AgentStackServer(memory_manager=AgentStackMemoryManager()) server.register(agent, name="Framework RAG agent", extensions=CustomExtensions) server.serve() if __name__ == "__main__": main() ``` ```ts TypeScript [expandable] theme={null} // COMING SOON ``` # MCP Source: https://framework.beeai.dev/integrations/mcp The MCP (Model Context Protocol), developed by Anthropic, is an open protocol that standardizes how applications provide context to LLMs. *** ## Prerequisites * **BeeAI Framework** installed with `pip install beeai-framework` * **BeeAI Framework extension for MCP** installed with `pip install 'beeai-framework[mcp]'` MCP Client MCPTool allows you to consume external tools exposed via MCP protocol. See the [MCP Tool page](/modules/tools#mcp) for more information. MCP Server MCPServer allows you to expose your components (Tools, Agents, Chat Models, Runnable) to external systems that support the Model Context Protocol (MCP) standard, enabling seamless integration with LLM tools ecosystems. Key benefits * Fast setup with minimal configuration * Support for multiple transport options * Register multiple tools on a single server * Custom server settings and instructions ```py Python [expandable] theme={null} from beeai_framework.adapters.mcp.serve.server import MCPServer, MCPServerConfig, MCPSettings from beeai_framework.tools import tool from beeai_framework.tools.types import StringToolOutput from beeai_framework.tools.weather.openmeteo import OpenMeteoTool @tool def reverse_tool(word: str) -> StringToolOutput: """A tool that reverses a word""" return StringToolOutput(result=word[::-1]) def main() -> None: """Create an MCP server with custom config, register ReverseTool and OpenMeteoTool to the MCP server and run it.""" config = MCPServerConfig(transport="streamable-http", settings=MCPSettings(port=8001)) # optional server = MCPServer(config=config) server.register_many([reverse_tool, OpenMeteoTool()]) server.serve() if __name__ == "__main__": main() ``` ```ts TypeScript [expandable] theme={null} import { StringToolOutput, Tool, ToolEmitter, ToolInput } from "beeai-framework/tools/base"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; import { MCPServer, MCPServerConfig } from "beeai-framework/adapters/mcp/serve/server"; import { Emitter } from "beeai-framework/emitter/emitter"; import { z } from "zod"; export class ReverseTool extends Tool { name = "ReverseTool"; description = "A tool that reverses a word"; public readonly emitter: ToolEmitter, StringToolOutput> = Emitter.root.child({ namespace: ["tool", "reverseTool"], creator: this, }); inputSchema() { return z.object({ word: z.string(), }); } protected async _run(input: ToolInput): Promise { return new StringToolOutput(input.word.split("").reverse().join("")); } } // create an MCP server with custom config, register ReverseTool and OpenMeteoTool and run it const config = new MCPServerConfig({ transport: "streamable-http", port: 8001 }); const server = new MCPServer(config); server.registerMany([new ReverseTool(), new OpenMeteoTool()]); await server.serve(); ``` You can also register agents, chat models, or any other runnable. ### Configuration The MCP Server Adapter uses the `MCPServerConfig` class to configure the MCP server: ```py Python [expandable] theme={null} class MCPServerConfig(BaseModel): transport: Literal["stdio", "sse", "streamable-http"] = "stdio" name: str = "MCP Server" instructions: str | None = None settings: MCPSettings | mcp_server.Settings = Field(default_factory=lambda: MCPSettings()) ``` ```ts TypeScript [expandable] theme={null} export class MCPServerConfig { transport: "stdio" | "sse" | "streamable-http" = "stdio"; hostname = "127.0.0.1"; port = 3000; name = "MCP Server"; version = "1.0.0"; settings?: ServerOptions; } ``` Transport Options: * `stdio`: Uses standard input/output for communication (default) * `streamable-http`: Uses HTTP POST and GET requests (replaces `sse`) * `sse`: Uses server-sent events over HTTP (deprecated) ### Custom Instances If you want to make your custom class compatible with an MCP server, you can do so by registering a custom mapper via `MCPServer.register_factory(...)`. # OpenAI API Source: https://framework.beeai.dev/integrations/openai-api The [OpenAI API](https://platform.openai.com/docs/quickstart) provides a simple interface to state-of-the-art AI models for text generation, natural language processing, computer vision, and more. *** OpenAI Server OpenAIServer allows you to expose your agents and LLMs to external systems that support the Chat completion or Responses API. Key benefits * Fast setup with minimal configuration * Support for [Chat Completion API](https://platform.openai.com/docs/api-reference/chat) and [Responses API](https://platform.openai.com/docs/api-reference/responses) * Register multiple agents and LLMs on a single server * Custom server settings ```py Python [expandable] theme={null} from beeai_framework.adapters.openai.serve.server import OpenAIAPIType, OpenAIServer, OpenAIServerConfig from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.memory import UnconstrainedMemory from beeai_framework.tools.weather import OpenMeteoTool def main() -> None: llm = ChatModel.from_name("ollama:granite4:micro") agent = RequirementAgent( llm=llm, tools=[OpenMeteoTool()], memory=UnconstrainedMemory(), ) server = OpenAIServer( config=OpenAIServerConfig( port=9998, api=OpenAIAPIType.RESPONSES, ) ) server.register(agent, name="agent") server.register(llm) server.serve() if __name__ == "__main__": main() ``` ```ts TypeScript [expandable] theme={null} // coming soon ``` You can easily call the exposed entities via cURL. ```sh Responses API theme={null} curl --location 'http://0.0.0.0:9998/responses' \ --header 'Content-Type: application/json' \ --data '{ "model": "agent", "conversation": "123", "stream": false, "input": "Hello, how are you?" }' ``` ```sh Chat Completion API theme={null} curl --location 'http://0.0.0.0:9998/chat/completions' \ --header 'Content-Type: application/json' \ --data '{ "model": "agent", "stream": false, "messages": [ { "role": "user", "content": "Hello, how are you?" } ] }' ``` # IBM watsonx Orchestrate Source: https://framework.beeai.dev/integrations/watsonx-orchestrate IBM watsonx Orchestrate is IBM’s AI-powered automation platform designed to streamline and automate workflows across diverse applications. By leveraging artificial intelligence and pre-built task modules, it empowers users to design, manage, and monitor end-to-end business processes through natural language interactions. As part of the IBM Watsonx suite, IBM watsonx Orchestrate makes automation accessible to both technical and non-technical users, helping organizations operationalize AI in their daily operations. *** ## Prerequisites Before integrating IBM watsonx Orchestrate with the BeeAI Framework, ensure you have the following: * An active **[IBM watsonx Orchestrate](https://www.ibm.com/products/watsonx-orchestrate)** account. * The **BeeAI Framework** installed: ```sh theme={null} pip install beeai-framework ``` * The **IBM watsonx Orchestrate extension** for BeeAI: ```sh theme={null} pip install 'beeai-framework[watsonx-orchestrate]' ``` *** Consuming from IBM watsonx Orchestrate (client) The `WatsonxOrchestrateAgent` class enables you to connect to any native agent hosted on IBM watsonx Orchestrate. This allows your BeeAI-powered applications to interact with IBM watsonx Orchestrate agents programmatically. ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.adapters.watsonx_orchestrate.agents import WatsonxOrchestrateAgent from beeai_framework.errors import FrameworkError from examples.helpers.io import ConsoleReader async def main() -> None: reader = ConsoleReader() agent = WatsonxOrchestrateAgent( # To find your instance URL, visit IBM watsonx Orchestrate -> Settings -> API Details # Example: https://api.eu-de.watson-orchestrate.cloud.ibm.com/instances/aaaaaa-bbbb-cccc-dddd-eeeeeeeee instance_url="YOUR_INSTANCE_URL", # To find agent's ID, visit IBM watsonx Orchestrate -> Select any existing agent -> copy the last part of the URL () # Example: 1xfa8c27-6d0f-4962-9eb5-4e1c0b8073d8 agent_id="YOUR_AGENT_ID", # Auth type, typically IAM (hosted version) or JWT for custom deployments auth_type="iam", # To find your API Key, visit IBM watsonx Orchestrate -> Settings -> API Details -> Generate API Key api_key="YOUR_API_KEY", ) for prompt in reader: response = await agent.run(prompt) reader.write("Agent 🤖 : ", response.last_message.text) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} // COMING SOON ``` *** Exposing to IBM watsonx Orchestrate (server) The `WatsonxOrchestrateServer` allows you to expose BeeAI agents as HTTP server with a chat completion endpoint compatible with IBM watsonx Orchestrate. This enables you to register and use your local BeeAI agents as external agents within IBM watsonx Orchestrate. ```py Python [expandable] theme={null} from beeai_framework.adapters.watsonx_orchestrate import WatsonxOrchestrateServer, WatsonxOrchestrateServerConfig from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.memory import UnconstrainedMemory from beeai_framework.serve.utils import LRUMemoryManager from beeai_framework.tools.weather import OpenMeteoTool def main() -> None: llm = ChatModel.from_name("ollama:granite4:micro") agent = RequirementAgent(llm=llm, tools=[OpenMeteoTool()], memory=UnconstrainedMemory(), role="a weather agent") config = WatsonxOrchestrateServerConfig(port=8080, host="0.0.0.0", api_key=None) # optional # use LRU memory manager to keep limited amount of sessions in the memory server = WatsonxOrchestrateServer(config=config, memory_manager=LRUMemoryManager(maxsize=100)) server.register(agent) # start an API with /chat/completions endpoint which is compatible with Watsonx Orchestrate server.serve() if __name__ == "__main__": main() ``` ```ts TypeScript [expandable] theme={null} // COMING SOON ``` You can't consume local agents in the hosted version. To use your agents in IBM watsonx Orchestrate, first deploy the server, then register it in the IBM watsonx Orchestrate UI or CLI. # Quickstart Source: https://framework.beeai.dev/introduction/quickstart Get up and running with the BeeAI framework Chose your prefered programming language and get started with the BeeAI Framework starter template. ```bash Python theme={null} git clone https://github.com/i-am-bee/beeai-framework-py-starter.git cd beeai-framework-py-starter ``` ```bash TypeScript theme={null} git clone https://github.com/i-am-bee/beeai-framework-ts-starter.git cd beeai-framework-ts-starter ``` If you're using python, make sure you have [uv](https://docs.astral.sh/uv/getting-started/installation/) installed. ```bash Python theme={null} uv sync ``` ```bash TypeScript theme={null} nvm install && nvm use npm ci ``` Create an `.env` file with the contents from `.env.template` ```bash macOS/Linux theme={null} cp .env.template .env ``` ```bash Windows (PowerShell) theme={null} Copy-Item .env.template .env ``` ```bash Windows (CMD) theme={null} copy .env.template .env ``` If you choose to run a local model, [Ollama](https://ollama.com/) must be installed and running, with the [granite3.3](https://ollama.com/library/granite3.3) model pulled. If you run into issues, run `ollama list` to verify the model name and ensure `granite3.3` is installed or that your alias points to it. ```bash shell theme={null} ollama pull granite3.3 ``` or If you chose to use a hosted model, edit the `LLM_CHAT_MODEL_NAME` in the `.env` file. ```bash example theme={null} # Examples (pick one that matches your provider/account): LLM_CHAT_MODEL_NAME="openai:gpt-5-mini" ``` Add your API key for your preferred provider to your `.env` file and uncomment the line. ```bash example theme={null} # Examples (pick one that matches your provider/account): OPENAI_API_KEY="YOUR API KEY" ``` This agent is an activity planner that can help you plan your day. Prompt it with your task and location. Exit the loop by typing "q" and enter. Take a look inside the code file to understand the example agent. ```bash Python theme={null} uv run python beeai_framework_starter/agent.py ``` ```bash TypeScript theme={null} npm run start src/agent.ts ``` Congradulations! You've ran your first BeeAI agent. Explore more examples in our [Python](https://github.com/i-am-bee/beeai-framework/tree/main/python/examples) and [TypeScript](https://github.com/i-am-bee/beeai-framework/tree/main/typescript/examples) libraries. # The Grand Tour Source: https://framework.beeai.dev/introduction/tour Take a guided tour through the BeeAI Framework, building agents from the basics to advanced capabilities This step-by-step guide shows how to start with your first agent and progressively add tools, debugging, reasoning, knowledge, and more. Each section introduces a single capability, so you can follow the full path or jump to the parts most useful to you. ## Journey Overview Here’s a quick map of the stages and modules: Your first chat agent using Agent, Backend, and Tool modules Add logging and monitoring to debug your agent's behavior Add reasoning rules, guardrails, and user permissions Ground your agent in data with RAG capabilities Coordinate teams of specialized agents with Workflows Scale with caching and error handling Expose agents as services (MCP, Agent Stack, A2A, IBM wxO) ## Before You Start * **Python 3.11+** * **BeeAI Framework**: `pip install 'beeai-framework[wikipedia]'` * **Ollama** running locally: [Download Ollama](https://ollama.com/) * **Model downloaded**: `ollama pull granite3.3` You can also use other LLM providers like OpenAI, Anthropic, or watsonx - see [Backend](../modules/backend) to learn more about supported providers. *** ## Foundation ### Your First Agent **Relevant Modules**: [Agent](../modules/agents), [Backend](../modules/backend) Let's start with the simplest possible agent - one that can respond to messages. ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel async def main(): agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), role="friendly AI assistant", instructions="Be helpful and conversational in all your interactions." ) response = await agent.run("Hello! What can you help me with?") print(response.last_message.text) if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} import { RequirementAgent } from "beeai-framework/agents/requirement/agent"; import { ChatModel } from "beeai-framework/backend/chat"; const agent = new RequirementAgent({ llm: await ChatModel.fromName("ollama:granite3.3"), role: "friendly AI assistant", instructions: "Be helpful and conversational in all your interactions.", }); const response = await agent.run({ prompt: "Hello! What can you help me with?" }); console.log(response.result.text); ``` **Try it:** 1. Save as `simple_agent.py` 2. Run `python simple_agent.py` 3. Test different prompts **Troubleshooting** Verify it's running: `ollama list`
Start the service: `ollama serve`
Pull the model: `ollama pull granite3.3`
List available models: `ollama list`
Create an alias: If your granite model doesn't have the name `granite3.3` give it the alias by trying this command in your terminal `ollama cp `
Update to the latest version: `pip install --upgrade beeai-framework`
Check Python version: `python --version` (must be >= 3.11)
### Add Real-World Knowledge Related Module: [Tools](https://framework.beeai.dev/modules/tools) Give your agent the ability to access real-world information, external systems, or running code by adding tools. ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.tools.search.wikipedia import WikipediaTool from beeai_framework.tools.weather import OpenMeteoTool async def main(): agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), role="friendly AI assistant", instructions="Be helpful and conversational in all your interactions. Use your tools to find accurate, current information.", tools=[WikipediaTool(), OpenMeteoTool()], ) response = await agent.run("What's the current weather in New York and tell me about the history of the city?") print(response.last_message.text) if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} import { RequirementAgent } from "beeai-framework/agents/requirement/agent"; import { ChatModel } from "beeai-framework/backend/chat"; import { WikipediaTool } from "beeai-framework/tools/search/wikipedia"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; const agent = new RequirementAgent({ llm: await ChatModel.fromName("ollama:granite3.3"), role: "friendly AI assistant", instructions: "Be helpful and conversational in all your interactions. Use your tools to find accurate, current information.", tools: [new WikipediaTool(), new OpenMeteoTool()], }); const response = await agent.run({ prompt: "What's the current weather in New York and tell me about the history of the city?", }); console.log(response.result.text); ``` Try these prompts: * "What's the weather in different cities around the world?" * "Tell me about quantum computing and the current weather in CERN's location" * "Compare the weather in New York and London, then tell me about their geographical similarity" Learn more about the [RequirementAgent](/modules/agents/requirement-agent), BeeAI's suggested agent implementation for reliability and control over agent behavior. *** ## Debugging Related Modules: [Emitter](../modules/emitter), [Events](../modules/events), [Observability](../modules/observability). Knowing what your application is doing is essential from the very start. The BeeAI Framework is based on the event system; each component in the framework emits events throughout its execution. You can listen and alter these events to build custom logic. ### Framework Insights The most simple way to see what's happening in your application is by using `GlobalTrajectoryMiddleware` which listens to all events and prints them to the console. ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools import Tool async def main(): agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), tools=[OpenMeteoTool()] ) response = await agent.run("What's the current weather in Paris?").middleware( GlobalTrajectoryMiddleware(included=[Tool])) # Only show tool executions print(response.last_message.text) if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} import { RequirementAgent } from "beeai-framework/agents/requirement/agent"; import { ChatModel } from "beeai-framework/backend/chat"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; import { GlobalTrajectoryMiddleware } from "beeai-framework/middleware/trajectory"; import { Tool } from "beeai-framework/tools/base"; const agent = new RequirementAgent({ llm: await ChatModel.fromName("ollama:granite3.3"), tools: [new OpenMeteoTool()], }); const response = await agent .run({ prompt: "What's the current weather in Paris?" }) .middleware(new GlobalTrajectoryMiddleware({ included: [Tool] })); // Only show tool executions console.log(response.result.text); ``` `Middleware` can be attached per-run (affects only that run) or at agent construction (applies to all runs, no need to attach each time). ### Catching events Sometimes you want to react to specific events. To see which events are emitted, you can use the `on` function. ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools import Tool async def main(): agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), tools=[OpenMeteoTool()] ) response = await agent.run("What's the current weather in Paris?").on("*.*", lambda data, event: print(event.name, data)) print(response.last_message.text) if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} import { RequirementAgent } from "beeai-framework/agents/requirement/agent"; import { ChatModel } from "beeai-framework/backend/chat"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; const agent = new RequirementAgent({ llm: await ChatModel.fromName("ollama:granite3.3"), tools: [new OpenMeteoTool()], }); const response = await agent .run({ prompt: "What's the current weather in Paris?" }) .observe((emitter) => { emitter.match("*.*", (data, event) => console.log(event.name, data)); }); console.log(response.result.text); ``` Listening for all events can be noisy. Filter to the events you are interested in capturing. Alternatively, you can listen for events on the class itself rather than for a specific run. ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.tools.weather import OpenMeteoTool from typing import Any from beeai_framework.emitter import EventMeta async def main(): agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), tools=[OpenMeteoTool()] ) @agent.emitter.on("*.*") async def handle_event(data: Any, event: EventMeta): print(event.name, data) response = await agent.run("What's the current weather in Paris?") print(response.last_message.text) if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} import { RequirementAgent } from "beeai-framework/agents/requirement/agent"; import { ChatModel } from "beeai-framework/backend/chat"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; const agent = new RequirementAgent({ llm: await ChatModel.fromName("ollama:granite3.3"), tools: [new OpenMeteoTool()], }); agent.emitter.match("*.*", (data, event) => { console.log(event.name, data); }); const response = await agent.run({ prompt: "What's the current weather in Paris?" }); console.log(response.result.text); ``` All events and appropriate typings are stored in the `events.py` file in the given module. All emitters inherit from the root emitter, which is accessible through `Emitter.root()`. You can use this to monitor all events happening in the application. ### Logging **Relevant Module**: [Logger](../modules/logger) Logging is a way to provide visibility into the state of your application. Set the `Logger` level of granularity and place logging statements at key points throughout your agent process. ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.tools.search.wikipedia import WikipediaTool from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools import Tool from beeai_framework.logger import Logger async def main(): # You create the logger and decide what to log logger = Logger("my-agent", level="TRACE") logger.info("Starting agent application") agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), role="friendly AI assistant", instructions="Be helpful and conversational in all your interactions. Use your tools to find accurate, current information.", tools=[WikipediaTool(), OpenMeteoTool()] ) logger.debug("About to process user message") # The `included` parameter filters what types of operations to trace: # - [Tool]: Show only tool executions (function calls, API calls, etc.) # - [ChatModel]: Show only LLM calls (model inference, token usage) # - [Tool, ChatModel]: Show both tools and LLM interactions # - [] or None: Show everything (agents, tools, models, requirements) response = await agent.run( "What's the weather in Paris and tell me about the Eiffel Tower?" ).middleware(GlobalTrajectoryMiddleware(included=[Tool])) logger.info("Agent response generated") print(response.last_message.text) if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` **Logger Output**: Traditional log messages with timestamps ``` 2024-01-15 10:30:45 | INFO | my-agent - Starting agent application 2024-01-15 10:30:45 | DEBUG | my-agent - About to process user message 2024-01-15 10:30:47 | INFO | my-agent - Agent response generated successfully ``` ### OpenTelemetry / OpenInference Logging to the console is great for development, but it's not enough for production monitoring. You can easily let the framework send traces and metrics to external platforms like Arize Phoenix, LangFuse, LangSmith, and more. To run this step: `pip install openinference-instrumentation-beeai opentelemetry-sdk opentelemetry-exporter-otlp` #### Set the Endpoint Set the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable. Some vendors also need API kesy like `OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer ` ```bash Linux/macOS theme={null} export OTEL_EXPORTER_OTLP_ENDPOINT="https://your-otel-endpoint" ``` ```bash Windows Powershell theme={null} $env:OTEL_EXPORTER_OTLP_ENDPOINT="https://your-otel-endpoint" ``` ```py Python [expandable] theme={null} from openinference.instrumentation.beeai import BeeAIInstrumentor from opentelemetry import trace as trace_api from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk import trace as trace_sdk from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace.export import SimpleSpanProcessor # Use BatchSpanProcessor in prod def setup_observability() -> None: tracer_provider = trace_sdk.TracerProvider(resource=Resource.create({})) tracer_provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter())) trace_api.set_tracer_provider(tracer_provider) BeeAIInstrumentor().instrument() # auto-instruments BeeAI Framework # Call this BEFORE creating/running any BeeAI agents setup_observability() ``` ```ts TypeScript [expandable] theme={null} // Note: OpenTelemetry instrumentation for TypeScript is coming soon // For now, use standard OpenTelemetry setup with manual instrumentation ``` Run your application and you should see traces and metrics in your selected dashboard. ## Enforce Rules with the `RequirementAgent` Use **requirements** to control the agent's behavior. Let’s add the `ThinkTool` and set up a `ConditionalRequirement` to enforce rules on when and how tools should be used. Learn more about the [RequirementAgent](/modules/agents/requirement-agent) ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.agents.requirement.requirements.conditional import ConditionalRequirement from beeai_framework.backend import ChatModel from beeai_framework.tools.search.wikipedia import WikipediaTool from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.tools.think import ThinkTool from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools import Tool from beeai_framework.logger import Logger async def main(): logger = Logger("my-agent", level="TRACE") logger.info("Starting agent application") agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), role="friendly AI assistant", instructions="Be helpful and conversational in all your interactions. Use your tools to find accurate, current information.", tools=[WikipediaTool(), OpenMeteoTool(), ThinkTool()], requirements=[ # Force agent to think before acting, and after each tool use ConditionalRequirement( ThinkTool, force_at_step=1, # Always think first force_after=Tool, # Think after using any tool consecutive_allowed=False # Don't think twice in a row ) ], middlewares=[GlobalTrajectoryMiddleware(included=[Tool])] ) logger.debug("About to process user message") response = await agent.run( "What's the weather in Paris and tell me about the Eiffel Tower?" ) logger.info("Agent response generated") logger.info(response.last_message.text) if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} import { RequirementAgent } from "beeai-framework/agents/requirement/agent"; import { ConditionalRequirement } from "beeai-framework/agents/requirement/requirements/conditional"; import { ChatModel } from "beeai-framework/backend/chat"; import { WikipediaTool } from "beeai-framework/tools/search/wikipedia"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; import { ThinkTool } from "beeai-framework/tools/think"; import { GlobalTrajectoryMiddleware } from "beeai-framework/middleware/trajectory"; import { Tool } from "beeai-framework/tools/base"; import { Logger } from "beeai-framework/logger/logger"; const logger = new Logger({ name: "my-agent", level: "trace" }); logger.info("Starting agent application"); const agent = new RequirementAgent({ llm: await ChatModel.fromName("ollama:granite3.3"), role: "friendly AI assistant", instructions: "Be helpful and conversational in all your interactions. Use your tools to find accurate, current information.", tools: [new WikipediaTool(), new OpenMeteoTool(), new ThinkTool()], requirements: [ // Force agent to think before acting, and after each tool use new ConditionalRequirement(ThinkTool, { forceAtStep: 1, // Always think first forceAfter: [Tool], // Think after using any tool consecutiveAllowed: false, // Don't think twice in a row }), ], middlewares: [new GlobalTrajectoryMiddleware({ included: [Tool] })], }); logger.debug("About to process user message"); const response = await agent.run({ prompt: "What's the weather in Paris and tell me about the Eiffel Tower?", }); logger.info("Agent response generated"); logger.info(response.result.text); ``` Learn more about Requirements and see examples in [documentation](/modules/agents/requirement-agent) ### Request User Permission with the `AskPermissionRequirement` Add user permission for when you want an action to be human validated before being executed: ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.agents.requirement.requirements.conditional import ConditionalRequirement from beeai_framework.agents.experimental.requirements.ask_permission import AskPermissionRequirement from beeai_framework.backend import ChatModel from beeai_framework.tools.search.wikipedia import WikipediaTool from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.tools.think import ThinkTool from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools import Tool from beeai_framework.logger import Logger async def main(): logger = Logger("my-agent", level="TRACE") logger.info("Starting agent application") agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3:8b"), role="friendly AI assistant", instructions="Be helpful and conversational in all your interactions. Use your tools to find accurate, current information.", tools=[WikipediaTool(), OpenMeteoTool(), ThinkTool()], requirements=[ ConditionalRequirement( ThinkTool, force_at_step=1, force_after=Tool, consecutive_allowed=False ), AskPermissionRequirement([OpenMeteoTool]) # Ask before using weather API ] ) logger.debug("About to process user message") response = await agent.run( "What's the weather in Paris?" ).middleware(GlobalTrajectoryMiddleware(included=[Tool])) logger.info("Agent response generated") print(response.last_message.text) if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} // Note: AskPermissionRequirement is not yet implemented in TypeScript // Coming soon ``` In the console, the output looks like: ``` Do you allow it? (yes/no): yes ``` *** ## Knowledge Now it's time to integrate data. from a vector store using RAG (retrieval augmented generation) Relevant Module: [RAG](../modules/rag) Install the RAG extras (if you haven’t already): `pip install "beeai-framework[rag]"`
Pull the `nomic-embed-text` model in Ollama.
Create synthetic or non-synthetic Markdown files to ingest into your vector store.
Let's give your agent access to a knowledge base of documents. ### Setup the Vector Store, Pre-process, and Load the Documents 1. Create a new file and name it `step1_knowledge_base` 2. Copy the following code into the file and replace the `file_paths` with your own files ```py Python [expandable] theme={null} import asyncio from beeai_framework.backend.document_loader import DocumentLoader from beeai_framework.backend.embedding import EmbeddingModel from beeai_framework.backend.text_splitter import TextSplitter from beeai_framework.backend.vector_store import VectorStore async def setup_knowledge_base(): # Create embedding model using Ollama embedding_model = EmbeddingModel.from_name("ollama:nomic-embed-text") # Create vector store vector_store = VectorStore.from_name( "beeai:TemporalVectorStore", embedding_model=embedding_model ) # Setup text splitter for chunking documents text_splitter = TextSplitter.from_name( "langchain:RecursiveCharacterTextSplitter", chunk_size=1000, chunk_overlap=200 ) return vector_store, text_splitter async def load_documents(vector_store, text_splitter, file_paths): """Load documents into the vector store""" all_chunks = [] for file_path in file_paths: try: # Load the document loader = DocumentLoader.from_name( "langchain:UnstructuredMarkdownLoader", file_path=file_path ) documents = await loader.load() # Split into chunks chunks = await text_splitter.split_documents(documents) all_chunks.extend(chunks) print(f"Loaded {len(chunks)} chunks from {file_path}") except Exception as e: print(f"Failed to load {file_path}: {e}") # Add all chunks to vector store if all_chunks: await vector_store.add_documents(all_chunks) print(f"Total chunks added: {len(all_chunks)}") return vector_store if all_chunks else None async def main(): # Setup the knowledge base vector_store, text_splitter = await setup_knowledge_base() # Replace with your actual markdown files file_paths = [ "your_document1.md", #replace these documents with the path to your local document "your_document2.md", #replace these documents with the path to your local document ] # Load documents loaded_vector_store = await load_documents(vector_store, text_splitter, file_paths) if loaded_vector_store: print("Knowledge base ready!") return loaded_vector_store else: print("No documents loaded") return None if __name__ == "__main__": # Run this first to setup your knowledge base asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` ### Create RAG-Enabled Agent 3. Create a new file that imports the helper functions from the `step1_knowledge_base` file and uses the vector store setup in the previous step 4. Copy the following code into a new file and replace the `file_paths` with your own paths ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.tools.search.wikipedia import WikipediaTool from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.tools.search.retrieval import VectorStoreSearchTool # Import the setup function from Step 1 from step1_knowledge_base import setup_knowledge_base, load_documents async def main(): # Setup knowledge base (from Step 1) vector_store, text_splitter = await setup_knowledge_base() # Load your documents file_paths = [ "your_document1.md", #replace these documents with the path to your local document "your_document2.md", #replace these documents with the path to your local document ] loaded_vector_store = await load_documents(vector_store, text_splitter, file_paths) if not loaded_vector_store: print("No documents loaded - exiting") return # Create RAG tool rag_tool = VectorStoreSearchTool(vector_store=loaded_vector_store) # Create agent with RAG capabilities agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), tools=[WikipediaTool(), OpenMeteoTool(), rag_tool], instructions="""You are a knowledgeable assistant with access to: 1. A document knowledge base (use VectorStoreSearch for specific document queries) 2. Wikipedia for general facts 3. Weather information When users ask about topics that might be in the documents, search your knowledge base first.""" ) # Test the RAG-enabled agent response = await agent.run("What information do you have in your knowledge base?") print(response.last_message.text) if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` 1. Add some markdown files with information about your company/project 2. Ask questions that should be answered from your documents 3. Compare how responses differ with vs. without the knowledge base or when using different pre-processing strategies ## Orchestration Relevant Module: [Workflows](https://framework.beeai.dev/modules/workflows) ### Multi-Agent Hand-offs Create a team of specialized agents that can collaborate: ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.memory import UnconstrainedMemory from beeai_framework.tools.search.wikipedia import WikipediaTool from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.tools.handoff import HandoffTool from beeai_framework.tools.think import ThinkTool from beeai_framework.logger import Logger async def main(): # Initialize logger logger = Logger("multi-agent-system", level="TRACE") logger.info("Starting multi-agent system") # Create specialized agents logger.debug("Creating knowledge agent") knowledge_agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), tools=[ThinkTool(), WikipediaTool()], memory=UnconstrainedMemory(), instructions="Provide detailed, accurate information using available knowledge sources. Think through problems step by step." ) logger.debug("Creating weather agent") weather_agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), tools=[ThinkTool(), OpenMeteoTool()], memory=UnconstrainedMemory(), instructions="Provide comprehensive weather information and forecasts. Always think before using tools." ) # Create a coordinator agent that manages handoffs logger.debug("Creating coordinator agent") coordinator_agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), memory=UnconstrainedMemory(), tools=[ HandoffTool( target=knowledge_agent, name="knowledge_specialist", description="For general knowledge and research questions" ), HandoffTool( target=weather_agent, name="weather_expert", description="For weather-related queries" ), ], instructions="""You coordinate between specialist agents. - For weather queries: use weather_expert - For research/knowledge questions: use knowledge_specialist - For mixed queries: break them down and use multiple specialists Always introduce yourself and explain which specialist will help.""" ) logger.info("Running query: What's the weather in Paris and tell me about its history?") try: response = await coordinator_agent.run("What's the weather in Paris and tell me about its history?") logger.info("Query completed successfully") print(response.last_message.text) except Exception as e: logger.error(f"Error during agent execution: {e}") raise logger.info("Multi-agent system execution completed") if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` 1. Ask the coordinator mixed questions: "What's the weather in Paris and tell me about its history?" 2. Test how it decides which agent to use 3. Try complex queries that need multiple specialists ### Advanced Workflows For complex, multi-step processes, a more advanced workflow system is coming soon! Join the discussion [here](https://github.com/i-am-bee/beeai-framework/discussions/1005) *** ## Production Now it's time for production-grade features. ### Caching for Speed & Efficiency Relevant Module: [Cache](../modules/cache) Caching helps you cut costs, reduce latency, and deliver consistent results by reusing previous computations. In BeeAI Framework, you can cache LLM responses and tool outputs. Caching the entire agent isn’t practical—every agent run is usually unique. Instead, focus on caching the components inside your agent. **1. Caching LLM Calls** Configure a cache on your LLM to avoid paying for repeated queries: ```py Python [expandable] theme={null} import asyncio from beeai_framework.backend import ChatModel, UserMessage from beeai_framework.cache import SlidingCache async def main(): # LLM with caching enabled llm = ChatModel.from_name("ollama:granite3.3") llm.config(cache=SlidingCache(size=50)) # Cache up to 50 responses # Must send a list of messages to the llm messages = [UserMessage("Hello, how are you?")] # First call (miss) res1 = await llm.run(messages) # Second call with identical input (hit) res2 = await llm.run(messages) print("First:", res1.last_message.text) print("Second (cached):", res2.last_message.text) asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` **2. Caching Tool Outputs** Many tools query APIs or perform expensive lookups. You can attach a cache directly: ```py Python [expandable] theme={null} from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.cache import UnconstrainedCache # Cache all results from the weather API weather_tool = OpenMeteoTool(options={"cache": UnconstrainedCache()}) ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` **3. Using Cached Components in an Agent** ```py Python [expandable] theme={null} from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.cache import UnconstrainedCache from beeai_framework.memory import UnconstrainedMemory from beeai_framework.backend import ChatModel from beeai_framework.cache import SlidingCache import asyncio async def main(): llm = ChatModel.from_name("ollama:granite3.3") llm.config(cache=SlidingCache(size=50)) # Cache up to 50 responses weather_tool = OpenMeteoTool(options={"cache": UnconstrainedCache()}) agent = RequirementAgent( llm=llm, # LLM with cache tools=[weather_tool], # Tool with cache memory=UnconstrainedMemory(), instructions="Provide answers efficiently using cached results when possible." ) response1 = await agent.run("What's the weather in New York?") response2 = await agent.run("What's the weather in New York?") # Weather tool cache should hit asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` Agent-level caching is rarely effective because every input is usually unique. Focus on caching LLMs and tools individually for best performance. ### Handle Errors Gracefully Relevant Module: [Errors](https://framework.beeai.dev/modules/errors) Make your system robust with comprehensive error management: ```py Python [expandable] theme={null} import asyncio import traceback from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.memory import UnconstrainedMemory from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.errors import FrameworkError async def main(): try: agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), memory=UnconstrainedMemory(), tools=[OpenMeteoTool()], instructions="You provide weather information." ) response = await agent.run("What's the weather in Invalid-City-Name?") print(response.last_message.text) except FrameworkError as e: print(f"Framework error occurred: {e.explain()}") traceback.print_exc() except Exception as e: print(f"Unexpected error: {e}") traceback.print_exc() if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` *** ## Integration Relevant Module: [Serve](../modules/serve), [MCP](../integrations/mcp), [A2A](../integrations/a2a), [IBM watsonX Orchestrate](../integrations/watsonx-orchestrate) ### Model Context Protocol (MCP) Expose your agent as an **MCP server**: ```py Python [expandable] theme={null} from beeai_framework.adapters.mcp import MCPServer from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.tools.search.wikipedia import WikipediaTool def main(): # Create an MCP server server = MCPServer() # Register tools that can be used by MCP clients server.register_many([ OpenMeteoTool(), WikipediaTool() ]) # Start the server server.serve() if __name__ == "__main__": main() ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` ### Agent Stack Expose your agent as a **Agent Stack server**: Make sure you have done the following: pip install `'beeai-framework[a2a]' 'beeai-framework[agentstack]'` and you have a compatible version of `uvicorn` installed. ```py Python [expandable] theme={null} from beeai_framework.adapters.agentstack.serve.server import AgentStackServer from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.memory import UnconstrainedMemory from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools.search.wikipedia import WikipediaTool from beeai_framework.tools.weather import OpenMeteoTool def main(): llm = ChatModel.from_name("ollama:granite3.3") agent = RequirementAgent( llm=llm, tools=[WikipediaTool(), OpenMeteoTool()], memory=UnconstrainedMemory(), middlewares=[GlobalTrajectoryMiddleware()], instructions="You are a helpful research assistant with access to Wikipedia and weather data." ) # Runs HTTP server that registers to Agent Stack server = AgentStackServer(config={"configure_telemetry": False}) server.register(agent) server.serve() if __name__ == "__main__": main() ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` ### Agent2Agent (A2A) Protocol Expose your agent as an **A2A server**: Make sure you have done the following: pip install `'beeai-framework[a2a]'` and you have a compatible version of `uvicorn` installed. ```py Python [expandable] theme={null} from beeai_framework.adapters.a2a import A2AServer, A2AServerConfig from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.memory import UnconstrainedMemory from beeai_framework.serve.utils import LRUMemoryManager from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool from beeai_framework.tools.weather import OpenMeteoTool def main() -> None: llm = ChatModel.from_name("ollama:granite3.3") agent = RequirementAgent( llm=llm, tools=[DuckDuckGoSearchTool(), OpenMeteoTool()], memory=UnconstrainedMemory(), ) # Register the agent with the A2A server and run the HTTP server # we use LRU memory manager to keep limited amount of sessions in the memory A2AServer(config=A2AServerConfig(port=9999), memory_manager=LRUMemoryManager(maxsize=100)).register(agent).serve() if __name__ == "__main__": main() ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` ### IBM watsonx Orchestrate Expose your agent as an **IBM watsonx Orchestrate server**: ```py Python [expandable] theme={null} from beeai_framework.adapters.watsonx_orchestrate import WatsonxOrchestrateServer, WatsonxOrchestrateServerConfig from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.memory import UnconstrainedMemory from beeai_framework.serve.utils import LRUMemoryManager from beeai_framework.tools.weather import OpenMeteoTool def main() -> None: llm = ChatModel.from_name("ollama:granite3.3") agent = RequirementAgent( llm=llm, tools=[OpenMeteoTool()], memory=UnconstrainedMemory(), instructions="You are a weather agent that provides accurate weather information." ) config = WatsonxOrchestrateServerConfig(port=8080, host="0.0.0.0", api_key=None) # optional # use LRU memory manager to keep limited amount of sessions in the memory server = WatsonxOrchestrateServer(config=config, memory_manager=LRUMemoryManager(maxsize=100)) server.register(agent) # start an API with /chat/completions endpoint which is compatible with Watsonx Orchestrate server.serve() if __name__ == "__main__": main() ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` *** ## What's Next? Congratulations! You've built a complete AI agent system from a simple chat bot to a production-ready, multi-agent workflow with knowledge bases, caching, error handling, and service endpoints. Each module page includes detailed guides, examples, and best practices. Here are some next steps: 1. **Explore Modules:** Dive deeper into specific modules that interest you 2. **Scale Your System:** Add more agents, tools, and knowledge bases 3. **Custom Tools:** Build your own tools for domain-specific functionality The framework is designed to scale with you. Start simple, then grow your system step by step as your needs evolve. You now have all the building blocks to create sophisticated and reliable AI agent systems! # Welcome to the BeeAI Framework Source: https://framework.beeai.dev/introduction/welcome Build reliable and production-ready multi-agent systems with our lightweight framework in Python or TypeScript **BeeAI Framework** is an open-source framework for building production-grade multi-agent systems. It is hosted by the Linux Foundation under open governance, ensuring transparency, community-driven development, and enterprise-grade stability. BeeAI goes beyond simple prompting by providing a lightweight yet powerful approach to reliable agent development, with **built-in constraint enforcement** and rule-based governance that preserves reasoning abilities while ensuring **predictable behavior**. The BeeAI Framework provides the flexibility and performance needed for scalable AI systems, supporting both **Python** and **TypeScript** with complete feature parity. ## Key Features | **Feature** | **Description** | | :-------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- | | **Production Optimization** | Built-in caching, memory optimization, and resource management for scalable deployment | | **Agents with Constraints** | Preserve your agent's reasoning abilities while enforcing deterministic rules instead of suggesting behavior | | **Dynamic Workflows** | Use simple decorators to design multi-agent systems with advanced patterns like parallelism, retries, and replanning | | **Declarative Orchestration** | Define complex agent systems in YAML for more predictable and maintainable orchestration | | **Pluggable Observability** | Integrate with your existing stack in minutes with native OpenTelemetry support for real-time monitoring, auditing, and detailed tracing | | **MCP and A2A Native** | Build MCP-compatible components, equip agents with MCP tools, and interoperate with any MCP or A2A agent system | | **Provider Agnostic** | Supports 10+ LLM providers including Ollama, Groq, OpenAI, Watsonx.ai, and more with seamless switching | | **Python and TypeScript Support** | Complete feature parity between Python and TypeScript implementations lets teams build with the tools they already know and love | *** ## Join the Community Support Announcements Tutorials # Agents Source: https://framework.beeai.dev/modules/agents ## Overview An **AI Agent** is a system built on language models (LLMs or SLMs) that can solve complex tasks through structured reasoning and autonomous or human-assisted actions. The BeeAI Framework serves as the orchestration layer that enables agents to do this and more: * Coordinate with LLMs: Manages communication between your agent and language models * Tool Management: Provides agents with access to external tools and handles their execution * Response Processing: Processes and validates tool outputs and model responses * Memory Management: Maintains conversation context and state across interactions * Error Handling: Manages retries, timeouts, and graceful failure recovery * Event Orchestration: Emits detailed events for monitoring and debugging agent behavior Unlike basic chatbots, agents built with this framework can perform multi-step reasoning, use tools to interact with external systems, maintain context across interactions, and adapt based on feedback. These capabilities make them ideal for planning, research, analysis, and complex execution tasks. Dive deeper into the concepts behind AI agents in this [research article](https://research.ibm.com/blog/what-are-ai-agents-llm) from IBM. Supported in Python and TypeScript. ## Customizing Agent Behavior You can customize your agent's behavior in several key ways: ### 1. Configuring the Language Model Backend The backend system manages your connection to different language model providers. The BeeAI Framework supports multiple LLM providers through a unified interface. Learn more about available backends and how to set their parameters in our [backend documentation](./backend). ```py Python [expandable] theme={null} from beeai_framework.backend import ChatModel from beeai_framework.agents.requirement import RequirementAgent # Using Ollama (local models) llm = ChatModel.from_name("ollama:granite3.3") # Using OpenAI llm = ChatModel.from_name("openai:gpt-5-mini") # Using Anthropic llm = ChatModel.from_name("anthropic:claude-sonnet-4") agent = RequirementAgent( llm=llm, # ... other configuration ) ``` ```ts TypeScript [expandable] theme={null} import { RequirementAgent } from "beeai-framework/agents/requirement/agent"; import { ChatModel } from "beeai-framework/backend/chat"; # Using Ollama (local models) llm = await ChatModel.from_name("ollama:granite3.3") # Using OpenAI llm = await ChatModel.from_name("openai:gpt-5-mini") # Using Anthropic llm = await ChatModel.from_name("anthropic:claude-sonnet-4") agent = new RequirementAgent({ llm, # ... other configuration }) ``` **Backend Features:** * Unified Interface: Work with different providers using the same API * Model Parameters: Configure temperature, max tokens, and other model settings * Provider Support: OpenAI, Anthropic, Ollama, Groq, and more * Local & Cloud: Support for both local and cloud-hosted models ### 2. Setting the System Prompt The system prompt defines your agent's behavior, personality, and capabilities. You can configure this through several parameters when initializing an agent: ```py Python [expandable] theme={null} agent = RequirementAgent( llm="ollama:granite4:micro", role="You are a helpful research assistant specializing in academic papers", instructions=[ "Always provide citations for your sources", "Focus on peer-reviewed research when possible", "Explain complex concepts in simple terms" ], notes=[ "Be especially careful about medical or legal advice", "If unsure about a fact, acknowledge the uncertainty" ], name="Research Assistant", description="An AI agent that helps with academic research tasks" ) ``` ```ts TypeScript [expandable] theme={null} const agent = new RequirementAgent({ llm: await ChatModel.from_name("ollama:granite4:micro"), role: "You are a helpful research assistant specializing in academic papers", instructions: [ "Always provide citations for your sources", "Focus on peer-reviewed research when possible", "Explain complex concepts in simple terms" ], notes: [ "Be especially careful about medical or legal advice", "If unsure about a fact, acknowledge the uncertainty" ], name: "Research Assistant", description: "An AI agent that helps with academic research tasks" }); ``` **Prompt Parameters:** * `role`: Defines the agent's persona and primary function * `instructions`: List of specific behavioral guidelines * `notes`: Additional context or special considerations * `name` and `description`: Help identify the agent's purpose and are helpful when using the `HandoffTool`, `Serve` module, and when you need to access agent metadata via `agent.meta` Setting instructions, notes, role will be integrated into the framework provided system prompt template. If you want to completely override the framework provided system prompt template, you can provide a custom [prompt template](./templates). ### 3. Configuring Agent Run Options When executing an agent, you can provide additional options to guide its behavior and execution settings: #### Setting Execution Settings and Guiding Agent Run Behavior ```py Python [expandable] theme={null} response = await agent.run( Analyze the latest AI research trends", expected_output="A structured summary with key findings and recommendations", # expected_output can also receive a Pydantic Model or a JSON Schema backstory="The user is preparing for a conference presentation on AI trends", total_max_retries=5, max_retries_per_step=2, max_iterations=15, ) #structured outputs can be accessed like this: print(response.output_structured) ``` ```ts TypeScript [expandable] theme={null} const schema = z.object({ firstName: z.string().min(1), lastName: z.string().min(1) }); const response = await agent.run({ prompt: "Generate profile of a citizen.", expectedOutput: schema, }); const person = response.state.result console.info(person.firstName, person.lastName); ``` **Available Options:** * `expected_output`: Guides the agent toward a specific unstructured or structured output format. `output_structured` is defined only when `expected_output` is a Pydantic model or a JSON schema. However, the text representation is always available via `response.output`. * `backstory`: Provides additional context to help the agent understand the user's situation * `total_max_retries`: Controls the total number of retry attempts across the entire agent execution * `max_retries_per_step`: Limits retries for individual steps (like tool calls or model responses) * `max_iterations`: Sets the maximum number of reasoning cycles the agent can perform The are defaults set for `max_iterations`, `total_max_retries`, and `max_retries_per_step`, but you can override them by setting your own preferences. ### 4. Adding Tools Enhance your agent's capabilities by providing it with tools to interact with external systems. Learn more about beeai provided tools and creating custom tools in our [tools documentation](./tools). ```py Python [expandable] theme={null} from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool from beeai_framework.tools.weather import OpenMeteoTool agent = RequirementAgent( llm="ollama:granite4", tools=[ DuckDuckGoSearchTool(), OpenMeteoTool(), # Add more tools as needed ] ) ``` ```ts TypeScript [expandable] theme={null} import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; import { ChatModel } from "beeai-framework/backend/chat"; import { RequirementAgent } from "beeai-framework/agents/requirement/agent"; const llm = await ChatModel.from_name("ollama:granite4"); const agent = new RequirementAgent({ llm, tools: [ new OpenMeteoTool(), // weather tool ], }); ``` ### 5. Configuring Memory Memory allows your agent to maintain context across multiple interactions. Different memory types serve different use cases. Learn more about our built in options in the [memory documentation](./memory). ```py Python [expandable] theme={null} from beeai_framework.memory import TokenMemory, UnconstrainedMemory from beeai_framework.agents.requirement import RequirementAgent agent = RequirementAgent( llm="ollama:granite4:micro", memory=TokenMemory(max_tokens=20*1024) ) ``` ```ts TypeScript [expandable] theme={null} import { TokenMemory } from "beeai-framework/memory/tokenMemory"; import { ChatModel } from "beeai-framework/backend/chat"; import { RequirementAgent } from "beeai-framework/agents/requirement/agent"; const agent = new RequirementAgent({ llm: await ChatModel.from_name("ollama:granite4:micro"), memory: new TokenMemory({ maxTokens: 20 * 1024 }) }); ``` ### Additional Agent Options * [Observability & Debugging](./observability): Monitor agent behavior with detailed event tracking and logging systems * [MCP (Model Context Protocol)](../integrations/mcp): Connect to external services and data sources * [A2A (Agent-to-Agent)](../integrations/a2a): Enable multi-agent communication and coordination * [Caching](./cache): Improve performance by caching LLM responses and tool outputs * [Event System](./events): Build reactive applications using the comprehensive emitter framework * [RAG Integration](./rag): Connect your agents to knowledge bases and document stores * [Serialization](./serialization): Save and restore agent state for persistence and deployment * [Error Handling](./errors): Implement robust error recovery and debugging strategies ## Agent Types BeeAI Framework provides several agent implementations: Upcoming change:
The Requirement agent will become the primary supported agent. The ReAct and tool-calling agents will not be actively supported.
### Requirement Agent This is the recommended agent. Currently only supported in Python. This agent provides the reliability needed for production scenarios through a rule system that defines execution constraints while keeping problem-solving flexibility intact. Unlike traditional approaches that require complex orchestration code, RequirementAgent uses a declarative interface where you define requirements and let the framework enforce them automatically. Learn more about RequirementAgent in its dedicated [page](/modules/agents/requirement-agent) or in the [blog post](https://beeai.dev/blog/reliable-ai-agents). ```py Python [expandable] theme={null} from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.agents.requirement.requirements.conditional import ConditionalRequirement from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool from beeai_framework.tools.think import ThinkTool from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.backend import ChatModel from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite4:micro"), tools=[ ThinkTool(), # to reason OpenMeteoTool(), # retrieve weather data DuckDuckGoSearchTool() # search web ], instructions="Plan activities for a given destination based on current weather and events.", requirements=[ # Force thinking first ConditionalRequirement(ThinkTool, force_at_step=1), # Search only after getting weather and at least once ConditionalRequirement(DuckDuckGoSearchTool, only_after=[OpenMeteoTool], min_invocations=1), # Weather tool be used at least once but not consecutively ConditionalRequirement(OpenMeteoTool, consecutive_allowed=False, min_invocations=1), ] ) # Run with execution logging response = await agent.run("What to do in Boston?").middleware(GlobalTrajectoryMiddleware()) print(f"Final Answer: {response.answer.text}") ``` ### Lite Agent Currently only supported in Python. An agent that leverages a language model and a suite of tools to solve problems. **The agent does not have any system prompt**. This design is ideal for: * Exploring the raw capabilities of a language model without bias from a framework’s built‑in prompt * Developers who want to understand how to build an agent from scratch ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.lite import LiteAgent from beeai_framework.backend import ChatModel, ChatModelOutput, ChatModelParameters, SystemMessage from beeai_framework.emitter import EventMeta from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool from beeai_framework.tools.think import ThinkTool from beeai_framework.tools.weather import OpenMeteoTool async def main() -> None: agent = LiteAgent( llm=ChatModel.from_name("ollama:granite4:micro", ChatModelParameters(stream=True)), tools=[ThinkTool(), OpenMeteoTool(), DuckDuckGoSearchTool()], middlewares=[GlobalTrajectoryMiddleware()], ) # Optionally set a custom system prompt await agent.memory.add(SystemMessage("You are a helpful assistant.")) @agent.emitter.on("final_answer") def stream_final_answer(data: ChatModelOutput, meta: EventMeta) -> None: print(data.get_text_content()) # emits chunks await agent.run("Hello") if __name__ == "__main__": asyncio.run(main()) ``` ### ReAct Agent The ReAct Agent is available in both Python and TypeScript, but no longer actively supported. The ReActAgent implements the ReAct ([Reasoning and Acting](https://arxiv.org/abs/2210.03629)) pattern, which structures agent behavior into a cyclical process of reasoning, action, and observation. This pattern allows agents to reason about a task, take actions using tools, observe results, and continue reasoning until reaching a conclusion. Let's see how a ReActAgent approaches a simple question: **Input prompt:** "What is the current weather in Las Vegas?" **First iteration:** ```log theme={null} thought: I need to retrieve the current weather in Las Vegas. I can use the OpenMeteo function to get the current weather forecast for a location. tool_name: OpenMeteo tool_input: {"location": {"name": "Las Vegas"}, "start_date": "2024-10-17", "end_date": "2024-10-17", "temperature_unit": "celsius"} ``` **Second iteration:** ```log theme={null} thought: I have the current weather in Las Vegas in Celsius. final_answer: The current weather in Las Vegas is 20.5°C with an apparent temperature of 18.3°C. ``` During execution, the agent emits partial updates as it generates each line, followed by complete updates. Updates follow a strict order: first all partial updates for "thought," then a complete "thought" update, then moving to the next component. ```py Python [expandable] theme={null} import asyncio import logging import os import sys import tempfile import traceback from typing import Any from dotenv import load_dotenv from beeai_framework.agents.react import ReActAgent from beeai_framework.backend import ChatModel, ChatModelParameters from beeai_framework.emitter import EmitterOptions, EventMeta from beeai_framework.errors import FrameworkError from beeai_framework.logger import Logger from beeai_framework.memory import TokenMemory from beeai_framework.tools import AnyTool from beeai_framework.tools.code import LocalPythonStorage, PythonTool from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool from beeai_framework.tools.search.wikipedia import WikipediaTool from beeai_framework.tools.weather import OpenMeteoTool from examples.helpers.io import ConsoleReader # Load environment variables load_dotenv() # Configure logging - using DEBUG instead of trace logger = Logger("app", level=logging.DEBUG) reader = ConsoleReader() def create_agent() -> ReActAgent: """Create and configure the agent with tools and LLM""" # Other models to try: # "llama3.1" # "granite3.3" # "deepseek-r1" # ensure the model is pulled before running llm = ChatModel.from_name( "ollama:granite4:micro", ChatModelParameters(temperature=0), ) # Configure tools tools: list[AnyTool] = [ WikipediaTool(), OpenMeteoTool(), DuckDuckGoSearchTool(), ] # Add code interpreter tool if URL is configured code_interpreter_url = os.getenv("CODE_INTERPRETER_URL") if code_interpreter_url: tools.append( PythonTool( code_interpreter_url, LocalPythonStorage( local_working_dir=tempfile.mkdtemp("code_interpreter_source"), interpreter_working_dir=os.getenv("CODE_INTERPRETER_TMPDIR", "./tmp/code_interpreter_target"), ), ) ) # Create agent with memory and tools agent = ReActAgent(llm=llm, tools=tools, memory=TokenMemory(llm)) return agent def process_agent_events(data: Any, event: EventMeta) -> None: """Process agent events and log appropriately""" if event.name == "error": reader.write("Agent 🤖 : ", FrameworkError.ensure(data.error).explain()) elif event.name == "retry": reader.write("Agent 🤖 : ", "retrying the action...") elif event.name == "update": reader.write(f"Agent({data.update.key}) 🤖 : ", data.update.parsed_value) elif event.name == "start": reader.write("Agent 🤖 : ", "starting new iteration") elif event.name == "success": reader.write("Agent 🤖 : ", "success") async def main() -> None: """Main application loop""" # Create agent agent = create_agent() # Log code interpreter status if configured code_interpreter_url = os.getenv("CODE_INTERPRETER_URL") if code_interpreter_url: reader.write( "🛠️ System: ", f"The code interpreter tool is enabled. Please ensure that it is running on {code_interpreter_url}", ) reader.write("🛠️ System: ", "Agent initialized with Wikipedia, DuckDuckGo, and Weather tools.") # Main interaction loop with user input for prompt in reader: # Run agent with the prompt response = await agent.run( prompt, max_retries_per_step=3, total_max_retries=10, max_iterations=20, ).on("*", process_agent_events, EmitterOptions(match_nested=False)) reader.write("Agent 🤖 : ", response.last_message.text) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config.js"; import { ReActAgent } from "beeai-framework/agents/react/agent"; import { createConsoleReader } from "../helpers/io.js"; import { FrameworkError } from "beeai-framework/errors"; import { TokenMemory } from "beeai-framework/memory/tokenMemory"; import { Logger } from "beeai-framework/logger/logger"; import { PythonTool } from "beeai-framework/tools/python/python"; import { LocalPythonStorage } from "beeai-framework/tools/python/storage"; import { DuckDuckGoSearchTool } from "beeai-framework/tools/search/duckDuckGoSearch"; import { WikipediaTool } from "beeai-framework/tools/search/wikipedia"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; Logger.root.level = "silent"; // disable internal logs const logger = new Logger({ name: "app", level: "trace" }); // Other models to try: // "llama3.1:70b" // "granite3.3" // "deepseek-r1:32b" // ensure the model is pulled before running const llm = new OllamaChatModel("granite4:micro"); const codeInterpreterUrl = process.env.CODE_INTERPRETER_URL; const __dirname = dirname(fileURLToPath(import.meta.url)); const codeInterpreterTmpdir = process.env.CODE_INTERPRETER_TMPDIR ?? "./examples/tmp/code_interpreter"; const localTmpdir = process.env.LOCAL_TMPDIR ?? "./examples/tmp/local"; const agent = new ReActAgent({ llm, memory: new TokenMemory(), tools: [ new DuckDuckGoSearchTool(), // new WebCrawlerTool(), // HTML web page crawler new WikipediaTool(), new OpenMeteoTool(), // weather tool // new ArXivTool(), // research papers // new DynamicTool() // custom python tool ...(codeInterpreterUrl ? [ new PythonTool({ codeInterpreter: { url: codeInterpreterUrl }, storage: new LocalPythonStorage({ interpreterWorkingDir: `${__dirname}/../../${codeInterpreterTmpdir}`, localWorkingDir: `${__dirname}/../../${localTmpdir}`, }), }), ] : []), ], }); const reader = createConsoleReader(); if (codeInterpreterUrl) { reader.write( "🛠️ System", `The code interpreter tool is enabled. Please ensure that it is running on ${codeInterpreterUrl}`, ); } try { for await (const { prompt } of reader) { const response = await agent .run( { prompt }, { execution: { maxRetriesPerStep: 3, totalMaxRetries: 10, maxIterations: 20, }, }, ) .observe((emitter) => { // emitter.on("start", () => { // reader.write(`Agent 🤖 : `, "starting new iteration"); // }); emitter.on("error", ({ error }) => { reader.write(`Agent 🤖 : `, FrameworkError.ensure(error).dump()); }); emitter.on("retry", () => { reader.write(`Agent 🤖 : `, "retrying the action..."); }); emitter.on("update", async ({ data, update, meta }) => { // log 'data' to see the whole state // to log only valid runs (no errors), check if meta.success === true reader.write(`Agent (${update.key}) 🤖 : `, update.value); }); emitter.on("partialUpdate", ({ data, update, meta }) => { // ideal for streaming (line by line) // log 'data' to see the whole state // to log only valid runs (no errors), check if meta.success === true // reader.write(`Agent (partial ${update.key}) 🤖 : `, update.value); }); // To observe all events (uncomment following block) // emitter.match("*.*", async (data: unknown, event) => { // logger.trace(event, `Received event "${event.path}"`); // }); // To get raw LLM input (uncomment following block) // emitter.match( // (event) => event.creator === llm && event.name === "start", // async (data: InferCallbackValue, event) => { // logger.trace( // event, // [ // `Received LLM event "${event.path}"`, // JSON.stringify(data.input), // array of messages // ].join("\n"), // ); // }, // ); }); reader.write(`Agent 🤖 : `, response.result.text); } } catch (error) { logger.error(FrameworkError.ensure(error).dump()); } finally { reader.close(); } ``` ### Tool Calling Agent The Tool Calling Agent is deprecated. Use [RequirementAgent](/modules/agents/requirement-agent) instead. The ToolCallingAgent is optimized for scenarios where tool usage is the primary focus. It handles tool calls more efficiently and can execute multiple tools in parallel. ```py Python [expandable] theme={null} import asyncio import logging import sys import traceback from typing import Any from dotenv import load_dotenv from beeai_framework.agents.tool_calling import ToolCallingAgent from beeai_framework.backend import ChatModel from beeai_framework.emitter import EventMeta from beeai_framework.errors import FrameworkError from beeai_framework.logger import Logger from beeai_framework.memory import UnconstrainedMemory from beeai_framework.tools.weather import OpenMeteoTool from examples.helpers.io import ConsoleReader # Load environment variables load_dotenv() # Configure logging - using DEBUG instead of trace logger = Logger("app", level=logging.DEBUG) reader = ConsoleReader() def process_agent_events(data: Any, event: EventMeta) -> None: """Process agent events and log appropriately""" if event.name == "start": reader.write("Agent (debug) 🤖 : ", "starting new iteration") elif event.name == "success": reader.write("Agent (debug) 🤖 : ", data.state.memory.messages[-1]) async def main() -> None: """Main application loop""" # Create agent agent = ToolCallingAgent( llm=ChatModel.from_name("ollama:llama3.1"), memory=UnconstrainedMemory(), tools=[OpenMeteoTool()] ) # Main interaction loop with user input for prompt in reader: response = await agent.run(prompt).on("*", process_agent_events) reader.write("Agent 🤖 : ", response.last_message.text) print("======DONE (showing the full message history)=======") # pyrefly: ignore [unbound-name] messages = response.state.memory.messages for msg in messages: print(msg) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config.js"; import { createConsoleReader } from "../../helpers/io.js"; import { FrameworkError } from "beeai-framework/errors"; import { TokenMemory } from "beeai-framework/memory/tokenMemory"; import { Logger } from "beeai-framework/logger/logger"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; import { ToolCallingAgent } from "beeai-framework/agents/toolCalling/agent"; Logger.root.level = "silent"; // disable internal logs const logger = new Logger({ name: "app", level: "trace" }); // Other models to try: // "llama3.1:70b" // "granite3.3" // "deepseek-r1:32b" // ensure the model is pulled before running const llm = new OllamaChatModel("granite4:micro"); const agent = new ToolCallingAgent({ llm, memory: new TokenMemory(), templates: { system: (template) => template.fork((config) => { config.defaults.instructions = "You are a helpful assistant that uses tools to answer questions."; }), }, tools: [ new OpenMeteoTool(), // weather tool ], }); const reader = createConsoleReader(); try { for await (const { prompt } of reader) { let messagesCount = agent.memory.messages.length + 1; const response = await agent.run({ prompt }).observe((emitter) => { emitter.on("success", async ({ state }) => { const newMessages = state.memory.messages.slice(messagesCount); messagesCount += newMessages.length; reader.write( `Agent (${newMessages.length} new messages) 🤖 :\n`, newMessages.map((msg) => `-> ${JSON.stringify(msg.toPlain())}`).join("\n"), ); }); // To observe all events (uncomment following block) // emitter.match("*.*", async (data: unknown, event) => { // logger.trace(event, `Received event "${event.path}"`); // }, { // matchNested: true // }); // To get raw LLM input (uncomment following block) // emitter.match( // (event) => event.creator === llm && event.name === "start", // async (data: InferCallbackValue, event) => { // logger.trace( // event, // [ // `Received LLM event "${event.path}"`, // JSON.stringify(data.input), // array of messages // ].join("\n"), // ); // }, // ); }); reader.write(`Agent 🤖 : `, response.result.text); } } catch (error) { logger.error(FrameworkError.ensure(error).dump()); } finally { reader.close(); } ``` ### Custom Agent For advanced use cases, you can create your own agent implementation by extending the `BaseAgent` class. ```py Python [expandable] theme={null} import asyncio import sys import traceback from typing import Unpack from pydantic import BaseModel, Field from beeai_framework.adapters.ollama import OllamaChatModel from beeai_framework.agents import AgentMeta, AgentOptions, AgentOutput, BaseAgent from beeai_framework.backend import AnyMessage, AssistantMessage, ChatModel, SystemMessage, UserMessage from beeai_framework.context import RunContext from beeai_framework.emitter import Emitter from beeai_framework.errors import FrameworkError from beeai_framework.memory import BaseMemory, UnconstrainedMemory from beeai_framework.runnable import runnable_entry class State(BaseModel): thought: str final_answer: str class CustomAgent(BaseAgent): def __init__(self, llm: ChatModel, memory: BaseMemory) -> None: super().__init__() self.model = llm self._memory = memory @property def memory(self) -> BaseMemory: return self._memory @memory.setter def memory(self, memory: BaseMemory) -> None: self._memory = memory def _create_emitter(self) -> Emitter: return Emitter.root().child( namespace=["agent", "custom"], creator=self, ) @runnable_entry async def run(self, input: str | list[AnyMessage], /, **kwargs: Unpack[AgentOptions]) -> AgentOutput: async def handler(context: RunContext) -> AgentOutput: class CustomSchema(BaseModel): thought: str = Field(description="Describe your thought process before coming with a final answer") final_answer: str = Field( description="Here you should provide concise answer to the original question." ) response = await self.model.run( [ SystemMessage("You are a helpful assistant. Always use JSON format for your responses."), *(self.memory.messages if self.memory is not None else []), *([UserMessage(input)] if isinstance(input, str) else input), ], response_format=CustomSchema, max_retries=kwargs.get("total_max_retries", 3), signal=context.signal, ) assert isinstance(response.output_structured, CustomSchema) result = AssistantMessage(response.output_structured.final_answer) await self.memory.add(result) if self.memory else None return AgentOutput( output=[result], context={ "state": State( thought=response.output_structured.thought, final_answer=response.output_structured.final_answer, ) }, ) return await handler(RunContext.get()) @property def meta(self) -> AgentMeta: return AgentMeta( name="CustomAgent", description="Custom Agent is a simple LLM agent.", tools=[], ) async def main() -> None: agent = CustomAgent( llm=OllamaChatModel("granite3.3"), memory=UnconstrainedMemory(), ) response = await agent.run([UserMessage("Why is the sky blue?")]) print(response.context.get("state")) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { BaseAgent, BaseAgentRunOptions } from "beeai-framework/agents/base"; import { AssistantMessage, Message, SystemMessage, UserMessage, } from "beeai-framework/backend/message"; import { Emitter } from "beeai-framework/emitter/emitter"; import { GetRunContext } from "beeai-framework/context"; import { z } from "zod"; import { AgentMeta } from "beeai-framework/agents/types"; import { BaseMemory } from "beeai-framework/memory/base"; import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory"; import { ChatModel } from "beeai-framework/backend/chat"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; interface RunInput { message: Message; } interface RunOutput { message: Message; state: { thought: string; final_answer: string; }; } interface RunOptions extends BaseAgentRunOptions { maxRetries?: number; } interface AgentInput { llm: ChatModel; memory: BaseMemory; } export class CustomAgent extends BaseAgent { public readonly memory: BaseMemory; protected readonly model: ChatModel; public emitter = Emitter.root.child({ namespace: ["agent", "custom"], creator: this, }); constructor(input: AgentInput) { super(); this.model = input.llm; this.memory = input.memory; } protected async _run( input: RunInput, options: RunOptions, run: GetRunContext, ): Promise { const response = await this.model.createStructure({ schema: z.object({ thought: z .string() .describe("Describe your thought process before coming with a final answer"), final_answer: z .string() .describe("Here you should provide concise answer to the original question."), }), messages: [ new SystemMessage("You are a helpful assistant. Always use JSON format for you responses."), ...this.memory.messages, input.message, ], maxRetries: options?.maxRetries, abortSignal: run.signal, }); const result = new AssistantMessage(response.object.final_answer); await this.memory.add(result); return { message: result, state: response.object, }; } public get meta(): AgentMeta { return { name: "CustomAgent", description: "Custom Agent is a simple LLM agent.", tools: [], }; } createSnapshot() { return { ...super.createSnapshot(), emitter: this.emitter, memory: this.memory, }; } loadSnapshot(snapshot: ReturnType) { Object.assign(this, snapshot); } } const agent = new CustomAgent({ llm: new OllamaChatModel("granite3.3"), memory: new UnconstrainedMemory(), }); const response = await agent.run({ message: new UserMessage("Why is the sky blue?"), }); console.info(response.state); ``` ## Multi-Agent Hand-offs Create a team of specialized agents that can collaborate: ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.agents.requirement.requirements.conditional import ConditionalRequirement from beeai_framework.backend import ChatModel from beeai_framework.errors import FrameworkError from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools import Tool from beeai_framework.tools.handoff import HandoffTool from beeai_framework.tools.search.wikipedia import WikipediaTool from beeai_framework.tools.think import ThinkTool from beeai_framework.tools.weather import OpenMeteoTool async def main() -> None: knowledge_agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite4:micro"), tools=[ThinkTool(), WikipediaTool()], requirements=[ConditionalRequirement(ThinkTool, force_at_step=1)], role="Knowledge Specialist", instructions="Provide answers to general questions about the world.", ) weather_agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite4:micro"), tools=[OpenMeteoTool()], role="Weather Specialist", instructions="Provide weather forecast for a given destination.", ) main_agent = RequirementAgent( name="MainAgent", llm=ChatModel.from_name("ollama:granite4:micro"), tools=[ ThinkTool(), HandoffTool( knowledge_agent, name="KnowledgeLookup", description="Consult the Knowledge Agent for general questions.", ), HandoffTool( weather_agent, name="WeatherLookup", description="Consult the Weather Agent for forecasts.", ), ], requirements=[ConditionalRequirement(ThinkTool, force_at_step=1)], # Log all tool calls to the console for easier debugging middlewares=[GlobalTrajectoryMiddleware(included=[Tool])], ) question = "If I travel to Rome next weekend, what should I expect in terms of weather, and also tell me one famous historical landmark there?" print(f"User: {question}") try: response = await main_agent.run(question, expected_output="Helpful and clear response.") print("Agent:", response.last_message.text) except FrameworkError as err: print("Error:", err.explain()) if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} COMING SOON ``` ## Workflows Upcoming change:
Workflows are under construction to support more dynamic multi-agent patterns. If you'd like to participate in shaping the vision, contribute to the discussion in this [V2 Workflow Proposal](https://github.com/i-am-bee/beeai-framework/discussions/1005).
For complex applications, you can create multi-agent workflows where specialized agents collaborate. ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.backend import ChatModel from beeai_framework.emitter import EmitterOptions from beeai_framework.errors import FrameworkError from beeai_framework.tools.search.wikipedia import WikipediaTool from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.workflows.agent import AgentWorkflow, AgentWorkflowInput from examples.helpers.io import ConsoleReader async def main() -> None: llm = ChatModel.from_name("ollama:llama3.1") workflow = AgentWorkflow(name="Smart assistant") workflow.add_agent( name="Researcher", role="A diligent researcher.", instructions="You look up and provide information about a specific topic.", tools=[WikipediaTool()], llm=llm, ) workflow.add_agent( name="WeatherForecaster", role="A weather reporter.", instructions="You provide detailed weather reports.", tools=[OpenMeteoTool()], llm=llm, ) workflow.add_agent( name="DataSynthesizer", role="A meticulous and creative data synthesizer", instructions="You can combine disparate information into a final coherent summary.", llm=llm, ) reader = ConsoleReader() reader.write("Assistant 🤖 : ", "What location do you want to learn about?") for prompt in reader: await ( workflow.run( inputs=[ AgentWorkflowInput(prompt="Provide a short history of the location.", context=prompt), AgentWorkflowInput( prompt="Provide a comprehensive weather summary for the location today.", expected_output="Essential weather details such as chance of rain, temperature and wind. Only report information that is available.", ), AgentWorkflowInput( prompt="Summarize the historical and weather data for the location.", expected_output="A paragraph that describes the history of the location, followed by the current weather conditions.", ), ] ) .on( # Event Matcher -> match agent's 'success' events lambda event: isinstance(event.creator, ChatModel) and event.name == "success", # log data to the console lambda data, event: reader.write( "->Got response from the LLM", " \n->".join([str(message.content[0].model_dump()) for message in data.value.messages]), ), EmitterOptions(match_nested=True), ) .on( "success", lambda data, event: reader.write( f"->Step '{data.step}' has been completed with the following outcome." f"\n\n{data.state.final_answer}\n\n", data.model_dump(exclude={"data"}), ), ) ) reader.write("Assistant 🤖 : ", "What location do you want to learn about?") if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config"; import { createConsoleReader } from "examples/helpers/io.js"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; import { WikipediaTool } from "beeai-framework/tools/search/wikipedia"; import { AgentWorkflow } from "beeai-framework/workflows/agent"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; const workflow = new AgentWorkflow("Smart assistant"); const llm = new OllamaChatModel("llama3.1"); workflow.addAgent({ name: "Researcher", role: "A diligent researcher", instructions: "You look up and provide information about a specific topic.", tools: [new WikipediaTool()], llm, }); workflow.addAgent({ name: "WeatherForecaster", role: "A weather reporter", instructions: "You provide detailed weather reports.", tools: [new OpenMeteoTool()], llm, }); workflow.addAgent({ name: "DataSynthesizer", role: "A meticulous and creative data synthesizer", instructions: "You can combine disparate information into a final coherent summary.", llm, }); const reader = createConsoleReader(); reader.write("Assistant 🤖 : ", "What location do you want to learn about?"); for await (const { prompt } of reader) { const { result } = await workflow .run([ { prompt: "Provide a short history of the location.", context: prompt }, { prompt: "Provide a comprehensive weather summary for the location today.", expectedOutput: "Essential weather details such as chance of rain, temperature and wind. Only report information that is available.", }, { prompt: "Summarize the historical and weather data for the location.", expectedOutput: "A paragraph that describes the history of the location, followed by the current weather conditions.", }, ]) .observe((emitter) => { emitter.on("success", (data) => { reader.write( `Step '${data.step}' has been completed with the following outcome:\n`, data.state?.finalAnswer ?? "-", ); }); }); reader.write(`Assistant 🤖`, result.finalAnswer); reader.write("Assistant 🤖 : ", "What location do you want to learn about?"); } ``` ## Examples Explore reference agent implementations in Python Explore reference agent implementations in TypeScript # Requirement Agent Source: https://framework.beeai.dev/modules/agents/requirement-agent The `RequirementAgent` is a declarative AI agent implementation that provides predictable, controlled execution behavior across different language models through rule-based constraints. Language models vary significantly in their reasoning capabilities and tool-calling sophistication, but RequirementAgent normalizes these differences by enforcing consistent execution patterns regardless of the underlying model's strengths or weaknesses. Rules can be configured as strict or flexible as necessary, adapting to task requirements while ensuring consistent execution regardless of the underlying model's reasoning or tool-calling capabilities. ### Core Problems Addressed **Traditional AI agents exhibit unpredictable behavior** in production environments: * Execution inconsistency: Agents may skip critical steps, terminate prematurely, or use inappropriate tools * Model variability: Different LLMs produce different execution patterns for the same task * Debugging complexity: Non-deterministic behavior makes troubleshooting difficult * Production reliability: Lack of guarantees makes agents unsuitable for critical workflows ### Value of `RequirementAgent` RequirementAgent ensures consistent agent behavior through declarative rules that define when and how tools are used, delivering reliable agents that: * Complete essential tasks systematically by enforcing proper execution sequences * Validate data and results comprehensively through mandatory verification steps * Select appropriate tools intelligently based on context and task requirements * Execute efficiently and safely with built-in protection against infinite loops and runaway processes ## Quickstart This example demonstrates how to create an agent with enforced tool execution order. This agent will: 1. First use `ThinkTool` to reason about the request enabling a "Re-Act" pattern 2. Check weather using `OpenMeteoTool`, which it must call at least once but not consecutively 3. Search for events using `DuckDuckGoSearchTool` at least once 4. Provide recommendations based on the gathered information ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.agents.requirement.requirements.conditional import ( ConditionalRequirement, ) from beeai_framework.backend import ChatModel from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool from beeai_framework.tools.think import ThinkTool from beeai_framework.tools.weather import OpenMeteoTool # Create an agent that plans activities based on weather and events async def main() -> None: agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite4:micro"), tools=[ ThinkTool(), # to reason OpenMeteoTool(), # retrieve weather data DuckDuckGoSearchTool(), # search web ], instructions="Plan activities for a given destination based on current weather and events.", requirements=[ # Force thinking first ConditionalRequirement(ThinkTool, force_at_step=1), # Search only after getting weather and at least once ConditionalRequirement( DuckDuckGoSearchTool, only_after=[OpenMeteoTool], min_invocations=1, max_invocations=2 ), # Weather tool be used at least once but not consecutively ConditionalRequirement(OpenMeteoTool, consecutive_allowed=False, min_invocations=1, max_invocations=2), ], ) # Run with execution logging response = await agent.run("What to do in Boston?").middleware(GlobalTrajectoryMiddleware()) print(f"Final Answer: {response.last_message.text}") if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} import { RequirementAgent } from "beeai-framework/agents/requirement/agent"; import { ConditionalRequirement } from "beeai-framework/agents/requirement/requirements/conditional"; import { ChatModel } from "beeai-framework/backend/chat"; import { DuckDuckGoSearchTool } from "beeai-framework/tools/search/duckDuckGoSearch"; import { ThinkTool } from "beeai-framework/tools/think"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; // Create an agent that plans activities based on weather and events const agent = new RequirementAgent({ llm: await ChatModel.fromName("ollama:granite3.3"), tools: [ new ThinkTool(), // to reason new OpenMeteoTool(), // retrieve weather data new DuckDuckGoSearchTool(), // search web ], instructions: "Plan activities for a given destination based on current weather and events.", requirements: [ // Force thinking first new ConditionalRequirement(ThinkTool, { forceAtStep: 1 }), // Search only after getting weather and at least once new ConditionalRequirement(DuckDuckGoSearchTool, { onlyAfter: [OpenMeteoTool], minInvocations: 1, maxInvocations: 2, }), // Weather tool be used at least once but not consecutively new ConditionalRequirement(OpenMeteoTool, { consecutiveAllowed: false, minInvocations: 1, maxInvocations: 2, }), ], }); // Run with execution logging const response = await agent.run({ prompt: "What to do in Boston?" }); console.log(`Final Answer: ${response.result.text}`); ``` ## How it Works `RequirementAgent` operates on a simple principle: developers declare rules on specific tools using `ConditionalRequirement` objects, while the framework automatically handles all orchestration logic behind the scenes. The developer can modify agent behavior by adjusting rule parameters, not rewriting complex state management logic. This creates clear separation between business logic (rules) and execution control (framework-managed). In `RequirementAgent`, **all capabilities (including data retrieval, web search, reasoning patterns, and `final_answer`) are implemented as tools** to ensure structured, reliable execution. Each `ConditionalRequirement` returns a `Rule` where each rule is bound to a single tool: | Attribute | Purpose | Value | | -------------- | --------------------------------------------------------------------------------------------- | ----- | | `target` | Which tool the rule applies to for a given turn | str | | `allowed` | Whether the tool can be used for a given turn and is present in the system prompt | bool | | `hidden` | Whether the tool definition is visible to the agent for a given turn and in the system prompt | bool | | `prevent_stop` | Whether rule prevents the agent from terminating for a given turn | bool | | `forced` | Whether tool must be invoked on a given turn | bool | | `reason` | Optinally explain to the LLM why the given rule is applied | str | When requirements generate conflicting rules, the system applies this precedence: * **Forbidden overrides all**: If any requirement forbids a tool, that tool cannot be used. * **Highest priority forced rule wins**: If multiple requirements force tools, the highest-priority requirement decides which tool is forced. * **Prevention rules accumulate**: All `prevent_stop` rules apply simultaneously ### Execution Flow 1. **State Initialization**: Creates `RequirementAgentRunState` with `UnconstrainedMemory`, execution steps, and iteration tracking 2. **Requirements Processing**: `RequirementsReasoner` analyzes requirements and determines allowed tools, tool choice preferences, and termination conditions 3. **Request Creation**: Creates a structured request with `allowed_tools`, `tool_choice`, and `can_stop` flags based on current state and requirements. The system evaluates requirements before each LLM call to determine which tools to make available to the LLM 4. **LLM Interaction**: Calls language model with system message, conversation history, and constrained tool set 5. **Tool Execution**: Executes requested tools via `_run_tools`, handles errors, and updates conversation memory 6. **Cycle Detection**: `ToolCallChecker` prevents infinite loops by detecting repeated tool call patterns 7. **Iteration Control**: Continues until requirements are satisfied or maximum iterations reached ### Basic Rule Definition Developers declare rules by creating `ConditionalRequirement` objects that target specific tools. The framework automatically handles all orchestration: ```py Python [expandable] theme={null} # Declare: agent must think before acting ConditionalRequirement(ThinkTool, force_at_step=1) # Declare: require weather check before web search ConditionalRequirement(DuckDuckGoSearchTool, only_after=[OpenMeteoTool]) # Declare: prevent consecutive uses of same tool ConditionalRequirement(OpenMeteoTool(), consecutive_allowed=False) ``` ### Complete Parameter Reference ```py Python [expandable] theme={null} ConditionalRequirement( target_tool, # Tool class, instance, or name (can also be specified as `target=...`) name="", # (optional) Name, useful for logging only_before=[...], # (optional) Disable target_tool after any of these tools are called only_after=[...], # (optional) Disable target_tool before all these tools are called force_after=[...], # (optional) Force target_tool execution immediately after any of these tools are called min_invocations=0, # (optional) Minimum times the tool must be called before agent can stop max_invocations=10, # (optional) Maximum times the tool can be called before being disabled force_at_step=1, # (optional) Step number at which the tool must be invoked only_success_invocations=True, # (optional) Whether 'force_at_step' counts only successful invocations priority=10, # (optional) Higher relative number means higher priority for requirement enforcement consecutive_allowed=True, # (optional) Whether the tool can be invoked twice in a row force_prevent_stop=False, # (optional) If True, prevents the agent from giving a final answer when a forced target_tool call occurs. enabled=True, # (optional) Whether to skip this requirement’s execution custom_checks=[ # (optional) Custom callbacks; all must pass for the tool to be used lambda state: any('weather' in msg.text for msg in state.memory.message if isinstance(msg, UserMessage)), lambda state: state.iteration > 0, ], ) ``` ```ts TypeScript [expandable] theme={null} new ConditionalRequirement( targetTool, // Tool class, instance, or name { name: "", // (optional) Name, useful for logging onlyBefore: [...], // (optional) Disable target_tool after any of these tools are called onlyAfter: [...], // (optional) Disable target_tool before all these tools are called forceAfter: [...], // (optional) Force target_tool execution immediately after any of these tools are called minInvocations: 0, // (optional) Minimum times the tool must be called before agent can stop maxInvocations: 10, // (optional) Maximum times the tool can be called before being disabled forceAtStep: 1, // (optional) Step number at which the tool must be invoked onlySuccessInvocations: true, // (optional) Whether 'forceAtStep' counts only successful invocations priority: 10, // (optional) Higher relative number means higher priority for requirement enforcement consecutiveAllowed: true, // (optional) Whether the tool can be invoked twice in a row forcePreventStop: false, // (optional) If true, prevents the agent from giving a final answer when a forced target_tool call occurs. enabled: true, // (optional) Whether to skip this requirement's execution customChecks: [ // (optional) Custom callbacks; all must pass for the tool to be used (state) => state.memory.messages.some(msg => msg.text?.includes('weather')), (state) => state.iteration > 0, ], } ) ``` Start with a single requirement and add more as needed. Curious to see it in action? Explore our [interactive exercises](https://github.com/i-am-bee/beeai-framework/tree/main/python/examples/agents/requirement/exercises) to discover how the agent solves real problems step by step! ## Example Agents ### Forced Execution Order This example forces the agent to use `ThinkTool` for reasoning followed by `DuckDuckGoSearchTool` to retrieve data. This trajectory ensures that even a small model can arrive at the correct answer by preventing it from skipping tool calls entirely. ```py Python [expandable] theme={null} RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), tools=[ThinkTool(), DuckDuckGoSearchTool()], requirements=[ ConditionalRequirement(ThinkTool, force_at_step=1), # Force ThinkTool at the first step ConditionalRequirement(DuckDuckGoSearchTool, force_at_step=2), # Force DuckDuckGo at the second step ], ) ``` ```ts TypeScript [expandable] theme={null} new RequirementAgent({ llm: await ChatModel.fromName("ollama:granite3.3"), tools: [new ThinkTool(), new DuckDuckGoSearchTool()], requirements: [ new ConditionalRequirement(ThinkTool, { forceAtStep: 1 }), // Force ThinkTool at the first step new ConditionalRequirement(DuckDuckGoSearchTool, { forceAtStep: 2 }), // Force DuckDuckGo at the second step ], }) ``` ### Creating a ReAct Agent A ReAct Agent (Reason and Act) follows this trajectory: ```text theme={null} Think -> Use a tool -> Think -> Use a tool -> Think -> ... -> End ``` You can achieve this by forcing the execution of the `Think` tool after every tool: ```py Python [expandable] theme={null} RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), tools=[ThinkTool(), WikipediaTool(), OpenMeteoTool()], requirements=[ConditionalRequirement(ThinkTool, force_at_step=1, force_after=Tool)], ) ``` ```ts TypeScript [expandable] theme={null} import { Tool } from "beeai-framework/tools/base"; new RequirementAgent({ llm: await ChatModel.fromName("ollama:granite3.3"), tools: [new ThinkTool(), new WikipediaTool(), new OpenMeteoTool()], requirements: [ new ConditionalRequirement(ThinkTool, { forceAtStep: 1, forceAfter: [Tool] }) ], }) ``` For a more general approach, use `ConditionalRequirement(ThinkTool, force_at_step=1, force_after=Tool, consecutive_allowed=False)`, where the option `consecutive_allowed=False` prevents `ThinkTool` from being used multiple times in a row. ### ReAct Agent with Custom Conditions You may want an agent that works like ReAct but skips the "reasoning" step under certain conditions. This example uses the priority option to tell the agent to send an email after creating an order, while calling `ThinkTool` as the first step and after `retrieve_basket`. ```py Python [expandable] theme={null} RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), tools=[ThinkTool(), retrieve_basket(), create_order(), send_email()], requirements=[ ConditionalRequirement(ThinkTool, force_at_step=1, force_after=retrieve_basket, priority=10), ConditionalRequirement(send_email, only_after=create_order, force_after=create_order, priority=20, max_invocations=1), ], ) ``` ```ts TypeScript [expandable] theme={null} new RequirementAgent({ llm: await ChatModel.fromName("ollama:granite3.3"), tools: [new ThinkTool(), retrieveBasket(), createOrder(), sendEmail()], requirements: [ new ConditionalRequirement(ThinkTool, { forceAtStep: 1, forceAfter: [retrieveBasket], priority: 10 }), new ConditionalRequirement(sendEmail, { onlyAfter: [createOrder], forceAfter: [createOrder], priority: 20, maxInvocations: 1 }), ], }) ``` ### Ask Permission Requirement Some tools may be expensive to run or have destructive effects. For these tools, you may want to get **approval from an external system or directly from the user**. The following agent first asks the user before it runs the `remove_data` or the `get_data` tool. ```py Python [expandable] theme={null} RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3"), tools=[get_data, remove_data, update_data], requirements=[ AskPermissionRequirement([remove_data, get_data]) ] ) ``` ```ts TypeScript [expandable] theme={null} // Note: AskPermissionRequirement is not yet implemented in TypeScript // Coming soon ``` #### Using a Custom `handler` for Human In the Loop Requirements By default, the approval process is done as a simple prompt in terminal. The framework provides a simple way to provide a custom implementation. ```py Python [expandable] theme={null} async def handler(tool: Tool, input: dict[str, Any]) -> bool: # your implementation return True AskPermissionRequirement(..., handler=handler) ``` ```ts TypeScript [expandable] theme={null} // Note: AskPermissionRequirement is not yet implemented in TypeScript // Coming soon ``` #### Complete `AskPermissionRequirement` Parameter Reference ```py Python [expandable] theme={null} AskPermissionRequirement( include=[...], # (optional) List of targets (tool name, instance, or class) requiring explicit approval exclude=[...], # (optional) List of targets to exclude remember_choices=False, # (optional) If approved, should the agent ask again? hide_disallowed=False, # (optional) Permanently disable disallowed targets always_allow=False, # (optional) Skip the asking part handler=input(f"The agent wants to use the '{tool.name}' tool.\nInput: {tool_input}\nDo you allow it? (yes/no): ").strip().startswith("yes") # (optional) Custom handler, can be async ) ``` ```ts TypeScript [expandable] theme={null} // Note: AskPermissionRequirement is not yet implemented in TypeScript // Coming soon ``` If no targets are specified, permission is required for all tools. ## Custom Requirements You can create a custom requirement by implementing the base Requirement class. The Requirement class has the following lifecycle: 1. An external caller invokes `init(tools)` method: * `tools` is a list of available tools for a given agent. * This method is called only once, at the very beginning. * It is an ideal place to introduce hooks, validate the presence of certain tools, etc. * The return type of the `init` method is `None`. 2. An external caller invokes `run(state)` method: * `state` is a generic parameter; in `RequirementAgent`, it refers to the `RequirementAgentRunState` class. * This method is called multiple times, typically before an LLM call. * The return type of the `run` method is a list of rules. ### Custom Premature Stop Requirement This example demonstrates how to write a requirement that prevents the agent from answering if the question contains a specific phrase: ```py Python [expandable] theme={null} import asyncio from beeai_framework.agents.requirement import RequirementAgent, RequirementAgentRunState from beeai_framework.agents.requirement.requirements.requirement import Requirement, Rule, run_with_context from beeai_framework.backend import AssistantMessage, ChatModel from beeai_framework.context import RunContext from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool class PrematureStopRequirement(Requirement[RequirementAgentRunState]): """Prevents the agent from answering if a certain phrase occurs in the conversation""" name = "premature_stop" def __init__(self, phrase: str, reason: str) -> None: super().__init__() self._reason = reason self._phrase = phrase self._priority = 100 # (optional), default is 10 @run_with_context async def run(self, state: RequirementAgentRunState, context: RunContext) -> list[Rule]: # we take the last step's output (if exists) or the user's input last_step = state.steps[-1].output.get_text_content() if state.steps else state.input.text if self._phrase in last_step: # We will nudge the agent to include explantation why it needs to stop in the final answer. await state.memory.add( AssistantMessage( f"The final answer is that I can't finish the task because {self._reason}", {"tempMessage": True}, # the message gets removed in the next iteration ) ) # The rule ensures that the agent will use the 'final_answer' tool immediately. return [Rule(target="final_answer", forced=True)] # or return [Rule(target=FinalAnswerTool, forced=True)] else: return [] async def main() -> None: agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite4:micro"), tools=[DuckDuckGoSearchTool()], requirements=[ PrematureStopRequirement(phrase="value of x", reason="algebraic expressions are not allowed"), PrematureStopRequirement(phrase="bomb", reason="such topic is not allowed"), ], ) for prompt in ["y = 2x + 4, what is the value of x?", "how to make a bomb?"]: print("👤 User: ", prompt) response = await agent.run(prompt).middleware(GlobalTrajectoryMiddleware()) print("🤖 Agent: ", response.last_message.text) print() if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} import { RequirementAgent } from "beeai-framework/agents/requirement/agent"; import { Requirement, Rule } from "beeai-framework/agents/requirement/requirements/requirement"; import { ChatModel } from "beeai-framework/backend/chat"; import { AssistantMessage } from "beeai-framework/backend/message"; import { DuckDuckGoSearchTool } from "beeai-framework/tools/search/duckDuckGoSearch"; import { RequirementAgentRunState } from "beeai-framework/agents/requirement/types"; class PrematureStopRequirement extends Requirement { /** Prevents the agent from answering if a certain phrase occurs in the conversation */ protected phrase: string; protected reason: string; constructor(phrase: string, reason: string) { super("premature_stop"); this.phrase = phrase; this.reason = reason; this.priority = 100; // (optional), default is 10 } async run(state: RequirementAgentRunState): Promise { // we take the last step's output (if exists) or the user's input const lastStep = state.steps?.at(-1)?.output.getTextContent() ?? ""; if (lastStep.includes(this.phrase)) { // We will nudge the agent to include explanation why it needs to stop in the final answer. await state.memory.add( new AssistantMessage( `The final answer is that I can't finish the task because ${this.reason}.`, { tempMessage: true }, // the message gets removed in the next iteration ), ); // The rule ensures that the agent will use the 'final_answer' tool immediately. return [ { target: "final_answer", allowed: true, forced: true, hidden: false, preventStop: false, }, ]; } else { return []; } } } const agent = new RequirementAgent({ llm: await ChatModel.fromName("ollama:llama3.1"), tools: [new DuckDuckGoSearchTool()], requirements: [ new PrematureStopRequirement("value of x", "algebraic expressions are not allowed"), new PrematureStopRequirement("bomb", "such topic is not allowed"), ], }); const prompts = ["y = 2x + 4, what is the value of x?", "how to make a bomb?"]; for (const prompt of prompts) { console.log("👤 User: ", prompt); const response = await agent.run({ prompt }); console.log("🤖 Agent: ", response.result.text); console.log(); } ``` ## More Code Examples **➡️ Check out the following additional examples** **Python:** * [Multi-agent](https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/agents/requirement/multi_agent.py) system via handoffs. * [ReAct](https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/agents/requirement/react.py) loop in a second. * Generating [text output](https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/agents/requirement/text_output.py) and [structured output](https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/agents/requirement/structured_output.py). * [Advanced](https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/agents/requirement/complex.py) (detailed configuration). **TypeScript:** * [Quickstart](https://github.com/i-am-bee/beeai-framework/blob/main/typescript/examples/agents/requirement/quickstart.ts) with ConditionalRequirement. * [Text output](https://github.com/i-am-bee/beeai-framework/blob/main/typescript/examples/agents/requirement/textOutput.ts) generation. * [Structured output](https://github.com/i-am-bee/beeai-framework/blob/main/typescript/examples/agents/requirement/structuredOutput.ts) with Zod schemas. * [Custom requirement](https://github.com/i-am-bee/beeai-framework/blob/main/typescript/examples/agents/requirement/customRequirement.ts) implementation. * [Advanced](https://github.com/i-am-bee/beeai-framework/blob/main/typescript/examples/agents/requirement/complex.ts) (detailed configuration). * [Interactive demo](https://github.com/i-am-bee/beeai-framework/blob/main/typescript/examples/agents/requirement/demo.ts) with event observation. Explore examples in Python Explore examples in TypeScript # Backend Source: https://framework.beeai.dev/modules/backend ## Overview `Backend` is an umbrella module that encapsulates a unified way to work with the following functionalities: * Chat Models (`ChatModel` class) * Embedding Models (`EmbeddingModel` class) * Audio Models (coming soon) * Image Models (coming soon) BeeAI framework's backend is designed with a provider-based architecture, allowing you to switch between different AI service providers while maintaining a consistent API. Supported in Python and TypeScript. *** ## Supported providers The following table depicts supported providers. Each provider requires specific configuration through environment variables. Ensure all required variables are set before initializing a provider. | Name | Chat | Embedding | Environment Variables | | :------------- | :--- | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Ollama | ✅ | ✅ | OLLAMA\_CHAT\_MODEL
OLLAMA\_BASE\_URL | | OpenAI | ✅ | ✅ | OPENAI\_CHAT\_MODEL
OPENAI\_EMBEDDING\_MODEL
OPENAI\_API\_BASE
**OPENAI\_API\_KEY**
OPENAI\_ORGANIZATION
OPENAI\_API\_HEADERS | | IBM watsonx.ai | ✅ | ✅ | WATSONX\_CHAT\_MODEL
WATSONX\_API\_KEY
**WATSONX\_PROJECT\_ID**
WATSONX\_SPACE\_ID
WATSONX\_TOKEN
WATSONX\_ZENAPIKEY
WATSONX\_URL
WATSONX\_REGION | | Anthropic | ✅ | ✅ | ANTHROPIC\_CHAT\_MODEL
**ANTHROPIC\_API\_KEY**
ANTHROPIC\_API\_HEADERS | | Groq | ✅ | ✅ | GROQ\_CHAT\_MODEL
GROQ\_EMBEDDING\_MODEL
**GROQ\_API\_KEY** | | Amazon Bedrock | ✅ | ✅ | AWS\_CHAT\_MODEL
**AWS\_BEDROCK\_API\_KEY**
**AWS\_ACCESS\_KEY\_ID**
**AWS\_SECRET\_ACCESS\_KEY**
**AWS\_REGION**
AWS\_API\_HEADERS | | Google Vertex | ✅ | ✅ | GOOGLE\_VERTEX\_CHAT\_MODEL
**GOOGLE\_VERTEX\_PROJECT**
**GOOGLE\_VERTEX\_LOCATION**
GOOGLE\_APPLICATION\_CREDENTIALS
GOOGLE\_APPLICATION\_CREDENTIALS\_JSON
GOOGLE\_CREDENTIALS
GOOGLE\_VERTEX\_API\_HEADERS | | Azure OpenAI | ✅ | ✅ | AZURE\_OPENAI\_CHAT\_MODEL
**AZURE\_OPENAI\_API\_KEY**
**AZURE\_OPENAI\_API\_BASE**
**AZURE\_OPENAI\_API\_VERSION**
AZURE\_AD\_TOKEN
AZURE\_API\_TYPE
AZURE\_API\_HEADERS | | xAI | ✅ | ✅ | XAI\_CHAT\_MODEL
**XAI\_API\_KEY** | | Google Gemini | ✅ | ✅ | GEMINI\_CHAT\_MODEL
**GEMINI\_API\_KEY**
GEMINI\_API\_HEADERS | | MistralAI | ✅ | ✅ | MISTRALAI\_CHAT\_MODEL
MISTRALAI\_EMBEDDING\_MODEL
**MISTRALAI\_API\_KEY**
MISTRALAI\_API\_BASE | | Transformers | ✅ | ✅ | TRANSFORMERS\_CHAT\_MODEL
HF\_TOKEN | If you don't see your provider raise an issue [here](https://github.com/i-am-bee/beeai-framework/issues). Meanwhile, you can use the Ollama for local models in [Python](https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/backend/providers/ollama.py) or [TypeScript](https://github.com/i-am-bee/beeai-framework/blob/main/typescript/examples/backend/providers/ollama.ts) or the [Langchain adapter](#adding-a-provider-using-the-langchain-adapter) for hosted providers. Google Gemini, MistralAI, and Transformers are supported in Python only. The Transformers chat model does not support tool calling. *** ### Backend initialization The `Backend` class serves as a central entry point to access models from your chosen provider. This example illustrate how to leverage the framework's unified interface for different provider model operations by showcasing various interaction patterns including: * Basic chat completion * Streaming responses with abort functionality * Structured output generation * Real-time response parsing * Tool calling with external APIs * Text embedding generation Explore more provider examples in [Python](https://github.com/i-am-bee/beeai-framework/tree/main/python/examples/backend/providers) or [TypeScript](https://github.com/i-am-bee/beeai-framework/tree/main/typescript/examples/backend/providers) ```py Python [expandable] theme={null} import asyncio import datetime import sys import traceback from pydantic import BaseModel, Field from beeai_framework.adapters.openai import OpenAIChatModel, OpenAIEmbeddingModel from beeai_framework.backend import ( ChatModel, ChatModelNewTokenEvent, ChatModelParameters, MessageToolResultContent, ToolMessage, UserMessage, ) from beeai_framework.emitter import EventMeta from beeai_framework.errors import AbortError, FrameworkError from beeai_framework.parsers.field import ParserField from beeai_framework.parsers.line_prefix import LinePrefixParser, LinePrefixParserNode from beeai_framework.tools.weather import OpenMeteoTool, OpenMeteoToolInput from beeai_framework.utils import AbortSignal async def openai_from_name() -> None: llm = ChatModel.from_name("openai:gpt-4.1-mini") user_message = UserMessage("what states are part of New England?") response = await llm.run([user_message]) print(response.get_text_content()) async def openai_granite_from_name() -> None: llm = ChatModel.from_name("openai:gpt-4.1-mini") user_message = UserMessage("what states are part of New England?") response = await llm.run([user_message]) print(response.get_text_content()) async def openai_sync() -> None: llm = OpenAIChatModel("gpt-4.1-mini") user_message = UserMessage("what is the capital of Massachusetts?") response = await llm.run([user_message]) print(response.get_text_content()) async def openai_stream() -> None: llm = OpenAIChatModel("gpt-4.1-mini") user_message = UserMessage("How many islands make up the country of Cape Verde?") response = await llm.run([user_message], stream=True) print(response.get_text_content()) async def openai_stream_abort() -> None: llm = OpenAIChatModel("gpt-4.1-mini") user_message = UserMessage("What is the smallest of the Cape Verde islands?") try: response = await llm.run([user_message], stream=True, signal=AbortSignal.timeout(0.5)) if response is not None: print(response.get_text_content()) else: print("No response returned.") except AbortError as err: print(f"Aborted: {err}") async def openai_structure() -> None: class TestSchema(BaseModel): answer: str = Field(description="your final answer") llm = OpenAIChatModel("gpt-4.1-mini") user_message = UserMessage("How many islands make up the country of Cape Verde?") response = await llm.run([user_message], response_format=TestSchema, stream=True) print(response.output_structured) async def openai_stream_parser() -> None: llm = OpenAIChatModel("gpt-4.1-mini") parser = LinePrefixParser( nodes={ "test": LinePrefixParserNode( prefix="Prefix: ", field=ParserField.from_type(str), is_start=True, is_end=True ) } ) async def on_new_token(data: ChatModelNewTokenEvent, event: EventMeta) -> None: await parser.add(data.value.get_text_content()) user_message = UserMessage("Produce 3 lines each starting with 'Prefix: ' followed by a sentence and a new line.") await llm.run([user_message], stream=True).observe(lambda emitter: emitter.on("new_token", on_new_token)) result = await parser.end() print(result) async def openai_tool_calling() -> None: llm = ChatModel.from_name("openai:gpt-4.1-mini", ChatModelParameters(stream=True, temperature=0)) user_message = UserMessage(f"What is the current weather in Boston? Current date is {datetime.datetime.today()}.") weather_tool = OpenMeteoTool() response = await llm.run([user_message], tools=[weather_tool]) tool_call_msg = response.get_tool_calls()[0] print(tool_call_msg.model_dump()) tool_response = await weather_tool.run(OpenMeteoToolInput(location_name="Boston")) tool_response_msg = ToolMessage( MessageToolResultContent( result=tool_response.get_text_content(), tool_name=weather_tool.name, tool_call_id=response.get_tool_calls()[0].id, ) ) print(tool_response_msg.to_plain()) final_response = await llm.run([user_message, *response.output, tool_response_msg], tools=[]) print(final_response.get_text_content()) async def openai_embedding() -> None: embedding_llm = OpenAIEmbeddingModel() response = await embedding_llm.create(["Text", "to", "embed"]) for row in response.embeddings: print(*row) async def openai_cloning() -> None: llm = OpenAIChatModel("gpt-4.1-mini") await llm.clone() embedding_llm = OpenAIEmbeddingModel() await embedding_llm.clone() async def openai_file_example() -> None: llm = ChatModel.from_name("openai:gpt-4.1-mini") data_uri = "data:application/pdf;base64,JVBERi0xLjQKMSAwIG9iago8PC9UeXBlIC9DYXRhbG9nCi9QYWdlcyAyIDAgUgo+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlIC9QYWdlcwovS2lkcyBbMyAwIFJdCi9Db3VudCAxCj4+CmVuZG9iagozIDAgb2JqCjw8L1R5cGUgL1BhZ2UKL1BhcmVudCAyIDAgUgovTWVkaWFCb3ggWzAgMCA1OTUgODQyXQovQ29udGVudHMgNSAwIFIKL1Jlc291cmNlcyA8PC9Qcm9jU2V0IFsvUERGIC9UZXh0XQovRm9udCA8PC9GMSA0IDAgUj4+Cj4+Cj4+CmVuZG9iago0IDAgb2JqCjw8L1R5cGUgL0ZvbnQKL1N1YnR5cGUgL1R5cGUxCi9OYW1lIC9GMQovQmFzZUZvbnQgL0hlbHZldGljYQovRW5jb2RpbmcgL01hY1JvbWFuRW5jb2RpbmcKPj4KZW5kb2JqCjUgMCBvYmoKPDwvTGVuZ3RoIDUzCj4+CnN0cmVhbQpCVAovRjEgMjAgVGYKMjIwIDQwMCBUZAooRHVtbXkgUERGKSBUagpFVAplbmRzdHJlYW0KZW5kb2JqCnhyZWYKMCA2CjAwMDAwMDAwMDAgNjU1MzUgZgowMDAwMDAwMDA5IDAwMDAwIG4KMDAwMDAwMDA2MyAwMDAwMCBuCjAwMDAwMDAxMjQgMDAwMDAgbgowMDAwMDAwMjc3IDAwMDAwIG4KMDAwMDAwMDM5MiAwMDAwMCBuCnRyYWlsZXIKPDwvU2l6ZSA2Ci9Sb290IDEgMCBSCj4+CnN0YXJ0eHJlZgo0OTUKJSVFT0YK" file_message = UserMessage.from_file(file_data=data_uri, format="text") print(file_message.to_plain()) response = await llm.run([UserMessage("Read content of the file."), file_message]) print(response.get_text_content()) async def main() -> None: print("*" * 10, "openai_from_name") await openai_from_name() print("*" * 10, "openai_granite_from_name") await openai_granite_from_name() print("*" * 10, "openai_sync") await openai_sync() print("*" * 10, "openai_stream") await openai_stream() print("*" * 10, "openai_stream_abort") await openai_stream_abort() print("*" * 10, "openai_structure") await openai_structure() print("*" * 10, "openai_stream_parser") await openai_stream_parser() print("*" * 10, "openai_tool_calling") await openai_tool_calling() print("*" * 10, "openai_embedding") await openai_embedding() print("*" * 10, "openai_cloning") await openai_cloning() if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config.js"; import { OpenAIChatModel } from "beeai-framework/adapters/openai/backend/chat"; import { ToolMessage, UserMessage } from "beeai-framework/backend/message"; import { ChatModel } from "beeai-framework/backend/chat"; import { z } from "zod"; import { ChatModelError } from "beeai-framework/backend/errors"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; const llm = new OpenAIChatModel( "gpt-5-nano", {}, // { // baseURL: "OPENAI_BASE_URL", // apiKey: "OPENAI_API_KEY", // organization: "OPENAI_ORGANIZATION", // project: "OPENAI_PROJECT", // }, ); llm.config({ parameters: { maxTokens: 2048, }, }); async function openaiFromName() { const openaiLLM = await ChatModel.fromName("openai:gpt-5-nano"); const response = await openaiLLM.create({ messages: [new UserMessage("what states are part of New England?")], }); console.info(response.getTextContent()); } async function openaiSync() { const response = await llm.create({ messages: [new UserMessage("what is the capital of Massachusetts?")], }); console.info(response.getTextContent()); } async function openaiStream() { const response = await llm.create({ messages: [new UserMessage("How many islands make up the country of Cape Verde?")], stream: true, }); console.info(response.getTextContent()); } async function openaiAbort() { try { const response = await llm.create({ messages: [new UserMessage("What is the smallest of the Cape Verde islands?")], stream: true, abortSignal: AbortSignal.timeout(1 * 500), }); console.info(response.getTextContent()); } catch (err) { if (err instanceof ChatModelError) { console.log("Aborted", { err }); } } } async function openaiStructure() { const response = await llm.createStructure({ schema: z.object({ answer: z.string({ description: "your final answer" }), }), messages: [new UserMessage("How many islands make up the country of Cape Verde?")], }); console.info(response.object); } async function openaiToolCalling() { const userMessage = new UserMessage( `What is the current weather in Boston? Current date is ${new Date().toISOString().split("T")[0]}.`, ); const weatherTool = new OpenMeteoTool({ retryOptions: { maxRetries: 3 } }); const response = await llm.create({ messages: [userMessage], tools: [weatherTool], toolChoice: weatherTool, }); const toolCallMsg = response.getToolCalls()[0]; console.debug(JSON.stringify(toolCallMsg)); const toolResponse = await weatherTool.run(toolCallMsg.input as any); const toolResponseMsg = new ToolMessage({ type: "tool-result", output: { type: "text", value: toolResponse.getTextContent() }, toolName: toolCallMsg.toolName, toolCallId: toolCallMsg.toolCallId, }); console.info(toolResponseMsg.toPlain()); const finalResponse = await llm.create({ messages: [userMessage, ...response.messages, toolResponseMsg], tools: [], }); console.info(finalResponse.getTextContent()); } async function openaiDebug() { // Log every request llm.emitter.match("*", (value, event) => console.debug( `Time: ${event.createdAt.toISOString()}`, `Event: ${event.name}`, `Data: ${JSON.stringify(value)}`, ), ); const response = await llm.create({ messages: [new UserMessage("Hello world!")], }); console.info(response.messages[0].toPlain()); } console.info(" openaiFromName".padStart(25, "*")); await openaiFromName(); console.info(" openaiSync".padStart(25, "*")); await openaiSync(); console.info(" openaiStream".padStart(25, "*")); await openaiStream(); console.info(" openaiAbort".padStart(25, "*")); await openaiAbort(); console.info(" openaiStructure".padStart(25, "*")); await openaiStructure(); console.info(" openaiToolCalling".padStart(25, "*")); await openaiToolCalling(); console.info(" openaiDebug".padStart(25, "*")); await openaiDebug(); ``` See the [events documentation](/modules/events) for more information on standard emitter events. *** ## Chat model The `ChatModel` class represents a Chat Language Model and provides methods for text generation, streaming responses, and more. You can initialize a chat model in multiple ways: **Method 1: Using the `from_name` method** ```py Python [expandable] theme={null} from beeai_framework.backend.chat import ChatModel model = ChatModel.from_name("ollama:llama3.1") ``` ```ts TypeScript [expandable] theme={null} import { ChatModel } from "beeai-framework/backend/chat"; const model = await ChatModel.fromName("ollama:granite3.3:8b"); ``` **Method 2: Directly specifying the provider class** ```py Python [expandable] theme={null} from beeai_framework.adapters.ollama.backend.chat import OllamaChatModel model = OllamaChatModel("llama3.1") ``` ```ts TypeScript [expandable] theme={null} import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; const model = new OllamaChatModel("llama3.1"); ``` ### File / Document Inputs (PDF etc.) You can attach files (e.g. PDFs) to a `UserMessage` using the `MessageFileContent` part or the convenience factory `UserMessage.from_file`. Provide either a remote `file_id`/URL or an inline base64 data URI (`file_data`). Optionally specify a MIME `format`. ```py Python [expandable] theme={null} from beeai_framework.backend import UserMessage, MessageFileContent # Using a remote / previously uploaded file URL or id (flattened API) msg_with_file_id = UserMessage([ MessageFileContent( file_id="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", format="application/pdf", ), "What's this file about?", ]) # Same using the factory helper msg_with_file_id_factory = UserMessage.from_file( file_id="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", format="application/pdf", ) # Using inline base64 data (shortened example) msg_with_file_data = UserMessage([ MessageFileContent( file_data="data:application/pdf;base64,AAA...", format="application/pdf", ), "Summarize the document", ]) # Inline base64 with factory msg_with_file_data_factory = UserMessage.from_file( file_data="data:application/pdf;base64,AAA...", format="application/pdf", ) ``` These content parts serialize to the flattened schema (legacy nested `{ "file": {...} }` removed): ```json theme={null} { "type": "file", "file_id": "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf", "format": "application/pdf" } ``` If neither `file_id` nor `file_data` is supplied a validation error is raised. ### Chat model configuration You can configure various parameters for your chat model. ```python Python theme={null} import asyncio import sys import traceback from beeai_framework.adapters.ollama import OllamaChatModel from beeai_framework.backend import UserMessage from beeai_framework.errors import FrameworkError from examples.helpers.io import ConsoleReader async def main() -> None: llm = OllamaChatModel("llama3.1") # Optionally one may set llm parameters llm.parameters.max_tokens = 10000 # high number yields longer potential output llm.parameters.top_p = 0.1 # higher number yields more complex vocabulary, recommend only changing p or k llm.parameters.frequency_penalty = 0 # higher number yields reduction in word reptition llm.parameters.temperature = 0 # higher number yields greater randomness and variation llm.parameters.top_k = 0 # higher number yields more variance, recommend only changing p or k llm.parameters.n = 1 # higher number yields more choices llm.parameters.presence_penalty = 0 # higher number yields reduction in repetition of words llm.parameters.seed = 10 # can help produce similar responses if prompt and seed are always the same llm.parameters.stop_sequences = ["q", "quit", "ahhhhhhhhh"] # stops the model on input of any of these strings llm.parameters.stream = False # determines whether or not to use streaming to receive incremental data reader = ConsoleReader() for prompt in reader: response = await llm.run([UserMessage(prompt)]) reader.write("LLM 🤖 (txt) : ", response.get_text_content()) reader.write("LLM 🤖 (raw) : ", "\n".join([str(msg.to_plain()) for msg in response.output])) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config.js"; import { createConsoleReader } from "examples/helpers/io.js"; import { UserMessage } from "beeai-framework/backend/message"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; const llm = new OllamaChatModel("granite4:micro"); // Optionally one may set llm parameters llm.parameters.maxTokens = 10000; // high number yields longer potential output llm.parameters.topP = 0; // higher number yields more complex vocabulary, recommend only changing p or k llm.parameters.frequencyPenalty = 0; // higher number yields reduction in word reptition llm.parameters.temperature = 0; // higher number yields greater randomness and variation llm.parameters.topK = 0; // higher number yields more variance, recommend only changing p or k llm.parameters.n = 1; // higher number yields more choices llm.parameters.presencePenalty = 0; // higher number yields reduction in repetition of words llm.parameters.seed = 10; // can help produce similar responses if prompt and seed are always the same llm.parameters.stopSequences = ["q", "quit", "ahhhhhhhhh"]; // stops the model on input of any of these strings // alternatively llm.config({ parameters: { maxTokens: 10000, // other parameters }, }); const reader = createConsoleReader(); for await (const { prompt } of reader) { const response = await llm.create({ messages: [new UserMessage(prompt)], }); reader.write(`LLM 🤖 (txt) : `, response.getTextContent()); reader.write(`LLM 🤖 (raw) : `, JSON.stringify(response.messages)); } ``` ### Text generation The most basic usage is to generate text responses: ```py Python [expandable] theme={null} from beeai_framework.adapters.ollama.backend.chat import OllamaChatModel from beeai_framework.backend.message import UserMessage model = OllamaChatModel("llama3.1") response = await model.create( messages=[UserMessage("what states are part of New England?")] ) print(response.get_text_content()) ``` ```ts TypeScript [expandable] theme={null} import { UserMessage } from "beeai-framework/backend/message"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; const llm = new OllamaChatModel("llama3.1"); const response = await llm.create({ messages: [new UserMessage("what states are part of New England?")], }); console.log(response.getTextContent()); ``` Execution parameters (those passed to `model.create({...})`) take precedent over ones defined via `config`. ### Streaming responses For applications requiring real-time responses: ```py Python [expandable] theme={null} from beeai_framework.adapters.ollama.backend.chat import OllamaChatModel from beeai_framework.backend.message import UserMessage llm = OllamaChatModel("llama3.1") user_message = UserMessage("How many islands make up the country of Cape Verde?") response = await llm.create(messages=[user_message], stream=True) .on( "new_token", lambda data, event: print(data.value.get_text_content())) ) ) print("Full response", response.get_text_content()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config.js"; import { createConsoleReader } from "examples/helpers/io.js"; import { UserMessage } from "beeai-framework/backend/message"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; const llm = new OllamaChatModel("granite4:micro"); const reader = createConsoleReader(); for await (const { prompt } of reader) { const response = await llm .create({ messages: [new UserMessage(prompt)], }) .observe((emitter) => emitter.match("*", (data, event) => { reader.write(`LLM 🤖 (event: ${event.name})`, JSON.stringify(data)); // if you want to close the stream prematurely, just uncomment the following line // callbacks.abort() }), ); reader.write(`LLM 🤖 (txt) : `, response.getTextContent()); reader.write(`LLM 🤖 (raw) : `, JSON.stringify(response.messages)); } ``` ### Structured generation Generate structured data according to a schema: ```py Python [expandable] theme={null} import asyncio import json import sys import traceback from pydantic import BaseModel, Field from beeai_framework.backend import ChatModel, UserMessage from beeai_framework.errors import FrameworkError async def main() -> None: model = ChatModel.from_name("ollama:granite4:micro") class ProfileSchema(BaseModel): first_name: str = Field(..., min_length=1) last_name: str = Field(..., min_length=1) address: str age: int hobby: str response = await model.run( [UserMessage("Generate a profile of a citizen of Europe.")], response_format=ProfileSchema ) assert isinstance(response.output_structured, ProfileSchema) print(json.dumps(response.output_structured.model_dump(), indent=4)) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { ChatModel, UserMessage } from "beeai-framework/backend/core"; import { z } from "zod"; const model = await ChatModel.fromName("ollama:granite4:micro"); const response = await model.createStructure({ schema: z.union([ z.object({ firstName: z.string().min(1), lastName: z.string().min(1), address: z.string(), age: z.number().int().min(1), hobby: z.string(), }), z.object({ error: z.string(), }), ]), messages: [new UserMessage("Generate a profile of a citizen of Europe.")], }); console.log(response.object); ``` ### Tool calling Integrate external tools with your AI model: ```py Python [expandable] theme={null} import asyncio import json import re import sys import traceback from beeai_framework.backend import ( AnyMessage, ChatModel, ChatModelParameters, MessageToolResultContent, SystemMessage, ToolMessage, UserMessage, ) from beeai_framework.errors import FrameworkError from beeai_framework.tools import AnyTool, ToolOutput from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool from beeai_framework.tools.weather.openmeteo import OpenMeteoTool async def main() -> None: model = ChatModel.from_name("ollama:llama3.1", ChatModelParameters(temperature=0)) tools: list[AnyTool] = [DuckDuckGoSearchTool(), OpenMeteoTool()] messages: list[AnyMessage] = [ SystemMessage("You are a helpful assistant. Use tools to provide a correct answer."), UserMessage("What's the fastest marathon time?"), ] while True: response = await model.run( messages, tools=tools, ) tool_calls = response.get_tool_calls() messages.extend(response.output) tool_results: list[ToolMessage] = [] for tool_call in tool_calls: print(f"-> running '{tool_call.tool_name}' tool with {tool_call.args}") tool: AnyTool = next(tool for tool in tools if tool.name == tool_call.tool_name) assert tool is not None res: ToolOutput = await tool.run(json.loads(tool_call.args)) result = res.get_text_content() print(f"<- got response from '{tool_call.tool_name}'", re.sub(r"\s+", " ", result)[:256] + " (truncated)") tool_results.append( ToolMessage( MessageToolResultContent( result=result, tool_name=tool_call.tool_name, tool_call_id=tool_call.id, ) ) ) messages.extend(tool_results) answer = response.get_text_content() if answer: print(f"Agent: {answer}") break if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config"; import { ChatModel, Message, SystemMessage, ToolMessage, UserMessage, } from "beeai-framework/backend/core"; import { DuckDuckGoSearchTool } from "beeai-framework/tools/search/duckDuckGoSearch"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; import { AnyTool, ToolOutput } from "beeai-framework/tools/base"; const model = await ChatModel.fromName("ollama:granite4:micro"); const tools: AnyTool[] = [new DuckDuckGoSearchTool(), new OpenMeteoTool()]; const messages: Message[] = [ new SystemMessage("You are a helpful assistant. Use tools to provide a correct answer."), new UserMessage("What's the fastest marathon time?"), ]; while (true) { const response = await model.create({ messages, tools, }); messages.push(...response.messages); const toolCalls = response.getToolCalls(); const toolResults = await Promise.all( toolCalls.map(async ({ input, toolName, toolCallId }) => { console.log(`-> running '${toolName}' tool with ${JSON.stringify(input)}`); const tool = tools.find((tool) => tool.name === toolName)!; const response: ToolOutput = await tool.run(input as any); const result = response.getTextContent(); console.log( `<- got response from '${toolName}'`, result.replaceAll(/\s+/g, " ").substring(0, 90).concat(" (truncated)"), ); return new ToolMessage({ type: "tool-result", output: { type: "text", value: result }, toolName, toolCallId, }); }), ); messages.push(...toolResults); const answer = response.getTextContent(); if (answer) { console.info(`Agent: ${answer}`); break; } } ``` *** ## Embedding model The `EmbedingModel` class provides functionality for generating vector embeddings from text. ### Embedding model initialization You can initialize an embedding model in multiple ways: **Method 1: Using the `from_name` method** ```py Python [expandable] theme={null} from beeai_framework.backend.embedding import EmbeddingModel model = EmbeddingModel.from_name("ollama:nomic-embed-text") ``` ```ts TypeScript [expandable] theme={null} import { EmbeddingModel } from "beeai-framework/backend/embedding"; const model = await EmbeddingModel.fromName("ollama:nomic-embed-text"); ``` **Method 2: Directly specifying the provider class** ```py Python [expandable] theme={null} from beeai_framework.adapters.ollama.backend import OllamaEmbeddingModel model = OllamaEmbeddingModel("nomic-embed-text") ``` ```ts TypeScript [expandable] theme={null} import { OpenAIEmbeddingModel } from "beeai-framework/adapters/openai/embedding"; const model = new OpenAIEmbeddingModel( "text-embedding-3-large", { dimensions: 512, maxEmbeddingsPerCall: 5, }, { baseURL: "your_custom_endpoint", compatibility: "compatible", headers: { CUSTOM_HEADER: "...", }, }, ); ``` ### Embedding model usage Generate embeddings for one or more text strings: ```py Python [expandable] theme={null} from beeai_framework.backend.embedding import EmbeddingModel model = EmbeddingModel.from_name("ollama:nomic-embed-text") response = await model.create(["Hello world!", "Hello Bee!"]) console.log(response.values) console.log(response.embeddings) ``` ```ts TypeScript [expandable] theme={null} import { EmbeddingModel } from "beeai-framework/backend/embedding"; const model = await EmbeddingModel.fromName("ollama:nomic-embed-text"); const response = await model.create({ values: ["Hello world!", "Hello Bee!"], }); console.log(response.values); console.log(response.embeddings); ``` *** ## Adding a Provider Using the LangChain Adapter If your preferred provider isn't directly supported, you can use the LangChain adapter as a bridge as long as that provider has LangChain compatibility. ```py Python [expandable] theme={null} import asyncio import json import sys import traceback from datetime import UTC, datetime from pydantic import BaseModel, Field from beeai_framework.adapters.langchain.backend.chat import LangChainChatModel from beeai_framework.backend import ( AnyMessage, ChatModelNewTokenEvent, MessageToolResultContent, SystemMessage, ToolMessage, UserMessage, ) from beeai_framework.emitter import EventMeta from beeai_framework.errors import AbortError, FrameworkError from beeai_framework.parsers.field import ParserField from beeai_framework.parsers.line_prefix import LinePrefixParser, LinePrefixParserNode from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.utils import AbortSignal # prevent import error for langchain_ollama (only needed in this context) cur_dir = sys.path.pop(0) while cur_dir in sys.path: sys.path.remove(cur_dir) from langchain_ollama.chat_models import ChatOllama as LangChainOllamaChat # noqa: E402 async def langchain_ollama_from_name() -> None: langchain_llm = LangChainOllamaChat(model="granite4:micro") llm = LangChainChatModel(langchain_llm) user_message = UserMessage("what states are part of New England?") response = await llm.run([user_message]) print(response.get_text_content()) async def langchain_ollama_granite_from_name() -> None: langchain_llm = LangChainOllamaChat(model="granite4:micro") llm = LangChainChatModel(langchain_llm) user_message = UserMessage("what states are part of New England?") response = await llm.run([user_message]) print(response.get_text_content()) async def langchain_ollama_sync() -> None: langchain_llm = LangChainOllamaChat(model="granite4:micro") llm = LangChainChatModel(langchain_llm) user_message = UserMessage("what is the capital of Massachusetts?") response = await llm.run([user_message]) print(response.get_text_content()) async def langchain_ollama_stream() -> None: langchain_llm = LangChainOllamaChat(model="granite4:micro") llm = LangChainChatModel(langchain_llm) user_message = UserMessage("How many islands make up the country of Cape Verde?") response = await llm.run([user_message], stream=True) print(response.get_text_content()) async def langchain_ollama_stream_abort() -> None: langchain_llm = LangChainOllamaChat(model="granite4:micro") llm = LangChainChatModel(langchain_llm) user_message = UserMessage("What is the smallest of the Cape Verde islands?") try: response = await llm.run([user_message], stream=True, signal=AbortSignal.timeout(0.5)) if response is not None: print(response.get_text_content()) else: print("No response returned.") except AbortError as err: print(f"Aborted: {err}") async def langchain_ollama_structure() -> None: class TestSchema(BaseModel): answer: str = Field(description="your final answer") langchain_llm = LangChainOllamaChat(model="granite4:micro") llm = LangChainChatModel(langchain_llm) user_message = UserMessage("How many islands make up the country of Cape Verde?") response = await llm.run([user_message], response_format=TestSchema) print(response.output_structured) async def langchain_ollama_stream_parser() -> None: langchain_llm = LangChainOllamaChat(model="granite4:micro") llm = LangChainChatModel(langchain_llm) parser = LinePrefixParser( nodes={ "test": LinePrefixParserNode( prefix="Prefix: ", field=ParserField.from_type(str), is_start=True, is_end=True ) } ) async def on_new_token(data: ChatModelNewTokenEvent, event: EventMeta) -> None: await parser.add(data.value.get_text_content()) user_message = UserMessage("Produce 3 lines each starting with 'Prefix: ' followed by a sentence and a new line.") await llm.run([user_message], stream=True).observe(lambda emitter: emitter.on("new_token", on_new_token)) result = await parser.end() print(result) async def langchain_ollama_tool_calling() -> None: langchain_llm = LangChainOllamaChat(model="granite4:micro") llm = LangChainChatModel(langchain_llm) llm.parameters.stream = True weather_tool = OpenMeteoTool() messages: list[AnyMessage] = [ SystemMessage( f"""You are a helpful assistant that uses tools to provide answers. Current date is {datetime.now(tz=UTC).date()!s} """ ), UserMessage("What is the current weather in Berlin?"), ] response = await llm.run(messages, tools=[weather_tool], tool_choice="required") messages.extend(response.output) tool_call_msg = response.get_tool_calls()[0] print(tool_call_msg.model_dump()) tool_response = await weather_tool.run(json.loads(tool_call_msg.args)) tool_response_msg = ToolMessage( MessageToolResultContent( result=tool_response.get_text_content(), tool_name=tool_call_msg.tool_name, tool_call_id=tool_call_msg.id ) ) print(tool_response_msg.to_plain()) final_response = await llm.run([*messages, tool_response_msg], tools=[]) print(final_response.get_text_content()) async def langchain_ollama_cloning() -> None: langchain_llm = LangChainOllamaChat(model="granite4:micro") llm = LangChainChatModel(langchain_llm) await llm.clone() async def main() -> None: print("*" * 10, "langchain_ollama_from_name") await langchain_ollama_from_name() print("*" * 10, "langchain_ollama_granite_from_name") await langchain_ollama_granite_from_name() print("*" * 10, "langchain_ollama_sync") await langchain_ollama_sync() print("*" * 10, "langchain_ollama_stream") await langchain_ollama_stream() print("*" * 10, "langchain_ollama_stream_abort") await langchain_ollama_stream_abort() print("*" * 10, "langchain_ollama_structure") await langchain_ollama_structure() print("*" * 10, "langchain_ollama_stream_parser") await langchain_ollama_stream_parser() print("*" * 10, "langchain_ollama_tool_calling") await langchain_ollama_tool_calling() print("*" * 10, "langchain_ollama_cloning") await langchain_ollama_cloning() if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} // NOTE: ensure you have installed following packages // - @langchain/core // - @langchain/cohere (or any other provider related package that you would like to use) // List of available providers: https://js.langchain.com/v0.2/docs/integrations/chat/ import { LangChainChatModel } from "beeai-framework/adapters/langchain/backend/chat"; // @ts-expect-error package not installed import { ChatCohere } from "@langchain/cohere"; import "dotenv/config.js"; import { ToolMessage, UserMessage } from "beeai-framework/backend/message"; import { z } from "zod"; import { ChatModelError } from "beeai-framework/backend/errors"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; const llm = new LangChainChatModel( new ChatCohere({ model: "command-r-plus", temperature: 0, }), ); async function langchainSync() { const response = await llm.create({ messages: [new UserMessage("what is the capital of Massachusetts?")], }); console.info(response.getTextContent()); } async function langchainStream() { const response = await llm.create({ messages: [new UserMessage("How many islands make up the country of Cape Verde?")], stream: true, }); console.info(response.getTextContent()); } async function langchainAbort() { try { const response = await llm.create({ messages: [new UserMessage("What is the smallest of the Cape Verde islands?")], stream: true, abortSignal: AbortSignal.timeout(1 * 1000), }); console.info(response.getTextContent()); } catch (err) { if (err instanceof ChatModelError) { console.log("Aborted", { err }); } } } async function langchainStructure() { const response = await llm.createStructure({ schema: z.object({ answer: z.string({ description: "your final answer" }), }), messages: [new UserMessage("How many islands make up the country of Cape Verde?")], }); console.info(response.object); } async function langchainToolCalling() { const userMessage = new UserMessage( `What is the current weather in Boston? Current date is ${new Date().toISOString().split("T")[0]}.`, ); const weatherTool = new OpenMeteoTool({ retryOptions: { maxRetries: 3 } }); const response = await llm.create({ messages: [userMessage], tools: [weatherTool] }); const toolCallMsg = response.getToolCalls()[0]; console.debug(JSON.stringify(toolCallMsg)); const toolResponse = await weatherTool.run(toolCallMsg.input as any); const toolResponseMsg = new ToolMessage({ type: "tool-result", output: { type: "text", value: toolResponse.getTextContent() }, toolName: toolCallMsg.toolName, toolCallId: toolCallMsg.toolCallId, }); console.info(toolResponseMsg.toPlain()); const finalResponse = await llm.create({ messages: [userMessage, ...response.messages, toolResponseMsg], tools: [], }); console.info(finalResponse.getTextContent()); } async function langchainDebug() { // Log every request llm.emitter.match("*", (value, event) => console.debug( `Time: ${event.createdAt.toISOString()}`, `Event: ${event.name}`, `Data: ${JSON.stringify(value)}`, ), ); const response = await llm.create({ messages: [new UserMessage("Hello world!")], }); console.info(response.messages[0].toPlain()); } console.info(" langchainSync".padStart(25, "*")); await langchainSync(); console.info(" langchainStream".padStart(25, "*")); await langchainStream(); console.info(" langchainAbort".padStart(25, "*")); await langchainAbort(); console.info(" langchainStructure".padStart(25, "*")); await langchainStructure(); console.info(" langchainToolCalling".padStart(25, "*")); await langchainToolCalling(); console.info(" langchainDebug".padStart(25, "*")); await langchainDebug(); ``` *** ## Troubleshooting Common issues and their solutions: 1. Authentication errors: Ensure all required environment variables are set correctly 2. Model not found: Verify that the model ID is correct and available for the selected provider *** ## Examples Explore reference backend implementations in Python Explore reference backend implementations in TypeScript # Cache Source: https://framework.beeai.dev/modules/cache ## Overview Caching is a technique used to temporarily store copies of data or computation results to improve performance by reducing the need to repeatedly fetch or compute the same data from slower or more resource-intensive sources. In the context of AI applications, caching provides several important benefits: * 🚀 **Performance improvement** - Avoid repeating expensive operations like API calls or complex calculations * 💰 **Cost reduction** - Minimize repeated calls to paid services (like external APIs or LLM providers) * ⚡ **Latency reduction** - Deliver faster responses to users by serving cached results * 🔄 **Consistency** - Ensure consistent responses for identical inputs BeeAI framework provides a robust caching system with multiple implementations to suit different use cases. *** ## Core concepts ### Cache types BeeAI framework offers several cache implementations out of the box: | Type | Description | | :--------------------- | :------------------------------------------------------------------- | | **UnconstrainedCache** | Simple in-memory cache with no limits | | **SlidingCache** | In-memory cache that maintains a maximum number of entries | | **FileCache** | Persistent cache that stores data on disk | | **NullCache** | Special implementation that performs no caching (useful for testing) | Each cache type implements the `BaseCache` interface, making them interchangeable in your code. ### Usage patterns BeeAI framework supports several caching patterns: | Usage pattern | Description | | :---------------------- | :----------------------------------- | | **Direct caching** | Manually store and retrieve values | | **Function decoration** | Automatically cache function returns | | **Tool integration** | Cache tool execution results | | **LLM integration** | Cache model responses | *** ## Basic usage ### Caching function output The simplest way to use caching is to wrap a function that produces deterministic output: ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.cache import UnconstrainedCache from beeai_framework.errors import FrameworkError async def main() -> None: cache: UnconstrainedCache[int] = UnconstrainedCache() async def fibonacci(n: int) -> int: cache_key = str(n) cached = await cache.get(cache_key) if cached: return int(cached) if n < 1: result = 0 elif n <= 2: result = 1 else: result = await fibonacci(n - 1) + await fibonacci(n - 2) await cache.set(cache_key, result) return result print(await fibonacci(10)) # 55 print(await fibonacci(9)) # 34 (retrieved from cache) print(f"Cache size {await cache.size()}") # 10 if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { UnconstrainedCache } from "beeai-framework/cache/unconstrainedCache"; const cache = new UnconstrainedCache(); async function fibonacci(n: number): Promise { const cacheKey = n.toString(); const cached = await cache.get(cacheKey); if (cached !== undefined) { return cached; } const result = n < 1 ? 0 : n <= 2 ? 1 : (await fibonacci(n - 1)) + (await fibonacci(n - 2)); await cache.set(cacheKey, result); return result; } console.info(await fibonacci(10)); // 55 console.info(await fibonacci(9)); // 34 (retrieved from cache) console.info(`Cache size ${await cache.size()}`); // 10 ``` ### Using with tools BeeAI framework's caching system seamlessly integrates with tools: ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.cache import SlidingCache from beeai_framework.errors import FrameworkError from beeai_framework.tools.search.wikipedia import ( WikipediaTool, WikipediaToolInput, ) async def main() -> None: wikipedia_client = WikipediaTool({"full_text": True, "cache": SlidingCache(size=100, ttl=5 * 60)}) print(await wikipedia_client.cache.size()) # 0 tool_input = WikipediaToolInput(query="United States") first = await wikipedia_client.run(tool_input) print(await wikipedia_client.cache.size()) # 1 # new request with the EXACTLY same input will be retrieved from the cache tool_input = WikipediaToolInput(query="United States") second = await wikipedia_client.run(tool_input) print(first.get_text_content() == second.get_text_content()) # True print(await wikipedia_client.cache.size()) # 1 if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { SlidingCache } from "beeai-framework/cache/slidingCache"; import { WikipediaTool } from "beeai-framework/tools/search/wikipedia"; const ddg = new WikipediaTool({ cache: new SlidingCache({ size: 100, // max 100 entries ttl: 5 * 60 * 1000, // 5 minutes lifespan }), }); const response = await ddg.run({ query: "United States", }); // upcoming requests with the EXACTLY same input will be retrieved from the cache ``` ### Using with LLMs You can also cache LLM responses to save on API costs: ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.adapters.ollama import OllamaChatModel from beeai_framework.backend import ChatModelParameters, UserMessage from beeai_framework.cache import SlidingCache from beeai_framework.errors import FrameworkError async def main() -> None: llm = OllamaChatModel("granite3.3") llm.config(parameters=ChatModelParameters(max_tokens=25), cache=SlidingCache(size=50)) print(await llm.cache.size()) # 0 first = await llm.run([UserMessage("Who is Amilcar Cabral?")]) print(await llm.cache.size()) # 1 # new request with the EXACTLY same input will be retrieved from the cache second = await llm.run([UserMessage("Who is Amilcar Cabral?")]) print(first.get_text_content() == second.get_text_content()) # True print(await llm.cache.size()) # 1 if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { SlidingCache } from "beeai-framework/cache/slidingCache"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; import { UserMessage } from "beeai-framework/backend/message"; const llm = new OllamaChatModel("granite4:micro"); llm.config({ cache: new SlidingCache({ size: 50, }), parameters: { maxTokens: 25, }, }); console.info(await llm.cache.size()); // 0 const first = await llm.create({ messages: [new UserMessage("Who was Alan Turing?")], }); // upcoming requests with the EXACTLY same input will be retrieved from the cache console.info(await llm.cache.size()); // 1 const second = await llm.create({ messages: [new UserMessage("Who was Alan Turing?")], }); console.info(first.getTextContent() === second.getTextContent()); // true console.info(await llm.cache.size()); // 1 ``` *** ## Cache types ### UnconstrainedCache The simplest cache type with no constraints on size or entry lifetime. Good for development and smaller applications. ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.cache import UnconstrainedCache from beeai_framework.errors import FrameworkError async def main() -> None: cache: UnconstrainedCache[int] = UnconstrainedCache() # Save await cache.set("a", 1) await cache.set("b", 2) # Read result = await cache.has("a") print(result) # True # Meta print(cache.enabled) # True print(await cache.has("a")) # True print(await cache.has("b")) # True print(await cache.has("c")) # False print(await cache.size()) # 2 # Delete await cache.delete("a") print(await cache.has("a")) # False # Clear await cache.clear() print(await cache.size()) # 0 if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { UnconstrainedCache } from "beeai-framework/cache/unconstrainedCache"; const cache = new UnconstrainedCache(); // Save await cache.set("a", 1); await cache.set("b", 2); // Read const result = await cache.get("a"); console.log(result); // 1 // Meta console.log(cache.enabled); // true console.log(await cache.has("a")); // true console.log(await cache.has("b")); // true console.log(await cache.has("c")); // false console.log(await cache.size()); // 2 // Delete await cache.delete("a"); console.log(await cache.has("a")); // false // Clear await cache.clear(); console.log(await cache.size()); // 0 ``` ### SlidingCache Maintains a maximum number of entries, removing the oldest entries when the limit is reached. ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.cache import SlidingCache from beeai_framework.errors import FrameworkError async def main() -> None: cache: SlidingCache[int] = SlidingCache( size=3, # (required) number of items that can be live in the cache at a single moment ttl=1, # // (optional, default is Infinity) Time in seconds after the element is removed from a cache ) await cache.set("a", 1) await cache.set("b", 2) await cache.set("c", 3) await cache.set("d", 4) # overflow - cache internally removes the oldest entry (key "a") print(await cache.has("a")) # False print(await cache.size()) # 3 if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { SlidingCache } from "beeai-framework/cache/slidingCache"; const cache = new SlidingCache({ size: 3, // (required) number of items that can be live in the cache at a single moment ttl: 1000, // (optional, default is Infinity) Time in milliseconds after the element is removed from a cache }); await cache.set("a", 1); await cache.set("b", 2); await cache.set("c", 3); await cache.set("d", 4); // overflow - cache internally removes the oldest entry (key "a") console.log(await cache.has("a")); // false console.log(await cache.size()); // 3 ``` ### FileCache Persists cache data to disk, allowing data to survive if application restarts. Use it when caches must survive process restarts or you need to share state between workers. Persisted entries still respect TTL and eviction settings, so design your limits accordingly. ```py Python [expandable] theme={null} import asyncio import json import sys import tempfile import time import traceback from collections import OrderedDict from collections.abc import Mapping from pathlib import Path from typing import Generic, TypeVar from beeai_framework.cache import BaseCache from beeai_framework.errors import FrameworkError T = TypeVar("T") class JsonFileCache(BaseCache[T], Generic[T]): """Simple file-backed cache with optional LRU eviction and TTL support.""" def __init__(self, path: Path, *, size: int = 128, ttl: float | None = None) -> None: super().__init__() self._path = path self._size = size self._ttl = ttl self._items: OrderedDict[str, tuple[T, float | None]] = OrderedDict() self._load_from_disk() @property def source(self) -> Path: return self._path @classmethod async def from_mapping( cls, path: Path, items: Mapping[str, T], *, size: int = 128, ttl: float | None = None, ) -> "JsonFileCache[T]": cache = cls(path, size=size, ttl=ttl) for key, value in items.items(): await cache.set(key, value) return cache async def size(self) -> int: await self._purge_expired() return len(self._items) async def set(self, key: str, value: T) -> None: await self._purge_expired() expires_at = time.time() + self._ttl if self._ttl is not None else None if key in self._items: self._items.pop(key) self._items[key] = (value, expires_at) await self._enforce_capacity() self._dump_to_disk() async def get(self, key: str) -> T | None: await self._purge_expired() if key not in self._items: return None value, expires_at = self._items.pop(key) self._items[key] = (value, expires_at) return value async def has(self, key: str) -> bool: await self._purge_expired() return key in self._items async def delete(self, key: str) -> bool: await self._purge_expired() if key not in self._items: return False self._items.pop(key) self._dump_to_disk() return True async def clear(self) -> None: self._items.clear() if self._path.exists(): self._path.unlink() async def reload(self) -> None: self._items.clear() self._load_from_disk() await self._purge_expired() async def _purge_expired(self) -> None: now = time.time() expired_keys = [ key for key, (_, expires_at) in list(self._items.items()) if expires_at is not None and expires_at <= now ] for key in expired_keys: self._items.pop(key, None) if expired_keys: self._dump_to_disk() async def _enforce_capacity(self) -> None: while len(self._items) > self._size: oldest_key, _ = self._items.popitem(last=False) def _load_from_disk(self) -> None: if not self._path.exists(): return try: raw = json.loads(self._path.read_text()) except json.JSONDecodeError: return now = time.time() for key, payload in raw.items(): expires_at = payload.get("expires_at") if expires_at is not None and expires_at <= now: continue self._items[key] = (payload["value"], expires_at) def _dump_to_disk(self) -> None: self._path.parent.mkdir(parents=True, exist_ok=True) data = {key: {"value": value, "expires_at": expires_at} for key, (value, expires_at) in self._items.items()} self._path.write_text(json.dumps(data, indent=2)) async def main() -> None: with tempfile.TemporaryDirectory() as tmpdir: path = Path(tmpdir) / "bee_cache.json" cache: JsonFileCache[dict[str, str]] = JsonFileCache(path, size=2, ttl=1.5) await cache.set("profile", {"name": "Bee", "role": "assistant"}) await cache.set("settings", {"theme": "dark"}) print(f"Cache persisted to {cache.source}") await cache.set("session", {"token": "abc123"}) print(await cache.has("profile")) # False -> evicted when capacity exceeded reloaded: JsonFileCache[dict[str, str]] = JsonFileCache(path, size=2, ttl=1.5) print(await reloaded.get("settings")) # {'theme': 'dark'} await asyncio.sleep(1.6) await reloaded.reload() print(await reloaded.get("session")) # None -> TTL expired if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { FileCache } from "beeai-framework/cache/fileCache"; import * as os from "node:os"; const cache = new FileCache({ fullPath: `${os.tmpdir()}/bee_file_cache_${Date.now()}.json`, }); console.log(`Saving cache to "${cache.source}"`); await cache.set("abc", { firstName: "John", lastName: "Doe" }); ``` #### With custom provider Seed a file-backed cache from another provider when you want to warm the disk cache before first use or promote hot data captured in memory. The example below clones an `UnconstrainedCache` into the JSON file cache so new processes can reuse it immediately. ```py Python [expandable] theme={null} import asyncio import sys import tempfile import traceback from pathlib import Path from typing import TypeVar from beeai_framework.cache import UnconstrainedCache from beeai_framework.errors import FrameworkError from examples.cache.file_cache import JsonFileCache T = TypeVar("T") async def export_cache(provider: UnconstrainedCache[T]) -> dict[str, T]: """Clone an in-memory cache so that we can safely persist its content.""" cloned = await provider.clone() # UnconstrainedCache stores entries in a simple dict, so cloning is inexpensive here. return getattr(cloned, "_provider", {}).copy() async def main() -> None: memory_cache: UnconstrainedCache[int] = UnconstrainedCache() await memory_cache.set("tasks:open", 7) await memory_cache.set("tasks:closed", 12) with tempfile.TemporaryDirectory() as tmpdir: path = Path(tmpdir) / "bee_cache.json" file_cache = await JsonFileCache.from_mapping(path, await export_cache(memory_cache), size=10, ttl=10) print(f"Promoted cache to disk: {file_cache.source}") print(await file_cache.get("tasks:open")) # 7 await file_cache.set("tasks:stale", 1) print(await file_cache.size()) # 3 reloaded: JsonFileCache[int] = JsonFileCache(path, size=10, ttl=10) print(await reloaded.get("tasks:closed")) # 12 if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { FileCache } from "beeai-framework/cache/fileCache"; import { UnconstrainedCache } from "beeai-framework/cache/unconstrainedCache"; import os from "node:os"; const memoryCache = new UnconstrainedCache(); await memoryCache.set("a", 1); const fileCache = await FileCache.fromProvider(memoryCache, { fullPath: `${os.tmpdir()}/bee_file_cache.json`, }); console.log(`Saving cache to "${fileCache.source}"`); console.log(await fileCache.get("a")); // 1 ``` ### NullCache A special cache that implements the `BaseCache` interface but performs no caching. Useful for testing or temporarily disabling caching. The reason for implementing is to enable [Null object pattern](https://en.wikipedia.org/wiki/Null_object_pattern). *** ## Advanced usage ### Cache decorator Create a reusable decorator when you want to keep caching logic close to your functions without wiring cache calls manually. ```py Python [expandable] theme={null} import asyncio import sys import time import traceback from beeai_framework.cache import SlidingCache, cached from beeai_framework.errors import FrameworkError request_cache: SlidingCache[str] = SlidingCache(size=8, ttl=2) class ReportGenerator: def __init__(self) -> None: self._call_counter = 0 @cached(request_cache) async def generate(self, department: str) -> str: self._call_counter += 1 await asyncio.sleep(0.1) timestamp = time.time() return f"{department}:{self._call_counter}@{timestamp:.0f}" async def main() -> None: generator = ReportGenerator() first = await generator.generate("sales") second = await generator.generate("sales") print(first == second) # True -> cached result await asyncio.sleep(2.1) # TTL expired third = await generator.generate("sales") print(first == third) # False -> cache miss, recomputed if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { Cache } from "beeai-framework/cache/decoratorCache"; class Generator { @Cache() get(seed: number) { return (Math.random() * 1000) / Math.max(seed, 1); } } const generator = new Generator(); const a = generator.get(5); const b = generator.get(5); console.info(a === b); // true console.info(a === generator.get(6)); // false ``` For more complex caching logic, you can customize the key generation: Use custom key builders to partition cache entries per tenant or time window, and clear the cache in response to deployment events. ```py Python [expandable] theme={null} import asyncio import datetime as dt import random import sys import traceback from typing import Any from beeai_framework.cache import BaseCache, SlidingCache, cached from beeai_framework.errors import FrameworkError activity_cache: SlidingCache[dict[str, Any]] = SlidingCache(size=16, ttl=5) def session_cache_key(args: tuple[Any, ...], kwargs: dict[str, Any]) -> str: user_id = kwargs.get("user_id") or args[0] scope = kwargs.get("scope", "default") bucket: int | None = kwargs.get("minute_bucket") payload = {"user_id": user_id, "scope": scope} if bucket is not None: payload["minute_bucket"] = bucket return BaseCache.generate_key(payload) class FeatureFlagService: def __init__(self, *, caching_enabled: bool = True) -> None: self._enabled = caching_enabled self._db_hits = 0 @cached(activity_cache, enabled=True, key_fn=session_cache_key) async def load_flags( self, user_id: str, scope: str = "default", minute_bucket: int | None = None ) -> dict[str, Any]: self._db_hits += 1 await asyncio.sleep(0.05) return { "user": user_id, "scope": scope, "db_hits": self._db_hits, "flags": {"beta_search": random.choice([True, False])}, "refreshed_at": dt.datetime.now(dt.UTC).isoformat(timespec="seconds"), } async def main() -> None: service = FeatureFlagService() bucket = int(dt.datetime.now(dt.UTC).timestamp() // 60) first = await service.load_flags("42", scope="admin", minute_bucket=bucket) second = await service.load_flags("42", scope="admin", minute_bucket=bucket) print(first == second) # True -> same cache key within a minute bucket await activity_cache.clear() # Manual invalidation when new feature set deployed refreshed = await service.load_flags("42", scope="admin", minute_bucket=bucket) print(refreshed["db_hits"]) # 2 -> cache miss due to clear # Changing scope hits a different cache entry without flushing existing data. other_scope = await service.load_flags("42", scope="viewer", minute_bucket=bucket) print(other_scope["scope"]) # viewer if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { Cache, SingletonCacheKeyFn } from "beeai-framework/cache/decoratorCache"; class MyService { @Cache({ cacheKey: SingletonCacheKeyFn, ttl: 3600, enumerable: true, enabled: true, }) get id() { return Math.floor(Math.random() * 1000); } reset() { Cache.getInstance(this, "id").clear(); } } const service = new MyService(); const a = service.id; console.info(a === service.id); // true service.reset(); console.info(a === service.id); // false ``` ### CacheFn helper For more dynamic caching needs, the `CacheFn` helper provides a functional approach: It is well-suited for API tokens or other resources that return an expiry with each refresh—call `update_ttl` before returning the value so the cache matches the upstream lifetime. ```py Python [expandable] theme={null} import asyncio import random import sys import traceback from typing_extensions import TypedDict from beeai_framework.cache import CacheFn from beeai_framework.errors import FrameworkError class TokenResponse(TypedDict): token: str expires_in: float async def main() -> None: async def fetch_api_token() -> str: response: TokenResponse = {"token": f"TOKEN-{random.randint(1000, 9999)}", "expires_in": 0.2} get_token.update_ttl(response["expires_in"]) await asyncio.sleep(0.05) return response["token"] get_token = CacheFn.create(fetch_api_token, default_ttl=0.1) first = await get_token() second = await get_token() print(first == second) # True -> cached value await asyncio.sleep(0.25) refreshed = await get_token() print(first == refreshed) # False -> TTL elapsed, value refreshed if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { CacheFn } from "beeai-framework/cache/decoratorCache"; import { setTimeout } from "node:timers/promises"; const getSecret = CacheFn.create( async () => { // instead of mocking response you would do a real fetch request const response = await Promise.resolve({ secret: Math.random(), expiresIn: 100 }); getSecret.updateTTL(response.expiresIn); return response.secret; }, {}, // options object ); const token = await getSecret(); console.info(token === (await getSecret())); // true await setTimeout(150); console.info(token === (await getSecret())); // false ``` *** ## Creating a custom cache provider You can create your own cache implementation by extending the `BaseCache` class: ```py Python [expandable] theme={null} from typing import TypeVar from beeai_framework.cache import BaseCache T = TypeVar("T") class CustomCache(BaseCache[T]): async def size(self) -> int: raise NotImplementedError("CustomCache 'size' not yet implemented") # pyrefly: ignore [bad-param-name-override] async def set(self, _key: str, _value: T) -> None: raise NotImplementedError("CustomCache 'set' not yet implemented") async def get(self, key: str) -> T | None: raise NotImplementedError("CustomCache 'get' not yet implemented") async def has(self, key: str) -> bool: raise NotImplementedError("CustomCache 'has' not yet implemented") async def delete(self, key: str) -> bool: raise NotImplementedError("CustomCache 'delete' not yet implemented") async def clear(self) -> None: raise NotImplementedError("CustomCache 'clear' not yet implemented") ``` ```ts TypeScript [expandable] theme={null} import { BaseCache } from "beeai-framework/cache/base"; import { NotImplementedError } from "beeai-framework/errors"; export class CustomCache extends BaseCache { size(): Promise { throw new NotImplementedError(); } set(key: string, value: T): Promise { throw new NotImplementedError(); } get(key: string): Promise { throw new NotImplementedError(); } has(key: string): Promise { throw new NotImplementedError(); } delete(key: string): Promise { throw new NotImplementedError(); } clear(): Promise { throw new NotImplementedError(); } createSnapshot() { throw new NotImplementedError(); } loadSnapshot(snapshot: ReturnType): void { throw new NotImplementedError(); } } ``` *** ## Examples Explore reference cache implementations in Python Explore reference cache implementations in TypeScript # Errors Source: https://framework.beeai.dev/modules/errors ## Overview Error handling is a critical part of any Python application, especially when dealing with asynchronous operations, various error types, and error propagation across multiple layers. In the BeeAI Framework, we provide a robust and consistent error-handling structure that ensures reliability and ease of debugging. Supported in Python and TypeScript. *** ## The FrameworkError class Within the BeeAI Framework, regular Python Exceptions are used to handle common issues such as `ValueError`, `TypeError`. However, to provide a more comprehensive error handling experience, we have introduced `FrameworkError`, which is a subclass of Exception. Where additional context is needed, we can use `FrameworkError` to provide additional information about the nature of the error. This may wrap the original exception following the standard Python approach. Benefits of using `FrameworkError`: * **Additional properties**: Exceptions may include additional properties to provide a more detailed view of the error. * **Preserved error chains**: Retains the full history of errors, giving developers full context for debugging. * **Context**: Each error can contain a dictionary of context, allowing you to store additional values to help the user identify and debug the error. * **Utility functions:** Includes methods for formatting error stack traces and explanations, making them suitable for use with LLMs and other external tools. * **Native support:** Built on native Python Exceptions functionality, avoiding the need for additional dependencies while leveraging familiar mechanisms. This structure ensures that users can trace the complete error history while clearly identifying any errors originating from the BeeAI Framework. ```py Python [expandable] theme={null} from beeai_framework.errors import FrameworkError # Create the main FrameworkError instance error = FrameworkError("Function 'getUser' has failed.", is_fatal=True, is_retryable=False) inner_error = FrameworkError("Cannot retrieve data from the API.") innermost_error = ValueError("User with Given ID Does not exist!") # Chain the errors together using __cause__ inner_error.__cause__ = innermost_error error.__cause__ = inner_error # Set the context dictionary for the top level error # Add any additional context here. This will help with debugging error.context["workflow"] = "activity_planner" error.context["provider"] = "ollama" error.context["chat_model"] = "granite3.2:8b" # Print some properties of the error print("\n-- Error properties:") print(f"Message: {error.message}") # Main error message # Is the error fatal/retryable? print(f"Meta: fatal:{FrameworkError.is_fatal(error)} retryable:{FrameworkError.is_retryable(error)}") print(f"Cause: {error.get_cause()}") # Prints the cause of the error print(f"Context: {error.context}") # Prints the dictionary of the error context print("\n-- Explain:") print(error.explain()) # Human-readable format without stack traces (ideal for LLMs) print("\n-- str():") print(str(error)) # Human-readable format (for debug) ``` ```ts TypeScript [expandable] theme={null} import { FrameworkError } from "beeai-framework/errors"; const error = new FrameworkError( "Function 'getUser' has failed.", [ new FrameworkError("Cannot retrieve data from the API.", [ new Error("User with given ID does not exist!"), ]), ], { context: { input: { id: "123" } }, isFatal: true, isRetryable: false, }, ); console.log("Message", error.message); // Main error message console.log("Meta", { fatal: error.isFatal, retryable: error.isRetryable }); // Is the error fatal/retryable? console.log("Context", error.context); // Context in which the error occurred console.log(error.explain()); // Human-readable format without stack traces (ideal for LLMs) console.log(error.dump()); // Full error dump, including sub-errors console.log(error.getCause()); // Retrieve the initial cause of the error ``` Framework error also has two additional properties which help with agent processing, though ultimately the code that catches the exception will determine the appropriate action. * **is\_retryable** : hints that the error is retryable. * **is\_fatal** : hints that the error is fatal. ### Specialized error classes The BeeAI Framework extends `FrameworkError` to create specialized error classes for different components or scenarios. This ensures that each part of the framework has clear and well-defined error types, improving debugging and error handling. Casting an unknown error to a `FrameworkError` can be done by calling the `FrameworkError.ensure` static method ([Python example](https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/errors/cast.py), [TypeScript example](https://github.com/i-am-bee/beeai-framework/blob/main/typescript/examples/errors/cast.ts)). The definitions for these classes are typically local to the module where they are raised. | Error Class | Category | Description | | :------------------------- | :--------------- | :--------------------------------------------------------------------------------------- | | `AbortError` | Aborts | Raised when an operation has been aborted | | `ToolError` | Tools | Raised when a problem is reported by a tool | | `ToolInputValidationError` | Tools | Extends ToolError, raised when input validation fails | | `AgentError` | Agents | Raised when problems occur in agents | | `PromptTemplateError` | Prompt Templates | Raised when problems occur processing prompt templates | | `LoggerError` | Loggers | Raised when errors occur during logging | | `SerializerError` | Serializers | Raised when problems occur serializing or deserializing objects | | `WorkflowError` | Workflow | Raised when a workflow encounters an error | | `ParserError` | Parser | Raised when a parser fails to parse the input data. Includes additional *Reason* | | `ResourceError` | Memory | Raised when an error occurs with processing agent memory | | `ResourceFatalError` | Memory | Extends ResourceError, raised for particularly severe errors that are likely to be fatal | | `EmitterError` | Emitter | Raised when a problem occurs in the emitter | | `BackendError` | Backend | Raised when a backend encounters an error | | `ChatModelError` | Backend | Extends BackendError, raised when a chat model fails to process input data | | `MessageError` | Backend | Raised when a message processing fails | ## Tools example ```py Python [expandable] theme={null} import asyncio from beeai_framework.tools import ToolError, tool async def main() -> None: @tool def dummy() -> None: """ A dummy tool. """ raise ToolError("Dummy error.") await dummy.run({}) if __name__ == "__main__": try: asyncio.run(main()) except ToolError as e: print("===CAUSE===") print(e.get_cause()) print("===EXPLAIN===") print(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { DynamicTool, ToolError } from "beeai-framework/tools/base"; import { FrameworkError } from "beeai-framework/errors"; import { z } from "zod"; const tool = new DynamicTool({ name: "dummy", description: "dummy", inputSchema: z.object({}), handler: async () => { throw new Error("Division has failed."); }, }); try { await tool.run({}); } catch (e) { const err = e as FrameworkError; console.log(e instanceof ToolError); // true console.log("===DUMP==="); console.log(err.dump()); console.log("===EXPLAIN==="); console.log(err.explain()); } ``` *** ## Usage ### Basic usage To use Framework error, add the following import: ```py Python [expandable] theme={null} from beeai_framework.errors import FrameworkError ``` Add any additional custom errors you need in your code to the import, for example ```py Python [expandable] theme={null} from beeai_framework.errors import FrameworkError, ChatModelError,ToolError ``` ### Creating custom errors If you wish to create additional errors, you can extend `FrameworkError` or any of the other errors above: ```py Python [expandable] theme={null} from beeai_framework.errors import FrameworkError class MyCustomError(FrameworkError): def __init__(self, message: str = "My custom error", *, cause: Exception | None = None, context: dict | None = None) -> None: super().__init__(message, is_fatal=True, is_retryable=False, cause=cause, context=context) ``` ### Wrapping existing errors You can wrap existing errors in a `FrameworkError`, for example: ```py Python [expandable] theme={null} inner_err: Exception = ValueError("Value error") error = FrameworkError.ensure(inner_err) raise(error) ``` ### Using properties and methods Framework error also has two additional properties which help with agent processing, though ultimately the code that catches the exception will determine the appropriate action. * **is\_retryable** : hints that the error is retryable. * **is\_fatal** : hints that the error is fatal. These can be accessed via: ```py Python [expandable] theme={null} err = FrameworkError("error") isfatal: bool = FrameworkError.is_fatal(err) isretryable: bool = FrameworkError.is_retryable(err) ``` This allows use of some useful functions within the error class. For example the `explain` static method will return a string that may be more useful for an LLM to interpret: ```py Python [expandable] theme={null} message: str = FrameworkError.ensure(error).explain() ``` See the source file [errors.py](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/errors.py) for additional methods. ## Examples Explore reference error implementations in Python Explore reference error implementations in TypeScript # Logger Source: https://framework.beeai.dev/modules/logger ## Overview Logger is a core component designed to record and track events, errors, and other important actions during application execution. It provides valuable insights into application behavior, performance, and potential issues, helping developers troubleshoot and monitor systems effectively. In the BeeAI framework, the `Logger` class is an abstraction built on top of Python's built-in logging module, offering enhanced capabilities specifically designed for AI agent workflows. Supported in Python and TypeScript. *** ## Key features * Custom log levels: Adds additional log levels like TRACE (below DEBUG) for fine-grained control * Customized formatting: Different formatting for regular logs vs. event messages * Agent interaction logging: Special handling for agent-generated events and communication * Error integration: Seamless integration with the framework's error handling system *** ## Getting started ### Basic usage To use the logger in your application: ```py Python [expandable] theme={null} from beeai_framework.logger import Logger # Configure logger with default log level from the BEEAI_LOG_LEVEL variable logger = Logger("app") # Log at different levels logger.trace("Trace!") logger.debug("Debug!") logger.info("Info!") logger.warning("Warning!") logger.error("Error!") logger.fatal("Fatal!") ``` ```ts TypeScript [expandable] theme={null} import { Logger, LoggerLevel } from "beeai-framework/logger/logger"; // Configure logger defaults Logger.defaults.pretty = true; // Pretty-print logs (default: false, can also be set via ENV: BEE_FRAMEWORK_LOG_PRETTY=true) Logger.defaults.level = LoggerLevel.TRACE; // Set log level to trace (default: TRACE, can also be set via ENV: BEE_FRAMEWORK_LOG_LEVEL=trace) Logger.defaults.name = undefined; // Optional name for logger (default: undefined) Logger.defaults.bindings = {}; // Optional bindings for structured logging (default: empty) // Create a child logger for your app const logger = Logger.root.child({ name: "app" }); // Log at different levels logger.trace("Trace!"); logger.debug("Debug!"); logger.info("Info!"); logger.warn("Warning!"); logger.error("Error!"); logger.fatal("Fatal!"); ``` ## Configuration The logger's behavior can be customized through environment variables: * `BEEAI_LOG_LEVEL`: Sets the default log level (defaults to "INFO") You can also set a specific level when initializing the logger. ### Custom log levels The logger adds a TRACE level below DEBUG for extremely detailed logging: ```py Python [expandable] theme={null} # Configure a logger with a specific level logger = Logger("app", level="TRACE") # Or use logging constants like logging.DEBUG # Log with the custom TRACE level logger.trace("This is a very low-level trace message") ``` *** ## Working with the logger ### Formatting The logger uses a custom formatter that distinguishes between regular log messages and event messages: * Regular logs: `{timestamp} | {level} | {module}:{function}:{line} - {message}` * Event messages: `{timestamp} | {level} | {message}` ### Icons and formatting When logging agent interactions, the logger automatically adds visual icons: * User messages: 👤 * Agent messages: 🤖 This makes logs easier to read and understand when reviewing conversational agent flows. ## Error handling The logger integrates with BeeAI framework's error handling system through the `LoggerError` class. *** ## Usage with agents The Logger seamlessly integrates with agents in the framework. Below is an example that demonstrates how logging can be used in conjunction with agents and event emitters. ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.agents.react import ReActAgent from beeai_framework.backend import ChatModel from beeai_framework.errors import FrameworkError from beeai_framework.logger import Logger from beeai_framework.memory import UnconstrainedMemory async def main() -> None: logger = Logger("app", level="TRACE") agent = ReActAgent(llm=ChatModel.from_name("ollama:llama3.1"), tools=[], memory=UnconstrainedMemory()) output = await agent.run("Hello!").observe( lambda emitter: emitter.on( "update", lambda data, event: logger.info(f"Event {event.path} triggered by {type(event.creator).__name__}") ) ) logger.info(f"Agent 🤖 : {output.last_message.text}") if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { ReActAgent } from "beeai-framework/agents/react/agent"; import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory"; import { Logger } from "beeai-framework/logger/logger"; import { Emitter } from "beeai-framework/emitter/emitter"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; // Set up logging Logger.defaults.pretty = true; const logger = Logger.root.child({ level: "trace", name: "app", }); // Log events emitted during agent execution Emitter.root.match("*.*", (data, event) => { const logLevel = event.path.includes(".run.") ? "trace" : "info"; logger[logLevel](`Event '${event.path}' triggered by '${event.creator.constructor.name}'`); }); // Create and run an agent const agent = new ReActAgent({ llm: new OllamaChatModel("granite4:micro"), memory: new UnconstrainedMemory(), tools: [], }); const response = await agent.run({ prompt: "Hello!" }); logger.info(response.result.text); ``` *** ## Examples COMING SOON: Explore reference logger implementations in Python COMING SOON: Explore reference logger implementations in TypeScript # Memory Source: https://framework.beeai.dev/modules/memory ## Overview Memory in the context of an agent refers to the system's capability to store, recall, and utilize information from past interactions. This enables the agent to maintain context over time, improve its responses based on previous exchanges, and provide a more personalized experience. BeeAI framework provides several memory implementations: | Type | Description | | ----------------------------------------------- | ------------------------------------------------------- | | [**UnconstrainedMemory**](#unconstrainedmemory) | Unlimited storage for all messages | | [**SlidingMemory**](#slidingmemory) | Keeps only the most recent k entries | | [**TokenMemory**](#tokenmemory) | Manages token usage to stay within model context limits | | [**SummarizeMemory**](#summarizememory) | Maintains a single summarization of the conversation | Supported in Python and TypeScript. *** ## Core concepts ### Messages Messages are the fundamental units stored in memory, representing interactions between users and agents: * Each message has a role (USER, ASSISTANT, SYSTEM) * Messages contain text content * Messages can be added, retrieved, and processed ### Memory types Different memory strategies are available depending on your requirements: * **Unconstrained** - Store unlimited messages * **Sliding Window** - Keep only the most recent N messages * **Token-based** - Manage a token budget to stay within model context limits * **Summarization** - Compress previous interactions into summaries ### Integration points Memory components integrate with other parts of the framework: * LLMs use memory to maintain conversation context * Agents access memory to process and respond to interactions * Workflows can share memory between different processing steps *** ## Basic usage ### Capabilities showcase ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.backend import AssistantMessage, SystemMessage, UserMessage from beeai_framework.errors import FrameworkError from beeai_framework.memory import UnconstrainedMemory async def main() -> None: memory = UnconstrainedMemory() # Single Message await memory.add(SystemMessage("You are a helpful assistant")) # Multiple Messages await memory.add_many([UserMessage("What can you do?"), AssistantMessage("Everything!")]) print(memory.is_empty()) # false for message in memory.messages: # prints the text of all messages print(message.text) print(memory.as_read_only()) # returns a new read only instance memory.reset() # removes all messages if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory"; import { AssistantMessage, SystemMessage, UserMessage } from "beeai-framework/backend/message"; const memory = new UnconstrainedMemory(); // Single message await memory.add(new SystemMessage(`You are a helpful assistant.`)); // Multiple messages await memory.addMany([new UserMessage(`What can you do?`), new AssistantMessage(`Everything!`)]); console.info(memory.isEmpty()); // false console.info(memory.messages); // prints all saved messages console.info(memory.asReadOnly()); // returns a NEW read only instance memory.reset(); // removes all messages ``` ### Usage with LLMs ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.adapters.ollama import OllamaChatModel from beeai_framework.backend import AssistantMessage, SystemMessage, UserMessage from beeai_framework.errors import FrameworkError from beeai_framework.memory import UnconstrainedMemory async def main() -> None: memory = UnconstrainedMemory() await memory.add_many( [ SystemMessage("Always respond very concisely."), UserMessage("Give me the first 5 prime numbers."), ] ) llm = OllamaChatModel("llama3.1") response = await llm.run(memory.messages) await memory.add(AssistantMessage(response.get_text_content())) print("Conversation history") for message in memory.messages: print(f"{message.role}: {message.text}") if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory"; import { Message } from "beeai-framework/backend/message"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; const memory = new UnconstrainedMemory(); await memory.addMany([ Message.of({ role: "system", text: `Always respond very concisely.`, }), Message.of({ role: "user", text: `Give me first 5 prime numbers.` }), ]); // Generate response const llm = new OllamaChatModel("granite4:micro"); const response = await llm.create({ messages: memory.messages }); await memory.add(Message.of({ role: "assistant", text: response.getTextContent() })); console.log(`Conversation history`); for (const message of memory) { console.log(`${message.role}: ${message.text}`); } ``` Memory for non-chat LLMs works exactly the same way. ### Usage with agents ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.agents.react import ReActAgent from beeai_framework.backend import AssistantMessage, ChatModel, UserMessage from beeai_framework.errors import FrameworkError from beeai_framework.memory import UnconstrainedMemory # Initialize the memory and LLM memory = UnconstrainedMemory() def create_agent() -> ReActAgent: llm = ChatModel.from_name("ollama:granite4:micro") # Initialize the agent agent = ReActAgent(llm=llm, memory=memory, tools=[]) return agent async def main() -> None: # Create user message user_input = "Hello world!" user_message = UserMessage(user_input) # Await adding user message to memory await memory.add(user_message) print("Added user message to memory") # Create agent agent = create_agent() response = await agent.run( user_input, max_retries_per_step=3, total_max_retries=10, max_iterations=20, ) print(f"Received response: {response}") # Create and store assistant's response assistant_message = AssistantMessage(response.last_message.text) # Await adding assistant message to memory await memory.add(assistant_message) print("Added assistant message to memory") # Print results print(f"\nMessages in memory: {len(agent.memory.messages)}") if len(agent.memory.messages) >= 1: user_msg = agent.memory.messages[0] print(f"User: {user_msg.text}") if len(agent.memory.messages) >= 2: agent_msg = agent.memory.messages[1] print(f"Agent: {agent_msg.text}") else: print("No agent message found in memory") if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory"; import { ReActAgent } from "beeai-framework/agents/react/agent"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; const agent = new ReActAgent({ memory: new UnconstrainedMemory(), llm: new OllamaChatModel("granite4:micro"), tools: [], }); await agent.run({ prompt: "Hello world!" }); console.info(agent.memory.messages.length); // 2 const userMessage = agent.memory.messages[0]; console.info(`User: ${userMessage.text}`); // User: Hello world! const agentMessage = agent.memory.messages[1]; console.info(`Agent: ${agentMessage.text}`); // Agent: Hello! It's nice to chat with you. ``` If your memory already contains the user message, run the agent with `prompt: null`. ReAct Agent internally uses `TokenMemory` to store intermediate steps for a given run. *** ## Memory types The framework provides multiple out-of-the-box memory implementations for different use cases. ### UnconstrainedMemory Unlimited in size, stores all messages without constraints. ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.backend import UserMessage from beeai_framework.errors import FrameworkError from beeai_framework.memory import UnconstrainedMemory async def main() -> None: # Create memory instance memory = UnconstrainedMemory() # Add a message await memory.add(UserMessage("Hello world!")) # Print results print(f"Is Empty: {memory.is_empty()}") # Should print: False print(f"Message Count: {len(memory.messages)}") # Should print: 1 print("\nMessages:") for msg in memory.messages: print(f"{msg.role}: {msg.text}") if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory"; import { Message } from "beeai-framework/backend/message"; const memory = new UnconstrainedMemory(); await memory.add( Message.of({ role: "user", text: `Hello world!`, }), ); console.info(memory.isEmpty()); // false console.log(memory.messages.length); // 1 console.log(memory.messages); ``` ### SlidingMemory Keeps last `k` entries in the memory. The oldest ones are deleted (unless specified otherwise). ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.backend import AssistantMessage, SystemMessage, UserMessage from beeai_framework.errors import FrameworkError from beeai_framework.memory import SlidingMemory, SlidingMemoryConfig async def main() -> None: # Create sliding memory with size 3 memory = SlidingMemory( SlidingMemoryConfig( size=3, handlers={"removal_selector": lambda messages: messages[0]}, # Remove oldest message ) ) # Add messages await memory.add(SystemMessage("You are a helpful assistant.")) await memory.add(UserMessage("What is Python?")) await memory.add(AssistantMessage("Python is a programming language.")) # Adding a fourth message should trigger sliding window await memory.add(UserMessage("What about JavaScript?")) # Print results print(f"Messages in memory: {len(memory.messages)}") # Should print 3 for msg in memory.messages: print(f"{msg.role}: {msg.text}") if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { SlidingMemory } from "beeai-framework/memory/slidingMemory"; import { Message } from "beeai-framework/backend/message"; const memory = new SlidingMemory({ size: 3, // (required) number of messages that can be in the memory at a single moment handlers: { // optional // we select a first non-system message (default behaviour is to select the oldest one) removalSelector: (messages) => messages.find((msg) => msg.role !== "system")!, }, }); await memory.add(Message.of({ role: "system", text: "You are a guide through France." })); await memory.add(Message.of({ role: "user", text: "What is the capital?" })); await memory.add(Message.of({ role: "assistant", text: "Paris" })); await memory.add(Message.of({ role: "user", text: "What language is spoken there?" })); // removes the first user's message await memory.add(Message.of({ role: "assistant", text: "French" })); // removes the first assistant's message console.info(memory.isEmpty()); // false console.log(memory.messages.length); // 3 console.log(memory.messages); ``` ### TokenMemory Ensures that the token sum of all messages is below the given threshold. If overflow occurs, the oldest message will be removed. ```py Python [expandable] theme={null} import asyncio import math import sys import traceback from beeai_framework.adapters.ollama import OllamaChatModel from beeai_framework.backend import Role, SystemMessage, UserMessage from beeai_framework.errors import FrameworkError from beeai_framework.memory import TokenMemory # Initialize the LLM llm = OllamaChatModel() # Initialize TokenMemory with handlers memory = TokenMemory( llm=llm, max_tokens=None, # Will be inferred from LLM capacity_threshold=0.75, sync_threshold=0.25, handlers={ "removal_selector": lambda messages: next((msg for msg in messages if msg.role != Role.SYSTEM), messages[0]), "estimate": lambda msg: math.ceil((len(msg.role) + len(msg.text)) / 4), }, ) async def main() -> None: # Add system message system_message = SystemMessage("You are a helpful assistant.") await memory.add(system_message) print(f"Added system message (hash: {hash(system_message)})") # Add user message user_message = UserMessage("Hello world!") await memory.add(user_message) print(f"Added user message (hash: {hash(user_message)})") # Check initial memory state print("\nInitial state:") print(f"Is Dirty: {memory.is_dirty}") print(f"Tokens Used: {memory.tokens_used}") # Sync token counts await memory.sync() print("\nAfter sync:") print(f"Is Dirty: {memory.is_dirty}") print(f"Tokens Used: {memory.tokens_used}") # Print all messages print("\nMessages in memory:") for msg in memory.messages: print(f"{msg.role}: {msg.text} (hash: {hash(msg)})") if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { TokenMemory } from "beeai-framework/memory/tokenMemory"; import { Message } from "beeai-framework/backend/message"; const memory = new TokenMemory({ maxTokens: undefined, // optional (default is 128k), capacityThreshold: 0.75, // maxTokens*capacityThreshold = threshold where we start removing old messages syncThreshold: 0.25, // maxTokens*syncThreshold = threshold where we start to use a real tokenization endpoint instead of guessing the number of tokens handlers: { // optional way to define which message should be deleted (default is the oldest one) removalSelector: (messages) => messages.find((msg) => msg.role !== "system")!, // optional way to estimate the number of tokens in a message before we use the actual tokenize endpoint (number of tokens < maxTokens*syncThreshold) estimate: (msg) => Math.ceil((msg.role.length + msg.text.length) / 4), }, }); await memory.add(Message.of({ role: "system", text: "You are a helpful assistant." })); await memory.add(Message.of({ role: "user", text: "Hello world!" })); console.info(memory.isDirty); // is the consumed token count estimated or retrieved via the tokenize endpoint? console.log(memory.tokensUsed); // number of used tokens console.log(memory.stats()); // prints statistics await memory.sync(); // calculates real token usage for all messages marked as "dirty" ``` ### SummarizeMemory Only a single summarization of the conversation is preserved. Summarization is updated with every new message. ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.backend import AssistantMessage, ChatModel, SystemMessage, UserMessage from beeai_framework.errors import FrameworkError from beeai_framework.memory import SummarizeMemory async def main() -> None: # Initialize the LLM with parameters llm = ChatModel.from_name( "ollama:granite4:micro", # ChatModelParameters(temperature=0), ) # Create summarize memory instance memory = SummarizeMemory(llm) # Add messages await memory.add_many( [ SystemMessage("You are a guide through France."), UserMessage("What is the capital?"), AssistantMessage("Paris"), UserMessage("What language is spoken there?"), ] ) # Print results print(f"Is Empty: {memory.is_empty()}") print(f"Message Count: {len(memory.messages)}") if memory.messages: print(f"Summary: {memory.messages[0].get_texts()[0].text}") if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { Message } from "beeai-framework/backend/message"; import { SummarizeMemory } from "beeai-framework/memory/summarizeMemory"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; const memory = new SummarizeMemory({ llm: new OllamaChatModel("granite4:micro"), }); await memory.addMany([ Message.of({ role: "system", text: "You are a guide through France." }), Message.of({ role: "user", text: "What is the capital?" }), Message.of({ role: "assistant", text: "Paris" }), Message.of({ role: "user", text: "What language is spoken there?" }), ]); console.info(memory.isEmpty()); // false console.log(memory.messages.length); // 1 console.log(memory.messages[0].text); // The capital city of France is Paris, ... ``` *** ## Creating custom memory To create your memory implementation, you must implement the `BaseMemory` class. ```py Python [expandable] theme={null} from typing import Any from beeai_framework.backend import AnyMessage from beeai_framework.memory import BaseMemory class MyMemory(BaseMemory): @property def messages(self) -> list[AnyMessage]: raise NotImplementedError("Method not yet implemented.") async def add(self, message: AnyMessage, index: int | None = None) -> None: raise NotImplementedError("Method not yet implemented.") async def delete(self, message: AnyMessage) -> bool: raise NotImplementedError("Method not yet implemented.") def reset(self) -> None: raise NotImplementedError("Method not yet implemented.") def create_snapshot(self) -> Any: raise NotImplementedError("Method not yet implemented.") def load_snapshot(self, state: Any) -> None: raise NotImplementedError("Method not yet implemented.") ``` ```ts TypeScript [expandable] theme={null} import { BaseMemory } from "beeai-framework/memory/base"; import { Message } from "beeai-framework/backend/message"; import { NotImplementedError } from "beeai-framework/errors"; export class MyMemory extends BaseMemory { get messages(): readonly Message[] { throw new NotImplementedError("Method not implemented."); } add(message: Message, index?: number): Promise { throw new NotImplementedError("Method not implemented."); } delete(message: Message): Promise { throw new NotImplementedError("Method not implemented."); } reset(): void { throw new NotImplementedError("Method not implemented."); } createSnapshot(): unknown { throw new NotImplementedError("Method not implemented."); } loadSnapshot(state: ReturnType): void { throw new NotImplementedError("Method not implemented."); } } ``` The simplest implementation is `UnconstrainedMemory`. *** ## Examples Explore reference memory implementations in Python Explore reference memory implementations in TypeScript # Middleware Source: https://framework.beeai.dev/modules/middleware ## Overview Middleware in the BeeAI Framework is code that runs "in the middle" of an execution lifecycle—intercepting the flow between when a component (like an Agent, Tool, or Model) starts and when it finishes. As these components execute, they emit events at key moments, such as starting a task, calling a tool, or completing a response . Middleware hooks into these events to inject behaviors like logging, filtering, or safety checks—all without modifying the component's core logic. This modular approach allows you to apply consistent policies across your entire system. You can use built-in tools like `GlobalTrajectoryMiddleware` for immediate debugging, or write custom middleware to handle complex needs like blocking unsafe content, enforcing rate limits, or managing authentication. Note on Terminology: In this framework, **Middleware** refers to the classic software design pattern (pipeline interceptors) that runs between execution steps. This is distinct from the industry term "Agentic Middleware," which typically refers to entire orchestration platforms. ## Built-in Middleware The following section showcases built-in middleware that you can start using right away. ### Global Trajectory The fastest way to understand your agent's execution flow is by using the `GlobalTrajectoryMiddleware`. It captures all events, including deeply-nested ones, and prints them to the console, using indentation to visualize the call stack . **Example** ```py Python [expandable] theme={null} from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware agent = RequirementAgent( llm="ollama:granite3.3", tools=[OpenMeteoTool()] ) await agent.run("What's the current weather in Miami?") .middleware(GlobalTrajectoryMiddleware()) ``` You can customize the output by passing parameters to the constructor: | Parameter | Description | | ------------------ | -------------------------------------------------------------------------------------------------------- | | `target` | Specify a file or stream to which to write the trajectory (pass `False` to disable). | | `included` | List of classes to include in the trajectory. | | `excluded` | List of classes to exclude from the trajectory. | | `pretty` | Use pretty formatting for the trajectory. | | `prefix_by_type` | Customize how instances of individual classes should be printed. | | `exclude_none` | Exclude `None` values from the printing. | | `enabled` | Enable/Disable the logging. | | `match_nested` | Whether to observe trajectories of nested run contexts. | | `emitter_priority` | Setting higher priority may result in capturing events without any modifications from other middlewares. | **Example** ```py Python [expandable] theme={null} from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools.tool import Tool from beeai_framework.backend.chat import ChatModel from beeai_framework.tools.weather.openmeteo import OpenMeteoTool from beeai_framework.agents.base import BaseAgent # Log only tool calls GlobalTrajectoryMiddleware(included=[Tool]) # Log only tool calls except OpenMeteoTool GlobalTrajectoryMiddleware(included=[Tool], excluded=[OpenMeteoTool]) # Log only ChatModel events GlobalTrajectoryMiddleware(included=[ChatModel]) # Use a Bee emoji for agents GlobalTrajectoryMiddleware(prefix_by_type={BaseAgent: "🐝 "}) ``` You can listen to events emitted throughout the execution to build your custom trajectory. ### Tool Call Streaming This middleware handles streaming tool calls in a `ChatModel`. It observes stream updates from the [Chat Model](/modules/backend#chat-model) and parses tool calls on demand so that they can be consumed immediately. It works even without streaming enabled, in which case it emits the `update` event at the end of the execution.. **Example** ```py Python [expandable] theme={null} import asyncio from beeai_framework.backend import ChatModel, UserMessage from beeai_framework.emitter import EventMeta from beeai_framework.middleware.stream_tool_call import StreamToolCallMiddleware, StreamToolCallMiddlewareUpdateEvent from beeai_framework.tools.weather import OpenMeteoTool async def main() -> None: llm = ChatModel.from_name("ollama:granite4:micro") weather_tool = OpenMeteoTool() middleware = StreamToolCallMiddleware( weather_tool, key="location_name", # name is taken from the OpenMeteoToolInput schema match_nested=False, # we are applying the middleware to the model directly force_streaming=True, # we want to let middleware enable streaming on the model ) @middleware.emitter.on("update") def log_thoughts(event: StreamToolCallMiddlewareUpdateEvent, meta: EventMeta) -> None: print( "Received update", event.delta, event.output_structured ) # event.delta contains an update of the 'location_name' field response = await llm.run([UserMessage("What's the current weather in New York?")], tools=[weather_tool]).middleware( middleware ) print(response.get_tool_calls()[0].args) if __name__ == "__main__": asyncio.run(main()) ``` The following parameters can be passed to the constructor: | Parameter | Description | | ----------------- | -------------------------------------------------------------------------------- | | `target` | The tool that we are waiting for to be called. | | `key` | Refers to the name of the attribute in the tool's schema that we want to stream. | | `match_nested` | Whether the middleware should be applied only to the top level. | | `force_streaming` | Sets the stream flag on the `ChatModel`. | ## Core Primatives The BeeAI Framework middleware is built on an underlying system of primitives, which are described in this section. Understanding these primitives is helpful for building complex middleware. ### Events An event refers to an action initiated by a component. It carries the details of what just happened within the system. Every event has three key properties: * Name: A string identifier (e.g., `start`, `success`, `error`, or custom names like `fetch_data`). * Data payload: The content of the event, typically astructured as a Pydantic model. * Metadata: Information about the context where the event was fired. You process these events using callbacks that follow this structure: ```py Python [expandable] theme={null} from beeai_framework.backend.chat import ChatModelStartEvent from beeai_framework.emitter import EventMeta async def handler(data: ChatModelStartEvent, meta: EventMeta) -> None: print(f"Received event {meta.name} ({meta.path}) at {meta.created_at}") print("-> created by", type(meta.creator)) print("-> data payload", meta.model_dump()) print("-> context", meta.context) print("-> trace", meta.trace) ``` ### Emitter The **Emitter** is the core component that lets you send and watch for events. While it is typically attached to a specific class, you can also use it on its own. An emitter instance is typically the child of a root emitter to which all events are propagated. Emitters can be nested (one can be a child of another), hence they internally create a tree hierarchy. Every emitter instance has the following **properties**: * `namespace` in which the emitter operates (eg: `agents.requirement`, `tool.open_meteo`, ...). * `creator` class which the given emitter belongs to. * `context` (dictionary which is attached to all events emitted via the given emitter). * `trace` metadata (such as current `id`, `run_id` and `parent_id`) It also gives you the following **methods** for managing listeners: * `on` for registering a new event listener * The method takes `matcher` (event name, callback, regex), `callback` (sync/async function), and `options` (priority, etc.) * The method can be used as a decorator or as a standalone function * `off` for deregistering an event listener * `pipe` for propagating all captured events to another emitter * `child` for creating a child emitter The event's `path` attribute is created by concatenating `namespace` with an event name (eg: `backend.chat.ollama.start`). **Example** The following example depicts a minimal application that does the following: 1. defines a data object for the `fetch_data` event, 2. creates an emitter from the root one, 3. registers a callback listening to the `fetch_data` event, which modifies its content, 4. fires the `fetch_data` event, 5. logs the modified event's data. ```py Python [expandable] theme={null} from pydantic import BaseModel from beeai_framework.emitter import EventMeta, Emitter # Define a data model for the event class FetchDataEvent(BaseModel): url: str # Create an emitter and pass events emitter = Emitter.root().child( namespace=["app"], events={"fetch_data": FetchDataEvent} # optional ) # Listen to an event @emitter.on("fetch_data") def handle_event(data: FetchDataEvent, meta: EventMeta) -> None: print(f"Retrieved event {meta}") data.url = "https://mywebsite.com" # Create and emit the event data = FetchDataEvent(url="https://example.com") await emitter.emit("fetch_data", data) print(data.url) # "https://mywebsite.com" # Deregister a callback (optional) emitter.off(callback=handle_event) ``` The `emitter.on` can be used directly and not just as a decorator. Example: `emitter.on("fetch_data", callback)`. If you name your function as either `handle_{event_name}` or `on_{event_name}`, then you don't need to provide the event name as a parameter, as it gets inferred automatically. ### Run (Context) The `Run` class **acts as a wrapper of the target implementation** with its **own lifecycle** (an emitter with a set of events) and **context** (data that gets propagated to all events). The `RunContext` class is a container that stores information about the current execution context. These abstractions allow you to: * modify `input` to the given target (listen to a `start` event and modify the content of the `input` property), * modify `output` from the given target (listen to a `success` event and modify the content of the `output` property), * stop the run early (listen to a `start` event and set the `output` property to a non-`None` value), * propagate `context` (dictionary) to any component of your system, * cancel the execution in an arbitrary place, * gain observability into runs via structured events (for logging, tracing, and debugging). **The Run and Run Context gets created** when a `run` method gets called on a framework class that can be executed (eg, `ChatModel`, `Agent`, ...). The run object has the following **methods**: * The `on` method allows registering a callback to its emitter. * The `middleware` for registering middleware (a function that takes `RunContext` as a first parameter or a class with a `bind` method that takes the `RunContext` as a first parameter). * The `context` allows data to be set for a given execution. That data will then be propagated as metadata in every event that gets emitted. The target implementation (handler) becomes part of the shared context (`RunContext`), which internally forms a hierarchical tree structure that shares the same context. In simpler terms, when you call one runnable (e.g., `ChatModel`) from within another runnable (e.g., `Agent`), the inner call (`ChatModel`) is attached to the context of the outer one (`Agent`). The current execution context can be retrieved anytime by calling `RunContext.get()`. ### Runnable The `Runnable[R]` class unifies common objects that can be executed and observed. It is an abstract class with the following traits: * It has an abstract `run` method that executes the class and returns a `Run[R]` (`R` is bound to the `RunnableOutput`). * It has an abstract `emitter` getter. * It has a `middlewares` getter that lists the existing middlewares. **Invoking a Runnable** Every runnable takes a list of messages as its first (positional) parameter, followed by the following optional keyword arguments (`RunnableOptions`): * `signal` (an instance of `AbortSignal`) — allows aborting the execution. * `context` (a dictionary) — used to propagate additional data. You can also pass extra arguments that may or may not be processed by the given handler. The `RunnableOutput` has the following properties: * `output`: a list of messages (can be empty) * `context`: a dictionary that can store additional data * `last_message` (getter): returns the last message if it exists, or creates an empty `AssistantMessage` otherwise **Creating a custom Runnable** ```py Python [expandable] theme={null} import asyncio from functools import cached_property from typing import Unpack from beeai_framework.backend import AnyMessage, AssistantMessage, UserMessage from beeai_framework.context import RunContext from beeai_framework.emitter import Emitter from beeai_framework.runnable import Runnable, RunnableOptions, RunnableOutput, runnable_entry class GreetingRunnable(Runnable[RunnableOutput]): @runnable_entry async def run(self, input: list[AnyMessage], /, **kwargs: Unpack[RunnableOptions]) -> RunnableOutput: # retrieves the current run contex run = RunContext.get() response = f"Hello, {run.context.get('name', 'stranger')}!" # sends an emit so that someone can react to it (optional) await run.emitter.emit("before_send", response) return RunnableOutput(output=[AssistantMessage(response)]) @cached_property def emitter(self) -> Emitter: return Emitter.root().child(namespace=["echo"]) async def main() -> None: echo = GreetingRunnable() response = await echo.run([UserMessage("Hello!")]).context({"name": "Alex"}) print(response.last_message.text) if __name__ == "__main__": asyncio.run(main()) ``` ## Event Handling Building robust agents requires precise control over the execution lifecycle. You need the ability to not only observe your agent's behavior but also intercept and modify it at specific points. The following sections covers the mechanics of the BeeAI Framework **event system** and will enable you to manage: * **Scopes**: Deciding whether to listen globally, per instance, or for a single run. * \***Config**: Controlling listener priority, persistence, and blocking behavior. * **Lifecycle**: Undestanding the exact sequence events that occur during execution. * **Debugging**: Inspecting raw event streams to see exactly what your agent is doing. * **Piping**: Linking emitters together via piping to create unified event streams. ### Scopes Events can be observed at three different levels. **1. Global Level** Every emitter provided by the out-of-the-box modules is a child of the root emitter. This means you can listen to **all events** directly from the root emitter. ```py Python [expandable] theme={null} from beeai_framework.emitter import Emitter, EventMeta from typing import Any def log_all_events(data: Any, meta: EventMeta): print(f"Received event ({meta.id}) with name {meta.name} and path {meta.path}. \ The event was created by {type(meta.creator)} and is a type of {type(data)}.") root = Emitter.root() root.on("*.*", log_all_events) ``` Listeners that are bound "closer" to the source are executed earlier. For those that reside at the same level, the order can be altered by setting a `priority` value which is part of the `EmitterOptions` class. A higher `priority` value means the listener will be executed earlier. The default priority is `0`. **2. Instance Level** You can also listen to events emitted by a **specific instance** of a class. ```py Python [expandable] theme={null} from beeai_framework.backend.chat import ChatModel, ChatModelStartEvent from beeai_framework.backend.message import UserMessage from beeai_framework.emitter import EventMeta from typing import Any def change_model_temperature(data: ChatModelStartEvent, meta: EventMeta): print(f"The chat model triggered a start event. Changing a temperature.") data.input.temperature = 0.5 model = ChatModel.from_name("ollama:granite3.3") model.emitter.on("start", change_model_temperature) await model.run([UserMessage("Hello!")]) ``` This registers a callback to the class's emitter so that all events in a given class will be captured. **3. Run (Invocation) Level** Sometimes you may want to listen to events emitted by **a single run** of a class. ```py Python [expandable] theme={null} from beeai_framework.backend.chat import ChatModel, ChatModelStartEvent from beeai_framework.backend.message import UserMessage from beeai_framework.emitter import EventMeta from typing import Any model = ChatModel.from_name("ollama:granite4") def change_model_temperature(data: ChatModelStartEvent, meta: EventMeta): print(f"The chat model triggered a start event. Changing a temperature.") data.input.temperature = 0.5 await model.run([UserMessage("Hello!")]) .on("start", change_model_temperature) ``` Here, the callback is registered on the **run instance** (created by the `run` method). The run’s emitter is a child of the class emitter, allowing you to modify behavior for a single invocation without affecting others. ### Config When working with multiple callbacks, you may need to control execution order, or ensure that some run exclusively. You can do this using the optional `options` argument of type `EmitterOptions`. **Example** ```py Python [expandable] theme={null} from beeai_framework.emitter import EmitterOptions, EventMeta from beeai_framework.backend.chat import ChatModel, ChatModelNewTokenEvent # Creates a chat model instance model = ChatModel.from_name("ollama:granite3.3", stream=True) # Defines a callback that will be executed when a new token is emitted by the model def cb(data: ChatModelNewTokenEvent, meta: EventMeta): print(f"[{meta.id}]: Received chunk", data.value.get_text_content()) # Creates a reference to the emitter e = model.emitter # will be executed only once and then gets unregistered (default is False) e.on("new_token", cb, EmitterOptions(once=True)) # will not be deleted (default is False) e.on("new_token", cb, EmitterOptions(persistent=True)) # will be executed before those with a lower priority (default is 0), priority can also be negative e.on("new_token", cb, EmitterOptions(priority=1)) # runs before every other callback with the same priority (default is False) e.on("new_token", cb, EmitterOptions(is_blocking=True)) # match events that are emitted by the same type of class but executed within a target (e.g., calling agent.run(...) inside another agent.run(...)) e.on("new_token", cb, EmitterOptions(match_nested=True)) ``` **Nested events** Based on the value of the `matcher` parameter (the one that is used to match the event), the framework decides whether to include/exclude nested events (events created from children emitters or from piping). The default value of the `match_nested` depends on the `matcher` value. Note that the value can be set directly as shown in the example above. | Matcher Type | Default `match_nested` | | ---------------------------------- | ---------------------- | | String without `.` (event name) | `False` | | String with `.` (event path) | `True` | | `"*"` (match all top-level events) | `False` | | `"*.*"` (match all events) | `True` | | Regex | `True` | | Function | `False` | If two events have the same priority, they are executed in the order they were added. ### Lifecycle When a framework component is executed, it creates a run context, which wraps the target handler and allows you to modify its input and output (Learn more in the [Run (Context)](#run-context) section). Once a `Run` instance is executed (i.e., **awaited**), its lifecycle proceeds through the following steps: 1. The `start` event is emitted. 2. The target implementation is executed. 3. Depending on the outcome, either a `success` or `error` event is emitted. 4. Finally, the `finish` event is emitted. The appropriate events are depicted in the following table: | Event | Data Type | Description | | :-------- | :----------------------- | :------------------------------- | | `start` | `RunContextStartEvent` | Triggered when the run starts. | | `success` | `RunContextSuccessEvent` | Triggered when the run succeeds. | | `error` | `FrameworkError` | Triggered when an error occurs. | | `finish` | `RunContextFinishEvent` | Triggered when the run finishes. | Below is an example showing how to listen to these events: ```py Python [expandable] theme={null} import asyncio from beeai_framework.backend import AnyMessage, AssistantMessage, ChatModelOutput from beeai_framework.backend.chat import ChatModel, ChatModelNewTokenEvent from beeai_framework.backend.message import UserMessage from beeai_framework.context import RunContextStartEvent, RunContextFinishEvent, RunContext from beeai_framework.emitter import EventMeta from beeai_framework.emitter.utils import create_internal_event_matcher model = ChatModel.from_name("ollama:granite3.3") def change_temperature(data: RunContextStartEvent, meta: EventMeta) -> None: """Modify the input of the model.run()""" print("debug: changing temperature to 0.5.\n") data.input["temperature"] = 0.5 # data.input contains all positional/keyword arguments of the called function def premature_stop(data: RunContextStartEvent, meta: EventMeta) -> None: """Checks whether the input contains malicious text. If so, we prevent the ChatModel from executing and immediately return a custom response. """ print("debug: Checking for a malicious input") messages: list[AnyMessage] = data.input["input"] # first parameter for message in messages: if "bomb" in message.text: print("debug: Premature stop detected.") data.output = ChatModelOutput(output=[AssistantMessage("Cannot answer that.")]) break def validate_new_token(data: ChatModelNewTokenEvent, meta: EventMeta) -> None: """Check if the stream contains a malicious word. If so, we abort the run.""" run = RunContext.get() if "fuse" in data.value.get_text_content(): print(f"Aborting run for user with ID {run.context.get('user_id')}") run._controller.abort("Policy violation. Aborting the run.") response = await ( model.run([UserMessage("How to make a bomb?")]) .on("new_token", validate_new_token) .on(create_internal_event_matcher("start", model), change_temperature) .on(create_internal_event_matcher("start", model), premature_stop) .context({"user_id": 123}) ) print("Agent:", response.get_text_content()) ``` In this example, `create_internal_event_matcher` ensures we correctly match the event. You can retrieve the current run context at any time within a callback using `RunContext.get()`. ### Debugging While the [Global Trajectory](#global-trajectory) middleware is excellent for visualizing the structural hierarchy of a run, sometimes you need to inspect the raw stream of events as they happen. To do this quickly without setting up a full middleware class, you can register a wildcard listener (`*.*`) directly on your run. This captures every single event emitted during that specific execution. ```py Python [expandable] theme={null} agent = RequirementAgent("ollama:granite3.3", tools=[OpenMeteoTool()]) response = await agent .run("What's the current weather in Miami?") .on("*.*", lambda data, meta: print(meta.path, 'by', type(meta.creator))) ``` ### Piping In some cases, one might want to propagate all events from one emitter to another (for instance when creating a child emitter). ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.emitter import Emitter from beeai_framework.errors import FrameworkError async def main() -> None: first: Emitter = Emitter(namespace=["app"]) first.on( "*.*", lambda data, event: print( f"'first' has retrieved the following event '{event.path}', isDirect: {event.source == first}" ), ) second: Emitter = Emitter(namespace=["app", "llm"]) second.on( "*.*", lambda data, event: print( f"'second' has retrieved the following event '{event.path}', isDirect: {event.source == second}" ), ) # Propagate all events from the 'second' emitter to the 'first' emitter unpipe = second.pipe(first) await first.emit("a", {}) await second.emit("b", {}) print("Unpipe") unpipe() await first.emit("c", {}) await second.emit("d", {}) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { Emitter, EventMeta } from "beeai-framework/emitter/emitter"; const first = new Emitter({ namespace: ["app"], }); first.match("*.*", (data: unknown, event: EventMeta) => { console.log( `'first' has retrieved the following event ${event.path}, isDirect: ${event.source === first}`, ); }); const second = new Emitter({ namespace: ["app", "llm"], }); second.match("*.*", (data: unknown, event: EventMeta) => { console.log( `'second' has retrieved the following event '${event.path}', isDirect: ${event.source === second}`, ); }); // Propagate all events from the 'second' emitter to the 'first' emitter const unpipe = second.pipe(first); await first.emit("a", {}); await second.emit("b", {}); console.log("Unpipe"); unpipe(); await first.emit("c", {}); await second.emit("d", {}); ``` ## Creating Custom Middleware While you can register individual callbacks to handle specific events, this approach can become cluttered if you have complex logic. To make your event handling reusable and modular, the BeeAI framework allows you to group listeners into a class called **Middleware.** ### **When to use Middleware vs. Callbacks** * **Use Callbacks (`.on` / `.match`)**: For simple, one-off logic, such as logging a specific event or debugging a single run. * **Use Middleware**: When the logic is complex, multi-step or needs to be reused across different parts of your application. ### **The Middleware Protocol** A middleware component is defined by how it interacts with the RunContext. It can be structured in two ways: 1. **A Function**: A simple function that accepts `RunContext` as its first parameter. 2. **A Class**: A class that implements a bind method, which accepts `RunContext` as its first parameter. The `RunContex` provides access to the emitter, the instance being run, and the shared memory for that specific execution. ### **Example: Intercepting and Overriding** A common use case for middleware is intercepting a request before the target component executes to modify the input or provide a mock response. The following example demonstrates a middleware that intercepts the `start` event. By setting the `output` property on the event data, the middleware effectively "mocks" the result, preventing the actual ChatModel from running. ```py Python [expandable] theme={null} import asyncio from typing import Any from beeai_framework.backend import AssistantMessage, ChatModel, ChatModelOutput, UserMessage from beeai_framework.context import RunContext, RunContextStartEvent, RunMiddlewareProtocol from beeai_framework.emitter import EmitterOptions, EventMeta from beeai_framework.emitter.utils import create_internal_event_matcher class OverrideResponseMiddleware(RunMiddlewareProtocol): """Middleware that sets the result value for a given runnable without executing it""" def __init__(self, result: Any) -> None: self._result = result def bind(self, ctx: RunContext) -> None: """Calls once the target is about to be run.""" ctx.emitter.on( create_internal_event_matcher("start", ctx.instance), self._run, # ensures that this callback will be the first invoked EmitterOptions(is_blocking=True, priority=1), ) async def _run(self, data: RunContextStartEvent, meta: EventMeta) -> None: """Set output property to the result which prevents an execution of the target handler.""" data.output = self._result async def main() -> None: middleware = OverrideResponseMiddleware(ChatModelOutput(output=[AssistantMessage("BeeAI is the best!")])) response = await ChatModel.from_name("ollama:granite4:micro").run([UserMessage("Hello!")]).middleware(middleware) print(response.get_text_content()) # "BeeAI is the best!" if __name__ == "__main__": asyncio.run(main()) ``` **Key Implementation Details:** * `create_internal_event_matcher`: A helper used to ensure you are matching the specific internal event (like `start` / `success` / `error` / `finish`) for the correct component instance. * `EmitterOptions`: Used here to set `priority=1` and `is_blocking=True`, ensuring this middleware executes early and takes precedence over other callbacks. * `data.output`: Setting this property during a `start` event signals the framework to skip the underlying execution (e.g., the LLM call) and return this value immediately. Ensure that your mock response matches the expected output type of the component you are intercepting. For example, if you override a `ChatModel`, the return type must be `ChatModelOutput`. ### **Registering Middleware** Once defined, you can attach middleware to a component using the `.middleware()` method just before execution. ```py Python [expandable] theme={null} async def main() -> None: # 1. Prepare the mock response mock_output = ChatModelOutput(output=[AssistantMessage("BeeAI is the best!")]) # 2. Initialize the middleware middleware = OverrideResponseMiddleware(mock_output) # 3. Attach middleware to the run llm = ChatModel.from_name("ollama:granite4") response = await llm.run([UserMessage("Hello!")]).middleware(middleware) print(response.get_text_content()) # Output: "BeeAI is the best!" if __name__ == "__main__": asyncio.run(main()) ``` Note that middleware is applied to the **Run** instance (the result of calling `.run()`), not the standalone emitter class itself. However, in some cases, middleware can be passed via the component's constructor if supported. ## Events glossary The following sections list all events that can be observed for built-in components. Note that your tools/agents/etc. can emit additional events. ### Tools The following events can be observed when calling `Tool.run(...)`. | Event | Data Type | Description | | :-------- | :----------------- | :----------------------------------------------------------------------- | | `start` | `ToolStartEvent` | Triggered when a tool starts executing. | | `success` | `ToolSuccessEvent` | Triggered when a tool completes execution successfully. | | `error` | `ToolErrorEvent` | Triggered when a tool encounters an error. | | `retry` | `ToolRetryEvent` | Triggered when a tool operation is being retried. | | `finish` | `None` | Triggered when tool execution finishes (regardless of success or error). | ### Chat Models The following events can be observed when calling `ChatModel.run(...)`. | Event | Data Type | Description | | :---------- | :----------------------- | :----------------------------------------------------------------------------------- | | `start` | `ChatModelStartEvent` | Triggered when model generation begins. | | `new_token` | `ChatModelNewTokenEvent` | Triggered when a new token is generated during streaming. Streaming must be enabled. | | `success` | `ChatModelSuccessEvent` | Triggered when the model generation completes successfully. | | `error` | `ChatModelErrorEvent` | Triggered when model generation encounters an error. | | `finish` | `None` | Triggered when model generation finishes (regardless of success or error). | [Check out the in-code definition](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/backend/events.py) ### Requirement Agent | Event | Data Type | Description | | :------------- | :--------------------------------- | :--------------------------------------------------------- | | `start` | `RequirementAgentStartEvent` | Triggered when the agent begins execution. | | `success` | `RequirementAgentSuccessEvent` | Triggered when the agent successfully completes execution. | | `final_answer` | `RequirementAgentFinalAnswerEvent` | Triggered with intermediate chunks of the final answer. | [Check out the in-code definition](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/agents/experimental/events.py). ### ToolCalling Agent The following events can be observed by calling `ToolCallingAgent.run(...)`. | Event | Data Type | Description | | :-------- | :----------------------------- | :--------------------------------------------------------- | | `start` | `ToolCallingAgentStartEvent` | Triggered when the agent begins execution. | | `success` | `ToolCallingAgentSuccessEvent` | Triggered when the agent successfully completes execution. | [Check out the in-code definition](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/agents/tool_calling/events.py). ### ReAct Agent The following events can be observed by calling `ReActAgent.run(...)`. | Event | Data Type | Description | | :---------------------------- | :----------------------- | :--------------------------------------------------------- | | `start` | `ReActAgentStartEvent` | Triggered when the agent begins execution. | | `error` | `ReActAgentErrorEvent` | Triggered when the agent encounters an error. | | `retry` | `ReActAgentRetryEvent` | Triggered when the agent is retrying an operation. | | `success` | `ReActAgentSuccessEvent` | Triggered when the agent successfully completes execution. | | `update` and `partial_update` | `ReActAgentUpdateEvent` | Triggered when the agent updates its state. | [Check out the in-code definition](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/agents/react/events.py) [Check out the in-code definition](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/tools/events.py). ### Workflow The following events can be observed when calling `Workflow.run(...)`. | Event | Data Type | Description | | :-------- | :--------------------- | :----------------------------------------------------- | | `start` | `WorkflowStartEvent` | Triggered when a workflow step begins execution. | | `success` | `WorkflowSuccessEvent` | Triggered when a workflow step completes successfully. | | `error` | `WorkflowErrorEvent` | Triggered when a workflow step encounters an error. | [Check out the in-code definition](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/workflows/events.py). ### LinePrefixParser The following events are caught internally by the `LinePrefixParser`. | Event | Data Type | Description | | :--------------- | :----------------------- | :-------------------------------------- | | `update` | `LinePrefixParserUpdate` | Triggered when an update occurs. | | `partial_update` | `LinePrefixParserUpdate` | Triggered when a partial update occurs. | ### StreamToolCallMiddleware The following events are caught internally by the `StreamToolCallMiddleware`. | Event | Data Type | Description | | :------- | :------------------------------------ | :------------------------------- | | `update` | `StreamToolCallMiddlewareUpdateEvent` | Triggered when an update occurs. | [Check out the in-code definition](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/middleware/stream_tool_call.py). ### GlobalTrajectoryMiddleware The following events are handled internally by the `GlobalTrajectoryMiddleware`: | Event | Data Type | Description | | :-------- | :--------------------------------------- | :--------------------------------------------------------------------------------- | | `start` | `GlobalTrajectoryMiddlewareStartEvent` | Triggered when a target begins execution. | | `success` | `GlobalTrajectoryMiddlewareSuccessEvent` | Triggered when a target completes successfully. | | `error` | `GlobalTrajectoryMiddlewareErrorEvent` | Triggered when an error occurs during target execution. | | `finish` | `GlobalTrajectoryMiddlewareFinishEvent` | Triggered after a target has finished execution, regardless of success or failure. | All events inherit from the `GlobalTrajectoryMiddlewareEvent` class. ```py Python [expandable] theme={null} class GlobalTrajectoryMiddlewareEvent(BaseModel): message: str level: TraceLevel origin: tuple[Any, EventMeta] ``` The first element of the `origin` attribute is the original event (e.g., `start` → `RunContextStartEvent`, etc.) that comes from the `RunContext`. ### RunContext Special events that are emitted before the target's handler gets executed. A run event contains `.run.` in its event's path and has `internal` set to true in the event's context object. | Event | Data Type | Description | | :-------- | :----------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `start` | `RunContextStartEvent` | Triggered when the run starts. Has `input` (positional/keyword argument with which the function was run) and `output` property. Set the `output` property to prevent the execution of the target handler. | | `success` | `RunContextSuccessEvent` | Triggered when the run succeeds. | | `error` | `FrameworkError` | Triggered when an error occurs. | | `finish` | `RunContextFinishEvent` | Triggered when the run finishes. | [Check out the in-code definition](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/context.py#L260-L273). Instead of a manual matching, use `create_internal_event_matcher` helper function. ## Examples Explore reference middleware implementations in Python. Explore reference middleware implementations in TypeScript. # Observability Source: https://framework.beeai.dev/modules/observability Monitor and debug your BeeAI Framework applications with OpenInference instrumentation ## Overview The BeeAI Framework provides comprehensive observability through OpenInference instrumentation, enabling you to trace and monitor your AI applications with industry-standard telemetry. This allows you to debug issues, optimize performance, and understand how your agents, tools, and workflows are performing in production. Currently supported in Python only. ## Quickstart ### 1. Install the package This package provides the OpenInference instrumentor specifically designed for the BeeAI Framework. ```bash theme={null} pip install openinference-instrumentation-beeai ``` ### 2. Set up observability Configure OpenTelemetry to create and export spans. This example sets up an OTLP HTTP exporter, a tracer provider, and the BeeAI instrumentor, with the endpoint pointing to a local Arize Phoenix instance. ```py Python [expandable] theme={null} from openinference.instrumentation.beeai import BeeAIInstrumentor from opentelemetry import trace as trace_api from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk import trace as trace_sdk from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace.export import SimpleSpanProcessor def setup_observability() -> None: resource = Resource(attributes={}) tracer_provider = trace_sdk.TracerProvider(resource=resource) tracer_provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter())) trace_api.set_tracer_provider(tracer_provider) BeeAIInstrumentor().instrument() ``` To override the default traces endpoint ([http://localhost:4318/v1/traces](http://localhost:4318/v1/traces)), set the `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` environment variable. For Arize Phoenix, set `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` to `http://localhost:6006/v1/traces`. ### 3. Enable instrumentation Call the setup function **before** running any BeeAI Framework code: ```py Python [expandable] theme={null} setup_observability() ``` The setup function must be called before importing and using any BeeAI Framework components to ensure all operations are properly instrumented. ## What Gets Instrumented When instrumentation is enabled, BeeAI emits spans and attributes for core runtime operations. ### Agents * Agent execution start/stop times * Input prompts and output responses * Tool usage within agent workflows * Memory operations and state changes ### Tools * Tool invocation details * Input parameters and return values * Execution time and success/failure status * Error details when tools fail ### Chat Models * Model inference requests (including streaming) * Token usage statistics * Model parameters (temperature, max tokens, etc.) * Response timing and latency ### Embedding Models * Text embedding requests * Input text and embedding dimensions * Processing time and batch sizes ### Workflows * Workflow step execution * State transitions and data flow * Step dependencies and execution order ## Observability Backends ### Arize Phoenix Open-source observability for LLM applications. **Documentation:** [Arize Phoenix](https://github.com/Arize-ai/phoenix) ### LangFuse Production-ready LLMOps platform with advanced analytics. **Documentation:** [LangFuse OpenTelemetry Integration](https://langfuse.com/integrations/native/opentelemetry#opentelemetry-native-langfuse-sdk-v3) ### LangSmith Comprehensive LLM development platform by LangChain. **Documentation:** [LangSmith OpenTelemetry Guide](https://docs.smith.langchain.com/observability/how_to_guides/trace_with_opentelemetry) ### Other Platforms Any backend supporting OpenTelemetry/OpenInference standards. # RAG Source: https://framework.beeai.dev/modules/rag Build intelligent agents that combine retrieval with generation for enhanced AI capabilities ## Overview Retrieval-Augmented Generation (RAG) is a powerful paradigm that enhances large language models by providing them with relevant information from external knowledge sources. This approach has become essential for enterprise AI applications that need to work with specific, up-to-date, or domain-specific information that wasn't part of the model's training data. RAG addresses key limitations of traditional LLMs: * **Knowledge cutoffs** - Access the most current information * **Domain expertise** - Integrate specialized knowledge bases * **Factual accuracy** - Reduce hallucinations with grounded responses * **Scalability** - Work with vast document collections efficiently Enterprises rely on RAG for applications like customer support, document analysis, knowledge management, and intelligent search systems. Location within the framework: [beeai\_framework/rag](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/rag). RAG is most effective when document chunking and retrieval strategies are tailored to your specific problem domain. It's recommended to experiment with different configurations such as chunk sizes, overlap settings, and retrieval parameters. Future releases of BeeAI will provide enhanced capabilities to streamline this optimization process. ## Philosophy BeeAI Framework's approach to RAG emphasizes **integration over invention**. Rather than building RAG components from scratch, we provide seamless adapters for proven, production-ready solutions from leading platforms like LangChain and Llama-Index. This philosophy offers several advantages: * **Leverage existing expertise** - Use battle-tested implementations * **Faster time-to-market** - Focus on your application logic, not infrastructure * **Community support** - Benefit from extensive documentation and community * **Flexibility** - Switch between providers as needs evolve ## Installation To use RAG components, install the framework with the RAG extras: ```bash theme={null} pip install "beeai-framework[rag]" ``` ## RAG Components The following table outlines the key RAG components available in the BeeAI Framework: | Component | Description | Compatibility | Future Compatibility | | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------- | -------------------- | | [**Document Loaders**](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/backend/document_loader.py) | Responsible for loading content from different formats and sources such as PDFs, web pages, and structured text files | LangChain | BeeAI | | [**Text Splitters**](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/backend/text_splitter.py) | Splits long documents into workable chunks using various strategies, e.g. fixed length or preserving context | LangChain | BeeAI | | [**Document**](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/backend/types.py) | The basic data structure to house text content, metadata, and relevant scores for retrieval operations | BeeAI | - | | [**Vector Store**](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/backend/vector_store.py) | Used to store document embeddings and retrieve them based on semantic similarity using embedding distance | LangChain | BeeAI, Llama-Index | | [**Document Processors**](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/backend/document_processor.py) | Used to process and refine documents during the retrieval-generation lifecycle including reranking and filtering | Llama-Index | - | ## Dynamic Module Loading BeeAI Framework provides a dynamic module loading system that allows you to instantiate RAG components using string identifiers. This approach enables configuration-driven architectures and easy provider switching. The `from_name` method uses the format `provider:ClassName` where: * `provider` identifies the integration module (e.g., "beeai", "langchain") * `ClassName` specifies the exact class to instantiate Dynamic loading enables you to switch between different vector store implementations without changing your application code - just update the configuration string. ### BeeAI Vector Store ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.adapters.beeai.backend.vector_store import TemporalVectorStore from beeai_framework.adapters.langchain.mappers.documents import lc_document_to_document from beeai_framework.backend.embedding import EmbeddingModel from beeai_framework.backend.vector_store import VectorStore from beeai_framework.errors import FrameworkError # LC dependencies - to be swapped with BAI dependencies try: from langchain_community.document_loaders import UnstructuredMarkdownLoader from langchain_text_splitters import RecursiveCharacterTextSplitter except ModuleNotFoundError as e: raise ModuleNotFoundError( "Optional modules are not found.\nRun 'pip install \"beeai-framework[rag]\"' to install." ) from e async def main() -> None: embedding_model = EmbeddingModel.from_name("watsonx:ibm/slate-125m-english-rtrvr-v2", truncate_input_tokens=500) # Document loading loader = UnstructuredMarkdownLoader(file_path="docs/modules/agents.mdx") docs = loader.load() text_splitter = RecursiveCharacterTextSplitter(chunk_size=2000, chunk_overlap=1000) all_splits = text_splitter.split_documents(docs) documents = [lc_document_to_document(document) for document in all_splits] print(f"Loaded {len(documents)} documents") # pyrefly: ignore [bad-assignment] vector_store: TemporalVectorStore = VectorStore.from_name( name="beeai:TemporalVectorStore", embedding_model=embedding_model ) # type: ignore[assignment] _ = await vector_store.add_documents(documents=documents) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` Native BeeAI modules can be loaded directly by importing and instantiating the module, e.g. `from beeai_framework.adapters.beeai.backend.vector_store import TemporalVectorStore`. ### Supported Provider's Vector Store ```py Python [expandable] theme={null} # LangChain integration vector_store = VectorStore.from_name( name="langchain:InMemoryVectorStore", embedding_model=embedding_model ) ``` You can customize dynamically loaded components by passing additional parameters directly to the `from_name` method. These parameters will be forwarded to the component's constructor, allowing you to configure settings like batch sizes, connection pools, or other provider-specific options without changing your code structure. The same dynamic loading pattern works for document loaders. For example, you can load documents using `DocumentLoader.from_name("langchain:UnstructuredMarkdownLoader", file_path="docs/modules/agents.mdx")` to get your documents ready for the vector store. ## RAG Agent The RAG Agent implements a sophisticated retrieval-augmented generation pipeline that combines the power of semantic search with large language models. The agent follows a three-stage process and supports advanced configuration options including custom reranking, flexible retrieval parameters, comprehensive error handling, and query flexibility using various object types. ### 1. Retrieval The agent searches the vector store using semantic similarity to find the most relevant documents for the user's query. You can configure the number of documents retrieved and similarity thresholds to optimize for your specific use case. ### 2. Reranking (Optional) Retrieved documents can be reranked using advanced LLM-based models to improve relevance and quality of the context provided to the generation stage. This step significantly enhances response accuracy for complex queries. ### 3. Generation The LLM generates a response using the retrieved documents as context, ensuring grounded and accurate answers. Built-in error handling ensures informative error messages are stored in memory when issues occur. ### Basic Usage Document loading and population of the vector store is the developers's responsibility and out of scope for the agent. ```py Python [expandable] theme={null} import asyncio import logging import os import sys import traceback from dotenv import load_dotenv from beeai_framework.adapters.beeai.backend.vector_store import TemporalVectorStore from beeai_framework.adapters.langchain.backend.vector_store import LangChainVectorStore from beeai_framework.agents.experimental.rag import RAGAgent from beeai_framework.backend.chat import ChatModel from beeai_framework.backend.document_loader import DocumentLoader from beeai_framework.backend.document_processor import DocumentProcessor from beeai_framework.backend.embedding import EmbeddingModel from beeai_framework.backend.text_splitter import TextSplitter from beeai_framework.backend.vector_store import VectorStore from beeai_framework.errors import FrameworkError from beeai_framework.logger import Logger from beeai_framework.memory import UnconstrainedMemory load_dotenv() # load environment variables logger = Logger("rag-agent", level=logging.DEBUG) POPULATE_VECTOR_DB = True VECTOR_DB_PATH_4_DUMP = "" # Set this path for persistency INPUT_DOCUMENTS_LOCATION = "docs/integrations" async def populate_documents() -> VectorStore | None: embedding_model = EmbeddingModel.from_name("watsonx:ibm/slate-125m-english-rtrvr-v2", truncate_input_tokens=500) # Load existing vector store if available # pyrefly: ignore [redundant-condition] if VECTOR_DB_PATH_4_DUMP and os.path.exists(VECTOR_DB_PATH_4_DUMP): print(f"Loading vector store from: {VECTOR_DB_PATH_4_DUMP}") preloaded_vector_store: VectorStore = TemporalVectorStore.load( path=VECTOR_DB_PATH_4_DUMP, embedding_model=embedding_model ) return preloaded_vector_store # Create new vector store if population is enabled if POPULATE_VECTOR_DB: loader = DocumentLoader.from_name( name="langchain:UnstructuredMarkdownLoader", file_path="docs/modules/agents.mdx" ) try: documents = await loader.load() except Exception: return None # Use abstracted text splitter text_splitter = TextSplitter.from_name( name="langchain:RecursiveCharacterTextSplitter", chunk_size=2000, chunk_overlap=1000 ) documents = await text_splitter.split_documents(documents) print(f"Loaded {len(documents)} documents") print("Rebuilding vector store") # Adapter example # pyrefly: ignore [bad-assignment] vector_store: TemporalVectorStore = VectorStore.from_name( name="beeai:TemporalVectorStore", embedding_model=embedding_model ) # type: ignore[assignment] # Native examples # vector_store: TemporalVectorStore = TemporalVectorStore(embedding_model=embedding_model) # vector_store = InMemoryVectorStore(embedding_model) _ = await vector_store.add_documents(documents=documents) # pyrefly: ignore [redundant-condition] if VECTOR_DB_PATH_4_DUMP and isinstance(vector_store, LangChainVectorStore): print(f"Dumping vector store to: {VECTOR_DB_PATH_4_DUMP}") # pyrefly: ignore [missing-attribute] vector_store.vector_store.dump(VECTOR_DB_PATH_4_DUMP) return vector_store # Neither existing DB found nor population enabled return None async def main() -> None: vector_store = await populate_documents() if vector_store is None: raise FileNotFoundError( f"Vector database not found at {VECTOR_DB_PATH_4_DUMP}. " "Either set POPULATE_VECTOR_DB=True to create a new one, or ensure the database file exists." ) llm = ChatModel.from_name("ollama:llama3.2") reranker = DocumentProcessor.from_name("beeai:LLMDocumentReranker", llm=llm) agent = RAGAgent(llm=llm, memory=UnconstrainedMemory(), vector_store=vector_store, reranker=reranker) response = await agent.run("What agents are available in BeeAI?") print(response.last_message.text) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` For production deployments, consider implementing document caching and index optimization to improve response times. ### RAG as Tools Vector store population (loading and chunking documents) is typically handled offline in production applications, making Vector Store the prominent RAG building block utilized as a tool. `VectorStoreSearchTool` enables any agent to perform semantic search against a pre-populated vector store. This provides flexibility for agents that need retrieval capabilities alongside other functionalities. The VectorStoreSearchTool can be dynamically instantiated using `VectorStoreSearchTool.from_vector_store_name("beeai:TemporalVectorStore", embedding_model=embedding_model)`, see RAG with RequirementAgent example for the full code. ## Examples Complete RAG agent implementation with document loading and processing Example showing how to use VectorStoreSearchTool with RequirementAgent for RAG capabilities # Serialization Source: https://framework.beeai.dev/modules/serialization ## Overview Serialization is a process of converting complex data structures or objects (e.g., agents, memories, or tools) into a format that can be easily stored, transmitted, and reconstructed later. Think of it as creating a blueprint of your object that can be used to rebuild it exactly as it was. BeeAI framework provides robust serialization capabilities through its built-in `Serializer` class that enables: * 💾 Persistence: Store agent state, memory, tools, and other components * 🔄 Transmission: Send complex objects across network boundaries or processes * 📦 Snapshots: Create point-in-time captures of component state * 🔧 Reconstruction: Rebuild objects from their serialized representation ```py Python [expandable] theme={null} Example coming soon ``` ```ts TypeScript [expandable] theme={null} import { Serializer } from "beeai-framework/serializer/serializer"; const original = new Date("2024-01-01T00:00:00.000Z"); const serialized = await Serializer.serialize(original); const deserialized = await Serializer.deserialize(serialized); console.info(deserialized instanceof Date); // true console.info(original.toISOString() === deserialized.toISOString()); // true ``` *** ## Core Concepts ### `Serializable` Class Most framework components implement the `Serializable` class with these key methods: | Method | Purpose | | ------------------------ | ---------------------------------------------------- | | `createSnapshot()` | Captures the current state | | `loadSnapshot(snapshot)` | Applies a snapshot to the current instance | | `fromSnapshot(snapshot)` | Creates a new instance from a snapshot (static) | | `fromSerialized(data)` | Creates a new instance from serialized data (static) | ### Serialization Process The serialization process involves: 1. Converting complex objects into a format that preserves their structure and data 2. Including type information to enable proper reconstruction 3. Managing references to maintain object identity across serialization boundaries 4. Handling special cases like circular references and custom types *** ## Basic Usage ### Serializing framework components Most BeeAI components can be serialized out of the box. Here's an example using memory: ```py Python [expandable] theme={null} Example coming soon ``` ```ts TypeScript [expandable] theme={null} import { TokenMemory } from "beeai-framework/memory/tokenMemory"; import { AssistantMessage, UserMessage } from "beeai-framework/backend/message"; const memory = new TokenMemory(); await memory.add(new UserMessage("What is your name?")); const serialized = await memory.serialize(); const deserialized = await TokenMemory.fromSerialized(serialized); await deserialized.add(new AssistantMessage("Bee")); ``` Most framework components are `Serializable`. ## Advanced Features ### Custom Serialization If you want to serialize a class that the `Serializer` does not know, you may register it using one of the following options. **1. Register External Classes** You can register external classes with the serializer: ```py Python [expandable] theme={null} Example coming soon ``` ```ts TypeScript [expandable] theme={null} import { Serializer } from "beeai-framework/serializer/serializer"; class MyClass { constructor(public readonly name: string) {} } Serializer.register(MyClass, { // Defines how to transform a class to a plain object (snapshot) toPlain: (instance) => ({ name: instance.name }), // Defines how to transform a plain object (snapshot) a class instance fromPlain: (snapshot) => new MyClass(snapshot.name), // optional handlers to support lazy initiation (handling circular dependencies) createEmpty: () => new MyClass(""), updateInstance: (instance, update) => { Object.assign(instance, update); }, }); const instance = new MyClass("Bee"); const serialized = await Serializer.serialize(instance); const deserialized = await Serializer.deserialize(serialized); console.info(instance); console.info(deserialized); ``` **2. Implement the `Serializable` Interface** For deeper integration, extend the Serializable class: ```py Python [expandable] theme={null} Example coming soon ``` ```ts TypeScript [expandable] theme={null} import { Serializable } from "beeai-framework/internals/serializable"; class MyClass extends Serializable { constructor(public readonly name: string) { super(); } static { // register class to the global serializer register this.register(); } createSnapshot(): unknown { return { name: this.name, }; } loadSnapshot(snapshot: ReturnType) { Object.assign(this, snapshot); } } const instance = new MyClass("Bee"); const serialized = await instance.serialize(); const deserialized = await MyClass.fromSerialized(serialized); console.info(instance); console.info(deserialized); ``` Failure to register a class that the `Serializer` does not know will result in the `SerializerError` error. BeeAI framework avoids importing all potential classes automatically to prevent increased application size and unnecessary dependencies. ## Context matters ```py Python [expandable] theme={null} Example coming soon ``` ```ts TypeScript [expandable] theme={null} import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory"; import { UserMessage } from "beeai-framework/backend/message"; // String containing serialized `UnconstrainedMemory` instance with one message in it. const serialized = `{"__version":"0.0.0","__root":{"__serializer":true,"__class":"Object","__ref":"18","__value":{"target":"UnconstrainedMemory","snapshot":{"__serializer":true,"__class":"Object","__ref":"17","__value":{"messages":{"__serializer":true,"__class":"Array","__ref":"1","__value":[{"__serializer":true,"__class":"SystemMessage","__ref":"2","__value":{"content":{"__serializer":true,"__class":"Array","__ref":"3","__value":[{"__serializer":true,"__class":"Object","__ref":"4","__value":{"type":"text","text":"You are a helpful assistant."}}]},"meta":{"__serializer":true,"__class":"Object","__ref":"5","__value":{"createdAt":{"__serializer":true,"__class":"Date","__ref":"6","__value":"2025-02-06T14:51:01.459Z"}}},"role":"system"}},{"__serializer":true,"__class":"UserMessage","__ref":"7","__value":{"content":{"__serializer":true,"__class":"Array","__ref":"8","__value":[{"__serializer":true,"__class":"Object","__ref":"9","__value":{"type":"text","text":"Hello!"}}]},"meta":{"__serializer":true,"__class":"Object","__ref":"10","__value":{"createdAt":{"__serializer":true,"__class":"Date","__ref":"11","__value":"2025-02-06T14:51:01.459Z"}}},"role":"user"}},{"__serializer":true,"__class":"AssistantMessage","__ref":"12","__value":{"content":{"__serializer":true,"__class":"Array","__ref":"13","__value":[{"__serializer":true,"__class":"Object","__ref":"14","__value":{"type":"text","text":"Hello, how can I help you?"}}]},"meta":{"__serializer":true,"__class":"Object","__ref":"15","__value":{"createdAt":{"__serializer":true,"__class":"Date","__ref":"16","__value":"2025-02-06T14:51:01.459Z"}}},"role":"assistant"}}]}}}}}}`; // If `Message` was not imported the serialization would fail because the `Message` had no chance to register itself. const memory = await UnconstrainedMemory.fromSerialized(serialized, { // this part can be omitted if all classes used in the serialized string are imported (and have `static` register block) or at least one initiated extraClasses: [UserMessage], }); console.info(memory.messages); ``` *** ## Examples Explore reference serialization implementations in TypeScript COMING SOON: Explore reference serialization implementations in Python # Serve Source: https://framework.beeai.dev/modules/serve The `Serve` module enables developers to expose components built with the BeeAI Framework through a server to external clients. Out of the box, we provide implementations for prominent protocols such as A2A and MCP, allowing you to quickly serve existing functionalities. You can also create your own custom adapter if needed. Location within the framework: [beeai\_framework/serve](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/serve). ## Supported Providers The following table lists the currently supported providers: | Name | Installation | | :------------------------------------------------------------------- | :---------------------------------------- | | [A2A](/integrations/a2a/#server) | `pip install beeai-framework[a2a]` | | [Agent Stack](/integrations/agent-stack/#server) | `pip install beeai-framework[agentstack]` | | [MCP](/integrations/mcp/#server) | `pip install beeai-framework[mcp]` | | [IBM watsonx Orchestrate](/integrations/watsonx-orchestrate/#server) | `pip install beeai-framework` | | [OpenAI Chat Completion API](/integrations/openai-api/#server) | `pip install beeai-framework` | | [OpenAI Responses API](/integrations/openai-api/#server) | `pip install beeai-framework` | Click on the provider name to see a dedicate page how to use it. ## How it works Once you initiate the appropriate server class (eg: `AgentStackServer`, `OpenAIServer`, ...) you can do the following. * Register a new member (`register` method). * Deregister an existing member (`deregister` method). * List existing members (`members` property). * Run the server (`serve` method / `aserve` method). You can typically register one of the followings: * [Tools](/modules/tools/) * [Agents](/modules/agents/) * [Chat Models](/modules/backend/) * [Runnable](/modules/middleware/) If the given instance is not supported, the `register` method will raise an exception. Nevertheless, you can easily register a custom factory to make it supported. ## Register a custom factory If the given server doesn't support (or you just want to override the conversion is done) you can register a custom factory function which takes the instance of a given type and converts it into an instance that the server knows how to work with. The following example showcases how we can add a support for `PromptTemplate` class (which exists in BeeAI Framework) so that it gets exposed as a [`Prompt`](https://modelcontextprotocol.info/docs/concepts/prompts/) in the MCP Server. ```py Python [expandable] theme={null} from typing import Any from mcp.server.fastmcp.prompts.base import Prompt as MCPPrompt from mcp.server.fastmcp.prompts.base import PromptArgument from pydantic import BaseModel from beeai_framework.adapters.mcp import MCPServer, MCPServerConfig from beeai_framework.template import PromptTemplate def add_prompt_template_factory() -> None: def factory(instance: PromptTemplate[Any]) -> MCPPrompt: return MCPPrompt( name=instance.name, title=instance.name, description=instance.description, arguments=[ PromptArgument( name=k, description=v.description, required=v.default is None and v.default_factory is None ) for k, v in instance.input_schema.model_fields.items() ], fn=lambda **kwargs: instance.render(kwargs), ) MCPServer.register_factory(PromptTemplate, factory, override=True) def run_server() -> None: class GreetingTemplateModel(BaseModel): name: str my_template = PromptTemplate( template="Hello {name}", schema=GreetingTemplateModel, ) server = MCPServer(config=MCPServerConfig(transport="streamable-http")) server.register(my_template) server.serve() if __name__ == "__main__": add_prompt_template_factory() run_server() ``` ## Examples Explore usage of the Serve module in Python Explore usage of the Serve module in TypeScript # Templates Source: https://framework.beeai.dev/modules/templates ## Overview Templates are predefined structures used to create consistent outputs. In the context of AI applications, prompt templates provide structured guidance for language models to generate targeted responses. They include placeholders that can be filled with specific information at runtime. The Framework implements this functionality through the `PromptTemplate` class, which uses Mustache-style syntax (via the `chevron` library) for variable substitution. The implementation adds type safety and validation using Pydantic or Zod schemas. At its core, the `PromptTemplate` class: * Validates input data against a Pydantic model schema * Handles template variable substitution * Supports dynamic content generation through callable functions * Provides default values for optional fields * Enables template customization through forking Prompt Templates are fundamental building blocks in the framework and are extensively used in agent implementations. Supported in Python and TypeScript. ## Basic usage ### Simple template Create templates with basic variable substitution and type validation. ```py Python [expandable] theme={null} import sys import traceback from pydantic import BaseModel from beeai_framework.errors import FrameworkError from beeai_framework.template import PromptTemplate def main() -> None: class UserMessage(BaseModel): label: str input: str template: PromptTemplate[UserMessage] = PromptTemplate( schema=UserMessage, template="""{{label}}: {{input}}""", ) prompt = template.render(label="Query", input="What interesting things happened on this day in history?") print(prompt) if __name__ == "__main__": try: main() except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { PromptTemplate } from "beeai-framework/template"; import { z } from "zod"; const greetTemplate = new PromptTemplate({ template: `Hello {{name}}`, schema: z.object({ name: z.string(), }), }); const output = greetTemplate.render({ name: "Alex", }); console.log(output); // Hello Alex! ``` This example creates a simple template that formats a user message with a label and input text. The Pydantic model or Zod schema ensures type safety for the template variables. ### Template functions Add dynamic content to templates using custom functions. ```py Python [expandable] theme={null} import sys import traceback from datetime import UTC, datetime from typing import Any from pydantic import BaseModel from beeai_framework.errors import FrameworkError from beeai_framework.template import PromptTemplate def main() -> None: class AuthorMessage(BaseModel): text: str author: str | None = None created_at: str | None = None def format_meta(data: dict[str, Any]) -> str: if data.get("author") is None and data.get("created_at") is None: return "" author = data.get("author") or "anonymous" created_at = data.get("created_at") or datetime.now(UTC).strftime("%A, %B %d, %Y at %I:%M:%S %p") return f"\nThis message was created at {created_at} by {author}." template: PromptTemplate[AuthorMessage] = PromptTemplate( schema=AuthorMessage, functions={ "format_meta": lambda data: format_meta(data), }, template="""Message: {{text}}{{format_meta}}""", ) # Message: Hello from 2024! # This message was created at 2024-01-01T00:00:00+00:00 by John. message = template.render( text="Hello from 2024!", author="John", created_at=datetime(2024, 1, 1, tzinfo=UTC).isoformat() ) print(message) # Message: Hello from the present! message = template.render(text="Hello from the present!") print(message) if __name__ == "__main__": try: main() except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { PromptTemplate } from "beeai-framework/template"; import { z } from "zod"; const messageTemplate = new PromptTemplate({ schema: z .object({ text: z.string(), author: z.string().optional(), createdAt: z.string().datetime().optional(), }) .passthrough(), functions: { formatMeta: function () { if (!this.author && !this.createdAt) { return ""; } const author = this.author || "anonymous"; const createdAt = this.createdAt || new Date().toISOString(); return `\nThis message was created at ${createdAt} by ${author}.`; }, }, template: `Message: {{text}}{{formatMeta}}`, }); // Message: Hello from 2024! // This message was created at 2024-01-01T00:00:00.000Z by John. console.log( messageTemplate.render({ text: "Hello from 2024!", author: "John", createdAt: new Date("2024-01-01").toISOString(), }), ); // Message: Hello from the present! console.log( messageTemplate.render({ text: "Hello from the present!", }), ); ``` This example demonstrates how to add custom functions to templates: * The `format_meta` function returns the date and author in a readable string * Functions can be called directly from the template using Mustache-style syntax ### Working with objects Handle complex nested data structures in templates with proper type validation. ```py Python [expandable] theme={null} import sys import traceback from pydantic import BaseModel from beeai_framework.errors import FrameworkError from beeai_framework.template import PromptTemplate def main() -> None: class Response(BaseModel): duration: int class ExpectedDuration(BaseModel): expected: int responses: list[Response] template: PromptTemplate[ExpectedDuration] = PromptTemplate( schema=ExpectedDuration, template="""Expected Duration: {{expected}}ms; Retrieved: {{#responses}}{{duration}}ms {{/responses}}""", defaults={"expected": 5}, ) # Expected Duration: 5ms; Retrieved: 3ms 5ms 6ms output = template.render(responses=[Response(duration=3), Response(duration=5), Response(duration=6)]) print(output) if __name__ == "__main__": try: main() except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { PromptTemplate } from "beeai-framework/template"; import { z } from "zod"; const template = new PromptTemplate({ template: `Expected Duration: {{expected}}ms; Retrieved: {{#responses}}{{duration}}ms {{/responses}}`, schema: z.object({ expected: z.number(), responses: z.array(z.object({ duration: z.number() })), }), defaults: { expected: 5, }, }); const output = template.render({ expected: undefined, // default value will be used responses: [{ duration: 3 }, { duration: 5 }, { duration: 6 }], }); console.log(output); // Expected Duration: 5ms; Retrieved: 3ms 5ms 6ms ``` This example shows how to work with nested objects in templates. The Mustache syntax allows for iterating through the responses array and accessing properties of each object. ### Working with arrays Process collections of data within templates for dynamic list generation. ```py Python [expandable] theme={null} import sys import traceback from pydantic import BaseModel, Field from beeai_framework.errors import FrameworkError from beeai_framework.template import PromptTemplate def main() -> None: class ColorsObject(BaseModel): colors: list[str] = Field(..., min_length=1) template: PromptTemplate[ColorsObject] = PromptTemplate( schema=ColorsObject, template="""Colors: {{#colors}}{{.}}, {{/colors}}""", ) # Colors: Green, Yellow, output = template.render(colors=["Green", "Yellow"]) print(output) if __name__ == "__main__": try: main() except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { PromptTemplate } from "beeai-framework/template"; import { z } from "zod"; const template = new PromptTemplate({ schema: z.object({ colors: z.array(z.string()).min(1), }), template: `Colors: {{#trim}}{{#colors}}{{.}},{{/colors}}{{/trim}}`, }); const output = template.render({ colors: ["Green", "Yellow"], }); console.log(output); // Colors: Green,Yellow ``` This example demonstrates how to iterate over arrays in templates using Mustache's section syntax. *Source: [python/examples/templates/arrays.py](https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/templates/arrays.py)* ### Template forking The fork() method allows you to create new templates based on existing ones, with customizations. Template forking is useful for: * Creating variations of templates while maintaining core functionality * Adding new fields or functionality to existing templates * Specializing generic templates for specific use cases ```py Python [expandable] theme={null} import sys import traceback from typing import Any from pydantic import BaseModel from beeai_framework.errors import FrameworkError from beeai_framework.template import PromptTemplate, PromptTemplateInput def main() -> None: class OriginalSchema(BaseModel): name: str objective: str original: PromptTemplate[OriginalSchema] = PromptTemplate( schema=OriginalSchema, template="""You are a helpful assistant called {{name}}. Your objective is to {{objective}}.""", ) def customizer(temp_input: PromptTemplateInput[Any]) -> PromptTemplateInput[Any]: new_temp = temp_input.model_copy() new_temp.template = f"""{temp_input.template} Your answers must be concise.""" new_temp.defaults["name"] = "Bee" return new_temp modified = original.fork(customizer=customizer) # You are a helpful assistant called Bee. Your objective is to fulfill the user needs. Your answers must be concise. prompt = modified.render(objective="fulfill the user needs") print(prompt) if __name__ == "__main__": try: main() except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` This example shows how to create a new template based on an existing one. *Source: [python/examples/templates/forking.py](https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/templates/forking.py)* ### Default values Provide default values for template variables that can be overridden at runtime. *** ## Using templates with agents The framework's agents use specialized templates to structure their behavior. You can customize these templates to alter how agents operate: ```py Python [expandable] theme={null} import sys import traceback from beeai_framework.agents.react.runners.default.prompts import ( SystemPromptTemplate, ToolDefinition, ) from beeai_framework.errors import FrameworkError from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.utils.strings import to_json def main() -> None: tool = OpenMeteoTool() tool_def = ToolDefinition( name=tool.name, description=tool.description, input_schema=to_json(tool.input_schema.model_json_schema()), ) # Render the granite system prompt prompt = SystemPromptTemplate.render( instructions="You are a helpful AI assistant!", tools=[tool_def], tools_length=1 ) print(prompt) if __name__ == "__main__": try: main() except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` This example demonstrates how to create a system prompt for an agent with tool definitions, which enables the agent to use external tools like weather data retrieval. *Source: [python/examples/templates/system\_prompt.py](https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/templates/system_prompt.py)* *** ## Examples Explore reference template implementations in Python Explore reference template implementations in TypeScript # Tools Source: https://framework.beeai.dev/modules/tools ## Overview Tools extend agent capabilities beyond text processing, enabling interaction with external systems and data sources. They act as specialized modules that extend the agent's abilities, allowing it to interact with external systems, access information, and execute actions in response to user queries. Supported in Python and TypeScript. Ready-to-use tools that provide immediate functionality for common agent tasks: | Tool | Description | | :---------------------------------------- | :------------------------------------------------------------------------------------------------- | | [MCP](#mcp) | Discover and use tools exposed by arbitrary [MCP Server](https://modelcontextprotocol.io/examples) | | [Think](#think) | Gives an agent a place to think | | [Handoff](#handoff) | Delegates a task to an expert agent | | [OpenAPI](#openapi) | Consume external APIs with an ease | | [OpenMeteo](#openmeteo) | Retrieve weather information for specific locations and dates | | [DuckDuckGo](#duckduckgo) | Search for data on DuckDuckGo | | [Wikipedia](#wikipedia) | Search for data on Wikipedia | | [VectorStoreSearch](#vector-store-search) | Search for documents in a vector database | | [Python](#python) | Let agent run an arbitrary Python code in a sandboxed environment | | [Sandbox](#sandbox) | Run custom Python functions in a sandboxed environment | ➕ [Request additional built-in tools](https://github.com/i-am-bee/beeai-framework/discussions) Would you like to use a tool from LangChain? See the LangChain tool example in [Python](https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/tools/langchain_tool.py). or [TypeScript](https://github.com/i-am-bee/beeai-framework/blob/main/typescript/examples/tools/langchain.ts). ## Usage ### Basic usage The simplest way to use a tool is to instantiate it directly and call its `run()` method with appropriate input: ```py Python [expandable] theme={null} import asyncio import datetime import sys from beeai_framework.errors import FrameworkError from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools.weather import OpenMeteoTool, OpenMeteoToolInput async def main() -> None: tool = OpenMeteoTool() result = await tool.run( input=OpenMeteoToolInput( location_name="New York", start_date=datetime.date.today(), end_date=datetime.date.today() ) ).middleware(GlobalTrajectoryMiddleware()) print(result.get_text_content()) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; const tool = new OpenMeteoTool(); const today = new Date().toISOString().split("T")[0]; const result = await tool.run({ location: { name: "New York" }, start_date: today, end_date: today, }); console.log(result.getTextContent()); ``` ### Advanced usage Tools often support additional configuration options to customize their behavior: ```py Python [expandable] theme={null} import asyncio import datetime import sys import traceback from beeai_framework.errors import FrameworkError from beeai_framework.tools.weather import OpenMeteoTool, OpenMeteoToolInput async def main() -> None: tool = OpenMeteoTool() result = await tool.run( input=OpenMeteoToolInput( location_name="New York", start_date=datetime.date.today(), end_date=datetime.date.today(), temperature_unit="celsius", ) ) print(result.get_text_content()) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; import { UnconstrainedCache } from "beeai-framework/cache/unconstrainedCache"; const tool = new OpenMeteoTool({ cache: new UnconstrainedCache(), retryOptions: { maxRetries: 3, }, }); console.log(tool.name); // OpenMeteo console.log(tool.description); // Retrieve current, past, or future weather forecasts for a location. console.log(tool.inputSchema()); // (zod/json schema) await tool.cache.clear(); const today = new Date().toISOString().split("T")[0]; const result = await tool.run({ location: { name: "New York" }, start_date: today, end_date: today, temperature_unit: "celsius", }); console.log(result.isEmpty()); // false console.log(result.result); // prints raw data console.log(result.getTextContent()); // prints data as text ``` ### Using tools with agents The true power of tools emerges when integrating them with agents. Tools extend the agent's capabilities, allowing it to perform actions beyond text generation: ```py Python [expandable] theme={null} from beeai_framework.adapters.ollama import OllamaChatModel from beeai_framework.agents.react import ReActAgent from beeai_framework.memory import UnconstrainedMemory from beeai_framework.tools.weather import OpenMeteoTool agent = ReActAgent(llm=OllamaChatModel("llama3.1"), tools=[OpenMeteoTool()], memory=UnconstrainedMemory()) ``` ```ts TypeScript [expandable] theme={null} import { ArXivTool } from "beeai-framework/tools/arxiv"; import { ReActAgent } from "beeai-framework/agents/react/agent"; import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; const agent = new ReActAgent({ llm: new OllamaChatModel("granite4:micro"), memory: new UnconstrainedMemory(), tools: [new ArXivTool()], }); ``` Creating a tool For simpler tools, you can use the `tool` decorator to quickly create a tool from a function: ```py Python [expandable] theme={null} import asyncio import json import sys import traceback from urllib.parse import quote import requests from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.errors import FrameworkError from beeai_framework.logger import Logger from beeai_framework.memory import UnconstrainedMemory from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools import StringToolOutput, tool logger = Logger(__name__) # defining a tool using the `tool` decorator @tool def basic_calculator(expression: str) -> StringToolOutput: """ A calculator tool that performs mathematical operations. Args: expression: The mathematical expression to evaluate (e.g., "2 + 3 * 4"). Returns: The result of the mathematical expression """ try: encoded_expression = quote(expression) math_url = f"https://newton.vercel.app/api/v2/simplify/{encoded_expression}" response = requests.get( math_url, headers={"Accept": "application/json"}, ) response.raise_for_status() return StringToolOutput(json.dumps(response.json())) except Exception as e: raise RuntimeError(f"Error evaluating expression: {e!s}") from Exception async def main() -> None: # using the tool in an agent chat_model = ChatModel.from_name("ollama:granite4:micro") agent = RequirementAgent(llm=chat_model, tools=[basic_calculator], memory=UnconstrainedMemory()) result = await agent.run("What is the square root of 36?", total_max_retries=10).middleware( GlobalTrajectoryMiddleware() ) print(result.last_message.text) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ## Built-in Tools DuckDuckGo search tool Use the DuckDuckGo search tool to retrieve real-time search results from across the internet, including news, current events, or content from specific websites or domains. ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.errors import FrameworkError from beeai_framework.memory import UnconstrainedMemory from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool async def main() -> None: chat_model = ChatModel.from_name("ollama:granite4:micro") agent = RequirementAgent(llm=chat_model, tools=[DuckDuckGoSearchTool()], memory=UnconstrainedMemory()) result = await agent.run("How tall is the mount Everest?") print(result.last_message.text) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { z } from "zod"; import { DuckDuckGoSearchTool, DuckDuckGoSearchToolSearchType as SafeSearchType, } from "beeai-framework/tools/search/duckDuckGoSearch"; const searchTool = new DuckDuckGoSearchTool(); const customSearchTool = searchTool.extend( z.object({ query: z.string(), safeSearch: z.boolean().default(true), }), (input, options) => { if (!options.search) { options.search = {}; } options.search.safeSearch = input.safeSearch ? SafeSearchType.STRICT : SafeSearchType.OFF; return { query: input.query }; }, ); const response = await customSearchTool.run( { query: "News in the world!", safeSearch: true, }, { signal: AbortSignal.timeout(10_000), }, ); console.info(response); ``` OpenMeteo weather tool Use the OpenMeteo tool to retrieve real-time weather forecasts including detailed information on temperature, wind speed, and precipitation. Access forecasts predicting weather up to 16 days in the future and archived forecasts for weather up to 30 days in the past. Ideal for obtaining up-to-date weather predictions and recent historical weather trends. ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.agents.react import ReActAgent from beeai_framework.backend import ChatModel from beeai_framework.errors import FrameworkError from beeai_framework.memory import UnconstrainedMemory from beeai_framework.tools.weather import OpenMeteoTool async def main() -> None: llm = ChatModel.from_name("ollama:llama3.1") agent = ReActAgent(llm=llm, tools=[OpenMeteoTool()], memory=UnconstrainedMemory()) result = await agent.run("What's the current weather in London?") print(result.last_message.text) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; const tool = new OpenMeteoTool(); const today = new Date().toISOString().split("T")[0]; const result = await tool.run({ location: { name: "New York" }, start_date: today, end_date: today, }); console.log(result.getTextContent()); ``` Wikipedia tool Use the Wikipedia tool to retrieve detailed information from Wikipedia.org on a wide range of topics, including famous individuals, locations, organizations, and historical events. Ideal for obtaining comprehensive overviews or specific details on well-documented subjects. May not be suitable for lesser-known or more recent topics. The information is subject to community edits which can be inaccurate. ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.errors import FrameworkError from beeai_framework.tools.search.wikipedia import ( WikipediaTool, WikipediaToolInput, ) async def main() -> None: wikipedia_client = WikipediaTool({"full_text": True}) tool_input = WikipediaToolInput(query="bee") result = await wikipedia_client.run(tool_input) print(result.get_text_content()) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { WikipediaTool } from "beeai-framework/tools/search/wikipedia"; import { SimilarityTool } from "beeai-framework/tools/similarity"; import { splitString } from "beeai-framework/internals/helpers/string"; import { z } from "zod"; const wikipedia = new WikipediaTool(); const similarity = new SimilarityTool({ maxResults: 5, provider: async (input) => input.documents.map((document) => ({ score: document.text .toLowerCase() .split(" ") .reduce((acc, word) => acc + (input.query.toLowerCase().includes(word) ? 1 : 0), 0), })), }); const wikipediaWithSimilarity = wikipedia .extend( z.object({ page: z.string().describe("Wikipedia page"), query: z.string().describe("Search query"), }), (newInput) => ({ query: newInput.page }), ) .pipe(similarity, (input, output) => ({ query: input.query, documents: output.results.flatMap((document) => Array.from(splitString(document.fields.markdown ?? "", { size: 1000, overlap: 50 })).map( (chunk) => ({ text: chunk, source: document, }), ), ), })); const response = await wikipediaWithSimilarity.run({ page: "JavaScript", query: "engine", }); console.info(response); ``` Vector Store Search Tool Use the VectorStoreSearchTool to perform semantic search against pre-populated vector stores. This tool enables agents to retrieve relevant documents from knowledge bases using semantic similarity, making it ideal for RAG (Retrieval-Augmented Generation) applications and knowledge-based question answering. This tool requires a pre-populated vector store. Vector store population (loading and chunking documents) is typically handled offline in production applications. ```py Python [expandable] theme={null} import asyncio import os from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.backend.document_loader import DocumentLoader from beeai_framework.backend.embedding import EmbeddingModel from beeai_framework.backend.text_splitter import TextSplitter from beeai_framework.backend.vector_store import VectorStore from beeai_framework.memory import UnconstrainedMemory from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools import Tool from beeai_framework.tools.search.retrieval import VectorStoreSearchTool POPULATE_VECTOR_DB = True VECTOR_DB_PATH_4_DUMP = "" # Set this path for persistency async def setup_vector_store() -> VectorStore | None: """ Setup vector store with BeeAI framework documentation. """ embedding_model = EmbeddingModel.from_name("watsonx:ibm/slate-125m-english-rtrvr-v2", truncate_input_tokens=500) # Load existing vector store if available # pyrefly: ignore [redundant-condition] if VECTOR_DB_PATH_4_DUMP and os.path.exists(VECTOR_DB_PATH_4_DUMP): print(f"Loading vector store from: {VECTOR_DB_PATH_4_DUMP}") from beeai_framework.adapters.beeai.backend.vector_store import TemporalVectorStore preloaded_vector_store: VectorStore = TemporalVectorStore.load( path=VECTOR_DB_PATH_4_DUMP, embedding_model=embedding_model ) return preloaded_vector_store # Create new vector store if population is enabled # NOTE: Vector store population is typically done offline in production applications if POPULATE_VECTOR_DB: # Load documentation about BeeAI agents - this serves as our knowledge base # for answering questions about the different types of agents available loader = DocumentLoader.from_name( name="langchain:UnstructuredMarkdownLoader", file_path="docs/modules/agents.mdx" ) try: documents = await loader.load() except Exception as e: print(f"Failed to load documents: {e}") return None # Split documents into chunks text_splitter = TextSplitter.from_name( name="langchain:RecursiveCharacterTextSplitter", chunk_size=1000, chunk_overlap=200 ) documents = await text_splitter.split_documents(documents) print(f"Loaded {len(documents)} document chunks") # Create vector store and add documents vector_store = VectorStore.from_name(name="beeai:TemporalVectorStore", embedding_model=embedding_model) await vector_store.add_documents(documents=documents) print("Vector store populated with documents") return vector_store return None async def main() -> None: """ Example demonstrating RequirementAgent using VectorStoreSearchTool. The agent will use the vector store search tool to find relevant information about BeeAI framework agents and provide comprehensive answers. Note: In typical applications, you would use a pre-populated vector store rather than populating it at runtime. This example includes population logic for demonstration purposes only. """ # Setup vector store with BeeAI documentation vector_store = await setup_vector_store() if vector_store is None: raise FileNotFoundError( "Failed to instantiate Vector Store. " "Either set POPULATE_VECTOR_DB=True to create a new one, or ensure the database file exists." ) # Create the vector store search tool search_tool = VectorStoreSearchTool(vector_store=vector_store) # Alternative: Create search tool using dynamic loading # embedding_model = EmbeddingModel.from_name("watsonx:ibm/slate-125m-english-rtrvr-v2", truncate_input_tokens=500) # search_tool = VectorStoreSearchTool.from_name( # name="beeai:TemporalVectorStore", # embedding_model=embedding_model # ) # Create RequirementAgent with the vector store search tool llm = ChatModel.from_name("ollama:llama3.1:8b") agent = RequirementAgent( llm=llm, memory=UnconstrainedMemory(), instructions=( "You are a helpful assistant that answers questions about the BeeAI framework. " "Use the vector store search tool to find relevant information from the documentation " "before providing your answer. Always search for information first, then provide a " "comprehensive response based on what you found." ), tools=[search_tool], # Log all tool calls to the console for easier debugging middlewares=[GlobalTrajectoryMiddleware(included=[Tool])], ) query = "What types of agents are available in BeeAI?" response = await agent.run(query) print(f"query: {query}\nResponse: {response.last_message.text}") if __name__ == "__main__": asyncio.run(main()) ``` MCP Tool Leverage the Model Context Protocol (MCP) to define, initialize, and utilize tools on compatible MCP servers. These servers expose executable functionalities, enabling AI models to perform tasks such as computations, API calls, or system operations. ```py Python [expandable] theme={null} import asyncio import os from mcp import StdioServerParameters, stdio_client from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools import Tool from beeai_framework.tools.mcp import MCPTool server_params = StdioServerParameters( command="npx", args=["-y", "@modelcontextprotocol/server-filesystem", os.getcwd()] ) async def main() -> None: """Using a local MCP file server to explore the file system.""" client = stdio_client(server_params) mcp_tools = await MCPTool.from_client(client) agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite4:micro"), tools=mcp_tools, middlewares=[GlobalTrajectoryMiddleware(included=[Tool, ChatModel])], ) prompt = "What's the current working directory?" print(f"User: {prompt}") response = await agent.run(prompt) print(f"Agent: {response.last_message.text}") if __name__ == "__main__": asyncio.run(main()) ``` ```ts TypeScript [expandable] theme={null} import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { MCPTool } from "beeai-framework/tools/mcp"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { ReActAgent } from "beeai-framework/agents/react/agent"; import { UnconstrainedMemory } from "beeai-framework/memory/unconstrainedMemory"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; // Create MCP Client const client = new Client( { name: "test-client", version: "1.0.0", }, { capabilities: {}, }, ); // Connect the client to any MCP server with tools capablity await client.connect( new StdioClientTransport({ command: "npx", args: ["-y", "@modelcontextprotocol/server-everything"], }), ); try { // Server usually supports several tools, use the factory for automatic discovery const tools = await MCPTool.fromClient(client); const agent = new ReActAgent({ llm: new OllamaChatModel("granite4:micro"), memory: new UnconstrainedMemory(), tools, }); // @modelcontextprotocol/server-everything contains "add" tool await agent.run({ prompt: "Find out how much is 4 + 7" }).observe((emitter) => { emitter.on("update", async ({ data, update, meta }) => { console.log(`Agent (${update.key}) 🤖 : `, update.value); }); }); } finally { // Close the MCP connection await client.close(); } ``` ```py Python [expandable] theme={null} import asyncio from mcp.client.streamable_http import streamablehttp_client from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools import Tool from beeai_framework.tools.mcp import MCPTool async def main() -> None: """Using BeeAI Framework Documentation MCP Server to learn more.""" client = streamablehttp_client("https://framework.beeai.dev/mcp") all_tools = await MCPTool.from_client(client) agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite4:micro"), tools=all_tools, middlewares=[GlobalTrajectoryMiddleware(included=[Tool, ChatModel])], ) prompt = "Why to use Requirement Agent?" print(f"User: {prompt}") response = await agent.run(prompt) print(f"Agent: {response.last_message.text}") if __name__ == "__main__": asyncio.run(main()) ``` ```py Python [expandable] theme={null} import asyncio from mcp.client.sse import sse_client from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.backend import ChatModel from beeai_framework.middleware.trajectory import GlobalTrajectoryMiddleware from beeai_framework.tools import Tool from beeai_framework.tools.mcp import MCPTool async def main() -> None: """Using IBM Cloud MCP Server with BeeAI Framework. ibmcloud login -r us-south --sso ibmcloud --mcp-transport http://127.0.0.1:7777 """ all_ibm_tools = await MCPTool.from_client(sse_client("http://127.0.0.1:7777/sse")) ibm_tools = [tool for tool in all_ibm_tools if tool.name in {"ibmcloud_account_show"}] agent = RequirementAgent( llm=ChatModel.from_name("ollama:granite3.3:8b"), tools=[*ibm_tools], instructions="Specify JSON as an output format for the tool calls if possible.", middlewares=[GlobalTrajectoryMiddleware(included=[Tool, ChatModel])], ) prompt = "Who is the owner of my IBM account?" print(f"User: {prompt}") response = await agent.run(prompt) print(f"Agent: {response.last_message.text}") if __name__ == "__main__": asyncio.run(main()) ``` Check out the [MCP Slack integration tutorial](/guides/mcp-slackbot) Currently, the client does not support Roots, Sampling, or Elicitation. Feel free to open a GitHub issue if needed. OpenAPI Tool Many APIs are described using the OpenAPI Specification, and a framework can read this specification to generate a set of tools that allow an agent to communicate with the API. ```py Python [expandable] theme={null} import asyncio import json import os import sys import traceback from aiofiles import open from beeai_framework.agents.requirement import RequirementAgent from beeai_framework.errors import FrameworkError from beeai_framework.tools.openapi import OpenAPITool async def main() -> None: # Retrieve the schema current_dir = os.path.dirname(__file__) # pyrefly: ignore [bad-context-manager] async with open(f"{current_dir}/assets/github_openapi.json") as file: content = await file.read() open_api_schema = json.loads(content) # Create a tool for each operation in the schema tools = OpenAPITool.from_schema(open_api_schema) print(f"Retrieved {len(tools)} tools") print("\n".join([t.name for t in tools])) # Create an agent agent = RequirementAgent(llm="ollama:granite4:micro", tools=tools) # Run the agent prompt = "How many repositories are in 'i-am-bee' org?" print("User:", prompt) response = await agent.run(prompt) print("Agent 🤖 : ", response.last_message.text) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config.js"; import { ReActAgent } from "beeai-framework/agents/react/agent"; import { TokenMemory } from "beeai-framework/memory/tokenMemory"; import { OpenAPITool } from "beeai-framework/tools/openapi"; import * as fs from "fs"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { ChatModel } from "beeai-framework/backend/chat"; const llm = await ChatModel.fromName("ollama:granite4:micro"); const __dirname = dirname(fileURLToPath(import.meta.url)); const openApiSchema = await fs.promises.readFile( `${__dirname}/assets/github_openapi.json`, "utf-8", ); const agent = new ReActAgent({ llm, memory: new TokenMemory(), tools: [new OpenAPITool({ openApiSchema })], }); const response = await agent .run({ prompt: 'How many repositories are in "i-am-bee" org?' }) .observe((emitter) => { emitter.on("update", async ({ data, update, meta }) => { console.log(`Agent (${update.key}) 🤖 : `, update.value); }); }); console.log(`Agent 🤖 : `, response.result.text); ``` Python Tool The Python tool allows AI agents to execute Python code within a secure, sandboxed environment. This tool enables access to files that are either provided by the user or created during execution. This enables agents to: * Perform calculations and data analysis * Create and modify files * Process and transform user data * Generate visualizations and reports * And more This tool requires [beeai-code-interpreter](https://github.com/i-am-bee/bee-code-interpreter) to use. Get started quickly with the BeeAI Framework starter template for [Python](https://github.com/i-am-bee/beeai-framework-py-starter) or [TypeScript](https://github.com/i-am-bee/beeai-framework-py-starter). Key components: * `LocalPythonStorage` – Handles where Python code is stored and run. * `local_working_dir` – A temporary folder where the code is saved before running. * `interpreter_working_dir` – The folder where the code actually runs, set by the `CODE_INTERPRETER_TMPDIR` setting. * `PythonTool` – Connects to an external Python interpreter to run code. * `code_interpreter_url` – The web address where the code gets executed (default: `http://127.0.0.1:50081`). * `storage` -- Controls where the code is stored. By default, it saves files locally using `LocalPythonStorage`. You can set up a different storage option, like cloud storage, if needed. ```py Python [expandable] theme={null} import asyncio import os import sys import tempfile import traceback from dotenv import load_dotenv from beeai_framework.adapters.ollama import OllamaChatModel from beeai_framework.agents.react import ReActAgent from beeai_framework.errors import FrameworkError from beeai_framework.memory import UnconstrainedMemory from beeai_framework.tools.code import LocalPythonStorage, PythonTool # Load environment variables load_dotenv() async def main() -> None: llm = OllamaChatModel("llama3.1") storage = LocalPythonStorage( local_working_dir=tempfile.mkdtemp("code_interpreter_source"), # CODE_INTERPRETER_TMPDIR should point to where code interpreter stores it's files interpreter_working_dir=os.getenv("CODE_INTERPRETER_TMPDIR", "./tmp/code_interpreter_target"), ) python_tool = PythonTool( code_interpreter_url=os.getenv("CODE_INTERPRETER_URL", "http://127.0.0.1:50081"), storage=storage, ) agent = ReActAgent(llm=llm, tools=[python_tool], memory=UnconstrainedMemory()) result = await agent.run("Calculate 5036 * 12856 and save the result to answer.txt").on( "update", lambda data, event: print(f"Agent 🤖 ({data.update.key}) : ", data.update.parsed_value) ) print(result.last_message.text) result = await agent.run("Read the content of answer.txt?").on( "update", lambda data, event: print(f"Agent 🤖 ({data.update.key}) : ", data.update.parsed_value) ) print(result.last_message.text) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config"; import { CustomTool } from "beeai-framework/tools/custom"; const customTool = await CustomTool.fromSourceCode( { // Ensure the env exists url: process.env.CODE_INTERPRETER_URL!, env: { API_URL: "https://riddles-api.vercel.app/random" }, }, `import requests import os from typing import Optional, Union, Dict def get_riddle() -> Optional[Dict[str, str]]: """ Fetches a random riddle from the Riddles API. This function retrieves a random riddle and its answer. It does not accept any input parameters. Returns: Optional[Dict[str, str]]: A dictionary containing: - 'riddle' (str): The riddle question. - 'answer' (str): The answer to the riddle. Returns None if the request fails. """ url = os.environ.get('API_URL') try: response = requests.get(url) response.raise_for_status() return response.json() except Exception as e: return None`, ); ``` Sandbox tool The Sandbox tool provides a way to define and run custom Python functions in a secure, sandboxed environment. It's ideal when you need to encapsulate specific functionality that can be called by the agent. This tool requires [beeai-code-interpreter](https://github.com/i-am-bee/bee-code-interpreter) to use. Get started quickly with [beeai-framework-py-starter](https://github.com/i-am-bee/beeai-framework-py-starter). ```py Python [expandable] theme={null} import asyncio import os import sys import traceback from dotenv import load_dotenv from beeai_framework.errors import FrameworkError from beeai_framework.tools.code import SandboxTool load_dotenv() async def main() -> None: sandbox_tool = await SandboxTool.from_source_code( url=os.getenv("CODE_INTERPRETER_URL", "http://127.0.0.1:50081"), env={"API_URL": "https://riddles-api.vercel.app/random"}, source_code=""" import requests import os from typing import Optional, Union, Dict def get_riddle() -> Optional[Dict[str, str]]: ''' Fetches a random riddle from the Riddles API. This function retrieves a random riddle and its answer. It does not accept any input parameters. Returns: Optional[Dict[str, str]]: A dictionary containing: - 'riddle' (str): The riddle question. - 'answer' (str): The answer to the riddle. Returns None if the request fails. ''' url = os.environ.get('API_URL') try: response = requests.get(url) response.raise_for_status() return response.json() except Exception as e: return None """, ) result = await sandbox_tool.run({}) print(result.get_text_content()) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config"; import { CustomTool } from "beeai-framework/tools/custom"; const customTool = await CustomTool.fromSourceCode( { // Ensure the env exists url: process.env.CODE_INTERPRETER_URL!, env: { API_URL: "https://riddles-api.vercel.app/random" }, }, `import requests import os from typing import Optional, Union, Dict def get_riddle() -> Optional[Dict[str, str]]: """ Fetches a random riddle from the Riddles API. This function retrieves a random riddle and its answer. It does not accept any input parameters. Returns: Optional[Dict[str, str]]: A dictionary containing: - 'riddle' (str): The riddle question. - 'answer' (str): The answer to the riddle. Returns None if the request fails. """ url = os.environ.get('API_URL') try: response = requests.get(url) response.raise_for_status() return response.json() except Exception as e: return None`, ); ``` Environmental variables can be overridden (or defined) in the following ways: 1. During the creation of a `SandboxTool`, either via the constructor or the factory function (`SandboxTool.from_source_code`). 2. By passing them directly as part of the options when invoking: `my_tool.run(..., env={ "MY_ENV": "MY_VALUE" })`. Only `PythonTool` can access files. *** ## Creating custom tools Custom tools allow you to build your own specialized tools to extend agent capabilities. To create a new tool, implement the base `Tool` class. The framework provides flexible options for tool creation, from simple to complex implementations. Initiate the `Tool` by passing your own handler (function) with the `name`, `description` and `input schema`. ### Basic custom tool Here's an example of a simple custom tool that provides riddles: ```py Python [expandable] theme={null} import asyncio import random import sys from typing import Any from pydantic import BaseModel, Field from beeai_framework.context import RunContext from beeai_framework.emitter import Emitter from beeai_framework.errors import FrameworkError from beeai_framework.tools import StringToolOutput, Tool, ToolRunOptions class RiddleToolInput(BaseModel): riddle_number: int = Field(description="Index of riddle to retrieve.") class RiddleTool(Tool[RiddleToolInput, ToolRunOptions, StringToolOutput]): name = "Riddle" description = "It selects a riddle to test your knowledge." input_schema = RiddleToolInput data = ( "What has hands but can't clap?", "What has a face and two hands but no arms or legs?", "What gets wetter the more it dries?", "What has to be broken before you can use it?", "What has a head, a tail, but no body?", "The more you take, the more you leave behind. What am I?", "What goes up but never comes down?", ) def __init__(self, options: dict[str, Any] | None = None) -> None: super().__init__(options) def _create_emitter(self) -> Emitter: return Emitter.root().child( namespace=["tool", "example", "riddle"], creator=self, ) async def _run( self, input: RiddleToolInput, options: ToolRunOptions | None, context: RunContext ) -> StringToolOutput: index = input.riddle_number % (len(self.data)) riddle = self.data[index] return StringToolOutput(result=riddle) async def main() -> None: tool = RiddleTool() tool_input = RiddleToolInput(riddle_number=random.randint(0, len(RiddleTool.data))) result = await tool.run(tool_input) print(result) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import "dotenv/config"; import { CustomTool } from "beeai-framework/tools/custom"; const customTool = await CustomTool.fromSourceCode( { // Ensure the env exists url: process.env.CODE_INTERPRETER_URL!, env: { API_URL: "https://riddles-api.vercel.app/random" }, }, `import requests import os from typing import Optional, Union, Dict def get_riddle() -> Optional[Dict[str, str]]: """ Fetches a random riddle from the Riddles API. This function retrieves a random riddle and its answer. It does not accept any input parameters. Returns: Optional[Dict[str, str]]: A dictionary containing: - 'riddle' (str): The riddle question. - 'answer' (str): The answer to the riddle. Returns None if the request fails. """ url = os.environ.get('API_URL') try: response = requests.get(url) response.raise_for_status() return response.json() except Exception as e: return None`, ); ``` The input schema (`inputSchema`) processing can be asynchronous when needed for more complex validation or preprocessing. For structured data responses, use `JSONToolOutput` or implement your own custom output type. ### Advanced custom tool For more complex scenarios, you can implement tools with robust input validation, error handling, and structured outputs: ```py Python [expandable] theme={null} import asyncio import sys from typing import Any import httpx from pydantic import BaseModel, Field from beeai_framework.context import RunContext from beeai_framework.emitter import Emitter from beeai_framework.errors import FrameworkError from beeai_framework.tools import JSONToolOutput, Tool, ToolError, ToolInputValidationError, ToolRunOptions class OpenLibraryToolInput(BaseModel): title: str | None = Field(description="Title of book to retrieve.", default=None) olid: str | None = Field(description="Open Library number of book to retrieve.", default=None) subjects: str | None = Field(description="Subject of a book to retrieve.", default=None) class OpenLibraryToolResult(BaseModel): preview_url: str info_url: str bib_key: str class OpenLibraryToolOutput(JSONToolOutput[OpenLibraryToolResult]): pass class OpenLibraryTool(Tool[OpenLibraryToolInput, ToolRunOptions, OpenLibraryToolOutput]): name = "OpenLibrary" description = """Provides access to a library of books with information about book titles, authors, contributors, publication dates, publisher and isbn.""" input_schema = OpenLibraryToolInput def __init__(self, options: dict[str, Any] | None = None) -> None: super().__init__(options) def _create_emitter(self) -> Emitter: return Emitter.root().child( namespace=["tool", "example", "openlibrary"], creator=self, ) # pyrefly: ignore [bad-param-name-override] async def _run( self, tool_input: OpenLibraryToolInput, options: ToolRunOptions | None, context: RunContext ) -> OpenLibraryToolOutput: key = "" value = "" input_vars = vars(tool_input) for val in input_vars: if input_vars[val] is not None: key = val value = input_vars[val] break else: raise ToolInputValidationError("All input values in OpenLibraryToolInput were empty.") from None async with httpx.AsyncClient() as client: response = await client.get( f"https://openlibrary.org/api/books?bibkeys={key}:{value}&jsmcd=data&format=json", headers={"Content-Type": "application/json", "Accept": "application/json"}, ) response.raise_for_status() result = response.json().get(f"{key}:{value}") if not result: raise ToolError(f"No book found with {key}={value}.") return OpenLibraryToolOutput(OpenLibraryToolResult.model_validate(result)) async def main() -> None: tool = OpenLibraryTool() tool_input = OpenLibraryToolInput(title="It") result = await tool.run(tool_input) print(result) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: sys.exit(e.explain()) ``` ```ts TypeScript [expandable] theme={null} import { BaseToolOptions, BaseToolRunOptions, Tool, ToolInput, JSONToolOutput, ToolError, ToolEmitter, } from "beeai-framework/tools/base"; import { z } from "zod"; import { createURLParams } from "beeai-framework/internals/fetcher"; import { GetRunContext } from "beeai-framework/context"; import { Callback, Emitter } from "beeai-framework/emitter/emitter"; type ToolOptions = BaseToolOptions & { maxResults?: number }; type ToolRunOptions = BaseToolRunOptions; export interface OpenLibraryResponse { numFound: number; start: number; numFoundExact: boolean; q: string; offset: number; docs: Record[]; } export class OpenLibraryToolOutput extends JSONToolOutput { isEmpty(): boolean { return !this.result || this.result.numFound === 0 || this.result.docs.length === 0; } } export class OpenLibraryTool extends Tool { name = "OpenLibrary"; description = "Provides access to a library of books with information about book titles, authors, contributors, publication dates, publisher and isbn."; inputSchema() { return z .object({ title: z.string(), author: z.string(), isbn: z.string(), subject: z.string(), place: z.string(), person: z.string(), publisher: z.string(), }) .partial(); } public readonly emitter: ToolEmitter< ToolInput, OpenLibraryToolOutput, { beforeFetch: Callback<{ request: { url: string; options: RequestInit } }>; afterFetch: Callback<{ data: OpenLibraryResponse }>; } > = Emitter.root.child({ namespace: ["tool", "search", "openLibrary"], creator: this, }); static { this.register(); } protected async _run( input: ToolInput, _options: Partial, run: GetRunContext, ) { const request = { url: `https://openlibrary.org?${createURLParams({ searchon: input, })}`, options: { signal: run.signal } as RequestInit, }; await run.emitter.emit("beforeFetch", { request }); const response = await fetch(request.url, request.options); if (!response.ok) { throw new ToolError( "Request to Open Library API has failed!", [new Error(await response.text())], { context: { input }, }, ); } const json: OpenLibraryResponse = await response.json(); if (this.options.maxResults) { json.docs.length = this.options.maxResults; } await run.emitter.emit("afterFetch", { data: json }); return new OpenLibraryToolOutput(json); } } ``` ### Implementation guidelines When creating custom tools, follow these key requirements: **1. Implement the `Tool` class** To create a custom tool, you need to extend the base `Tool` class and implement several required components. The output must be an implementation of the `ToolOutput` interface, such as `StringToolOutput` for text responses or `JSONToolOutput` for structured data. **2. Create a descriptive name** Your tool needs a clear, descriptive name that follows naming conventions: ```py Python [expandable] theme={null} name = "MyNewTool" ``` The name must only contain characters a-z, A-Z, 0-9, or one of - or \_. **3. Write an effective description** The description is crucial as it determines when the agent uses your tool: ```py Python [expandable] theme={null} description = "Takes X action when given Y input resulting in Z output" ``` You should experiment with different natural language descriptions to ensure the tool is used in the correct circumstances. You can also include usage tips and guidance for the agent in the description, but its advisable to keep the description succinct in order to reduce the probability of conflicting with other tools, or adversely affecting agent behavior. **4. Define a clear input schema** Create a Pydantic model that defines the expected inputs with helpful descriptions: ```py Python [expandable] theme={null} class OpenMeteoToolInput(BaseModel): location_name: str = Field(description="The name of the location to retrieve weather information.") country: str | None = Field(description="Country name.", default=None) start_date: str | None = Field( description="Start date for the weather forecast in the format YYYY-MM-DD (UTC)", default=None ) end_date: str | None = Field( description="End date for the weather forecast in the format YYYY-MM-DD (UTC)", default=None ) temperature_unit: Literal["celsius", "fahrenheit"] = Field( description="The unit to express temperature", default="celsius" ) ``` *Source: [/python/beeai\_framework/tools/weather/openmeteo.py](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/tools/weather/openmeteo.py)* The input schema is a required field used to define the format of the input to your tool. The agent will formalise the natural language input(s) it has received and structure them into the fields described in the tool's input. The input schema will be created based on the `MyNewToolInput` class. Keep your tool input schema simple and provide schema descriptions to help the agent to interpret fields. **5. Implement the `_run()` method** This method contains the core functionality of your tool, processing the input and returning the appropriate output. ```py Python [expandable] theme={null} def _run(self, input: OpenMeteoToolInput, options: Any = None) -> None: params = urlencode(self.get_params(input), doseq=True) logger.debug(f"Using OpenMeteo URL: https://api.open-meteo.com/v1/forecast?{params}") response = requests.get( f"https://api.open-meteo.com/v1/forecast?{params}", headers={"Content-Type": "application/json", "Accept": "application/json"}, ) response.raise_for_status() return StringToolOutput(json.dumps(response.json())) ``` *Source: [/python/beeai\_framework/tools/weather/openmeteo.py](https://github.com/i-am-bee/beeai-framework/blob/main/python/beeai_framework/tools/weather/openmeteo.py)* *** ## Best practices ### 1. Data minimization If your tool is providing data to the agent, try to ensure that the data is relevant and free of extraneous metatdata. Preprocessing data to improve relevance and minimize unnecessary data conserves agent memory, improving overall performance. ### 2. Provide hints If your tool encounters an error that is fixable, you can return a hint to the agent; the agent will try to reuse the tool in the context of the hint. This can improve the agent's ability to recover from errors. ### 3. Security and stability When building tools, consider that the tool is being invoked by a somewhat unpredictable third party (the agent). You should ensure that sufficient guardrails are in place to prevent adverse outcomes. *** ## Examples Explore reference tool implementations in Python Explore reference tool implementations in TypeScript # Workflows Source: https://framework.beeai.dev/modules/workflows ## Overview Workflows provide a flexible and extensible component for managing and executing structured sequences of tasks. They are particularly useful for: * 🔄 **Dynamic Execution:** Steps can direct the flow based on state or results * ✅ **Validation:** Define schemas for data consistency and type safety * 🧩 **Modularity:** Steps can be standalone or invoke nested workflows * 👁️ **Observability:** Emit events during execution to track progress or handle errors Supported in Python and TypeScript. *** ## Core Concepts ### State State is the central data structure in a workflow. It's a Pydantic model that: * Holds the data passed between steps * Provides type validation and safety * Persists throughout the workflow execution ### Steps Steps are the building blocks of a workflow. Each step is a function that: * Takes the current state as input * Can modify the state * Returns the name of the next step to execute or a special reserved value ### Transitions Transitions determine the flow of execution between steps. Each step returns either: * The name of the next step to execute * `Workflow.NEXT` - proceed to the next step in order * `Workflow.SELF` - repeat the current step * `Workflow.END` - end the workflow execution *** ## Basic Usage ### Simple Workflow The example below demonstrates a minimal workflow that processes steps in sequence. This pattern is useful for straightforward, linear processes where each step builds on the previous one. ```py Python [expandable] theme={null} import asyncio import sys import traceback from pydantic import BaseModel from beeai_framework.errors import FrameworkError from beeai_framework.workflows import Workflow async def main() -> None: # State class State(BaseModel): input: str workflow = Workflow(State) workflow.add_step("first", lambda state: print("Running first step!")) workflow.add_step("second", lambda state: print("Running second step!")) workflow.add_step("third", lambda state: print("Running third step!")) await workflow.run(State(input="Hello")) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts Typescript [expandable] theme={null} import { Workflow } from "beeai-framework/workflows/workflow"; import { z } from "zod"; const schema = z.object({ hops: z.number().default(0), }); const workflow = new Workflow({ schema }) .addStep("a", async (state) => { state.hops += 1; }) .addStep("b", () => (Math.random() > 0.5 ? Workflow.PREV : Workflow.END)); const response = await workflow.run({ hops: 0 }).observe((emitter) => { emitter.on("start", (data) => console.log(`-> start ${data.step}`)); emitter.on("error", (data) => console.log(`-> error ${data.step}`)); emitter.on("success", (data) => console.log(`-> finish ${data.step}`)); }); console.log(`Hops: ${response.result.hops}`); console.log(`-> steps`, response.steps.map((step) => step.name).join(",")); ``` *** ## Advanced Features ### Multi-Step and Nested Workflows This advanced example showcases a workflow that implements multiplication through repeated addition—demonstrating control flow, state manipulation, nesting, and conditional logic. Workflow nesting allows complex behaviors to be encapsulated as reusable components, enabling hierarchical composition of workflows. This promotes modularity, reusability, and better organization of complex agent logic. ```py Python [expandable] theme={null} import asyncio import sys import traceback from typing import Literal, TypeAlias from pydantic import BaseModel from beeai_framework.errors import FrameworkError from beeai_framework.workflows import Workflow, WorkflowReservedStepName WorkflowStep: TypeAlias = Literal["pre_process", "add_loop", "post_process"] async def main() -> None: # State class State(BaseModel): x: int y: int abs_repetitions: int | None = None result: int | None = None def pre_process(state: State) -> WorkflowStep: print("pre_process") state.abs_repetitions = abs(state.y) return "add_loop" def add_loop(state: State) -> WorkflowStep | WorkflowReservedStepName: if state.abs_repetitions and state.abs_repetitions > 0: result = (state.result if state.result is not None else 0) + state.x abs_repetitions = (state.abs_repetitions if state.abs_repetitions is not None else 0) - 1 print(f"add_loop: intermediate result {result}") state.abs_repetitions = abs_repetitions state.result = result return Workflow.SELF else: return "post_process" def post_process(state: State) -> WorkflowReservedStepName: print("post_process") if state.y < 0: result = -(state.result if state.result is not None else 0) state.result = result return Workflow.END multiplication_workflow = Workflow[State, WorkflowStep](name="MultiplicationWorkflow", schema=State) multiplication_workflow.add_step("pre_process", pre_process) multiplication_workflow.add_step("add_loop", add_loop) multiplication_workflow.add_step("post_process", post_process) response = await multiplication_workflow.run(State(x=8, y=5)) print(f"result: {response.state.result}") response = await multiplication_workflow.run(State(x=8, y=-5)) print(f"result: {response.state.result}") if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts Typescript [expandable] theme={null} import { Workflow } from "beeai-framework/workflows/workflow"; import { z } from "zod"; const schema = z.object({ threshold: z.number().min(0).max(1), counter: z.number().default(0), }); const addFlow = new Workflow({ schema }).addStep("run", async (state) => { state.counter += 1; return Math.random() > 0.5 ? Workflow.SELF : Workflow.END; }); const subtractFlow = new Workflow({ schema, }).addStep("run", async (state) => { state.counter -= 1; return Math.random() > 0.5 ? Workflow.SELF : Workflow.END; }); const workflow = new Workflow({ schema, }) .addStep("start", (state) => Math.random() > state.threshold ? "delegateAdd" : "delegateSubtract", ) .addStep("delegateAdd", addFlow.asStep({ next: Workflow.END })) .addStep("delegateSubtract", subtractFlow.asStep({ next: Workflow.END })); const response = await workflow.run({ threshold: 0.5 }).observe((emitter) => { emitter.on("start", (data, event) => console.log(`-> step ${data.step}`, event.trace?.parentRunId ? "(nested flow)" : ""), ); }); console.info(`Counter:`, response.result); ``` This workflow demonstrates several powerful concepts: * Implementing loops by returning `Workflow.SELF` * Conditional transitions between steps * Progressive state modification to accumulate results * Sign handling through state transformation * Type-safe step transitions using Literal types *** ### Multi-Agent Workflows The multi-agent workflow pattern enables the orchestration of specialized agents that collaborate to solve complex problems. Each agent focuses on a specific domain or capability, with results combined by a coordinator agent. ```py Python [expandable] theme={null} import asyncio import sys import traceback from beeai_framework.backend import ChatModel from beeai_framework.emitter import EmitterOptions from beeai_framework.errors import FrameworkError from beeai_framework.tools.search.wikipedia import WikipediaTool from beeai_framework.tools.weather import OpenMeteoTool from beeai_framework.workflows.agent import AgentWorkflow, AgentWorkflowInput from examples.helpers.io import ConsoleReader async def main() -> None: llm = ChatModel.from_name("ollama:llama3.1") workflow = AgentWorkflow(name="Smart assistant") workflow.add_agent( name="Researcher", role="A diligent researcher.", instructions="You look up and provide information about a specific topic.", tools=[WikipediaTool()], llm=llm, ) workflow.add_agent( name="WeatherForecaster", role="A weather reporter.", instructions="You provide detailed weather reports.", tools=[OpenMeteoTool()], llm=llm, ) workflow.add_agent( name="DataSynthesizer", role="A meticulous and creative data synthesizer", instructions="You can combine disparate information into a final coherent summary.", llm=llm, ) reader = ConsoleReader() reader.write("Assistant 🤖 : ", "What location do you want to learn about?") for prompt in reader: await ( workflow.run( inputs=[ AgentWorkflowInput(prompt="Provide a short history of the location.", context=prompt), AgentWorkflowInput( prompt="Provide a comprehensive weather summary for the location today.", expected_output="Essential weather details such as chance of rain, temperature and wind. Only report information that is available.", ), AgentWorkflowInput( prompt="Summarize the historical and weather data for the location.", expected_output="A paragraph that describes the history of the location, followed by the current weather conditions.", ), ] ) .on( # Event Matcher -> match agent's 'success' events lambda event: isinstance(event.creator, ChatModel) and event.name == "success", # log data to the console lambda data, event: reader.write( "->Got response from the LLM", " \n->".join([str(message.content[0].model_dump()) for message in data.value.messages]), ), EmitterOptions(match_nested=True), ) .on( "success", lambda data, event: reader.write( f"->Step '{data.step}' has been completed with the following outcome." f"\n\n{data.state.final_answer}\n\n", data.model_dump(exclude={"data"}), ), ) ) reader.write("Assistant 🤖 : ", "What location do you want to learn about?") if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts Typescript [expandable] theme={null} import "dotenv/config"; import { createConsoleReader } from "examples/helpers/io.js"; import { OpenMeteoTool } from "beeai-framework/tools/weather/openMeteo"; import { WikipediaTool } from "beeai-framework/tools/search/wikipedia"; import { AgentWorkflow } from "beeai-framework/workflows/agent"; import { OllamaChatModel } from "beeai-framework/adapters/ollama/backend/chat"; const workflow = new AgentWorkflow("Smart assistant"); const llm = new OllamaChatModel("granite4:micro"); workflow.addAgent({ name: "Researcher", role: "A diligent researcher", instructions: "You look up and provide information about a specific topic.", tools: [new WikipediaTool()], llm, }); workflow.addAgent({ name: "WeatherForecaster", role: "A weather reporter", instructions: "You provide detailed weather reports.", tools: [new OpenMeteoTool()], llm, }); workflow.addAgent({ name: "DataSynthesizer", role: "A meticulous and creative data synthesizer", instructions: "You can combine disparate information into a final coherent summary.", llm, }); const reader = createConsoleReader(); reader.write("Assistant 🤖 : ", "What location do you want to learn about?"); for await (const { prompt } of reader) { const { result } = await workflow .run([ { prompt: "Provide a short history of the location.", context: prompt }, { prompt: "Provide a comprehensive weather summary for the location today.", expectedOutput: "Essential weather details such as chance of rain, temperature and wind. Only report information that is available.", }, { prompt: "Summarize the historical and weather data for the location.", expectedOutput: "A paragraph that describes the history of the location, followed by the current weather conditions.", }, ]) .observe((emitter) => { emitter.on("success", (data) => { reader.write( `Step '${data.step}' has been completed with the following outcome:\n`, data.state?.finalAnswer ?? "-", ); }); }); reader.write(`Assistant 🤖`, result.finalAnswer); reader.write("Assistant 🤖 : ", "What location do you want to learn about?"); } ``` This pattern demonstrates: * Role specialization through focused agent configuration * Efficient tool distribution to relevant specialists * Parallel processing of different aspects of a query * Synthesis of multiple expert perspectives into a cohesive response See the [events documentation](/modules/events) for more information on standard emitter events. ### Memory in Workflows Integrating memory into workflows allows agents to maintain context across interactions, enabling conversational interfaces and stateful processing. This example demonstrates a simple conversational echo workflow with persistent memory. ```py Python [expandable] theme={null} import asyncio import sys import traceback from pydantic import BaseModel, InstanceOf from beeai_framework.backend import AssistantMessage, UserMessage from beeai_framework.errors import FrameworkError from beeai_framework.memory import UnconstrainedMemory from beeai_framework.workflows import Workflow from examples.helpers.io import ConsoleReader async def main() -> None: # State with memory class State(BaseModel): memory: InstanceOf[UnconstrainedMemory] output: str = "" async def echo(state: State) -> str: # Get the last message in memory last_message = state.memory.messages[-1] state.output = last_message.text[::-1] return Workflow.END reader = ConsoleReader() memory = UnconstrainedMemory() workflow = Workflow(State) workflow.add_step("echo", echo) for prompt in reader: # Add user message to memory await memory.add(UserMessage(content=prompt)) # Run workflow with memory response = await workflow.run(State(memory=memory)) # Add assistant response to memory await memory.add(AssistantMessage(content=response.state.output)) reader.write("Assistant 🤖 : ", response.state.output) if __name__ == "__main__": try: asyncio.run(main()) except FrameworkError as e: traceback.print_exc() sys.exit(e.explain()) ``` ```ts Typescript [expandable] theme={null} Example coming soon ``` This pattern demonstrates: * Integration of memory as a first-class citizen in workflow state * Conversation loops that preserve context across interactions * Bidirectional memory updating (reading recent messages, storing responses) * Clean separation between the persistent memory and workflow-specific state *** ## Examples Explore reference workflow implementations in Python Explore reference workflow implementations in TypeScript