From bce27a9ef0cdb0502c728006b52fa814e1d32a7e Mon Sep 17 00:00:00 2001 From: Yash Pratap Solanky <101447028+ysolanky@users.noreply.github.com> Date: Thu, 27 Nov 2025 07:15:59 -0500 Subject: [PATCH] chore: release 2.3.3 (#5524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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 Co-authored-by: Mustafa Esoofally --- .github/workflows/test.yml | 4 +- .github/workflows/test_on_release.yml | 22 +-- .../observability/traceloop_op.py | 36 ++++ cookbook/models/google/gemini/search.py | 2 +- libs/agno/agno/agent/agent.py | 51 ++--- libs/agno/agno/db/mysql/mysql.py | 2 +- libs/agno/agno/db/postgres/async_postgres.py | 2 +- libs/agno/agno/db/postgres/postgres.py | 2 +- libs/agno/agno/db/singlestore/singlestore.py | 74 +++---- libs/agno/agno/db/sqlite/async_sqlite.py | 2 +- libs/agno/agno/db/sqlite/sqlite.py | 2 +- .../agno/knowledge/reader/tavily_reader.py | 1 - .../agno/models/cerebras/cerebras_openai.py | 6 +- libs/agno/agno/os/schema.py | 27 ++- libs/agno/agno/team/team.py | 55 +++--- libs/agno/agno/utils/agent.py | 120 +++++++++++- libs/agno/pyproject.toml | 41 ++-- .../integration/agent/test_instructions.py | 54 ++++++ .../agent/test_output_schema_override.py | 12 -- libs/agno/tests/integration/conftest.py | 12 ++ .../knowledge/test_pdf_knowledge.py | 5 +- .../models/anthropic/test_format_tools.py | 1 + .../cerebras/cerebras_openai/test_basic.py | 12 +- .../test_structured_response.py | 6 +- .../cerebras/cerebras_openai/test_tool_use.py | 19 +- .../integration/models/cerebras/test_basic.py | 2 +- .../models/cerebras/test_tool_use.py | 10 +- .../models/deepinfra/test_tool_use.py | 41 +--- .../integration/models/groq/test_tool_use.py | 2 - .../models/ollama_tools/__init__.py | 0 .../models/ollama_tools/test_basic.py | 146 -------------- .../models/ollama_tools/test_tool_use.py | 182 ------------------ .../models/openai/chat/test_basic.py | 13 -- .../tests/integration/os/test_agent_runs.py | 3 +- .../tests/integration/teams/test_input.py | 44 +---- .../workflows/test_background_execution.py | 2 +- .../tests/unit/reader/test_tavily_reader.py | 2 +- libs/agno_infra/pyproject.toml | 2 +- scripts/dev_setup.bat | 4 +- scripts/dev_setup.sh | 23 +-- scripts/test_setup.bat | 68 +++++++ scripts/test_setup.sh | 47 +++++ 42 files changed, 542 insertions(+), 619 deletions(-) create mode 100644 cookbook/integrations/observability/traceloop_op.py create mode 100644 libs/agno/tests/integration/agent/test_instructions.py delete mode 100644 libs/agno/tests/integration/models/ollama_tools/__init__.py delete mode 100644 libs/agno/tests/integration/models/ollama_tools/test_basic.py delete mode 100644 libs/agno/tests/integration/models/ollama_tools/test_tool_use.py create mode 100644 scripts/test_setup.bat create mode 100755 scripts/test_setup.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bcdc66d0d3..e84ebd2972 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/.github/workflows/test_on_release.yml b/.github/workflows/test_on_release.yml index 850ae18d48..f3e0e4ac97 100644 --- a/.github/workflows/test_on_release.yml +++ b/.github/workflows/test_on_release.yml @@ -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 diff --git a/cookbook/integrations/observability/traceloop_op.py b/cookbook/integrations/observability/traceloop_op.py new file mode 100644 index 0000000000..323f4735c0 --- /dev/null +++ b/cookbook/integrations/observability/traceloop_op.py @@ -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=` +""" + +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) diff --git a/cookbook/models/google/gemini/search.py b/cookbook/models/google/gemini/search.py index 87f32e683f..56cd996d26 100644 --- a/cookbook/models/google/gemini/search.py +++ b/cookbook/models/google/gemini/search.py @@ -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, ) diff --git a/libs/agno/agno/agent/agent.py b/libs/agno/agno/agent/agent.py index c3056d5ca5..3a1d654027 100644 --- a/libs/agno/agno/agent/agent.py +++ b/libs/agno/agno/agent/agent.py @@ -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) diff --git a/libs/agno/agno/db/mysql/mysql.py b/libs/agno/agno/db/mysql/mysql.py index 039092299b..510b321a6f 100644 --- a/libs/agno/agno/db/mysql/mysql.py +++ b/libs/agno/agno/db/mysql/mysql.py @@ -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": diff --git a/libs/agno/agno/db/postgres/async_postgres.py b/libs/agno/agno/db/postgres/async_postgres.py index d1aaca4282..6c7558cfc9 100644 --- a/libs/agno/agno/db/postgres/async_postgres.py +++ b/libs/agno/agno/db/postgres/async_postgres.py @@ -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: """ diff --git a/libs/agno/agno/db/postgres/postgres.py b/libs/agno/agno/db/postgres/postgres.py index bb5ca1bbbe..356d8b1c62 100644 --- a/libs/agno/agno/db/postgres/postgres.py +++ b/libs/agno/agno/db/postgres/postgres.py @@ -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: """ diff --git a/libs/agno/agno/db/singlestore/singlestore.py b/libs/agno/agno/db/singlestore/singlestore.py index bc5c485635..48826e2586 100644 --- a/libs/agno/agno/db/singlestore/singlestore.py +++ b/libs/agno/agno/db/singlestore/singlestore.py @@ -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: """ diff --git a/libs/agno/agno/db/sqlite/async_sqlite.py b/libs/agno/agno/db/sqlite/async_sqlite.py index c2ff1f2780..9bc4638891 100644 --- a/libs/agno/agno/db/sqlite/async_sqlite.py +++ b/libs/agno/agno/db/sqlite/async_sqlite.py @@ -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: """ diff --git a/libs/agno/agno/db/sqlite/sqlite.py b/libs/agno/agno/db/sqlite/sqlite.py index 0a7bd19f22..94e3de4fa9 100644 --- a/libs/agno/agno/db/sqlite/sqlite.py +++ b/libs/agno/agno/db/sqlite/sqlite.py @@ -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: """ diff --git a/libs/agno/agno/knowledge/reader/tavily_reader.py b/libs/agno/agno/knowledge/reader/tavily_reader.py index 5dd9e7b7d5..ecb9913a02 100644 --- a/libs/agno/agno/knowledge/reader/tavily_reader.py +++ b/libs/agno/agno/knowledge/reader/tavily_reader.py @@ -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: diff --git a/libs/agno/agno/models/cerebras/cerebras_openai.py b/libs/agno/agno/models/cerebras/cerebras_openai.py index 72bcadf138..f7b6639776 100644 --- a/libs/agno/agno/models/cerebras/cerebras_openai.py +++ b/libs/agno/agno/models/cerebras/cerebras_openai.py @@ -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 diff --git a/libs/agno/agno/os/schema.py b/libs/agno/agno/os/schema.py index ac0259f80e..4b4d084393 100644 --- a/libs/agno/agno/os/schema.py +++ b/libs/agno/agno/os/schema.py @@ -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, diff --git a/libs/agno/agno/team/team.py b/libs/agno/agno/team/team.py index 672064748a..5b1843596d 100644 --- a/libs/agno/agno/team/team.py +++ b/libs/agno/agno/team/team.py @@ -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, diff --git a/libs/agno/agno/utils/agent.py b/libs/agno/agno/utils/agent.py index 99c72edf6e..c59ba5f434 100644 --- a/libs/agno/agno/utils/agent.py +++ b/libs/agno/agno/utils/agent.py @@ -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) diff --git a/libs/agno/pyproject.toml b/libs/agno/pyproject.toml index 993f34239f..f3b76672e7 100644 --- a/libs/agno/pyproject.toml +++ b/libs/agno/pyproject.toml @@ -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" diff --git a/libs/agno/tests/integration/agent/test_instructions.py b/libs/agno/tests/integration/agent/test_instructions.py new file mode 100644 index 0000000000..baedf747c5 --- /dev/null +++ b/libs/agno/tests/integration/agent/test_instructions.py @@ -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") diff --git a/libs/agno/tests/integration/agent/test_output_schema_override.py b/libs/agno/tests/integration/agent/test_output_schema_override.py index 5101da7a5e..16ace35ddb 100644 --- a/libs/agno/tests/integration/agent/test_output_schema_override.py +++ b/libs/agno/tests/integration/agent/test_output_schema_override.py @@ -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") diff --git a/libs/agno/tests/integration/conftest.py b/libs/agno/tests/integration/conftest.py index 6d08eb0ac5..7bee3335ee 100644 --- a/libs/agno/tests/integration/conftest.py +++ b/libs/agno/tests/integration/conftest.py @@ -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.""" diff --git a/libs/agno/tests/integration/knowledge/test_pdf_knowledge.py b/libs/agno/tests/integration/knowledge/test_pdf_knowledge.py index 6696eac254..e5f7c838e2 100644 --- a/libs/agno/tests/integration/knowledge/test_pdf_knowledge.py +++ b/libs/agno/tests/integration/knowledge/test_pdf_knowledge.py @@ -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() diff --git a/libs/agno/tests/integration/models/anthropic/test_format_tools.py b/libs/agno/tests/integration/models/anthropic/test_format_tools.py index 8aa269f7af..0011c077ba 100644 --- a/libs/agno/tests/integration/models/anthropic/test_format_tools.py +++ b/libs/agno/tests/integration/models/anthropic/test_format_tools.py @@ -64,6 +64,7 @@ def test_simple_function_tool(): "units": {"type": "string", "description": "Temperature units (celsius or fahrenheit)"}, }, "required": ["location", "units"], + "additionalProperties": False, }, } ] diff --git a/libs/agno/tests/integration/models/cerebras/cerebras_openai/test_basic.py b/libs/agno/tests/integration/models/cerebras/cerebras_openai/test_basic.py index 23fd553f74..c332a4874a 100644 --- a/libs/agno/tests/integration/models/cerebras/cerebras_openai/test_basic.py +++ b/libs/agno/tests/integration/models/cerebras/cerebras_openai/test_basic.py @@ -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, ) diff --git a/libs/agno/tests/integration/models/cerebras/cerebras_openai/test_structured_response.py b/libs/agno/tests/integration/models/cerebras/cerebras_openai/test_structured_response.py index 27aaad4d1b..0886ad1a2c 100644 --- a/libs/agno/tests/integration/models/cerebras/cerebras_openai/test_structured_response.py +++ b/libs/agno/tests/integration/models/cerebras/cerebras_openai/test_structured_response.py @@ -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, ) diff --git a/libs/agno/tests/integration/models/cerebras/cerebras_openai/test_tool_use.py b/libs/agno/tests/integration/models/cerebras/cerebras_openai/test_tool_use.py index 5664386385..94b5eb90e5 100644 --- a/libs/agno/tests/integration/models/cerebras/cerebras_openai/test_tool_use.py +++ b/libs/agno/tests/integration/models/cerebras/cerebras_openai/test_tool_use.py @@ -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 diff --git a/libs/agno/tests/integration/models/cerebras/test_basic.py b/libs/agno/tests/integration/models/cerebras/test_basic.py index 5c8b47fef5..ca4597209e 100644 --- a/libs/agno/tests/integration/models/cerebras/test_basic.py +++ b/libs/agno/tests/integration/models/cerebras/test_basic.py @@ -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): diff --git a/libs/agno/tests/integration/models/cerebras/test_tool_use.py b/libs/agno/tests/integration/models/cerebras/test_tool_use.py index babe3ba822..554198d8e5 100644 --- a/libs/agno/tests/integration/models/cerebras/test_tool_use.py +++ b/libs/agno/tests/integration/models/cerebras/test_tool_use.py @@ -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, ) diff --git a/libs/agno/tests/integration/models/deepinfra/test_tool_use.py b/libs/agno/tests/integration/models/deepinfra/test_tool_use.py index d134bf021d..3bd6228c74 100644 --- a/libs/agno/tests/integration/models/deepinfra/test_tool_use.py +++ b/libs/agno/tests/integration/models/deepinfra/test_tool_use.py @@ -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 diff --git a/libs/agno/tests/integration/models/groq/test_tool_use.py b/libs/agno/tests/integration/models/groq/test_tool_use.py index 65f6876377..3265677ed3 100644 --- a/libs/agno/tests/integration/models/groq/test_tool_use.py +++ b/libs/agno/tests/integration/models/groq/test_tool_use.py @@ -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(): diff --git a/libs/agno/tests/integration/models/ollama_tools/__init__.py b/libs/agno/tests/integration/models/ollama_tools/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/libs/agno/tests/integration/models/ollama_tools/test_basic.py b/libs/agno/tests/integration/models/ollama_tools/test_basic.py deleted file mode 100644 index ca436324b2..0000000000 --- a/libs/agno/tests/integration/models/ollama_tools/test_basic.py +++ /dev/null @@ -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 diff --git a/libs/agno/tests/integration/models/ollama_tools/test_tool_use.py b/libs/agno/tests/integration/models/ollama_tools/test_tool_use.py deleted file mode 100644 index eb4ef7b000..0000000000 --- a/libs/agno/tests/integration/models/ollama_tools/test_tool_use.py +++ /dev/null @@ -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 diff --git a/libs/agno/tests/integration/models/openai/chat/test_basic.py b/libs/agno/tests/integration/models/openai/chat/test_basic.py index 0fc44c3e15..cb114af83c 100644 --- a/libs/agno/tests/integration/models/openai/chat/test_basic.py +++ b/libs/agno/tests/integration/models/openai/chat/test_basic.py @@ -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) diff --git a/libs/agno/tests/integration/os/test_agent_runs.py b/libs/agno/tests/integration/os/test_agent_runs.py index 3a782e5705..d39b6ddc1f 100644 --- a/libs/agno/tests/integration/os/test_agent_runs.py +++ b/libs/agno/tests/integration/os/test_agent_runs.py @@ -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"} diff --git a/libs/agno/tests/integration/teams/test_input.py b/libs/agno/tests/integration/teams/test_input.py index 22e66627eb..ae69469294 100644 --- a/libs/agno/tests/integration/teams/test_input.py +++ b/libs/agno/tests/integration/teams/test_input.py @@ -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 diff --git a/libs/agno/tests/integration/workflows/test_background_execution.py b/libs/agno/tests/integration/workflows/test_background_execution.py index 1cbcbd410d..0101496165 100644 --- a/libs/agno/tests/integration/workflows/test_background_execution.py +++ b/libs/agno/tests/integration/workflows/test_background_execution.py @@ -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 diff --git a/libs/agno/tests/unit/reader/test_tavily_reader.py b/libs/agno/tests/unit/reader/test_tavily_reader.py index b915db93aa..14ab4efaa9 100644 --- a/libs/agno/tests/unit/reader/test_tavily_reader.py +++ b/libs/agno/tests/unit/reader/test_tavily_reader.py @@ -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 diff --git a/libs/agno_infra/pyproject.toml b/libs/agno_infra/pyproject.toml index 8b31371c89..a8ac2233bf 100644 --- a/libs/agno_infra/pyproject.toml +++ b/libs/agno_infra/pyproject.toml @@ -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" diff --git a/scripts/dev_setup.bat b/scripts/dev_setup.bat index 05ca9ab1b1..8a42c01ba7 100644 --- a/scripts/dev_setup.bat +++ b/scripts/dev_setup.bat @@ -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 diff --git a/scripts/dev_setup.sh b/scripts/dev_setup.sh index 4d301d91ac..89960915a8 100755 --- a/scripts/dev_setup.sh +++ b/scripts/dev_setup.sh @@ -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" \ No newline at end of file diff --git a/scripts/test_setup.bat b/scripts/test_setup.bat new file mode 100644 index 0000000000..05ca9ab1b1 --- /dev/null +++ b/scripts/test_setup.bat @@ -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 diff --git a/scripts/test_setup.sh b/scripts/test_setup.sh new file mode 100755 index 0000000000..c23cc89a89 --- /dev/null +++ b/scripts/test_setup.sh @@ -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" \ No newline at end of file