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.
Files changed (383) hide show
  1. agno/agent/__init__.py +6 -0
  2. agno/agent/agent.py +5252 -3145
  3. agno/agent/remote.py +525 -0
  4. agno/api/api.py +2 -0
  5. agno/client/__init__.py +3 -0
  6. agno/client/a2a/__init__.py +10 -0
  7. agno/client/a2a/client.py +554 -0
  8. agno/client/a2a/schemas.py +112 -0
  9. agno/client/a2a/utils.py +369 -0
  10. agno/client/os.py +2669 -0
  11. agno/compression/__init__.py +3 -0
  12. agno/compression/manager.py +247 -0
  13. agno/culture/manager.py +2 -2
  14. agno/db/base.py +927 -6
  15. agno/db/dynamo/dynamo.py +788 -2
  16. agno/db/dynamo/schemas.py +128 -0
  17. agno/db/dynamo/utils.py +26 -3
  18. agno/db/firestore/firestore.py +674 -50
  19. agno/db/firestore/schemas.py +41 -0
  20. agno/db/firestore/utils.py +25 -10
  21. agno/db/gcs_json/gcs_json_db.py +506 -3
  22. agno/db/gcs_json/utils.py +14 -2
  23. agno/db/in_memory/in_memory_db.py +203 -4
  24. agno/db/in_memory/utils.py +14 -2
  25. agno/db/json/json_db.py +498 -2
  26. agno/db/json/utils.py +14 -2
  27. agno/db/migrations/manager.py +199 -0
  28. agno/db/migrations/utils.py +19 -0
  29. agno/db/migrations/v1_to_v2.py +54 -16
  30. agno/db/migrations/versions/__init__.py +0 -0
  31. agno/db/migrations/versions/v2_3_0.py +977 -0
  32. agno/db/mongo/async_mongo.py +1013 -39
  33. agno/db/mongo/mongo.py +684 -4
  34. agno/db/mongo/schemas.py +48 -0
  35. agno/db/mongo/utils.py +17 -0
  36. agno/db/mysql/__init__.py +2 -1
  37. agno/db/mysql/async_mysql.py +2958 -0
  38. agno/db/mysql/mysql.py +722 -53
  39. agno/db/mysql/schemas.py +77 -11
  40. agno/db/mysql/utils.py +151 -8
  41. agno/db/postgres/async_postgres.py +1254 -137
  42. agno/db/postgres/postgres.py +2316 -93
  43. agno/db/postgres/schemas.py +153 -21
  44. agno/db/postgres/utils.py +22 -7
  45. agno/db/redis/redis.py +531 -3
  46. agno/db/redis/schemas.py +36 -0
  47. agno/db/redis/utils.py +31 -15
  48. agno/db/schemas/evals.py +1 -0
  49. agno/db/schemas/memory.py +20 -9
  50. agno/db/singlestore/schemas.py +70 -1
  51. agno/db/singlestore/singlestore.py +737 -74
  52. agno/db/singlestore/utils.py +13 -3
  53. agno/db/sqlite/async_sqlite.py +1069 -89
  54. agno/db/sqlite/schemas.py +133 -1
  55. agno/db/sqlite/sqlite.py +2203 -165
  56. agno/db/sqlite/utils.py +21 -11
  57. agno/db/surrealdb/models.py +25 -0
  58. agno/db/surrealdb/surrealdb.py +603 -1
  59. agno/db/utils.py +60 -0
  60. agno/eval/__init__.py +26 -3
  61. agno/eval/accuracy.py +25 -12
  62. agno/eval/agent_as_judge.py +871 -0
  63. agno/eval/base.py +29 -0
  64. agno/eval/performance.py +10 -4
  65. agno/eval/reliability.py +22 -13
  66. agno/eval/utils.py +2 -1
  67. agno/exceptions.py +42 -0
  68. agno/hooks/__init__.py +3 -0
  69. agno/hooks/decorator.py +164 -0
  70. agno/integrations/discord/client.py +13 -2
  71. agno/knowledge/__init__.py +4 -0
  72. agno/knowledge/chunking/code.py +90 -0
  73. agno/knowledge/chunking/document.py +65 -4
  74. agno/knowledge/chunking/fixed.py +4 -1
  75. agno/knowledge/chunking/markdown.py +102 -11
  76. agno/knowledge/chunking/recursive.py +2 -2
  77. agno/knowledge/chunking/semantic.py +130 -48
  78. agno/knowledge/chunking/strategy.py +18 -0
  79. agno/knowledge/embedder/azure_openai.py +0 -1
  80. agno/knowledge/embedder/google.py +1 -1
  81. agno/knowledge/embedder/mistral.py +1 -1
  82. agno/knowledge/embedder/nebius.py +1 -1
  83. agno/knowledge/embedder/openai.py +16 -12
  84. agno/knowledge/filesystem.py +412 -0
  85. agno/knowledge/knowledge.py +4261 -1199
  86. agno/knowledge/protocol.py +134 -0
  87. agno/knowledge/reader/arxiv_reader.py +3 -2
  88. agno/knowledge/reader/base.py +9 -7
  89. agno/knowledge/reader/csv_reader.py +91 -42
  90. agno/knowledge/reader/docx_reader.py +9 -10
  91. agno/knowledge/reader/excel_reader.py +225 -0
  92. agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
  93. agno/knowledge/reader/firecrawl_reader.py +3 -2
  94. agno/knowledge/reader/json_reader.py +16 -22
  95. agno/knowledge/reader/markdown_reader.py +15 -14
  96. agno/knowledge/reader/pdf_reader.py +33 -28
  97. agno/knowledge/reader/pptx_reader.py +9 -10
  98. agno/knowledge/reader/reader_factory.py +135 -1
  99. agno/knowledge/reader/s3_reader.py +8 -16
  100. agno/knowledge/reader/tavily_reader.py +3 -3
  101. agno/knowledge/reader/text_reader.py +15 -14
  102. agno/knowledge/reader/utils/__init__.py +17 -0
  103. agno/knowledge/reader/utils/spreadsheet.py +114 -0
  104. agno/knowledge/reader/web_search_reader.py +8 -65
  105. agno/knowledge/reader/website_reader.py +16 -13
  106. agno/knowledge/reader/wikipedia_reader.py +36 -3
  107. agno/knowledge/reader/youtube_reader.py +3 -2
  108. agno/knowledge/remote_content/__init__.py +33 -0
  109. agno/knowledge/remote_content/config.py +266 -0
  110. agno/knowledge/remote_content/remote_content.py +105 -17
  111. agno/knowledge/utils.py +76 -22
  112. agno/learn/__init__.py +71 -0
  113. agno/learn/config.py +463 -0
  114. agno/learn/curate.py +185 -0
  115. agno/learn/machine.py +725 -0
  116. agno/learn/schemas.py +1114 -0
  117. agno/learn/stores/__init__.py +38 -0
  118. agno/learn/stores/decision_log.py +1156 -0
  119. agno/learn/stores/entity_memory.py +3275 -0
  120. agno/learn/stores/learned_knowledge.py +1583 -0
  121. agno/learn/stores/protocol.py +117 -0
  122. agno/learn/stores/session_context.py +1217 -0
  123. agno/learn/stores/user_memory.py +1495 -0
  124. agno/learn/stores/user_profile.py +1220 -0
  125. agno/learn/utils.py +209 -0
  126. agno/media.py +22 -6
  127. agno/memory/__init__.py +14 -1
  128. agno/memory/manager.py +223 -8
  129. agno/memory/strategies/__init__.py +15 -0
  130. agno/memory/strategies/base.py +66 -0
  131. agno/memory/strategies/summarize.py +196 -0
  132. agno/memory/strategies/types.py +37 -0
  133. agno/models/aimlapi/aimlapi.py +17 -0
  134. agno/models/anthropic/claude.py +434 -59
  135. agno/models/aws/bedrock.py +121 -20
  136. agno/models/aws/claude.py +131 -274
  137. agno/models/azure/ai_foundry.py +10 -6
  138. agno/models/azure/openai_chat.py +33 -10
  139. agno/models/base.py +1162 -561
  140. agno/models/cerebras/cerebras.py +120 -24
  141. agno/models/cerebras/cerebras_openai.py +21 -2
  142. agno/models/cohere/chat.py +65 -6
  143. agno/models/cometapi/cometapi.py +18 -1
  144. agno/models/dashscope/dashscope.py +2 -3
  145. agno/models/deepinfra/deepinfra.py +18 -1
  146. agno/models/deepseek/deepseek.py +69 -3
  147. agno/models/fireworks/fireworks.py +18 -1
  148. agno/models/google/gemini.py +959 -89
  149. agno/models/google/utils.py +22 -0
  150. agno/models/groq/groq.py +48 -18
  151. agno/models/huggingface/huggingface.py +17 -6
  152. agno/models/ibm/watsonx.py +16 -6
  153. agno/models/internlm/internlm.py +18 -1
  154. agno/models/langdb/langdb.py +13 -1
  155. agno/models/litellm/chat.py +88 -9
  156. agno/models/litellm/litellm_openai.py +18 -1
  157. agno/models/message.py +24 -5
  158. agno/models/meta/llama.py +40 -13
  159. agno/models/meta/llama_openai.py +22 -21
  160. agno/models/metrics.py +12 -0
  161. agno/models/mistral/mistral.py +8 -4
  162. agno/models/n1n/__init__.py +3 -0
  163. agno/models/n1n/n1n.py +57 -0
  164. agno/models/nebius/nebius.py +6 -7
  165. agno/models/nvidia/nvidia.py +20 -3
  166. agno/models/ollama/__init__.py +2 -0
  167. agno/models/ollama/chat.py +17 -6
  168. agno/models/ollama/responses.py +100 -0
  169. agno/models/openai/__init__.py +2 -0
  170. agno/models/openai/chat.py +117 -26
  171. agno/models/openai/open_responses.py +46 -0
  172. agno/models/openai/responses.py +110 -32
  173. agno/models/openrouter/__init__.py +2 -0
  174. agno/models/openrouter/openrouter.py +67 -2
  175. agno/models/openrouter/responses.py +146 -0
  176. agno/models/perplexity/perplexity.py +19 -1
  177. agno/models/portkey/portkey.py +7 -6
  178. agno/models/requesty/requesty.py +19 -2
  179. agno/models/response.py +20 -2
  180. agno/models/sambanova/sambanova.py +20 -3
  181. agno/models/siliconflow/siliconflow.py +19 -2
  182. agno/models/together/together.py +20 -3
  183. agno/models/vercel/v0.py +20 -3
  184. agno/models/vertexai/claude.py +124 -4
  185. agno/models/vllm/vllm.py +19 -14
  186. agno/models/xai/xai.py +19 -2
  187. agno/os/app.py +467 -137
  188. agno/os/auth.py +253 -5
  189. agno/os/config.py +22 -0
  190. agno/os/interfaces/a2a/a2a.py +7 -6
  191. agno/os/interfaces/a2a/router.py +635 -26
  192. agno/os/interfaces/a2a/utils.py +32 -33
  193. agno/os/interfaces/agui/agui.py +5 -3
  194. agno/os/interfaces/agui/router.py +26 -16
  195. agno/os/interfaces/agui/utils.py +97 -57
  196. agno/os/interfaces/base.py +7 -7
  197. agno/os/interfaces/slack/router.py +16 -7
  198. agno/os/interfaces/slack/slack.py +7 -7
  199. agno/os/interfaces/whatsapp/router.py +35 -7
  200. agno/os/interfaces/whatsapp/security.py +3 -1
  201. agno/os/interfaces/whatsapp/whatsapp.py +11 -8
  202. agno/os/managers.py +326 -0
  203. agno/os/mcp.py +652 -79
  204. agno/os/middleware/__init__.py +4 -0
  205. agno/os/middleware/jwt.py +718 -115
  206. agno/os/middleware/trailing_slash.py +27 -0
  207. agno/os/router.py +105 -1558
  208. agno/os/routers/agents/__init__.py +3 -0
  209. agno/os/routers/agents/router.py +655 -0
  210. agno/os/routers/agents/schema.py +288 -0
  211. agno/os/routers/components/__init__.py +3 -0
  212. agno/os/routers/components/components.py +475 -0
  213. agno/os/routers/database.py +155 -0
  214. agno/os/routers/evals/evals.py +111 -18
  215. agno/os/routers/evals/schemas.py +38 -5
  216. agno/os/routers/evals/utils.py +80 -11
  217. agno/os/routers/health.py +3 -3
  218. agno/os/routers/knowledge/knowledge.py +284 -35
  219. agno/os/routers/knowledge/schemas.py +14 -2
  220. agno/os/routers/memory/memory.py +274 -11
  221. agno/os/routers/memory/schemas.py +44 -3
  222. agno/os/routers/metrics/metrics.py +30 -15
  223. agno/os/routers/metrics/schemas.py +10 -6
  224. agno/os/routers/registry/__init__.py +3 -0
  225. agno/os/routers/registry/registry.py +337 -0
  226. agno/os/routers/session/session.py +143 -14
  227. agno/os/routers/teams/__init__.py +3 -0
  228. agno/os/routers/teams/router.py +550 -0
  229. agno/os/routers/teams/schema.py +280 -0
  230. agno/os/routers/traces/__init__.py +3 -0
  231. agno/os/routers/traces/schemas.py +414 -0
  232. agno/os/routers/traces/traces.py +549 -0
  233. agno/os/routers/workflows/__init__.py +3 -0
  234. agno/os/routers/workflows/router.py +757 -0
  235. agno/os/routers/workflows/schema.py +139 -0
  236. agno/os/schema.py +157 -584
  237. agno/os/scopes.py +469 -0
  238. agno/os/settings.py +3 -0
  239. agno/os/utils.py +574 -185
  240. agno/reasoning/anthropic.py +85 -1
  241. agno/reasoning/azure_ai_foundry.py +93 -1
  242. agno/reasoning/deepseek.py +102 -2
  243. agno/reasoning/default.py +6 -7
  244. agno/reasoning/gemini.py +87 -3
  245. agno/reasoning/groq.py +109 -2
  246. agno/reasoning/helpers.py +6 -7
  247. agno/reasoning/manager.py +1238 -0
  248. agno/reasoning/ollama.py +93 -1
  249. agno/reasoning/openai.py +115 -1
  250. agno/reasoning/vertexai.py +85 -1
  251. agno/registry/__init__.py +3 -0
  252. agno/registry/registry.py +68 -0
  253. agno/remote/__init__.py +3 -0
  254. agno/remote/base.py +581 -0
  255. agno/run/__init__.py +2 -4
  256. agno/run/agent.py +134 -19
  257. agno/run/base.py +49 -1
  258. agno/run/cancel.py +65 -52
  259. agno/run/cancellation_management/__init__.py +9 -0
  260. agno/run/cancellation_management/base.py +78 -0
  261. agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
  262. agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
  263. agno/run/requirement.py +181 -0
  264. agno/run/team.py +111 -19
  265. agno/run/workflow.py +2 -1
  266. agno/session/agent.py +57 -92
  267. agno/session/summary.py +1 -1
  268. agno/session/team.py +62 -115
  269. agno/session/workflow.py +353 -57
  270. agno/skills/__init__.py +17 -0
  271. agno/skills/agent_skills.py +377 -0
  272. agno/skills/errors.py +32 -0
  273. agno/skills/loaders/__init__.py +4 -0
  274. agno/skills/loaders/base.py +27 -0
  275. agno/skills/loaders/local.py +216 -0
  276. agno/skills/skill.py +65 -0
  277. agno/skills/utils.py +107 -0
  278. agno/skills/validator.py +277 -0
  279. agno/table.py +10 -0
  280. agno/team/__init__.py +5 -1
  281. agno/team/remote.py +447 -0
  282. agno/team/team.py +3769 -2202
  283. agno/tools/brandfetch.py +27 -18
  284. agno/tools/browserbase.py +225 -16
  285. agno/tools/crawl4ai.py +3 -0
  286. agno/tools/duckduckgo.py +25 -71
  287. agno/tools/exa.py +0 -21
  288. agno/tools/file.py +14 -13
  289. agno/tools/file_generation.py +12 -6
  290. agno/tools/firecrawl.py +15 -7
  291. agno/tools/function.py +94 -113
  292. agno/tools/google_bigquery.py +11 -2
  293. agno/tools/google_drive.py +4 -3
  294. agno/tools/knowledge.py +9 -4
  295. agno/tools/mcp/mcp.py +301 -18
  296. agno/tools/mcp/multi_mcp.py +269 -14
  297. agno/tools/mem0.py +11 -10
  298. agno/tools/memory.py +47 -46
  299. agno/tools/mlx_transcribe.py +10 -7
  300. agno/tools/models/nebius.py +5 -5
  301. agno/tools/models_labs.py +20 -10
  302. agno/tools/nano_banana.py +151 -0
  303. agno/tools/parallel.py +0 -7
  304. agno/tools/postgres.py +76 -36
  305. agno/tools/python.py +14 -6
  306. agno/tools/reasoning.py +30 -23
  307. agno/tools/redshift.py +406 -0
  308. agno/tools/shopify.py +1519 -0
  309. agno/tools/spotify.py +919 -0
  310. agno/tools/tavily.py +4 -1
  311. agno/tools/toolkit.py +253 -18
  312. agno/tools/websearch.py +93 -0
  313. agno/tools/website.py +1 -1
  314. agno/tools/wikipedia.py +1 -1
  315. agno/tools/workflow.py +56 -48
  316. agno/tools/yfinance.py +12 -11
  317. agno/tracing/__init__.py +12 -0
  318. agno/tracing/exporter.py +161 -0
  319. agno/tracing/schemas.py +276 -0
  320. agno/tracing/setup.py +112 -0
  321. agno/utils/agent.py +251 -10
  322. agno/utils/cryptography.py +22 -0
  323. agno/utils/dttm.py +33 -0
  324. agno/utils/events.py +264 -7
  325. agno/utils/hooks.py +111 -3
  326. agno/utils/http.py +161 -2
  327. agno/utils/mcp.py +49 -8
  328. agno/utils/media.py +22 -1
  329. agno/utils/models/ai_foundry.py +9 -2
  330. agno/utils/models/claude.py +20 -5
  331. agno/utils/models/cohere.py +9 -2
  332. agno/utils/models/llama.py +9 -2
  333. agno/utils/models/mistral.py +4 -2
  334. agno/utils/os.py +0 -0
  335. agno/utils/print_response/agent.py +99 -16
  336. agno/utils/print_response/team.py +223 -24
  337. agno/utils/print_response/workflow.py +0 -2
  338. agno/utils/prompts.py +8 -6
  339. agno/utils/remote.py +23 -0
  340. agno/utils/response.py +1 -13
  341. agno/utils/string.py +91 -2
  342. agno/utils/team.py +62 -12
  343. agno/utils/tokens.py +657 -0
  344. agno/vectordb/base.py +15 -2
  345. agno/vectordb/cassandra/cassandra.py +1 -1
  346. agno/vectordb/chroma/__init__.py +2 -1
  347. agno/vectordb/chroma/chromadb.py +468 -23
  348. agno/vectordb/clickhouse/clickhousedb.py +1 -1
  349. agno/vectordb/couchbase/couchbase.py +6 -2
  350. agno/vectordb/lancedb/lance_db.py +7 -38
  351. agno/vectordb/lightrag/lightrag.py +7 -6
  352. agno/vectordb/milvus/milvus.py +118 -84
  353. agno/vectordb/mongodb/__init__.py +2 -1
  354. agno/vectordb/mongodb/mongodb.py +14 -31
  355. agno/vectordb/pgvector/pgvector.py +120 -66
  356. agno/vectordb/pineconedb/pineconedb.py +2 -19
  357. agno/vectordb/qdrant/__init__.py +2 -1
  358. agno/vectordb/qdrant/qdrant.py +33 -56
  359. agno/vectordb/redis/__init__.py +2 -1
  360. agno/vectordb/redis/redisdb.py +19 -31
  361. agno/vectordb/singlestore/singlestore.py +17 -9
  362. agno/vectordb/surrealdb/surrealdb.py +2 -38
  363. agno/vectordb/weaviate/__init__.py +2 -1
  364. agno/vectordb/weaviate/weaviate.py +7 -3
  365. agno/workflow/__init__.py +5 -1
  366. agno/workflow/agent.py +2 -2
  367. agno/workflow/condition.py +12 -10
  368. agno/workflow/loop.py +28 -9
  369. agno/workflow/parallel.py +21 -13
  370. agno/workflow/remote.py +362 -0
  371. agno/workflow/router.py +12 -9
  372. agno/workflow/step.py +261 -36
  373. agno/workflow/steps.py +12 -8
  374. agno/workflow/types.py +40 -77
  375. agno/workflow/workflow.py +939 -213
  376. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
  377. agno-2.4.3.dist-info/RECORD +677 -0
  378. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
  379. agno/tools/googlesearch.py +0 -98
  380. agno/tools/memori.py +0 -339
  381. agno-2.2.13.dist-info/RECORD +0 -575
  382. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
  383. {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
- from typing import Any, Callable, Dict, List, Optional, Set, Union
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.agent import 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.team.team import Team
16
- from agno.tools import Toolkit
17
- from agno.tools.function import Function
18
- from agno.utils.log import logger
19
- from agno.workflow.workflow import Workflow
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(knowledge_instances: List[Knowledge], db_id: Optional[str] = None) -> Knowledge:
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
- # Otherwise use the original user message
147
- else:
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
- if run is None:
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
- if not isinstance(run, dict):
190
- run = run.to_dict()
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
- input = run_dict.get("input", {})
208
- input_media["images"].extend(input.get("images", []))
209
- input_media["videos"].extend(input.get("videos", []))
210
- input_media["audios"].extend(input.get("audios", []))
211
- input_media["files"].extend(input.get("files", []))
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 format_tools(agent_tools: List[Union[Dict[str, Any], Toolkit, Function, Callable]]):
264
- formatted_tools: List[Dict] = []
265
- if agent_tools is not None:
266
- for tool in agent_tools:
267
- if isinstance(tool, dict):
268
- formatted_tools.append(tool)
269
- elif isinstance(tool, Toolkit):
270
- for _, f in tool.functions.items():
271
- formatted_tools.append(f.to_dict())
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
- def format_team_tools(team_tools: List[Union[Function, dict]]):
283
- formatted_tools: List[Dict] = []
284
- if team_tools is not None:
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
- def get_agent_by_id(agent_id: str, agents: Optional[List[Agent]] = None) -> Optional[Agent]:
294
- if agent_id is None or agents is None:
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
- for agent in agents:
298
- if agent.id == agent_id:
299
- return agent
300
- return None
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
- def get_team_by_id(team_id: str, teams: Optional[List[Team]] = None) -> Optional[Team]:
304
- if team_id is None or teams is None:
305
- return None
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 get_workflow_by_id(workflow_id: str, workflows: Optional[List[Workflow]] = None) -> Optional[Workflow]:
314
- if workflow_id is None or workflows is None:
315
- return None
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
- for workflow in workflows:
318
- if workflow.id == workflow_id:
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
- # INPUT SCHEMA VALIDATIONS
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
- def get_agent_input_schema_dict(agent: Agent) -> Optional[Dict[str, Any]]:
327
- """Get input schema as dictionary for API responses"""
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
- return agent.input_schema.model_json_schema()
332
- except Exception:
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 get_team_input_schema_dict(team: Team) -> Optional[Dict[str, Any]]:
339
- """Get input schema as dictionary for API responses"""
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
- if team.input_schema is not None:
342
- try:
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
- return None
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
- def get_workflow_input_schema_dict(workflow: Workflow) -> Optional[Dict[str, Any]]:
351
- """Get input schema as dictionary for API responses"""
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
- return workflow.input_schema.model_json_schema()
357
- except Exception:
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 _generate_schema_from_params(params: Dict[str, Any]) -> Dict[str, Any]:
371
- """Convert function parameters to JSON schema"""
372
- properties: Dict[str, Any] = {}
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
- schema = {"type": "object", "properties": properties}
573
+ Args:
574
+ user_origins: Optional list of user-provided CORS origins
408
575
 
409
- if required:
410
- schema["required"] = required
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
- return schema
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
- type_name = type(tool).__name__
515
- if type_name in ("MCPTools", "MultiMCPTools"):
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
- type_name = type(tool).__name__
526
- if type_name in ("MCPTools", "MultiMCPTools"):
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
- type_name = type(tool).__name__
571
- if type_name in ("MCPTools", "MultiMCPTools"):
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
- type_name = type(tool).__name__
594
- if type_name in ("MCPTools", "MultiMCPTools"):
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