agno 2.0.0rc2__py3-none-any.whl → 2.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. agno/agent/agent.py +6009 -2874
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/culture/__init__.py +3 -0
  5. agno/culture/manager.py +956 -0
  6. agno/db/async_postgres/__init__.py +3 -0
  7. agno/db/base.py +385 -6
  8. agno/db/dynamo/dynamo.py +388 -81
  9. agno/db/dynamo/schemas.py +47 -10
  10. agno/db/dynamo/utils.py +63 -4
  11. agno/db/firestore/firestore.py +435 -64
  12. agno/db/firestore/schemas.py +11 -0
  13. agno/db/firestore/utils.py +102 -4
  14. agno/db/gcs_json/gcs_json_db.py +384 -42
  15. agno/db/gcs_json/utils.py +60 -26
  16. agno/db/in_memory/in_memory_db.py +351 -66
  17. agno/db/in_memory/utils.py +60 -2
  18. agno/db/json/json_db.py +339 -48
  19. agno/db/json/utils.py +60 -26
  20. agno/db/migrations/manager.py +199 -0
  21. agno/db/migrations/v1_to_v2.py +510 -37
  22. agno/db/migrations/versions/__init__.py +0 -0
  23. agno/db/migrations/versions/v2_3_0.py +938 -0
  24. agno/db/mongo/__init__.py +15 -1
  25. agno/db/mongo/async_mongo.py +2036 -0
  26. agno/db/mongo/mongo.py +653 -76
  27. agno/db/mongo/schemas.py +13 -0
  28. agno/db/mongo/utils.py +80 -8
  29. agno/db/mysql/mysql.py +687 -25
  30. agno/db/mysql/schemas.py +61 -37
  31. agno/db/mysql/utils.py +60 -2
  32. agno/db/postgres/__init__.py +2 -1
  33. agno/db/postgres/async_postgres.py +2001 -0
  34. agno/db/postgres/postgres.py +676 -57
  35. agno/db/postgres/schemas.py +43 -18
  36. agno/db/postgres/utils.py +164 -2
  37. agno/db/redis/redis.py +344 -38
  38. agno/db/redis/schemas.py +18 -0
  39. agno/db/redis/utils.py +60 -2
  40. agno/db/schemas/__init__.py +2 -1
  41. agno/db/schemas/culture.py +120 -0
  42. agno/db/schemas/memory.py +13 -0
  43. agno/db/singlestore/schemas.py +26 -1
  44. agno/db/singlestore/singlestore.py +687 -53
  45. agno/db/singlestore/utils.py +60 -2
  46. agno/db/sqlite/__init__.py +2 -1
  47. agno/db/sqlite/async_sqlite.py +2371 -0
  48. agno/db/sqlite/schemas.py +24 -0
  49. agno/db/sqlite/sqlite.py +774 -85
  50. agno/db/sqlite/utils.py +168 -5
  51. agno/db/surrealdb/__init__.py +3 -0
  52. agno/db/surrealdb/metrics.py +292 -0
  53. agno/db/surrealdb/models.py +309 -0
  54. agno/db/surrealdb/queries.py +71 -0
  55. agno/db/surrealdb/surrealdb.py +1361 -0
  56. agno/db/surrealdb/utils.py +147 -0
  57. agno/db/utils.py +50 -22
  58. agno/eval/accuracy.py +50 -43
  59. agno/eval/performance.py +6 -3
  60. agno/eval/reliability.py +6 -3
  61. agno/eval/utils.py +33 -16
  62. agno/exceptions.py +68 -1
  63. agno/filters.py +354 -0
  64. agno/guardrails/__init__.py +6 -0
  65. agno/guardrails/base.py +19 -0
  66. agno/guardrails/openai.py +144 -0
  67. agno/guardrails/pii.py +94 -0
  68. agno/guardrails/prompt_injection.py +52 -0
  69. agno/integrations/discord/client.py +1 -0
  70. agno/knowledge/chunking/agentic.py +13 -10
  71. agno/knowledge/chunking/fixed.py +1 -1
  72. agno/knowledge/chunking/semantic.py +40 -8
  73. agno/knowledge/chunking/strategy.py +59 -15
  74. agno/knowledge/embedder/aws_bedrock.py +9 -4
  75. agno/knowledge/embedder/azure_openai.py +54 -0
  76. agno/knowledge/embedder/base.py +2 -0
  77. agno/knowledge/embedder/cohere.py +184 -5
  78. agno/knowledge/embedder/fastembed.py +1 -1
  79. agno/knowledge/embedder/google.py +79 -1
  80. agno/knowledge/embedder/huggingface.py +9 -4
  81. agno/knowledge/embedder/jina.py +63 -0
  82. agno/knowledge/embedder/mistral.py +78 -11
  83. agno/knowledge/embedder/nebius.py +1 -1
  84. agno/knowledge/embedder/ollama.py +13 -0
  85. agno/knowledge/embedder/openai.py +37 -65
  86. agno/knowledge/embedder/sentence_transformer.py +8 -4
  87. agno/knowledge/embedder/vllm.py +262 -0
  88. agno/knowledge/embedder/voyageai.py +69 -16
  89. agno/knowledge/knowledge.py +595 -187
  90. agno/knowledge/reader/base.py +9 -2
  91. agno/knowledge/reader/csv_reader.py +8 -10
  92. agno/knowledge/reader/docx_reader.py +5 -6
  93. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  94. agno/knowledge/reader/json_reader.py +6 -5
  95. agno/knowledge/reader/markdown_reader.py +13 -13
  96. agno/knowledge/reader/pdf_reader.py +43 -68
  97. agno/knowledge/reader/pptx_reader.py +101 -0
  98. agno/knowledge/reader/reader_factory.py +51 -6
  99. agno/knowledge/reader/s3_reader.py +3 -15
  100. agno/knowledge/reader/tavily_reader.py +194 -0
  101. agno/knowledge/reader/text_reader.py +13 -13
  102. agno/knowledge/reader/web_search_reader.py +2 -43
  103. agno/knowledge/reader/website_reader.py +43 -25
  104. agno/knowledge/reranker/__init__.py +3 -0
  105. agno/knowledge/types.py +9 -0
  106. agno/knowledge/utils.py +20 -0
  107. agno/media.py +339 -266
  108. agno/memory/manager.py +336 -82
  109. agno/models/aimlapi/aimlapi.py +2 -2
  110. agno/models/anthropic/claude.py +183 -37
  111. agno/models/aws/bedrock.py +52 -112
  112. agno/models/aws/claude.py +33 -1
  113. agno/models/azure/ai_foundry.py +33 -15
  114. agno/models/azure/openai_chat.py +25 -8
  115. agno/models/base.py +1011 -566
  116. agno/models/cerebras/cerebras.py +19 -13
  117. agno/models/cerebras/cerebras_openai.py +8 -5
  118. agno/models/cohere/chat.py +27 -1
  119. agno/models/cometapi/__init__.py +5 -0
  120. agno/models/cometapi/cometapi.py +57 -0
  121. agno/models/dashscope/dashscope.py +1 -0
  122. agno/models/deepinfra/deepinfra.py +2 -2
  123. agno/models/deepseek/deepseek.py +2 -2
  124. agno/models/fireworks/fireworks.py +2 -2
  125. agno/models/google/gemini.py +110 -37
  126. agno/models/groq/groq.py +28 -11
  127. agno/models/huggingface/huggingface.py +2 -1
  128. agno/models/internlm/internlm.py +2 -2
  129. agno/models/langdb/langdb.py +4 -4
  130. agno/models/litellm/chat.py +18 -1
  131. agno/models/litellm/litellm_openai.py +2 -2
  132. agno/models/llama_cpp/__init__.py +5 -0
  133. agno/models/llama_cpp/llama_cpp.py +22 -0
  134. agno/models/message.py +143 -4
  135. agno/models/meta/llama.py +27 -10
  136. agno/models/meta/llama_openai.py +5 -17
  137. agno/models/nebius/nebius.py +6 -6
  138. agno/models/nexus/__init__.py +3 -0
  139. agno/models/nexus/nexus.py +22 -0
  140. agno/models/nvidia/nvidia.py +2 -2
  141. agno/models/ollama/chat.py +60 -6
  142. agno/models/openai/chat.py +102 -43
  143. agno/models/openai/responses.py +103 -106
  144. agno/models/openrouter/openrouter.py +41 -3
  145. agno/models/perplexity/perplexity.py +4 -5
  146. agno/models/portkey/portkey.py +3 -3
  147. agno/models/requesty/__init__.py +5 -0
  148. agno/models/requesty/requesty.py +52 -0
  149. agno/models/response.py +81 -5
  150. agno/models/sambanova/sambanova.py +2 -2
  151. agno/models/siliconflow/__init__.py +5 -0
  152. agno/models/siliconflow/siliconflow.py +25 -0
  153. agno/models/together/together.py +2 -2
  154. agno/models/utils.py +254 -8
  155. agno/models/vercel/v0.py +2 -2
  156. agno/models/vertexai/__init__.py +0 -0
  157. agno/models/vertexai/claude.py +96 -0
  158. agno/models/vllm/vllm.py +1 -0
  159. agno/models/xai/xai.py +3 -2
  160. agno/os/app.py +543 -175
  161. agno/os/auth.py +24 -14
  162. agno/os/config.py +1 -0
  163. agno/os/interfaces/__init__.py +1 -0
  164. agno/os/interfaces/a2a/__init__.py +3 -0
  165. agno/os/interfaces/a2a/a2a.py +42 -0
  166. agno/os/interfaces/a2a/router.py +250 -0
  167. agno/os/interfaces/a2a/utils.py +924 -0
  168. agno/os/interfaces/agui/agui.py +23 -7
  169. agno/os/interfaces/agui/router.py +27 -3
  170. agno/os/interfaces/agui/utils.py +242 -142
  171. agno/os/interfaces/base.py +6 -2
  172. agno/os/interfaces/slack/router.py +81 -23
  173. agno/os/interfaces/slack/slack.py +29 -14
  174. agno/os/interfaces/whatsapp/router.py +11 -4
  175. agno/os/interfaces/whatsapp/whatsapp.py +14 -7
  176. agno/os/mcp.py +111 -54
  177. agno/os/middleware/__init__.py +7 -0
  178. agno/os/middleware/jwt.py +233 -0
  179. agno/os/router.py +556 -139
  180. agno/os/routers/evals/evals.py +71 -34
  181. agno/os/routers/evals/schemas.py +31 -31
  182. agno/os/routers/evals/utils.py +6 -5
  183. agno/os/routers/health.py +31 -0
  184. agno/os/routers/home.py +52 -0
  185. agno/os/routers/knowledge/knowledge.py +185 -38
  186. agno/os/routers/knowledge/schemas.py +82 -22
  187. agno/os/routers/memory/memory.py +158 -53
  188. agno/os/routers/memory/schemas.py +20 -16
  189. agno/os/routers/metrics/metrics.py +20 -8
  190. agno/os/routers/metrics/schemas.py +16 -16
  191. agno/os/routers/session/session.py +499 -38
  192. agno/os/schema.py +308 -198
  193. agno/os/utils.py +401 -41
  194. agno/reasoning/anthropic.py +80 -0
  195. agno/reasoning/azure_ai_foundry.py +2 -2
  196. agno/reasoning/deepseek.py +2 -2
  197. agno/reasoning/default.py +3 -1
  198. agno/reasoning/gemini.py +73 -0
  199. agno/reasoning/groq.py +2 -2
  200. agno/reasoning/ollama.py +2 -2
  201. agno/reasoning/openai.py +7 -2
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +266 -112
  205. agno/run/base.py +53 -24
  206. agno/run/team.py +252 -111
  207. agno/run/workflow.py +156 -45
  208. agno/session/agent.py +105 -89
  209. agno/session/summary.py +65 -25
  210. agno/session/team.py +176 -96
  211. agno/session/workflow.py +406 -40
  212. agno/team/team.py +3854 -1692
  213. agno/tools/brightdata.py +3 -3
  214. agno/tools/cartesia.py +3 -5
  215. agno/tools/dalle.py +9 -8
  216. agno/tools/decorator.py +4 -2
  217. agno/tools/desi_vocal.py +2 -2
  218. agno/tools/duckduckgo.py +15 -11
  219. agno/tools/e2b.py +20 -13
  220. agno/tools/eleven_labs.py +26 -28
  221. agno/tools/exa.py +21 -16
  222. agno/tools/fal.py +4 -4
  223. agno/tools/file.py +153 -23
  224. agno/tools/file_generation.py +350 -0
  225. agno/tools/firecrawl.py +4 -4
  226. agno/tools/function.py +257 -37
  227. agno/tools/giphy.py +2 -2
  228. agno/tools/gmail.py +238 -14
  229. agno/tools/google_drive.py +270 -0
  230. agno/tools/googlecalendar.py +36 -8
  231. agno/tools/googlesheets.py +20 -5
  232. agno/tools/jira.py +20 -0
  233. agno/tools/knowledge.py +3 -3
  234. agno/tools/lumalab.py +3 -3
  235. agno/tools/mcp/__init__.py +10 -0
  236. agno/tools/mcp/mcp.py +331 -0
  237. agno/tools/mcp/multi_mcp.py +347 -0
  238. agno/tools/mcp/params.py +24 -0
  239. agno/tools/mcp_toolbox.py +284 -0
  240. agno/tools/mem0.py +11 -17
  241. agno/tools/memori.py +1 -53
  242. agno/tools/memory.py +419 -0
  243. agno/tools/models/azure_openai.py +2 -2
  244. agno/tools/models/gemini.py +3 -3
  245. agno/tools/models/groq.py +3 -5
  246. agno/tools/models/nebius.py +7 -7
  247. agno/tools/models_labs.py +25 -15
  248. agno/tools/notion.py +204 -0
  249. agno/tools/openai.py +4 -9
  250. agno/tools/opencv.py +3 -3
  251. agno/tools/parallel.py +314 -0
  252. agno/tools/replicate.py +7 -7
  253. agno/tools/scrapegraph.py +58 -31
  254. agno/tools/searxng.py +2 -2
  255. agno/tools/serper.py +2 -2
  256. agno/tools/slack.py +18 -3
  257. agno/tools/spider.py +2 -2
  258. agno/tools/tavily.py +146 -0
  259. agno/tools/whatsapp.py +1 -1
  260. agno/tools/workflow.py +278 -0
  261. agno/tools/yfinance.py +12 -11
  262. agno/utils/agent.py +820 -0
  263. agno/utils/audio.py +27 -0
  264. agno/utils/common.py +90 -1
  265. agno/utils/events.py +222 -7
  266. agno/utils/gemini.py +181 -23
  267. agno/utils/hooks.py +57 -0
  268. agno/utils/http.py +111 -0
  269. agno/utils/knowledge.py +12 -5
  270. agno/utils/log.py +1 -0
  271. agno/utils/mcp.py +95 -5
  272. agno/utils/media.py +188 -10
  273. agno/utils/merge_dict.py +22 -1
  274. agno/utils/message.py +60 -0
  275. agno/utils/models/claude.py +40 -11
  276. agno/utils/models/cohere.py +1 -1
  277. agno/utils/models/watsonx.py +1 -1
  278. agno/utils/openai.py +1 -1
  279. agno/utils/print_response/agent.py +105 -21
  280. agno/utils/print_response/team.py +103 -38
  281. agno/utils/print_response/workflow.py +251 -34
  282. agno/utils/reasoning.py +22 -1
  283. agno/utils/serialize.py +32 -0
  284. agno/utils/streamlit.py +16 -10
  285. agno/utils/string.py +41 -0
  286. agno/utils/team.py +98 -9
  287. agno/utils/tools.py +1 -1
  288. agno/vectordb/base.py +23 -4
  289. agno/vectordb/cassandra/cassandra.py +65 -9
  290. agno/vectordb/chroma/chromadb.py +182 -38
  291. agno/vectordb/clickhouse/clickhousedb.py +64 -11
  292. agno/vectordb/couchbase/couchbase.py +105 -10
  293. agno/vectordb/lancedb/lance_db.py +183 -135
  294. agno/vectordb/langchaindb/langchaindb.py +25 -7
  295. agno/vectordb/lightrag/lightrag.py +17 -3
  296. agno/vectordb/llamaindex/__init__.py +3 -0
  297. agno/vectordb/llamaindex/llamaindexdb.py +46 -7
  298. agno/vectordb/milvus/milvus.py +126 -9
  299. agno/vectordb/mongodb/__init__.py +7 -1
  300. agno/vectordb/mongodb/mongodb.py +112 -7
  301. agno/vectordb/pgvector/pgvector.py +142 -21
  302. agno/vectordb/pineconedb/pineconedb.py +80 -8
  303. agno/vectordb/qdrant/qdrant.py +125 -39
  304. agno/vectordb/redis/__init__.py +9 -0
  305. agno/vectordb/redis/redisdb.py +694 -0
  306. agno/vectordb/singlestore/singlestore.py +111 -25
  307. agno/vectordb/surrealdb/surrealdb.py +31 -5
  308. agno/vectordb/upstashdb/upstashdb.py +76 -8
  309. agno/vectordb/weaviate/weaviate.py +86 -15
  310. agno/workflow/__init__.py +2 -0
  311. agno/workflow/agent.py +299 -0
  312. agno/workflow/condition.py +112 -18
  313. agno/workflow/loop.py +69 -10
  314. agno/workflow/parallel.py +266 -118
  315. agno/workflow/router.py +110 -17
  316. agno/workflow/step.py +645 -136
  317. agno/workflow/steps.py +65 -6
  318. agno/workflow/types.py +71 -33
  319. agno/workflow/workflow.py +2113 -300
  320. agno-2.3.0.dist-info/METADATA +618 -0
  321. agno-2.3.0.dist-info/RECORD +577 -0
  322. agno-2.3.0.dist-info/licenses/LICENSE +201 -0
  323. agno/knowledge/reader/url_reader.py +0 -128
  324. agno/tools/googlesearch.py +0 -98
  325. agno/tools/mcp.py +0 -610
  326. agno/utils/models/aws_claude.py +0 -170
  327. agno-2.0.0rc2.dist-info/METADATA +0 -355
  328. agno-2.0.0rc2.dist-info/RECORD +0 -515
  329. agno-2.0.0rc2.dist-info/licenses/LICENSE +0 -375
  330. {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
  331. {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
agno/os/utils.py CHANGED
@@ -1,13 +1,17 @@
1
- from typing import Any, Callable, Dict, List, Optional, Union
2
- from uuid import uuid4
1
+ from typing import Any, Callable, Dict, List, Optional, Set, Union
3
2
 
4
- from fastapi import HTTPException, UploadFile
3
+ from fastapi import FastAPI, HTTPException, UploadFile
4
+ from fastapi.routing import APIRoute, APIRouter
5
+ from pydantic import BaseModel
6
+ from starlette.middleware.cors import CORSMiddleware
5
7
 
6
8
  from agno.agent.agent import Agent
7
- from agno.db.base import BaseDb
9
+ from agno.db.base import AsyncBaseDb, BaseDb
8
10
  from agno.knowledge.knowledge import Knowledge
9
11
  from agno.media import Audio, Image, Video
10
12
  from agno.media import File as FileMedia
13
+ from agno.models.message import Message
14
+ from agno.os.config import AgentOSConfig
11
15
  from agno.team.team import Team
12
16
  from agno.tools import Toolkit
13
17
  from agno.tools.function import Function
@@ -15,23 +19,69 @@ from agno.utils.log import logger
15
19
  from agno.workflow.workflow import Workflow
16
20
 
17
21
 
18
- def get_db(dbs: dict[str, BaseDb], db_id: Optional[str] = None) -> BaseDb:
19
- """Return the database with the given ID, or the first database if no ID is provided."""
22
+ 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]:
25
+ """Return the database with the given ID and/or table, or the first database if no ID/table is provided."""
26
+
27
+ if table and not db_id:
28
+ raise HTTPException(status_code=400, detail="The db_id query parameter is required when passing a table")
29
+
30
+ async def _has_table(db: Union[BaseDb, AsyncBaseDb], table_name: str) -> bool:
31
+ """Check if this database has the specified table (configured and actually exists)."""
32
+ # First check if table name is configured
33
+ is_configured = (
34
+ hasattr(db, "session_table_name")
35
+ and db.session_table_name == table_name
36
+ or hasattr(db, "memory_table_name")
37
+ and db.memory_table_name == table_name
38
+ or hasattr(db, "metrics_table_name")
39
+ and db.metrics_table_name == table_name
40
+ or hasattr(db, "eval_table_name")
41
+ and db.eval_table_name == table_name
42
+ or hasattr(db, "knowledge_table_name")
43
+ and db.knowledge_table_name == table_name
44
+ )
45
+
46
+ if not is_configured:
47
+ return False
48
+
49
+ # Then check if table actually exists in the database
50
+ try:
51
+ if isinstance(db, AsyncBaseDb):
52
+ # For async databases, await the check
53
+ return await db.table_exists(table_name)
54
+ else:
55
+ # For sync databases, call directly
56
+ return db.table_exists(table_name)
57
+ except (NotImplementedError, AttributeError):
58
+ # If table_exists not implemented, fall back to configuration check
59
+ return is_configured
60
+
61
+ # If db_id is provided, first find the database with that ID
62
+ if db_id:
63
+ target_db_list = dbs.get(db_id)
64
+ if not target_db_list:
65
+ raise HTTPException(status_code=404, detail=f"No database found with id '{db_id}'")
66
+
67
+ # If table is also specified, search through all databases with this ID to find one with the table
68
+ if table:
69
+ for db in target_db_list:
70
+ if await _has_table(db, table):
71
+ return db
72
+ raise HTTPException(status_code=404, detail=f"No database with id '{db_id}' has table '{table}'")
73
+
74
+ # If no table specified, return the first database with this ID
75
+ return target_db_list[0]
20
76
 
21
77
  # Raise if multiple databases are provided but no db_id is provided
22
- if not db_id and len(dbs) > 1:
78
+ if len(dbs) > 1:
23
79
  raise HTTPException(
24
80
  status_code=400, detail="The db_id query parameter is required when using multiple databases"
25
81
  )
26
82
 
27
- # Get and return the database with the given ID, or raise if not found
28
- if db_id:
29
- db = dbs.get(db_id)
30
- if not db:
31
- raise HTTPException(status_code=404, detail=f"Database with id '{db_id}' not found")
32
- else:
33
- db = next(iter(dbs.values()))
34
- return db
83
+ # Return the first (and only) database
84
+ return next(db for dbs in dbs.values() for db in dbs)
35
85
 
36
86
 
37
87
  def get_knowledge_instance_by_db_id(knowledge_instances: List[Knowledge], db_id: Optional[str] = None) -> Knowledge:
@@ -52,17 +102,33 @@ def get_knowledge_instance_by_db_id(knowledge_instances: List[Knowledge], db_id:
52
102
 
53
103
 
54
104
  def get_run_input(run_dict: Dict[str, Any], is_workflow_run: bool = False) -> str:
55
- """Get the run input from the given run dictionary"""
105
+ """Get the run input from the given run dictionary
106
+
107
+ Uses the RunInput/TeamRunInput object which stores the original user input.
108
+ """
109
+
110
+ # For agent or team runs, use the stored input_content
111
+ if not is_workflow_run and run_dict.get("input") is not None:
112
+ input_data = run_dict.get("input")
113
+ if isinstance(input_data, dict) and input_data.get("input_content") is not None:
114
+ return stringify_input_content(input_data["input_content"])
56
115
 
57
116
  if is_workflow_run:
117
+ # Check the input field directly
118
+ if run_dict.get("input") is not None:
119
+ input_value = run_dict.get("input")
120
+ return str(input_value)
121
+
122
+ # Check the step executor runs for fallback
58
123
  step_executor_runs = run_dict.get("step_executor_runs", [])
59
124
  if step_executor_runs:
60
- for message in step_executor_runs[0].get("messages", []):
125
+ for message in reversed(step_executor_runs[0].get("messages", [])):
61
126
  if message.get("role") == "user":
62
127
  return message.get("content", "")
63
128
 
129
+ # Final fallback: scan messages
64
130
  if run_dict.get("messages") is not None:
65
- for message in run_dict["messages"]:
131
+ for message in reversed(run_dict["messages"]):
66
132
  if message.get("role") == "user":
67
133
  return message.get("content", "")
68
134
 
@@ -79,22 +145,46 @@ def get_session_name(session: Dict[str, Any]) -> str:
79
145
 
80
146
  # Otherwise use the original user message
81
147
  else:
82
- runs = session.get("runs", [])
148
+ runs = session.get("runs", []) or []
83
149
 
84
150
  # For teams, identify the first Team run and avoid using the first member's run
85
151
  if session.get("session_type") == "team":
86
- run = runs[0] if not runs[0].get("agent_id") else runs[1]
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]
87
162
 
88
- # For workflows, pass along the first step_executor_run
89
163
  elif session.get("session_type") == "workflow":
90
164
  try:
91
- run = session["runs"][0]["step_executor_runs"][0]
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 ""
92
179
  except (KeyError, IndexError, TypeError):
93
180
  return ""
94
181
 
95
182
  # For agents, use the first run
96
183
  else:
97
- run = runs[0]
184
+ run = runs[0] if runs else None
185
+
186
+ if run is None:
187
+ return ""
98
188
 
99
189
  if not isinstance(run, dict):
100
190
  run = run.to_dict()
@@ -106,31 +196,42 @@ def get_session_name(session: Dict[str, Any]) -> str:
106
196
  return ""
107
197
 
108
198
 
199
+ def extract_input_media(run_dict: Dict[str, Any]) -> Dict[str, Any]:
200
+ input_media: Dict[str, List[Any]] = {
201
+ "images": [],
202
+ "videos": [],
203
+ "audios": [],
204
+ "files": [],
205
+ }
206
+
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", []))
212
+
213
+ return input_media
214
+
215
+
109
216
  def process_image(file: UploadFile) -> Image:
110
217
  content = file.file.read()
111
218
  if not content:
112
219
  raise HTTPException(status_code=400, detail="Empty file")
113
- return Image(content=content)
220
+ return Image(content=content, format=extract_format(file), mime_type=file.content_type)
114
221
 
115
222
 
116
223
  def process_audio(file: UploadFile) -> Audio:
117
224
  content = file.file.read()
118
225
  if not content:
119
226
  raise HTTPException(status_code=400, detail="Empty file")
120
- format = None
121
- if file.filename and "." in file.filename:
122
- format = file.filename.split(".")[-1].lower()
123
- elif file.content_type:
124
- format = file.content_type.split("/")[-1]
125
-
126
- return Audio(content=content, format=format)
227
+ return Audio(content=content, format=extract_format(file), mime_type=file.content_type)
127
228
 
128
229
 
129
230
  def process_video(file: UploadFile) -> Video:
130
231
  content = file.file.read()
131
232
  if not content:
132
233
  raise HTTPException(status_code=400, detail="Empty file")
133
- return Video(content=content, format=file.content_type)
234
+ return Video(content=content, format=extract_format(file), mime_type=file.content_type)
134
235
 
135
236
 
136
237
  def process_document(file: UploadFile) -> Optional[FileMedia]:
@@ -138,15 +239,29 @@ def process_document(file: UploadFile) -> Optional[FileMedia]:
138
239
  content = file.file.read()
139
240
  if not content:
140
241
  raise HTTPException(status_code=400, detail="Empty file")
141
-
142
- return FileMedia(content=content)
242
+ return FileMedia(
243
+ content=content, filename=file.filename, format=extract_format(file), mime_type=file.content_type
244
+ )
143
245
  except Exception as e:
144
246
  logger.error(f"Error processing document {file.filename}: {e}")
145
247
  return None
146
248
 
147
249
 
250
+ def extract_format(file: UploadFile) -> Optional[str]:
251
+ """Extract the File format from file name or content_type."""
252
+ # Get the format from the filename
253
+ if file.filename and "." in file.filename:
254
+ return file.filename.split(".")[-1].lower()
255
+
256
+ # Fallback to the file content_type
257
+ if file.content_type:
258
+ return file.content_type.strip().split("/")[-1]
259
+
260
+ return None
261
+
262
+
148
263
  def format_tools(agent_tools: List[Union[Dict[str, Any], Toolkit, Function, Callable]]):
149
- formatted_tools = []
264
+ formatted_tools: List[Dict] = []
150
265
  if agent_tools is not None:
151
266
  for tool in agent_tools:
152
267
  if isinstance(tool, dict):
@@ -164,8 +279,15 @@ def format_tools(agent_tools: List[Union[Dict[str, Any], Toolkit, Function, Call
164
279
  return formatted_tools
165
280
 
166
281
 
167
- def format_team_tools(team_tools: List[Function]):
168
- return [tool.to_dict() for tool in team_tools]
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
169
291
 
170
292
 
171
293
  def get_agent_by_id(agent_id: str, agents: Optional[List[Agent]] = None) -> Optional[Agent]:
@@ -198,6 +320,33 @@ def get_workflow_by_id(workflow_id: str, workflows: Optional[List[Workflow]] = N
198
320
  return None
199
321
 
200
322
 
323
+ # INPUT SCHEMA VALIDATIONS
324
+
325
+
326
+ def get_agent_input_schema_dict(agent: Agent) -> Optional[Dict[str, Any]]:
327
+ """Get input schema as dictionary for API responses"""
328
+
329
+ if agent.input_schema is not None:
330
+ try:
331
+ return agent.input_schema.model_json_schema()
332
+ except Exception:
333
+ return None
334
+
335
+ return None
336
+
337
+
338
+ def get_team_input_schema_dict(team: Team) -> Optional[Dict[str, Any]]:
339
+ """Get input schema as dictionary for API responses"""
340
+
341
+ if team.input_schema is not None:
342
+ try:
343
+ return team.input_schema.model_json_schema()
344
+ except Exception:
345
+ return None
346
+
347
+ return None
348
+
349
+
201
350
  def get_workflow_input_schema_dict(workflow: Workflow) -> Optional[Dict[str, Any]]:
202
351
  """Get input schema as dictionary for API responses"""
203
352
 
@@ -263,8 +412,219 @@ def _generate_schema_from_params(params: Dict[str, Any]) -> Dict[str, Any]:
263
412
  return schema
264
413
 
265
414
 
266
- def generate_id(name: Optional[str] = None) -> str:
267
- if name:
268
- return name.lower().replace(" ", "-").replace("_", "-")
415
+ def update_cors_middleware(app: FastAPI, new_origins: list):
416
+ existing_origins: List[str] = []
417
+
418
+ # TODO: Allow more options where CORS is properly merged and user can disable this behaviour
419
+
420
+ # Extract existing origins from current CORS middleware
421
+ for middleware in app.user_middleware:
422
+ if middleware.cls == CORSMiddleware:
423
+ if hasattr(middleware, "kwargs"):
424
+ origins_value = middleware.kwargs.get("allow_origins", [])
425
+ if isinstance(origins_value, list):
426
+ existing_origins = origins_value
427
+ else:
428
+ existing_origins = []
429
+ break
430
+ # Merge origins
431
+ merged_origins = list(set(new_origins + existing_origins))
432
+ final_origins = [origin for origin in merged_origins if origin != "*"]
433
+
434
+ # Remove existing CORS
435
+ app.user_middleware = [m for m in app.user_middleware if m.cls != CORSMiddleware]
436
+ app.middleware_stack = None
437
+
438
+ # Add updated CORS
439
+ app.add_middleware(
440
+ CORSMiddleware, # type: ignore
441
+ allow_origins=final_origins,
442
+ allow_credentials=True,
443
+ allow_methods=["*"],
444
+ allow_headers=["*"],
445
+ expose_headers=["*"],
446
+ )
447
+
448
+
449
+ def get_existing_route_paths(fastapi_app: FastAPI) -> Dict[str, List[str]]:
450
+ """Get all existing route paths and methods from the FastAPI app.
451
+
452
+ Returns:
453
+ Dict[str, List[str]]: Dictionary mapping paths to list of HTTP methods
454
+ """
455
+ existing_paths: Dict[str, Any] = {}
456
+ for route in fastapi_app.routes:
457
+ if isinstance(route, APIRoute):
458
+ path = route.path
459
+ methods = list(route.methods) if route.methods else []
460
+ if path in existing_paths:
461
+ existing_paths[path].extend(methods)
462
+ else:
463
+ existing_paths[path] = methods
464
+ return existing_paths
465
+
466
+
467
+ def find_conflicting_routes(fastapi_app: FastAPI, router: APIRouter) -> List[Dict[str, Any]]:
468
+ """Find conflicting routes in the FastAPI app.
469
+
470
+ Args:
471
+ fastapi_app: The FastAPI app with all existing routes
472
+ router: The APIRouter to add
473
+
474
+ Returns:
475
+ List[Dict[str, Any]]: List of conflicting routes
476
+ """
477
+ existing_paths = get_existing_route_paths(fastapi_app)
478
+
479
+ conflicts = []
480
+
481
+ for route in router.routes:
482
+ if isinstance(route, APIRoute):
483
+ full_path = route.path
484
+ route_methods = list(route.methods) if route.methods else []
485
+
486
+ if full_path in existing_paths:
487
+ conflicting_methods: Set[str] = set(route_methods) & set(existing_paths[full_path])
488
+ if conflicting_methods:
489
+ conflicts.append({"path": full_path, "methods": list(conflicting_methods), "route": route})
490
+ return conflicts
491
+
492
+
493
+ def load_yaml_config(config_file_path: str) -> AgentOSConfig:
494
+ """Load a YAML config file and return the configuration as an AgentOSConfig instance."""
495
+ from pathlib import Path
496
+
497
+ import yaml
498
+
499
+ # Validate that the path points to a YAML file
500
+ path = Path(config_file_path)
501
+ if path.suffix.lower() not in [".yaml", ".yml"]:
502
+ raise ValueError(f"Config file must have a .yaml or .yml extension, got: {config_file_path}")
503
+
504
+ # Load the YAML file
505
+ with open(config_file_path, "r") as f:
506
+ return AgentOSConfig.model_validate(yaml.safe_load(f))
507
+
508
+
509
+ def collect_mcp_tools_from_team(team: Team, mcp_tools: List[Any]) -> None:
510
+ """Recursively collect MCP tools from a team and its members."""
511
+ # Check the team tools
512
+ if team.tools:
513
+ for tool in team.tools:
514
+ type_name = type(tool).__name__
515
+ if type_name in ("MCPTools", "MultiMCPTools"):
516
+ if tool not in mcp_tools:
517
+ mcp_tools.append(tool)
518
+
519
+ # Recursively check team members
520
+ if team.members:
521
+ for member in team.members:
522
+ if isinstance(member, Agent):
523
+ if member.tools:
524
+ for tool in member.tools:
525
+ type_name = type(tool).__name__
526
+ if type_name in ("MCPTools", "MultiMCPTools"):
527
+ if tool not in mcp_tools:
528
+ mcp_tools.append(tool)
529
+
530
+ elif isinstance(member, Team):
531
+ # Recursively check nested team
532
+ collect_mcp_tools_from_team(member, mcp_tools)
533
+
534
+
535
+ def collect_mcp_tools_from_workflow(workflow: Workflow, mcp_tools: List[Any]) -> None:
536
+ """Recursively collect MCP tools from a workflow and its steps."""
537
+ from agno.workflow.steps import Steps
538
+
539
+ # Recursively check workflow steps
540
+ if workflow.steps:
541
+ if isinstance(workflow.steps, list):
542
+ # Handle list of steps
543
+ for step in workflow.steps:
544
+ collect_mcp_tools_from_workflow_step(step, mcp_tools)
545
+
546
+ elif isinstance(workflow.steps, Steps):
547
+ # Handle Steps container
548
+ if steps := workflow.steps.steps:
549
+ for step in steps:
550
+ collect_mcp_tools_from_workflow_step(step, mcp_tools)
551
+
552
+ elif callable(workflow.steps):
553
+ pass
554
+
555
+
556
+ def collect_mcp_tools_from_workflow_step(step: Any, mcp_tools: List[Any]) -> None:
557
+ """Collect MCP tools from a single workflow step."""
558
+ from agno.workflow.condition import Condition
559
+ from agno.workflow.loop import Loop
560
+ from agno.workflow.parallel import Parallel
561
+ from agno.workflow.router import Router
562
+ from agno.workflow.step import Step
563
+ from agno.workflow.steps import Steps
564
+
565
+ if isinstance(step, Step):
566
+ # Check step's agent
567
+ if step.agent:
568
+ if step.agent.tools:
569
+ for tool in step.agent.tools:
570
+ type_name = type(tool).__name__
571
+ if type_name in ("MCPTools", "MultiMCPTools"):
572
+ if tool not in mcp_tools:
573
+ mcp_tools.append(tool)
574
+ # Check step's team
575
+ if step.team:
576
+ collect_mcp_tools_from_team(step.team, mcp_tools)
577
+
578
+ elif isinstance(step, Steps):
579
+ if steps := step.steps:
580
+ for step in steps:
581
+ collect_mcp_tools_from_workflow_step(step, mcp_tools)
582
+
583
+ elif isinstance(step, (Parallel, Loop, Condition, Router)):
584
+ # These contain other steps - recursively check them
585
+ if hasattr(step, "steps") and step.steps:
586
+ for sub_step in step.steps:
587
+ collect_mcp_tools_from_workflow_step(sub_step, mcp_tools)
588
+
589
+ elif isinstance(step, Agent):
590
+ # Direct agent in workflow steps
591
+ if step.tools:
592
+ for tool in step.tools:
593
+ type_name = type(tool).__name__
594
+ if type_name in ("MCPTools", "MultiMCPTools"):
595
+ if tool not in mcp_tools:
596
+ mcp_tools.append(tool)
597
+
598
+ elif isinstance(step, Team):
599
+ # Direct team in workflow steps
600
+ collect_mcp_tools_from_team(step, mcp_tools)
601
+
602
+ elif isinstance(step, Workflow):
603
+ # Nested workflow
604
+ collect_mcp_tools_from_workflow(step, mcp_tools)
605
+
606
+
607
+ def stringify_input_content(input_content: Union[str, Dict[str, Any], List[Any], BaseModel]) -> str:
608
+ """Convert any given input_content into its string representation.
609
+
610
+ This handles both serialized (dict) and live (object) input_content formats.
611
+ """
612
+ import json
613
+
614
+ if isinstance(input_content, str):
615
+ return input_content
616
+ elif isinstance(input_content, Message):
617
+ return json.dumps(input_content.to_dict())
618
+ elif isinstance(input_content, dict):
619
+ return json.dumps(input_content, indent=2, default=str)
620
+ elif isinstance(input_content, list):
621
+ if input_content:
622
+ # Handle live Message objects
623
+ if isinstance(input_content[0], Message):
624
+ return json.dumps([m.to_dict() for m in input_content])
625
+ # Handle serialized Message dicts
626
+ elif isinstance(input_content[0], dict) and input_content[0].get("role") == "user":
627
+ return input_content[0].get("content", str(input_content))
628
+ return str(input_content)
269
629
  else:
270
- return str(uuid4())
630
+ return str(input_content)
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional
4
+
5
+ from agno.models.base import Model
6
+ from agno.models.message import Message
7
+ from agno.utils.log import logger
8
+
9
+
10
+ def is_anthropic_reasoning_model(reasoning_model: Model) -> bool:
11
+ """Check if the model is an Anthropic Claude model with thinking support."""
12
+ is_claude = reasoning_model.__class__.__name__ == "Claude"
13
+ if not is_claude:
14
+ return False
15
+
16
+ # Check if provider is Anthropic (not VertexAI)
17
+ is_anthropic_provider = hasattr(reasoning_model, "provider") and reasoning_model.provider == "Anthropic"
18
+
19
+ # Check if thinking parameter is set
20
+ has_thinking = hasattr(reasoning_model, "thinking") and reasoning_model.thinking is not None
21
+
22
+ return is_claude and is_anthropic_provider and has_thinking
23
+
24
+
25
+ def get_anthropic_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
26
+ """Get reasoning from an Anthropic Claude model."""
27
+ from agno.run.agent import RunOutput
28
+
29
+ try:
30
+ reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
31
+ except Exception as e:
32
+ logger.warning(f"Reasoning error: {e}")
33
+ return None
34
+
35
+ reasoning_content: str = ""
36
+ redacted_reasoning_content: Optional[str] = None
37
+
38
+ if reasoning_agent_response.messages is not None:
39
+ for msg in reasoning_agent_response.messages:
40
+ if msg.reasoning_content is not None:
41
+ reasoning_content = msg.reasoning_content
42
+ if hasattr(msg, "redacted_reasoning_content") and msg.redacted_reasoning_content is not None:
43
+ redacted_reasoning_content = msg.redacted_reasoning_content
44
+ break
45
+
46
+ return Message(
47
+ role="assistant",
48
+ content=f"<thinking>\n{reasoning_content}\n</thinking>",
49
+ reasoning_content=reasoning_content,
50
+ redacted_reasoning_content=redacted_reasoning_content,
51
+ )
52
+
53
+
54
+ async def aget_anthropic_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
55
+ """Get reasoning from an Anthropic Claude model asynchronously."""
56
+ from agno.run.agent import RunOutput
57
+
58
+ try:
59
+ reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
60
+ except Exception as e:
61
+ logger.warning(f"Reasoning error: {e}")
62
+ return None
63
+
64
+ reasoning_content: str = ""
65
+ redacted_reasoning_content: Optional[str] = None
66
+
67
+ if reasoning_agent_response.messages is not None:
68
+ for msg in reasoning_agent_response.messages:
69
+ if msg.reasoning_content is not None:
70
+ reasoning_content = msg.reasoning_content
71
+ if hasattr(msg, "redacted_reasoning_content") and msg.redacted_reasoning_content is not None:
72
+ redacted_reasoning_content = msg.redacted_reasoning_content
73
+ break
74
+
75
+ return Message(
76
+ role="assistant",
77
+ content=f"<thinking>\n{reasoning_content}\n</thinking>",
78
+ reasoning_content=reasoning_content,
79
+ redacted_reasoning_content=redacted_reasoning_content,
80
+ )
@@ -20,7 +20,7 @@ def get_ai_foundry_reasoning(reasoning_agent: "Agent", messages: List[Message])
20
20
  from agno.run.agent import RunOutput
21
21
 
22
22
  try:
23
- reasoning_agent_response: RunOutput = reasoning_agent.run(messages=messages)
23
+ reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
24
24
  except Exception as e:
25
25
  logger.warning(f"Reasoning error: {e}")
26
26
  return None
@@ -46,7 +46,7 @@ async def aget_ai_foundry_reasoning(reasoning_agent: "Agent", messages: List[Mes
46
46
  from agno.run.agent import RunOutput
47
47
 
48
48
  try:
49
- reasoning_agent_response: RunOutput = await reasoning_agent.arun(messages=messages)
49
+ reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
50
50
  except Exception as e:
51
51
  logger.warning(f"Reasoning error: {e}")
52
52
  return None
@@ -20,7 +20,7 @@ def get_deepseek_reasoning(reasoning_agent: "Agent", messages: List[Message]) ->
20
20
  message.role = "system"
21
21
 
22
22
  try:
23
- reasoning_agent_response: RunOutput = reasoning_agent.run(messages=messages)
23
+ reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
24
24
  except Exception as e:
25
25
  logger.warning(f"Reasoning error: {e}")
26
26
  return None
@@ -46,7 +46,7 @@ async def aget_deepseek_reasoning(reasoning_agent: "Agent", messages: List[Messa
46
46
  message.role = "system"
47
47
 
48
48
  try:
49
- reasoning_agent_response: RunOutput = await reasoning_agent.arun(messages=messages)
49
+ reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
50
50
  except Exception as e:
51
51
  logger.warning(f"Reasoning error: {e}")
52
52
  return None