Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/test_on_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ jobs:
working-directory: .
run: |
./scripts/dev_setup.sh
- name: Run remaining integration tests
- name: Run agent integration tests
working-directory: .
run: |
source .venv/bin/activate
Expand Down Expand Up @@ -609,7 +609,7 @@ jobs:
working-directory: .
run: |
./scripts/dev_setup.sh
- name: Run remaining integration tests
- name: Run teams integration tests
working-directory: .
run: |
source .venv/bin/activate
Expand Down Expand Up @@ -638,7 +638,7 @@ jobs:
working-directory: .
run: |
./scripts/dev_setup.sh
- name: Run remaining integration tests
- name: Run knowledge integration tests
working-directory: .
run: |
source .venv/bin/activate
Expand Down Expand Up @@ -672,4 +672,4 @@ jobs:
working-directory: .
run: |
source .venv/bin/activate
python -m pytest --ignore=./libs/agno/tests/integration/models --ignore=./libs/agno/tests/integration/agents --ignore=./libs/agno/tests/integration/knowledge --ignore=./libs/agno/tests/integration/teams ./libs/agno/tests/integration
python -m pytest --ignore=./libs/agno/tests/integration/models --ignore=./libs/agno/tests/integration/agent --ignore=./libs/agno/tests/integration/knowledge --ignore=./libs/agno/tests/integration/teams ./libs/agno/tests/integration
2 changes: 1 addition & 1 deletion libs/agno/agno/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2031,7 +2031,7 @@ def add_tools_to_model(
if name not in _functions_for_model:
func._agent = self
func.process_entrypoint(strict=strict)
if strict:
if strict and func.strict is None:
func.strict = True
if self.tool_hooks is not None:
func.tool_hooks = self.tool_hooks
Expand Down
1 change: 1 addition & 0 deletions libs/agno/agno/memory/v2/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,7 @@ def add_run(self, session_id: str, run: Union[RunResponse, TeamRunResponse]) ->
"""Adds a RunResponse to the runs list."""
if not self.runs:
self.runs = {}

self.runs.setdefault(session_id, []).append(run)
log_debug("Added RunResponse to Memory")

Expand Down
1 change: 0 additions & 1 deletion libs/agno/agno/models/openai/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,6 @@ async def ainvoke(self, messages: List[Message]) -> Union[ChatCompletion, Parsed
Returns:
ChatCompletion: The chat completion response from the API.
"""

try:
if self.response_format is not None and self.structured_outputs:
if isinstance(self.response_format, type) and issubclass(self.response_format, BaseModel):
Expand Down
8 changes: 4 additions & 4 deletions libs/agno/agno/team/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -6084,13 +6084,13 @@ def load_team_session(self, session: TeamSession):
try:
if self.memory.runs is None:
self.memory.runs = {}
self.memory.runs[session.session_id] = []
for run in session.memory["runs"]:
session_id = run["session_id"]
self.memory.runs[session_id] = []
run_session_id = run["session_id"]
if "team_id" in run:
self.memory.runs[session_id].append(TeamRunResponse.from_dict(run))
self.memory.runs[run_session_id].append(TeamRunResponse.from_dict(run))
else:
self.memory.runs[session_id].append(RunResponse.from_dict(run))
self.memory.runs[run_session_id].append(RunResponse.from_dict(run))
except Exception as e:
log_warning(f"Failed to load runs from memory: {e}")
if "team_context" in session.memory:
Expand Down
6 changes: 3 additions & 3 deletions libs/agno/agno/tools/apify.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@


class ApifyTools(Toolkit):
def __init__(self, actors: Union[str, List[str]] = None, apify_api_token: Optional[str] = None):
def __init__(self, actors: Optional[Union[str, List[str]]] = None, apify_api_token: Optional[str] = None):
"""Initialize ApifyTools with specific Actors.

Args:
Expand Down Expand Up @@ -173,7 +173,7 @@ def actor_function(**kwargs) -> str:

# Utility functions
def props_to_json_schema(input_dict, required_fields=None):
schema = {"type": "object", "properties": {}, "required": required_fields or []}
schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": required_fields or []}

def infer_array_item_type(prop):
type_map = {
Expand All @@ -194,7 +194,7 @@ def infer_array_item_type(prop):
return "string" # Fallback for arrays like searchStringsArray

for key, value in input_dict.items():
prop_schema = {}
prop_schema: Dict[str, Any] = {}
prop_type = value.get("type")

if "enum" in value:
Expand Down
6 changes: 6 additions & 0 deletions libs/agno/agno/tools/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ def process_entrypoint(self, strict: bool = False):
from agno.utils.json_schema import get_json_schema

if self.skip_entrypoint_processing:
if strict:
self.process_schema_for_strict()
return

if self.entrypoint is None:
Expand Down Expand Up @@ -260,6 +262,10 @@ def process_entrypoint(self, strict: bool = False):
except Exception as e:
log_warning(f"Failed to add validate decorator to entrypoint: {e}")

def process_schema_for_strict(self):
self.parameters["additionalProperties"] = False
self.parameters["required"] = [name for name in self.parameters["properties"] if name not in ["agent", "team"]]

def get_type_name(self, t: Type[T]):
name = str(t)
if "list" in name or "dict" in name:
Expand Down
1 change: 0 additions & 1 deletion libs/agno/agno/tools/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,6 @@ async def initialize(self) -> None:
try:
# Get an entrypoint for the tool
entrypoint = get_entrypoint_for_tool(tool, self.session)

# Create a Function for the tool
f = Function(
name=tool.name,
Expand Down
2 changes: 1 addition & 1 deletion libs/agno/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "agno"
version = "1.4.4"
version = "1.4.5"
description = "Agno: a lightweight library for building Reasoning Agents"
requires-python = ">=3.7,<4"
readme = "README.md"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,16 +70,21 @@ def test_knowledge_tools_non_streaming(knowledge_base):
# Run the agent in non-streaming mode
response = agent.run("What does Paul Graham explain about reading in his essay?", stream=False)

# Print the reasoning_content when received
if hasattr(response, "reasoning_content") and response.reasoning_content:
print("\n=== KnowledgeTools (non-streaming) reasoning_content ===")
print(response.reasoning_content)
print("=========================================================\n")

# Assert that reasoning_content exists and is populated
assert hasattr(response, "reasoning_content"), "Response should have reasoning_content attribute"
assert response.reasoning_content is not None, "reasoning_content should not be None"
assert len(response.reasoning_content) > 0, "reasoning_content should not be empty"
think_called = False
for tool_call in response.formatted_tool_calls:
if "think" in tool_call:
think_called = True
if think_called:
# Print the reasoning_content when received
if hasattr(response, "reasoning_content") and response.reasoning_content:
print("\n=== KnowledgeTools (non-streaming) reasoning_content ===")
print(response.reasoning_content)
print("=========================================================\n")

# Assert that reasoning_content exists and is populated
assert hasattr(response, "reasoning_content"), "Response should have reasoning_content attribute"
assert response.reasoning_content is not None, "reasoning_content should not be None"
assert len(response.reasoning_content) > 0, "reasoning_content should not be empty"


@pytest.mark.integration
Expand All @@ -102,20 +107,25 @@ def test_knowledge_tools_streaming(knowledge_base):
agent.run("What are Paul Graham's suggestions on what to read?", stream=True, stream_intermediate_steps=True)
)

# Print the reasoning_content when received
if (
hasattr(agent, "run_response")
and agent.run_response
and hasattr(agent.run_response, "reasoning_content")
and agent.run_response.reasoning_content
):
print("\n=== KnowledgeTools (streaming) reasoning_content ===")
print(agent.run_response.reasoning_content)
print("====================================================\n")

# Check the agent's run_response directly after streaming is complete
assert hasattr(agent, "run_response"), "Agent should have run_response after streaming"
assert agent.run_response is not None, "Agent's run_response should not be None"
assert hasattr(agent.run_response, "reasoning_content"), "Response should have reasoning_content attribute"
assert agent.run_response.reasoning_content is not None, "reasoning_content should not be None"
assert len(agent.run_response.reasoning_content) > 0, "reasoning_content should not be empty"
think_called = False
for tool_call in agent.run_response.formatted_tool_calls:
if "think" in tool_call:
think_called = True
if think_called:
# Print the reasoning_content when received
if (
hasattr(agent, "run_response")
and agent.run_response
and hasattr(agent.run_response, "reasoning_content")
and agent.run_response.reasoning_content
):
print("\n=== KnowledgeTools (streaming) reasoning_content ===")
print(agent.run_response.reasoning_content)
print("====================================================\n")

# Check the agent's run_response directly after streaming is complete
assert hasattr(agent, "run_response"), "Agent should have run_response after streaming"
assert agent.run_response is not None, "Agent's run_response should not be None"
assert hasattr(agent.run_response, "reasoning_content"), "Response should have reasoning_content attribute"
assert agent.run_response.reasoning_content is not None, "reasoning_content should not be None"
assert len(agent.run_response.reasoning_content) > 0, "reasoning_content should not be empty"
2 changes: 1 addition & 1 deletion libs/agno/tests/integration/models/xai/test_tool_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,5 +212,5 @@ def test_tool_call_list_parameters():
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"]
assert call["function"]["name"] in ["search_exa", "get_contents", "exa_answer"]
assert response.content is not None
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import pytest

from agno.agent import Agent
from agno.memory.v2.db.sqlite import SqliteMemoryDb
from agno.memory.v2.memory import Memory
from agno.models.anthropic.claude import Claude
Expand Down Expand Up @@ -64,51 +63,56 @@ def memory(memory_db):


@pytest.fixture
def web_agent():
"""Create a web agent for testing."""
from agno.tools.duckduckgo import DuckDuckGoTools

return Agent(
name="Web Agent",
model=OpenAIChat(id="gpt-4o-mini"),
role="Search the web for information",
tools=[DuckDuckGoTools(cache_results=True)],
)


@pytest.fixture
def finance_agent():
"""Create a finance agent for testing."""
from agno.tools.yfinance import YFinanceTools

return Agent(
name="Finance Agent",
model=OpenAIChat(id="gpt-4o-mini"),
role="Get financial data",
tools=[YFinanceTools(stock_price=True)],
)


@pytest.fixture
def analysis_agent():
"""Create an analysis agent for testing."""
return Agent(name="Analysis Agent", model=OpenAIChat(id="gpt-4o-mini"), role="Analyze data and provide insights")


@pytest.fixture
def route_team(web_agent, finance_agent, analysis_agent, team_storage, memory):
def route_team(team_storage, memory):
"""Create a route team with storage and memory for testing."""
return Team(
name="Route Team",
mode="route",
model=OpenAIChat(id="gpt-4o-mini"),
members=[web_agent, finance_agent, analysis_agent],
members=[],
storage=team_storage,
memory=memory,
enable_user_memories=True,
)


@pytest.mark.asyncio
async def test_run_history_persistence(route_team, team_storage, memory):
"""Test that all runs within a session are persisted in storage."""
user_id = "john@example.com"
session_id = "session_123"
num_turns = 5

# Clear memory for this specific test case
memory.clear()

# Perform multiple turns
conversation_messages = [
"What's the weather like today?",
"What about tomorrow?",
"Any recommendations for indoor activities?",
"Search for nearby museums.",
"Which one has the best reviews?",
]

assert len(conversation_messages) == num_turns

for msg in conversation_messages:
await route_team.arun(msg, user_id=user_id, session_id=session_id)

# Verify the stored session data after all turns
team_session = team_storage.read(session_id=session_id)

stored_memory_data = team_session.memory
assert stored_memory_data is not None, "Memory data not found in stored session."

stored_runs = stored_memory_data["runs"]
assert isinstance(stored_runs, list), "Stored runs data is not a list."

first_user_message_content = stored_runs[0]["messages"][1]["content"]
assert first_user_message_content == conversation_messages[0]


@pytest.mark.asyncio
async def test_multi_user_multi_session_route_team(route_team, team_storage, memory):
"""Test multi-user multi-session route team with storage and memory."""
Expand Down
Loading