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
@@ -1,6 +1,6 @@
1
1
  """Main class for the AG-UI app, used to expose an Agno Agent or Team in an AG-UI compatible format."""
2
2
 
3
- from typing import Optional
3
+ from typing import List, Optional
4
4
 
5
5
  from fastapi.routing import APIRouter
6
6
 
@@ -15,16 +15,32 @@ class AGUI(BaseInterface):
15
15
 
16
16
  router: APIRouter
17
17
 
18
- def __init__(self, agent: Optional[Agent] = None, team: Optional[Team] = None):
18
+ def __init__(
19
+ self,
20
+ agent: Optional[Agent] = None,
21
+ team: Optional[Team] = None,
22
+ prefix: str = "",
23
+ tags: Optional[List[str]] = None,
24
+ ):
25
+ """
26
+ Initialize the AGUI interface.
27
+
28
+ Args:
29
+ agent: The agent to expose via AG-UI
30
+ team: The team to expose via AG-UI
31
+ prefix: Custom prefix for the router (e.g., "/agui/v1", "/chat/public")
32
+ tags: Custom tags for the router (e.g., ["AGUI", "Chat"], defaults to ["AGUI"])
33
+ """
19
34
  self.agent = agent
20
35
  self.team = team
36
+ self.prefix = prefix
37
+ self.tags = tags or ["AGUI"]
21
38
 
22
- if not self.agent and not self.team:
23
- raise ValueError("AGUI requires an agent and a team")
39
+ if not (self.agent or self.team):
40
+ raise ValueError("AGUI requires an agent or a team")
24
41
 
25
- def get_router(self, **kwargs) -> APIRouter:
26
- # Cannot be overridden
27
- self.router = APIRouter(tags=["AGUI"])
42
+ def get_router(self) -> APIRouter:
43
+ self.router = APIRouter(prefix=self.prefix, tags=self.tags) # type: ignore
28
44
 
29
45
  self.router = attach_routes(router=self.router, agent=self.agent, team=self.team)
30
46
 
@@ -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
 
@@ -34,12 +35,22 @@ async def run_agent(agent: Agent, run_input: RunAgentInput) -> AsyncIterator[Bas
34
35
  messages = convert_agui_messages_to_agno_messages(run_input.messages or [])
35
36
  yield RunStartedEvent(type=EventType.RUN_STARTED, thread_id=run_input.thread_id, run_id=run_id)
36
37
 
38
+ # Look for user_id in run_input.forwarded_props
39
+ user_id = None
40
+ if run_input.forwarded_props and isinstance(run_input.forwarded_props, dict):
41
+ user_id = run_input.forwarded_props.get("user_id")
42
+
43
+ # Validating the session state is of the expected type (dict)
44
+ session_state = validate_agui_state(run_input.state, run_input.thread_id)
45
+
37
46
  # Request streaming response from agent
38
47
  response_stream = agent.arun(
39
48
  input=messages,
40
49
  session_id=run_input.thread_id,
41
50
  stream=True,
42
- stream_intermediate_steps=True,
51
+ stream_events=True,
52
+ user_id=user_id,
53
+ session_state=session_state,
43
54
  )
44
55
 
45
56
  # Stream the response content in AG-UI format
@@ -64,12 +75,22 @@ async def run_team(team: Team, input: RunAgentInput) -> AsyncIterator[BaseEvent]
64
75
  messages = convert_agui_messages_to_agno_messages(input.messages or [])
65
76
  yield RunStartedEvent(type=EventType.RUN_STARTED, thread_id=input.thread_id, run_id=run_id)
66
77
 
78
+ # Look for user_id in input.forwarded_props
79
+ user_id = None
80
+ if input.forwarded_props and isinstance(input.forwarded_props, dict):
81
+ user_id = input.forwarded_props.get("user_id")
82
+
83
+ # Validating the session state is of the expected type (dict)
84
+ session_state = validate_agui_state(input.state, input.thread_id)
85
+
67
86
  # Request streaming response from team
68
87
  response_stream = team.arun(
69
88
  input=messages,
70
89
  session_id=input.thread_id,
71
90
  stream=True,
72
- stream_intermediate_steps=True,
91
+ stream_steps=True,
92
+ user_id=user_id,
93
+ session_state=session_state,
73
94
  )
74
95
 
75
96
  # Stream the response content in AG-UI format
@@ -89,7 +110,10 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
89
110
 
90
111
  encoder = EventEncoder()
91
112
 
92
- @router.post("/agui")
113
+ @router.post(
114
+ "/agui",
115
+ name="run_agent",
116
+ )
93
117
  async def run_agent_agui(run_input: RunAgentInput):
94
118
  async def event_generator():
95
119
  if agent:
@@ -2,13 +2,13 @@
2
2
 
3
3
  import json
4
4
  import uuid
5
- from collections import deque
6
5
  from collections.abc import Iterator
7
- from dataclasses import dataclass
8
- from typing import AsyncIterator, Deque, List, Optional, Set, Tuple, Union
6
+ from dataclasses import asdict, dataclass, is_dataclass
7
+ from typing import Any, AsyncIterator, Dict, List, Optional, Set, Tuple, Union
9
8
 
10
9
  from ag_ui.core import (
11
10
  BaseEvent,
11
+ CustomEvent,
12
12
  EventType,
13
13
  RunFinishedEvent,
14
14
  StepFinishedEvent,
@@ -22,50 +22,96 @@ from ag_ui.core import (
22
22
  ToolCallStartEvent,
23
23
  )
24
24
  from ag_ui.core.types import Message as AGUIMessage
25
+ from pydantic import BaseModel
25
26
 
26
27
  from agno.models.message import Message
27
28
  from agno.run.agent import RunContentEvent, RunEvent, RunOutputEvent, RunPausedEvent
28
29
  from agno.run.team import RunContentEvent as TeamRunContentEvent
29
30
  from agno.run.team import TeamRunEvent, TeamRunOutputEvent
31
+ from agno.utils.log import log_warning
30
32
  from agno.utils.message import get_text_from_message
31
33
 
32
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
+
33
67
  @dataclass
34
68
  class EventBuffer:
35
69
  """Buffer to manage event ordering constraints, relevant when mapping Agno responses to AG-UI events."""
36
70
 
37
- buffer: Deque[BaseEvent]
38
- blocking_tool_call_id: Optional[str] # The tool call that's currently blocking the buffer
39
71
  active_tool_call_ids: Set[str] # All currently active tool calls
40
72
  ended_tool_call_ids: Set[str] # All tool calls that have ended
73
+ current_text_message_id: str = "" # ID of the current text message context (for tool call parenting)
74
+ next_text_message_id: str = "" # Pre-generated ID for the next text message
75
+ pending_tool_calls_parent_id: str = "" # Parent message ID for pending tool calls
41
76
 
42
77
  def __init__(self):
43
- self.buffer = deque()
44
- self.blocking_tool_call_id = None
45
78
  self.active_tool_call_ids = set()
46
79
  self.ended_tool_call_ids = set()
47
-
48
- def is_blocked(self) -> bool:
49
- """Check if the buffer is currently blocked by an active tool call."""
50
- return self.blocking_tool_call_id is not None
80
+ self.current_text_message_id = ""
81
+ self.next_text_message_id = str(uuid.uuid4())
82
+ self.pending_tool_calls_parent_id = ""
51
83
 
52
84
  def start_tool_call(self, tool_call_id: str) -> None:
53
- """Start a new tool call, marking it the current blocking tool call if needed."""
85
+ """Start a new tool call."""
54
86
  self.active_tool_call_ids.add(tool_call_id)
55
- if self.blocking_tool_call_id is None:
56
- self.blocking_tool_call_id = tool_call_id
57
87
 
58
- def end_tool_call(self, tool_call_id: str) -> bool:
59
- """End a tool call, marking it as ended and unblocking the buffer if needed."""
88
+ def end_tool_call(self, tool_call_id: str) -> None:
89
+ """End a tool call."""
60
90
  self.active_tool_call_ids.discard(tool_call_id)
61
91
  self.ended_tool_call_ids.add(tool_call_id)
62
92
 
63
- # Unblock the buffer if the current blocking tool call is the one ending
64
- if tool_call_id == self.blocking_tool_call_id:
65
- self.blocking_tool_call_id = None
66
- return True
93
+ def start_text_message(self) -> str:
94
+ """Start a new text message and return its ID."""
95
+ # Use the pre-generated next ID as current, and generate a new next ID
96
+ self.current_text_message_id = self.next_text_message_id
97
+ self.next_text_message_id = str(uuid.uuid4())
98
+ return self.current_text_message_id
99
+
100
+ def get_parent_message_id_for_tool_call(self) -> str:
101
+ """Get the message ID to use as parent for tool calls."""
102
+ # If we have a pending parent ID set (from text message end), use that
103
+ if self.pending_tool_calls_parent_id:
104
+ return self.pending_tool_calls_parent_id
105
+ # Otherwise use current text message ID
106
+ return self.current_text_message_id
107
+
108
+ def set_pending_tool_calls_parent_id(self, parent_id: str) -> None:
109
+ """Set the parent message ID for upcoming tool calls."""
110
+ self.pending_tool_calls_parent_id = parent_id
67
111
 
68
- return False
112
+ def clear_pending_tool_calls_parent_id(self) -> None:
113
+ """Clear the pending parent ID when a new text message starts."""
114
+ self.pending_tool_calls_parent_id = ""
69
115
 
70
116
 
71
117
  def convert_agui_messages_to_agno_messages(messages: List[AGUIMessage]) -> List[Message]:
@@ -131,10 +177,18 @@ def _create_events_from_chunk(
131
177
  message_id: str,
132
178
  message_started: bool,
133
179
  event_buffer: EventBuffer,
134
- ) -> Tuple[List[BaseEvent], bool]:
180
+ ) -> Tuple[List[BaseEvent], bool, str]:
135
181
  """
136
182
  Process a single chunk and return events to emit + updated message_started state.
137
- Returns: (events_to_emit, new_message_started_state)
183
+
184
+ Args:
185
+ chunk: The event chunk to process
186
+ message_id: Current message identifier
187
+ message_started: Whether a message is currently active
188
+ event_buffer: Event buffer for tracking tool call state
189
+
190
+ Returns:
191
+ Tuple of (events_to_emit, new_message_started_state, message_id)
138
192
  """
139
193
  events_to_emit: List[BaseEvent] = []
140
194
 
@@ -151,6 +205,11 @@ def _create_events_from_chunk(
151
205
  # Handle the message start event, emitted once per message
152
206
  if not message_started:
153
207
  message_started = True
208
+ message_id = event_buffer.start_text_message()
209
+
210
+ # Clear pending tool calls parent ID when starting new text message
211
+ event_buffer.clear_pending_tool_calls_parent_id()
212
+
154
213
  start_event = TextMessageStartEvent(
155
214
  type=EventType.TEXT_MESSAGE_START,
156
215
  message_id=message_id,
@@ -167,15 +226,37 @@ def _create_events_from_chunk(
167
226
  )
168
227
  events_to_emit.append(content_event) # type: ignore
169
228
 
170
- # Handle starting a new tool call
171
- elif chunk.event == RunEvent.tool_call_started:
229
+ # Handle starting a new tool
230
+ elif chunk.event == RunEvent.tool_call_started or chunk.event == TeamRunEvent.tool_call_started:
172
231
  if chunk.tool is not None: # type: ignore
173
232
  tool_call = chunk.tool # type: ignore
233
+
234
+ # End current text message and handle for tool calls
235
+ current_message_id = message_id
236
+ if message_started:
237
+ # End the current text message
238
+ end_message_event = TextMessageEndEvent(type=EventType.TEXT_MESSAGE_END, message_id=current_message_id)
239
+ events_to_emit.append(end_message_event)
240
+
241
+ # Set this message as the parent for any upcoming tool calls
242
+ # This ensures multiple sequential tool calls all use the same parent
243
+ event_buffer.set_pending_tool_calls_parent_id(current_message_id)
244
+
245
+ # Reset message started state and generate new message_id for future messages
246
+ message_started = False
247
+ message_id = str(uuid.uuid4())
248
+
249
+ # Get the parent message ID - this will use pending parent if set, ensuring multiple tool calls in sequence have the same parent
250
+ parent_message_id = event_buffer.get_parent_message_id_for_tool_call()
251
+
252
+ if not parent_message_id:
253
+ parent_message_id = current_message_id
254
+
174
255
  start_event = ToolCallStartEvent(
175
256
  type=EventType.TOOL_CALL_START,
176
257
  tool_call_id=tool_call.tool_call_id, # type: ignore
177
258
  tool_call_name=tool_call.tool_name, # type: ignore
178
- parent_message_id=message_id,
259
+ parent_message_id=parent_message_id,
179
260
  )
180
261
  events_to_emit.append(start_event)
181
262
 
@@ -187,7 +268,7 @@ def _create_events_from_chunk(
187
268
  events_to_emit.append(args_event) # type: ignore
188
269
 
189
270
  # Handle tool call completion
190
- elif chunk.event == RunEvent.tool_call_completed:
271
+ elif chunk.event == RunEvent.tool_call_completed or chunk.event == TeamRunEvent.tool_call_completed:
191
272
  if chunk.tool is not None: # type: ignore
192
273
  tool_call = chunk.tool # type: ignore
193
274
  if tool_call.tool_call_id not in event_buffer.ended_tool_call_ids:
@@ -195,7 +276,7 @@ def _create_events_from_chunk(
195
276
  type=EventType.TOOL_CALL_END,
196
277
  tool_call_id=tool_call.tool_call_id, # type: ignore
197
278
  )
198
- events_to_emit.append(end_event) # type: ignore
279
+ events_to_emit.append(end_event)
199
280
 
200
281
  if tool_call.result is not None:
201
282
  result_event = ToolCallResultEvent(
@@ -205,27 +286,34 @@ def _create_events_from_chunk(
205
286
  role="tool",
206
287
  message_id=str(uuid.uuid4()),
207
288
  )
208
- events_to_emit.append(result_event) # type: ignore
209
-
210
- if tool_call.result is not None:
211
- result_event = ToolCallResultEvent(
212
- type=EventType.TOOL_CALL_RESULT,
213
- tool_call_id=tool_call.tool_call_id, # type: ignore
214
- content=str(tool_call.result),
215
- role="tool",
216
- message_id=str(uuid.uuid4()),
217
- )
218
- events_to_emit.append(result_event) # type: ignore
289
+ events_to_emit.append(result_event)
219
290
 
220
291
  # Handle reasoning
221
292
  elif chunk.event == RunEvent.reasoning_started:
222
- step_started_event = StepStartedEvent(type=EventType.STEP_STARTED, step_name="reasoning") # type: ignore
223
- events_to_emit.append(step_started_event) # type: ignore
293
+ step_started_event = StepStartedEvent(type=EventType.STEP_STARTED, step_name="reasoning")
294
+ events_to_emit.append(step_started_event)
224
295
  elif chunk.event == RunEvent.reasoning_completed:
225
- step_started_event = StepFinishedEvent(type=EventType.STEP_FINISHED, step_name="reasoning") # type: ignore
226
- events_to_emit.append(step_started_event) # type: ignore
296
+ step_finished_event = StepFinishedEvent(type=EventType.STEP_FINISHED, step_name="reasoning")
297
+ events_to_emit.append(step_finished_event)
298
+
299
+ # Handle custom events
300
+ elif chunk.event == RunEvent.custom_event:
301
+ # Use the name of the event class if available, otherwise default to the CustomEvent
302
+ try:
303
+ custom_event_name = chunk.__class__.__name__
304
+ except Exception:
305
+ custom_event_name = chunk.event
227
306
 
228
- return events_to_emit, message_started # type: ignore
307
+ # Use the complete Agno event as value if parsing it works, else the event content field
308
+ try:
309
+ custom_event_value = chunk.to_dict()
310
+ except Exception:
311
+ custom_event_value = chunk.content # type: ignore
312
+
313
+ custom_event = CustomEvent(name=custom_event_name, value=custom_event_value)
314
+ events_to_emit.append(custom_event)
315
+
316
+ return events_to_emit, message_started, message_id
229
317
 
230
318
 
231
319
  def _create_completion_events(
@@ -251,37 +339,36 @@ def _create_completion_events(
251
339
  # End the message and run, denoting the end of the session
252
340
  if message_started:
253
341
  end_message_event = TextMessageEndEvent(type=EventType.TEXT_MESSAGE_END, message_id=message_id)
254
- events_to_emit.append(end_message_event) # type: ignore
342
+ events_to_emit.append(end_message_event)
255
343
 
256
344
  # emit frontend tool calls, i.e. external_execution=True
257
345
  if isinstance(chunk, RunPausedEvent) and chunk.tools is not None:
258
- for tool in chunk.tools:
259
- if tool.tool_call_id is None or tool.tool_name is None:
260
- continue
261
-
262
- start_event = ToolCallStartEvent(
263
- type=EventType.TOOL_CALL_START,
264
- tool_call_id=tool.tool_call_id,
265
- tool_call_name=tool.tool_name,
266
- parent_message_id=message_id,
346
+ # First, emit an assistant message for external tool calls
347
+ assistant_message_id = str(uuid.uuid4())
348
+ assistant_start_event = TextMessageStartEvent(
349
+ type=EventType.TEXT_MESSAGE_START,
350
+ message_id=assistant_message_id,
351
+ role="assistant",
352
+ )
353
+ events_to_emit.append(assistant_start_event)
354
+
355
+ # Add any text content if present for the assistant message
356
+ if chunk.content:
357
+ content_event = TextMessageContentEvent(
358
+ type=EventType.TEXT_MESSAGE_CONTENT,
359
+ message_id=assistant_message_id,
360
+ delta=str(chunk.content),
267
361
  )
268
- events_to_emit.append(start_event) # type: ignore
362
+ events_to_emit.append(content_event)
269
363
 
270
- args_event = ToolCallArgsEvent(
271
- type=EventType.TOOL_CALL_ARGS,
272
- tool_call_id=tool.tool_call_id,
273
- delta=json.dumps(tool.tool_args),
274
- )
275
- events_to_emit.append(args_event) # type: ignore
364
+ # End the assistant message
365
+ assistant_end_event = TextMessageEndEvent(
366
+ type=EventType.TEXT_MESSAGE_END,
367
+ message_id=assistant_message_id,
368
+ )
369
+ events_to_emit.append(assistant_end_event)
276
370
 
277
- end_event = ToolCallEndEvent(
278
- type=EventType.TOOL_CALL_END,
279
- tool_call_id=tool.tool_call_id,
280
- )
281
- events_to_emit.append(end_event)
282
-
283
- # emit frontend tool calls, i.e. external_execution=True
284
- if isinstance(chunk, RunPausedEvent) and chunk.tools is not None:
371
+ # Now emit the tool call events with the assistant message as parent
285
372
  for tool in chunk.tools:
286
373
  if tool.tool_call_id is None or tool.tool_name is None:
287
374
  continue
@@ -290,75 +377,42 @@ def _create_completion_events(
290
377
  type=EventType.TOOL_CALL_START,
291
378
  tool_call_id=tool.tool_call_id,
292
379
  tool_call_name=tool.tool_name,
293
- parent_message_id=message_id,
380
+ parent_message_id=assistant_message_id, # Use the assistant message as parent
294
381
  )
295
- events_to_emit.append(start_event) # type: ignore
382
+ events_to_emit.append(start_event)
296
383
 
297
384
  args_event = ToolCallArgsEvent(
298
385
  type=EventType.TOOL_CALL_ARGS,
299
386
  tool_call_id=tool.tool_call_id,
300
387
  delta=json.dumps(tool.tool_args),
301
388
  )
302
- events_to_emit.append(args_event) # type: ignore
389
+ events_to_emit.append(args_event)
303
390
 
304
391
  end_event = ToolCallEndEvent(
305
392
  type=EventType.TOOL_CALL_END,
306
393
  tool_call_id=tool.tool_call_id,
307
394
  )
308
- events_to_emit.append(end_event) # type: ignore
395
+ events_to_emit.append(end_event)
309
396
 
310
397
  run_finished_event = RunFinishedEvent(type=EventType.RUN_FINISHED, thread_id=thread_id, run_id=run_id)
311
- events_to_emit.append(run_finished_event) # type: ignore
398
+ events_to_emit.append(run_finished_event)
312
399
 
313
- return events_to_emit # type: ignore
400
+ return events_to_emit
314
401
 
315
402
 
316
403
  def _emit_event_logic(event: BaseEvent, event_buffer: EventBuffer) -> List[BaseEvent]:
317
- """Process an event through the buffer and return events to actually emit."""
318
- events_to_emit: List[BaseEvent] = []
319
-
320
- if event_buffer.is_blocked():
321
- # Handle events related to the current blocking tool call
322
- if event.type == EventType.TOOL_CALL_ARGS:
323
- if hasattr(event, "tool_call_id") and event.tool_call_id in event_buffer.active_tool_call_ids: # type: ignore
324
- events_to_emit.append(event)
325
- else:
326
- event_buffer.buffer.append(event)
327
- elif event.type == EventType.TOOL_CALL_END:
328
- tool_call_id = getattr(event, "tool_call_id", None)
329
- if tool_call_id and tool_call_id == event_buffer.blocking_tool_call_id:
330
- events_to_emit.append(event)
331
- event_buffer.end_tool_call(tool_call_id)
332
- # Flush buffered events after ending the blocking tool call
333
- while event_buffer.buffer:
334
- buffered_event = event_buffer.buffer.popleft()
335
- # Recursively process buffered events
336
- nested_events = _emit_event_logic(buffered_event, event_buffer)
337
- events_to_emit.extend(nested_events)
338
- elif tool_call_id and tool_call_id in event_buffer.active_tool_call_ids:
339
- event_buffer.buffer.append(event)
340
- event_buffer.end_tool_call(tool_call_id)
341
- else:
342
- event_buffer.buffer.append(event)
343
- # Handle all other events
344
- elif event.type == EventType.TOOL_CALL_START:
345
- event_buffer.buffer.append(event)
346
- else:
347
- event_buffer.buffer.append(event)
348
- # If the buffer is not blocked, emit the events normally
349
- else:
350
- if event.type == EventType.TOOL_CALL_START:
351
- tool_call_id = getattr(event, "tool_call_id", None)
352
- if tool_call_id:
353
- event_buffer.start_tool_call(tool_call_id)
354
- events_to_emit.append(event)
355
- elif event.type == EventType.TOOL_CALL_END:
356
- tool_call_id = getattr(event, "tool_call_id", None)
357
- if tool_call_id:
358
- event_buffer.end_tool_call(tool_call_id)
359
- events_to_emit.append(event)
360
- else:
361
- events_to_emit.append(event)
404
+ """Process an event and return events to actually emit."""
405
+ events_to_emit: List[BaseEvent] = [event]
406
+
407
+ # Update the event buffer state for tracking purposes
408
+ if event.type == EventType.TOOL_CALL_START:
409
+ tool_call_id = getattr(event, "tool_call_id", None)
410
+ if tool_call_id:
411
+ event_buffer.start_tool_call(tool_call_id)
412
+ elif event.type == EventType.TOOL_CALL_END:
413
+ tool_call_id = getattr(event, "tool_call_id", None)
414
+ if tool_call_id:
415
+ event_buffer.end_tool_call(tool_call_id)
362
416
 
363
417
  return events_to_emit
364
418
 
@@ -367,27 +421,26 @@ def stream_agno_response_as_agui_events(
367
421
  response_stream: Iterator[Union[RunOutputEvent, TeamRunOutputEvent]], thread_id: str, run_id: str
368
422
  ) -> Iterator[BaseEvent]:
369
423
  """Map the Agno response stream to AG-UI format, handling event ordering constraints."""
370
- message_id = str(uuid.uuid4())
424
+ message_id = "" # Will be set by EventBuffer when text message starts
371
425
  message_started = False
372
426
  event_buffer = EventBuffer()
427
+ stream_completed = False
428
+
429
+ completion_chunk = None
373
430
 
374
431
  for chunk in response_stream:
375
- # Handle the lifecycle end event
432
+ # Check if this is a completion event
376
433
  if (
377
434
  chunk.event == RunEvent.run_completed
378
435
  or chunk.event == TeamRunEvent.run_completed
379
436
  or chunk.event == RunEvent.run_paused
380
437
  ):
381
- completion_events = _create_completion_events(
382
- chunk, event_buffer, message_started, message_id, thread_id, run_id
383
- )
384
- for event in completion_events:
385
- events_to_emit = _emit_event_logic(event_buffer=event_buffer, event=event)
386
- for emit_event in events_to_emit:
387
- yield emit_event
438
+ # Store completion chunk but don't process it yet
439
+ completion_chunk = chunk
440
+ stream_completed = True
388
441
  else:
389
- # Process regular chunk
390
- events_from_chunk, message_started = _create_events_from_chunk(
442
+ # Process regular chunk immediately
443
+ events_from_chunk, message_started, message_id = _create_events_from_chunk(
391
444
  chunk, message_id, message_started, event_buffer
392
445
  )
393
446
 
@@ -396,6 +449,30 @@ def stream_agno_response_as_agui_events(
396
449
  for emit_event in events_to_emit:
397
450
  yield emit_event
398
451
 
452
+ # Process ONLY completion cleanup events, not content from completion chunk
453
+ if completion_chunk:
454
+ completion_events = _create_completion_events(
455
+ completion_chunk, event_buffer, message_started, message_id, thread_id, run_id
456
+ )
457
+ for event in completion_events:
458
+ events_to_emit = _emit_event_logic(event_buffer=event_buffer, event=event)
459
+ for emit_event in events_to_emit:
460
+ yield emit_event
461
+
462
+ # Ensure completion events are always emitted even when stream ends naturally
463
+ if not stream_completed:
464
+ # Create a synthetic completion event to ensure proper cleanup
465
+ from agno.run.agent import RunCompletedEvent
466
+
467
+ synthetic_completion = RunCompletedEvent()
468
+ completion_events = _create_completion_events(
469
+ synthetic_completion, event_buffer, message_started, message_id, thread_id, run_id
470
+ )
471
+ for event in completion_events:
472
+ events_to_emit = _emit_event_logic(event_buffer=event_buffer, event=event)
473
+ for emit_event in events_to_emit:
474
+ yield emit_event
475
+
399
476
 
400
477
  # Async version - thin wrapper
401
478
  async def async_stream_agno_response_as_agui_events(
@@ -404,27 +481,26 @@ async def async_stream_agno_response_as_agui_events(
404
481
  run_id: str,
405
482
  ) -> AsyncIterator[BaseEvent]:
406
483
  """Map the Agno response stream to AG-UI format, handling event ordering constraints."""
407
- message_id = str(uuid.uuid4())
484
+ message_id = "" # Will be set by EventBuffer when text message starts
408
485
  message_started = False
409
486
  event_buffer = EventBuffer()
487
+ stream_completed = False
488
+
489
+ completion_chunk = None
410
490
 
411
491
  async for chunk in response_stream:
412
- # Handle the lifecycle end event
492
+ # Check if this is a completion event
413
493
  if (
414
494
  chunk.event == RunEvent.run_completed
415
495
  or chunk.event == TeamRunEvent.run_completed
416
496
  or chunk.event == RunEvent.run_paused
417
497
  ):
418
- completion_events = _create_completion_events(
419
- chunk, event_buffer, message_started, message_id, thread_id, run_id
420
- )
421
- for event in completion_events:
422
- events_to_emit = _emit_event_logic(event_buffer=event_buffer, event=event)
423
- for emit_event in events_to_emit:
424
- yield emit_event
498
+ # Store completion chunk but don't process it yet
499
+ completion_chunk = chunk
500
+ stream_completed = True
425
501
  else:
426
- # Process regular chunk
427
- events_from_chunk, message_started = _create_events_from_chunk(
502
+ # Process regular chunk immediately
503
+ events_from_chunk, message_started, message_id = _create_events_from_chunk(
428
504
  chunk, message_id, message_started, event_buffer
429
505
  )
430
506
 
@@ -432,3 +508,27 @@ async def async_stream_agno_response_as_agui_events(
432
508
  events_to_emit = _emit_event_logic(event_buffer=event_buffer, event=event)
433
509
  for emit_event in events_to_emit:
434
510
  yield emit_event
511
+
512
+ # Process ONLY completion cleanup events, not content from completion chunk
513
+ if completion_chunk:
514
+ completion_events = _create_completion_events(
515
+ completion_chunk, event_buffer, message_started, message_id, thread_id, run_id
516
+ )
517
+ for event in completion_events:
518
+ events_to_emit = _emit_event_logic(event_buffer=event_buffer, event=event)
519
+ for emit_event in events_to_emit:
520
+ yield emit_event
521
+
522
+ # Ensure completion events are always emitted even when stream ends naturally
523
+ if not stream_completed:
524
+ # Create a synthetic completion event to ensure proper cleanup
525
+ from agno.run.agent import RunCompletedEvent
526
+
527
+ synthetic_completion = RunCompletedEvent()
528
+ completion_events = _create_completion_events(
529
+ synthetic_completion, event_buffer, message_started, message_id, thread_id, run_id
530
+ )
531
+ for event in completion_events:
532
+ events_to_emit = _emit_event_logic(event_buffer=event_buffer, event=event)
533
+ for emit_event in events_to_emit:
534
+ yield emit_event