agno 0.1.2__py3-none-any.whl → 2.3.13__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 (723) hide show
  1. agno/__init__.py +8 -0
  2. agno/agent/__init__.py +44 -5
  3. agno/agent/agent.py +10531 -2975
  4. agno/api/agent.py +14 -53
  5. agno/api/api.py +7 -46
  6. agno/api/evals.py +22 -0
  7. agno/api/os.py +17 -0
  8. agno/api/routes.py +6 -25
  9. agno/api/schemas/__init__.py +9 -0
  10. agno/api/schemas/agent.py +6 -9
  11. agno/api/schemas/evals.py +16 -0
  12. agno/api/schemas/os.py +14 -0
  13. agno/api/schemas/team.py +10 -10
  14. agno/api/schemas/utils.py +21 -0
  15. agno/api/schemas/workflows.py +16 -0
  16. agno/api/settings.py +53 -0
  17. agno/api/team.py +22 -26
  18. agno/api/workflow.py +28 -0
  19. agno/cloud/aws/base.py +214 -0
  20. agno/cloud/aws/s3/__init__.py +2 -0
  21. agno/cloud/aws/s3/api_client.py +43 -0
  22. agno/cloud/aws/s3/bucket.py +195 -0
  23. agno/cloud/aws/s3/object.py +57 -0
  24. agno/compression/__init__.py +3 -0
  25. agno/compression/manager.py +247 -0
  26. agno/culture/__init__.py +3 -0
  27. agno/culture/manager.py +956 -0
  28. agno/db/__init__.py +24 -0
  29. agno/db/async_postgres/__init__.py +3 -0
  30. agno/db/base.py +946 -0
  31. agno/db/dynamo/__init__.py +3 -0
  32. agno/db/dynamo/dynamo.py +2781 -0
  33. agno/db/dynamo/schemas.py +442 -0
  34. agno/db/dynamo/utils.py +743 -0
  35. agno/db/firestore/__init__.py +3 -0
  36. agno/db/firestore/firestore.py +2379 -0
  37. agno/db/firestore/schemas.py +181 -0
  38. agno/db/firestore/utils.py +376 -0
  39. agno/db/gcs_json/__init__.py +3 -0
  40. agno/db/gcs_json/gcs_json_db.py +1791 -0
  41. agno/db/gcs_json/utils.py +228 -0
  42. agno/db/in_memory/__init__.py +3 -0
  43. agno/db/in_memory/in_memory_db.py +1312 -0
  44. agno/db/in_memory/utils.py +230 -0
  45. agno/db/json/__init__.py +3 -0
  46. agno/db/json/json_db.py +1777 -0
  47. agno/db/json/utils.py +230 -0
  48. agno/db/migrations/manager.py +199 -0
  49. agno/db/migrations/v1_to_v2.py +635 -0
  50. agno/db/migrations/versions/v2_3_0.py +938 -0
  51. agno/db/mongo/__init__.py +17 -0
  52. agno/db/mongo/async_mongo.py +2760 -0
  53. agno/db/mongo/mongo.py +2597 -0
  54. agno/db/mongo/schemas.py +119 -0
  55. agno/db/mongo/utils.py +276 -0
  56. agno/db/mysql/__init__.py +4 -0
  57. agno/db/mysql/async_mysql.py +2912 -0
  58. agno/db/mysql/mysql.py +2923 -0
  59. agno/db/mysql/schemas.py +186 -0
  60. agno/db/mysql/utils.py +488 -0
  61. agno/db/postgres/__init__.py +4 -0
  62. agno/db/postgres/async_postgres.py +2579 -0
  63. agno/db/postgres/postgres.py +2870 -0
  64. agno/db/postgres/schemas.py +187 -0
  65. agno/db/postgres/utils.py +442 -0
  66. agno/db/redis/__init__.py +3 -0
  67. agno/db/redis/redis.py +2141 -0
  68. agno/db/redis/schemas.py +159 -0
  69. agno/db/redis/utils.py +346 -0
  70. agno/db/schemas/__init__.py +4 -0
  71. agno/db/schemas/culture.py +120 -0
  72. agno/db/schemas/evals.py +34 -0
  73. agno/db/schemas/knowledge.py +40 -0
  74. agno/db/schemas/memory.py +61 -0
  75. agno/db/singlestore/__init__.py +3 -0
  76. agno/db/singlestore/schemas.py +179 -0
  77. agno/db/singlestore/singlestore.py +2877 -0
  78. agno/db/singlestore/utils.py +384 -0
  79. agno/db/sqlite/__init__.py +4 -0
  80. agno/db/sqlite/async_sqlite.py +2911 -0
  81. agno/db/sqlite/schemas.py +181 -0
  82. agno/db/sqlite/sqlite.py +2908 -0
  83. agno/db/sqlite/utils.py +429 -0
  84. agno/db/surrealdb/__init__.py +3 -0
  85. agno/db/surrealdb/metrics.py +292 -0
  86. agno/db/surrealdb/models.py +334 -0
  87. agno/db/surrealdb/queries.py +71 -0
  88. agno/db/surrealdb/surrealdb.py +1908 -0
  89. agno/db/surrealdb/utils.py +147 -0
  90. agno/db/utils.py +118 -0
  91. agno/eval/__init__.py +24 -0
  92. agno/eval/accuracy.py +666 -276
  93. agno/eval/agent_as_judge.py +861 -0
  94. agno/eval/base.py +29 -0
  95. agno/eval/performance.py +779 -0
  96. agno/eval/reliability.py +241 -62
  97. agno/eval/utils.py +120 -0
  98. agno/exceptions.py +143 -1
  99. agno/filters.py +354 -0
  100. agno/guardrails/__init__.py +6 -0
  101. agno/guardrails/base.py +19 -0
  102. agno/guardrails/openai.py +144 -0
  103. agno/guardrails/pii.py +94 -0
  104. agno/guardrails/prompt_injection.py +52 -0
  105. agno/hooks/__init__.py +3 -0
  106. agno/hooks/decorator.py +164 -0
  107. agno/integrations/discord/__init__.py +3 -0
  108. agno/integrations/discord/client.py +203 -0
  109. agno/knowledge/__init__.py +5 -1
  110. agno/{document → knowledge}/chunking/agentic.py +22 -14
  111. agno/{document → knowledge}/chunking/document.py +2 -2
  112. agno/{document → knowledge}/chunking/fixed.py +7 -6
  113. agno/knowledge/chunking/markdown.py +151 -0
  114. agno/{document → knowledge}/chunking/recursive.py +15 -3
  115. agno/knowledge/chunking/row.py +39 -0
  116. agno/knowledge/chunking/semantic.py +91 -0
  117. agno/knowledge/chunking/strategy.py +165 -0
  118. agno/knowledge/content.py +74 -0
  119. agno/knowledge/document/__init__.py +5 -0
  120. agno/{document → knowledge/document}/base.py +12 -2
  121. agno/knowledge/embedder/__init__.py +5 -0
  122. agno/knowledge/embedder/aws_bedrock.py +343 -0
  123. agno/knowledge/embedder/azure_openai.py +210 -0
  124. agno/{embedder → knowledge/embedder}/base.py +8 -0
  125. agno/knowledge/embedder/cohere.py +323 -0
  126. agno/knowledge/embedder/fastembed.py +62 -0
  127. agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
  128. agno/knowledge/embedder/google.py +258 -0
  129. agno/knowledge/embedder/huggingface.py +94 -0
  130. agno/knowledge/embedder/jina.py +182 -0
  131. agno/knowledge/embedder/langdb.py +22 -0
  132. agno/knowledge/embedder/mistral.py +206 -0
  133. agno/knowledge/embedder/nebius.py +13 -0
  134. agno/knowledge/embedder/ollama.py +154 -0
  135. agno/knowledge/embedder/openai.py +195 -0
  136. agno/knowledge/embedder/sentence_transformer.py +63 -0
  137. agno/{embedder → knowledge/embedder}/together.py +1 -1
  138. agno/knowledge/embedder/vllm.py +262 -0
  139. agno/knowledge/embedder/voyageai.py +165 -0
  140. agno/knowledge/knowledge.py +3006 -0
  141. agno/knowledge/reader/__init__.py +7 -0
  142. agno/knowledge/reader/arxiv_reader.py +81 -0
  143. agno/knowledge/reader/base.py +95 -0
  144. agno/knowledge/reader/csv_reader.py +164 -0
  145. agno/knowledge/reader/docx_reader.py +82 -0
  146. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  147. agno/knowledge/reader/firecrawl_reader.py +201 -0
  148. agno/knowledge/reader/json_reader.py +88 -0
  149. agno/knowledge/reader/markdown_reader.py +137 -0
  150. agno/knowledge/reader/pdf_reader.py +431 -0
  151. agno/knowledge/reader/pptx_reader.py +101 -0
  152. agno/knowledge/reader/reader_factory.py +313 -0
  153. agno/knowledge/reader/s3_reader.py +89 -0
  154. agno/knowledge/reader/tavily_reader.py +193 -0
  155. agno/knowledge/reader/text_reader.py +127 -0
  156. agno/knowledge/reader/web_search_reader.py +325 -0
  157. agno/knowledge/reader/website_reader.py +455 -0
  158. agno/knowledge/reader/wikipedia_reader.py +91 -0
  159. agno/knowledge/reader/youtube_reader.py +78 -0
  160. agno/knowledge/remote_content/remote_content.py +88 -0
  161. agno/knowledge/reranker/__init__.py +3 -0
  162. agno/{reranker → knowledge/reranker}/base.py +1 -1
  163. agno/{reranker → knowledge/reranker}/cohere.py +2 -2
  164. agno/knowledge/reranker/infinity.py +195 -0
  165. agno/knowledge/reranker/sentence_transformer.py +54 -0
  166. agno/knowledge/types.py +39 -0
  167. agno/knowledge/utils.py +234 -0
  168. agno/media.py +439 -95
  169. agno/memory/__init__.py +16 -3
  170. agno/memory/manager.py +1474 -123
  171. agno/memory/strategies/__init__.py +15 -0
  172. agno/memory/strategies/base.py +66 -0
  173. agno/memory/strategies/summarize.py +196 -0
  174. agno/memory/strategies/types.py +37 -0
  175. agno/models/aimlapi/__init__.py +5 -0
  176. agno/models/aimlapi/aimlapi.py +62 -0
  177. agno/models/anthropic/__init__.py +4 -0
  178. agno/models/anthropic/claude.py +960 -496
  179. agno/models/aws/__init__.py +15 -0
  180. agno/models/aws/bedrock.py +686 -451
  181. agno/models/aws/claude.py +190 -183
  182. agno/models/azure/__init__.py +18 -1
  183. agno/models/azure/ai_foundry.py +489 -0
  184. agno/models/azure/openai_chat.py +89 -40
  185. agno/models/base.py +2477 -550
  186. agno/models/cerebras/__init__.py +12 -0
  187. agno/models/cerebras/cerebras.py +565 -0
  188. agno/models/cerebras/cerebras_openai.py +131 -0
  189. agno/models/cohere/__init__.py +4 -0
  190. agno/models/cohere/chat.py +306 -492
  191. agno/models/cometapi/__init__.py +5 -0
  192. agno/models/cometapi/cometapi.py +74 -0
  193. agno/models/dashscope/__init__.py +5 -0
  194. agno/models/dashscope/dashscope.py +90 -0
  195. agno/models/deepinfra/__init__.py +5 -0
  196. agno/models/deepinfra/deepinfra.py +45 -0
  197. agno/models/deepseek/__init__.py +4 -0
  198. agno/models/deepseek/deepseek.py +110 -9
  199. agno/models/fireworks/__init__.py +4 -0
  200. agno/models/fireworks/fireworks.py +19 -22
  201. agno/models/google/__init__.py +3 -7
  202. agno/models/google/gemini.py +1717 -662
  203. agno/models/google/utils.py +22 -0
  204. agno/models/groq/__init__.py +4 -0
  205. agno/models/groq/groq.py +391 -666
  206. agno/models/huggingface/__init__.py +4 -0
  207. agno/models/huggingface/huggingface.py +266 -538
  208. agno/models/ibm/__init__.py +5 -0
  209. agno/models/ibm/watsonx.py +432 -0
  210. agno/models/internlm/__init__.py +3 -0
  211. agno/models/internlm/internlm.py +20 -3
  212. agno/models/langdb/__init__.py +1 -0
  213. agno/models/langdb/langdb.py +60 -0
  214. agno/models/litellm/__init__.py +14 -0
  215. agno/models/litellm/chat.py +503 -0
  216. agno/models/litellm/litellm_openai.py +42 -0
  217. agno/models/llama_cpp/__init__.py +5 -0
  218. agno/models/llama_cpp/llama_cpp.py +22 -0
  219. agno/models/lmstudio/__init__.py +5 -0
  220. agno/models/lmstudio/lmstudio.py +25 -0
  221. agno/models/message.py +361 -39
  222. agno/models/meta/__init__.py +12 -0
  223. agno/models/meta/llama.py +502 -0
  224. agno/models/meta/llama_openai.py +79 -0
  225. agno/models/metrics.py +120 -0
  226. agno/models/mistral/__init__.py +4 -0
  227. agno/models/mistral/mistral.py +293 -393
  228. agno/models/nebius/__init__.py +3 -0
  229. agno/models/nebius/nebius.py +53 -0
  230. agno/models/nexus/__init__.py +3 -0
  231. agno/models/nexus/nexus.py +22 -0
  232. agno/models/nvidia/__init__.py +4 -0
  233. agno/models/nvidia/nvidia.py +22 -3
  234. agno/models/ollama/__init__.py +4 -2
  235. agno/models/ollama/chat.py +257 -492
  236. agno/models/openai/__init__.py +7 -0
  237. agno/models/openai/chat.py +725 -770
  238. agno/models/openai/like.py +16 -2
  239. agno/models/openai/responses.py +1121 -0
  240. agno/models/openrouter/__init__.py +4 -0
  241. agno/models/openrouter/openrouter.py +62 -5
  242. agno/models/perplexity/__init__.py +5 -0
  243. agno/models/perplexity/perplexity.py +203 -0
  244. agno/models/portkey/__init__.py +3 -0
  245. agno/models/portkey/portkey.py +82 -0
  246. agno/models/requesty/__init__.py +5 -0
  247. agno/models/requesty/requesty.py +69 -0
  248. agno/models/response.py +177 -7
  249. agno/models/sambanova/__init__.py +4 -0
  250. agno/models/sambanova/sambanova.py +23 -4
  251. agno/models/siliconflow/__init__.py +5 -0
  252. agno/models/siliconflow/siliconflow.py +42 -0
  253. agno/models/together/__init__.py +4 -0
  254. agno/models/together/together.py +21 -164
  255. agno/models/utils.py +266 -0
  256. agno/models/vercel/__init__.py +3 -0
  257. agno/models/vercel/v0.py +43 -0
  258. agno/models/vertexai/__init__.py +0 -1
  259. agno/models/vertexai/claude.py +190 -0
  260. agno/models/vllm/__init__.py +3 -0
  261. agno/models/vllm/vllm.py +83 -0
  262. agno/models/xai/__init__.py +2 -0
  263. agno/models/xai/xai.py +111 -7
  264. agno/os/__init__.py +3 -0
  265. agno/os/app.py +1027 -0
  266. agno/os/auth.py +244 -0
  267. agno/os/config.py +126 -0
  268. agno/os/interfaces/__init__.py +1 -0
  269. agno/os/interfaces/a2a/__init__.py +3 -0
  270. agno/os/interfaces/a2a/a2a.py +42 -0
  271. agno/os/interfaces/a2a/router.py +249 -0
  272. agno/os/interfaces/a2a/utils.py +924 -0
  273. agno/os/interfaces/agui/__init__.py +3 -0
  274. agno/os/interfaces/agui/agui.py +47 -0
  275. agno/os/interfaces/agui/router.py +147 -0
  276. agno/os/interfaces/agui/utils.py +574 -0
  277. agno/os/interfaces/base.py +25 -0
  278. agno/os/interfaces/slack/__init__.py +3 -0
  279. agno/os/interfaces/slack/router.py +148 -0
  280. agno/os/interfaces/slack/security.py +30 -0
  281. agno/os/interfaces/slack/slack.py +47 -0
  282. agno/os/interfaces/whatsapp/__init__.py +3 -0
  283. agno/os/interfaces/whatsapp/router.py +210 -0
  284. agno/os/interfaces/whatsapp/security.py +55 -0
  285. agno/os/interfaces/whatsapp/whatsapp.py +36 -0
  286. agno/os/mcp.py +293 -0
  287. agno/os/middleware/__init__.py +9 -0
  288. agno/os/middleware/jwt.py +797 -0
  289. agno/os/router.py +258 -0
  290. agno/os/routers/__init__.py +3 -0
  291. agno/os/routers/agents/__init__.py +3 -0
  292. agno/os/routers/agents/router.py +599 -0
  293. agno/os/routers/agents/schema.py +261 -0
  294. agno/os/routers/evals/__init__.py +3 -0
  295. agno/os/routers/evals/evals.py +450 -0
  296. agno/os/routers/evals/schemas.py +174 -0
  297. agno/os/routers/evals/utils.py +231 -0
  298. agno/os/routers/health.py +31 -0
  299. agno/os/routers/home.py +52 -0
  300. agno/os/routers/knowledge/__init__.py +3 -0
  301. agno/os/routers/knowledge/knowledge.py +1008 -0
  302. agno/os/routers/knowledge/schemas.py +178 -0
  303. agno/os/routers/memory/__init__.py +3 -0
  304. agno/os/routers/memory/memory.py +661 -0
  305. agno/os/routers/memory/schemas.py +88 -0
  306. agno/os/routers/metrics/__init__.py +3 -0
  307. agno/os/routers/metrics/metrics.py +190 -0
  308. agno/os/routers/metrics/schemas.py +47 -0
  309. agno/os/routers/session/__init__.py +3 -0
  310. agno/os/routers/session/session.py +997 -0
  311. agno/os/routers/teams/__init__.py +3 -0
  312. agno/os/routers/teams/router.py +512 -0
  313. agno/os/routers/teams/schema.py +257 -0
  314. agno/os/routers/traces/__init__.py +3 -0
  315. agno/os/routers/traces/schemas.py +414 -0
  316. agno/os/routers/traces/traces.py +499 -0
  317. agno/os/routers/workflows/__init__.py +3 -0
  318. agno/os/routers/workflows/router.py +624 -0
  319. agno/os/routers/workflows/schema.py +75 -0
  320. agno/os/schema.py +534 -0
  321. agno/os/scopes.py +469 -0
  322. agno/{playground → os}/settings.py +7 -15
  323. agno/os/utils.py +973 -0
  324. agno/reasoning/anthropic.py +80 -0
  325. agno/reasoning/azure_ai_foundry.py +67 -0
  326. agno/reasoning/deepseek.py +63 -0
  327. agno/reasoning/default.py +97 -0
  328. agno/reasoning/gemini.py +73 -0
  329. agno/reasoning/groq.py +71 -0
  330. agno/reasoning/helpers.py +24 -1
  331. agno/reasoning/ollama.py +67 -0
  332. agno/reasoning/openai.py +86 -0
  333. agno/reasoning/step.py +2 -1
  334. agno/reasoning/vertexai.py +76 -0
  335. agno/run/__init__.py +6 -0
  336. agno/run/agent.py +822 -0
  337. agno/run/base.py +247 -0
  338. agno/run/cancel.py +81 -0
  339. agno/run/requirement.py +181 -0
  340. agno/run/team.py +767 -0
  341. agno/run/workflow.py +708 -0
  342. agno/session/__init__.py +10 -0
  343. agno/session/agent.py +260 -0
  344. agno/session/summary.py +265 -0
  345. agno/session/team.py +342 -0
  346. agno/session/workflow.py +501 -0
  347. agno/table.py +10 -0
  348. agno/team/__init__.py +37 -0
  349. agno/team/team.py +9536 -0
  350. agno/tools/__init__.py +7 -0
  351. agno/tools/agentql.py +120 -0
  352. agno/tools/airflow.py +22 -12
  353. agno/tools/api.py +122 -0
  354. agno/tools/apify.py +276 -83
  355. agno/tools/{arxiv_toolkit.py → arxiv.py} +20 -12
  356. agno/tools/aws_lambda.py +28 -7
  357. agno/tools/aws_ses.py +66 -0
  358. agno/tools/baidusearch.py +11 -4
  359. agno/tools/bitbucket.py +292 -0
  360. agno/tools/brandfetch.py +213 -0
  361. agno/tools/bravesearch.py +106 -0
  362. agno/tools/brightdata.py +367 -0
  363. agno/tools/browserbase.py +209 -0
  364. agno/tools/calcom.py +32 -23
  365. agno/tools/calculator.py +24 -37
  366. agno/tools/cartesia.py +187 -0
  367. agno/tools/{clickup_tool.py → clickup.py} +17 -28
  368. agno/tools/confluence.py +91 -26
  369. agno/tools/crawl4ai.py +139 -43
  370. agno/tools/csv_toolkit.py +28 -22
  371. agno/tools/dalle.py +36 -22
  372. agno/tools/daytona.py +475 -0
  373. agno/tools/decorator.py +169 -14
  374. agno/tools/desi_vocal.py +23 -11
  375. agno/tools/discord.py +32 -29
  376. agno/tools/docker.py +716 -0
  377. agno/tools/duckdb.py +76 -81
  378. agno/tools/duckduckgo.py +43 -40
  379. agno/tools/e2b.py +703 -0
  380. agno/tools/eleven_labs.py +65 -54
  381. agno/tools/email.py +13 -5
  382. agno/tools/evm.py +129 -0
  383. agno/tools/exa.py +324 -42
  384. agno/tools/fal.py +39 -35
  385. agno/tools/file.py +196 -30
  386. agno/tools/file_generation.py +356 -0
  387. agno/tools/financial_datasets.py +288 -0
  388. agno/tools/firecrawl.py +108 -33
  389. agno/tools/function.py +960 -122
  390. agno/tools/giphy.py +34 -12
  391. agno/tools/github.py +1294 -97
  392. agno/tools/gmail.py +922 -0
  393. agno/tools/google_bigquery.py +117 -0
  394. agno/tools/google_drive.py +271 -0
  395. agno/tools/google_maps.py +253 -0
  396. agno/tools/googlecalendar.py +607 -107
  397. agno/tools/googlesheets.py +377 -0
  398. agno/tools/hackernews.py +20 -12
  399. agno/tools/jina.py +24 -14
  400. agno/tools/jira.py +48 -19
  401. agno/tools/knowledge.py +218 -0
  402. agno/tools/linear.py +82 -43
  403. agno/tools/linkup.py +58 -0
  404. agno/tools/local_file_system.py +15 -7
  405. agno/tools/lumalab.py +41 -26
  406. agno/tools/mcp/__init__.py +10 -0
  407. agno/tools/mcp/mcp.py +331 -0
  408. agno/tools/mcp/multi_mcp.py +347 -0
  409. agno/tools/mcp/params.py +24 -0
  410. agno/tools/mcp_toolbox.py +284 -0
  411. agno/tools/mem0.py +193 -0
  412. agno/tools/memory.py +419 -0
  413. agno/tools/mlx_transcribe.py +11 -9
  414. agno/tools/models/azure_openai.py +190 -0
  415. agno/tools/models/gemini.py +203 -0
  416. agno/tools/models/groq.py +158 -0
  417. agno/tools/models/morph.py +186 -0
  418. agno/tools/models/nebius.py +124 -0
  419. agno/tools/models_labs.py +163 -82
  420. agno/tools/moviepy_video.py +18 -13
  421. agno/tools/nano_banana.py +151 -0
  422. agno/tools/neo4j.py +134 -0
  423. agno/tools/newspaper.py +15 -4
  424. agno/tools/newspaper4k.py +19 -6
  425. agno/tools/notion.py +204 -0
  426. agno/tools/openai.py +181 -17
  427. agno/tools/openbb.py +27 -20
  428. agno/tools/opencv.py +321 -0
  429. agno/tools/openweather.py +233 -0
  430. agno/tools/oxylabs.py +385 -0
  431. agno/tools/pandas.py +25 -15
  432. agno/tools/parallel.py +314 -0
  433. agno/tools/postgres.py +238 -185
  434. agno/tools/pubmed.py +125 -13
  435. agno/tools/python.py +48 -35
  436. agno/tools/reasoning.py +283 -0
  437. agno/tools/reddit.py +207 -29
  438. agno/tools/redshift.py +406 -0
  439. agno/tools/replicate.py +69 -26
  440. agno/tools/resend.py +11 -6
  441. agno/tools/scrapegraph.py +179 -19
  442. agno/tools/searxng.py +23 -31
  443. agno/tools/serpapi.py +15 -10
  444. agno/tools/serper.py +255 -0
  445. agno/tools/shell.py +23 -12
  446. agno/tools/shopify.py +1519 -0
  447. agno/tools/slack.py +56 -14
  448. agno/tools/sleep.py +8 -6
  449. agno/tools/spider.py +35 -11
  450. agno/tools/spotify.py +919 -0
  451. agno/tools/sql.py +34 -19
  452. agno/tools/tavily.py +158 -8
  453. agno/tools/telegram.py +18 -8
  454. agno/tools/todoist.py +218 -0
  455. agno/tools/toolkit.py +134 -9
  456. agno/tools/trafilatura.py +388 -0
  457. agno/tools/trello.py +25 -28
  458. agno/tools/twilio.py +18 -9
  459. agno/tools/user_control_flow.py +78 -0
  460. agno/tools/valyu.py +228 -0
  461. agno/tools/visualization.py +467 -0
  462. agno/tools/webbrowser.py +28 -0
  463. agno/tools/webex.py +76 -0
  464. agno/tools/website.py +23 -19
  465. agno/tools/webtools.py +45 -0
  466. agno/tools/whatsapp.py +286 -0
  467. agno/tools/wikipedia.py +28 -19
  468. agno/tools/workflow.py +285 -0
  469. agno/tools/{twitter.py → x.py} +142 -46
  470. agno/tools/yfinance.py +41 -39
  471. agno/tools/youtube.py +34 -17
  472. agno/tools/zendesk.py +15 -5
  473. agno/tools/zep.py +454 -0
  474. agno/tools/zoom.py +86 -37
  475. agno/tracing/__init__.py +12 -0
  476. agno/tracing/exporter.py +157 -0
  477. agno/tracing/schemas.py +276 -0
  478. agno/tracing/setup.py +111 -0
  479. agno/utils/agent.py +938 -0
  480. agno/utils/audio.py +37 -1
  481. agno/utils/certs.py +27 -0
  482. agno/utils/code_execution.py +11 -0
  483. agno/utils/common.py +103 -20
  484. agno/utils/cryptography.py +22 -0
  485. agno/utils/dttm.py +33 -0
  486. agno/utils/events.py +700 -0
  487. agno/utils/functions.py +107 -37
  488. agno/utils/gemini.py +426 -0
  489. agno/utils/hooks.py +171 -0
  490. agno/utils/http.py +185 -0
  491. agno/utils/json_schema.py +159 -37
  492. agno/utils/knowledge.py +36 -0
  493. agno/utils/location.py +19 -0
  494. agno/utils/log.py +221 -8
  495. agno/utils/mcp.py +214 -0
  496. agno/utils/media.py +335 -14
  497. agno/utils/merge_dict.py +22 -1
  498. agno/utils/message.py +77 -2
  499. agno/utils/models/ai_foundry.py +50 -0
  500. agno/utils/models/claude.py +373 -0
  501. agno/utils/models/cohere.py +94 -0
  502. agno/utils/models/llama.py +85 -0
  503. agno/utils/models/mistral.py +100 -0
  504. agno/utils/models/openai_responses.py +140 -0
  505. agno/utils/models/schema_utils.py +153 -0
  506. agno/utils/models/watsonx.py +41 -0
  507. agno/utils/openai.py +257 -0
  508. agno/utils/pickle.py +1 -1
  509. agno/utils/pprint.py +124 -8
  510. agno/utils/print_response/agent.py +930 -0
  511. agno/utils/print_response/team.py +1914 -0
  512. agno/utils/print_response/workflow.py +1668 -0
  513. agno/utils/prompts.py +111 -0
  514. agno/utils/reasoning.py +108 -0
  515. agno/utils/response.py +163 -0
  516. agno/utils/serialize.py +32 -0
  517. agno/utils/shell.py +4 -4
  518. agno/utils/streamlit.py +487 -0
  519. agno/utils/string.py +204 -51
  520. agno/utils/team.py +139 -0
  521. agno/utils/timer.py +9 -2
  522. agno/utils/tokens.py +657 -0
  523. agno/utils/tools.py +19 -1
  524. agno/utils/whatsapp.py +305 -0
  525. agno/utils/yaml_io.py +3 -3
  526. agno/vectordb/__init__.py +2 -0
  527. agno/vectordb/base.py +87 -9
  528. agno/vectordb/cassandra/__init__.py +5 -1
  529. agno/vectordb/cassandra/cassandra.py +383 -27
  530. agno/vectordb/chroma/__init__.py +4 -0
  531. agno/vectordb/chroma/chromadb.py +748 -83
  532. agno/vectordb/clickhouse/__init__.py +7 -1
  533. agno/vectordb/clickhouse/clickhousedb.py +554 -53
  534. agno/vectordb/couchbase/__init__.py +3 -0
  535. agno/vectordb/couchbase/couchbase.py +1446 -0
  536. agno/vectordb/lancedb/__init__.py +5 -0
  537. agno/vectordb/lancedb/lance_db.py +730 -98
  538. agno/vectordb/langchaindb/__init__.py +5 -0
  539. agno/vectordb/langchaindb/langchaindb.py +163 -0
  540. agno/vectordb/lightrag/__init__.py +5 -0
  541. agno/vectordb/lightrag/lightrag.py +388 -0
  542. agno/vectordb/llamaindex/__init__.py +3 -0
  543. agno/vectordb/llamaindex/llamaindexdb.py +166 -0
  544. agno/vectordb/milvus/__init__.py +3 -0
  545. agno/vectordb/milvus/milvus.py +966 -78
  546. agno/vectordb/mongodb/__init__.py +9 -1
  547. agno/vectordb/mongodb/mongodb.py +1175 -172
  548. agno/vectordb/pgvector/__init__.py +8 -0
  549. agno/vectordb/pgvector/pgvector.py +599 -115
  550. agno/vectordb/pineconedb/__init__.py +5 -1
  551. agno/vectordb/pineconedb/pineconedb.py +406 -43
  552. agno/vectordb/qdrant/__init__.py +4 -0
  553. agno/vectordb/qdrant/qdrant.py +914 -61
  554. agno/vectordb/redis/__init__.py +9 -0
  555. agno/vectordb/redis/redisdb.py +682 -0
  556. agno/vectordb/singlestore/__init__.py +8 -1
  557. agno/vectordb/singlestore/singlestore.py +771 -0
  558. agno/vectordb/surrealdb/__init__.py +3 -0
  559. agno/vectordb/surrealdb/surrealdb.py +663 -0
  560. agno/vectordb/upstashdb/__init__.py +5 -0
  561. agno/vectordb/upstashdb/upstashdb.py +718 -0
  562. agno/vectordb/weaviate/__init__.py +8 -0
  563. agno/vectordb/weaviate/index.py +15 -0
  564. agno/vectordb/weaviate/weaviate.py +1009 -0
  565. agno/workflow/__init__.py +23 -1
  566. agno/workflow/agent.py +299 -0
  567. agno/workflow/condition.py +759 -0
  568. agno/workflow/loop.py +756 -0
  569. agno/workflow/parallel.py +853 -0
  570. agno/workflow/router.py +723 -0
  571. agno/workflow/step.py +1564 -0
  572. agno/workflow/steps.py +613 -0
  573. agno/workflow/types.py +556 -0
  574. agno/workflow/workflow.py +4327 -514
  575. agno-2.3.13.dist-info/METADATA +639 -0
  576. agno-2.3.13.dist-info/RECORD +613 -0
  577. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +1 -1
  578. agno-2.3.13.dist-info/licenses/LICENSE +201 -0
  579. agno/api/playground.py +0 -91
  580. agno/api/schemas/playground.py +0 -22
  581. agno/api/schemas/user.py +0 -22
  582. agno/api/schemas/workspace.py +0 -46
  583. agno/api/user.py +0 -160
  584. agno/api/workspace.py +0 -151
  585. agno/cli/auth_server.py +0 -118
  586. agno/cli/config.py +0 -275
  587. agno/cli/console.py +0 -88
  588. agno/cli/credentials.py +0 -23
  589. agno/cli/entrypoint.py +0 -571
  590. agno/cli/operator.py +0 -355
  591. agno/cli/settings.py +0 -85
  592. agno/cli/ws/ws_cli.py +0 -817
  593. agno/constants.py +0 -13
  594. agno/document/__init__.py +0 -1
  595. agno/document/chunking/semantic.py +0 -47
  596. agno/document/chunking/strategy.py +0 -31
  597. agno/document/reader/__init__.py +0 -1
  598. agno/document/reader/arxiv_reader.py +0 -41
  599. agno/document/reader/base.py +0 -22
  600. agno/document/reader/csv_reader.py +0 -84
  601. agno/document/reader/docx_reader.py +0 -46
  602. agno/document/reader/firecrawl_reader.py +0 -99
  603. agno/document/reader/json_reader.py +0 -43
  604. agno/document/reader/pdf_reader.py +0 -219
  605. agno/document/reader/s3/pdf_reader.py +0 -46
  606. agno/document/reader/s3/text_reader.py +0 -51
  607. agno/document/reader/text_reader.py +0 -41
  608. agno/document/reader/website_reader.py +0 -175
  609. agno/document/reader/youtube_reader.py +0 -50
  610. agno/embedder/__init__.py +0 -1
  611. agno/embedder/azure_openai.py +0 -86
  612. agno/embedder/cohere.py +0 -72
  613. agno/embedder/fastembed.py +0 -37
  614. agno/embedder/google.py +0 -73
  615. agno/embedder/huggingface.py +0 -54
  616. agno/embedder/mistral.py +0 -80
  617. agno/embedder/ollama.py +0 -57
  618. agno/embedder/openai.py +0 -74
  619. agno/embedder/sentence_transformer.py +0 -38
  620. agno/embedder/voyageai.py +0 -64
  621. agno/eval/perf.py +0 -201
  622. agno/file/__init__.py +0 -1
  623. agno/file/file.py +0 -16
  624. agno/file/local/csv.py +0 -32
  625. agno/file/local/txt.py +0 -19
  626. agno/infra/app.py +0 -240
  627. agno/infra/base.py +0 -144
  628. agno/infra/context.py +0 -20
  629. agno/infra/db_app.py +0 -52
  630. agno/infra/resource.py +0 -205
  631. agno/infra/resources.py +0 -55
  632. agno/knowledge/agent.py +0 -230
  633. agno/knowledge/arxiv.py +0 -22
  634. agno/knowledge/combined.py +0 -22
  635. agno/knowledge/csv.py +0 -28
  636. agno/knowledge/csv_url.py +0 -19
  637. agno/knowledge/document.py +0 -20
  638. agno/knowledge/docx.py +0 -30
  639. agno/knowledge/json.py +0 -28
  640. agno/knowledge/langchain.py +0 -71
  641. agno/knowledge/llamaindex.py +0 -66
  642. agno/knowledge/pdf.py +0 -28
  643. agno/knowledge/pdf_url.py +0 -26
  644. agno/knowledge/s3/base.py +0 -60
  645. agno/knowledge/s3/pdf.py +0 -21
  646. agno/knowledge/s3/text.py +0 -23
  647. agno/knowledge/text.py +0 -30
  648. agno/knowledge/website.py +0 -88
  649. agno/knowledge/wikipedia.py +0 -31
  650. agno/knowledge/youtube.py +0 -22
  651. agno/memory/agent.py +0 -392
  652. agno/memory/classifier.py +0 -104
  653. agno/memory/db/__init__.py +0 -1
  654. agno/memory/db/base.py +0 -42
  655. agno/memory/db/mongodb.py +0 -189
  656. agno/memory/db/postgres.py +0 -203
  657. agno/memory/db/sqlite.py +0 -193
  658. agno/memory/memory.py +0 -15
  659. agno/memory/row.py +0 -36
  660. agno/memory/summarizer.py +0 -192
  661. agno/memory/summary.py +0 -19
  662. agno/memory/workflow.py +0 -38
  663. agno/models/google/gemini_openai.py +0 -26
  664. agno/models/ollama/hermes.py +0 -221
  665. agno/models/ollama/tools.py +0 -362
  666. agno/models/vertexai/gemini.py +0 -595
  667. agno/playground/__init__.py +0 -3
  668. agno/playground/async_router.py +0 -421
  669. agno/playground/deploy.py +0 -249
  670. agno/playground/operator.py +0 -92
  671. agno/playground/playground.py +0 -91
  672. agno/playground/schemas.py +0 -76
  673. agno/playground/serve.py +0 -55
  674. agno/playground/sync_router.py +0 -405
  675. agno/reasoning/agent.py +0 -68
  676. agno/run/response.py +0 -112
  677. agno/storage/agent/__init__.py +0 -0
  678. agno/storage/agent/base.py +0 -38
  679. agno/storage/agent/dynamodb.py +0 -350
  680. agno/storage/agent/json.py +0 -92
  681. agno/storage/agent/mongodb.py +0 -228
  682. agno/storage/agent/postgres.py +0 -367
  683. agno/storage/agent/session.py +0 -79
  684. agno/storage/agent/singlestore.py +0 -303
  685. agno/storage/agent/sqlite.py +0 -357
  686. agno/storage/agent/yaml.py +0 -93
  687. agno/storage/workflow/__init__.py +0 -0
  688. agno/storage/workflow/base.py +0 -40
  689. agno/storage/workflow/mongodb.py +0 -233
  690. agno/storage/workflow/postgres.py +0 -366
  691. agno/storage/workflow/session.py +0 -60
  692. agno/storage/workflow/sqlite.py +0 -359
  693. agno/tools/googlesearch.py +0 -88
  694. agno/utils/defaults.py +0 -57
  695. agno/utils/filesystem.py +0 -39
  696. agno/utils/git.py +0 -52
  697. agno/utils/json_io.py +0 -30
  698. agno/utils/load_env.py +0 -19
  699. agno/utils/py_io.py +0 -19
  700. agno/utils/pyproject.py +0 -18
  701. agno/utils/resource_filter.py +0 -31
  702. agno/vectordb/singlestore/s2vectordb.py +0 -390
  703. agno/vectordb/singlestore/s2vectordb2.py +0 -355
  704. agno/workspace/__init__.py +0 -0
  705. agno/workspace/config.py +0 -325
  706. agno/workspace/enums.py +0 -6
  707. agno/workspace/helpers.py +0 -48
  708. agno/workspace/operator.py +0 -758
  709. agno/workspace/settings.py +0 -63
  710. agno-0.1.2.dist-info/LICENSE +0 -375
  711. agno-0.1.2.dist-info/METADATA +0 -502
  712. agno-0.1.2.dist-info/RECORD +0 -352
  713. agno-0.1.2.dist-info/entry_points.txt +0 -3
  714. /agno/{cli → db/migrations}/__init__.py +0 -0
  715. /agno/{cli/ws → db/migrations/versions}/__init__.py +0 -0
  716. /agno/{document/chunking/__init__.py → db/schemas/metrics.py} +0 -0
  717. /agno/{document/reader/s3 → integrations}/__init__.py +0 -0
  718. /agno/{file/local → knowledge/chunking}/__init__.py +0 -0
  719. /agno/{infra → knowledge/remote_content}/__init__.py +0 -0
  720. /agno/{knowledge/s3 → tools/models}/__init__.py +0 -0
  721. /agno/{reranker → utils/models}/__init__.py +0 -0
  722. /agno/{storage → utils/print_response}/__init__.py +0 -0
  723. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/os/utils.py ADDED
@@ -0,0 +1,973 @@
1
+ import json
2
+ from datetime import datetime, timezone
3
+ from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
4
+
5
+ from fastapi import FastAPI, HTTPException, Request, UploadFile
6
+ from fastapi.routing import APIRoute, APIRouter
7
+ from pydantic import BaseModel, create_model
8
+ from starlette.middleware.cors import CORSMiddleware
9
+
10
+ from agno.agent.agent import Agent
11
+ from agno.db.base import AsyncBaseDb, BaseDb
12
+ from agno.knowledge.knowledge import Knowledge
13
+ from agno.media import Audio, Image, Video
14
+ from agno.media import File as FileMedia
15
+ from agno.models.message import Message
16
+ from agno.os.config import AgentOSConfig
17
+ from agno.run.agent import RunOutputEvent
18
+ from agno.run.team import TeamRunOutputEvent
19
+ from agno.run.workflow import WorkflowRunOutputEvent
20
+ from agno.team.team import Team
21
+ from agno.tools import Toolkit
22
+ from agno.tools.function import Function
23
+ from agno.utils.log import log_warning, logger
24
+ from agno.workflow.workflow import Workflow
25
+
26
+
27
+ async def get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict[str, Any]:
28
+ """Given a Request and an endpoint function, return a dictionary with all extra form data fields.
29
+ Args:
30
+ request: The FastAPI Request object
31
+ endpoint_func: The function exposing the endpoint that received the request
32
+
33
+ Returns:
34
+ A dictionary of kwargs
35
+ """
36
+ import inspect
37
+
38
+ form_data = await request.form()
39
+ sig = inspect.signature(endpoint_func)
40
+ known_fields = set(sig.parameters.keys())
41
+ kwargs: Dict[str, Any] = {key: value for key, value in form_data.items() if key not in known_fields}
42
+
43
+ # Handle JSON parameters. They are passed as strings and need to be deserialized.
44
+ if session_state := kwargs.get("session_state"):
45
+ try:
46
+ if isinstance(session_state, str):
47
+ session_state_dict = json.loads(session_state) # type: ignore
48
+ kwargs["session_state"] = session_state_dict
49
+ except json.JSONDecodeError:
50
+ kwargs.pop("session_state")
51
+ log_warning(f"Invalid session_state parameter couldn't be loaded: {session_state}")
52
+
53
+ if dependencies := kwargs.get("dependencies"):
54
+ try:
55
+ if isinstance(dependencies, str):
56
+ dependencies_dict = json.loads(dependencies) # type: ignore
57
+ kwargs["dependencies"] = dependencies_dict
58
+ except json.JSONDecodeError:
59
+ kwargs.pop("dependencies")
60
+ log_warning(f"Invalid dependencies parameter couldn't be loaded: {dependencies}")
61
+
62
+ if metadata := kwargs.get("metadata"):
63
+ try:
64
+ if isinstance(metadata, str):
65
+ metadata_dict = json.loads(metadata) # type: ignore
66
+ kwargs["metadata"] = metadata_dict
67
+ except json.JSONDecodeError:
68
+ kwargs.pop("metadata")
69
+ log_warning(f"Invalid metadata parameter couldn't be loaded: {metadata}")
70
+
71
+ if knowledge_filters := kwargs.get("knowledge_filters"):
72
+ try:
73
+ if isinstance(knowledge_filters, str):
74
+ knowledge_filters_dict = json.loads(knowledge_filters) # type: ignore
75
+
76
+ # Try to deserialize FilterExpr objects
77
+ from agno.filters import from_dict
78
+
79
+ # Check if it's a single FilterExpr dict or a list of FilterExpr dicts
80
+ if isinstance(knowledge_filters_dict, dict) and "op" in knowledge_filters_dict:
81
+ # Single FilterExpr - convert to list format
82
+ kwargs["knowledge_filters"] = [from_dict(knowledge_filters_dict)]
83
+ elif isinstance(knowledge_filters_dict, list):
84
+ # List of FilterExprs or mixed content
85
+ deserialized = []
86
+ for item in knowledge_filters_dict:
87
+ if isinstance(item, dict) and "op" in item:
88
+ deserialized.append(from_dict(item))
89
+ else:
90
+ # Keep non-FilterExpr items as-is
91
+ deserialized.append(item)
92
+ kwargs["knowledge_filters"] = deserialized
93
+ else:
94
+ # Regular dict filter
95
+ kwargs["knowledge_filters"] = knowledge_filters_dict
96
+ except json.JSONDecodeError:
97
+ kwargs.pop("knowledge_filters")
98
+ log_warning(f"Invalid knowledge_filters parameter couldn't be loaded: {knowledge_filters}")
99
+ except ValueError as e:
100
+ # Filter deserialization failed
101
+ kwargs.pop("knowledge_filters")
102
+ log_warning(f"Invalid FilterExpr in knowledge_filters: {e}")
103
+
104
+ # Handle output_schema - convert JSON schema to dynamic Pydantic model
105
+ if output_schema := kwargs.get("output_schema"):
106
+ try:
107
+ if isinstance(output_schema, str):
108
+ from agno.os.utils import json_schema_to_pydantic_model
109
+
110
+ schema_dict = json.loads(output_schema)
111
+ dynamic_model = json_schema_to_pydantic_model(schema_dict)
112
+ kwargs["output_schema"] = dynamic_model
113
+ except json.JSONDecodeError:
114
+ kwargs.pop("output_schema")
115
+ log_warning(f"Invalid output_schema JSON: {output_schema}")
116
+ except Exception as e:
117
+ kwargs.pop("output_schema")
118
+ log_warning(f"Failed to create output_schema model: {e}")
119
+
120
+ # Parse boolean and null values
121
+ for key, value in kwargs.items():
122
+ if isinstance(value, str) and value.lower() in ["true", "false"]:
123
+ kwargs[key] = value.lower() == "true"
124
+ elif isinstance(value, str) and value.lower() in ["null", "none"]:
125
+ kwargs[key] = None
126
+
127
+ return kwargs
128
+
129
+
130
+ def format_sse_event(event: Union[RunOutputEvent, TeamRunOutputEvent, WorkflowRunOutputEvent]) -> str:
131
+ """Parse JSON data into SSE-compliant format.
132
+
133
+ Args:
134
+ event_dict: Dictionary containing the event data
135
+
136
+ Returns:
137
+ SSE-formatted response:
138
+
139
+ ```
140
+ event: EventName
141
+ data: { ... }
142
+
143
+ event: AnotherEventName
144
+ data: { ... }
145
+ ```
146
+ """
147
+ try:
148
+ # Parse the JSON to extract the event type
149
+ event_type = event.event or "message"
150
+
151
+ # Serialize to valid JSON with double quotes and no newlines
152
+ clean_json = event.to_json(separators=(",", ":"), indent=None)
153
+
154
+ return f"event: {event_type}\ndata: {clean_json}\n\n"
155
+ except json.JSONDecodeError:
156
+ clean_json = event.to_json(separators=(",", ":"), indent=None)
157
+ return f"event: message\ndata: {clean_json}\n\n"
158
+
159
+
160
+ async def get_db(
161
+ dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]], db_id: Optional[str] = None, table: Optional[str] = None
162
+ ) -> Union[BaseDb, AsyncBaseDb]:
163
+ """Return the database with the given ID and/or table, or the first database if no ID/table is provided."""
164
+
165
+ if table and not db_id:
166
+ raise HTTPException(status_code=400, detail="The db_id query parameter is required when passing a table")
167
+
168
+ async def _has_table(db: Union[BaseDb, AsyncBaseDb], table_name: str) -> bool:
169
+ """Check if this database has the specified table (configured and actually exists)."""
170
+ # First check if table name is configured
171
+ is_configured = (
172
+ hasattr(db, "session_table_name")
173
+ and db.session_table_name == table_name
174
+ or hasattr(db, "memory_table_name")
175
+ and db.memory_table_name == table_name
176
+ or hasattr(db, "metrics_table_name")
177
+ and db.metrics_table_name == table_name
178
+ or hasattr(db, "eval_table_name")
179
+ and db.eval_table_name == table_name
180
+ or hasattr(db, "knowledge_table_name")
181
+ and db.knowledge_table_name == table_name
182
+ )
183
+
184
+ if not is_configured:
185
+ return False
186
+
187
+ # Then check if table actually exists in the database
188
+ try:
189
+ if isinstance(db, AsyncBaseDb):
190
+ # For async databases, await the check
191
+ return await db.table_exists(table_name)
192
+ else:
193
+ # For sync databases, call directly
194
+ return db.table_exists(table_name)
195
+ except (NotImplementedError, AttributeError):
196
+ # If table_exists not implemented, fall back to configuration check
197
+ return is_configured
198
+
199
+ # If db_id is provided, first find the database with that ID
200
+ if db_id:
201
+ target_db_list = dbs.get(db_id)
202
+ if not target_db_list:
203
+ raise HTTPException(status_code=404, detail=f"No database found with id '{db_id}'")
204
+
205
+ # If table is also specified, search through all databases with this ID to find one with the table
206
+ if table:
207
+ for db in target_db_list:
208
+ if await _has_table(db, table):
209
+ return db
210
+ raise HTTPException(status_code=404, detail=f"No database with id '{db_id}' has table '{table}'")
211
+
212
+ # If no table specified, return the first database with this ID
213
+ return target_db_list[0]
214
+
215
+ # Raise if multiple databases are provided but no db_id is provided
216
+ if len(dbs) > 1:
217
+ raise HTTPException(
218
+ status_code=400, detail="The db_id query parameter is required when using multiple databases"
219
+ )
220
+
221
+ # Return the first (and only) database
222
+ return next(db for dbs in dbs.values() for db in dbs)
223
+
224
+
225
+ def get_knowledge_instance_by_db_id(knowledge_instances: List[Knowledge], db_id: Optional[str] = None) -> Knowledge:
226
+ """Return the knowledge instance with the given ID, or the first knowledge instance if no ID is provided."""
227
+ if not db_id and len(knowledge_instances) == 1:
228
+ return next(iter(knowledge_instances))
229
+
230
+ if not db_id:
231
+ raise HTTPException(
232
+ status_code=400, detail="The db_id query parameter is required when using multiple databases"
233
+ )
234
+
235
+ for knowledge in knowledge_instances:
236
+ if knowledge.contents_db and knowledge.contents_db.id == db_id:
237
+ return knowledge
238
+
239
+ raise HTTPException(status_code=404, detail=f"Knowledge instance with id '{db_id}' not found")
240
+
241
+
242
+ def get_run_input(run_dict: Dict[str, Any], is_workflow_run: bool = False) -> str:
243
+ """Get the run input from the given run dictionary
244
+
245
+ Uses the RunInput/TeamRunInput object which stores the original user input.
246
+ """
247
+
248
+ # For agent or team runs, use the stored input_content
249
+ if not is_workflow_run and run_dict.get("input") is not None:
250
+ input_data = run_dict.get("input")
251
+ if isinstance(input_data, dict) and input_data.get("input_content") is not None:
252
+ return stringify_input_content(input_data["input_content"])
253
+
254
+ if is_workflow_run:
255
+ # Check the input field directly
256
+ if run_dict.get("input") is not None:
257
+ input_value = run_dict.get("input")
258
+ return str(input_value)
259
+
260
+ # Check the step executor runs for fallback
261
+ step_executor_runs = run_dict.get("step_executor_runs", [])
262
+ if step_executor_runs:
263
+ for message in reversed(step_executor_runs[0].get("messages", [])):
264
+ if message.get("role") == "user":
265
+ return message.get("content", "")
266
+
267
+ # Final fallback: scan messages
268
+ if run_dict.get("messages") is not None:
269
+ for message in reversed(run_dict["messages"]):
270
+ if message.get("role") == "user":
271
+ return message.get("content", "")
272
+
273
+ return ""
274
+
275
+
276
+ def get_session_name(session: Dict[str, Any]) -> str:
277
+ """Get the session name from the given session dictionary"""
278
+
279
+ # If session_data.session_name is set, return that
280
+ session_data = session.get("session_data")
281
+ if session_data is not None and session_data.get("session_name") is not None:
282
+ return session_data["session_name"]
283
+
284
+ # Otherwise use the original user message
285
+ else:
286
+ runs = session.get("runs", []) or []
287
+
288
+ # For teams, identify the first Team run and avoid using the first member's run
289
+ if session.get("session_type") == "team":
290
+ run = None
291
+ for r in runs:
292
+ # If agent_id is not present, it's a team run
293
+ if not r.get("agent_id"):
294
+ run = r
295
+ break
296
+
297
+ # Fallback to first run if no team run found
298
+ if run is None and runs:
299
+ run = runs[0]
300
+
301
+ elif session.get("session_type") == "workflow":
302
+ try:
303
+ workflow_run = runs[0]
304
+ workflow_input = workflow_run.get("input")
305
+ if isinstance(workflow_input, str):
306
+ return workflow_input
307
+ elif isinstance(workflow_input, dict):
308
+ try:
309
+ import json
310
+
311
+ return json.dumps(workflow_input)
312
+ except (TypeError, ValueError):
313
+ pass
314
+
315
+ workflow_name = session.get("workflow_data", {}).get("name")
316
+ return f"New {workflow_name} Session" if workflow_name else ""
317
+ except (KeyError, IndexError, TypeError):
318
+ return ""
319
+
320
+ # For agents, use the first run
321
+ else:
322
+ run = runs[0] if runs else None
323
+
324
+ if run is None:
325
+ return ""
326
+
327
+ if not isinstance(run, dict):
328
+ run = run.to_dict()
329
+
330
+ if run and run.get("messages"):
331
+ for message in run["messages"]:
332
+ if message["role"] == "user":
333
+ return message["content"]
334
+ return ""
335
+
336
+
337
+ def extract_input_media(run_dict: Dict[str, Any]) -> Dict[str, Any]:
338
+ input_media: Dict[str, List[Any]] = {
339
+ "images": [],
340
+ "videos": [],
341
+ "audios": [],
342
+ "files": [],
343
+ }
344
+
345
+ input = run_dict.get("input", {})
346
+ input_media["images"].extend(input.get("images", []))
347
+ input_media["videos"].extend(input.get("videos", []))
348
+ input_media["audios"].extend(input.get("audios", []))
349
+ input_media["files"].extend(input.get("files", []))
350
+
351
+ return input_media
352
+
353
+
354
+ def process_image(file: UploadFile) -> Image:
355
+ content = file.file.read()
356
+ if not content:
357
+ raise HTTPException(status_code=400, detail="Empty file")
358
+ return Image(content=content, format=extract_format(file), mime_type=file.content_type)
359
+
360
+
361
+ def process_audio(file: UploadFile) -> Audio:
362
+ content = file.file.read()
363
+ if not content:
364
+ raise HTTPException(status_code=400, detail="Empty file")
365
+ return Audio(content=content, format=extract_format(file), mime_type=file.content_type)
366
+
367
+
368
+ def process_video(file: UploadFile) -> Video:
369
+ content = file.file.read()
370
+ if not content:
371
+ raise HTTPException(status_code=400, detail="Empty file")
372
+ return Video(content=content, format=extract_format(file), mime_type=file.content_type)
373
+
374
+
375
+ def process_document(file: UploadFile) -> Optional[FileMedia]:
376
+ try:
377
+ content = file.file.read()
378
+ if not content:
379
+ raise HTTPException(status_code=400, detail="Empty file")
380
+ return FileMedia(
381
+ content=content, filename=file.filename, format=extract_format(file), mime_type=file.content_type
382
+ )
383
+ except Exception as e:
384
+ logger.error(f"Error processing document {file.filename}: {e}")
385
+ return None
386
+
387
+
388
+ def extract_format(file: UploadFile) -> Optional[str]:
389
+ """Extract the File format from file name or content_type."""
390
+ # Get the format from the filename
391
+ if file.filename and "." in file.filename:
392
+ return file.filename.split(".")[-1].lower()
393
+
394
+ # Fallback to the file content_type
395
+ if file.content_type:
396
+ return file.content_type.strip().split("/")[-1]
397
+
398
+ return None
399
+
400
+
401
+ def format_tools(agent_tools: List[Union[Dict[str, Any], Toolkit, Function, Callable]]):
402
+ formatted_tools: List[Dict] = []
403
+ if agent_tools is not None:
404
+ for tool in agent_tools:
405
+ if isinstance(tool, dict):
406
+ formatted_tools.append(tool)
407
+ elif isinstance(tool, Toolkit):
408
+ for _, f in tool.functions.items():
409
+ formatted_tools.append(f.to_dict())
410
+ elif isinstance(tool, Function):
411
+ formatted_tools.append(tool.to_dict())
412
+ elif callable(tool):
413
+ func = Function.from_callable(tool)
414
+ formatted_tools.append(func.to_dict())
415
+ else:
416
+ logger.warning(f"Unknown tool type: {type(tool)}")
417
+ return formatted_tools
418
+
419
+
420
+ def format_team_tools(team_tools: List[Union[Function, dict]]):
421
+ formatted_tools: List[Dict] = []
422
+ if team_tools is not None:
423
+ for tool in team_tools:
424
+ if isinstance(tool, dict):
425
+ formatted_tools.append(tool)
426
+ elif isinstance(tool, Function):
427
+ formatted_tools.append(tool.to_dict())
428
+ return formatted_tools
429
+
430
+
431
+ def get_agent_by_id(agent_id: str, agents: Optional[List[Agent]] = None) -> Optional[Agent]:
432
+ if agent_id is None or agents is None:
433
+ return None
434
+
435
+ for agent in agents:
436
+ if agent.id == agent_id:
437
+ return agent
438
+ return None
439
+
440
+
441
+ def get_team_by_id(team_id: str, teams: Optional[List[Team]] = None) -> Optional[Team]:
442
+ if team_id is None or teams is None:
443
+ return None
444
+
445
+ for team in teams:
446
+ if team.id == team_id:
447
+ return team
448
+ return None
449
+
450
+
451
+ def get_workflow_by_id(workflow_id: str, workflows: Optional[List[Workflow]] = None) -> Optional[Workflow]:
452
+ if workflow_id is None or workflows is None:
453
+ return None
454
+
455
+ for workflow in workflows:
456
+ if workflow.id == workflow_id:
457
+ return workflow
458
+ return None
459
+
460
+
461
+ # INPUT SCHEMA VALIDATIONS
462
+
463
+
464
+ def get_agent_input_schema_dict(agent: Agent) -> Optional[Dict[str, Any]]:
465
+ """Get input schema as dictionary for API responses"""
466
+
467
+ if agent.input_schema is not None:
468
+ try:
469
+ return agent.input_schema.model_json_schema()
470
+ except Exception:
471
+ return None
472
+
473
+ return None
474
+
475
+
476
+ def get_team_input_schema_dict(team: Team) -> Optional[Dict[str, Any]]:
477
+ """Get input schema as dictionary for API responses"""
478
+
479
+ if team.input_schema is not None:
480
+ try:
481
+ return team.input_schema.model_json_schema()
482
+ except Exception:
483
+ return None
484
+
485
+ return None
486
+
487
+
488
+ def get_workflow_input_schema_dict(workflow: Workflow) -> Optional[Dict[str, Any]]:
489
+ """Get input schema as dictionary for API responses"""
490
+
491
+ # Priority 1: Explicit input_schema (Pydantic model)
492
+ if workflow.input_schema is not None:
493
+ try:
494
+ return workflow.input_schema.model_json_schema()
495
+ except Exception:
496
+ return None
497
+
498
+ # Priority 2: Auto-generate from custom kwargs
499
+ if workflow.steps and callable(workflow.steps):
500
+ custom_params = workflow.run_parameters
501
+ if custom_params and len(custom_params) > 1: # More than just 'message'
502
+ return _generate_schema_from_params(custom_params)
503
+
504
+ # Priority 3: No schema (expects string message)
505
+ return None
506
+
507
+
508
+ def _generate_schema_from_params(params: Dict[str, Any]) -> Dict[str, Any]:
509
+ """Convert function parameters to JSON schema"""
510
+ properties: Dict[str, Any] = {}
511
+ required: List[str] = []
512
+
513
+ for param_name, param_info in params.items():
514
+ # Skip the default 'message' parameter for custom kwargs workflows
515
+ if param_name == "message":
516
+ continue
517
+
518
+ # Map Python types to JSON schema types
519
+ param_type = param_info.get("annotation", "str")
520
+ default_value = param_info.get("default")
521
+ is_required = param_info.get("required", False)
522
+
523
+ # Convert Python type annotations to JSON schema types
524
+ if param_type == "str":
525
+ properties[param_name] = {"type": "string"}
526
+ elif param_type == "bool":
527
+ properties[param_name] = {"type": "boolean"}
528
+ elif param_type == "int":
529
+ properties[param_name] = {"type": "integer"}
530
+ elif param_type == "float":
531
+ properties[param_name] = {"type": "number"}
532
+ elif "List" in str(param_type):
533
+ properties[param_name] = {"type": "array", "items": {"type": "string"}}
534
+ else:
535
+ properties[param_name] = {"type": "string"} # fallback
536
+
537
+ # Add default value if present
538
+ if default_value is not None:
539
+ properties[param_name]["default"] = default_value
540
+
541
+ # Add to required if no default value
542
+ if is_required and default_value is None:
543
+ required.append(param_name)
544
+
545
+ schema = {"type": "object", "properties": properties}
546
+
547
+ if required:
548
+ schema["required"] = required
549
+
550
+ return schema
551
+
552
+
553
+ def resolve_origins(user_origins: Optional[List[str]] = None, default_origins: Optional[List[str]] = None) -> List[str]:
554
+ """
555
+ Get CORS origins - user-provided origins override defaults.
556
+
557
+ Args:
558
+ user_origins: Optional list of user-provided CORS origins
559
+
560
+ Returns:
561
+ List of allowed CORS origins (user-provided if set, otherwise defaults)
562
+ """
563
+ # User-provided origins override defaults
564
+ if user_origins:
565
+ return user_origins
566
+
567
+ # Default Agno domains
568
+ return default_origins or [
569
+ "http://localhost:3000",
570
+ "https://agno.com",
571
+ "https://www.agno.com",
572
+ "https://app.agno.com",
573
+ "https://os-stg.agno.com",
574
+ "https://os.agno.com",
575
+ ]
576
+
577
+
578
+ def update_cors_middleware(app: FastAPI, new_origins: list):
579
+ existing_origins: List[str] = []
580
+
581
+ # TODO: Allow more options where CORS is properly merged and user can disable this behaviour
582
+
583
+ # Extract existing origins from current CORS middleware
584
+ for middleware in app.user_middleware:
585
+ if middleware.cls == CORSMiddleware:
586
+ if hasattr(middleware, "kwargs"):
587
+ origins_value = middleware.kwargs.get("allow_origins", [])
588
+ if isinstance(origins_value, list):
589
+ existing_origins = origins_value
590
+ else:
591
+ existing_origins = []
592
+ break
593
+ # Merge origins
594
+ merged_origins = list(set(new_origins + existing_origins))
595
+ final_origins = [origin for origin in merged_origins if origin != "*"]
596
+
597
+ # Remove existing CORS
598
+ app.user_middleware = [m for m in app.user_middleware if m.cls != CORSMiddleware]
599
+ app.middleware_stack = None
600
+
601
+ # Add updated CORS
602
+ app.add_middleware(
603
+ CORSMiddleware, # type: ignore
604
+ allow_origins=final_origins,
605
+ allow_credentials=True,
606
+ allow_methods=["*"],
607
+ allow_headers=["*"],
608
+ expose_headers=["*"],
609
+ )
610
+
611
+
612
+ def get_existing_route_paths(fastapi_app: FastAPI) -> Dict[str, List[str]]:
613
+ """Get all existing route paths and methods from the FastAPI app.
614
+
615
+ Returns:
616
+ Dict[str, List[str]]: Dictionary mapping paths to list of HTTP methods
617
+ """
618
+ existing_paths: Dict[str, Any] = {}
619
+ for route in fastapi_app.routes:
620
+ if isinstance(route, APIRoute):
621
+ path = route.path
622
+ methods = list(route.methods) if route.methods else []
623
+ if path in existing_paths:
624
+ existing_paths[path].extend(methods)
625
+ else:
626
+ existing_paths[path] = methods
627
+ return existing_paths
628
+
629
+
630
+ def find_conflicting_routes(fastapi_app: FastAPI, router: APIRouter) -> List[Dict[str, Any]]:
631
+ """Find conflicting routes in the FastAPI app.
632
+
633
+ Args:
634
+ fastapi_app: The FastAPI app with all existing routes
635
+ router: The APIRouter to add
636
+
637
+ Returns:
638
+ List[Dict[str, Any]]: List of conflicting routes
639
+ """
640
+ existing_paths = get_existing_route_paths(fastapi_app)
641
+
642
+ conflicts = []
643
+
644
+ for route in router.routes:
645
+ if isinstance(route, APIRoute):
646
+ full_path = route.path
647
+ route_methods = list(route.methods) if route.methods else []
648
+
649
+ if full_path in existing_paths:
650
+ conflicting_methods: Set[str] = set(route_methods) & set(existing_paths[full_path])
651
+ if conflicting_methods:
652
+ conflicts.append({"path": full_path, "methods": list(conflicting_methods), "route": route})
653
+ return conflicts
654
+
655
+
656
+ def load_yaml_config(config_file_path: str) -> AgentOSConfig:
657
+ """Load a YAML config file and return the configuration as an AgentOSConfig instance."""
658
+ from pathlib import Path
659
+
660
+ import yaml
661
+
662
+ # Validate that the path points to a YAML file
663
+ path = Path(config_file_path)
664
+ if path.suffix.lower() not in [".yaml", ".yml"]:
665
+ raise ValueError(f"Config file must have a .yaml or .yml extension, got: {config_file_path}")
666
+
667
+ # Load the YAML file
668
+ with open(config_file_path, "r") as f:
669
+ return AgentOSConfig.model_validate(yaml.safe_load(f))
670
+
671
+
672
+ def collect_mcp_tools_from_team(team: Team, mcp_tools: List[Any]) -> None:
673
+ """Recursively collect MCP tools from a team and its members."""
674
+ # Check the team tools
675
+ if team.tools:
676
+ for tool in team.tools:
677
+ # Alternate method of using isinstance(tool, (MCPTools, MultiMCPTools)) to avoid imports
678
+ if hasattr(type(tool), "__mro__") and any(
679
+ c.__name__ in ["MCPTools", "MultiMCPTools"] for c in type(tool).__mro__
680
+ ):
681
+ if tool not in mcp_tools:
682
+ mcp_tools.append(tool)
683
+
684
+ # Recursively check team members
685
+ if team.members:
686
+ for member in team.members:
687
+ if isinstance(member, Agent):
688
+ if member.tools:
689
+ for tool in member.tools:
690
+ # Alternate method of using isinstance(tool, (MCPTools, MultiMCPTools)) to avoid imports
691
+ if hasattr(type(tool), "__mro__") and any(
692
+ c.__name__ in ["MCPTools", "MultiMCPTools"] for c in type(tool).__mro__
693
+ ):
694
+ if tool not in mcp_tools:
695
+ mcp_tools.append(tool)
696
+
697
+ elif isinstance(member, Team):
698
+ # Recursively check nested team
699
+ collect_mcp_tools_from_team(member, mcp_tools)
700
+
701
+
702
+ def collect_mcp_tools_from_workflow(workflow: Workflow, mcp_tools: List[Any]) -> None:
703
+ """Recursively collect MCP tools from a workflow and its steps."""
704
+ from agno.workflow.steps import Steps
705
+
706
+ # Recursively check workflow steps
707
+ if workflow.steps:
708
+ if isinstance(workflow.steps, list):
709
+ # Handle list of steps
710
+ for step in workflow.steps:
711
+ collect_mcp_tools_from_workflow_step(step, mcp_tools)
712
+
713
+ elif isinstance(workflow.steps, Steps):
714
+ # Handle Steps container
715
+ if steps := workflow.steps.steps:
716
+ for step in steps:
717
+ collect_mcp_tools_from_workflow_step(step, mcp_tools)
718
+
719
+ elif callable(workflow.steps):
720
+ pass
721
+
722
+
723
+ def collect_mcp_tools_from_workflow_step(step: Any, mcp_tools: List[Any]) -> None:
724
+ """Collect MCP tools from a single workflow step."""
725
+ from agno.workflow.condition import Condition
726
+ from agno.workflow.loop import Loop
727
+ from agno.workflow.parallel import Parallel
728
+ from agno.workflow.router import Router
729
+ from agno.workflow.step import Step
730
+ from agno.workflow.steps import Steps
731
+
732
+ if isinstance(step, Step):
733
+ # Check step's agent
734
+ if step.agent:
735
+ if step.agent.tools:
736
+ for tool in step.agent.tools:
737
+ # Alternate method of using isinstance(tool, (MCPTools, MultiMCPTools)) to avoid imports
738
+ if hasattr(type(tool), "__mro__") and any(
739
+ c.__name__ in ["MCPTools", "MultiMCPTools"] for c in type(tool).__mro__
740
+ ):
741
+ if tool not in mcp_tools:
742
+ mcp_tools.append(tool)
743
+ # Check step's team
744
+ if step.team:
745
+ collect_mcp_tools_from_team(step.team, mcp_tools)
746
+
747
+ elif isinstance(step, Steps):
748
+ if steps := step.steps:
749
+ for step in steps:
750
+ collect_mcp_tools_from_workflow_step(step, mcp_tools)
751
+
752
+ elif isinstance(step, (Parallel, Loop, Condition, Router)):
753
+ # These contain other steps - recursively check them
754
+ if hasattr(step, "steps") and step.steps:
755
+ for sub_step in step.steps:
756
+ collect_mcp_tools_from_workflow_step(sub_step, mcp_tools)
757
+
758
+ elif isinstance(step, Agent):
759
+ # Direct agent in workflow steps
760
+ if step.tools:
761
+ for tool in step.tools:
762
+ # Alternate method of using isinstance(tool, (MCPTools, MultiMCPTools)) to avoid imports
763
+ if hasattr(type(tool), "__mro__") and any(
764
+ c.__name__ in ["MCPTools", "MultiMCPTools"] for c in type(tool).__mro__
765
+ ):
766
+ if tool not in mcp_tools:
767
+ mcp_tools.append(tool)
768
+
769
+ elif isinstance(step, Team):
770
+ # Direct team in workflow steps
771
+ collect_mcp_tools_from_team(step, mcp_tools)
772
+
773
+ elif isinstance(step, Workflow):
774
+ # Nested workflow
775
+ collect_mcp_tools_from_workflow(step, mcp_tools)
776
+
777
+
778
+ def stringify_input_content(input_content: Union[str, Dict[str, Any], List[Any], BaseModel]) -> str:
779
+ """Convert any given input_content into its string representation.
780
+
781
+ This handles both serialized (dict) and live (object) input_content formats.
782
+ """
783
+ import json
784
+
785
+ if isinstance(input_content, str):
786
+ return input_content
787
+ elif isinstance(input_content, Message):
788
+ return json.dumps(input_content.to_dict())
789
+ elif isinstance(input_content, dict):
790
+ return json.dumps(input_content, indent=2, default=str)
791
+ elif isinstance(input_content, list):
792
+ if input_content:
793
+ # Handle live Message objects
794
+ if isinstance(input_content[0], Message):
795
+ return json.dumps([m.to_dict() for m in input_content])
796
+ # Handle serialized Message dicts
797
+ elif isinstance(input_content[0], dict) and input_content[0].get("role") == "user":
798
+ return input_content[0].get("content", str(input_content))
799
+ return str(input_content)
800
+ else:
801
+ return str(input_content)
802
+
803
+
804
+ def _get_python_type_from_json_schema(field_schema: Dict[str, Any], field_name: str = "NestedModel") -> Type:
805
+ """Map JSON schema type to Python type with recursive handling.
806
+
807
+ Args:
808
+ field_schema: JSON schema dictionary for a single field
809
+ field_name: Name of the field (used for nested model naming)
810
+
811
+ Returns:
812
+ Python type corresponding to the JSON schema type
813
+ """
814
+ if not isinstance(field_schema, dict):
815
+ return Any
816
+
817
+ json_type = field_schema.get("type")
818
+
819
+ # Handle basic types
820
+ if json_type == "string":
821
+ return str
822
+ elif json_type == "integer":
823
+ return int
824
+ elif json_type == "number":
825
+ return float
826
+ elif json_type == "boolean":
827
+ return bool
828
+ elif json_type == "null":
829
+ return type(None)
830
+ elif json_type == "array":
831
+ # Handle arrays with item type specification
832
+ items_schema = field_schema.get("items")
833
+ if items_schema and isinstance(items_schema, dict):
834
+ item_type = _get_python_type_from_json_schema(items_schema, f"{field_name}Item")
835
+ return List[item_type] # type: ignore
836
+ else:
837
+ # No item type specified - use generic list
838
+ return List[Any]
839
+ elif json_type == "object":
840
+ # Recursively create nested Pydantic model
841
+ nested_properties = field_schema.get("properties", {})
842
+ nested_required = field_schema.get("required", [])
843
+ nested_title = field_schema.get("title", field_name)
844
+
845
+ # Build field definitions for nested model
846
+ nested_fields = {}
847
+ for nested_field_name, nested_field_schema in nested_properties.items():
848
+ nested_field_type = _get_python_type_from_json_schema(nested_field_schema, nested_field_name)
849
+
850
+ if nested_field_name in nested_required:
851
+ nested_fields[nested_field_name] = (nested_field_type, ...)
852
+ else:
853
+ nested_fields[nested_field_name] = (Optional[nested_field_type], None) # type: ignore[assignment]
854
+
855
+ # Create nested model if it has fields
856
+ if nested_fields:
857
+ return create_model(nested_title, **nested_fields) # type: ignore
858
+ else:
859
+ # Empty object schema - use generic dict
860
+ return Dict[str, Any]
861
+ else:
862
+ # Unknown or unspecified type - fallback to Any
863
+ if json_type:
864
+ logger.warning(f"Unknown JSON schema type '{json_type}' for field '{field_name}', using Any")
865
+ return Any
866
+
867
+
868
+ def json_schema_to_pydantic_model(schema: Dict[str, Any]) -> Type[BaseModel]:
869
+ """Convert a JSON schema dictionary to a Pydantic BaseModel class.
870
+
871
+ This function dynamically creates a Pydantic model from a JSON schema specification,
872
+ handling nested objects, arrays, and optional fields.
873
+
874
+ Args:
875
+ schema: JSON schema dictionary with 'properties', 'required', 'type', etc.
876
+
877
+ Returns:
878
+ Dynamically created Pydantic BaseModel class
879
+ """
880
+ import copy
881
+
882
+ # Deep copy to avoid modifying the original schema
883
+ schema = copy.deepcopy(schema)
884
+
885
+ # Extract schema components
886
+ model_name = schema.get("title", "DynamicModel")
887
+ properties = schema.get("properties", {})
888
+ required_fields = schema.get("required", [])
889
+
890
+ # Validate schema has properties
891
+ if not properties:
892
+ logger.warning(f"JSON schema '{model_name}' has no properties, creating empty model")
893
+
894
+ # Build field definitions for create_model
895
+ field_definitions = {}
896
+ for field_name, field_schema in properties.items():
897
+ try:
898
+ field_type = _get_python_type_from_json_schema(field_schema, field_name)
899
+
900
+ if field_name in required_fields:
901
+ # Required field: (type, ...)
902
+ field_definitions[field_name] = (field_type, ...)
903
+ else:
904
+ # Optional field: (Optional[type], None)
905
+ field_definitions[field_name] = (Optional[field_type], None) # type: ignore[assignment]
906
+ except Exception as e:
907
+ logger.warning(f"Failed to process field '{field_name}' in schema '{model_name}': {e}")
908
+ # Skip problematic fields rather than failing entirely
909
+ continue
910
+
911
+ # Create and return the dynamic model
912
+ try:
913
+ return create_model(model_name, **field_definitions) # type: ignore
914
+ except Exception as e:
915
+ logger.error(f"Failed to create dynamic model '{model_name}': {e}")
916
+ # Return a minimal model as fallback
917
+ return create_model(model_name)
918
+
919
+
920
+ def setup_tracing_for_os(db: Union[BaseDb, AsyncBaseDb]) -> None:
921
+ """Set up OpenTelemetry tracing for this agent/team/workflow."""
922
+ try:
923
+ from agno.tracing import setup_tracing
924
+
925
+ setup_tracing(db=db)
926
+ except ImportError:
927
+ logger.warning(
928
+ "tracing=True but OpenTelemetry packages not installed. "
929
+ "Install with: pip install opentelemetry-api opentelemetry-sdk openinference-instrumentation-agno"
930
+ )
931
+ except Exception as e:
932
+ logger.warning(f"Failed to enable tracing: {e}")
933
+
934
+
935
+ def format_duration_ms(duration_ms: Optional[int]) -> str:
936
+ """Format a duration in milliseconds to a human-readable string.
937
+
938
+ Args:
939
+ duration_ms: Duration in milliseconds
940
+
941
+ Returns:
942
+ Formatted string like "150ms" or "1.50s"
943
+ """
944
+ if duration_ms is None or duration_ms < 1000:
945
+ return f"{duration_ms or 0}ms"
946
+ return f"{duration_ms / 1000:.2f}s"
947
+
948
+
949
+ def parse_datetime_to_utc(datetime_str: str, param_name: str = "datetime") -> "datetime":
950
+ """Parse an ISO 8601 datetime string and convert to UTC.
951
+
952
+ Args:
953
+ datetime_str: ISO 8601 formatted datetime string (e.g., '2025-11-19T10:00:00Z' or '2025-11-19T15:30:00+05:30')
954
+ param_name: Name of the parameter for error messages
955
+
956
+ Returns:
957
+ datetime object in UTC timezone
958
+
959
+ Raises:
960
+ HTTPException: If the datetime string is invalid
961
+ """
962
+ try:
963
+ dt = datetime.fromisoformat(datetime_str.replace("Z", "+00:00"))
964
+ # Convert to UTC if timezone-aware, otherwise assume UTC
965
+ if dt.tzinfo is not None:
966
+ return dt.astimezone(timezone.utc)
967
+ else:
968
+ return dt.replace(tzinfo=timezone.utc)
969
+ except ValueError as e:
970
+ raise HTTPException(
971
+ status_code=400,
972
+ 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}",
973
+ )