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/utils/http.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
import threading
|
|
3
4
|
from time import sleep
|
|
4
5
|
from typing import Optional
|
|
5
6
|
|
|
@@ -10,6 +11,164 @@ logger = logging.getLogger(__name__)
|
|
|
10
11
|
DEFAULT_MAX_RETRIES = 3
|
|
11
12
|
DEFAULT_BACKOFF_FACTOR = 2 # Exponential backoff: 1, 2, 4, 8...
|
|
12
13
|
|
|
14
|
+
# Global httpx clients for resource efficiency
|
|
15
|
+
# These are shared across all models to reuse connection pools and avoid resource leaks.
|
|
16
|
+
# Consumers can override these at application startup using set_default_sync_client()
|
|
17
|
+
# and set_default_async_client() to customize limits, timeouts, proxies, etc.
|
|
18
|
+
_global_sync_client: Optional[httpx.Client] = None
|
|
19
|
+
_global_async_client: Optional[httpx.AsyncClient] = None
|
|
20
|
+
|
|
21
|
+
# Locks for thread-safe lazy initialization
|
|
22
|
+
_sync_client_lock = threading.Lock()
|
|
23
|
+
_async_client_lock = threading.Lock()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_default_sync_client() -> httpx.Client:
|
|
27
|
+
"""Get or create the global synchronous httpx client.
|
|
28
|
+
|
|
29
|
+
Thread-safe lazy initialization using double-checked locking.
|
|
30
|
+
|
|
31
|
+
Note: HTTP/2 is disabled for the sync client because HTTP/2's stream
|
|
32
|
+
multiplexing is not thread-safe when sharing a client across threads
|
|
33
|
+
(e.g., when using ThreadPoolExecutor). HTTP/1.1 uses connection pooling
|
|
34
|
+
where each connection handles one request at a time, which is thread-safe.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
A singleton httpx.Client instance with default limits.
|
|
38
|
+
"""
|
|
39
|
+
global _global_sync_client
|
|
40
|
+
|
|
41
|
+
if _global_sync_client is not None and not _global_sync_client.is_closed:
|
|
42
|
+
return _global_sync_client
|
|
43
|
+
|
|
44
|
+
with _sync_client_lock:
|
|
45
|
+
if _global_sync_client is None or _global_sync_client.is_closed:
|
|
46
|
+
_global_sync_client = httpx.Client(
|
|
47
|
+
limits=httpx.Limits(max_connections=1000, max_keepalive_connections=200),
|
|
48
|
+
http2=False, # Disabled for thread safety in multi-threaded contexts
|
|
49
|
+
follow_redirects=True,
|
|
50
|
+
)
|
|
51
|
+
return _global_sync_client
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_default_async_client() -> httpx.AsyncClient:
|
|
55
|
+
"""Get or create the global asynchronous httpx client.
|
|
56
|
+
|
|
57
|
+
Thread-safe lazy initialization using double-checked locking.
|
|
58
|
+
|
|
59
|
+
Note: HTTP/2 is enabled for the async client because asyncio runs in a
|
|
60
|
+
single-threaded event loop where HTTP/2 stream multiplexing is safe.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A singleton httpx.AsyncClient instance with default limits.
|
|
64
|
+
"""
|
|
65
|
+
global _global_async_client
|
|
66
|
+
|
|
67
|
+
if _global_async_client is not None and not _global_async_client.is_closed:
|
|
68
|
+
return _global_async_client
|
|
69
|
+
|
|
70
|
+
with _async_client_lock:
|
|
71
|
+
if _global_async_client is None or _global_async_client.is_closed:
|
|
72
|
+
_global_async_client = httpx.AsyncClient(
|
|
73
|
+
limits=httpx.Limits(max_connections=1000, max_keepalive_connections=200),
|
|
74
|
+
http2=True, # Safe in async context (single-threaded event loop)
|
|
75
|
+
follow_redirects=True,
|
|
76
|
+
)
|
|
77
|
+
return _global_async_client
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def close_sync_client() -> None:
|
|
81
|
+
"""Closes the global sync httpx client.
|
|
82
|
+
|
|
83
|
+
Thread-safe. Should be called during application shutdown.
|
|
84
|
+
"""
|
|
85
|
+
global _global_sync_client
|
|
86
|
+
with _sync_client_lock:
|
|
87
|
+
if _global_sync_client is not None and not _global_sync_client.is_closed:
|
|
88
|
+
_global_sync_client.close()
|
|
89
|
+
_global_sync_client = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def aclose_default_clients() -> None:
|
|
93
|
+
"""Asynchronously close the global httpx clients.
|
|
94
|
+
|
|
95
|
+
Thread-safe. Should be called during application shutdown in async contexts.
|
|
96
|
+
"""
|
|
97
|
+
global _global_sync_client, _global_async_client
|
|
98
|
+
|
|
99
|
+
with _sync_client_lock:
|
|
100
|
+
if _global_sync_client is not None and not _global_sync_client.is_closed:
|
|
101
|
+
_global_sync_client.close()
|
|
102
|
+
_global_sync_client = None
|
|
103
|
+
|
|
104
|
+
with _async_client_lock:
|
|
105
|
+
if _global_async_client is not None and not _global_async_client.is_closed:
|
|
106
|
+
await _global_async_client.aclose()
|
|
107
|
+
_global_async_client = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def set_default_sync_client(client: httpx.Client) -> None:
|
|
111
|
+
"""Set the global synchronous httpx client.
|
|
112
|
+
|
|
113
|
+
Thread-safe. Call before creating any model instances for best results,
|
|
114
|
+
though this can be called at any time.
|
|
115
|
+
|
|
116
|
+
Allows consumers to override the default httpx client with custom configuration
|
|
117
|
+
(e.g., custom limits, timeouts, proxies, SSL verification, etc.).
|
|
118
|
+
This is useful at application startup to customize how all models connect.
|
|
119
|
+
|
|
120
|
+
Warning: If using this client in multi-threaded contexts (e.g., ThreadPoolExecutor),
|
|
121
|
+
consider disabling HTTP/2 (http2=False) to avoid thread-safety issues with
|
|
122
|
+
HTTP/2 stream multiplexing.
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
>>> import httpx
|
|
126
|
+
>>> from agno.utils.http import set_default_sync_client
|
|
127
|
+
>>> custom_client = httpx.Client(
|
|
128
|
+
... limits=httpx.Limits(max_connections=500),
|
|
129
|
+
... timeout=httpx.Timeout(30.0),
|
|
130
|
+
... http2=False, # Recommended for multi-threaded use
|
|
131
|
+
... verify=False # for dev environments
|
|
132
|
+
... )
|
|
133
|
+
>>> set_default_sync_client(custom_client)
|
|
134
|
+
>>> # All models will now use this custom client
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
client: An httpx.Client instance to use as the global sync client.
|
|
138
|
+
"""
|
|
139
|
+
global _global_sync_client
|
|
140
|
+
with _sync_client_lock:
|
|
141
|
+
_global_sync_client = client
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def set_default_async_client(client: httpx.AsyncClient) -> None:
|
|
145
|
+
"""Set the global asynchronous httpx client.
|
|
146
|
+
|
|
147
|
+
Thread-safe. Call before creating any model instances for best results,
|
|
148
|
+
though this can be called at any time.
|
|
149
|
+
|
|
150
|
+
Allows consumers to override the default async httpx client with custom configuration
|
|
151
|
+
(e.g., custom limits, timeouts, proxies, SSL verification, etc.).
|
|
152
|
+
This is useful at application startup to customize how all models connect.
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
>>> import httpx
|
|
156
|
+
>>> from agno.utils.http import set_default_async_client
|
|
157
|
+
>>> custom_client = httpx.AsyncClient(
|
|
158
|
+
... limits=httpx.Limits(max_connections=500),
|
|
159
|
+
... timeout=httpx.Timeout(30.0),
|
|
160
|
+
... verify=False # for dev environments
|
|
161
|
+
... )
|
|
162
|
+
>>> set_default_async_client(custom_client)
|
|
163
|
+
>>> # All models will now use this custom client
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
client: An httpx.AsyncClient instance to use as the global async client.
|
|
167
|
+
"""
|
|
168
|
+
global _global_async_client
|
|
169
|
+
with _async_client_lock:
|
|
170
|
+
_global_async_client = client
|
|
171
|
+
|
|
13
172
|
|
|
14
173
|
def fetch_with_retry(
|
|
15
174
|
url: str,
|
|
@@ -29,7 +188,7 @@ def fetch_with_retry(
|
|
|
29
188
|
logger.error(f"Failed to fetch {url} after {max_retries} attempts: {e}")
|
|
30
189
|
raise
|
|
31
190
|
wait_time = backoff_factor**attempt
|
|
32
|
-
logger.warning(
|
|
191
|
+
logger.warning("Connection error.")
|
|
33
192
|
sleep(wait_time)
|
|
34
193
|
except httpx.HTTPStatusError as e:
|
|
35
194
|
logger.error(f"HTTP error for {url}: {e.response.status_code} - {e.response.text}")
|
|
@@ -65,7 +224,7 @@ async def async_fetch_with_retry(
|
|
|
65
224
|
logger.error(f"Failed to fetch {url} after {max_retries} attempts: {e}")
|
|
66
225
|
raise
|
|
67
226
|
wait_time = backoff_factor**attempt
|
|
68
|
-
logger.warning(
|
|
227
|
+
logger.warning("Connection error.")
|
|
69
228
|
await asyncio.sleep(wait_time)
|
|
70
229
|
except httpx.HTTPStatusError as e:
|
|
71
230
|
logger.error(f"HTTP error for {url}: {e.response.status_code} - {e.response.text}")
|
agno/utils/mcp.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from functools import partial
|
|
3
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
3
4
|
from uuid import uuid4
|
|
4
5
|
|
|
5
6
|
from agno.utils.log import log_debug, log_exception
|
|
@@ -15,28 +16,68 @@ except (ImportError, ModuleNotFoundError):
|
|
|
15
16
|
from agno.media import Image
|
|
16
17
|
from agno.tools.function import ToolResult
|
|
17
18
|
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from agno.agent import Agent
|
|
21
|
+
from agno.run import RunContext
|
|
22
|
+
from agno.team.team import Team
|
|
23
|
+
from agno.tools.mcp.mcp import MCPTools
|
|
24
|
+
from agno.tools.mcp.multi_mcp import MultiMCPTools
|
|
18
25
|
|
|
19
|
-
|
|
26
|
+
|
|
27
|
+
def get_entrypoint_for_tool(
|
|
28
|
+
tool: MCPTool,
|
|
29
|
+
session: ClientSession,
|
|
30
|
+
mcp_tools_instance: Optional[Union["MCPTools", "MultiMCPTools"]] = None,
|
|
31
|
+
server_idx: int = 0,
|
|
32
|
+
):
|
|
20
33
|
"""
|
|
21
34
|
Return an entrypoint for an MCP tool.
|
|
22
35
|
|
|
23
36
|
Args:
|
|
24
37
|
tool: The MCP tool to create an entrypoint for
|
|
25
|
-
session: The
|
|
38
|
+
session: The MCP ClientSession to use
|
|
39
|
+
mcp_tools_instance: Optional MCPTools or MultiMCPTools instance
|
|
40
|
+
server_idx: Index of the server (for MultiMCPTools)
|
|
26
41
|
|
|
27
42
|
Returns:
|
|
28
43
|
Callable: The entrypoint function for the tool
|
|
29
44
|
"""
|
|
30
45
|
|
|
31
|
-
async def call_tool(
|
|
46
|
+
async def call_tool(
|
|
47
|
+
tool_name: str,
|
|
48
|
+
run_context: Optional["RunContext"] = None,
|
|
49
|
+
agent: Optional["Agent"] = None,
|
|
50
|
+
team: Optional["Team"] = None,
|
|
51
|
+
**kwargs,
|
|
52
|
+
) -> ToolResult:
|
|
53
|
+
# Execute the MCP tool call
|
|
32
54
|
try:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
# Get the appropriate session for this run
|
|
56
|
+
# If mcp_tools_instance has header_provider and run_context is provided,
|
|
57
|
+
# this will create/reuse a session with dynamic headers
|
|
58
|
+
if mcp_tools_instance and hasattr(mcp_tools_instance, "get_session_for_run"):
|
|
59
|
+
# Import here to avoid circular imports
|
|
60
|
+
from agno.tools.mcp.multi_mcp import MultiMCPTools
|
|
61
|
+
|
|
62
|
+
# For MultiMCPTools, pass server_idx; for MCPTools, only pass run_context
|
|
63
|
+
if isinstance(mcp_tools_instance, MultiMCPTools):
|
|
64
|
+
active_session = await mcp_tools_instance.get_session_for_run(
|
|
65
|
+
run_context=run_context, server_idx=server_idx, agent=agent, team=team
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
active_session = await mcp_tools_instance.get_session_for_run(
|
|
69
|
+
run_context=run_context, agent=agent, team=team
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
active_session = session
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
await active_session.send_ping()
|
|
76
|
+
except Exception as e:
|
|
77
|
+
log_exception(e)
|
|
36
78
|
|
|
37
|
-
try:
|
|
38
79
|
log_debug(f"Calling MCP Tool '{tool_name}' with args: {kwargs}")
|
|
39
|
-
result: CallToolResult = await
|
|
80
|
+
result: CallToolResult = await active_session.call_tool(tool_name, kwargs) # type: ignore
|
|
40
81
|
|
|
41
82
|
# Return an error if the tool call failed
|
|
42
83
|
if result.isError:
|
agno/utils/media.py
CHANGED
|
@@ -56,6 +56,17 @@ def download_image(url: str, output_path: str) -> bool:
|
|
|
56
56
|
return False
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def download_audio(url: str, output_path: str) -> str:
|
|
60
|
+
"""Download audio from URL"""
|
|
61
|
+
response = httpx.get(url)
|
|
62
|
+
response.raise_for_status()
|
|
63
|
+
|
|
64
|
+
with open(output_path, "wb") as f:
|
|
65
|
+
for chunk in response.iter_bytes(chunk_size=8192):
|
|
66
|
+
f.write(chunk)
|
|
67
|
+
return output_path
|
|
68
|
+
|
|
69
|
+
|
|
59
70
|
def download_video(url: str, output_path: str) -> str:
|
|
60
71
|
"""Download video from URL"""
|
|
61
72
|
response = httpx.get(url)
|
|
@@ -280,7 +291,7 @@ def reconstruct_file_from_dict(file_data):
|
|
|
280
291
|
if isinstance(file_data, dict):
|
|
281
292
|
# If content is base64 string, decode it back to bytes
|
|
282
293
|
if "content" in file_data and isinstance(file_data["content"], str):
|
|
283
|
-
|
|
294
|
+
file_obj = File.from_base64(
|
|
284
295
|
file_data["content"],
|
|
285
296
|
id=file_data.get("id"),
|
|
286
297
|
mime_type=file_data.get("mime_type"),
|
|
@@ -288,6 +299,16 @@ def reconstruct_file_from_dict(file_data):
|
|
|
288
299
|
name=file_data.get("name"),
|
|
289
300
|
format=file_data.get("format"),
|
|
290
301
|
)
|
|
302
|
+
# Preserve additional fields that from_base64 doesn't handle
|
|
303
|
+
if file_data.get("size") is not None:
|
|
304
|
+
file_obj.size = file_data.get("size")
|
|
305
|
+
if file_data.get("file_type") is not None:
|
|
306
|
+
file_obj.file_type = file_data.get("file_type")
|
|
307
|
+
if file_data.get("filepath") is not None:
|
|
308
|
+
file_obj.filepath = file_data.get("filepath")
|
|
309
|
+
if file_data.get("url") is not None:
|
|
310
|
+
file_obj.url = file_data.get("url")
|
|
311
|
+
return file_obj
|
|
291
312
|
else:
|
|
292
313
|
# Regular file (filepath/url)
|
|
293
314
|
return File(**file_data)
|
agno/utils/models/ai_foundry.py
CHANGED
|
@@ -5,19 +5,26 @@ from agno.utils.log import log_warning
|
|
|
5
5
|
from agno.utils.openai import images_to_message
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def format_message(message: Message) -> Dict[str, Any]:
|
|
8
|
+
def format_message(message: Message, compress_tool_results: bool = False) -> Dict[str, Any]:
|
|
9
9
|
"""
|
|
10
10
|
Format a message into the format expected by OpenAI.
|
|
11
11
|
|
|
12
12
|
Args:
|
|
13
13
|
message (Message): The message to format.
|
|
14
|
+
compress_tool_results: Whether to compress tool results.
|
|
14
15
|
|
|
15
16
|
Returns:
|
|
16
17
|
Dict[str, Any]: The formatted message.
|
|
17
18
|
"""
|
|
19
|
+
# Use compressed content for tool messages if compression is active
|
|
20
|
+
content = message.content
|
|
21
|
+
|
|
22
|
+
if message.role == "tool":
|
|
23
|
+
content = message.get_content(use_compressed_content=compress_tool_results)
|
|
24
|
+
|
|
18
25
|
message_dict: Dict[str, Any] = {
|
|
19
26
|
"role": message.role,
|
|
20
|
-
"content":
|
|
27
|
+
"content": content,
|
|
21
28
|
"name": message.name,
|
|
22
29
|
"tool_call_id": message.tool_call_id,
|
|
23
30
|
"tool_calls": message.tool_calls,
|
agno/utils/models/claude.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
4
4
|
|
|
5
5
|
from agno.media import File, Image
|
|
6
6
|
from agno.models.message import Message
|
|
@@ -221,17 +221,20 @@ def _format_file_for_message(file: File) -> Optional[Dict[str, Any]]:
|
|
|
221
221
|
return None
|
|
222
222
|
|
|
223
223
|
|
|
224
|
-
def format_messages(
|
|
224
|
+
def format_messages(
|
|
225
|
+
messages: List[Message], compress_tool_results: bool = False
|
|
226
|
+
) -> Tuple[List[Dict[str, Union[str, list]]], str]:
|
|
225
227
|
"""
|
|
226
228
|
Process the list of messages and separate them into API messages and system messages.
|
|
227
229
|
|
|
228
230
|
Args:
|
|
229
231
|
messages (List[Message]): The list of messages to process.
|
|
232
|
+
compress_tool_results: Whether to compress tool results.
|
|
230
233
|
|
|
231
234
|
Returns:
|
|
232
|
-
Tuple[List[Dict[str, str]], str]: A tuple containing the list of API messages and the concatenated system messages.
|
|
235
|
+
Tuple[List[Dict[str, Union[str, list]]], str]: A tuple containing the list of API messages and the concatenated system messages.
|
|
233
236
|
"""
|
|
234
|
-
chat_messages: List[Dict[str, str]] = []
|
|
237
|
+
chat_messages: List[Dict[str, Union[str, list]]] = []
|
|
235
238
|
system_messages: List[str] = []
|
|
236
239
|
|
|
237
240
|
for message in messages:
|
|
@@ -301,11 +304,15 @@ def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]
|
|
|
301
304
|
)
|
|
302
305
|
elif message.role == "tool":
|
|
303
306
|
content = []
|
|
307
|
+
|
|
308
|
+
# Use compressed content for tool messages if compression is active
|
|
309
|
+
tool_result = message.get_content(use_compressed_content=compress_tool_results)
|
|
310
|
+
|
|
304
311
|
content.append(
|
|
305
312
|
{
|
|
306
313
|
"type": "tool_result",
|
|
307
314
|
"tool_use_id": message.tool_call_id,
|
|
308
|
-
"content": str(
|
|
315
|
+
"content": str(tool_result),
|
|
309
316
|
}
|
|
310
317
|
)
|
|
311
318
|
|
|
@@ -320,6 +327,7 @@ def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]
|
|
|
320
327
|
def format_tools_for_model(tools: Optional[List[Dict[str, Any]]] = None) -> Optional[List[Dict[str, Any]]]:
|
|
321
328
|
"""
|
|
322
329
|
Transforms function definitions into a format accepted by the Anthropic API.
|
|
330
|
+
Now supports strict mode for structured outputs.
|
|
323
331
|
"""
|
|
324
332
|
if not tools:
|
|
325
333
|
return None
|
|
@@ -352,7 +360,14 @@ def format_tools_for_model(tools: Optional[List[Dict[str, Any]]] = None) -> Opti
|
|
|
352
360
|
"type": parameters.get("type", "object"),
|
|
353
361
|
"properties": input_properties,
|
|
354
362
|
"required": required_params,
|
|
363
|
+
"additionalProperties": False,
|
|
355
364
|
},
|
|
356
365
|
}
|
|
366
|
+
|
|
367
|
+
# Add strict mode if specified (check both function dict and tool_def top level)
|
|
368
|
+
strict_mode = func_def.get("strict") or tool_def.get("strict")
|
|
369
|
+
if strict_mode is True:
|
|
370
|
+
tool["strict"] = True
|
|
371
|
+
|
|
357
372
|
parsed_tools.append(tool)
|
|
358
373
|
return parsed_tools
|
agno/utils/models/cohere.py
CHANGED
|
@@ -46,21 +46,28 @@ def _format_images_for_message(message: Message, images: Sequence[Image]) -> Lis
|
|
|
46
46
|
return message_content_with_image
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def format_messages(messages: List[Message]) -> List[Dict[str, Any]]:
|
|
49
|
+
def format_messages(messages: List[Message], compress_tool_results: bool = False) -> List[Dict[str, Any]]:
|
|
50
50
|
"""
|
|
51
51
|
Format messages for the Cohere API.
|
|
52
52
|
|
|
53
53
|
Args:
|
|
54
54
|
messages (List[Message]): The list of messages.
|
|
55
|
+
compress_tool_results: Whether to compress tool results.
|
|
55
56
|
|
|
56
57
|
Returns:
|
|
57
58
|
List[Dict[str, Any]]: The formatted messages.
|
|
58
59
|
"""
|
|
59
60
|
formatted_messages = []
|
|
60
61
|
for message in messages:
|
|
62
|
+
# Use compressed content for tool messages if compression is active
|
|
63
|
+
content = message.content
|
|
64
|
+
|
|
65
|
+
if message.role == "tool":
|
|
66
|
+
content = message.get_content(use_compressed_content=compress_tool_results)
|
|
67
|
+
|
|
61
68
|
message_dict = {
|
|
62
69
|
"role": message.role,
|
|
63
|
-
"content":
|
|
70
|
+
"content": content,
|
|
64
71
|
"name": message.name,
|
|
65
72
|
"tool_call_id": message.tool_call_id,
|
|
66
73
|
"tool_calls": message.tool_calls,
|
agno/utils/models/llama.py
CHANGED
|
@@ -19,13 +19,17 @@ TOOL_CALL_ROLE_MAP = {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def format_message(
|
|
22
|
+
def format_message(
|
|
23
|
+
message: Message, openai_like: bool = False, tool_calls: bool = False, compress_tool_results: bool = False
|
|
24
|
+
) -> Dict[str, Any]:
|
|
23
25
|
"""
|
|
24
26
|
Format a message into the format expected by Llama API.
|
|
25
27
|
|
|
26
28
|
Args:
|
|
27
29
|
message (Message): The message to format.
|
|
28
30
|
openai_like (bool): Whether to format the message as an OpenAI-like message.
|
|
31
|
+
tool_calls (bool): Whether tool calls are present.
|
|
32
|
+
compress_tool_results: Whether to compress tool results.
|
|
29
33
|
|
|
30
34
|
Returns:
|
|
31
35
|
Dict[str, Any]: The formatted message.
|
|
@@ -52,10 +56,13 @@ def format_message(message: Message, openai_like: bool = False, tool_calls: bool
|
|
|
52
56
|
log_warning("Audio input is currently unsupported.")
|
|
53
57
|
|
|
54
58
|
if message.role == "tool":
|
|
59
|
+
# Use compressed content if compression is active
|
|
60
|
+
content = message.get_content(use_compressed_content=compress_tool_results)
|
|
61
|
+
|
|
55
62
|
message_dict = {
|
|
56
63
|
"role": "tool",
|
|
57
64
|
"tool_call_id": message.tool_call_id,
|
|
58
|
-
"content":
|
|
65
|
+
"content": content,
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
if message.role == "assistant":
|
agno/utils/models/mistral.py
CHANGED
|
@@ -48,7 +48,7 @@ def _format_image_for_message(image: Image) -> Optional[ImageURLChunk]:
|
|
|
48
48
|
return None
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def format_messages(messages: List[Message]) -> List[MistralMessage]:
|
|
51
|
+
def format_messages(messages: List[Message], compress_tool_results: bool = False) -> List[MistralMessage]:
|
|
52
52
|
mistral_messages: List[MistralMessage] = []
|
|
53
53
|
|
|
54
54
|
for message in messages:
|
|
@@ -84,7 +84,9 @@ def format_messages(messages: List[Message]) -> List[MistralMessage]:
|
|
|
84
84
|
elif message.role == "system":
|
|
85
85
|
mistral_message = SystemMessage(role="system", content=message.content)
|
|
86
86
|
elif message.role == "tool":
|
|
87
|
-
|
|
87
|
+
# Get compressed content if compression is active
|
|
88
|
+
tool_content = message.get_content(use_compressed_content=compress_tool_results)
|
|
89
|
+
mistral_message = ToolMessage(name="tool", content=tool_content, tool_call_id=message.tool_call_id)
|
|
88
90
|
else:
|
|
89
91
|
raise ValueError(f"Unknown role: {message.role}")
|
|
90
92
|
|
agno/utils/os.py
ADDED
|
File without changes
|