agno 2.0.1__py3-none-any.whl → 2.3.0__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/agent.py +6015 -2823
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +385 -6
- agno/db/dynamo/dynamo.py +388 -81
- agno/db/dynamo/schemas.py +47 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +435 -64
- agno/db/firestore/schemas.py +11 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +384 -42
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +351 -66
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +339 -48
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +510 -37
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2036 -0
- agno/db/mongo/mongo.py +653 -76
- agno/db/mongo/schemas.py +13 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/mysql.py +687 -25
- agno/db/mysql/schemas.py +61 -37
- agno/db/mysql/utils.py +60 -2
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2001 -0
- agno/db/postgres/postgres.py +676 -57
- agno/db/postgres/schemas.py +43 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +344 -38
- agno/db/redis/schemas.py +18 -0
- agno/db/redis/utils.py +60 -2
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/memory.py +13 -0
- agno/db/singlestore/schemas.py +26 -1
- agno/db/singlestore/singlestore.py +687 -53
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2371 -0
- agno/db/sqlite/schemas.py +24 -0
- agno/db/sqlite/sqlite.py +774 -85
- agno/db/sqlite/utils.py +168 -5
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +309 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1361 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +50 -22
- agno/eval/accuracy.py +50 -43
- agno/eval/performance.py +6 -3
- agno/eval/reliability.py +6 -3
- agno/eval/utils.py +33 -16
- agno/exceptions.py +68 -1
- agno/filters.py +354 -0
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +52 -0
- agno/integrations/discord/client.py +1 -0
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +1 -1
- agno/knowledge/chunking/semantic.py +40 -8
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/aws_bedrock.py +9 -4
- agno/knowledge/embedder/azure_openai.py +54 -0
- agno/knowledge/embedder/base.py +2 -0
- agno/knowledge/embedder/cohere.py +184 -5
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/google.py +79 -1
- agno/knowledge/embedder/huggingface.py +9 -4
- agno/knowledge/embedder/jina.py +63 -0
- agno/knowledge/embedder/mistral.py +78 -11
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +13 -0
- agno/knowledge/embedder/openai.py +37 -65
- agno/knowledge/embedder/sentence_transformer.py +8 -4
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/embedder/voyageai.py +69 -16
- agno/knowledge/knowledge.py +594 -186
- agno/knowledge/reader/base.py +9 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
- agno/knowledge/reader/json_reader.py +6 -5
- agno/knowledge/reader/markdown_reader.py +13 -13
- agno/knowledge/reader/pdf_reader.py +43 -68
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +51 -6
- agno/knowledge/reader/s3_reader.py +3 -15
- agno/knowledge/reader/tavily_reader.py +194 -0
- agno/knowledge/reader/text_reader.py +13 -13
- agno/knowledge/reader/web_search_reader.py +2 -43
- agno/knowledge/reader/website_reader.py +43 -25
- agno/knowledge/reranker/__init__.py +2 -8
- agno/knowledge/types.py +9 -0
- agno/knowledge/utils.py +20 -0
- agno/media.py +72 -0
- agno/memory/manager.py +336 -82
- agno/models/aimlapi/aimlapi.py +2 -2
- agno/models/anthropic/claude.py +183 -37
- agno/models/aws/bedrock.py +52 -112
- agno/models/aws/claude.py +33 -1
- agno/models/azure/ai_foundry.py +33 -15
- agno/models/azure/openai_chat.py +25 -8
- agno/models/base.py +999 -519
- agno/models/cerebras/cerebras.py +19 -13
- agno/models/cerebras/cerebras_openai.py +8 -5
- agno/models/cohere/chat.py +27 -1
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +57 -0
- agno/models/dashscope/dashscope.py +1 -0
- agno/models/deepinfra/deepinfra.py +2 -2
- agno/models/deepseek/deepseek.py +2 -2
- agno/models/fireworks/fireworks.py +2 -2
- agno/models/google/gemini.py +103 -31
- agno/models/groq/groq.py +28 -11
- agno/models/huggingface/huggingface.py +2 -1
- agno/models/internlm/internlm.py +2 -2
- agno/models/langdb/langdb.py +4 -4
- agno/models/litellm/chat.py +18 -1
- agno/models/litellm/litellm_openai.py +2 -2
- agno/models/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- agno/models/message.py +139 -0
- agno/models/meta/llama.py +27 -10
- agno/models/meta/llama_openai.py +5 -17
- agno/models/nebius/nebius.py +6 -6
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +22 -0
- agno/models/nvidia/nvidia.py +2 -2
- agno/models/ollama/chat.py +59 -5
- agno/models/openai/chat.py +69 -29
- agno/models/openai/responses.py +103 -106
- agno/models/openrouter/openrouter.py +41 -3
- agno/models/perplexity/perplexity.py +4 -5
- agno/models/portkey/portkey.py +3 -3
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +52 -0
- agno/models/response.py +77 -1
- agno/models/sambanova/sambanova.py +2 -2
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +25 -0
- agno/models/together/together.py +2 -2
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +2 -2
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +96 -0
- agno/models/vllm/vllm.py +1 -0
- agno/models/xai/xai.py +3 -2
- agno/os/app.py +543 -178
- agno/os/auth.py +24 -14
- agno/os/config.py +1 -0
- agno/os/interfaces/__init__.py +1 -0
- agno/os/interfaces/a2a/__init__.py +3 -0
- agno/os/interfaces/a2a/a2a.py +42 -0
- agno/os/interfaces/a2a/router.py +250 -0
- agno/os/interfaces/a2a/utils.py +924 -0
- agno/os/interfaces/agui/agui.py +23 -7
- agno/os/interfaces/agui/router.py +27 -3
- agno/os/interfaces/agui/utils.py +242 -142
- agno/os/interfaces/base.py +6 -2
- agno/os/interfaces/slack/router.py +81 -23
- agno/os/interfaces/slack/slack.py +29 -14
- agno/os/interfaces/whatsapp/router.py +11 -4
- agno/os/interfaces/whatsapp/whatsapp.py +14 -7
- agno/os/mcp.py +111 -54
- agno/os/middleware/__init__.py +7 -0
- agno/os/middleware/jwt.py +233 -0
- agno/os/router.py +556 -139
- agno/os/routers/evals/evals.py +71 -34
- agno/os/routers/evals/schemas.py +31 -31
- agno/os/routers/evals/utils.py +6 -5
- agno/os/routers/health.py +31 -0
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/knowledge.py +185 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +158 -53
- agno/os/routers/memory/schemas.py +20 -16
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +499 -38
- agno/os/schema.py +308 -198
- agno/os/utils.py +401 -41
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/azure_ai_foundry.py +2 -2
- agno/reasoning/deepseek.py +2 -2
- agno/reasoning/default.py +3 -1
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/groq.py +2 -2
- agno/reasoning/ollama.py +2 -2
- agno/reasoning/openai.py +7 -2
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +248 -94
- agno/run/base.py +44 -5
- agno/run/team.py +238 -97
- agno/run/workflow.py +144 -33
- agno/session/agent.py +105 -89
- agno/session/summary.py +65 -25
- agno/session/team.py +176 -96
- agno/session/workflow.py +406 -40
- agno/team/team.py +3854 -1610
- agno/tools/dalle.py +2 -4
- agno/tools/decorator.py +4 -2
- agno/tools/duckduckgo.py +15 -11
- agno/tools/e2b.py +14 -7
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +350 -0
- agno/tools/firecrawl.py +4 -4
- agno/tools/function.py +250 -30
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +270 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/knowledge.py +3 -3
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/mem0.py +11 -17
- agno/tools/memori.py +1 -53
- agno/tools/memory.py +419 -0
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/scrapegraph.py +58 -31
- agno/tools/searxng.py +2 -2
- agno/tools/serper.py +2 -2
- agno/tools/slack.py +18 -3
- agno/tools/spider.py +2 -2
- agno/tools/tavily.py +146 -0
- agno/tools/whatsapp.py +1 -1
- agno/tools/workflow.py +278 -0
- agno/tools/yfinance.py +12 -11
- agno/utils/agent.py +820 -0
- agno/utils/audio.py +27 -0
- agno/utils/common.py +90 -1
- agno/utils/events.py +217 -2
- agno/utils/gemini.py +180 -22
- agno/utils/hooks.py +57 -0
- agno/utils/http.py +111 -0
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +188 -10
- agno/utils/merge_dict.py +22 -1
- agno/utils/message.py +60 -0
- agno/utils/models/claude.py +40 -11
- agno/utils/print_response/agent.py +105 -21
- agno/utils/print_response/team.py +103 -38
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/reasoning.py +22 -1
- agno/utils/serialize.py +32 -0
- agno/utils/streamlit.py +16 -10
- agno/utils/string.py +41 -0
- agno/utils/team.py +98 -9
- agno/utils/tools.py +1 -1
- agno/vectordb/base.py +23 -4
- agno/vectordb/cassandra/cassandra.py +65 -9
- agno/vectordb/chroma/chromadb.py +182 -38
- agno/vectordb/clickhouse/clickhousedb.py +64 -11
- agno/vectordb/couchbase/couchbase.py +105 -10
- agno/vectordb/lancedb/lance_db.py +124 -133
- agno/vectordb/langchaindb/langchaindb.py +25 -7
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/__init__.py +3 -0
- agno/vectordb/llamaindex/llamaindexdb.py +46 -7
- agno/vectordb/milvus/milvus.py +126 -9
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +112 -7
- agno/vectordb/pgvector/pgvector.py +142 -21
- agno/vectordb/pineconedb/pineconedb.py +80 -8
- agno/vectordb/qdrant/qdrant.py +125 -39
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +694 -0
- agno/vectordb/singlestore/singlestore.py +111 -25
- agno/vectordb/surrealdb/surrealdb.py +31 -5
- agno/vectordb/upstashdb/upstashdb.py +76 -8
- agno/vectordb/weaviate/weaviate.py +86 -15
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +112 -18
- agno/workflow/loop.py +69 -10
- agno/workflow/parallel.py +266 -118
- agno/workflow/router.py +110 -17
- agno/workflow/step.py +638 -129
- agno/workflow/steps.py +65 -6
- agno/workflow/types.py +61 -23
- agno/workflow/workflow.py +2085 -272
- {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/METADATA +182 -58
- agno-2.3.0.dist-info/RECORD +577 -0
- agno/knowledge/reader/url_reader.py +0 -128
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -610
- agno/utils/models/aws_claude.py +0 -170
- agno-2.0.1.dist-info/RECORD +0 -515
- {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
- {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
agno/utils/gemini.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from typing import Any, Dict, List, Optional
|
|
2
|
+
from typing import Any, Dict, List, Optional, Type, Union
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
3
5
|
|
|
4
6
|
from agno.media import Image
|
|
5
7
|
from agno.utils.log import log_error, log_warning
|
|
@@ -9,12 +11,119 @@ try:
|
|
|
9
11
|
FunctionDeclaration,
|
|
10
12
|
Schema,
|
|
11
13
|
Tool,
|
|
12
|
-
|
|
14
|
+
)
|
|
15
|
+
from google.genai.types import (
|
|
16
|
+
Type as GeminiType,
|
|
13
17
|
)
|
|
14
18
|
except ImportError:
|
|
15
19
|
raise ImportError("`google-genai` not installed. Please install it using `pip install google-genai`")
|
|
16
20
|
|
|
17
21
|
|
|
22
|
+
def prepare_response_schema(pydantic_model: Type[BaseModel]) -> Union[Type[BaseModel], Schema]:
|
|
23
|
+
"""
|
|
24
|
+
Prepare a Pydantic model for use as Gemini response schema.
|
|
25
|
+
|
|
26
|
+
Returns the model directly if Gemini can handle it natively,
|
|
27
|
+
otherwise converts to Gemini's Schema format.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
pydantic_model: A Pydantic model class
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Either the original Pydantic model or a converted Schema object
|
|
34
|
+
"""
|
|
35
|
+
schema_dict = pydantic_model.model_json_schema()
|
|
36
|
+
|
|
37
|
+
# Convert to Gemini Schema if the model has problematic patterns
|
|
38
|
+
if needs_conversion(schema_dict):
|
|
39
|
+
try:
|
|
40
|
+
converted = convert_schema(schema_dict)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
log_warning(f"Failed to convert schema for {pydantic_model}: {e}")
|
|
43
|
+
converted = None
|
|
44
|
+
|
|
45
|
+
if converted is None:
|
|
46
|
+
# If conversion fails, let Gemini handle it directly
|
|
47
|
+
return pydantic_model
|
|
48
|
+
return converted
|
|
49
|
+
|
|
50
|
+
# Gemini can handle this model directly
|
|
51
|
+
return pydantic_model
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def needs_conversion(schema_dict: Dict[str, Any]) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Check if a schema needs conversion for Gemini.
|
|
57
|
+
|
|
58
|
+
Returns True if the schema has:
|
|
59
|
+
- Self-references or circular references
|
|
60
|
+
- Dict fields (additionalProperties) that Gemini doesn't handle well
|
|
61
|
+
- Empty object definitions that Gemini rejects
|
|
62
|
+
"""
|
|
63
|
+
# Check for dict fields (additionalProperties) anywhere in the schema
|
|
64
|
+
if has_additional_properties(schema_dict):
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
# Check if schema has $defs with circular references
|
|
68
|
+
if "$defs" in schema_dict:
|
|
69
|
+
defs = schema_dict["$defs"]
|
|
70
|
+
for def_name, def_schema in defs.items():
|
|
71
|
+
ref_path = f"#/$defs/{def_name}"
|
|
72
|
+
if has_self_reference(def_schema, ref_path):
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def has_additional_properties(schema: Any) -> bool:
|
|
79
|
+
"""Check if schema has additionalProperties (Dict fields)"""
|
|
80
|
+
if isinstance(schema, dict):
|
|
81
|
+
# Direct check
|
|
82
|
+
if "additionalProperties" in schema:
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
# Check properties recursively
|
|
86
|
+
if "properties" in schema:
|
|
87
|
+
for prop_schema in schema["properties"].values():
|
|
88
|
+
if has_additional_properties(prop_schema):
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
# Check array items
|
|
92
|
+
if "items" in schema:
|
|
93
|
+
if has_additional_properties(schema["items"]):
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def has_self_reference(schema: Dict, target_ref: str) -> bool:
|
|
100
|
+
"""Check if a schema references itself (directly or indirectly)"""
|
|
101
|
+
if isinstance(schema, dict):
|
|
102
|
+
# Direct self-reference
|
|
103
|
+
if schema.get("$ref") == target_ref:
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
# Check properties
|
|
107
|
+
if "properties" in schema:
|
|
108
|
+
for prop_schema in schema["properties"].values():
|
|
109
|
+
if has_self_reference(prop_schema, target_ref):
|
|
110
|
+
return True
|
|
111
|
+
|
|
112
|
+
# Check array items
|
|
113
|
+
if "items" in schema:
|
|
114
|
+
if has_self_reference(schema["items"], target_ref):
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
# Check anyOf/oneOf/allOf
|
|
118
|
+
for key in ["anyOf", "oneOf", "allOf"]:
|
|
119
|
+
if key in schema:
|
|
120
|
+
for sub_schema in schema[key]:
|
|
121
|
+
if has_self_reference(sub_schema, target_ref):
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
18
127
|
def format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
|
|
19
128
|
# Case 1: Image is a URL
|
|
20
129
|
# Download the image from the URL and add it as base64 encoded data
|
|
@@ -66,7 +175,9 @@ def format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
|
|
|
66
175
|
return None
|
|
67
176
|
|
|
68
177
|
|
|
69
|
-
def convert_schema(
|
|
178
|
+
def convert_schema(
|
|
179
|
+
schema_dict: Dict[str, Any], root_schema: Optional[Dict[str, Any]] = None, visited_refs: Optional[set] = None
|
|
180
|
+
) -> Optional[Schema]:
|
|
70
181
|
"""
|
|
71
182
|
Recursively convert a JSON-like schema dictionary to a types.Schema object.
|
|
72
183
|
|
|
@@ -74,23 +185,39 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
|
|
|
74
185
|
schema_dict (dict): The JSON schema dictionary with keys like "type", "description",
|
|
75
186
|
"properties", and "required".
|
|
76
187
|
root_schema (dict, optional): The root schema containing $defs for resolving $ref
|
|
188
|
+
visited_refs (set, optional): Set of visited $ref paths to detect circular references
|
|
77
189
|
|
|
78
190
|
Returns:
|
|
79
191
|
types.Schema: The converted schema.
|
|
80
192
|
"""
|
|
81
193
|
|
|
82
|
-
# If this is the initial call, set root_schema to self
|
|
194
|
+
# If this is the initial call, set root_schema to self and initialize visited_refs
|
|
83
195
|
if root_schema is None:
|
|
84
196
|
root_schema = schema_dict
|
|
197
|
+
if visited_refs is None:
|
|
198
|
+
visited_refs = set()
|
|
85
199
|
|
|
86
|
-
# Handle $ref references
|
|
200
|
+
# Handle $ref references with cycle detection
|
|
87
201
|
if "$ref" in schema_dict:
|
|
88
202
|
ref_path = schema_dict["$ref"]
|
|
203
|
+
|
|
204
|
+
# Check for circular reference
|
|
205
|
+
if ref_path in visited_refs:
|
|
206
|
+
# Return a basic object schema to break the cycle
|
|
207
|
+
return Schema(
|
|
208
|
+
type=GeminiType.OBJECT,
|
|
209
|
+
description=f"Circular reference to {ref_path}",
|
|
210
|
+
)
|
|
211
|
+
|
|
89
212
|
if ref_path.startswith("#/$defs/"):
|
|
90
213
|
def_name = ref_path.split("/")[-1]
|
|
91
214
|
if "$defs" in root_schema and def_name in root_schema["$defs"]:
|
|
215
|
+
# Add to visited set before recursing
|
|
216
|
+
new_visited = visited_refs.copy()
|
|
217
|
+
new_visited.add(ref_path)
|
|
218
|
+
|
|
92
219
|
referenced_schema = root_schema["$defs"][def_name]
|
|
93
|
-
return convert_schema(referenced_schema, root_schema)
|
|
220
|
+
return convert_schema(referenced_schema, root_schema, new_visited)
|
|
94
221
|
# If we can't resolve the reference, return None
|
|
95
222
|
return None
|
|
96
223
|
|
|
@@ -98,12 +225,13 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
|
|
|
98
225
|
if schema_type is None or schema_type == "null":
|
|
99
226
|
return None
|
|
100
227
|
description = schema_dict.get("description", None)
|
|
228
|
+
title = schema_dict.get("title", None)
|
|
101
229
|
default = schema_dict.get("default", None)
|
|
102
230
|
|
|
103
231
|
# Handle enum types
|
|
104
232
|
if "enum" in schema_dict:
|
|
105
233
|
enum_values = schema_dict["enum"]
|
|
106
|
-
return Schema(type=
|
|
234
|
+
return Schema(type=GeminiType.STRING, enum=enum_values, description=description, default=default, title=title)
|
|
107
235
|
|
|
108
236
|
if schema_type == "object":
|
|
109
237
|
# Handle regular objects with properties
|
|
@@ -117,25 +245,30 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
|
|
|
117
245
|
prop_def["type"] = prop_type[0]
|
|
118
246
|
is_nullable = True
|
|
119
247
|
|
|
120
|
-
# Process property schema (pass root_schema for $ref resolution)
|
|
121
|
-
converted_schema = convert_schema(prop_def, root_schema)
|
|
248
|
+
# Process property schema (pass root_schema and visited_refs for $ref resolution)
|
|
249
|
+
converted_schema = convert_schema(prop_def, root_schema, visited_refs)
|
|
122
250
|
if converted_schema is not None:
|
|
123
251
|
if is_nullable:
|
|
124
252
|
converted_schema.nullable = True
|
|
125
253
|
properties[key] = converted_schema
|
|
254
|
+
else:
|
|
255
|
+
properties[key] = Schema(
|
|
256
|
+
title=prop_def.get("title", None), description=prop_def.get("description", None)
|
|
257
|
+
)
|
|
126
258
|
|
|
127
259
|
required = schema_dict.get("required", [])
|
|
128
260
|
|
|
129
261
|
if properties:
|
|
130
262
|
return Schema(
|
|
131
|
-
type=
|
|
263
|
+
type=GeminiType.OBJECT,
|
|
132
264
|
properties=properties,
|
|
133
265
|
required=required,
|
|
134
266
|
description=description,
|
|
135
267
|
default=default,
|
|
268
|
+
title=title,
|
|
136
269
|
)
|
|
137
270
|
else:
|
|
138
|
-
return Schema(type=
|
|
271
|
+
return Schema(type=GeminiType.OBJECT, description=description, default=default, title=title)
|
|
139
272
|
|
|
140
273
|
# Handle Dict types (objects with additionalProperties but no properties)
|
|
141
274
|
elif "additionalProperties" in schema_dict:
|
|
@@ -146,50 +279,67 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
|
|
|
146
279
|
# For Gemini, we need to represent Dict[str, T] as an object with at least one property
|
|
147
280
|
# to avoid the "properties should be non-empty" error.
|
|
148
281
|
# We'll create a generic property that represents the dictionary structure
|
|
149
|
-
|
|
282
|
+
|
|
283
|
+
# Handle both single types and union types (arrays) from Zod schemas
|
|
284
|
+
type_value = additional_props.get("type", "string")
|
|
285
|
+
if isinstance(type_value, list):
|
|
286
|
+
value_type = type_value[0].upper() if type_value else "STRING"
|
|
287
|
+
union_types = ", ".join(type_value)
|
|
288
|
+
type_description_suffix = f" (supports union types: {union_types})"
|
|
289
|
+
else:
|
|
290
|
+
# Single type
|
|
291
|
+
value_type = type_value.upper()
|
|
292
|
+
type_description_suffix = ""
|
|
293
|
+
|
|
150
294
|
# Create a placeholder property to satisfy Gemini's requirements
|
|
151
295
|
# This is a workaround since Gemini doesn't support additionalProperties directly
|
|
152
296
|
placeholder_properties = {
|
|
153
297
|
"example_key": Schema(
|
|
154
298
|
type=value_type,
|
|
155
|
-
description=f"Example key-value pair. This object can contain any number of keys with {value_type.lower()} values.",
|
|
299
|
+
description=f"Example key-value pair. This object can contain any number of keys with {value_type.lower()} values{type_description_suffix}.",
|
|
156
300
|
)
|
|
157
301
|
}
|
|
158
302
|
if value_type == "ARRAY":
|
|
159
303
|
placeholder_properties["example_key"].items = {} # type: ignore
|
|
160
304
|
|
|
161
305
|
return Schema(
|
|
162
|
-
type=
|
|
306
|
+
type=GeminiType.OBJECT,
|
|
163
307
|
properties=placeholder_properties,
|
|
164
308
|
description=description
|
|
165
|
-
or f"Dictionary with {value_type.lower()} values. Can contain any number of key-value pairs.",
|
|
309
|
+
or f"Dictionary with {value_type.lower()} values{type_description_suffix}. Can contain any number of key-value pairs.",
|
|
166
310
|
default=default,
|
|
167
311
|
)
|
|
168
312
|
else:
|
|
169
313
|
# additionalProperties is false or true
|
|
170
|
-
return Schema(type=
|
|
314
|
+
return Schema(type=GeminiType.OBJECT, description=description, default=default, title=title)
|
|
171
315
|
|
|
172
316
|
# Handle empty objects
|
|
173
317
|
else:
|
|
174
|
-
return Schema(type=
|
|
318
|
+
return Schema(type=GeminiType.OBJECT, description=description, default=default, title=title)
|
|
175
319
|
|
|
176
320
|
elif schema_type == "array" and "items" in schema_dict:
|
|
177
|
-
|
|
321
|
+
if not schema_dict["items"]: # Handle empty {}
|
|
322
|
+
items = Schema(type=GeminiType.STRING)
|
|
323
|
+
else:
|
|
324
|
+
converted_items = convert_schema(schema_dict["items"], root_schema, visited_refs)
|
|
325
|
+
items = converted_items if converted_items is not None else Schema(type=GeminiType.STRING)
|
|
178
326
|
min_items = schema_dict.get("minItems")
|
|
179
327
|
max_items = schema_dict.get("maxItems")
|
|
180
328
|
return Schema(
|
|
181
|
-
type=
|
|
329
|
+
type=GeminiType.ARRAY,
|
|
182
330
|
description=description,
|
|
183
331
|
items=items,
|
|
184
332
|
min_items=min_items,
|
|
185
333
|
max_items=max_items,
|
|
334
|
+
title=title,
|
|
186
335
|
)
|
|
187
336
|
|
|
188
337
|
elif schema_type == "string":
|
|
189
338
|
schema_kwargs = {
|
|
190
|
-
"type":
|
|
339
|
+
"type": GeminiType.STRING,
|
|
191
340
|
"description": description,
|
|
192
341
|
"default": default,
|
|
342
|
+
"title": title,
|
|
193
343
|
}
|
|
194
344
|
if "format" in schema_dict:
|
|
195
345
|
schema_kwargs["format"] = schema_dict["format"]
|
|
@@ -200,6 +350,7 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
|
|
|
200
350
|
"type": schema_type.upper(),
|
|
201
351
|
"description": description,
|
|
202
352
|
"default": default,
|
|
353
|
+
"title": title,
|
|
203
354
|
}
|
|
204
355
|
if "maximum" in schema_dict:
|
|
205
356
|
schema_kwargs["maximum"] = schema_dict["maximum"]
|
|
@@ -210,7 +361,7 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
|
|
|
210
361
|
elif schema_type == "" and "anyOf" in schema_dict:
|
|
211
362
|
any_of = []
|
|
212
363
|
for sub_schema in schema_dict["anyOf"]:
|
|
213
|
-
sub_schema_converted = convert_schema(sub_schema, root_schema)
|
|
364
|
+
sub_schema_converted = convert_schema(sub_schema, root_schema, visited_refs)
|
|
214
365
|
any_of.append(sub_schema_converted)
|
|
215
366
|
|
|
216
367
|
is_nullable = False
|
|
@@ -231,12 +382,19 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
|
|
|
231
382
|
any_of=any_of,
|
|
232
383
|
description=description,
|
|
233
384
|
default=default,
|
|
385
|
+
title=title,
|
|
234
386
|
)
|
|
235
387
|
else:
|
|
388
|
+
if isinstance(schema_type, list):
|
|
389
|
+
non_null_types = [t for t in schema_type if t != "null"]
|
|
390
|
+
if non_null_types:
|
|
391
|
+
schema_type = non_null_types[0]
|
|
392
|
+
else:
|
|
393
|
+
schema_type = ""
|
|
236
394
|
# Only convert to uppercase if schema_type is not empty
|
|
237
395
|
if schema_type:
|
|
238
396
|
schema_type = schema_type.upper()
|
|
239
|
-
return Schema(type=schema_type, description=description, default=default)
|
|
397
|
+
return Schema(type=schema_type, description=description, default=default, title=title)
|
|
240
398
|
else:
|
|
241
399
|
# If we get here with an empty type and no other handlers matched,
|
|
242
400
|
# something is wrong with the schema
|
agno/utils/hooks.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
from agno.guardrails.base import BaseGuardrail
|
|
4
|
+
from agno.utils.log import log_warning
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def normalize_hooks(
|
|
8
|
+
hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail]]],
|
|
9
|
+
async_mode: bool = False,
|
|
10
|
+
) -> Optional[List[Callable[..., Any]]]:
|
|
11
|
+
"""Normalize hooks to a list format"""
|
|
12
|
+
result_hooks: List[Callable[..., Any]] = []
|
|
13
|
+
|
|
14
|
+
if hooks is not None:
|
|
15
|
+
for hook in hooks:
|
|
16
|
+
if isinstance(hook, BaseGuardrail):
|
|
17
|
+
if async_mode:
|
|
18
|
+
result_hooks.append(hook.async_check)
|
|
19
|
+
else:
|
|
20
|
+
result_hooks.append(hook.check)
|
|
21
|
+
else:
|
|
22
|
+
# Check if the hook is async and used within sync methods
|
|
23
|
+
if not async_mode:
|
|
24
|
+
import asyncio
|
|
25
|
+
|
|
26
|
+
if asyncio.iscoroutinefunction(hook):
|
|
27
|
+
raise ValueError(
|
|
28
|
+
f"Cannot use {hook.__name__} (an async hook) with `run()`. Use `arun()` instead."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
result_hooks.append(hook)
|
|
32
|
+
return result_hooks if result_hooks else None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def filter_hook_args(hook: Callable[..., Any], all_args: Dict[str, Any]) -> Dict[str, Any]:
|
|
36
|
+
"""Filter arguments to only include those that the hook function accepts."""
|
|
37
|
+
import inspect
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
sig = inspect.signature(hook)
|
|
41
|
+
accepted_params = set(sig.parameters.keys())
|
|
42
|
+
|
|
43
|
+
has_var_keyword = any(param.kind == inspect.Parameter.VAR_KEYWORD for param in sig.parameters.values())
|
|
44
|
+
|
|
45
|
+
# If the function has **kwargs, pass all arguments
|
|
46
|
+
if has_var_keyword:
|
|
47
|
+
return all_args
|
|
48
|
+
|
|
49
|
+
# Otherwise, filter to only include accepted parameters
|
|
50
|
+
filtered_args = {key: value for key, value in all_args.items() if key in accepted_params}
|
|
51
|
+
|
|
52
|
+
return filtered_args
|
|
53
|
+
|
|
54
|
+
except Exception as e:
|
|
55
|
+
log_warning(f"Could not inspect hook signature, passing all arguments: {e}")
|
|
56
|
+
# If signature inspection fails, pass all arguments as fallback
|
|
57
|
+
return all_args
|
agno/utils/http.py
CHANGED
|
@@ -10,6 +10,117 @@ logger = logging.getLogger(__name__)
|
|
|
10
10
|
DEFAULT_MAX_RETRIES = 3
|
|
11
11
|
DEFAULT_BACKOFF_FACTOR = 2 # Exponential backoff: 1, 2, 4, 8...
|
|
12
12
|
|
|
13
|
+
# Global httpx clients for resource efficiency
|
|
14
|
+
# These are shared across all models to reuse connection pools and avoid resource leaks.
|
|
15
|
+
# Consumers can override these at application startup using set_default_sync_client()
|
|
16
|
+
# and set_default_async_client() to customize limits, timeouts, proxies, etc.
|
|
17
|
+
_global_sync_client: Optional[httpx.Client] = None
|
|
18
|
+
_global_async_client: Optional[httpx.AsyncClient] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_default_sync_client() -> httpx.Client:
|
|
22
|
+
"""Get or create the global synchronous httpx client.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A singleton httpx.Client instance with default limits.
|
|
26
|
+
"""
|
|
27
|
+
global _global_sync_client
|
|
28
|
+
if _global_sync_client is None or _global_sync_client.is_closed:
|
|
29
|
+
_global_sync_client = httpx.Client(
|
|
30
|
+
limits=httpx.Limits(max_connections=1000, max_keepalive_connections=200), http2=True, follow_redirects=True
|
|
31
|
+
)
|
|
32
|
+
return _global_sync_client
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_default_async_client() -> httpx.AsyncClient:
|
|
36
|
+
"""Get or create the global asynchronous httpx client.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A singleton httpx.AsyncClient instance with default limits.
|
|
40
|
+
"""
|
|
41
|
+
global _global_async_client
|
|
42
|
+
if _global_async_client is None or _global_async_client.is_closed:
|
|
43
|
+
_global_async_client = httpx.AsyncClient(
|
|
44
|
+
limits=httpx.Limits(max_connections=1000, max_keepalive_connections=200), http2=True, follow_redirects=True
|
|
45
|
+
)
|
|
46
|
+
return _global_async_client
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def close_sync_client() -> None:
|
|
50
|
+
"""Closes the global sync httpx client.
|
|
51
|
+
|
|
52
|
+
Should be called during application shutdown.
|
|
53
|
+
"""
|
|
54
|
+
global _global_sync_client
|
|
55
|
+
if _global_sync_client is not None and not _global_sync_client.is_closed:
|
|
56
|
+
_global_sync_client.close()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def aclose_default_clients() -> None:
|
|
60
|
+
"""Asynchronously close the global httpx clients.
|
|
61
|
+
|
|
62
|
+
Should be called during application shutdown in async contexts.
|
|
63
|
+
"""
|
|
64
|
+
global _global_sync_client, _global_async_client
|
|
65
|
+
if _global_sync_client is not None and not _global_sync_client.is_closed:
|
|
66
|
+
_global_sync_client.close()
|
|
67
|
+
if _global_async_client is not None and not _global_async_client.is_closed:
|
|
68
|
+
await _global_async_client.aclose()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def set_default_sync_client(client: httpx.Client) -> None:
|
|
72
|
+
"""Set the global synchronous httpx client.
|
|
73
|
+
|
|
74
|
+
IMPORTANT: Call before creating any model instances. Models cache clients on first use.
|
|
75
|
+
|
|
76
|
+
Allows consumers to override the default httpx client with custom configuration
|
|
77
|
+
(e.g., custom limits, timeouts, proxies, SSL verification, etc.).
|
|
78
|
+
This is useful at application startup to customize how all models connect.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> import httpx
|
|
82
|
+
>>> from agno.utils.http import set_default_sync_client
|
|
83
|
+
>>> custom_client = httpx.Client(
|
|
84
|
+
... limits=httpx.Limits(max_connections=500),
|
|
85
|
+
... timeout=httpx.Timeout(30.0),
|
|
86
|
+
... verify=False # for dev environments
|
|
87
|
+
... )
|
|
88
|
+
>>> set_default_sync_client(custom_client)
|
|
89
|
+
>>> # All models will now use this custom client
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
client: An httpx.Client instance to use as the global sync client.
|
|
93
|
+
"""
|
|
94
|
+
global _global_sync_client
|
|
95
|
+
_global_sync_client = client
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def set_default_async_client(client: httpx.AsyncClient) -> None:
|
|
99
|
+
"""Set the global asynchronous httpx client.
|
|
100
|
+
|
|
101
|
+
IMPORTANT: Call before creating any model instances. Models cache clients on first use.
|
|
102
|
+
|
|
103
|
+
Allows consumers to override the default async httpx client with custom configuration
|
|
104
|
+
(e.g., custom limits, timeouts, proxies, SSL verification, etc.).
|
|
105
|
+
This is useful at application startup to customize how all models connect.
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
>>> import httpx
|
|
109
|
+
>>> from agno.utils.http import set_default_async_client
|
|
110
|
+
>>> custom_client = httpx.AsyncClient(
|
|
111
|
+
... limits=httpx.Limits(max_connections=500),
|
|
112
|
+
... timeout=httpx.Timeout(30.0),
|
|
113
|
+
... verify=False # for dev environments
|
|
114
|
+
... )
|
|
115
|
+
>>> set_default_async_client(custom_client)
|
|
116
|
+
>>> # All models will now use this custom client
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
client: An httpx.AsyncClient instance to use as the global async client.
|
|
120
|
+
"""
|
|
121
|
+
global _global_async_client
|
|
122
|
+
_global_async_client = client
|
|
123
|
+
|
|
13
124
|
|
|
14
125
|
def fetch_with_retry(
|
|
15
126
|
url: str,
|
agno/utils/knowledge.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
from typing import Any, Dict, Optional
|
|
1
|
+
from typing import Any, Dict, List, Optional, Union
|
|
2
2
|
|
|
3
|
+
from agno.filters import FilterExpr
|
|
3
4
|
from agno.utils.log import log_info
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def get_agentic_or_user_search_filters(
|
|
7
|
-
filters: Optional[Dict[str, Any]], effective_filters: Optional[Dict[str, Any]]
|
|
8
|
+
filters: Optional[Dict[str, Any]], effective_filters: Optional[Union[Dict[str, Any], List[FilterExpr]]]
|
|
8
9
|
) -> Dict[str, Any]:
|
|
9
10
|
"""Helper function to determine the final filters to use for the search.
|
|
10
11
|
|
|
@@ -15,7 +16,7 @@ def get_agentic_or_user_search_filters(
|
|
|
15
16
|
Returns:
|
|
16
17
|
Dict[str, Any]: The final filters to use for the search.
|
|
17
18
|
"""
|
|
18
|
-
search_filters =
|
|
19
|
+
search_filters = None
|
|
19
20
|
|
|
20
21
|
# If agentic filters exist and manual filters (passed by user) do not, use agentic filters
|
|
21
22
|
if filters and not effective_filters:
|
|
@@ -23,7 +24,13 @@ def get_agentic_or_user_search_filters(
|
|
|
23
24
|
|
|
24
25
|
# If both agentic filters exist and manual filters (passed by user) exist, use manual filters (give priority to user and override)
|
|
25
26
|
if filters and effective_filters:
|
|
26
|
-
|
|
27
|
+
if isinstance(effective_filters, dict):
|
|
28
|
+
search_filters = effective_filters
|
|
29
|
+
elif isinstance(effective_filters, list):
|
|
30
|
+
# If effective_filters is a list (likely List[FilterExpr]), convert both filters and effective_filters to a dict if possible, otherwise raise
|
|
31
|
+
raise ValueError(
|
|
32
|
+
"Merging dict and list of filters is not supported; effective_filters should be a dict for search compatibility."
|
|
33
|
+
)
|
|
27
34
|
|
|
28
35
|
log_info(f"Filters used by Agent: {search_filters}")
|
|
29
|
-
return search_filters
|
|
36
|
+
return search_filters or {}
|
agno/utils/log.py
CHANGED
agno/utils/mcp.py
CHANGED
|
@@ -27,9 +27,13 @@ def get_entrypoint_for_tool(tool: MCPTool, session: ClientSession):
|
|
|
27
27
|
Returns:
|
|
28
28
|
Callable: The entrypoint function for the tool
|
|
29
29
|
"""
|
|
30
|
-
from agno.agent import Agent
|
|
31
30
|
|
|
32
|
-
async def call_tool(
|
|
31
|
+
async def call_tool(tool_name: str, **kwargs) -> ToolResult:
|
|
32
|
+
try:
|
|
33
|
+
await session.send_ping()
|
|
34
|
+
except Exception as e:
|
|
35
|
+
print(e)
|
|
36
|
+
|
|
33
37
|
try:
|
|
34
38
|
log_debug(f"Calling MCP Tool '{tool_name}' with args: {kwargs}")
|
|
35
39
|
result: CallToolResult = await session.call_tool(tool_name, kwargs) # type: ignore
|
|
@@ -122,3 +126,89 @@ def get_entrypoint_for_tool(tool: MCPTool, session: ClientSession):
|
|
|
122
126
|
return ToolResult(content=f"Error: {e}")
|
|
123
127
|
|
|
124
128
|
return partial(call_tool, tool_name=tool.name)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def prepare_command(command: str) -> list[str]:
|
|
132
|
+
"""Sanitize a command and split it into parts before using it to run a MCP server."""
|
|
133
|
+
import os
|
|
134
|
+
import shutil
|
|
135
|
+
from shlex import split
|
|
136
|
+
|
|
137
|
+
# Block dangerous characters
|
|
138
|
+
if any(char in command for char in ["&", "|", ";", "`", "$", "(", ")"]):
|
|
139
|
+
raise ValueError("MCP command can't contain shell metacharacters")
|
|
140
|
+
|
|
141
|
+
parts = split(command)
|
|
142
|
+
if not parts:
|
|
143
|
+
raise ValueError("MCP command can't be empty")
|
|
144
|
+
|
|
145
|
+
# Only allow specific executables
|
|
146
|
+
ALLOWED_COMMANDS = {
|
|
147
|
+
# Python
|
|
148
|
+
"python",
|
|
149
|
+
"python3",
|
|
150
|
+
"uv",
|
|
151
|
+
"uvx",
|
|
152
|
+
"pipx",
|
|
153
|
+
# Node
|
|
154
|
+
"node",
|
|
155
|
+
"npm",
|
|
156
|
+
"npx",
|
|
157
|
+
"yarn",
|
|
158
|
+
"pnpm",
|
|
159
|
+
"bun",
|
|
160
|
+
# Other runtimes
|
|
161
|
+
"deno",
|
|
162
|
+
"java",
|
|
163
|
+
"ruby",
|
|
164
|
+
"docker",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
executable = parts[0].split("/")[-1]
|
|
168
|
+
|
|
169
|
+
# Check if it's a relative path starting with ./ or ../
|
|
170
|
+
if executable.startswith("./") or executable.startswith("../"):
|
|
171
|
+
# Allow relative paths to binaries
|
|
172
|
+
return parts
|
|
173
|
+
|
|
174
|
+
# Check if it's an absolute path to a binary
|
|
175
|
+
if executable.startswith("/") and os.path.isfile(executable):
|
|
176
|
+
# Allow absolute paths to existing files
|
|
177
|
+
return parts
|
|
178
|
+
|
|
179
|
+
# Check if it's a binary in current directory without ./
|
|
180
|
+
if "/" not in executable and os.path.isfile(executable):
|
|
181
|
+
# Allow binaries in current directory
|
|
182
|
+
return parts
|
|
183
|
+
|
|
184
|
+
# Check if it's a binary in PATH
|
|
185
|
+
if shutil.which(executable):
|
|
186
|
+
return parts
|
|
187
|
+
|
|
188
|
+
if executable not in ALLOWED_COMMANDS:
|
|
189
|
+
raise ValueError(f"MCP command needs to use one of the following executables: {ALLOWED_COMMANDS}")
|
|
190
|
+
|
|
191
|
+
first_part = parts[0]
|
|
192
|
+
executable = first_part.split("/")[-1]
|
|
193
|
+
|
|
194
|
+
# Allow known commands
|
|
195
|
+
if executable in ALLOWED_COMMANDS:
|
|
196
|
+
return parts
|
|
197
|
+
|
|
198
|
+
# Allow relative paths to custom binaries
|
|
199
|
+
if first_part.startswith(("./", "../")):
|
|
200
|
+
return parts
|
|
201
|
+
|
|
202
|
+
# Allow absolute paths to existing files
|
|
203
|
+
if first_part.startswith("/") and os.path.isfile(first_part):
|
|
204
|
+
return parts
|
|
205
|
+
|
|
206
|
+
# Allow binaries in current directory without ./
|
|
207
|
+
if "/" not in first_part and os.path.isfile(first_part):
|
|
208
|
+
return parts
|
|
209
|
+
|
|
210
|
+
# Allow binaries in PATH
|
|
211
|
+
if shutil.which(first_part):
|
|
212
|
+
return parts
|
|
213
|
+
|
|
214
|
+
raise ValueError(f"MCP command needs to use one of the following executables: {ALLOWED_COMMANDS}")
|