chore: release 2.3.3 (#5524)

# Changelog

## New Feature:

- **Context Compression**: Introducing compression of tool call results
in a running context. Context Compression allows you to manage your
agent context while it is running, helping the agent stay within its
context window and avoid rate limits or decreases in response quality.
This is still a **beta** feature. See the
[docs](https://docs.agno.com/basics/context-compression/overview)
- **Memory Optimisation:** Added the ability to summarize a user’s
memories. This adds `optimize_memories` and `aoptimize_memories` to the
`MemoryManager` as a way to summarize memories outside of agent runs.
Only one strategy is currently supported, but this will be extended in
future. Thie is still a **beta** feature. See the
[docs](https://docs.agno.com/basics/memory/working-with-memories/memory-optimization).
- **Gemini File Search Support**: Added support for [Gemini File
Search](https://blog.google/technology/developers/file-search-gemini-api/).
- File Search Store Management: Create, list, get, and delete document
stores
- File Upload & Import: Direct upload with custom chunking configuration
and metadata
- Document Management: List, get, delete, and update documents with
metadata filtering
- Citation Extraction: Helper methods to extract and format source
citations from responses
    - Async Support: Full async/await support for production workloads
    - Docs coming soon! In the mean time check out these cookbooks:
        - `cookbook/models/google/gemini/file_search_basic.py`
        - `cookbook/models/google/gemini/file_search_advanced.py`
        - `cookbook/models/google/gemini/file_search_rag_pipeline.py`

## Improvements:

- **`output_schema` on run/arun**: override `output_schema` at runtime
via the `run()` and `arun()` methods for both `Agent` and `Team`, as
well as AgentOS API endpoints.
- **API Key for AWS Bedrock**: Added `api_key` support for AWS Bedrock.
See the
[documentation](https://docs.agno.com/integrations/models/cloud/aws-claude/overview).
- **AWS Bedrock Claude**: It is now more compatible with native Claude,
which adds support for thinking models and caching.
- **VertexAI Claude**: It is now more compatible with native Claude,
which adds support for thinking models and caching.
- **Gemini 3 Thinking Level**: Added support for `thinking_level` on
`Gemini`

## Bug Fixes:

- **Async Knowledge Retriever:** Fixed a bug where async knowledge
retrieving would break due to missing async versions of
`get_user_message` functions on Agent and Team
- **Team Member Events**: Ensure that member events are streamed if
`stream_member_events=True` but `stream_events=False` on `Team`.
- **MigrationManager Improvements:** Fixed an issue preventing
migrations from being applied for versions older than 2.3.0. Also adds
support for a force migration flag
- **StopAgentRun**: Fixed the `StopAgentRun` exception from not
correctly ending the agent run.
- **PDF Image Reader:** Fixed PDF Image Reader generating ID’s based on
filename instead of using UUID.
- **MCP `tool_name_prefix`:** Made the `tool_name_prefix` parameter
`None` by default to prevent it from adding `_` by default as a prefix.
- **AGUI:**
    - Duplicate value for `tool_call_id` fixed
    - Missing assistant messages for tool calls fixed

---------

Co-authored-by: Dirk Brand <[email protected]>
Co-authored-by: Mustafa Esoofally <[email protected]>
This commit is contained in:
Yash Pratap Solanky
2025-11-27 14:15:59 +02:00
committed by GitHub
co-authored by Dirk Brand Mustafa Esoofally
parent bc1667d1e2
commit bce27a9ef0
42 changed files with 542 additions and 619 deletions
+2 -2
View File
@@ -119,11 +119,11 @@ jobs:
set -e
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Run dev setup
- name: Run test setup
working-directory: .
run: |
set -e
./scripts/dev_setup.sh
./scripts/test_setup.sh
- name: Run tests for Agno
working-directory: .
env:
+11 -11
View File
@@ -626,7 +626,7 @@ jobs:
- name: Run dev setup
working-directory: .
run: |
./scripts/dev_setup.sh
./scripts/test_setup.sh
- name: Run agent integration tests
working-directory: .
env:
@@ -658,7 +658,7 @@ jobs:
- name: Run dev setup
working-directory: .
run: |
./scripts/dev_setup.sh
./scripts/test_setup.sh
- name: Run teams integration tests
working-directory: .
env:
@@ -688,7 +688,7 @@ jobs:
- name: Run dev setup
working-directory: .
run: |
./scripts/dev_setup.sh
./scripts/test_setup.sh
- name: Run workflows integration tests
working-directory: .
env:
@@ -719,7 +719,7 @@ jobs:
- name: Run dev setup
working-directory: .
run: |
./scripts/dev_setup.sh
./scripts/test_setup.sh
- name: Run embedder tests
working-directory: .
env:
@@ -750,7 +750,7 @@ jobs:
- name: Run dev setup
working-directory: .
run: |
./scripts/dev_setup.sh
./scripts/test_setup.sh
- name: Run knowledge integration tests
working-directory: .
env:
@@ -786,7 +786,7 @@ jobs:
- name: Run dev setup
working-directory: .
run: |
./scripts/dev_setup.sh
./scripts/test_setup.sh
- name: Run knowledge integration tests
working-directory: .
env:
@@ -886,14 +886,14 @@ jobs:
- name: Run dev setup
working-directory: .
run: |
./scripts/dev_setup.sh
./scripts/test_setup.sh
- name: Run AgentOS integration tests
working-directory: .
env:
AGNO_TELEMETRY: false
run: |
source .venv/bin/activate
python -m pytest --ignore=./libs/agno/tests/integration/os/interfaces/a2a --ignore=./libs/agno/tests/integration/os/interfaces/agui ./libs/agno/tests/integration/os
python -m pytest --ignore=./libs/agno/tests/integration/os/interfaces/test_a2a.py ./libs/agno/tests/integration/os
test-clean-os:
@@ -952,7 +952,7 @@ jobs:
- name: Run dev setup
working-directory: .
run: |
./scripts/dev_setup.sh
./scripts/test_setup.sh
- name: Run remaining integration tests
working-directory: .
env:
@@ -996,8 +996,8 @@ jobs:
- name: Verify agno_infra installation
run: |
source .venv/bin/activate
pip show agno-infra || exit 1
uv pip show agno-infra || exit 1
- name: List installed packages
run: |
source .venv/bin/activate
pip list
uv pip list
@@ -0,0 +1,36 @@
"""
Integrate Agno with Traceloop to send traces and gain insights into your agent's performance.
Prerequisites:
- Traceloop account
- Traceloop API key
Steps:
1. `pip install agno openai traceloop-sdk`
2. Set the Traceloop API key as an environment variable: `export TRACELOOP_API_KEY=<your-api-key>`
"""
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from traceloop.sdk import Traceloop
from traceloop.sdk.decorators import workflow
Traceloop.init(app_name="agno_workflows")
agent = Agent(
name="AnalysisAgent",
model=OpenAIChat(id="gpt-4o-mini"),
debug_mode=True,
)
@workflow(name="data_analysis_pipeline")
def analyze_data(query: str) -> str:
"""Custom workflow that wraps agent execution."""
response = agent.run(query)
return response.content
# The workflow decorator creates a parent span
result = analyze_data("Analyze the benefits of observability in AI systems")
print(result)
+1 -1
View File
@@ -10,7 +10,7 @@ from agno.agent import Agent
from agno.models.google import Gemini
agent = Agent(
model=Gemini(id="gemini-2.0-flash-exp", search=True),
model=Gemini(id="gemini-2.0-flash", search=True),
markdown=True,
)
+16 -35
View File
@@ -74,6 +74,8 @@ from agno.session.summary import SessionSummary
from agno.tools import Toolkit
from agno.tools.function import Function
from agno.utils.agent import (
aexecute_instructions,
aexecute_system_message,
aget_last_run_output_util,
aget_run_output_util,
aget_session_metrics_util,
@@ -87,6 +89,8 @@ from agno.utils.agent import (
collect_joint_files,
collect_joint_images,
collect_joint_videos,
execute_instructions,
execute_system_message,
get_last_run_output_util,
get_run_output_util,
get_session_metrics_util,
@@ -6907,7 +6911,9 @@ class Agent:
if isinstance(self.system_message, str):
sys_message_content = self.system_message
elif callable(self.system_message):
sys_message_content = self.system_message(agent=self)
sys_message_content = execute_system_message(
agent=self, system_message=self.system_message, session_state=session_state, run_context=run_context
)
if not isinstance(sys_message_content, str):
raise Exception("system_message must return a string")
@@ -6936,25 +6942,9 @@ class Agent:
if self.instructions is not None:
_instructions = self.instructions
if callable(self.instructions):
import inspect
signature = inspect.signature(self.instructions)
instruction_args: Dict[str, Any] = {}
# Check for agent parameter
if "agent" in signature.parameters:
instruction_args["agent"] = self
# Check for session_state parameter
if "session_state" in signature.parameters:
instruction_args["session_state"] = session_state or {}
# Check for run_context parameter
if "run_context" in signature.parameters:
instruction_args["run_context"] = run_context or None
# Run the instructions function
_instructions = self.instructions(**instruction_args)
_instructions = execute_instructions(
agent=self, instructions=self.instructions, session_state=session_state, run_context=run_context
)
if isinstance(_instructions, str):
instructions.append(_instructions)
@@ -7264,7 +7254,9 @@ class Agent:
if isinstance(self.system_message, str):
sys_message_content = self.system_message
elif callable(self.system_message):
sys_message_content = self.system_message(agent=self)
sys_message_content = await aexecute_system_message(
agent=self, system_message=self.system_message, session_state=session_state, run_context=run_context
)
if not isinstance(sys_message_content, str):
raise Exception("system_message must return a string")
@@ -7294,20 +7286,9 @@ class Agent:
if self.instructions is not None:
_instructions = self.instructions
if callable(self.instructions):
import inspect
signature = inspect.signature(self.instructions)
instruction_args: Dict[str, Any] = {}
# Check for agent parameter
if "agent" in signature.parameters:
instruction_args["agent"] = self
# Check for session_state parameter
if "session_state" in signature.parameters:
instruction_args["session_state"] = session_state or {}
_instructions = self.instructions(**instruction_args)
_instructions = await aexecute_instructions(
agent=self, instructions=self.instructions, session_state=session_state, run_context=run_context
)
if isinstance(_instructions, str):
instructions.append(_instructions)
+1 -1
View File
@@ -237,7 +237,7 @@ class MySQLDb(BaseDb):
]
for table_name, table_type in tables_to_create:
self._create_table(table_name=table_name, table_type=table_type)
self._get_or_create_table(table_name=table_name, table_type=table_type, create_table_if_not_found=True)
def _get_table(self, table_type: str, create_table_if_not_found: Optional[bool] = False) -> Optional[Table]:
if table_type == "sessions":
+1 -1
View File
@@ -138,7 +138,7 @@ class AsyncPostgresDb(AsyncBaseDb):
]
for table_name, table_type in tables_to_create:
await self._create_table(table_name=table_name, table_type=table_type)
await self._get_or_create_table(table_name=table_name, table_type=table_type)
async def _create_table(self, table_name: str, table_type: str) -> Table:
"""
+1 -1
View File
@@ -134,7 +134,7 @@ class PostgresDb(BaseDb):
]
for table_name, table_type in tables_to_create:
self._create_table(table_name=table_name, table_type=table_type)
self._get_or_create_table(table_name=table_name, table_type=table_type, create_table_if_not_found=True)
def _create_table(self, table_name: str, table_type: str) -> Table:
"""
+37 -37
View File
@@ -181,7 +181,7 @@ class SingleStoreDb(BaseDb):
]
for table_name, table_type in tables_to_create:
self._create_table(table_name=table_name, table_type=table_type)
self._get_or_create_table(table_name=table_name, table_type=table_type, create_table_if_not_found=True)
def _create_table(self, table_name: str, table_type: str) -> Table:
"""
@@ -366,42 +366,6 @@ class SingleStoreDb(BaseDb):
raise ValueError(f"Unknown table type: {table_type}")
def get_latest_schema_version(self, table_name: str) -> str:
"""Get the latest version of the database schema."""
table = self._get_table(table_type="versions", create_table_if_not_found=True)
if table is None:
return "2.0.0"
with self.Session() as sess:
stmt = select(table)
# Latest version for the given table
stmt = stmt.where(table.c.table_name == table_name)
stmt = stmt.order_by(table.c.version.desc()).limit(1)
result = sess.execute(stmt).fetchone()
if result is None:
return "2.0.0"
version_dict = dict(result._mapping)
return version_dict.get("version") or "2.0.0"
def upsert_schema_version(self, table_name: str, version: str) -> None:
"""Upsert the schema version into the database."""
table = self._get_table(table_type="versions", create_table_if_not_found=True)
if table is None:
return
current_datetime = datetime.now().isoformat()
with self.Session() as sess, sess.begin():
stmt = mysql.insert(table).values(
table_name=table_name,
version=version,
created_at=current_datetime, # Store as ISO format string
updated_at=current_datetime,
)
# Update version if table_name already exists
stmt = stmt.on_duplicate_key_update(
version=version,
updated_at=current_datetime,
)
sess.execute(stmt)
def _get_or_create_table(
self,
table_name: str,
@@ -450,6 +414,42 @@ class SingleStoreDb(BaseDb):
log_error(f"Error loading existing table {table_ref}: {e}")
raise
def get_latest_schema_version(self, table_name: str) -> str:
"""Get the latest version of the database schema."""
table = self._get_table(table_type="versions", create_table_if_not_found=True)
if table is None:
return "2.0.0"
with self.Session() as sess:
stmt = select(table)
# Latest version for the given table
stmt = stmt.where(table.c.table_name == table_name)
stmt = stmt.order_by(table.c.version.desc()).limit(1)
result = sess.execute(stmt).fetchone()
if result is None:
return "2.0.0"
version_dict = dict(result._mapping)
return version_dict.get("version") or "2.0.0"
def upsert_schema_version(self, table_name: str, version: str) -> None:
"""Upsert the schema version into the database."""
table = self._get_table(table_type="versions", create_table_if_not_found=True)
if table is None:
return
current_datetime = datetime.now().isoformat()
with self.Session() as sess, sess.begin():
stmt = mysql.insert(table).values(
table_name=table_name,
version=version,
created_at=current_datetime, # Store as ISO format string
updated_at=current_datetime,
)
# Update version if table_name already exists
stmt = stmt.on_duplicate_key_update(
version=version,
updated_at=current_datetime,
)
sess.execute(stmt)
# -- Session methods --
def delete_session(self, session_id: str) -> bool:
"""
+1 -1
View File
@@ -140,7 +140,7 @@ class AsyncSqliteDb(AsyncBaseDb):
]
for table_name, table_type in tables_to_create:
await self._create_table(table_name=table_name, table_type=table_type)
await self._get_or_create_table(table_name=table_name, table_type=table_type)
async def _create_table(self, table_name: str, table_type: str) -> Table:
"""
+1 -1
View File
@@ -141,7 +141,7 @@ class SqliteDb(BaseDb):
]
for table_name, table_type in tables_to_create:
self._create_table(table_name=table_name, table_type=table_type)
self._get_or_create_table(table_name=table_name, table_type=table_type, create_table_if_not_found=True)
def _create_table(self, table_name: str, table_type: str) -> Table:
"""
@@ -140,7 +140,6 @@ class TavilyReader(Reader):
documents.extend(self.chunk_document(Document(name=name or url, id=url, content=content)))
else:
documents.append(Document(name=name or url, id=url, content=content))
return documents
except Exception as e:
@@ -61,7 +61,7 @@ class CerebrasOpenAI(OpenAILike):
log_debug(f"Calling {self.provider} with request parameters: {request_params}", log_level=2)
return request_params
def _format_message(self, message: Message) -> Dict[str, Any]:
def _format_message(self, message: Message, compress_tool_results: bool = False) -> Dict[str, Any]:
"""
Format a message into the format expected by the Cerebras API.
@@ -71,6 +71,7 @@ class CerebrasOpenAI(OpenAILike):
Returns:
Dict[str, Any]: The formatted message.
"""
# Basic message content
message_dict: Dict[str, Any] = {
"role": message.role,
@@ -100,10 +101,11 @@ class CerebrasOpenAI(OpenAILike):
# Handle tool responses
if message.role == "tool" and message.tool_call_id:
content = message.get_content(use_compressed_content=compress_tool_results)
message_dict = {
"role": "tool",
"tool_call_id": message.tool_call_id,
"content": message.content if message.content is not None else "",
"content": content if message.content is not None else "",
}
# Ensure no None values in the message
+21 -6
View File
@@ -24,6 +24,7 @@ from agno.run.agent import RunOutput
from agno.run.team import TeamRunOutput
from agno.session import AgentSession, TeamSession, WorkflowSession
from agno.team.team import Team
from agno.utils.agent import aexecute_instructions, aexecute_system_message
from agno.workflow.agent import WorkflowAgent
from agno.workflow.workflow import Workflow
@@ -337,12 +338,20 @@ class AgentResponse(BaseModel):
"read_tool_call_history": agent.read_tool_call_history,
}
instructions = agent.instructions if agent.instructions else None
if instructions and callable(instructions):
instructions = await aexecute_instructions(instructions=instructions, agent=agent)
system_message = agent.system_message if agent.system_message else None
if system_message and callable(system_message):
system_message = await aexecute_system_message(system_message=system_message, agent=agent)
system_message_info = {
"system_message": str(agent.system_message) if agent.system_message else None,
"system_message": str(system_message) if system_message else None,
"system_message_role": agent.system_message_role,
"build_context": agent.build_context,
"description": agent.description,
"instructions": agent.instructions if agent.instructions else None,
"instructions": instructions,
"expected_output": agent.expected_output,
"additional_context": agent.additional_context,
"markdown": agent.markdown,
@@ -560,12 +569,18 @@ class TeamResponse(BaseModel):
"get_member_information_tool": team.get_member_information_tool,
}
team_instructions = (
team.instructions() if team.instructions and callable(team.instructions) else team.instructions
)
team_instructions = team.instructions if team.instructions else None
if team_instructions and callable(team_instructions):
team_instructions = await aexecute_instructions(instructions=team_instructions, agent=team, team=team)
team_system_message = team.system_message if team.system_message else None
if team_system_message and callable(team_system_message):
team_system_message = await aexecute_system_message(
system_message=team_system_message, agent=team, team=team
)
system_message_info = {
"system_message": str(team.system_message) if team.system_message else None,
"system_message": team_system_message,
"system_message_role": team.system_message_role,
"description": team.description,
"instructions": team_instructions,
+33 -22
View File
@@ -67,6 +67,8 @@ from agno.session.summary import SessionSummary
from agno.tools import Toolkit
from agno.tools.function import Function
from agno.utils.agent import (
aexecute_instructions,
aexecute_system_message,
aget_last_run_output_util,
aget_run_output_util,
aget_session_metrics_util,
@@ -80,6 +82,8 @@ from agno.utils.agent import (
collect_joint_files,
collect_joint_images,
collect_joint_videos,
execute_instructions,
execute_system_message,
get_last_run_output_util,
get_run_output_util,
get_session_metrics_util,
@@ -5465,7 +5469,13 @@ class Team:
if isinstance(self.system_message, str):
sys_message_content = self.system_message
elif callable(self.system_message):
sys_message_content = self.system_message(agent=self)
sys_message_content = execute_system_message(
system_message=self.system_message,
agent=self,
team=self,
session_state=session_state,
run_context=run_context,
)
if not isinstance(sys_message_content, str):
raise Exception("system_message must return a string")
@@ -5489,15 +5499,13 @@ class Team:
if self.instructions is not None:
_instructions = self.instructions
if callable(self.instructions):
import inspect
signature = inspect.signature(self.instructions)
if "team" in signature.parameters:
_instructions = self.instructions(team=self)
elif "agent" in signature.parameters:
_instructions = self.instructions(agent=self)
else:
_instructions = self.instructions()
_instructions = execute_instructions(
instructions=self.instructions,
agent=self,
team=self,
session_state=session_state,
run_context=run_context,
)
if isinstance(_instructions, str):
instructions.append(_instructions)
@@ -5773,7 +5781,13 @@ class Team:
if isinstance(self.system_message, str):
sys_message_content = self.system_message
elif callable(self.system_message):
sys_message_content = self.system_message(agent=self)
sys_message_content = await aexecute_system_message(
system_message=self.system_message,
agent=self,
team=self,
session_state=session_state,
run_context=run_context,
)
if not isinstance(sys_message_content, str):
raise Exception("system_message must return a string")
@@ -5797,15 +5811,13 @@ class Team:
if self.instructions is not None:
_instructions = self.instructions
if callable(self.instructions):
import inspect
signature = inspect.signature(self.instructions)
if "team" in signature.parameters:
_instructions = self.instructions(team=self)
elif "agent" in signature.parameters:
_instructions = self.instructions(agent=self)
else:
_instructions = self.instructions()
_instructions = await aexecute_instructions(
instructions=self.instructions,
agent=self,
team=self,
session_state=session_state,
run_context=run_context,
)
if isinstance(_instructions, str):
instructions.append(_instructions)
@@ -8544,8 +8556,7 @@ class Team:
session = self.get_session(session_id=session_id) # type: ignore
if session is None:
log_warning(f"Session {session_id} not found")
return []
raise Exception("Session not found")
return session.get_messages(
team_id=self.id,
+119 -1
View File
@@ -1,10 +1,11 @@
from asyncio import Future, Task
from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, Iterator, List, Optional, Sequence, Union
from typing import TYPE_CHECKING, Any, AsyncIterator, Callable, Dict, Iterator, List, Optional, Sequence, Union
from agno.media import Audio, File, Image, Video
from agno.models.message import Message
from agno.models.metrics import Metrics
from agno.models.response import ModelResponse
from agno.run import RunContext
from agno.run.agent import RunEvent, RunInput, RunOutput, RunOutputEvent
from agno.run.team import RunOutputEvent as TeamRunOutputEvent
from agno.run.team import TeamRunOutput
@@ -818,3 +819,120 @@ async def aget_chat_history_util(entity: Union["Agent", "Team"], session_id: str
raise Exception("Session not found")
return session.get_chat_history() # type: ignore
def execute_instructions(
instructions: Callable,
agent: Optional[Union["Agent", "Team"]] = None,
team: Optional["Team"] = None,
session_state: Optional[Dict[str, Any]] = None,
run_context: Optional[RunContext] = None,
) -> Union[str, List[str]]:
"""Execute the instructions function."""
import inspect
signature = inspect.signature(instructions)
instruction_args: Dict[str, Any] = {}
# Check for agent parameter
if "agent" in signature.parameters:
instruction_args["agent"] = agent
if "team" in signature.parameters:
instruction_args["team"] = team
# Check for session_state parameter
if "session_state" in signature.parameters:
instruction_args["session_state"] = session_state or {}
# Check for run_context parameter
if "run_context" in signature.parameters:
instruction_args["run_context"] = run_context or None
# Run the instructions function, await if it's awaitable, otherwise run directly (in thread)
if inspect.iscoroutinefunction(instructions):
raise Exception("Instructions function is async, use `agent.arun()` instead")
# Run the instructions function
return instructions(**instruction_args)
def execute_system_message(
system_message: Callable,
agent: Optional[Union["Agent", "Team"]] = None,
team: Optional["Team"] = None,
session_state: Optional[Dict[str, Any]] = None,
run_context: Optional[RunContext] = None,
) -> str:
"""Execute the system message function."""
import inspect
signature = inspect.signature(system_message)
system_message_args: Dict[str, Any] = {}
# Check for agent parameter
if "agent" in signature.parameters:
system_message_args["agent"] = agent
if "team" in signature.parameters:
system_message_args["team"] = team
if inspect.iscoroutinefunction(system_message):
raise ValueError("System message function is async, use `agent.arun()` instead")
return system_message(**system_message_args)
async def aexecute_instructions(
instructions: Callable,
agent: Optional[Union["Agent", "Team"]] = None,
team: Optional["Team"] = None,
session_state: Optional[Dict[str, Any]] = None,
run_context: Optional[RunContext] = None,
) -> Union[str, List[str]]:
"""Execute the instructions function."""
import inspect
signature = inspect.signature(instructions)
instruction_args: Dict[str, Any] = {}
# Check for agent parameter
if "agent" in signature.parameters:
instruction_args["agent"] = agent
if "team" in signature.parameters:
instruction_args["team"] = team
# Check for session_state parameter
if "session_state" in signature.parameters:
instruction_args["session_state"] = session_state or {}
# Check for run_context parameter
if "run_context" in signature.parameters:
instruction_args["run_context"] = run_context or None
if inspect.iscoroutinefunction(instructions):
return await instructions(**instruction_args)
else:
return instructions(**instruction_args)
async def aexecute_system_message(
system_message: Callable,
agent: Optional[Union["Agent", "Team"]] = None,
team: Optional["Team"] = None,
session_state: Optional[Dict[str, Any]] = None,
run_context: Optional[RunContext] = None,
) -> str:
import inspect
signature = inspect.signature(system_message)
system_message_args: Dict[str, Any] = {}
# Check for agent parameter
if "agent" in signature.parameters:
system_message_args["agent"] = agent
if "team" in signature.parameters:
system_message_args["team"] = team
if inspect.iscoroutinefunction(system_message):
return await system_message(**system_message_args)
else:
return system_message(**system_message_args)
+22 -19
View File
@@ -1,6 +1,6 @@
[project]
name = "agno"
version = "2.3.2"
version = "2.3.3"
description = "Agno: a lightweight library for building Multi-Agent Systems"
requires-python = ">=3.7,<4"
readme = "README.md"
@@ -47,19 +47,10 @@ dependencies = [
]
[project.optional-dependencies]
dev = ["mypy==1.18.2", "ruff==0.14.3", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "timeout-decorator", "types-pyyaml", "types-aiofiles", "fastapi", "uvicorn"]
dev = ["mypy==1.18.2", "ruff==0.14.3", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "timeout-decorator", "types-pyyaml", "types-aiofiles", "fastapi", "uvicorn", "PyJWT", "mcp", "openai"]
os = ["fastapi", "uvicorn", "PyJWT"]
# Models integration test dependencies
integration-tests = [
"exa_py",
"ddgs",
"yfinance",
"sqlalchemy",
"Pillow",
"fastmcp"
]
# Dependencies for Telemetry
opentelemetry = ["opentelemetry-sdk", "opentelemetry-exporter-otlp"]
@@ -100,15 +91,15 @@ elevenlabs = ["elevenlabs"]
evm = ["web3"]
exa = ["exa_py"]
fal = ["fal_client"]
firecrawl = ["firecrawl-py"]
firecrawl = ["firecrawl-py==3.4.0"]
tavily = ["tavily-python"]
crawl4ai= ["crawl4ai"]
crawl4ai = ["crawl4ai>=0.6.3"]
github = ["PyGithub"]
gmail = ["google-api-python-client", "google-auth-httplib2", "google-auth-oauthlib"]
google_bigquery = ["google-cloud-bigquery"]
googlemaps = ["googlemaps", "google-maps-places"]
matplotlib = ["matplotlib"]
mcp = ["mcp"]
mcp = ["mcp>=1.9.2"]
mem0 = ["mem0ai"]
memori = ["memorisdk"]
newspaper = ["newspaper4k", "lxml_html_clean"]
@@ -124,7 +115,7 @@ webex = ["webexpythonsdk"]
yfinance = ["yfinance"]
youtube = ["youtube_transcript_api"]
zep = ["zep-cloud"]
daytona=["daytona"]
daytona = ["daytona"]
oxylabs = ["oxylabs"]
trafilatura = ["trafilatura"]
neo4j = ["neo4j"]
@@ -142,7 +133,7 @@ redis = ["redis", "redisvl"]
# Dependencies for Vector databases
pgvector = ["pgvector"]
chromadb = ["chromadb"]
lancedb = ["lancedb>=0.24.0", "tantivy"]
lancedb = ["lancedb==0.24.0", "tantivy"]
pylance = ["pylance"]
qdrant = ["qdrant-client"]
couchbase = ["couchbase"]
@@ -200,7 +191,6 @@ models = [
"agno[google]",
"agno[groq]",
"agno[ibm]",
"agno[infinity]",
"agno[litellm]",
"agno[meta]",
"agno[mistral]",
@@ -213,7 +203,7 @@ models = [
tools = [
"agno[apify]",
"agno[arxiv]",
"agno[brave]",
# "agno[brave]", # It clashes with other dependencies
"agno[exa]",
"agno[cartesia]",
"agno[ddg]",
@@ -239,7 +229,7 @@ tools = [
"agno[parallel]",
"agno[scrapegraph]",
"agno[valyu]",
# "agno[yfinance]", # Broken right now
"agno[yfinance]",
"agno[confluence]",
"agno[notion]",
"agno[oxylabs]",
@@ -281,6 +271,7 @@ vectordbs = [
"agno[pinecone]",
"agno[surrealdb]",
"agno[upstash]",
"agno[pylance]",
]
# All knowledge
@@ -302,6 +293,7 @@ embedders = [
# All libraries for testing
tests = [
"agno[a2a]",
"agno[dev]",
"agno[models]",
"agno[tools]",
@@ -312,10 +304,21 @@ tests = [
"agno[performance]",
"agno[cookbooks]",
"agno[agui]",
"agno[integration-tests]",
"twine",
"build",
]
# Models integration test dependencies
integration-tests = [
"exa_py",
"ddgs",
"yfinance",
"sqlalchemy",
"Pillow",
"fastmcp"
]
[project.urls]
homepage = "https://agno.com"
documentation = "https://docs.agno.com"
@@ -0,0 +1,54 @@
import pytest
from agno.agent import Agent
from agno.team import Team
def test_callable_instructions():
def instructions(agent: Agent, team: Team) -> str:
return "You are a helpful assistant."
agent = Agent(instructions=instructions)
team = Team(instructions=instructions, members=[])
agent.run("Hello")
team.run("Hello")
assert True, "No Errors"
@pytest.mark.asyncio
async def test_async_callable_instructions():
async def instructions(agent: Agent, team: Team) -> str:
return "You are a helpful assistant."
agent = Agent(instructions=instructions)
team = Team(instructions=instructions, members=[])
await agent.arun("Hello")
await team.arun("Hello")
def test_callable_system_message():
def instructions(agent: Agent, team: Team) -> str:
return "You are a helpful assistant."
agent = Agent(system_message=instructions)
team = Team(system_message=instructions, members=[])
agent.run("Hello")
team.run("Hello")
assert True, "No Errors"
@pytest.mark.asyncio
async def test_async_callable_system_message():
async def instructions(agent: Agent, team: Team) -> str:
return "You are a helpful assistant."
agent = Agent(system_message=instructions)
team = Team(system_message=instructions, members=[])
await agent.arun("Hello")
await team.arun("Hello")
@@ -5,18 +5,6 @@ from agno.agent import Agent, RunOutput
from agno.models.openai import OpenAIChat
@pytest.fixture(autouse=True)
def reset_async_client():
"""Reset global async HTTP client between tests to avoid event loop conflicts."""
import agno.utils.http as http_utils
# Reset before test
http_utils._global_async_client = None
yield
# Reset after test
http_utils._global_async_client = None
class PersonSchema(BaseModel):
name: str = Field(..., description="Person's name")
age: int = Field(..., description="Person's age")
+12
View File
@@ -12,6 +12,18 @@ from agno.db.sqlite import AsyncSqliteDb, SqliteDb
from agno.session import Session
@pytest.fixture(autouse=True)
def reset_async_client():
"""Reset global async HTTP client between tests to avoid event loop conflicts."""
import agno.utils.http as http_utils
# Reset before test
http_utils._global_async_client = None
yield
# Reset after test
http_utils._global_async_client = None
@pytest.fixture
def temp_storage_db_file():
"""Create a temporary SQLite database file for agent storage testing."""
@@ -6,14 +6,15 @@ import pytest
from agno.agent import Agent
from agno.db.sqlite.sqlite import SqliteDb
from agno.knowledge.knowledge import Knowledge
from agno.vectordb.chroma import ChromaDb
from agno.vectordb.lancedb.lance_db import LanceDb
@pytest.fixture
def setup_vector_db():
"""Setup a temporary vector DB for testing."""
table_name = f"docx_test_{os.urandom(4).hex()}"
vector_db = LanceDb(table_name=table_name, uri="tmp/lancedb")
path = f"tmp/chromadb_{os.urandom(4).hex()}"
vector_db = ChromaDb(collection="vectors", path=path, persistent_client=True)
yield vector_db
# Clean up after test
vector_db.drop()
@@ -64,6 +64,7 @@ def test_simple_function_tool():
"units": {"type": "string", "description": "Temperature units (celsius or fahrenheit)"},
},
"required": ["location", "units"],
"additionalProperties": False,
},
}
]
@@ -19,7 +19,7 @@ def _assert_metrics(response: RunOutput):
def test_basic():
agent = Agent(model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"), markdown=True, telemetry=False)
agent = Agent(model=CerebrasOpenAI(id="gpt-oss-120b"), markdown=True, telemetry=False)
response: RunOutput = agent.run("Share a 2 sentence horror story")
@@ -31,7 +31,7 @@ def test_basic():
def test_basic_stream():
agent = Agent(model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"), markdown=True, telemetry=False)
agent = Agent(model=CerebrasOpenAI(id="gpt-oss-120b"), markdown=True, telemetry=False)
response_stream = agent.run("Share a 2 sentence horror story", stream=True)
@@ -46,7 +46,7 @@ def test_basic_stream():
@pytest.mark.asyncio
async def test_async_basic():
agent = Agent(model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"), markdown=True, telemetry=False)
agent = Agent(model=CerebrasOpenAI(id="gpt-oss-120b"), markdown=True, telemetry=False)
response = await agent.arun("Share a 2 sentence horror story")
@@ -58,7 +58,7 @@ async def test_async_basic():
@pytest.mark.asyncio
async def test_async_basic_stream():
agent = Agent(model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"), markdown=True, telemetry=False)
agent = Agent(model=CerebrasOpenAI(id="gpt-oss-120b"), markdown=True, telemetry=False)
async for response in agent.arun("Share a 2 sentence horror story", stream=True):
assert response.content is not None
@@ -67,7 +67,7 @@ async def test_async_basic_stream():
def test_with_memory():
agent = Agent(
db=SqliteDb(db_file="tmp/test_with_memory.db"),
model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"),
model=CerebrasOpenAI(id="gpt-oss-120b"),
add_history_to_context=True,
num_history_runs=5,
markdown=True,
@@ -99,7 +99,7 @@ def test_structured_output():
plot: str = Field(..., description="Brief plot summary")
agent = Agent(
model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"),
model=CerebrasOpenAI(id="gpt-oss-120b"),
output_schema=MovieScript,
telemetry=False,
)
@@ -24,7 +24,7 @@ class MovieScript(BaseModel):
def test_structured_response():
structured_output_agent = Agent(
model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"),
model=CerebrasOpenAI(id="gpt-oss-120b"),
description="You help people write movie scripts.",
output_schema=MovieScript,
)
@@ -52,7 +52,7 @@ def test_structured_response_with_enum_fields():
rating: Grade
structured_output_agent = Agent(
model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"),
model=CerebrasOpenAI(id="gpt-oss-120b"),
description="You help generate recipe names and ratings.",
output_schema=Recipe,
)
@@ -65,7 +65,7 @@ def test_structured_response_with_enum_fields():
def test_structured_response_strict_output_false():
"""Test structured response with strict_output=False (guided mode)"""
guided_output_agent = Agent(
model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct", strict_output=False),
model=CerebrasOpenAI(id="gpt-oss-120b", strict_output=False),
description="You write movie scripts.",
output_schema=MovieScript,
)
@@ -2,12 +2,13 @@ import pytest
from agno.agent import Agent
from agno.models.cerebras import CerebrasOpenAI
from agno.tools.duckduckgo import DuckDuckGoTools
from agno.tools.yfinance import YFinanceTools
def test_tool_use():
agent = Agent(
model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"),
model=CerebrasOpenAI(id="gpt-oss-120b"),
tools=[YFinanceTools(cache_results=True)],
telemetry=False,
)
@@ -18,12 +19,11 @@ def test_tool_use():
assert response.messages is not None
assert any(msg.tool_calls for msg in response.messages)
assert response.content is not None
assert "TSLA" in response.content
def test_tool_use_stream():
agent = Agent(
model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"),
model=CerebrasOpenAI(id="gpt-oss-120b"),
tools=[YFinanceTools(cache_results=True)],
markdown=True,
telemetry=False,
@@ -44,14 +44,13 @@ def test_tool_use_stream():
assert len(responses) > 0
assert tool_call_seen, "No tool calls observed in stream"
assert any("TSLA" in r.content for r in responses if r.content)
@pytest.mark.asyncio
async def test_async_tool_use():
agent = Agent(
model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"),
tools=[YFinanceTools(cache_results=True)],
model=CerebrasOpenAI(id="gpt-oss-120b"),
tools=[DuckDuckGoTools(cache_results=True)],
telemetry=False,
)
@@ -61,13 +60,12 @@ async def test_async_tool_use():
assert response.messages is not None
assert any(msg.tool_calls for msg in response.messages if msg.role == "assistant")
assert response.content is not None
assert "France" in response.content
@pytest.mark.asyncio
async def test_async_tool_use_stream():
agent = Agent(
model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"),
model=CerebrasOpenAI(id="gpt-oss-120b"),
tools=[YFinanceTools(cache_results=True)],
telemetry=False,
)
@@ -92,8 +90,8 @@ async def test_async_tool_use_stream():
def test_tool_use_with_content():
agent = Agent(
model=CerebrasOpenAI(id="llama-4-scout-17b-16e-instruct"),
tools=[YFinanceTools(cache_results=True)],
model=CerebrasOpenAI(id="gpt-oss-120b"),
tools=[DuckDuckGoTools(cache_results=True)],
telemetry=False,
)
@@ -103,4 +101,3 @@ def test_tool_use_with_content():
assert response.messages is not None
assert any(msg.tool_calls for msg in response.messages if msg.tool_calls is not None)
assert response.content is not None
assert "France" in response.content
@@ -9,7 +9,7 @@ from agno.models.cerebras import Cerebras
@pytest.fixture(scope="module")
def cerebras_model():
"""Fixture that provides a Cerebras model and reuses it across all tests in the module."""
return Cerebras(id="llama-4-scout-17b-16e-instruct")
return Cerebras(id="gpt-oss-120b")
def _assert_metrics(response: RunOutput):
@@ -7,7 +7,7 @@ from agno.tools.duckduckgo import DuckDuckGoTools
def test_tool_use():
agent = Agent(
model=Cerebras(id="llama-4-scout-17b-16e-instruct"),
model=Cerebras(id="gpt-oss-120b"),
tools=[DuckDuckGoTools(cache_results=True)],
telemetry=False,
)
@@ -23,7 +23,7 @@ def test_tool_use():
def test_tool_use_stream():
agent = Agent(
model=Cerebras(id="llama-4-scout-17b-16e-instruct"),
model=Cerebras(id="gpt-oss-120b"),
tools=[DuckDuckGoTools(cache_results=True)],
telemetry=False,
)
@@ -48,7 +48,7 @@ def test_tool_use_stream():
@pytest.mark.asyncio
async def test_async_tool_use():
agent = Agent(
model=Cerebras(id="llama-4-scout-17b-16e-instruct"),
model=Cerebras(id="gpt-oss-120b"),
tools=[DuckDuckGoTools(cache_results=True)],
telemetry=False,
)
@@ -65,7 +65,7 @@ async def test_async_tool_use():
@pytest.mark.asyncio
async def test_async_tool_use_stream():
agent = Agent(
model=Cerebras(id="llama-4-scout-17b-16e-instruct"),
model=Cerebras(id="gpt-oss-120b"),
tools=[DuckDuckGoTools(cache_results=True)],
telemetry=False,
)
@@ -90,7 +90,7 @@ async def test_async_tool_use_stream():
def test_tool_use_with_content():
agent = Agent(
model=Cerebras(id="llama-4-scout-17b-16e-instruct"),
model=Cerebras(id="gpt-oss-120b"),
tools=[DuckDuckGoTools(cache_results=True)],
telemetry=False,
)
@@ -4,13 +4,12 @@ import pytest
from agno.agent import Agent, RunOutput # noqa
from agno.models.deepinfra import DeepInfra
from agno.tools.exa import ExaTools
from agno.tools.yfinance import YFinanceTools
def test_tool_use():
agent = Agent(
model=DeepInfra(id="meta-llama/Llama-2-70b-chat-hf"),
model=DeepInfra(id="openai/gpt-oss-120b"),
tools=[YFinanceTools(cache_results=True)],
markdown=True,
telemetry=False,
@@ -27,7 +26,7 @@ def test_tool_use():
def test_tool_use_stream():
agent = Agent(
model=DeepInfra(id="meta-llama/Llama-2-70b-chat-hf"),
model=DeepInfra(id="openai/gpt-oss-120b"),
tools=[YFinanceTools(cache_results=True)],
markdown=True,
telemetry=False,
@@ -57,7 +56,7 @@ def test_tool_use_stream():
@pytest.mark.asyncio
async def test_async_tool_use():
agent = Agent(
model=DeepInfra(id="meta-llama/Llama-2-70b-chat-hf"),
model=DeepInfra(id="openai/gpt-oss-120b"),
tools=[YFinanceTools(cache_results=True)],
markdown=True,
telemetry=False,
@@ -75,7 +74,7 @@ async def test_async_tool_use():
@pytest.mark.asyncio
async def test_async_tool_use_stream():
agent = Agent(
model=DeepInfra(id="meta-llama/Llama-2-70b-chat-hf"),
model=DeepInfra(id="openai/gpt-oss-120b"),
tools=[YFinanceTools(cache_results=True)],
markdown=True,
telemetry=False,
@@ -104,7 +103,7 @@ async def test_async_tool_use_stream():
def test_parallel_tool_calls():
agent = Agent(
model=DeepInfra(id="meta-llama/Llama-2-70b-chat-hf"),
model=DeepInfra(id="openai/gpt-oss-120b"),
tools=[YFinanceTools(cache_results=True)],
markdown=True,
telemetry=False,
@@ -131,7 +130,7 @@ def test_tool_call_custom_tool_no_parameters():
return "It is currently 70 degrees and cloudy in Tokyo"
agent = Agent(
model=DeepInfra(id="meta-llama/Llama-2-70b-chat-hf"),
model=DeepInfra(id="openai/gpt-oss-120b"),
tools=[get_the_weather_in_tokyo],
markdown=True,
telemetry=False,
@@ -159,7 +158,7 @@ def test_tool_call_custom_tool_optional_parameters():
return f"It is currently 70 degrees and cloudy in {city}"
agent = Agent(
model=DeepInfra(id="meta-llama/Llama-2-70b-chat-hf"),
model=DeepInfra(id="openai/gpt-oss-120b"),
tools=[get_the_weather],
markdown=True,
telemetry=False,
@@ -169,32 +168,6 @@ def test_tool_call_custom_tool_optional_parameters():
# Verify tool usage
assert response.messages is not None
assert any(msg.tool_calls for msg in response.messages if msg.tool_calls is not None)
assert response.content is not None
assert "70" in response.content
def test_tool_call_list_parameters():
agent = Agent(
model=DeepInfra(id="meta-llama/Llama-2-70b-chat-hf"),
tools=[ExaTools()],
instructions="Use a single tool call if possible",
markdown=True,
telemetry=False,
)
response = agent.run(
"What are the papers at https://arxiv.org/pdf/2307.06435 and https://arxiv.org/pdf/2502.09601 about?"
)
# Verify tool usage
assert response.messages is not None
assert any(msg.tool_calls for msg in response.messages if msg.tool_calls is not None)
tool_calls = []
for msg in response.messages:
if msg.tool_calls:
tool_calls.extend(msg.tool_calls)
for call in tool_calls:
if call.get("type", "") == "function":
assert call["function"]["name"] in ["search_exa", "get_contents", "exa_answer"]
assert response.content is not None
@@ -155,7 +155,6 @@ def test_tool_call_custom_tool_no_parameters():
assert response.messages is not None
assert any(msg.tool_calls for msg in response.messages if msg.tool_calls is not None)
assert response.content is not None
assert "70" in response.content
def test_tool_call_custom_tool_optional_parameters():
@@ -184,7 +183,6 @@ def test_tool_call_custom_tool_optional_parameters():
assert response.messages is not None
assert any(msg.tool_calls for msg in response.messages if msg.tool_calls is not None)
assert response.content is not None
assert "70" in response.content
def test_tool_call_list_parameters():
@@ -1,146 +0,0 @@
import pytest
from pydantic import BaseModel, Field
from agno.agent import Agent, RunOutput
from agno.db.sqlite import SqliteDb
from agno.models.ollama import OllamaTools
def _assert_metrics(response: RunOutput):
assert response.metrics is not None
input_tokens = response.metrics.input_tokens
output_tokens = response.metrics.output_tokens
total_tokens = response.metrics.total_tokens
assert input_tokens > 0
assert output_tokens > 0
assert total_tokens > 0
assert total_tokens == input_tokens + output_tokens
def test_basic():
agent = Agent(model=OllamaTools(id="llama3.2:latest"), markdown=True, telemetry=False)
response: RunOutput = agent.run("Share a 2 sentence horror story")
assert response.content is not None
assert response.messages is not None
assert len(response.messages) == 3
assert [m.role for m in response.messages] == ["system", "user", "assistant"]
_assert_metrics(response)
def test_basic_stream():
agent = Agent(model=OllamaTools(id="llama3.2:latest"), markdown=True, telemetry=False)
for response in agent.run("Share a 2 sentence horror story", stream=True):
assert response.content is not None
@pytest.mark.asyncio
async def test_async_basic():
agent = Agent(model=OllamaTools(id="llama3.2:latest"), markdown=True, telemetry=False)
response = await agent.arun("Share a 2 sentence horror story")
assert response.content is not None
assert response.messages is not None
assert len(response.messages) == 3
assert [m.role for m in response.messages] == ["system", "user", "assistant"]
_assert_metrics(response)
@pytest.mark.asyncio
async def test_async_basic_stream():
agent = Agent(model=OllamaTools(id="llama3.2:latest"), markdown=True, telemetry=False)
async for response in agent.arun("Share a 2 sentence horror story", stream=True):
assert response.content is not None
def test_with_memory():
agent = Agent(
db=SqliteDb(db_file="tmp/test_with_memory.db"),
model=OllamaTools(id="llama3.2:latest"),
add_history_to_context=True,
markdown=True,
telemetry=False,
)
# First interaction
response1 = agent.run("My name is John Smith")
assert response1.content is not None
# Second interaction should remember the name
response2 = agent.run("What's my name?")
assert response2.content is not None
assert "John Smith" in response2.content # type: ignore
# Verify memories were created
messages = agent.get_session_messages()
assert len(messages) == 5
assert [m.role for m in messages] == ["system", "user", "assistant", "user", "assistant"]
# Test metrics structure and types
_assert_metrics(response2)
def test_output_schema():
class MovieScript(BaseModel):
title: str = Field(..., description="Movie title")
genre: str = Field(..., description="Movie genre")
plot: str = Field(..., description="Brief plot summary")
agent = Agent(model=OllamaTools(id="llama3.2:latest"), markdown=True, telemetry=False, output_schema=MovieScript)
response = agent.run("Create a movie about time travel")
# Verify structured output
assert isinstance(response.content, MovieScript)
assert response.content.title is not None
assert response.content.genre is not None
assert response.content.plot is not None
def test_json_response_mode():
class MovieScript(BaseModel):
title: str = Field(..., description="Movie title")
genre: str = Field(..., description="Movie genre")
plot: str = Field(..., description="Brief plot summary")
agent = Agent(
model=OllamaTools(id="llama3.2:latest"),
use_json_mode=True,
telemetry=False,
output_schema=MovieScript,
)
response = agent.run("Create a movie about time travel")
# Verify structured output
assert isinstance(response.content, MovieScript)
assert response.content.title is not None
assert response.content.genre is not None
assert response.content.plot is not None
def test_history():
agent = Agent(
model=OllamaTools(id="llama3.2:latest"),
db=SqliteDb(db_file="tmp/ollama_tools/test_basic.db"),
add_history_to_context=True,
telemetry=False,
)
run_output = agent.run("Hello")
assert run_output.messages is not None
assert len(run_output.messages) == 3
run_output = agent.run("Hello 2")
assert run_output.messages is not None
assert len(run_output.messages) == 5
run_output = agent.run("Hello 3")
assert run_output.messages is not None
assert len(run_output.messages) == 7
run_output = agent.run("Hello 4")
assert run_output.messages is not None
assert len(run_output.messages) == 9
@@ -1,182 +0,0 @@
from typing import Optional
import pytest
from agno.agent import Agent
from agno.models.ollama import OllamaTools
from agno.tools.duckduckgo import DuckDuckGoTools
from agno.tools.exa import ExaTools
from agno.tools.yfinance import YFinanceTools
def test_tool_use():
agent = Agent(
model=OllamaTools(id="llama3.2:latest"),
tools=[YFinanceTools(cache_results=True)],
markdown=True,
telemetry=False,
)
response = agent.run("What is the current price of TSLA?")
# Verify tool usage
assert response.messages is not None
assert any(msg.tool_calls for msg in response.messages)
assert response.content is not None
assert "TSLA" in response.content
def test_tool_use_stream():
agent = Agent(
model=OllamaTools(id="llama3.2:latest"),
tools=[YFinanceTools(cache_results=True)],
markdown=True,
telemetry=False,
)
for chunk in agent.run("What is the current price of TSLA?", stream=True, stream_events=True):
if chunk.event in ["ToolCallStarted", "ToolCallCompleted"] and hasattr(chunk, "tool") and chunk.tool: # type: ignore
if chunk.tool.tool_name: # type: ignore
tool_call_seen = True
if chunk.content is not None and "TSLA" in chunk.content:
keyword_seen_in_response = True
assert tool_call_seen, "No tool calls observed in stream"
assert keyword_seen_in_response, "Keyword not found in response"
@pytest.mark.asyncio
async def test_async_tool_use():
agent = Agent(
model=OllamaTools(id="llama3.2:latest"),
tools=[YFinanceTools(cache_results=True)],
markdown=True,
telemetry=False,
)
response = await agent.arun("What is the current price of TSLA?")
# Verify tool usage
assert response.messages is not None
assert any(msg.tool_calls for msg in response.messages if msg.role == "assistant")
assert response.content is not None
assert "TSLA" in response.content
@pytest.mark.asyncio
async def test_async_tool_use_stream():
agent = Agent(
model=OllamaTools(id="llama3.2:latest"),
tools=[YFinanceTools(cache_results=True)],
markdown=True,
telemetry=False,
)
async for chunk in agent.arun("What is the current price of TSLA?", stream=True, stream_events=True):
if chunk.event in ["ToolCallStarted", "ToolCallCompleted"] and hasattr(chunk, "tool") and chunk.tool: # type: ignore
if chunk.tool.tool_name: # type: ignore
tool_call_seen = True
if chunk.content is not None and "TSLA" in chunk.content:
keyword_seen_in_response = True
assert tool_call_seen, "No tool calls observed in stream"
assert keyword_seen_in_response, "Keyword not found in response"
def test_multiple_tool_calls():
agent = Agent(
model=OllamaTools(id="llama3.2:latest"),
tools=[YFinanceTools(cache_results=True), DuckDuckGoTools(cache_results=True)],
markdown=True,
telemetry=False,
)
response = agent.run("What is the current price of TSLA and what is the latest news about it?")
# Verify tool usage
assert response.messages is not None
tool_calls = []
for msg in response.messages:
if msg.tool_calls:
tool_calls.extend(msg.tool_calls)
assert len([call for call in tool_calls if call.get("type", "") == "function"]) >= 2
assert response.content is not None
assert "TSLA" in response.content and "latest news" in response.content.lower()
def test_tool_call_custom_tool_no_parameters():
def get_the_weather_in_tokyo():
"""
Get the weather in Tokyo
"""
return "It is currently 70 degrees and cloudy in Tokyo"
agent = Agent(
model=OllamaTools(id="llama3.2:latest"),
tools=[get_the_weather_in_tokyo],
markdown=True,
telemetry=False,
)
response = agent.run("What is the weather in Tokyo?")
# Verify tool usage
assert response.messages is not None
assert any(msg.tool_calls for msg in response.messages)
assert response.content is not None
assert "70" in response.content
def test_tool_call_custom_tool_optional_parameters():
def get_the_weather(city: Optional[str] = None):
"""
Get the weather in a city
Args:
city: The city to get the weather for
"""
if city is None:
return "It is currently 70 degrees and cloudy in Tokyo"
else:
return f"It is currently 70 degrees and cloudy in {city}"
agent = Agent(
model=OllamaTools(id="llama3.2:latest"),
tools=[get_the_weather],
markdown=True,
telemetry=False,
)
response = agent.run("What is the weather in Paris?")
# Verify tool usage
assert response.messages is not None
assert any(msg.tool_calls for msg in response.messages)
assert response.content is not None
assert "70" in response.content
def test_tool_call_list_parameters():
agent = Agent(
model=OllamaTools(id="llama3.2:latest"),
tools=[ExaTools()],
instructions="Use a single tool call if possible",
markdown=True,
telemetry=False,
)
response = agent.run(
"What are the papers at https://arxiv.org/pdf/2307.06435 and https://arxiv.org/pdf/2502.09601 about?"
)
# Verify tool usage
assert response.messages is not None
assert any(msg.tool_calls for msg in response.messages)
tool_calls = []
for msg in response.messages:
if msg.tool_calls:
tool_calls.extend(msg.tool_calls)
for call in tool_calls:
if call.get("type", "") == "function":
assert call["function"]["name"] in ["get_contents", "exa_answer", "search_exa"]
assert response.content is not None
@@ -204,19 +204,6 @@ def test_history(openai_model):
assert len(run_output.messages) == 8
def test_cache_read_tokens(openai_model):
"""Assert cache_read_tokens is populated correctly and returned in the metrics"""
agent = Agent(model=openai_model, markdown=True, telemetry=False)
# Multiple + one large prompt to ensure token caching is triggered
agent.run("Share a 2 sentence horror story" * 250)
response = agent.run("Share a 2 sentence horror story" * 250)
assert response.metrics is not None
assert response.metrics.cache_read_tokens is not None
assert response.metrics.cache_read_tokens > 0
def test_reasoning_tokens():
"""Assert reasoning_tokens is populated correctly and returned in the metrics"""
agent = Agent(model=OpenAIChat(id="o3-mini"), markdown=True, telemetry=False)
@@ -112,7 +112,8 @@ def test_kwargs_propagate_to_run_context(test_os_client, test_agent: Agent):
def assert_run_context(run_context: RunContext):
assert run_context.user_id == "test-user-123"
assert run_context.session_id == "test-session-123"
assert run_context.session_state == {"test_session_state": "test-session-state"}
assert "test_session_state" in run_context.session_state
assert run_context.session_state["test_session_state"] == "test-session-state"
assert run_context.dependencies == {"test_dependencies": "test-dependencies"}
assert run_context.metadata == {"test_metadata": "test-metadata"}
assert run_context.knowledge_filters == {"test_knowledge_filters": "test-knowledge-filters"}
@@ -57,7 +57,7 @@ def test_list_as_input():
{
"type": "image_url",
"image_url": {
"url": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg",
"url": "https://www.exp1.com/wp-content/uploads/sites/7/2018/08/Golden-Gate-Bridge.jpg",
},
},
]
@@ -143,11 +143,7 @@ def test_empty_string_with_image():
response = team.run(
input="",
images=[
Image(
url="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
)
],
images=[Image(url="https://www.exp1.com/wp-content/uploads/sites/7/2018/08/Golden-Gate-Bridge.jpg")],
)
assert response.content is not None
assert len(response.content) > 0
@@ -176,11 +172,7 @@ def test_none_input_with_image():
response = team.run(
input=None,
images=[
Image(
url="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
)
],
images=[Image(url="https://www.exp1.com/wp-content/uploads/sites/7/2018/08/Golden-Gate-Bridge.jpg")],
)
assert response.content is not None
assert len(response.content) > 0
@@ -209,11 +201,7 @@ def test_empty_string_with_multiple_media():
response = team.run(
input="",
images=[
Image(
url="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
)
],
images=[Image(url="https://www.exp1.com/wp-content/uploads/sites/7/2018/08/Golden-Gate-Bridge.jpg")],
)
assert response.content is not None
assert len(response.content) > 0
@@ -248,11 +236,7 @@ def test_empty_string_with_image_and_user_memories():
response = team.run(
input="",
images=[
Image(
url="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
)
],
images=[Image(url="https://www.exp1.com/wp-content/uploads/sites/7/2018/08/Golden-Gate-Bridge.jpg")],
)
assert response.content is not None
assert len(response.content) > 0
@@ -287,11 +271,7 @@ def test_none_input_with_image_and_user_memories():
response = team.run(
input=None,
images=[
Image(
url="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
)
],
images=[Image(url="https://www.exp1.com/wp-content/uploads/sites/7/2018/08/Golden-Gate-Bridge.jpg")],
)
assert response.content is not None
assert len(response.content) > 0
@@ -326,11 +306,7 @@ def test_empty_string_with_image_and_session_summaries():
response = team.run(
input="",
images=[
Image(
url="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
)
],
images=[Image(url="https://www.exp1.com/wp-content/uploads/sites/7/2018/08/Golden-Gate-Bridge.jpg")],
)
assert response.content is not None
assert len(response.content) > 0
@@ -365,11 +341,7 @@ def test_none_input_with_image_and_session_summaries():
response = team.run(
input=None,
images=[
Image(
url="https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg"
)
],
images=[Image(url="https://www.exp1.com/wp-content/uploads/sites/7/2018/08/Golden-Gate-Bridge.jpg")],
)
assert response.content is not None
assert len(response.content) > 0
@@ -59,7 +59,7 @@ async def test_multi_step_background_execution(multi_step_workflow):
assert response.run_id is not None
# Poll for completion with step tracking
max_polls = 45 # Longer timeout for multi-step
max_polls = 60 # Longer timeout for multi-step
poll_count = 0
seen_running = False
@@ -263,7 +263,7 @@ def test_extract_with_custom_name():
}
# Create reader and call read with custom name
reader = TavilyReader()
reader = TavilyReader(chunk=False)
documents = reader.read("https://example.com", name="Custom Name")
# Verify custom name was used
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "agno-infra"
version = "1.0.4"
version = "1.0.5"
description = "A lightweight framework and CLI for managing Agentic Infrastructure"
requires-python = ">=3.7,<4"
readme = "README.md"
+2 -2
View File
@@ -33,8 +33,8 @@ CALL :print_heading "Installing agno"
ECHO [INFO] Installing dependencies from %AGNO_DIR%\requirements.txt
pip install -r "%AGNO_DIR%\requirements.txt"
CALL :print_heading "Installing agno in editable mode with tests dependencies"
pip install -e "%AGNO_DIR%[tests]"
CALL :print_heading "Installing agno in editable mode with dev dependencies"
pip install -e "%AGNO_DIR%[dev]"
CALL :print_heading "Installing agno-os"
ECHO [INFO] Installing dependencies from %AGNO_INFRA_DIR%\requirements.txt
+5 -18
View File
@@ -31,32 +31,19 @@ print_heading "Installing agno"
print_info "VIRTUAL_ENV=${VENV_DIR} uv pip install -r ${AGNO_DIR}/requirements.txt"
VIRTUAL_ENV=${VENV_DIR} uv pip install -r ${AGNO_DIR}/requirements.txt
print_heading "Installing agno in editable mode with tests dependencies"
# Install dev dependencies first (includes pytest-cov)
print_heading "Installing agno in editable mode with dev dependencies"
VIRTUAL_ENV=${VENV_DIR} uv pip install -U -e ${AGNO_DIR}[dev]
# Then install tests dependencies (includes all models, tools, etc.)
VIRTUAL_ENV=${VENV_DIR} uv pip install -U -e ${AGNO_DIR}[tests]
VIRTUAL_ENV=${VENV_DIR} uv pip install yfinance
#TODO: Improve the dev setup to handle conflicts which results in missing dependencies
VIRTUAL_ENV=${VENV_DIR} uv pip install google-genai==1.52.0
VIRTUAL_ENV=${VENV_DIR} uv pip install mcp==1.9.2
VIRTUAL_ENV=${VENV_DIR} uv pip install fastmcp
VIRTUAL_ENV=${VENV_DIR} uv pip install crawl4ai==0.6.3
VIRTUAL_ENV=${VENV_DIR} uv pip install firecrawl-py==3.4.0
VIRTUAL_ENV=${VENV_DIR} uv pip install chonkie[st] chonkie
VIRTUAL_ENV=${VENV_DIR} uv pip install pylance
VIRTUAL_ENV=${VENV_DIR} uv pip install psycopg-binary psycopg psycopg2
print_heading "Installing agno-os"
print_heading "Installing agno-infra"
print_info "VIRTUAL_ENV=${VENV_DIR} uv pip install -r ${AGNO_INFRA_DIR}/requirements.txt"
VIRTUAL_ENV=${VENV_DIR} uv pip install -r ${AGNO_INFRA_DIR}/requirements.txt
print_heading "Installing agno-os in editable mode with dev dependencies"
print_heading "Installing agno-infra in editable mode with dev dependencies"
VIRTUAL_ENV=${VENV_DIR} uv pip install -e ${AGNO_INFRA_DIR}[dev]
print_heading "uv pip list"
VIRTUAL_ENV=${VENV_DIR} uv pip list
print_heading "Development setup complete"
print_heading "Activate venv using: source .venv/bin/activate"
print_heading "Activate venv using: source .venv/bin/activate"
+68
View File
@@ -0,0 +1,68 @@
@echo off
REM ###########################################################################
REM # Agno Development Setup (Windows)
REM # - Create a virtual environment and install libraries in editable mode.
REM # - Please deactivate the existing virtual environment before running.
REM # Usage: scripts\dev_setup.bat
REM ###########################################################################
SETLOCAL ENABLEDELAYEDEXPANSION
REM Get current directory
SET "CURR_DIR=%~dp0"
SET "REPO_ROOT=%CURR_DIR%\.."
SET "AGNO_DIR=%REPO_ROOT%\libs\agno"
SET "AGNO_INFRA_DIR=%REPO_ROOT%\libs\agno_infra"
SET "VENV_DIR=%REPO_ROOT%\.venv"
REM Function to print headings
CALL :print_heading "Development setup..."
CALL :print_heading "Removing virtual env"
ECHO [INFO] Removing %VENV_DIR%
rmdir /s /q "%VENV_DIR%" 2>nul
CALL :print_heading "Creating virtual env"
ECHO [INFO] Creating virtual environment in %VENV_DIR%
python -m venv "%VENV_DIR%"
REM Activate virtual environment
CALL :activate_venv
CALL :print_heading "Installing agno"
ECHO [INFO] Installing dependencies from %AGNO_DIR%\requirements.txt
pip install -r "%AGNO_DIR%\requirements.txt"
CALL :print_heading "Installing agno in editable mode with tests dependencies"
pip install -e "%AGNO_DIR%[tests]"
CALL :print_heading "Installing agno-os"
ECHO [INFO] Installing dependencies from %AGNO_INFRA_DIR%\requirements.txt
pip install -r "%AGNO_INFRA_DIR%\requirements.txt"
CALL :print_heading "Installing agno-os in editable mode with dev dependencies"
pip install -e "%AGNO_INFRA_DIR%[dev]"
CALL :print_heading "pip list"
pip list
CALL :print_heading "Development setup complete"
ECHO.
ECHO Activate venv using: .\.venv\Scripts\activate
ECHO.
EXIT /B
REM Function to print headings
:print_heading
ECHO.
ECHO ##################################################
ECHO # %1
ECHO ##################################################
ECHO.
EXIT /B
REM Function to activate virtual environment
:activate_venv
ECHO [INFO] Activating virtual environment...
CALL "%VENV_DIR%\Scripts\activate"
EXIT /B
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
############################################################################
# Agno Development Setup
# - Create a virtual environment and install libraries in editable mode.
# - Please install uv before running this script.
# - Please deactivate the existing virtual environment before running.
# Usage: ./scripts/dev_setup.sh
############################################################################
CURR_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "${CURR_DIR}")"
AGNO_DIR="${REPO_ROOT}/libs/agno"
AGNO_INFRA_DIR="${REPO_ROOT}/libs/agno_infra"
source "${CURR_DIR}/_utils.sh"
VENV_DIR="${REPO_ROOT}/.venv"
PYTHON_VERSION=$(python3 --version)
print_heading "Development setup..."
print_heading "Removing virtual env"
print_info "rm -rf ${VENV_DIR}"
rm -rf ${VENV_DIR}
print_heading "Creating virtual env"
print_info "VIRTUAL_ENV=${VENV_DIR} uv venv --python 3.12"
VIRTUAL_ENV=${VENV_DIR} uv venv --python 3.12
print_heading "Installing agno"
print_info "VIRTUAL_ENV=${VENV_DIR} uv pip install -r ${AGNO_DIR}/requirements.txt"
VIRTUAL_ENV=${VENV_DIR} uv pip install -r ${AGNO_DIR}/requirements.txt
print_heading "Installing agno in editable mode with tests dependencies"
VIRTUAL_ENV=${VENV_DIR} uv pip install -U -e ${AGNO_DIR}[tests]
VIRTUAL_ENV=${VENV_DIR} uv pip install brave-search
print_heading "Installing agno-infra"
print_info "VIRTUAL_ENV=${VENV_DIR} uv pip install -r ${AGNO_INFRA_DIR}/requirements.txt"
VIRTUAL_ENV=${VENV_DIR} uv pip install -r ${AGNO_INFRA_DIR}/requirements.txt
print_heading "Installing agno-infra in editable mode with dev dependencies"
VIRTUAL_ENV=${VENV_DIR} uv pip install -e ${AGNO_INFRA_DIR}[dev]
print_heading "Development setup complete"
print_heading "Activate venv using: source .venv/bin/activate"