agno 2.2.13__py3-none-any.whl → 2.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/agent/__init__.py +6 -0
- agno/agent/agent.py +5252 -3145
- agno/agent/remote.py +525 -0
- agno/api/api.py +2 -0
- agno/client/__init__.py +3 -0
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/client/os.py +2669 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/manager.py +2 -2
- agno/db/base.py +927 -6
- agno/db/dynamo/dynamo.py +788 -2
- agno/db/dynamo/schemas.py +128 -0
- agno/db/dynamo/utils.py +26 -3
- agno/db/firestore/firestore.py +674 -50
- agno/db/firestore/schemas.py +41 -0
- agno/db/firestore/utils.py +25 -10
- agno/db/gcs_json/gcs_json_db.py +506 -3
- agno/db/gcs_json/utils.py +14 -2
- agno/db/in_memory/in_memory_db.py +203 -4
- agno/db/in_memory/utils.py +14 -2
- agno/db/json/json_db.py +498 -2
- agno/db/json/utils.py +14 -2
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +977 -0
- agno/db/mongo/async_mongo.py +1013 -39
- agno/db/mongo/mongo.py +684 -4
- agno/db/mongo/schemas.py +48 -0
- agno/db/mongo/utils.py +17 -0
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2958 -0
- agno/db/mysql/mysql.py +722 -53
- agno/db/mysql/schemas.py +77 -11
- agno/db/mysql/utils.py +151 -8
- agno/db/postgres/async_postgres.py +1254 -137
- agno/db/postgres/postgres.py +2316 -93
- agno/db/postgres/schemas.py +153 -21
- agno/db/postgres/utils.py +22 -7
- agno/db/redis/redis.py +531 -3
- agno/db/redis/schemas.py +36 -0
- agno/db/redis/utils.py +31 -15
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +20 -9
- agno/db/singlestore/schemas.py +70 -1
- agno/db/singlestore/singlestore.py +737 -74
- agno/db/singlestore/utils.py +13 -3
- agno/db/sqlite/async_sqlite.py +1069 -89
- agno/db/sqlite/schemas.py +133 -1
- agno/db/sqlite/sqlite.py +2203 -165
- agno/db/sqlite/utils.py +21 -11
- agno/db/surrealdb/models.py +25 -0
- agno/db/surrealdb/surrealdb.py +603 -1
- agno/db/utils.py +60 -0
- agno/eval/__init__.py +26 -3
- agno/eval/accuracy.py +25 -12
- agno/eval/agent_as_judge.py +871 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +10 -4
- agno/eval/reliability.py +22 -13
- agno/eval/utils.py +2 -1
- agno/exceptions.py +42 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +13 -2
- agno/knowledge/__init__.py +4 -0
- agno/knowledge/chunking/code.py +90 -0
- agno/knowledge/chunking/document.py +65 -4
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/markdown.py +102 -11
- agno/knowledge/chunking/recursive.py +2 -2
- agno/knowledge/chunking/semantic.py +130 -48
- agno/knowledge/chunking/strategy.py +18 -0
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/knowledge/embedder/mistral.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/openai.py +16 -12
- agno/knowledge/filesystem.py +412 -0
- agno/knowledge/knowledge.py +4261 -1199
- agno/knowledge/protocol.py +134 -0
- agno/knowledge/reader/arxiv_reader.py +3 -2
- agno/knowledge/reader/base.py +9 -7
- agno/knowledge/reader/csv_reader.py +91 -42
- agno/knowledge/reader/docx_reader.py +9 -10
- agno/knowledge/reader/excel_reader.py +225 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
- agno/knowledge/reader/firecrawl_reader.py +3 -2
- agno/knowledge/reader/json_reader.py +16 -22
- agno/knowledge/reader/markdown_reader.py +15 -14
- agno/knowledge/reader/pdf_reader.py +33 -28
- agno/knowledge/reader/pptx_reader.py +9 -10
- agno/knowledge/reader/reader_factory.py +135 -1
- agno/knowledge/reader/s3_reader.py +8 -16
- agno/knowledge/reader/tavily_reader.py +3 -3
- agno/knowledge/reader/text_reader.py +15 -14
- agno/knowledge/reader/utils/__init__.py +17 -0
- agno/knowledge/reader/utils/spreadsheet.py +114 -0
- agno/knowledge/reader/web_search_reader.py +8 -65
- agno/knowledge/reader/website_reader.py +16 -13
- agno/knowledge/reader/wikipedia_reader.py +36 -3
- agno/knowledge/reader/youtube_reader.py +3 -2
- agno/knowledge/remote_content/__init__.py +33 -0
- agno/knowledge/remote_content/config.py +266 -0
- agno/knowledge/remote_content/remote_content.py +105 -17
- agno/knowledge/utils.py +76 -22
- agno/learn/__init__.py +71 -0
- agno/learn/config.py +463 -0
- agno/learn/curate.py +185 -0
- agno/learn/machine.py +725 -0
- agno/learn/schemas.py +1114 -0
- agno/learn/stores/__init__.py +38 -0
- agno/learn/stores/decision_log.py +1156 -0
- agno/learn/stores/entity_memory.py +3275 -0
- agno/learn/stores/learned_knowledge.py +1583 -0
- agno/learn/stores/protocol.py +117 -0
- agno/learn/stores/session_context.py +1217 -0
- agno/learn/stores/user_memory.py +1495 -0
- agno/learn/stores/user_profile.py +1220 -0
- agno/learn/utils.py +209 -0
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +223 -8
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +434 -59
- agno/models/aws/bedrock.py +121 -20
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +10 -6
- agno/models/azure/openai_chat.py +33 -10
- agno/models/base.py +1162 -561
- agno/models/cerebras/cerebras.py +120 -24
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +65 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +959 -89
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +48 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +88 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +24 -5
- agno/models/meta/llama.py +40 -13
- agno/models/meta/llama_openai.py +22 -21
- agno/models/metrics.py +12 -0
- agno/models/mistral/mistral.py +8 -4
- agno/models/n1n/__init__.py +3 -0
- agno/models/n1n/n1n.py +57 -0
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/__init__.py +2 -0
- agno/models/ollama/chat.py +17 -6
- agno/models/ollama/responses.py +100 -0
- agno/models/openai/__init__.py +2 -0
- agno/models/openai/chat.py +117 -26
- agno/models/openai/open_responses.py +46 -0
- agno/models/openai/responses.py +110 -32
- agno/models/openrouter/__init__.py +2 -0
- agno/models/openrouter/openrouter.py +67 -2
- agno/models/openrouter/responses.py +146 -0
- agno/models/perplexity/perplexity.py +19 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +19 -2
- agno/models/response.py +20 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/claude.py +124 -4
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +467 -137
- agno/os/auth.py +253 -5
- agno/os/config.py +22 -0
- agno/os/interfaces/a2a/a2a.py +7 -6
- agno/os/interfaces/a2a/router.py +635 -26
- agno/os/interfaces/a2a/utils.py +32 -33
- agno/os/interfaces/agui/agui.py +5 -3
- agno/os/interfaces/agui/router.py +26 -16
- agno/os/interfaces/agui/utils.py +97 -57
- agno/os/interfaces/base.py +7 -7
- agno/os/interfaces/slack/router.py +16 -7
- agno/os/interfaces/slack/slack.py +7 -7
- agno/os/interfaces/whatsapp/router.py +35 -7
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/interfaces/whatsapp/whatsapp.py +11 -8
- agno/os/managers.py +326 -0
- agno/os/mcp.py +652 -79
- agno/os/middleware/__init__.py +4 -0
- agno/os/middleware/jwt.py +718 -115
- agno/os/middleware/trailing_slash.py +27 -0
- agno/os/router.py +105 -1558
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +655 -0
- agno/os/routers/agents/schema.py +288 -0
- agno/os/routers/components/__init__.py +3 -0
- agno/os/routers/components/components.py +475 -0
- agno/os/routers/database.py +155 -0
- agno/os/routers/evals/evals.py +111 -18
- agno/os/routers/evals/schemas.py +38 -5
- agno/os/routers/evals/utils.py +80 -11
- agno/os/routers/health.py +3 -3
- agno/os/routers/knowledge/knowledge.py +284 -35
- agno/os/routers/knowledge/schemas.py +14 -2
- agno/os/routers/memory/memory.py +274 -11
- agno/os/routers/memory/schemas.py +44 -3
- agno/os/routers/metrics/metrics.py +30 -15
- agno/os/routers/metrics/schemas.py +10 -6
- agno/os/routers/registry/__init__.py +3 -0
- agno/os/routers/registry/registry.py +337 -0
- agno/os/routers/session/session.py +143 -14
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +550 -0
- agno/os/routers/teams/schema.py +280 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +549 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +757 -0
- agno/os/routers/workflows/schema.py +139 -0
- agno/os/schema.py +157 -584
- agno/os/scopes.py +469 -0
- agno/os/settings.py +3 -0
- agno/os/utils.py +574 -185
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +102 -2
- agno/reasoning/default.py +6 -7
- agno/reasoning/gemini.py +87 -3
- agno/reasoning/groq.py +109 -2
- agno/reasoning/helpers.py +6 -7
- agno/reasoning/manager.py +1238 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +115 -1
- agno/reasoning/vertexai.py +85 -1
- agno/registry/__init__.py +3 -0
- agno/registry/registry.py +68 -0
- agno/remote/__init__.py +3 -0
- agno/remote/base.py +581 -0
- agno/run/__init__.py +2 -4
- agno/run/agent.py +134 -19
- agno/run/base.py +49 -1
- agno/run/cancel.py +65 -52
- agno/run/cancellation_management/__init__.py +9 -0
- agno/run/cancellation_management/base.py +78 -0
- agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
- agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +111 -19
- agno/run/workflow.py +2 -1
- agno/session/agent.py +57 -92
- agno/session/summary.py +1 -1
- agno/session/team.py +62 -115
- agno/session/workflow.py +353 -57
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +377 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/table.py +10 -0
- agno/team/__init__.py +5 -1
- agno/team/remote.py +447 -0
- agno/team/team.py +3769 -2202
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +225 -16
- agno/tools/crawl4ai.py +3 -0
- agno/tools/duckduckgo.py +25 -71
- agno/tools/exa.py +0 -21
- agno/tools/file.py +14 -13
- agno/tools/file_generation.py +12 -6
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +94 -113
- agno/tools/google_bigquery.py +11 -2
- agno/tools/google_drive.py +4 -3
- agno/tools/knowledge.py +9 -4
- agno/tools/mcp/mcp.py +301 -18
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/tools/mem0.py +11 -10
- agno/tools/memory.py +47 -46
- agno/tools/mlx_transcribe.py +10 -7
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/parallel.py +0 -7
- agno/tools/postgres.py +76 -36
- agno/tools/python.py +14 -6
- agno/tools/reasoning.py +30 -23
- agno/tools/redshift.py +406 -0
- agno/tools/shopify.py +1519 -0
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +4 -1
- agno/tools/toolkit.py +253 -18
- agno/tools/websearch.py +93 -0
- agno/tools/website.py +1 -1
- agno/tools/wikipedia.py +1 -1
- agno/tools/workflow.py +56 -48
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +161 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +112 -0
- agno/utils/agent.py +251 -10
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +264 -7
- agno/utils/hooks.py +111 -3
- agno/utils/http.py +161 -2
- agno/utils/mcp.py +49 -8
- agno/utils/media.py +22 -1
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +20 -5
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/os.py +0 -0
- agno/utils/print_response/agent.py +99 -16
- agno/utils/print_response/team.py +223 -24
- agno/utils/print_response/workflow.py +0 -2
- agno/utils/prompts.py +8 -6
- agno/utils/remote.py +23 -0
- agno/utils/response.py +1 -13
- agno/utils/string.py +91 -2
- agno/utils/team.py +62 -12
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +15 -2
- agno/vectordb/cassandra/cassandra.py +1 -1
- agno/vectordb/chroma/__init__.py +2 -1
- agno/vectordb/chroma/chromadb.py +468 -23
- agno/vectordb/clickhouse/clickhousedb.py +1 -1
- agno/vectordb/couchbase/couchbase.py +6 -2
- agno/vectordb/lancedb/lance_db.py +7 -38
- agno/vectordb/lightrag/lightrag.py +7 -6
- agno/vectordb/milvus/milvus.py +118 -84
- agno/vectordb/mongodb/__init__.py +2 -1
- agno/vectordb/mongodb/mongodb.py +14 -31
- agno/vectordb/pgvector/pgvector.py +120 -66
- agno/vectordb/pineconedb/pineconedb.py +2 -19
- agno/vectordb/qdrant/__init__.py +2 -1
- agno/vectordb/qdrant/qdrant.py +33 -56
- agno/vectordb/redis/__init__.py +2 -1
- agno/vectordb/redis/redisdb.py +19 -31
- agno/vectordb/singlestore/singlestore.py +17 -9
- agno/vectordb/surrealdb/surrealdb.py +2 -38
- agno/vectordb/weaviate/__init__.py +2 -1
- agno/vectordb/weaviate/weaviate.py +7 -3
- agno/workflow/__init__.py +5 -1
- agno/workflow/agent.py +2 -2
- agno/workflow/condition.py +12 -10
- agno/workflow/loop.py +28 -9
- agno/workflow/parallel.py +21 -13
- agno/workflow/remote.py +362 -0
- agno/workflow/router.py +12 -9
- agno/workflow/step.py +261 -36
- agno/workflow/steps.py +12 -8
- agno/workflow/types.py +40 -77
- agno/workflow/workflow.py +939 -213
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
- agno-2.4.3.dist-info/RECORD +677 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
- agno/tools/googlesearch.py +0 -98
- agno/tools/memori.py +0 -339
- agno-2.2.13.dist-info/RECORD +0 -575
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/top_level.txt +0 -0
agno/tools/mcp/multi_mcp.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import time
|
|
3
|
+
import warnings
|
|
1
4
|
import weakref
|
|
2
5
|
from contextlib import AsyncExitStack
|
|
3
6
|
from dataclasses import asdict
|
|
4
7
|
from datetime import timedelta
|
|
5
8
|
from types import TracebackType
|
|
6
|
-
from typing import List, Literal, Optional, Union
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Tuple, Union
|
|
7
10
|
|
|
8
11
|
from agno.tools import Toolkit
|
|
9
12
|
from agno.tools.function import Function
|
|
@@ -11,6 +14,11 @@ from agno.tools.mcp.params import SSEClientParams, StreamableHTTPClientParams
|
|
|
11
14
|
from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
12
15
|
from agno.utils.mcp import get_entrypoint_for_tool, prepare_command
|
|
13
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from agno.agent import Agent
|
|
19
|
+
from agno.run import RunContext
|
|
20
|
+
from agno.team.team import Team
|
|
21
|
+
|
|
14
22
|
try:
|
|
15
23
|
from mcp import ClientSession, StdioServerParameters
|
|
16
24
|
from mcp.client.sse import sse_client
|
|
@@ -47,6 +55,7 @@ class MultiMCPTools(Toolkit):
|
|
|
47
55
|
exclude_tools: Optional[list[str]] = None,
|
|
48
56
|
refresh_connection: bool = False,
|
|
49
57
|
allow_partial_failure: bool = False,
|
|
58
|
+
header_provider: Optional[Callable[..., dict[str, Any]]] = None,
|
|
50
59
|
**kwargs,
|
|
51
60
|
):
|
|
52
61
|
"""
|
|
@@ -64,7 +73,14 @@ class MultiMCPTools(Toolkit):
|
|
|
64
73
|
exclude_tools: Optional list of tool names to exclude (if None, excludes none).
|
|
65
74
|
allow_partial_failure: If True, allows toolkit to initialize even if some MCP servers fail to connect. If False, any failure will raise an exception.
|
|
66
75
|
refresh_connection: If True, the connection and tools will be refreshed on each run
|
|
76
|
+
header_provider: Header provider function for all servers. Takes RunContext and returns dict of HTTP headers.
|
|
67
77
|
"""
|
|
78
|
+
warnings.warn(
|
|
79
|
+
"The MultiMCPTools class is deprecated and will be removed in a future version. Please use multiple MCPTools instances instead.",
|
|
80
|
+
DeprecationWarning,
|
|
81
|
+
stacklevel=2,
|
|
82
|
+
)
|
|
83
|
+
|
|
68
84
|
super().__init__(name="MultiMCPTools", **kwargs)
|
|
69
85
|
|
|
70
86
|
if urls_transports is not None:
|
|
@@ -86,6 +102,16 @@ class MultiMCPTools(Toolkit):
|
|
|
86
102
|
self.exclude_tools = exclude_tools
|
|
87
103
|
self.refresh_connection = refresh_connection
|
|
88
104
|
|
|
105
|
+
self.header_provider = header_provider
|
|
106
|
+
|
|
107
|
+
# Validate header_provider signature
|
|
108
|
+
if header_provider:
|
|
109
|
+
try:
|
|
110
|
+
# Just verify we can inspect the signature - no parameter requirements
|
|
111
|
+
inspect.signature(header_provider)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
log_warning(f"Could not validate header_provider signature: {e}")
|
|
114
|
+
|
|
89
115
|
if server_params_list is None and commands is None and urls is None:
|
|
90
116
|
raise ValueError("Either server_params_list or commands or urls must be provided")
|
|
91
117
|
|
|
@@ -130,6 +156,14 @@ class MultiMCPTools(Toolkit):
|
|
|
130
156
|
self._connection_task = None
|
|
131
157
|
self._successful_connections = 0
|
|
132
158
|
self._sessions: list[ClientSession] = []
|
|
159
|
+
self._session_to_server_idx: Dict[int, int] = {} # Maps session list index to server params index
|
|
160
|
+
|
|
161
|
+
# Session management for per-agent-run sessions with dynamic headers
|
|
162
|
+
# For MultiMCP, we track sessions per (run_id, server_idx) since we have multiple servers
|
|
163
|
+
# Maps (run_id, server_idx) to (session, timestamp) for TTL-based cleanup
|
|
164
|
+
self._run_sessions: Dict[Tuple[str, int], Tuple[ClientSession, float]] = {}
|
|
165
|
+
self._run_session_contexts: Dict[Tuple[str, int], Any] = {} # Maps (run_id, server_idx) to context managers
|
|
166
|
+
self._session_ttl_seconds: float = 300.0 # 5 minutes default TTL
|
|
133
167
|
|
|
134
168
|
self.allow_partial_failure = allow_partial_failure
|
|
135
169
|
|
|
@@ -153,6 +187,205 @@ class MultiMCPTools(Toolkit):
|
|
|
153
187
|
except (RuntimeError, BaseException):
|
|
154
188
|
return False
|
|
155
189
|
|
|
190
|
+
def _call_header_provider(
|
|
191
|
+
self,
|
|
192
|
+
run_context: Optional["RunContext"] = None,
|
|
193
|
+
agent: Optional["Agent"] = None,
|
|
194
|
+
team: Optional["Team"] = None,
|
|
195
|
+
) -> dict[str, Any]:
|
|
196
|
+
"""Call the header_provider with run_context, agent, and/or team based on its signature.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
run_context: The RunContext for the current agent run
|
|
200
|
+
agent: The Agent instance (if running within an agent)
|
|
201
|
+
team: The Team instance (if running within a team)
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
dict[str, Any]: The headers returned by the header_provider
|
|
205
|
+
"""
|
|
206
|
+
header_provider = getattr(self, "header_provider", None)
|
|
207
|
+
if header_provider is None:
|
|
208
|
+
return {}
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
sig = inspect.signature(header_provider)
|
|
212
|
+
param_names = set(sig.parameters.keys())
|
|
213
|
+
|
|
214
|
+
# Build kwargs based on what the function accepts
|
|
215
|
+
call_kwargs: dict[str, Any] = {}
|
|
216
|
+
|
|
217
|
+
if "run_context" in param_names:
|
|
218
|
+
call_kwargs["run_context"] = run_context
|
|
219
|
+
if "agent" in param_names:
|
|
220
|
+
call_kwargs["agent"] = agent
|
|
221
|
+
if "team" in param_names:
|
|
222
|
+
call_kwargs["team"] = team
|
|
223
|
+
|
|
224
|
+
# Check if function accepts **kwargs (VAR_KEYWORD)
|
|
225
|
+
has_var_keyword = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values())
|
|
226
|
+
|
|
227
|
+
if has_var_keyword:
|
|
228
|
+
# Pass all available context to **kwargs
|
|
229
|
+
call_kwargs = {"run_context": run_context, "agent": agent, "team": team}
|
|
230
|
+
return header_provider(**call_kwargs)
|
|
231
|
+
elif call_kwargs:
|
|
232
|
+
return header_provider(**call_kwargs)
|
|
233
|
+
else:
|
|
234
|
+
# Function takes no recognized parameters - check for positional
|
|
235
|
+
positional_params = [
|
|
236
|
+
p
|
|
237
|
+
for p in sig.parameters.values()
|
|
238
|
+
if p.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
239
|
+
]
|
|
240
|
+
if positional_params:
|
|
241
|
+
# Legacy support: pass run_context as first positional arg
|
|
242
|
+
return header_provider(run_context)
|
|
243
|
+
else:
|
|
244
|
+
# Function takes no parameters
|
|
245
|
+
return header_provider()
|
|
246
|
+
except Exception as e:
|
|
247
|
+
log_warning(f"Error calling header_provider: {e}")
|
|
248
|
+
return {}
|
|
249
|
+
|
|
250
|
+
async def _cleanup_stale_sessions(self) -> None:
|
|
251
|
+
"""Clean up sessions older than TTL to prevent memory leaks."""
|
|
252
|
+
if not self._run_sessions:
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
now = time.time()
|
|
256
|
+
stale_keys = [
|
|
257
|
+
cache_key
|
|
258
|
+
for cache_key, (_, created_at) in self._run_sessions.items()
|
|
259
|
+
if now - created_at > self._session_ttl_seconds
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
for run_id, server_idx in stale_keys:
|
|
263
|
+
log_debug(f"Cleaning up stale session for run_id={run_id}, server_idx={server_idx}")
|
|
264
|
+
await self.cleanup_run_session(run_id, server_idx)
|
|
265
|
+
|
|
266
|
+
async def get_session_for_run(
|
|
267
|
+
self,
|
|
268
|
+
run_context: Optional["RunContext"] = None,
|
|
269
|
+
server_idx: int = 0,
|
|
270
|
+
agent: Optional["Agent"] = None,
|
|
271
|
+
team: Optional["Team"] = None,
|
|
272
|
+
) -> ClientSession:
|
|
273
|
+
"""
|
|
274
|
+
Get or create a session for the given run_context and server index.
|
|
275
|
+
|
|
276
|
+
If header_provider is configured and run_context is provided, this creates
|
|
277
|
+
a new session with dynamic headers for this specific agent run and server.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
run_context: The RunContext containing user_id, metadata, etc.
|
|
281
|
+
server_idx: Index of the server in self._sessions list
|
|
282
|
+
agent: The Agent instance (if running within an agent)
|
|
283
|
+
team: The Team instance (if running within a team)
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
ClientSession: Either the default session or a per-run session with dynamic headers
|
|
287
|
+
"""
|
|
288
|
+
# If no header_provider or no run_context, use the default session
|
|
289
|
+
if not self.header_provider or not run_context:
|
|
290
|
+
# Return the default session for this server
|
|
291
|
+
if server_idx < len(self._sessions):
|
|
292
|
+
return self._sessions[server_idx]
|
|
293
|
+
raise ValueError(f"Server index {server_idx} out of range")
|
|
294
|
+
|
|
295
|
+
# Lazy cleanup of stale sessions
|
|
296
|
+
await self._cleanup_stale_sessions()
|
|
297
|
+
|
|
298
|
+
# Check if we already have a session for this (run_id, server_idx)
|
|
299
|
+
run_id = run_context.run_id
|
|
300
|
+
cache_key = (run_id, server_idx)
|
|
301
|
+
if cache_key in self._run_sessions:
|
|
302
|
+
session, _ = self._run_sessions[cache_key]
|
|
303
|
+
return session
|
|
304
|
+
|
|
305
|
+
# Create a new session with dynamic headers for this run and server
|
|
306
|
+
log_debug(f"Creating new session for run_id={run_id}, server_idx={server_idx} with dynamic headers")
|
|
307
|
+
|
|
308
|
+
# Generate dynamic headers from the provider
|
|
309
|
+
dynamic_headers = self._call_header_provider(run_context=run_context, agent=agent, team=team)
|
|
310
|
+
|
|
311
|
+
# Get the server params for this server index
|
|
312
|
+
if server_idx >= len(self.server_params_list):
|
|
313
|
+
raise ValueError(f"Server index {server_idx} out of range")
|
|
314
|
+
|
|
315
|
+
server_params = self.server_params_list[server_idx]
|
|
316
|
+
|
|
317
|
+
# Create new session with merged headers based on transport type
|
|
318
|
+
if isinstance(server_params, SSEClientParams):
|
|
319
|
+
params_dict = asdict(server_params)
|
|
320
|
+
existing_headers = params_dict.get("headers") or {}
|
|
321
|
+
params_dict["headers"] = {**existing_headers, **dynamic_headers}
|
|
322
|
+
|
|
323
|
+
context = sse_client(**params_dict) # type: ignore
|
|
324
|
+
client_timeout = min(self.timeout_seconds, params_dict.get("timeout", self.timeout_seconds))
|
|
325
|
+
|
|
326
|
+
elif isinstance(server_params, StreamableHTTPClientParams):
|
|
327
|
+
params_dict = asdict(server_params)
|
|
328
|
+
existing_headers = params_dict.get("headers") or {}
|
|
329
|
+
params_dict["headers"] = {**existing_headers, **dynamic_headers}
|
|
330
|
+
|
|
331
|
+
context = streamablehttp_client(**params_dict) # type: ignore
|
|
332
|
+
params_timeout = params_dict.get("timeout", self.timeout_seconds)
|
|
333
|
+
if isinstance(params_timeout, timedelta):
|
|
334
|
+
params_timeout = int(params_timeout.total_seconds())
|
|
335
|
+
client_timeout = min(self.timeout_seconds, params_timeout)
|
|
336
|
+
else:
|
|
337
|
+
# stdio doesn't support headers, fall back to default session
|
|
338
|
+
log_warning(
|
|
339
|
+
f"Cannot use dynamic headers with stdio transport for server {server_idx}, using default session"
|
|
340
|
+
)
|
|
341
|
+
if server_idx < len(self._sessions):
|
|
342
|
+
return self._sessions[server_idx]
|
|
343
|
+
raise ValueError(f"Server index {server_idx} out of range")
|
|
344
|
+
|
|
345
|
+
# Enter the context and create session
|
|
346
|
+
session_params = await context.__aenter__() # type: ignore
|
|
347
|
+
read, write = session_params[0:2]
|
|
348
|
+
|
|
349
|
+
session_context = ClientSession(read, write, read_timeout_seconds=timedelta(seconds=client_timeout)) # type: ignore
|
|
350
|
+
session = await session_context.__aenter__() # type: ignore
|
|
351
|
+
|
|
352
|
+
# Initialize the session
|
|
353
|
+
await session.initialize()
|
|
354
|
+
|
|
355
|
+
# Store the session with timestamp and context for cleanup
|
|
356
|
+
self._run_sessions[cache_key] = (session, time.time())
|
|
357
|
+
self._run_session_contexts[cache_key] = (context, session_context)
|
|
358
|
+
|
|
359
|
+
return session
|
|
360
|
+
|
|
361
|
+
async def cleanup_run_session(self, run_id: str, server_idx: int) -> None:
|
|
362
|
+
"""Clean up a per-run session."""
|
|
363
|
+
cache_key = (run_id, server_idx)
|
|
364
|
+
if cache_key not in self._run_sessions:
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
context, session_context = self._run_session_contexts[cache_key]
|
|
369
|
+
|
|
370
|
+
# Exit session context - silently ignore errors
|
|
371
|
+
try:
|
|
372
|
+
await session_context.__aexit__(None, None, None)
|
|
373
|
+
except (RuntimeError, Exception):
|
|
374
|
+
pass # Silently ignore
|
|
375
|
+
|
|
376
|
+
# Exit transport context - silently ignore errors
|
|
377
|
+
try:
|
|
378
|
+
await context.__aexit__(None, None, None)
|
|
379
|
+
except (RuntimeError, Exception):
|
|
380
|
+
pass # Silently ignore
|
|
381
|
+
|
|
382
|
+
except Exception:
|
|
383
|
+
pass # Silently ignore all cleanup errors
|
|
384
|
+
finally:
|
|
385
|
+
# Remove from cache
|
|
386
|
+
self._run_sessions.pop(cache_key, None)
|
|
387
|
+
self._run_session_contexts.pop(cache_key, None)
|
|
388
|
+
|
|
156
389
|
async def connect(self, force: bool = False):
|
|
157
390
|
"""Initialize a MultiMCPTools instance and connect to the MCP servers"""
|
|
158
391
|
|
|
@@ -214,7 +447,7 @@ class MultiMCPTools(Toolkit):
|
|
|
214
447
|
|
|
215
448
|
server_connection_errors = []
|
|
216
449
|
|
|
217
|
-
for server_params in self.server_params_list:
|
|
450
|
+
for server_idx, server_params in enumerate(self.server_params_list):
|
|
218
451
|
try:
|
|
219
452
|
# Handle stdio connections
|
|
220
453
|
if isinstance(server_params, StdioServerParameters):
|
|
@@ -223,7 +456,7 @@ class MultiMCPTools(Toolkit):
|
|
|
223
456
|
session = await self._async_exit_stack.enter_async_context(
|
|
224
457
|
ClientSession(read, write, read_timeout_seconds=timedelta(seconds=self.timeout_seconds))
|
|
225
458
|
)
|
|
226
|
-
await self.initialize(session)
|
|
459
|
+
await self.initialize(session, server_idx)
|
|
227
460
|
self._successful_connections += 1
|
|
228
461
|
|
|
229
462
|
# Handle SSE connections
|
|
@@ -233,7 +466,7 @@ class MultiMCPTools(Toolkit):
|
|
|
233
466
|
)
|
|
234
467
|
read, write = client_connection
|
|
235
468
|
session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
|
|
236
|
-
await self.initialize(session)
|
|
469
|
+
await self.initialize(session, server_idx)
|
|
237
470
|
self._successful_connections += 1
|
|
238
471
|
|
|
239
472
|
# Handle Streamable HTTP connections
|
|
@@ -243,7 +476,7 @@ class MultiMCPTools(Toolkit):
|
|
|
243
476
|
)
|
|
244
477
|
read, write = client_connection[0:2]
|
|
245
478
|
session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
|
|
246
|
-
await self.initialize(session)
|
|
479
|
+
await self.initialize(session, server_idx)
|
|
247
480
|
self._successful_connections += 1
|
|
248
481
|
|
|
249
482
|
except Exception as e:
|
|
@@ -268,13 +501,26 @@ class MultiMCPTools(Toolkit):
|
|
|
268
501
|
if not self._initialized:
|
|
269
502
|
return
|
|
270
503
|
|
|
271
|
-
|
|
272
|
-
await self._async_exit_stack.aclose()
|
|
273
|
-
self._sessions = []
|
|
274
|
-
self._successful_connections = 0
|
|
504
|
+
import warnings
|
|
275
505
|
|
|
276
|
-
|
|
277
|
-
|
|
506
|
+
# Suppress async generator cleanup warnings
|
|
507
|
+
with warnings.catch_warnings():
|
|
508
|
+
warnings.filterwarnings("ignore", category=RuntimeWarning, message=".*async_generator.*")
|
|
509
|
+
warnings.filterwarnings("ignore", message=".*cancel scope.*")
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
# Clean up all per-run sessions first
|
|
513
|
+
cache_keys = list(self._run_sessions.keys())
|
|
514
|
+
for run_id, server_idx in cache_keys:
|
|
515
|
+
await self.cleanup_run_session(run_id, server_idx)
|
|
516
|
+
|
|
517
|
+
# Clean up main sessions
|
|
518
|
+
await self._async_exit_stack.aclose()
|
|
519
|
+
self._sessions = []
|
|
520
|
+
self._successful_connections = 0
|
|
521
|
+
|
|
522
|
+
except (RuntimeError, BaseException):
|
|
523
|
+
pass # Silently ignore all cleanup errors
|
|
278
524
|
|
|
279
525
|
self._initialized = False
|
|
280
526
|
|
|
@@ -298,7 +544,7 @@ class MultiMCPTools(Toolkit):
|
|
|
298
544
|
self._successful_connections = 0
|
|
299
545
|
|
|
300
546
|
async def build_tools(self) -> None:
|
|
301
|
-
for session in self._sessions:
|
|
547
|
+
for session_list_idx, session in enumerate(self._sessions):
|
|
302
548
|
# Get the list of tools from the MCP server
|
|
303
549
|
available_tools = await session.list_tools()
|
|
304
550
|
|
|
@@ -314,7 +560,12 @@ class MultiMCPTools(Toolkit):
|
|
|
314
560
|
for tool in filtered_tools:
|
|
315
561
|
try:
|
|
316
562
|
# Get an entrypoint for the tool
|
|
317
|
-
entrypoint = get_entrypoint_for_tool(
|
|
563
|
+
entrypoint = get_entrypoint_for_tool(
|
|
564
|
+
tool=tool,
|
|
565
|
+
session=session,
|
|
566
|
+
mcp_tools_instance=self, # Pass self to enable dynamic headers
|
|
567
|
+
server_idx=session_list_idx, # Pass session list index for session lookup
|
|
568
|
+
)
|
|
318
569
|
|
|
319
570
|
# Create a Function for the tool
|
|
320
571
|
f = Function(
|
|
@@ -333,14 +584,18 @@ class MultiMCPTools(Toolkit):
|
|
|
333
584
|
log_error(f"Failed to register tool {tool.name}: {e}")
|
|
334
585
|
raise
|
|
335
586
|
|
|
336
|
-
async def initialize(self, session: ClientSession) -> None:
|
|
587
|
+
async def initialize(self, session: ClientSession, server_idx: int = 0) -> None:
|
|
337
588
|
"""Initialize the MCP toolkit by getting available tools from the MCP server"""
|
|
338
589
|
|
|
339
590
|
try:
|
|
340
591
|
# Initialize the session if not already initialized
|
|
341
592
|
await session.initialize()
|
|
342
593
|
|
|
594
|
+
# Track which server index this session belongs to
|
|
595
|
+
session_list_idx = len(self._sessions)
|
|
343
596
|
self._sessions.append(session)
|
|
597
|
+
self._session_to_server_idx[session_list_idx] = server_idx
|
|
598
|
+
|
|
344
599
|
self._initialized = True
|
|
345
600
|
except Exception as e:
|
|
346
601
|
log_error(f"Failed to get MCP tools: {e}")
|
agno/tools/mem0.py
CHANGED
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
from os import getenv
|
|
3
3
|
from typing import Any, Dict, List, Optional, Union
|
|
4
4
|
|
|
5
|
+
from agno.run import RunContext
|
|
5
6
|
from agno.tools import Toolkit
|
|
6
7
|
from agno.utils.log import log_debug, log_error, log_warning
|
|
7
8
|
|
|
@@ -68,13 +69,13 @@ class Mem0Tools(Toolkit):
|
|
|
68
69
|
def _get_user_id(
|
|
69
70
|
self,
|
|
70
71
|
method_name: str,
|
|
71
|
-
|
|
72
|
+
run_context: RunContext,
|
|
72
73
|
) -> str:
|
|
73
74
|
"""Resolve the user ID"""
|
|
74
75
|
resolved_user_id = self.user_id
|
|
75
76
|
if not resolved_user_id:
|
|
76
77
|
try:
|
|
77
|
-
resolved_user_id =
|
|
78
|
+
resolved_user_id = run_context.user_id
|
|
78
79
|
except Exception:
|
|
79
80
|
pass
|
|
80
81
|
if not resolved_user_id:
|
|
@@ -85,7 +86,7 @@ class Mem0Tools(Toolkit):
|
|
|
85
86
|
|
|
86
87
|
def add_memory(
|
|
87
88
|
self,
|
|
88
|
-
|
|
89
|
+
run_context: RunContext,
|
|
89
90
|
content: Union[str, Dict[str, str]],
|
|
90
91
|
) -> str:
|
|
91
92
|
"""Add facts to the user's memory.
|
|
@@ -98,7 +99,7 @@ class Mem0Tools(Toolkit):
|
|
|
98
99
|
str: JSON-encoded Mem0 response or an error message.
|
|
99
100
|
"""
|
|
100
101
|
|
|
101
|
-
resolved_user_id = self._get_user_id("add_memory",
|
|
102
|
+
resolved_user_id = self._get_user_id("add_memory", run_context=run_context)
|
|
102
103
|
if isinstance(resolved_user_id, str) and resolved_user_id.startswith("Error in add_memory:"):
|
|
103
104
|
return resolved_user_id
|
|
104
105
|
try:
|
|
@@ -121,12 +122,12 @@ class Mem0Tools(Toolkit):
|
|
|
121
122
|
|
|
122
123
|
def search_memory(
|
|
123
124
|
self,
|
|
124
|
-
|
|
125
|
+
run_context: RunContext,
|
|
125
126
|
query: str,
|
|
126
127
|
) -> str:
|
|
127
128
|
"""Semantic search for *query* across the user's stored memories."""
|
|
128
129
|
|
|
129
|
-
resolved_user_id = self._get_user_id("search_memory",
|
|
130
|
+
resolved_user_id = self._get_user_id("search_memory", run_context=run_context)
|
|
130
131
|
if isinstance(resolved_user_id, str) and resolved_user_id.startswith("Error in search_memory:"):
|
|
131
132
|
return resolved_user_id
|
|
132
133
|
try:
|
|
@@ -151,10 +152,10 @@ class Mem0Tools(Toolkit):
|
|
|
151
152
|
log_error(f"Error searching memory: {e}")
|
|
152
153
|
return f"Error searching memory: {e}"
|
|
153
154
|
|
|
154
|
-
def get_all_memories(self,
|
|
155
|
+
def get_all_memories(self, run_context: RunContext) -> str:
|
|
155
156
|
"""Return **all** memories for the current user as a JSON string."""
|
|
156
157
|
|
|
157
|
-
resolved_user_id = self._get_user_id("get_all_memories",
|
|
158
|
+
resolved_user_id = self._get_user_id("get_all_memories", run_context=run_context)
|
|
158
159
|
if isinstance(resolved_user_id, str) and resolved_user_id.startswith("Error in get_all_memories:"):
|
|
159
160
|
return resolved_user_id
|
|
160
161
|
try:
|
|
@@ -177,10 +178,10 @@ class Mem0Tools(Toolkit):
|
|
|
177
178
|
log_error(f"Error getting all memories: {e}")
|
|
178
179
|
return f"Error getting all memories: {e}"
|
|
179
180
|
|
|
180
|
-
def delete_all_memories(self,
|
|
181
|
+
def delete_all_memories(self, run_context: RunContext) -> str:
|
|
181
182
|
"""Delete *all* memories associated with the current user"""
|
|
182
183
|
|
|
183
|
-
resolved_user_id = self._get_user_id("delete_all_memories",
|
|
184
|
+
resolved_user_id = self._get_user_id("delete_all_memories", run_context=run_context)
|
|
184
185
|
if isinstance(resolved_user_id, str) and resolved_user_id.startswith("Error in delete_all_memories:"):
|
|
185
186
|
error_msg = resolved_user_id
|
|
186
187
|
log_error(error_msg)
|