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/os/utils.py
CHANGED
|
@@ -1,33 +1,205 @@
|
|
|
1
|
-
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
|
|
2
4
|
|
|
3
|
-
from fastapi import FastAPI, HTTPException, UploadFile
|
|
5
|
+
from fastapi import FastAPI, HTTPException, Request, UploadFile
|
|
4
6
|
from fastapi.routing import APIRoute, APIRouter
|
|
5
|
-
from pydantic import BaseModel
|
|
7
|
+
from pydantic import BaseModel, create_model
|
|
6
8
|
from starlette.middleware.cors import CORSMiddleware
|
|
7
9
|
|
|
8
|
-
from agno.agent
|
|
10
|
+
from agno.agent import Agent, RemoteAgent
|
|
9
11
|
from agno.db.base import AsyncBaseDb, BaseDb
|
|
10
12
|
from agno.knowledge.knowledge import Knowledge
|
|
11
13
|
from agno.media import Audio, Image, Video
|
|
12
14
|
from agno.media import File as FileMedia
|
|
13
15
|
from agno.models.message import Message
|
|
14
16
|
from agno.os.config import AgentOSConfig
|
|
15
|
-
from agno.
|
|
16
|
-
from agno.
|
|
17
|
-
from agno.
|
|
18
|
-
from agno.
|
|
19
|
-
from agno.
|
|
17
|
+
from agno.registry import Registry
|
|
18
|
+
from agno.remote.base import RemoteDb, RemoteKnowledge
|
|
19
|
+
from agno.run.agent import RunOutputEvent
|
|
20
|
+
from agno.run.team import TeamRunOutputEvent
|
|
21
|
+
from agno.run.workflow import WorkflowRunOutputEvent
|
|
22
|
+
from agno.team import RemoteTeam, Team
|
|
23
|
+
from agno.tools import Function, Toolkit
|
|
24
|
+
from agno.utils.log import log_warning, logger
|
|
25
|
+
from agno.workflow import RemoteWorkflow, Workflow
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def to_utc_datetime(value: Optional[Union[int, float, datetime]]) -> Optional[datetime]:
|
|
29
|
+
"""Convert a timestamp to a UTC datetime."""
|
|
30
|
+
if value is None:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
if isinstance(value, datetime):
|
|
34
|
+
# If already a datetime, make sure the timezone is UTC
|
|
35
|
+
if value.tzinfo is None:
|
|
36
|
+
return value.replace(tzinfo=timezone.utc)
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
return datetime.fromtimestamp(value, tz=timezone.utc)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict[str, Any]:
|
|
43
|
+
"""Given a Request and an endpoint function, return a dictionary with all extra form data fields.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
request: The FastAPI Request object
|
|
47
|
+
endpoint_func: The function exposing the endpoint that received the request
|
|
48
|
+
|
|
49
|
+
Supported form parameters:
|
|
50
|
+
- session_state: JSON string of session state dict
|
|
51
|
+
- dependencies: JSON string of dependencies dict
|
|
52
|
+
- metadata: JSON string of metadata dict
|
|
53
|
+
- knowledge_filters: JSON string of knowledge filters
|
|
54
|
+
- output_schema: JSON schema string (converted to Pydantic model by default)
|
|
55
|
+
- use_json_schema: If "true", keeps output_schema as dict instead of converting to Pydantic model
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
A dictionary of kwargs to pass to Agent/Team run methods
|
|
59
|
+
"""
|
|
60
|
+
import inspect
|
|
61
|
+
|
|
62
|
+
form_data = await request.form()
|
|
63
|
+
sig = inspect.signature(endpoint_func)
|
|
64
|
+
known_fields = set(sig.parameters.keys())
|
|
65
|
+
kwargs: Dict[str, Any] = {key: value for key, value in form_data.items() if key not in known_fields}
|
|
66
|
+
|
|
67
|
+
# Handle JSON parameters. They are passed as strings and need to be deserialized.
|
|
68
|
+
if session_state := kwargs.get("session_state"):
|
|
69
|
+
try:
|
|
70
|
+
if isinstance(session_state, str):
|
|
71
|
+
session_state_dict = json.loads(session_state) # type: ignore
|
|
72
|
+
kwargs["session_state"] = session_state_dict
|
|
73
|
+
except json.JSONDecodeError:
|
|
74
|
+
kwargs.pop("session_state")
|
|
75
|
+
log_warning(f"Invalid session_state parameter couldn't be loaded: {session_state}")
|
|
76
|
+
|
|
77
|
+
if dependencies := kwargs.get("dependencies"):
|
|
78
|
+
try:
|
|
79
|
+
if isinstance(dependencies, str):
|
|
80
|
+
dependencies_dict = json.loads(dependencies) # type: ignore
|
|
81
|
+
kwargs["dependencies"] = dependencies_dict
|
|
82
|
+
except json.JSONDecodeError:
|
|
83
|
+
kwargs.pop("dependencies")
|
|
84
|
+
log_warning(f"Invalid dependencies parameter couldn't be loaded: {dependencies}")
|
|
85
|
+
|
|
86
|
+
if metadata := kwargs.get("metadata"):
|
|
87
|
+
try:
|
|
88
|
+
if isinstance(metadata, str):
|
|
89
|
+
metadata_dict = json.loads(metadata) # type: ignore
|
|
90
|
+
kwargs["metadata"] = metadata_dict
|
|
91
|
+
except json.JSONDecodeError:
|
|
92
|
+
kwargs.pop("metadata")
|
|
93
|
+
log_warning(f"Invalid metadata parameter couldn't be loaded: {metadata}")
|
|
94
|
+
|
|
95
|
+
if knowledge_filters := kwargs.get("knowledge_filters"):
|
|
96
|
+
try:
|
|
97
|
+
if isinstance(knowledge_filters, str):
|
|
98
|
+
knowledge_filters_dict = json.loads(knowledge_filters) # type: ignore
|
|
99
|
+
|
|
100
|
+
# Try to deserialize FilterExpr objects
|
|
101
|
+
from agno.filters import from_dict
|
|
102
|
+
|
|
103
|
+
# Check if it's a single FilterExpr dict or a list of FilterExpr dicts
|
|
104
|
+
if isinstance(knowledge_filters_dict, dict) and "op" in knowledge_filters_dict:
|
|
105
|
+
# Single FilterExpr - convert to list format
|
|
106
|
+
kwargs["knowledge_filters"] = [from_dict(knowledge_filters_dict)]
|
|
107
|
+
elif isinstance(knowledge_filters_dict, list):
|
|
108
|
+
# List of FilterExprs or mixed content
|
|
109
|
+
deserialized = []
|
|
110
|
+
for item in knowledge_filters_dict:
|
|
111
|
+
if isinstance(item, dict) and "op" in item:
|
|
112
|
+
deserialized.append(from_dict(item))
|
|
113
|
+
else:
|
|
114
|
+
# Keep non-FilterExpr items as-is
|
|
115
|
+
deserialized.append(item)
|
|
116
|
+
kwargs["knowledge_filters"] = deserialized
|
|
117
|
+
else:
|
|
118
|
+
# Regular dict filter
|
|
119
|
+
kwargs["knowledge_filters"] = knowledge_filters_dict
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
kwargs.pop("knowledge_filters")
|
|
122
|
+
log_warning(f"Invalid knowledge_filters parameter couldn't be loaded: {knowledge_filters}")
|
|
123
|
+
except ValueError as e:
|
|
124
|
+
# Filter deserialization failed
|
|
125
|
+
kwargs.pop("knowledge_filters")
|
|
126
|
+
log_warning(f"Invalid FilterExpr in knowledge_filters: {e}")
|
|
127
|
+
|
|
128
|
+
# Handle output_schema - convert JSON schema to Pydantic model or keep as dict
|
|
129
|
+
# use_json_schema is a control flag consumed here (not passed to Agent/Team)
|
|
130
|
+
# When true, output_schema stays as dict for direct JSON output
|
|
131
|
+
use_json_schema = kwargs.pop("use_json_schema", False)
|
|
132
|
+
if isinstance(use_json_schema, str):
|
|
133
|
+
use_json_schema = use_json_schema.lower() == "true"
|
|
134
|
+
|
|
135
|
+
if output_schema := kwargs.get("output_schema"):
|
|
136
|
+
try:
|
|
137
|
+
if isinstance(output_schema, str):
|
|
138
|
+
schema_dict = json.loads(output_schema)
|
|
139
|
+
|
|
140
|
+
if use_json_schema:
|
|
141
|
+
# Keep as dict schema for direct JSON output
|
|
142
|
+
kwargs["output_schema"] = schema_dict
|
|
143
|
+
else:
|
|
144
|
+
# Convert to Pydantic model (default behavior)
|
|
145
|
+
dynamic_model = json_schema_to_pydantic_model(schema_dict)
|
|
146
|
+
kwargs["output_schema"] = dynamic_model
|
|
147
|
+
except json.JSONDecodeError:
|
|
148
|
+
kwargs.pop("output_schema")
|
|
149
|
+
log_warning(f"Invalid output_schema JSON: {output_schema}")
|
|
150
|
+
except Exception as e:
|
|
151
|
+
kwargs.pop("output_schema")
|
|
152
|
+
log_warning(f"Failed to create output_schema model: {e}")
|
|
153
|
+
|
|
154
|
+
# Parse boolean and null values
|
|
155
|
+
for key, value in kwargs.items():
|
|
156
|
+
if isinstance(value, str) and value.lower() in ["true", "false"]:
|
|
157
|
+
kwargs[key] = value.lower() == "true"
|
|
158
|
+
elif isinstance(value, str) and value.lower() in ["null", "none"]:
|
|
159
|
+
kwargs[key] = None
|
|
160
|
+
|
|
161
|
+
return kwargs
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def format_sse_event(event: Union[RunOutputEvent, TeamRunOutputEvent, WorkflowRunOutputEvent]) -> str:
|
|
165
|
+
"""Parse JSON data into SSE-compliant format.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
event_dict: Dictionary containing the event data
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
SSE-formatted response:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
event: EventName
|
|
175
|
+
data: { ... }
|
|
176
|
+
|
|
177
|
+
event: AnotherEventName
|
|
178
|
+
data: { ... }
|
|
179
|
+
```
|
|
180
|
+
"""
|
|
181
|
+
try:
|
|
182
|
+
# Parse the JSON to extract the event type
|
|
183
|
+
event_type = event.event or "message"
|
|
184
|
+
|
|
185
|
+
# Serialize to valid JSON with double quotes and no newlines
|
|
186
|
+
clean_json = event.to_json(separators=(",", ":"), indent=None)
|
|
187
|
+
|
|
188
|
+
return f"event: {event_type}\ndata: {clean_json}\n\n"
|
|
189
|
+
except json.JSONDecodeError:
|
|
190
|
+
clean_json = event.to_json(separators=(",", ":"), indent=None)
|
|
191
|
+
return f"event: message\ndata: {clean_json}\n\n"
|
|
20
192
|
|
|
21
193
|
|
|
22
194
|
async def get_db(
|
|
23
|
-
dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]], db_id: Optional[str] = None, table: Optional[str] = None
|
|
24
|
-
) -> Union[BaseDb, AsyncBaseDb]:
|
|
195
|
+
dbs: dict[str, list[Union[BaseDb, AsyncBaseDb, RemoteDb]]], db_id: Optional[str] = None, table: Optional[str] = None
|
|
196
|
+
) -> Union[BaseDb, AsyncBaseDb, RemoteDb]:
|
|
25
197
|
"""Return the database with the given ID and/or table, or the first database if no ID/table is provided."""
|
|
26
198
|
|
|
27
199
|
if table and not db_id:
|
|
28
200
|
raise HTTPException(status_code=400, detail="The db_id query parameter is required when passing a table")
|
|
29
201
|
|
|
30
|
-
async def _has_table(db: Union[BaseDb, AsyncBaseDb], table_name: str) -> bool:
|
|
202
|
+
async def _has_table(db: Union[BaseDb, AsyncBaseDb, RemoteDb], table_name: str) -> bool:
|
|
31
203
|
"""Check if this database has the specified table (configured and actually exists)."""
|
|
32
204
|
# First check if table name is configured
|
|
33
205
|
is_configured = (
|
|
@@ -46,6 +218,10 @@ async def get_db(
|
|
|
46
218
|
if not is_configured:
|
|
47
219
|
return False
|
|
48
220
|
|
|
221
|
+
if isinstance(db, RemoteDb):
|
|
222
|
+
# We have to assume remote DBs are always configured and exist
|
|
223
|
+
return True
|
|
224
|
+
|
|
49
225
|
# Then check if table actually exists in the database
|
|
50
226
|
try:
|
|
51
227
|
if isinstance(db, AsyncBaseDb):
|
|
@@ -84,7 +260,9 @@ async def get_db(
|
|
|
84
260
|
return next(db for dbs in dbs.values() for db in dbs)
|
|
85
261
|
|
|
86
262
|
|
|
87
|
-
def get_knowledge_instance_by_db_id(
|
|
263
|
+
def get_knowledge_instance_by_db_id(
|
|
264
|
+
knowledge_instances: List[Union[Knowledge, RemoteKnowledge]], db_id: Optional[str] = None
|
|
265
|
+
) -> Union[Knowledge, RemoteKnowledge]:
|
|
88
266
|
"""Return the knowledge instance with the given ID, or the first knowledge instance if no ID is provided."""
|
|
89
267
|
if not db_id and len(knowledge_instances) == 1:
|
|
90
268
|
return next(iter(knowledge_instances))
|
|
@@ -143,56 +321,45 @@ def get_session_name(session: Dict[str, Any]) -> str:
|
|
|
143
321
|
if session_data is not None and session_data.get("session_name") is not None:
|
|
144
322
|
return session_data["session_name"]
|
|
145
323
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
runs = session.get("runs", []) or []
|
|
149
|
-
|
|
150
|
-
# For teams, identify the first Team run and avoid using the first member's run
|
|
151
|
-
if session.get("session_type") == "team":
|
|
152
|
-
run = None
|
|
153
|
-
for r in runs:
|
|
154
|
-
# If agent_id is not present, it's a team run
|
|
155
|
-
if not r.get("agent_id"):
|
|
156
|
-
run = r
|
|
157
|
-
break
|
|
158
|
-
|
|
159
|
-
# Fallback to first run if no team run found
|
|
160
|
-
if run is None and runs:
|
|
161
|
-
run = runs[0]
|
|
162
|
-
|
|
163
|
-
elif session.get("session_type") == "workflow":
|
|
164
|
-
try:
|
|
165
|
-
workflow_run = runs[0]
|
|
166
|
-
workflow_input = workflow_run.get("input")
|
|
167
|
-
if isinstance(workflow_input, str):
|
|
168
|
-
return workflow_input
|
|
169
|
-
elif isinstance(workflow_input, dict):
|
|
170
|
-
try:
|
|
171
|
-
import json
|
|
172
|
-
|
|
173
|
-
return json.dumps(workflow_input)
|
|
174
|
-
except (TypeError, ValueError):
|
|
175
|
-
pass
|
|
176
|
-
|
|
177
|
-
workflow_name = session.get("workflow_data", {}).get("name")
|
|
178
|
-
return f"New {workflow_name} Session" if workflow_name else ""
|
|
179
|
-
except (KeyError, IndexError, TypeError):
|
|
180
|
-
return ""
|
|
181
|
-
|
|
182
|
-
# For agents, use the first run
|
|
183
|
-
else:
|
|
184
|
-
run = runs[0] if runs else None
|
|
324
|
+
runs = session.get("runs", []) or []
|
|
325
|
+
session_type = session.get("session_type")
|
|
185
326
|
|
|
186
|
-
|
|
327
|
+
# Handle workflows separately
|
|
328
|
+
if session_type == "workflow":
|
|
329
|
+
if not runs:
|
|
187
330
|
return ""
|
|
331
|
+
workflow_run = runs[0]
|
|
332
|
+
workflow_input = workflow_run.get("input")
|
|
333
|
+
if isinstance(workflow_input, str):
|
|
334
|
+
return workflow_input
|
|
335
|
+
elif isinstance(workflow_input, dict):
|
|
336
|
+
try:
|
|
337
|
+
return json.dumps(workflow_input)
|
|
338
|
+
except (TypeError, ValueError):
|
|
339
|
+
pass
|
|
340
|
+
workflow_name = session.get("workflow_data", {}).get("name")
|
|
341
|
+
return f"New {workflow_name} Session" if workflow_name else ""
|
|
342
|
+
|
|
343
|
+
# For team, filter to team runs (runs without agent_id); for agents, use all runs
|
|
344
|
+
if session_type == "team":
|
|
345
|
+
runs_to_check = [r for r in runs if not r.get("agent_id")]
|
|
346
|
+
else:
|
|
347
|
+
runs_to_check = runs
|
|
348
|
+
|
|
349
|
+
# Find the first user message across runs
|
|
350
|
+
for r in runs_to_check:
|
|
351
|
+
if r is None:
|
|
352
|
+
continue
|
|
353
|
+
run_dict = r if isinstance(r, dict) else r.to_dict()
|
|
354
|
+
|
|
355
|
+
for message in run_dict.get("messages") or []:
|
|
356
|
+
if message.get("role") == "user" and message.get("content"):
|
|
357
|
+
return message["content"]
|
|
188
358
|
|
|
189
|
-
|
|
190
|
-
|
|
359
|
+
run_input = r.get("input")
|
|
360
|
+
if run_input is not None:
|
|
361
|
+
return stringify_input_content(run_input)
|
|
191
362
|
|
|
192
|
-
if run and run.get("messages"):
|
|
193
|
-
for message in run["messages"]:
|
|
194
|
-
if message["role"] == "user":
|
|
195
|
-
return message["content"]
|
|
196
363
|
return ""
|
|
197
364
|
|
|
198
365
|
|
|
@@ -204,11 +371,12 @@ def extract_input_media(run_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
204
371
|
"files": [],
|
|
205
372
|
}
|
|
206
373
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
374
|
+
input_data = run_dict.get("input", {})
|
|
375
|
+
if isinstance(input_data, dict):
|
|
376
|
+
input_media["images"].extend(input_data.get("images", []))
|
|
377
|
+
input_media["videos"].extend(input_data.get("videos", []))
|
|
378
|
+
input_media["audios"].extend(input_data.get("audios", []))
|
|
379
|
+
input_media["files"].extend(input_data.get("files", []))
|
|
212
380
|
|
|
213
381
|
return input_media
|
|
214
382
|
|
|
@@ -260,156 +428,167 @@ def extract_format(file: UploadFile) -> Optional[str]:
|
|
|
260
428
|
return None
|
|
261
429
|
|
|
262
430
|
|
|
263
|
-
def
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
elif isinstance(tool, Function):
|
|
273
|
-
formatted_tools.append(tool.to_dict())
|
|
274
|
-
elif callable(tool):
|
|
275
|
-
func = Function.from_callable(tool)
|
|
276
|
-
formatted_tools.append(func.to_dict())
|
|
277
|
-
else:
|
|
278
|
-
logger.warning(f"Unknown tool type: {type(tool)}")
|
|
279
|
-
return formatted_tools
|
|
280
|
-
|
|
431
|
+
def get_agent_by_id(
|
|
432
|
+
agent_id: str,
|
|
433
|
+
agents: Optional[List[Union[Agent, RemoteAgent]]] = None,
|
|
434
|
+
db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
|
|
435
|
+
registry: Optional[Registry] = None,
|
|
436
|
+
version: Optional[int] = None,
|
|
437
|
+
create_fresh: bool = False,
|
|
438
|
+
) -> Optional[Union[Agent, RemoteAgent]]:
|
|
439
|
+
"""Get an agent by ID, optionally creating a fresh instance for request isolation.
|
|
281
440
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
for tool in team_tools:
|
|
286
|
-
if isinstance(tool, dict):
|
|
287
|
-
formatted_tools.append(tool)
|
|
288
|
-
elif isinstance(tool, Function):
|
|
289
|
-
formatted_tools.append(tool.to_dict())
|
|
290
|
-
return formatted_tools
|
|
441
|
+
When create_fresh=True, creates a new agent instance using deep_copy() to prevent
|
|
442
|
+
state contamination between concurrent requests. The new instance shares heavy
|
|
443
|
+
resources (db, model, MCP tools) but has isolated mutable state.
|
|
291
444
|
|
|
445
|
+
Args:
|
|
446
|
+
agent_id: The agent ID to look up
|
|
447
|
+
agents: List of agents to search
|
|
448
|
+
create_fresh: If True, creates a new instance using deep_copy()
|
|
292
449
|
|
|
293
|
-
|
|
294
|
-
|
|
450
|
+
Returns:
|
|
451
|
+
The agent instance (shared or fresh copy based on create_fresh)
|
|
452
|
+
"""
|
|
453
|
+
if agent_id is None:
|
|
295
454
|
return None
|
|
296
455
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
456
|
+
# Try to get the agent from the list of agents
|
|
457
|
+
if agents:
|
|
458
|
+
for agent in agents:
|
|
459
|
+
if agent.id == agent_id:
|
|
460
|
+
if create_fresh and isinstance(agent, Agent):
|
|
461
|
+
return agent.deep_copy()
|
|
462
|
+
return agent
|
|
301
463
|
|
|
464
|
+
# Try to get the agent from the database
|
|
465
|
+
if db and isinstance(db, BaseDb):
|
|
466
|
+
from agno.agent.agent import get_agent_by_id as get_agent_by_id_db
|
|
302
467
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
468
|
+
try:
|
|
469
|
+
db_agent = get_agent_by_id_db(db=db, id=agent_id, version=version, registry=registry)
|
|
470
|
+
return db_agent
|
|
471
|
+
except Exception as e:
|
|
472
|
+
logger.error(f"Error getting agent {agent_id} from database: {e}")
|
|
473
|
+
return None
|
|
306
474
|
|
|
307
|
-
for team in teams:
|
|
308
|
-
if team.id == team_id:
|
|
309
|
-
return team
|
|
310
475
|
return None
|
|
311
476
|
|
|
312
477
|
|
|
313
|
-
def
|
|
314
|
-
|
|
315
|
-
|
|
478
|
+
def get_team_by_id(
|
|
479
|
+
team_id: str,
|
|
480
|
+
teams: Optional[List[Union[Team, RemoteTeam]]] = None,
|
|
481
|
+
create_fresh: bool = False,
|
|
482
|
+
db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
|
|
483
|
+
version: Optional[int] = None,
|
|
484
|
+
registry: Optional[Registry] = None,
|
|
485
|
+
) -> Optional[Union[Team, RemoteTeam]]:
|
|
486
|
+
"""Get a team by ID, optionally creating a fresh instance for request isolation.
|
|
316
487
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return workflow
|
|
320
|
-
return None
|
|
488
|
+
When create_fresh=True, creates a new team instance using deep_copy() to prevent
|
|
489
|
+
state contamination between concurrent requests. Member agents are also deep copied.
|
|
321
490
|
|
|
491
|
+
Args:
|
|
492
|
+
team_id: The team ID to look up
|
|
493
|
+
teams: List of teams to search
|
|
494
|
+
create_fresh: If True, creates a new instance using deep_copy()
|
|
322
495
|
|
|
323
|
-
|
|
496
|
+
Returns:
|
|
497
|
+
The team instance (shared or fresh copy based on create_fresh)
|
|
498
|
+
"""
|
|
499
|
+
if team_id is None:
|
|
500
|
+
return None
|
|
324
501
|
|
|
502
|
+
if teams:
|
|
503
|
+
for team in teams:
|
|
504
|
+
if team.id == team_id:
|
|
505
|
+
if create_fresh and isinstance(team, Team):
|
|
506
|
+
return team.deep_copy()
|
|
507
|
+
return team
|
|
325
508
|
|
|
326
|
-
|
|
327
|
-
|
|
509
|
+
if db and isinstance(db, BaseDb):
|
|
510
|
+
from agno.team.team import get_team_by_id as get_team_by_id_db
|
|
328
511
|
|
|
329
|
-
if agent.input_schema is not None:
|
|
330
512
|
try:
|
|
331
|
-
|
|
332
|
-
|
|
513
|
+
db_team = get_team_by_id_db(db=db, id=team_id, version=version, registry=registry)
|
|
514
|
+
return db_team
|
|
515
|
+
except Exception as e:
|
|
516
|
+
logger.error(f"Error getting team {team_id} from database: {e}")
|
|
333
517
|
return None
|
|
334
518
|
|
|
335
519
|
return None
|
|
336
520
|
|
|
337
521
|
|
|
338
|
-
def
|
|
339
|
-
|
|
522
|
+
def get_workflow_by_id(
|
|
523
|
+
workflow_id: str,
|
|
524
|
+
workflows: Optional[List[Union[Workflow, RemoteWorkflow]]] = None,
|
|
525
|
+
create_fresh: bool = False,
|
|
526
|
+
db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
|
|
527
|
+
version: Optional[int] = None,
|
|
528
|
+
registry: Optional[Registry] = None,
|
|
529
|
+
) -> Optional[Union[Workflow, RemoteWorkflow]]:
|
|
530
|
+
"""Get a workflow by ID, optionally creating a fresh instance for request isolation.
|
|
340
531
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return team.input_schema.model_json_schema()
|
|
344
|
-
except Exception:
|
|
345
|
-
return None
|
|
532
|
+
When create_fresh=True, creates a new workflow instance using deep_copy() to prevent
|
|
533
|
+
state contamination between concurrent requests. Steps containing agents/teams are also deep copied.
|
|
346
534
|
|
|
347
|
-
|
|
535
|
+
Args:
|
|
536
|
+
workflow_id: The workflow ID to look up
|
|
537
|
+
workflows: List of workflows to search
|
|
538
|
+
create_fresh: If True, creates a new instance using deep_copy()
|
|
539
|
+
db: Optional database interface
|
|
540
|
+
version: Workflow version, if needed
|
|
541
|
+
registry: Optional Registry instance
|
|
348
542
|
|
|
543
|
+
Returns:
|
|
544
|
+
The workflow instance (shared or fresh copy based on create_fresh)
|
|
545
|
+
"""
|
|
546
|
+
if workflow_id is None:
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
if workflows:
|
|
550
|
+
for workflow in workflows:
|
|
551
|
+
if workflow.id == workflow_id:
|
|
552
|
+
if create_fresh and isinstance(workflow, Workflow):
|
|
553
|
+
return workflow.deep_copy()
|
|
554
|
+
return workflow
|
|
349
555
|
|
|
350
|
-
|
|
351
|
-
|
|
556
|
+
if db and isinstance(db, BaseDb):
|
|
557
|
+
from agno.workflow.workflow import get_workflow_by_id as get_workflow_by_id_db
|
|
352
558
|
|
|
353
|
-
# Priority 1: Explicit input_schema (Pydantic model)
|
|
354
|
-
if workflow.input_schema is not None:
|
|
355
559
|
try:
|
|
356
|
-
|
|
357
|
-
|
|
560
|
+
db_workflow = get_workflow_by_id_db(db=db, id=workflow_id, version=version, registry=registry)
|
|
561
|
+
return db_workflow
|
|
562
|
+
except Exception as e:
|
|
563
|
+
logger.error(f"Error getting workflow {workflow_id} from database: {e}")
|
|
358
564
|
return None
|
|
359
565
|
|
|
360
|
-
# Priority 2: Auto-generate from custom kwargs
|
|
361
|
-
if workflow.steps and callable(workflow.steps):
|
|
362
|
-
custom_params = workflow.run_parameters
|
|
363
|
-
if custom_params and len(custom_params) > 1: # More than just 'message'
|
|
364
|
-
return _generate_schema_from_params(custom_params)
|
|
365
|
-
|
|
366
|
-
# Priority 3: No schema (expects string message)
|
|
367
566
|
return None
|
|
368
567
|
|
|
369
568
|
|
|
370
|
-
def
|
|
371
|
-
"""
|
|
372
|
-
|
|
373
|
-
required: List[str] = []
|
|
374
|
-
|
|
375
|
-
for param_name, param_info in params.items():
|
|
376
|
-
# Skip the default 'message' parameter for custom kwargs workflows
|
|
377
|
-
if param_name == "message":
|
|
378
|
-
continue
|
|
379
|
-
|
|
380
|
-
# Map Python types to JSON schema types
|
|
381
|
-
param_type = param_info.get("annotation", "str")
|
|
382
|
-
default_value = param_info.get("default")
|
|
383
|
-
is_required = param_info.get("required", False)
|
|
384
|
-
|
|
385
|
-
# Convert Python type annotations to JSON schema types
|
|
386
|
-
if param_type == "str":
|
|
387
|
-
properties[param_name] = {"type": "string"}
|
|
388
|
-
elif param_type == "bool":
|
|
389
|
-
properties[param_name] = {"type": "boolean"}
|
|
390
|
-
elif param_type == "int":
|
|
391
|
-
properties[param_name] = {"type": "integer"}
|
|
392
|
-
elif param_type == "float":
|
|
393
|
-
properties[param_name] = {"type": "number"}
|
|
394
|
-
elif "List" in str(param_type):
|
|
395
|
-
properties[param_name] = {"type": "array", "items": {"type": "string"}}
|
|
396
|
-
else:
|
|
397
|
-
properties[param_name] = {"type": "string"} # fallback
|
|
398
|
-
|
|
399
|
-
# Add default value if present
|
|
400
|
-
if default_value is not None:
|
|
401
|
-
properties[param_name]["default"] = default_value
|
|
402
|
-
|
|
403
|
-
# Add to required if no default value
|
|
404
|
-
if is_required and default_value is None:
|
|
405
|
-
required.append(param_name)
|
|
569
|
+
def resolve_origins(user_origins: Optional[List[str]] = None, default_origins: Optional[List[str]] = None) -> List[str]:
|
|
570
|
+
"""
|
|
571
|
+
Get CORS origins - user-provided origins override defaults.
|
|
406
572
|
|
|
407
|
-
|
|
573
|
+
Args:
|
|
574
|
+
user_origins: Optional list of user-provided CORS origins
|
|
408
575
|
|
|
409
|
-
|
|
410
|
-
|
|
576
|
+
Returns:
|
|
577
|
+
List of allowed CORS origins (user-provided if set, otherwise defaults)
|
|
578
|
+
"""
|
|
579
|
+
# User-provided origins override defaults
|
|
580
|
+
if user_origins:
|
|
581
|
+
return user_origins
|
|
411
582
|
|
|
412
|
-
|
|
583
|
+
# Default Agno domains
|
|
584
|
+
return default_origins or [
|
|
585
|
+
"http://localhost:3000",
|
|
586
|
+
"https://agno.com",
|
|
587
|
+
"https://www.agno.com",
|
|
588
|
+
"https://app.agno.com",
|
|
589
|
+
"https://os-stg.agno.com",
|
|
590
|
+
"https://os.agno.com",
|
|
591
|
+
]
|
|
413
592
|
|
|
414
593
|
|
|
415
594
|
def update_cors_middleware(app: FastAPI, new_origins: list):
|
|
@@ -511,8 +690,10 @@ def collect_mcp_tools_from_team(team: Team, mcp_tools: List[Any]) -> None:
|
|
|
511
690
|
# Check the team tools
|
|
512
691
|
if team.tools:
|
|
513
692
|
for tool in team.tools:
|
|
514
|
-
|
|
515
|
-
if
|
|
693
|
+
# Alternate method of using isinstance(tool, (MCPTools, MultiMCPTools)) to avoid imports
|
|
694
|
+
if hasattr(type(tool), "__mro__") and any(
|
|
695
|
+
c.__name__ in ["MCPTools", "MultiMCPTools"] for c in type(tool).__mro__
|
|
696
|
+
):
|
|
516
697
|
if tool not in mcp_tools:
|
|
517
698
|
mcp_tools.append(tool)
|
|
518
699
|
|
|
@@ -522,8 +703,10 @@ def collect_mcp_tools_from_team(team: Team, mcp_tools: List[Any]) -> None:
|
|
|
522
703
|
if isinstance(member, Agent):
|
|
523
704
|
if member.tools:
|
|
524
705
|
for tool in member.tools:
|
|
525
|
-
|
|
526
|
-
if
|
|
706
|
+
# Alternate method of using isinstance(tool, (MCPTools, MultiMCPTools)) to avoid imports
|
|
707
|
+
if hasattr(type(tool), "__mro__") and any(
|
|
708
|
+
c.__name__ in ["MCPTools", "MultiMCPTools"] for c in type(tool).__mro__
|
|
709
|
+
):
|
|
527
710
|
if tool not in mcp_tools:
|
|
528
711
|
mcp_tools.append(tool)
|
|
529
712
|
|
|
@@ -567,8 +750,10 @@ def collect_mcp_tools_from_workflow_step(step: Any, mcp_tools: List[Any]) -> Non
|
|
|
567
750
|
if step.agent:
|
|
568
751
|
if step.agent.tools:
|
|
569
752
|
for tool in step.agent.tools:
|
|
570
|
-
|
|
571
|
-
if
|
|
753
|
+
# Alternate method of using isinstance(tool, (MCPTools, MultiMCPTools)) to avoid imports
|
|
754
|
+
if hasattr(type(tool), "__mro__") and any(
|
|
755
|
+
c.__name__ in ["MCPTools", "MultiMCPTools"] for c in type(tool).__mro__
|
|
756
|
+
):
|
|
572
757
|
if tool not in mcp_tools:
|
|
573
758
|
mcp_tools.append(tool)
|
|
574
759
|
# Check step's team
|
|
@@ -590,8 +775,10 @@ def collect_mcp_tools_from_workflow_step(step: Any, mcp_tools: List[Any]) -> Non
|
|
|
590
775
|
# Direct agent in workflow steps
|
|
591
776
|
if step.tools:
|
|
592
777
|
for tool in step.tools:
|
|
593
|
-
|
|
594
|
-
if
|
|
778
|
+
# Alternate method of using isinstance(tool, (MCPTools, MultiMCPTools)) to avoid imports
|
|
779
|
+
if hasattr(type(tool), "__mro__") and any(
|
|
780
|
+
c.__name__ in ["MCPTools", "MultiMCPTools"] for c in type(tool).__mro__
|
|
781
|
+
):
|
|
595
782
|
if tool not in mcp_tools:
|
|
596
783
|
mcp_tools.append(tool)
|
|
597
784
|
|
|
@@ -604,6 +791,208 @@ def collect_mcp_tools_from_workflow_step(step: Any, mcp_tools: List[Any]) -> Non
|
|
|
604
791
|
collect_mcp_tools_from_workflow(step, mcp_tools)
|
|
605
792
|
|
|
606
793
|
|
|
794
|
+
def _get_python_type_from_json_schema(field_schema: Dict[str, Any], field_name: str = "NestedModel") -> Type:
|
|
795
|
+
"""Map JSON schema type to Python type with recursive handling.
|
|
796
|
+
|
|
797
|
+
Args:
|
|
798
|
+
field_schema: JSON schema dictionary for a single field
|
|
799
|
+
field_name: Name of the field (used for nested model naming)
|
|
800
|
+
|
|
801
|
+
Returns:
|
|
802
|
+
Python type corresponding to the JSON schema type
|
|
803
|
+
"""
|
|
804
|
+
if not isinstance(field_schema, dict):
|
|
805
|
+
return Any
|
|
806
|
+
|
|
807
|
+
json_type = field_schema.get("type")
|
|
808
|
+
|
|
809
|
+
# Handle basic types
|
|
810
|
+
if json_type == "string":
|
|
811
|
+
return str
|
|
812
|
+
elif json_type == "integer":
|
|
813
|
+
return int
|
|
814
|
+
elif json_type == "number":
|
|
815
|
+
return float
|
|
816
|
+
elif json_type == "boolean":
|
|
817
|
+
return bool
|
|
818
|
+
elif json_type == "null":
|
|
819
|
+
return type(None)
|
|
820
|
+
elif json_type == "array":
|
|
821
|
+
# Handle arrays with item type specification
|
|
822
|
+
items_schema = field_schema.get("items")
|
|
823
|
+
if items_schema and isinstance(items_schema, dict):
|
|
824
|
+
item_type = _get_python_type_from_json_schema(items_schema, f"{field_name}Item")
|
|
825
|
+
return List[item_type] # type: ignore
|
|
826
|
+
else:
|
|
827
|
+
# No item type specified - use generic list
|
|
828
|
+
return List[Any]
|
|
829
|
+
elif json_type == "object":
|
|
830
|
+
# Recursively create nested Pydantic model
|
|
831
|
+
nested_properties = field_schema.get("properties", {})
|
|
832
|
+
nested_required = field_schema.get("required", [])
|
|
833
|
+
nested_title = field_schema.get("title", field_name)
|
|
834
|
+
|
|
835
|
+
# Build field definitions for nested model
|
|
836
|
+
nested_fields = {}
|
|
837
|
+
for nested_field_name, nested_field_schema in nested_properties.items():
|
|
838
|
+
nested_field_type = _get_python_type_from_json_schema(nested_field_schema, nested_field_name)
|
|
839
|
+
|
|
840
|
+
if nested_field_name in nested_required:
|
|
841
|
+
nested_fields[nested_field_name] = (nested_field_type, ...)
|
|
842
|
+
else:
|
|
843
|
+
nested_fields[nested_field_name] = (Optional[nested_field_type], None) # type: ignore[assignment]
|
|
844
|
+
|
|
845
|
+
# Create nested model if it has fields
|
|
846
|
+
if nested_fields:
|
|
847
|
+
return create_model(nested_title, **nested_fields) # type: ignore
|
|
848
|
+
else:
|
|
849
|
+
# Empty object schema - use generic dict
|
|
850
|
+
return Dict[str, Any]
|
|
851
|
+
else:
|
|
852
|
+
# Unknown or unspecified type - fallback to Any
|
|
853
|
+
if json_type:
|
|
854
|
+
logger.warning(f"Unknown JSON schema type '{json_type}' for field '{field_name}', using Any")
|
|
855
|
+
return Any # type: ignore
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def json_schema_to_pydantic_model(schema: Dict[str, Any]) -> Type[BaseModel]:
|
|
859
|
+
"""Convert a JSON schema dictionary to a Pydantic BaseModel class.
|
|
860
|
+
|
|
861
|
+
This function dynamically creates a Pydantic model from a JSON schema specification,
|
|
862
|
+
handling nested objects, arrays, and optional fields.
|
|
863
|
+
|
|
864
|
+
Args:
|
|
865
|
+
schema: JSON schema dictionary with 'properties', 'required', 'type', etc.
|
|
866
|
+
|
|
867
|
+
Returns:
|
|
868
|
+
Dynamically created Pydantic BaseModel class
|
|
869
|
+
"""
|
|
870
|
+
import copy
|
|
871
|
+
|
|
872
|
+
# Deep copy to avoid modifying the original schema
|
|
873
|
+
schema = copy.deepcopy(schema)
|
|
874
|
+
|
|
875
|
+
# Extract schema components
|
|
876
|
+
model_name = schema.get("title", "DynamicModel")
|
|
877
|
+
properties = schema.get("properties", {})
|
|
878
|
+
required_fields = schema.get("required", [])
|
|
879
|
+
|
|
880
|
+
# Validate schema has properties
|
|
881
|
+
if not properties:
|
|
882
|
+
logger.warning(f"JSON schema '{model_name}' has no properties, creating empty model")
|
|
883
|
+
|
|
884
|
+
# Build field definitions for create_model
|
|
885
|
+
field_definitions = {}
|
|
886
|
+
for field_name, field_schema in properties.items():
|
|
887
|
+
try:
|
|
888
|
+
field_type = _get_python_type_from_json_schema(field_schema, field_name)
|
|
889
|
+
|
|
890
|
+
if field_name in required_fields:
|
|
891
|
+
# Required field: (type, ...)
|
|
892
|
+
field_definitions[field_name] = (field_type, ...)
|
|
893
|
+
else:
|
|
894
|
+
# Optional field: (Optional[type], None)
|
|
895
|
+
field_definitions[field_name] = (Optional[field_type], None) # type: ignore[assignment]
|
|
896
|
+
except Exception as e:
|
|
897
|
+
logger.warning(f"Failed to process field '{field_name}' in schema '{model_name}': {e}")
|
|
898
|
+
# Skip problematic fields rather than failing entirely
|
|
899
|
+
continue
|
|
900
|
+
|
|
901
|
+
# Create and return the dynamic model
|
|
902
|
+
try:
|
|
903
|
+
return create_model(model_name, **field_definitions) # type: ignore
|
|
904
|
+
except Exception as e:
|
|
905
|
+
logger.error(f"Failed to create dynamic model '{model_name}': {e}")
|
|
906
|
+
# Return a minimal model as fallback
|
|
907
|
+
return create_model(model_name)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def setup_tracing_for_os(db: Union[BaseDb, AsyncBaseDb, RemoteDb]) -> None:
|
|
911
|
+
"""Set up OpenTelemetry tracing for this agent/team/workflow."""
|
|
912
|
+
try:
|
|
913
|
+
from agno.tracing import setup_tracing
|
|
914
|
+
|
|
915
|
+
setup_tracing(db=db)
|
|
916
|
+
except ImportError:
|
|
917
|
+
logger.warning(
|
|
918
|
+
"tracing=True but OpenTelemetry packages not installed. "
|
|
919
|
+
"Install with: pip install opentelemetry-api opentelemetry-sdk openinference-instrumentation-agno"
|
|
920
|
+
)
|
|
921
|
+
except Exception as e:
|
|
922
|
+
logger.warning(f"Failed to enable tracing: {e}")
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def format_duration_ms(duration_ms: Optional[int]) -> str:
|
|
926
|
+
"""Format a duration in milliseconds to a human-readable string.
|
|
927
|
+
|
|
928
|
+
Args:
|
|
929
|
+
duration_ms: Duration in milliseconds
|
|
930
|
+
|
|
931
|
+
Returns:
|
|
932
|
+
Formatted string like "150ms" or "1.50s"
|
|
933
|
+
"""
|
|
934
|
+
if duration_ms is None or duration_ms < 1000:
|
|
935
|
+
return f"{duration_ms or 0}ms"
|
|
936
|
+
return f"{duration_ms / 1000:.2f}s"
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def timestamp_to_datetime(datetime_str: str, param_name: str = "datetime") -> "datetime":
|
|
940
|
+
"""Parse an ISO 8601 datetime string and convert to UTC.
|
|
941
|
+
|
|
942
|
+
Args:
|
|
943
|
+
datetime_str: ISO 8601 formatted datetime string (e.g., '2025-11-19T10:00:00Z' or '2025-11-19T15:30:00+05:30')
|
|
944
|
+
param_name: Name of the parameter for error messages
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
datetime object in UTC timezone
|
|
948
|
+
|
|
949
|
+
Raises:
|
|
950
|
+
HTTPException: If the datetime string is invalid
|
|
951
|
+
"""
|
|
952
|
+
try:
|
|
953
|
+
dt = datetime.fromisoformat(datetime_str.replace("Z", "+00:00"))
|
|
954
|
+
# Convert to UTC if timezone-aware, otherwise assume UTC
|
|
955
|
+
if dt.tzinfo is not None:
|
|
956
|
+
return dt.astimezone(timezone.utc)
|
|
957
|
+
else:
|
|
958
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
959
|
+
except ValueError as e:
|
|
960
|
+
raise HTTPException(
|
|
961
|
+
status_code=400,
|
|
962
|
+
detail=f"Invalid {param_name} format. Use ISO 8601 format (e.g., '2025-11-19T10:00:00Z' or '2025-11-19T10:00:00+05:30'): {e}",
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def format_team_tools(team_tools: List[Union[Function, dict]]):
|
|
967
|
+
formatted_tools: List[Dict] = []
|
|
968
|
+
if team_tools is not None:
|
|
969
|
+
for tool in team_tools:
|
|
970
|
+
if isinstance(tool, dict):
|
|
971
|
+
formatted_tools.append(tool)
|
|
972
|
+
elif isinstance(tool, Function):
|
|
973
|
+
formatted_tools.append(tool.to_dict())
|
|
974
|
+
return formatted_tools
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def format_tools(agent_tools: List[Union[Dict[str, Any], Toolkit, Function, Callable]]):
|
|
978
|
+
formatted_tools: List[Dict] = []
|
|
979
|
+
if agent_tools is not None:
|
|
980
|
+
for tool in agent_tools:
|
|
981
|
+
if isinstance(tool, dict):
|
|
982
|
+
formatted_tools.append(tool)
|
|
983
|
+
elif isinstance(tool, Toolkit):
|
|
984
|
+
for _, f in tool.functions.items():
|
|
985
|
+
formatted_tools.append(f.to_dict())
|
|
986
|
+
elif isinstance(tool, Function):
|
|
987
|
+
formatted_tools.append(tool.to_dict())
|
|
988
|
+
elif callable(tool):
|
|
989
|
+
func = Function.from_callable(tool)
|
|
990
|
+
formatted_tools.append(func.to_dict())
|
|
991
|
+
else:
|
|
992
|
+
logger.warning(f"Unknown tool type: {type(tool)}")
|
|
993
|
+
return formatted_tools
|
|
994
|
+
|
|
995
|
+
|
|
607
996
|
def stringify_input_content(input_content: Union[str, Dict[str, Any], List[Any], BaseModel]) -> str:
|
|
608
997
|
"""Convert any given input_content into its string representation.
|
|
609
998
|
|