- Overview
- Implementation in BeeAI Framework
- Customizing Agent Behavior
- Creating Your Own Agent
- Agent with Memory
- Agent Workflows
- Examples
AI agents built on large language models (LLMs) provide a structured approach to solving complex problems. Unlike simple LLM interactions, agents can:
- 🔄 Execute multi-step reasoning processes
- 🛠️ Utilize tools to interact with external systems
- 📝 Remember context from previous interactions
- 🔍 Plan and revise their approach based on feedback
Agents control the path to solving a problem, acting on feedback to refine their plan, a capability that improves performance and helps them accomplish sophisticated tasks.
Tip
For a deeper understanding of AI agents, read this research article on AI agents and LLMs.
Note
Location within the framework: beeai_framework/agents.
BeeAI framework provides a robust implementation of the ReAct
pattern (Reasoning and Acting), which follows this general flow:
Thought → Action → Observation → Thought → ...
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:
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:
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.
Note
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.
For practical examples, see:
- simple.py - Basic example of a Bee Agent using OpenMeteo and DuckDuckGo tools
- bee.py - More complete example using Wikipedia integration
- granite.py - Example using the Granite model
You can customize your agent's behavior in five ways:
Control how the agent runs by configuring retries, timeouts, and iteration limits.
response = await agent.run(
prompt=prompt,
execution=AgentExecutionConfig(max_retries_per_step=3, total_max_retries=10, max_iterations=20),
).observe(observer)
Source: examples/agents/bee.py
Tip
The default is zero retries and no timeout. For complex tasks, increasing the max_iterations is recommended.
Customize how the agent formats prompts, including the system prompt that defines its behavior.
import sys
import traceback
from beeai_framework.agents.runners.default.prompts import (
SystemPromptTemplate,
ToolDefinition,
)
from beeai_framework.errors import FrameworkError
from beeai_framework.tools.weather.openmeteo 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())
Source: examples/templates/system_prompt.py
The agent uses several templates that you can override:
- System Prompt - Defines the agent's behavior and capabilities
- User Prompt - Formats the user's input
- Tool Error - Handles tool execution errors
- Tool Input Error - Handles validation errors
- Tool No Result Error - Handles empty results
- Tool Not Found Error - Handles references to missing tools
- Invalid Schema Error - Handles parsing errors
Enhance your agent's capabilities by providing it with tools to interact with external systems.
agent = BeeAgent(
llm=llm,
tools=[DuckDuckGoSearchTool(), OpenMeteoTool()],
memory=UnconstrainedMemory()
)
Source: examples/agents/simple.py
Available tools include:
- Search tools (
DuckDuckGoSearchTool
) - Weather tools (
OpenMeteoTool
) - Knowledge tools (
LangChainWikipediaTool
) - And many more in the
beeai_framework.tools
module
Tip
See the tools documentation for more information on available tools and creating custom tools.
Memory allows your agent to maintain context across multiple interactions.
agent = BeeAgent(
llm=llm,
tools=[DuckDuckGoSearchTool(), OpenMeteoTool()],
memory=UnconstrainedMemory()
)
Source: examples/agents/simple.py
Memory types for different use cases:
- UnconstrainedMemory - For unlimited storage
- SlidingMemory - For keeping only the most recent messages
- TokenMemory - For managing token limits
- SummarizeMemory - For summarizing previous conversations
Tip
See the memory documentation for more information on memory types.
Monitor the agent's execution by observing events it emits. This allows you to track its reasoning process, handle errors, or implement custom logging.
def update_callback(data: dict, event: EventMeta) -> None:
print(f"Agent({data['update']['key']}) 🤖 : ", data['update']['parsedValue'])
def on_update(emitter: Emitter) -> None:
emitter.on("update", update_callback)
output: BeeRunOutput = await agent.run("What's the current weather in Las Vegas?").observe(on_update)
Source: examples/agents/simple.py
Tip
See the emitter documentation for more information on event observation.
To create your own agent, you must implement the agent's base class (BaseAgent
).
import asyncio
import sys
import traceback
from pydantic import BaseModel, Field, InstanceOf
from beeai_framework import (
AssistantMessage,
BaseAgent,
BaseMemory,
Message,
SystemMessage,
UnconstrainedMemory,
UserMessage,
)
from beeai_framework.adapters.ollama.backend.chat import OllamaChatModel
from beeai_framework.agents.types import AgentMeta, BeeRunInput, BeeRunOptions
from beeai_framework.backend.chat import ChatModel
from beeai_framework.context import RunContext
from beeai_framework.emitter import Emitter
from beeai_framework.errors import FrameworkError
from beeai_framework.utils.models import ModelLike, to_model, to_model_optional
class State(BaseModel):
thought: str
final_answer: str
class RunOutput(BaseModel):
message: InstanceOf[Message]
state: State
class RunOptions(BeeRunOptions):
max_retries: int | None = None
class CustomAgent(BaseAgent[RunOutput]):
memory: BaseMemory | None = None
def __init__(self, llm: ChatModel, memory: BaseMemory) -> None:
self.model = llm
self.memory = memory
self.emitter = Emitter.root().child(
namespace=["agent", "custom"],
creator=self,
)
async def _run(
self, run_input: ModelLike[BeeRunInput], options: ModelLike[BeeRunOptions] | None, context: RunContext
) -> RunOutput:
run_input = to_model(BeeRunInput, run_input)
options = to_model_optional(BeeRunOptions, options)
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.create_structure(
schema=CustomSchema,
messages=[
SystemMessage("You are a helpful assistant. Always use JSON format for your responses."),
*(self.memory.messages if self.memory is not None else []),
UserMessage(run_input.prompt),
],
max_retries=options.execution.total_max_retries if options and options.execution else None,
abort_signal=context.signal,
)
result = AssistantMessage(response.object["final_answer"])
await self.memory.add(result) if self.memory else None
return RunOutput(
message=result,
state=State(thought=response.object["thought"], final_answer=response.object["final_answer"]),
)
@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.1-dense:8b"),
memory=UnconstrainedMemory(),
)
response = await agent.run("Why is the sky blue?")
print(response.state)
if __name__ == "__main__":
try:
asyncio.run(main())
except FrameworkError as e:
traceback.print_exc()
sys.exit(e.explain())
Agents can be configured to use memory to maintain conversation context and state.
import asyncio
import sys
import traceback
from beeai_framework.agents.bee.agent import BeeAgent
from beeai_framework.agents.types import AgentExecutionConfig
from beeai_framework.backend.chat import ChatModel
from beeai_framework.backend.message import AssistantMessage, UserMessage
from beeai_framework.errors import FrameworkError
from beeai_framework.memory.unconstrained_memory import UnconstrainedMemory
# Initialize the memory and LLM
memory = UnconstrainedMemory()
def create_agent() -> BeeAgent:
llm = ChatModel.from_name("ollama:granite3.1-dense:8b")
# Initialize the agent
agent = BeeAgent(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(
prompt=user_input,
execution=AgentExecutionConfig(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.result.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())
Source: examples/memory/agent_memory.py
Memory types for different use cases:
- UnconstrainedMemory - For unlimited storage
- SlidingMemory - For keeping only the most recent messages
- TokenMemory - For managing token limits
- SummarizeMemory - For summarizing previous conversations
For complex applications, you can create multi-agent workflows where specialized agents collaborate.
import asyncio
import sys
import traceback
from beeai_framework.agents.types import AgentExecutionConfig
from beeai_framework.backend.chat import ChatModel
from beeai_framework.backend.message import UserMessage
from beeai_framework.errors import FrameworkError
from beeai_framework.memory import UnconstrainedMemory
from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool
from beeai_framework.tools.weather.openmeteo import OpenMeteoTool
from beeai_framework.workflows.agent import AgentFactoryInput, AgentWorkflow
async def main() -> None:
llm = ChatModel.from_name("ollama:granite3.1-dense:8b")
workflow = AgentWorkflow(name="Smart assistant")
workflow.add_agent(
agent=AgentFactoryInput(
name="WeatherForecaster",
instructions="You are a weather assistant.",
tools=[OpenMeteoTool()],
llm=llm,
execution=AgentExecutionConfig(max_iterations=3, total_max_retries=10, max_retries_per_step=3),
)
)
workflow.add_agent(
agent=AgentFactoryInput(
name="Researcher",
instructions="You are a researcher assistant.",
tools=[DuckDuckGoSearchTool()],
llm=llm,
)
)
workflow.add_agent(
agent=AgentFactoryInput(
name="Solver",
instructions="""Your task is to provide the most useful final answer based on the assistants'
responses which all are relevant. Ignore those where assistant do not know.""",
llm=llm,
)
)
prompt = "What is the weather in New York?"
memory = UnconstrainedMemory()
await memory.add(UserMessage(content=prompt))
response = await workflow.run(messages=memory.messages)
print(f"result: {response.state.final_answer}")
if __name__ == "__main__":
try:
asyncio.run(main())
except FrameworkError as e:
traceback.print_exc()
sys.exit(e.explain())
Source: examples/workflows/multi_agents.py
Example Workflow Patterns:
- multi_agents.py - Multiple specialized agents working together
- memory.py - Memory-aware workflow for conversation
Tip
See the workflows documentation for more information.
- simple.py - Basic agent implementation
- bee.py - More complete implementation
- granite.py - Using Granite model
- agents.ipynb - Interactive notebook examples