agno 2.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 (314) hide show
  1. agno/agent/agent.py +5540 -2273
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/compression/__init__.py +3 -0
  5. agno/compression/manager.py +247 -0
  6. agno/culture/__init__.py +3 -0
  7. agno/culture/manager.py +956 -0
  8. agno/db/async_postgres/__init__.py +3 -0
  9. agno/db/base.py +689 -6
  10. agno/db/dynamo/dynamo.py +933 -37
  11. agno/db/dynamo/schemas.py +174 -10
  12. agno/db/dynamo/utils.py +63 -4
  13. agno/db/firestore/firestore.py +831 -9
  14. agno/db/firestore/schemas.py +51 -0
  15. agno/db/firestore/utils.py +102 -4
  16. agno/db/gcs_json/gcs_json_db.py +660 -12
  17. agno/db/gcs_json/utils.py +60 -26
  18. agno/db/in_memory/in_memory_db.py +287 -14
  19. agno/db/in_memory/utils.py +60 -2
  20. agno/db/json/json_db.py +590 -14
  21. agno/db/json/utils.py +60 -26
  22. agno/db/migrations/manager.py +199 -0
  23. agno/db/migrations/v1_to_v2.py +43 -13
  24. agno/db/migrations/versions/__init__.py +0 -0
  25. agno/db/migrations/versions/v2_3_0.py +938 -0
  26. agno/db/mongo/__init__.py +15 -1
  27. agno/db/mongo/async_mongo.py +2760 -0
  28. agno/db/mongo/mongo.py +879 -11
  29. agno/db/mongo/schemas.py +42 -0
  30. agno/db/mongo/utils.py +80 -8
  31. agno/db/mysql/__init__.py +2 -1
  32. agno/db/mysql/async_mysql.py +2912 -0
  33. agno/db/mysql/mysql.py +946 -68
  34. agno/db/mysql/schemas.py +72 -10
  35. agno/db/mysql/utils.py +198 -7
  36. agno/db/postgres/__init__.py +2 -1
  37. agno/db/postgres/async_postgres.py +2579 -0
  38. agno/db/postgres/postgres.py +942 -57
  39. agno/db/postgres/schemas.py +81 -18
  40. agno/db/postgres/utils.py +164 -2
  41. agno/db/redis/redis.py +671 -7
  42. agno/db/redis/schemas.py +50 -0
  43. agno/db/redis/utils.py +65 -7
  44. agno/db/schemas/__init__.py +2 -1
  45. agno/db/schemas/culture.py +120 -0
  46. agno/db/schemas/evals.py +1 -0
  47. agno/db/schemas/memory.py +17 -2
  48. agno/db/singlestore/schemas.py +63 -0
  49. agno/db/singlestore/singlestore.py +949 -83
  50. agno/db/singlestore/utils.py +60 -2
  51. agno/db/sqlite/__init__.py +2 -1
  52. agno/db/sqlite/async_sqlite.py +2911 -0
  53. agno/db/sqlite/schemas.py +62 -0
  54. agno/db/sqlite/sqlite.py +965 -46
  55. agno/db/sqlite/utils.py +169 -8
  56. agno/db/surrealdb/__init__.py +3 -0
  57. agno/db/surrealdb/metrics.py +292 -0
  58. agno/db/surrealdb/models.py +334 -0
  59. agno/db/surrealdb/queries.py +71 -0
  60. agno/db/surrealdb/surrealdb.py +1908 -0
  61. agno/db/surrealdb/utils.py +147 -0
  62. agno/db/utils.py +2 -0
  63. agno/eval/__init__.py +10 -0
  64. agno/eval/accuracy.py +75 -55
  65. agno/eval/agent_as_judge.py +861 -0
  66. agno/eval/base.py +29 -0
  67. agno/eval/performance.py +16 -7
  68. agno/eval/reliability.py +28 -16
  69. agno/eval/utils.py +35 -17
  70. agno/exceptions.py +27 -2
  71. agno/filters.py +354 -0
  72. agno/guardrails/prompt_injection.py +1 -0
  73. agno/hooks/__init__.py +3 -0
  74. agno/hooks/decorator.py +164 -0
  75. agno/integrations/discord/client.py +1 -1
  76. agno/knowledge/chunking/agentic.py +13 -10
  77. agno/knowledge/chunking/fixed.py +4 -1
  78. agno/knowledge/chunking/semantic.py +9 -4
  79. agno/knowledge/chunking/strategy.py +59 -15
  80. agno/knowledge/embedder/fastembed.py +1 -1
  81. agno/knowledge/embedder/nebius.py +1 -1
  82. agno/knowledge/embedder/ollama.py +8 -0
  83. agno/knowledge/embedder/openai.py +8 -8
  84. agno/knowledge/embedder/sentence_transformer.py +6 -2
  85. agno/knowledge/embedder/vllm.py +262 -0
  86. agno/knowledge/knowledge.py +1618 -318
  87. agno/knowledge/reader/base.py +6 -2
  88. agno/knowledge/reader/csv_reader.py +8 -10
  89. agno/knowledge/reader/docx_reader.py +5 -6
  90. agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
  91. agno/knowledge/reader/json_reader.py +5 -4
  92. agno/knowledge/reader/markdown_reader.py +8 -8
  93. agno/knowledge/reader/pdf_reader.py +17 -19
  94. agno/knowledge/reader/pptx_reader.py +101 -0
  95. agno/knowledge/reader/reader_factory.py +32 -3
  96. agno/knowledge/reader/s3_reader.py +3 -3
  97. agno/knowledge/reader/tavily_reader.py +193 -0
  98. agno/knowledge/reader/text_reader.py +22 -10
  99. agno/knowledge/reader/web_search_reader.py +1 -48
  100. agno/knowledge/reader/website_reader.py +10 -10
  101. agno/knowledge/reader/wikipedia_reader.py +33 -1
  102. agno/knowledge/types.py +1 -0
  103. agno/knowledge/utils.py +72 -7
  104. agno/media.py +22 -6
  105. agno/memory/__init__.py +14 -1
  106. agno/memory/manager.py +544 -83
  107. agno/memory/strategies/__init__.py +15 -0
  108. agno/memory/strategies/base.py +66 -0
  109. agno/memory/strategies/summarize.py +196 -0
  110. agno/memory/strategies/types.py +37 -0
  111. agno/models/aimlapi/aimlapi.py +17 -0
  112. agno/models/anthropic/claude.py +515 -40
  113. agno/models/aws/bedrock.py +102 -21
  114. agno/models/aws/claude.py +131 -274
  115. agno/models/azure/ai_foundry.py +41 -19
  116. agno/models/azure/openai_chat.py +39 -8
  117. agno/models/base.py +1249 -525
  118. agno/models/cerebras/cerebras.py +91 -21
  119. agno/models/cerebras/cerebras_openai.py +21 -2
  120. agno/models/cohere/chat.py +40 -6
  121. agno/models/cometapi/cometapi.py +18 -1
  122. agno/models/dashscope/dashscope.py +2 -3
  123. agno/models/deepinfra/deepinfra.py +18 -1
  124. agno/models/deepseek/deepseek.py +69 -3
  125. agno/models/fireworks/fireworks.py +18 -1
  126. agno/models/google/gemini.py +877 -80
  127. agno/models/google/utils.py +22 -0
  128. agno/models/groq/groq.py +51 -18
  129. agno/models/huggingface/huggingface.py +17 -6
  130. agno/models/ibm/watsonx.py +16 -6
  131. agno/models/internlm/internlm.py +18 -1
  132. agno/models/langdb/langdb.py +13 -1
  133. agno/models/litellm/chat.py +44 -9
  134. agno/models/litellm/litellm_openai.py +18 -1
  135. agno/models/message.py +28 -5
  136. agno/models/meta/llama.py +47 -14
  137. agno/models/meta/llama_openai.py +22 -17
  138. agno/models/mistral/mistral.py +8 -4
  139. agno/models/nebius/nebius.py +6 -7
  140. agno/models/nvidia/nvidia.py +20 -3
  141. agno/models/ollama/chat.py +24 -8
  142. agno/models/openai/chat.py +104 -29
  143. agno/models/openai/responses.py +101 -81
  144. agno/models/openrouter/openrouter.py +60 -3
  145. agno/models/perplexity/perplexity.py +17 -1
  146. agno/models/portkey/portkey.py +7 -6
  147. agno/models/requesty/requesty.py +24 -4
  148. agno/models/response.py +73 -2
  149. agno/models/sambanova/sambanova.py +20 -3
  150. agno/models/siliconflow/siliconflow.py +19 -2
  151. agno/models/together/together.py +20 -3
  152. agno/models/utils.py +254 -8
  153. agno/models/vercel/v0.py +20 -3
  154. agno/models/vertexai/__init__.py +0 -0
  155. agno/models/vertexai/claude.py +190 -0
  156. agno/models/vllm/vllm.py +19 -14
  157. agno/models/xai/xai.py +19 -2
  158. agno/os/app.py +549 -152
  159. agno/os/auth.py +190 -3
  160. agno/os/config.py +23 -0
  161. agno/os/interfaces/a2a/router.py +8 -11
  162. agno/os/interfaces/a2a/utils.py +1 -1
  163. agno/os/interfaces/agui/router.py +18 -3
  164. agno/os/interfaces/agui/utils.py +152 -39
  165. agno/os/interfaces/slack/router.py +55 -37
  166. agno/os/interfaces/slack/slack.py +9 -1
  167. agno/os/interfaces/whatsapp/router.py +0 -1
  168. agno/os/interfaces/whatsapp/security.py +3 -1
  169. agno/os/mcp.py +110 -52
  170. agno/os/middleware/__init__.py +2 -0
  171. agno/os/middleware/jwt.py +676 -112
  172. agno/os/router.py +40 -1478
  173. agno/os/routers/agents/__init__.py +3 -0
  174. agno/os/routers/agents/router.py +599 -0
  175. agno/os/routers/agents/schema.py +261 -0
  176. agno/os/routers/evals/evals.py +96 -39
  177. agno/os/routers/evals/schemas.py +65 -33
  178. agno/os/routers/evals/utils.py +80 -10
  179. agno/os/routers/health.py +10 -4
  180. agno/os/routers/knowledge/knowledge.py +196 -38
  181. agno/os/routers/knowledge/schemas.py +82 -22
  182. agno/os/routers/memory/memory.py +279 -52
  183. agno/os/routers/memory/schemas.py +46 -17
  184. agno/os/routers/metrics/metrics.py +20 -8
  185. agno/os/routers/metrics/schemas.py +16 -16
  186. agno/os/routers/session/session.py +462 -34
  187. agno/os/routers/teams/__init__.py +3 -0
  188. agno/os/routers/teams/router.py +512 -0
  189. agno/os/routers/teams/schema.py +257 -0
  190. agno/os/routers/traces/__init__.py +3 -0
  191. agno/os/routers/traces/schemas.py +414 -0
  192. agno/os/routers/traces/traces.py +499 -0
  193. agno/os/routers/workflows/__init__.py +3 -0
  194. agno/os/routers/workflows/router.py +624 -0
  195. agno/os/routers/workflows/schema.py +75 -0
  196. agno/os/schema.py +256 -693
  197. agno/os/scopes.py +469 -0
  198. agno/os/utils.py +514 -36
  199. agno/reasoning/anthropic.py +80 -0
  200. agno/reasoning/gemini.py +73 -0
  201. agno/reasoning/openai.py +5 -0
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +155 -32
  205. agno/run/base.py +55 -3
  206. agno/run/requirement.py +181 -0
  207. agno/run/team.py +125 -38
  208. agno/run/workflow.py +72 -18
  209. agno/session/agent.py +102 -89
  210. agno/session/summary.py +56 -15
  211. agno/session/team.py +164 -90
  212. agno/session/workflow.py +405 -40
  213. agno/table.py +10 -0
  214. agno/team/team.py +3974 -1903
  215. agno/tools/dalle.py +2 -4
  216. agno/tools/eleven_labs.py +23 -25
  217. agno/tools/exa.py +21 -16
  218. agno/tools/file.py +153 -23
  219. agno/tools/file_generation.py +16 -10
  220. agno/tools/firecrawl.py +15 -7
  221. agno/tools/function.py +193 -38
  222. agno/tools/gmail.py +238 -14
  223. agno/tools/google_drive.py +271 -0
  224. agno/tools/googlecalendar.py +36 -8
  225. agno/tools/googlesheets.py +20 -5
  226. agno/tools/jira.py +20 -0
  227. agno/tools/mcp/__init__.py +10 -0
  228. agno/tools/mcp/mcp.py +331 -0
  229. agno/tools/mcp/multi_mcp.py +347 -0
  230. agno/tools/mcp/params.py +24 -0
  231. agno/tools/mcp_toolbox.py +3 -3
  232. agno/tools/models/nebius.py +5 -5
  233. agno/tools/models_labs.py +20 -10
  234. agno/tools/nano_banana.py +151 -0
  235. agno/tools/notion.py +204 -0
  236. agno/tools/parallel.py +314 -0
  237. agno/tools/postgres.py +76 -36
  238. agno/tools/redshift.py +406 -0
  239. agno/tools/scrapegraph.py +1 -1
  240. agno/tools/shopify.py +1519 -0
  241. agno/tools/slack.py +18 -3
  242. agno/tools/spotify.py +919 -0
  243. agno/tools/tavily.py +146 -0
  244. agno/tools/toolkit.py +25 -0
  245. agno/tools/workflow.py +8 -1
  246. agno/tools/yfinance.py +12 -11
  247. agno/tracing/__init__.py +12 -0
  248. agno/tracing/exporter.py +157 -0
  249. agno/tracing/schemas.py +276 -0
  250. agno/tracing/setup.py +111 -0
  251. agno/utils/agent.py +938 -0
  252. agno/utils/cryptography.py +22 -0
  253. agno/utils/dttm.py +33 -0
  254. agno/utils/events.py +151 -3
  255. agno/utils/gemini.py +15 -5
  256. agno/utils/hooks.py +118 -4
  257. agno/utils/http.py +113 -2
  258. agno/utils/knowledge.py +12 -5
  259. agno/utils/log.py +1 -0
  260. agno/utils/mcp.py +92 -2
  261. agno/utils/media.py +187 -1
  262. agno/utils/merge_dict.py +3 -3
  263. agno/utils/message.py +60 -0
  264. agno/utils/models/ai_foundry.py +9 -2
  265. agno/utils/models/claude.py +49 -14
  266. agno/utils/models/cohere.py +9 -2
  267. agno/utils/models/llama.py +9 -2
  268. agno/utils/models/mistral.py +4 -2
  269. agno/utils/print_response/agent.py +109 -16
  270. agno/utils/print_response/team.py +223 -30
  271. agno/utils/print_response/workflow.py +251 -34
  272. agno/utils/streamlit.py +1 -1
  273. agno/utils/team.py +98 -9
  274. agno/utils/tokens.py +657 -0
  275. agno/vectordb/base.py +39 -7
  276. agno/vectordb/cassandra/cassandra.py +21 -5
  277. agno/vectordb/chroma/chromadb.py +43 -12
  278. agno/vectordb/clickhouse/clickhousedb.py +21 -5
  279. agno/vectordb/couchbase/couchbase.py +29 -5
  280. agno/vectordb/lancedb/lance_db.py +92 -181
  281. agno/vectordb/langchaindb/langchaindb.py +24 -4
  282. agno/vectordb/lightrag/lightrag.py +17 -3
  283. agno/vectordb/llamaindex/llamaindexdb.py +25 -5
  284. agno/vectordb/milvus/milvus.py +50 -37
  285. agno/vectordb/mongodb/__init__.py +7 -1
  286. agno/vectordb/mongodb/mongodb.py +36 -30
  287. agno/vectordb/pgvector/pgvector.py +201 -77
  288. agno/vectordb/pineconedb/pineconedb.py +41 -23
  289. agno/vectordb/qdrant/qdrant.py +67 -54
  290. agno/vectordb/redis/__init__.py +9 -0
  291. agno/vectordb/redis/redisdb.py +682 -0
  292. agno/vectordb/singlestore/singlestore.py +50 -29
  293. agno/vectordb/surrealdb/surrealdb.py +31 -41
  294. agno/vectordb/upstashdb/upstashdb.py +34 -6
  295. agno/vectordb/weaviate/weaviate.py +53 -14
  296. agno/workflow/__init__.py +2 -0
  297. agno/workflow/agent.py +299 -0
  298. agno/workflow/condition.py +120 -18
  299. agno/workflow/loop.py +77 -10
  300. agno/workflow/parallel.py +231 -143
  301. agno/workflow/router.py +118 -17
  302. agno/workflow/step.py +609 -170
  303. agno/workflow/steps.py +73 -6
  304. agno/workflow/types.py +96 -21
  305. agno/workflow/workflow.py +2039 -262
  306. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
  307. agno-2.3.13.dist-info/RECORD +613 -0
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -679
  310. agno/tools/memori.py +0 -339
  311. agno-2.1.2.dist-info/RECORD +0 -543
  312. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
  313. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/os/auth.py CHANGED
@@ -1,6 +1,9 @@
1
- from fastapi import Depends, HTTPException
1
+ from typing import List, Set
2
+
3
+ from fastapi import Depends, HTTPException, Request
2
4
  from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
3
5
 
6
+ from agno.os.scopes import get_accessible_resource_ids
4
7
  from agno.os.settings import AgnoAPISettings
5
8
 
6
9
  # Create a global HTTPBearer instance
@@ -18,7 +21,7 @@ def get_authentication_dependency(settings: AgnoAPISettings):
18
21
  A dependency function that can be used with FastAPI's Depends()
19
22
  """
20
23
 
21
- def auth_dependency(credentials: HTTPAuthorizationCredentials = Depends(security)) -> bool:
24
+ async def auth_dependency(credentials: HTTPAuthorizationCredentials = Depends(security)) -> bool:
22
25
  # If no security key is set, skip authentication entirely
23
26
  if not settings or not settings.os_security_key:
24
27
  return True
@@ -40,7 +43,7 @@ def get_authentication_dependency(settings: AgnoAPISettings):
40
43
 
41
44
  def validate_websocket_token(token: str, settings: AgnoAPISettings) -> bool:
42
45
  """
43
- Validate a bearer token for WebSocket authentication.
46
+ Validate a bearer token for WebSocket authentication (legacy os_security_key method).
44
47
 
45
48
  Args:
46
49
  token: The bearer token to validate
@@ -55,3 +58,187 @@ def validate_websocket_token(token: str, settings: AgnoAPISettings) -> bool:
55
58
 
56
59
  # Verify the token matches the configured security key
57
60
  return token == settings.os_security_key
61
+
62
+
63
+ def get_accessible_resources(request: Request, resource_type: str) -> Set[str]:
64
+ """
65
+ Get the set of resource IDs the user has access to based on their scopes.
66
+
67
+ This function is used to filter lists of resources (agents, teams, workflows)
68
+ based on the user's scopes from their JWT token.
69
+
70
+ Args:
71
+ request: The FastAPI request object (contains request.state.scopes)
72
+ resource_type: Type of resource ("agents", "teams", "workflows")
73
+
74
+ Returns:
75
+ Set of resource IDs the user can access. Returns {"*"} for wildcard access.
76
+
77
+ Usage:
78
+ accessible_ids = get_accessible_resources(request, "agents")
79
+ if "*" not in accessible_ids:
80
+ agents = [a for a in agents if a.id in accessible_ids]
81
+
82
+ Examples:
83
+ >>> # User with specific agent access
84
+ >>> # Token scopes: ["agent-os:my-os:agents:my-agent:read"]
85
+ >>> get_accessible_resources(request, "agents")
86
+ {'my-agent'}
87
+
88
+ >>> # User with wildcard access
89
+ >>> # Token scopes: ["agent-os:my-os:agents:*:read"] or ["admin"]
90
+ >>> get_accessible_resources(request, "agents")
91
+ {'*'}
92
+
93
+ >>> # User with agent-os level access (global resource scope)
94
+ >>> # Token scopes: ["agent-os:my-os:agents:read"]
95
+ >>> get_accessible_resources(request, "agents")
96
+ {'*'}
97
+ """
98
+ # Check if accessible_resource_ids is already cached in request state (set by JWT middleware)
99
+ # This happens when user doesn't have global scope but has specific resource scopes
100
+ cached_ids = getattr(request.state, "accessible_resource_ids", None)
101
+ if cached_ids is not None:
102
+ return cached_ids
103
+
104
+ # Get user's scopes from request state (set by JWT middleware)
105
+ user_scopes = getattr(request.state, "scopes", [])
106
+
107
+ # Get accessible resource IDs
108
+ accessible_ids = get_accessible_resource_ids(user_scopes=user_scopes, resource_type=resource_type)
109
+
110
+ return accessible_ids
111
+
112
+
113
+ def filter_resources_by_access(request: Request, resources: List, resource_type: str) -> List:
114
+ """
115
+ Filter a list of resources based on user's access permissions.
116
+
117
+ Args:
118
+ request: The FastAPI request object
119
+ resources: List of resource objects (agents, teams, or workflows) with 'id' attribute
120
+ resource_type: Type of resource ("agents", "teams", "workflows")
121
+
122
+ Returns:
123
+ Filtered list of resources the user has access to
124
+
125
+ Usage:
126
+ agents = filter_resources_by_access(request, all_agents, "agents")
127
+ teams = filter_resources_by_access(request, all_teams, "teams")
128
+ workflows = filter_resources_by_access(request, all_workflows, "workflows")
129
+
130
+ Examples:
131
+ >>> # User with specific access
132
+ >>> agents = [Agent(id="agent-1"), Agent(id="agent-2"), Agent(id="agent-3")]
133
+ >>> # Token scopes: ["agent-os:my-os:agents:agent-1:read", "agent-os:my-os:agents:agent-2:read"]
134
+ >>> filter_resources_by_access(request, agents, "agents")
135
+ [Agent(id="agent-1"), Agent(id="agent-2")]
136
+
137
+ >>> # User with wildcard access
138
+ >>> # Token scopes: ["admin"]
139
+ >>> filter_resources_by_access(request, agents, "agents")
140
+ [Agent(id="agent-1"), Agent(id="agent-2"), Agent(id="agent-3")]
141
+ """
142
+ accessible_ids = get_accessible_resources(request, resource_type)
143
+
144
+ # Wildcard access - return all resources
145
+ if "*" in accessible_ids:
146
+ return resources
147
+
148
+ # Filter to only accessible resources
149
+ return [r for r in resources if r.id in accessible_ids]
150
+
151
+
152
+ def check_resource_access(request: Request, resource_id: str, resource_type: str, action: str = "read") -> bool:
153
+ """
154
+ Check if user has access to a specific resource.
155
+
156
+ Args:
157
+ request: The FastAPI request object
158
+ resource_id: ID of the resource to check
159
+ resource_type: Type of resource ("agents", "teams", "workflows")
160
+ action: Action to check ("read", "run", etc.)
161
+
162
+ Returns:
163
+ True if user has access, False otherwise
164
+
165
+ Usage:
166
+ if not check_resource_access(request, agent_id, "agents", "run"):
167
+ raise HTTPException(status_code=403, detail="Access denied")
168
+
169
+ Examples:
170
+ >>> # Token scopes: ["agent-os:my-os:agents:my-agent:read", "agent-os:my-os:agents:my-agent:run"]
171
+ >>> check_resource_access(request, "my-agent", "agents", "run")
172
+ True
173
+
174
+ >>> check_resource_access(request, "other-agent", "agents", "run")
175
+ False
176
+ """
177
+ accessible_ids = get_accessible_resources(request, resource_type)
178
+
179
+ # Wildcard access grants all permissions
180
+ if "*" in accessible_ids:
181
+ return True
182
+
183
+ # Check if user has access to this specific resource
184
+ return resource_id in accessible_ids
185
+
186
+
187
+ def require_resource_access(resource_type: str, action: str, resource_id_param: str):
188
+ """
189
+ Create a dependency that checks if the user has access to a specific resource.
190
+
191
+ This dependency factory creates a FastAPI dependency that automatically checks
192
+ authorization when authorization is enabled. It extracts the resource ID from
193
+ the path parameters and verifies the user has the required access.
194
+
195
+ Args:
196
+ resource_type: Type of resource ("agents", "teams", "workflows")
197
+ action: Action to check ("read", "run")
198
+ resource_id_param: Name of the path parameter containing the resource ID
199
+
200
+ Returns:
201
+ A dependency function for use with FastAPI's Depends()
202
+
203
+ Usage:
204
+ @router.post("/agents/{agent_id}/runs")
205
+ async def create_agent_run(
206
+ agent_id: str,
207
+ request: Request,
208
+ _: None = Depends(require_resource_access("agents", "run", "agent_id")),
209
+ ):
210
+ ...
211
+
212
+ @router.get("/agents/{agent_id}")
213
+ async def get_agent(
214
+ agent_id: str,
215
+ request: Request,
216
+ _: None = Depends(require_resource_access("agents", "read", "agent_id")),
217
+ ):
218
+ ...
219
+
220
+ Examples:
221
+ >>> # Creates dependency for checking agent run access
222
+ >>> dep = require_resource_access("agents", "run", "agent_id")
223
+
224
+ >>> # Creates dependency for checking team read access
225
+ >>> dep = require_resource_access("teams", "read", "team_id")
226
+ """
227
+ # Map resource_type to singular form for error messages
228
+ resource_singular = {
229
+ "agents": "agent",
230
+ "teams": "team",
231
+ "workflows": "workflow",
232
+ }.get(resource_type, resource_type.rstrip("s"))
233
+
234
+ async def dependency(request: Request):
235
+ # Only check authorization if it's enabled
236
+ if not getattr(request.state, "authorization_enabled", False):
237
+ return
238
+
239
+ # Get the resource_id from path parameters
240
+ resource_id = request.path_params.get(resource_id_param)
241
+ if resource_id and not check_resource_access(request, resource_id, resource_type, action):
242
+ raise HTTPException(status_code=403, detail=f"Access denied to {action} this {resource_singular}")
243
+
244
+ return dependency
agno/os/config.py CHANGED
@@ -5,6 +5,15 @@ from typing import Generic, List, Optional, TypeVar
5
5
  from pydantic import BaseModel, field_validator
6
6
 
7
7
 
8
+ class AuthorizationConfig(BaseModel):
9
+ """Configuration for the JWT middleware"""
10
+
11
+ verification_keys: Optional[List[str]] = None
12
+ jwks_file: Optional[str] = None
13
+ algorithm: Optional[str] = None
14
+ verify_audience: Optional[bool] = None
15
+
16
+
8
17
  class EvalsDomainConfig(BaseModel):
9
18
  """Configuration for the Evals domain of the AgentOS"""
10
19
 
@@ -36,6 +45,12 @@ class MemoryDomainConfig(BaseModel):
36
45
  display_name: Optional[str] = None
37
46
 
38
47
 
48
+ class TracesDomainConfig(BaseModel):
49
+ """Configuration for the Traces domain of the AgentOS"""
50
+
51
+ display_name: Optional[str] = None
52
+
53
+
39
54
  DomainConfigType = TypeVar("DomainConfigType")
40
55
 
41
56
 
@@ -44,6 +59,7 @@ class DatabaseConfig(BaseModel, Generic[DomainConfigType]):
44
59
 
45
60
  db_id: str
46
61
  domain_config: Optional[DomainConfigType] = None
62
+ tables: Optional[List[str]] = None
47
63
 
48
64
 
49
65
  class EvalsConfig(EvalsDomainConfig):
@@ -76,6 +92,12 @@ class MetricsConfig(MetricsDomainConfig):
76
92
  dbs: Optional[List[DatabaseConfig[MetricsDomainConfig]]] = None
77
93
 
78
94
 
95
+ class TracesConfig(TracesDomainConfig):
96
+ """Configuration for the Traces domain of the AgentOS"""
97
+
98
+ dbs: Optional[List[DatabaseConfig[TracesDomainConfig]]] = None
99
+
100
+
79
101
  class ChatConfig(BaseModel):
80
102
  """Configuration for the Chat page of the AgentOS"""
81
103
 
@@ -101,3 +123,4 @@ class AgentOSConfig(BaseModel):
101
123
  memory: Optional[MemoryConfig] = None
102
124
  session: Optional[SessionConfig] = None
103
125
  metrics: Optional[MetricsConfig] = None
126
+ traces: Optional[TracesConfig] = None
@@ -11,7 +11,7 @@ from typing_extensions import List
11
11
  try:
12
12
  from a2a.types import SendMessageSuccessResponse, Task, TaskState, TaskStatus
13
13
  except ImportError as e:
14
- raise ImportError("`a2a` not installed. Please install it with `pip install -U a2a`") from e
14
+ raise ImportError("`a2a` not installed. Please install it with `pip install -U a2a-sdk`") from e
15
15
 
16
16
  from agno.agent import Agent
17
17
  from agno.os.interfaces.a2a.utils import (
@@ -19,8 +19,7 @@ from agno.os.interfaces.a2a.utils import (
19
19
  map_run_output_to_a2a_task,
20
20
  stream_a2a_response_with_error_handling,
21
21
  )
22
- from agno.os.router import _get_request_kwargs
23
- from agno.os.utils import get_agent_by_id, get_team_by_id, get_workflow_by_id
22
+ from agno.os.utils import get_agent_by_id, get_request_kwargs, get_team_by_id, get_workflow_by_id
24
23
  from agno.team import Team
25
24
  from agno.workflow import Workflow
26
25
 
@@ -36,9 +35,8 @@ def attach_routes(
36
35
 
37
36
  @router.post(
38
37
  "/message/send",
39
- tags=["A2A"],
40
38
  operation_id="send_message",
41
- summary="Send message to Agent, Team, or Workflow (A2A Protocol)",
39
+ name="send_message",
42
40
  description="Send a message to an Agno Agent, Team, or Workflow. "
43
41
  "The Agent, Team or Workflow is identified via the 'agentId' field in params.message or X-Agent-ID header. "
44
42
  "Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata.",
@@ -76,7 +74,7 @@ def attach_routes(
76
74
  )
77
75
  async def a2a_send_message(request: Request):
78
76
  request_body = await request.json()
79
- kwargs = await _get_request_kwargs(request, a2a_send_message)
77
+ kwargs = await get_request_kwargs(request, a2a_send_message)
80
78
 
81
79
  # 1. Get the Agent, Team, or Workflow to run
82
80
  agent_id = request_body.get("params", {}).get("message", {}).get("agentId") or request.headers.get("X-Agent-ID")
@@ -159,9 +157,8 @@ def attach_routes(
159
157
 
160
158
  @router.post(
161
159
  "/message/stream",
162
- tags=["A2A"],
163
160
  operation_id="stream_message",
164
- summary="Stream message to Agent, Team, or Workflow (A2A Protocol)",
161
+ name="stream_message",
165
162
  description="Stream a message to an Agno Agent, Team, or Workflow."
166
163
  "The Agent, Team or Workflow is identified via the 'agentId' field in params.message or X-Agent-ID header. "
167
164
  "Optional: Pass user ID via X-User-ID header (recommended) or 'userId' in params.message.metadata. "
@@ -183,7 +180,7 @@ def attach_routes(
183
180
  )
184
181
  async def a2a_stream_message(request: Request):
185
182
  request_body = await request.json()
186
- kwargs = await _get_request_kwargs(request, a2a_stream_message)
183
+ kwargs = await get_request_kwargs(request, a2a_stream_message)
187
184
 
188
185
  # 1. Get the Agent, Team, or Workflow to run
189
186
  agent_id = request_body.get("params", {}).get("message", {}).get("agentId")
@@ -223,7 +220,7 @@ def attach_routes(
223
220
  session_id=context_id,
224
221
  user_id=user_id,
225
222
  stream=True,
226
- stream_intermediate_steps=True,
223
+ stream_events=True,
227
224
  **kwargs,
228
225
  )
229
226
  else:
@@ -236,7 +233,7 @@ def attach_routes(
236
233
  session_id=context_id,
237
234
  user_id=user_id,
238
235
  stream=True,
239
- stream_intermediate_steps=True,
236
+ stream_events=True,
240
237
  **kwargs,
241
238
  )
242
239
 
@@ -110,7 +110,7 @@ async def map_a2a_request_to_run_input(request_body: dict, stream: bool = True)
110
110
 
111
111
  Returns:
112
112
  RunInput: The Agno RunInput
113
- stream: Wheter we are in stream mode
113
+ stream: Whether we are in stream mode
114
114
  """
115
115
 
116
116
  # 1. Validate the request
@@ -19,6 +19,7 @@ from agno.agent.agent import Agent
19
19
  from agno.os.interfaces.agui.utils import (
20
20
  async_stream_agno_response_as_agui_events,
21
21
  convert_agui_messages_to_agno_messages,
22
+ validate_agui_state,
22
23
  )
23
24
  from agno.team.team import Team
24
25
 
@@ -32,6 +33,7 @@ async def run_agent(agent: Agent, run_input: RunAgentInput) -> AsyncIterator[Bas
32
33
  try:
33
34
  # Preparing the input for the Agent and emitting the run started event
34
35
  messages = convert_agui_messages_to_agno_messages(run_input.messages or [])
36
+
35
37
  yield RunStartedEvent(type=EventType.RUN_STARTED, thread_id=run_input.thread_id, run_id=run_id)
36
38
 
37
39
  # Look for user_id in run_input.forwarded_props
@@ -39,13 +41,18 @@ async def run_agent(agent: Agent, run_input: RunAgentInput) -> AsyncIterator[Bas
39
41
  if run_input.forwarded_props and isinstance(run_input.forwarded_props, dict):
40
42
  user_id = run_input.forwarded_props.get("user_id")
41
43
 
44
+ # Validating the session state is of the expected type (dict)
45
+ session_state = validate_agui_state(run_input.state, run_input.thread_id)
46
+
42
47
  # Request streaming response from agent
43
48
  response_stream = agent.arun(
44
49
  input=messages,
45
50
  session_id=run_input.thread_id,
46
51
  stream=True,
47
- stream_intermediate_steps=True,
52
+ stream_events=True,
48
53
  user_id=user_id,
54
+ session_state=session_state,
55
+ run_id=run_id,
49
56
  )
50
57
 
51
58
  # Stream the response content in AG-UI format
@@ -75,13 +82,18 @@ async def run_team(team: Team, input: RunAgentInput) -> AsyncIterator[BaseEvent]
75
82
  if input.forwarded_props and isinstance(input.forwarded_props, dict):
76
83
  user_id = input.forwarded_props.get("user_id")
77
84
 
85
+ # Validating the session state is of the expected type (dict)
86
+ session_state = validate_agui_state(input.state, input.thread_id)
87
+
78
88
  # Request streaming response from team
79
89
  response_stream = team.arun(
80
90
  input=messages,
81
91
  session_id=input.thread_id,
82
92
  stream=True,
83
- stream_intermediate_steps=True,
93
+ stream_steps=True,
84
94
  user_id=user_id,
95
+ session_state=session_state,
96
+ run_id=run_id,
85
97
  )
86
98
 
87
99
  # Stream the response content in AG-UI format
@@ -101,7 +113,10 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
101
113
 
102
114
  encoder = EventEncoder()
103
115
 
104
- @router.post("/agui")
116
+ @router.post(
117
+ "/agui",
118
+ name="run_agent",
119
+ )
105
120
  async def run_agent_agui(run_input: RunAgentInput):
106
121
  async def event_generator():
107
122
  if agent:
@@ -3,11 +3,12 @@
3
3
  import json
4
4
  import uuid
5
5
  from collections.abc import Iterator
6
- from dataclasses import dataclass
7
- from typing import AsyncIterator, List, Set, Tuple, Union
6
+ from dataclasses import asdict, dataclass, is_dataclass
7
+ from typing import Any, AsyncIterator, Dict, List, Optional, Set, Tuple, Union
8
8
 
9
9
  from ag_ui.core import (
10
10
  BaseEvent,
11
+ CustomEvent,
11
12
  EventType,
12
13
  RunFinishedEvent,
13
14
  StepFinishedEvent,
@@ -21,14 +22,48 @@ from ag_ui.core import (
21
22
  ToolCallStartEvent,
22
23
  )
23
24
  from ag_ui.core.types import Message as AGUIMessage
25
+ from pydantic import BaseModel
24
26
 
25
27
  from agno.models.message import Message
26
28
  from agno.run.agent import RunContentEvent, RunEvent, RunOutputEvent, RunPausedEvent
27
29
  from agno.run.team import RunContentEvent as TeamRunContentEvent
28
30
  from agno.run.team import TeamRunEvent, TeamRunOutputEvent
31
+ from agno.utils.log import log_debug, log_warning
29
32
  from agno.utils.message import get_text_from_message
30
33
 
31
34
 
35
+ def validate_agui_state(state: Any, thread_id: str) -> Optional[Dict[str, Any]]:
36
+ """Validate the given AGUI state is of the expected type (dict)."""
37
+ if state is None:
38
+ return None
39
+
40
+ if isinstance(state, dict):
41
+ return state
42
+
43
+ if isinstance(state, BaseModel):
44
+ try:
45
+ return state.model_dump()
46
+ except Exception:
47
+ pass
48
+
49
+ if is_dataclass(state):
50
+ try:
51
+ return asdict(state) # type: ignore
52
+ except Exception:
53
+ pass
54
+
55
+ if hasattr(state, "to_dict") and callable(getattr(state, "to_dict")):
56
+ try:
57
+ result = state.to_dict() # type: ignore
58
+ if isinstance(result, dict):
59
+ return result
60
+ except Exception:
61
+ pass
62
+
63
+ log_warning(f"AGUI state must be a dict, got {type(state).__name__}. State will be ignored. Thread: {thread_id}")
64
+ return None
65
+
66
+
32
67
  @dataclass
33
68
  class EventBuffer:
34
69
  """Buffer to manage event ordering constraints, relevant when mapping Agno responses to AG-UI events."""
@@ -81,23 +116,43 @@ class EventBuffer:
81
116
 
82
117
  def convert_agui_messages_to_agno_messages(messages: List[AGUIMessage]) -> List[Message]:
83
118
  """Convert AG-UI messages to Agno messages."""
84
- result = []
119
+ # First pass: collect all tool_call_ids that have results
120
+ tool_call_ids_with_results: Set[str] = set()
121
+ for msg in messages:
122
+ if msg.role == "tool" and msg.tool_call_id:
123
+ tool_call_ids_with_results.add(msg.tool_call_id)
124
+
125
+ # Second pass: convert messages
126
+ result: List[Message] = []
127
+ seen_tool_call_ids: Set[str] = set()
128
+
85
129
  for msg in messages:
86
130
  if msg.role == "tool":
131
+ # Deduplicate tool results - keep only first occurrence
132
+ if msg.tool_call_id in seen_tool_call_ids:
133
+ log_debug(f"Skipping duplicate AGUI tool result: {msg.tool_call_id}")
134
+ continue
135
+ seen_tool_call_ids.add(msg.tool_call_id)
87
136
  result.append(Message(role="tool", tool_call_id=msg.tool_call_id, content=msg.content))
137
+
88
138
  elif msg.role == "assistant":
89
139
  tool_calls = None
90
140
  if msg.tool_calls:
91
- tool_calls = [call.model_dump() for call in msg.tool_calls]
92
- result.append(
93
- Message(
94
- role="assistant",
95
- content=msg.content,
96
- tool_calls=tool_calls,
97
- )
98
- )
141
+ # Filter tool_calls to only those with results in this message sequence
142
+ filtered_calls = [call for call in msg.tool_calls if call.id in tool_call_ids_with_results]
143
+ if filtered_calls:
144
+ tool_calls = [call.model_dump() for call in filtered_calls]
145
+ result.append(Message(role="assistant", content=msg.content, tool_calls=tool_calls))
146
+
99
147
  elif msg.role == "user":
100
148
  result.append(Message(role="user", content=msg.content))
149
+
150
+ elif msg.role == "system":
151
+ pass # Skip - agent builds its own system message from configuration
152
+
153
+ else:
154
+ log_warning(f"Unknown AGUI message role: {msg.role}")
155
+
101
156
  return result
102
157
 
103
158
 
@@ -215,7 +270,25 @@ def _create_events_from_chunk(
215
270
  parent_message_id = event_buffer.get_parent_message_id_for_tool_call()
216
271
 
217
272
  if not parent_message_id:
218
- parent_message_id = current_message_id
273
+ # Create parent message for tool calls without preceding assistant message
274
+ parent_message_id = str(uuid.uuid4())
275
+
276
+ # Emit a text message to serve as the parent
277
+ text_start = TextMessageStartEvent(
278
+ type=EventType.TEXT_MESSAGE_START,
279
+ message_id=parent_message_id,
280
+ role="assistant",
281
+ )
282
+ events_to_emit.append(text_start)
283
+
284
+ text_end = TextMessageEndEvent(
285
+ type=EventType.TEXT_MESSAGE_END,
286
+ message_id=parent_message_id,
287
+ )
288
+ events_to_emit.append(text_end)
289
+
290
+ # Set this as the pending parent for subsequent tool calls in this batch
291
+ event_buffer.set_pending_tool_calls_parent_id(parent_message_id)
219
292
 
220
293
  start_event = ToolCallStartEvent(
221
294
  type=EventType.TOOL_CALL_START,
@@ -261,6 +334,23 @@ def _create_events_from_chunk(
261
334
  step_finished_event = StepFinishedEvent(type=EventType.STEP_FINISHED, step_name="reasoning")
262
335
  events_to_emit.append(step_finished_event)
263
336
 
337
+ # Handle custom events
338
+ elif chunk.event == RunEvent.custom_event:
339
+ # Use the name of the event class if available, otherwise default to the CustomEvent
340
+ try:
341
+ custom_event_name = chunk.__class__.__name__
342
+ except Exception:
343
+ custom_event_name = chunk.event
344
+
345
+ # Use the complete Agno event as value if parsing it works, else the event content field
346
+ try:
347
+ custom_event_value = chunk.to_dict()
348
+ except Exception:
349
+ custom_event_value = chunk.content # type: ignore
350
+
351
+ custom_event = CustomEvent(name=custom_event_name, value=custom_event_value)
352
+ events_to_emit.append(custom_event)
353
+
264
354
  return events_to_emit, message_started, message_id
265
355
 
266
356
 
@@ -289,37 +379,60 @@ def _create_completion_events(
289
379
  end_message_event = TextMessageEndEvent(type=EventType.TEXT_MESSAGE_END, message_id=message_id)
290
380
  events_to_emit.append(end_message_event)
291
381
 
292
- # emit frontend tool calls, i.e. external_execution=True
293
- if isinstance(chunk, RunPausedEvent) and chunk.tools is not None:
294
- for tool in chunk.tools:
295
- if tool.tool_call_id is None or tool.tool_name is None:
296
- continue
297
-
298
- # Use the current text message ID from event buffer as parent
299
- parent_message_id = event_buffer.get_parent_message_id_for_tool_call()
300
- if not parent_message_id:
301
- parent_message_id = message_id # Fallback to the passed message_id
302
-
303
- start_event = ToolCallStartEvent(
304
- type=EventType.TOOL_CALL_START,
305
- tool_call_id=tool.tool_call_id,
306
- tool_call_name=tool.tool_name,
307
- parent_message_id=parent_message_id,
382
+ # Emit external execution tools
383
+ if isinstance(chunk, RunPausedEvent):
384
+ external_tools = chunk.tools_awaiting_external_execution
385
+ if external_tools:
386
+ # First, emit an assistant message for external tool calls
387
+ assistant_message_id = str(uuid.uuid4())
388
+ assistant_start_event = TextMessageStartEvent(
389
+ type=EventType.TEXT_MESSAGE_START,
390
+ message_id=assistant_message_id,
391
+ role="assistant",
308
392
  )
309
- events_to_emit.append(start_event)
393
+ events_to_emit.append(assistant_start_event)
394
+
395
+ # Add any text content if present for the assistant message
396
+ if chunk.content:
397
+ content_event = TextMessageContentEvent(
398
+ type=EventType.TEXT_MESSAGE_CONTENT,
399
+ message_id=assistant_message_id,
400
+ delta=str(chunk.content),
401
+ )
402
+ events_to_emit.append(content_event)
310
403
 
311
- args_event = ToolCallArgsEvent(
312
- type=EventType.TOOL_CALL_ARGS,
313
- tool_call_id=tool.tool_call_id,
314
- delta=json.dumps(tool.tool_args),
404
+ # End the assistant message
405
+ assistant_end_event = TextMessageEndEvent(
406
+ type=EventType.TEXT_MESSAGE_END,
407
+ message_id=assistant_message_id,
315
408
  )
316
- events_to_emit.append(args_event)
409
+ events_to_emit.append(assistant_end_event)
410
+
411
+ # Emit tool call events for external execution
412
+ for tool in external_tools:
413
+ if tool.tool_call_id is None or tool.tool_name is None:
414
+ continue
415
+
416
+ start_event = ToolCallStartEvent(
417
+ type=EventType.TOOL_CALL_START,
418
+ tool_call_id=tool.tool_call_id,
419
+ tool_call_name=tool.tool_name,
420
+ parent_message_id=assistant_message_id, # Use the assistant message as parent
421
+ )
422
+ events_to_emit.append(start_event)
317
423
 
318
- end_event = ToolCallEndEvent(
319
- type=EventType.TOOL_CALL_END,
320
- tool_call_id=tool.tool_call_id,
321
- )
322
- events_to_emit.append(end_event)
424
+ args_event = ToolCallArgsEvent(
425
+ type=EventType.TOOL_CALL_ARGS,
426
+ tool_call_id=tool.tool_call_id,
427
+ delta=json.dumps(tool.tool_args),
428
+ )
429
+ events_to_emit.append(args_event)
430
+
431
+ end_event = ToolCallEndEvent(
432
+ type=EventType.TOOL_CALL_END,
433
+ tool_call_id=tool.tool_call_id,
434
+ )
435
+ events_to_emit.append(end_event)
323
436
 
324
437
  run_finished_event = RunFinishedEvent(type=EventType.RUN_FINISHED, thread_id=thread_id, run_id=run_id)
325
438
  events_to_emit.append(run_finished_event)