diff --git a/.github/workflows/test_on_release.yml b/.github/workflows/test_on_release.yml index 5f1e43fd74e..e70bbc0076a 100644 --- a/.github/workflows/test_on_release.yml +++ b/.github/workflows/test_on_release.yml @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/libs/agno/agno/agent/agent.py b/libs/agno/agno/agent/agent.py index 3504dcc93ea..e3638ae1f28 100644 --- a/libs/agno/agno/agent/agent.py +++ b/libs/agno/agno/agent/agent.py @@ -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 diff --git a/libs/agno/agno/memory/v2/memory.py b/libs/agno/agno/memory/v2/memory.py index 4a5dd5aea2a..2da4004ad16 100644 --- a/libs/agno/agno/memory/v2/memory.py +++ b/libs/agno/agno/memory/v2/memory.py @@ -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") diff --git a/libs/agno/agno/models/openai/chat.py b/libs/agno/agno/models/openai/chat.py index 7dcddcbdc38..740d670595b 100644 --- a/libs/agno/agno/models/openai/chat.py +++ b/libs/agno/agno/models/openai/chat.py @@ -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): diff --git a/libs/agno/agno/team/team.py b/libs/agno/agno/team/team.py index 099cdb9c865..34ce10780d5 100644 --- a/libs/agno/agno/team/team.py +++ b/libs/agno/agno/team/team.py @@ -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: diff --git a/libs/agno/agno/tools/apify.py b/libs/agno/agno/tools/apify.py index 5f98b384b46..d3e4acae696 100644 --- a/libs/agno/agno/tools/apify.py +++ b/libs/agno/agno/tools/apify.py @@ -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: @@ -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 = { @@ -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: diff --git a/libs/agno/agno/tools/function.py b/libs/agno/agno/tools/function.py index 4407903a9aa..ec7f6186ac3 100644 --- a/libs/agno/agno/tools/function.py +++ b/libs/agno/agno/tools/function.py @@ -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: @@ -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: diff --git a/libs/agno/agno/tools/mcp.py b/libs/agno/agno/tools/mcp.py index fdc5b1c7df4..d8c1dd6f3d4 100644 --- a/libs/agno/agno/tools/mcp.py +++ b/libs/agno/agno/tools/mcp.py @@ -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, diff --git a/libs/agno/pyproject.toml b/libs/agno/pyproject.toml index f2db73d0fea..332aca3116d 100644 --- a/libs/agno/pyproject.toml +++ b/libs/agno/pyproject.toml @@ -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" diff --git a/libs/agno/tests/integration/agent/test_reasoning_content_knowledge_tools.py b/libs/agno/tests/integration/agent/test_reasoning_content_knowledge_tools.py index 856873fc8a4..4494d6e2ea2 100644 --- a/libs/agno/tests/integration/agent/test_reasoning_content_knowledge_tools.py +++ b/libs/agno/tests/integration/agent/test_reasoning_content_knowledge_tools.py @@ -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 @@ -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" diff --git a/libs/agno/tests/integration/models/xai/test_tool_use.py b/libs/agno/tests/integration/models/xai/test_tool_use.py index 2d4991dd47f..2f86ec305e1 100644 --- a/libs/agno/tests/integration/models/xai/test_tool_use.py +++ b/libs/agno/tests/integration/models/xai/test_tool_use.py @@ -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 diff --git a/libs/agno/tests/integration/teams/test_team_with_storage_and_memory copy.py b/libs/agno/tests/integration/teams/test_team_with_storage_and_memory.py similarity index 78% rename from libs/agno/tests/integration/teams/test_team_with_storage_and_memory copy.py rename to libs/agno/tests/integration/teams/test_team_with_storage_and_memory.py index 0315a5b13f8..312598de680 100644 --- a/libs/agno/tests/integration/teams/test_team_with_storage_and_memory copy.py +++ b/libs/agno/tests/integration/teams/test_team_with_storage_and_memory.py @@ -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 @@ -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."""