agno 2.0.1__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.
- agno/agent/agent.py +6015 -2823
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +385 -6
- agno/db/dynamo/dynamo.py +388 -81
- agno/db/dynamo/schemas.py +47 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +435 -64
- agno/db/firestore/schemas.py +11 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +384 -42
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +351 -66
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +339 -48
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +510 -37
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2036 -0
- agno/db/mongo/mongo.py +653 -76
- agno/db/mongo/schemas.py +13 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/mysql.py +687 -25
- agno/db/mysql/schemas.py +61 -37
- agno/db/mysql/utils.py +60 -2
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2001 -0
- agno/db/postgres/postgres.py +676 -57
- agno/db/postgres/schemas.py +43 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +344 -38
- agno/db/redis/schemas.py +18 -0
- agno/db/redis/utils.py +60 -2
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/memory.py +13 -0
- agno/db/singlestore/schemas.py +26 -1
- agno/db/singlestore/singlestore.py +687 -53
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2371 -0
- agno/db/sqlite/schemas.py +24 -0
- agno/db/sqlite/sqlite.py +774 -85
- agno/db/sqlite/utils.py +168 -5
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +309 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1361 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +50 -22
- agno/eval/accuracy.py +50 -43
- agno/eval/performance.py +6 -3
- agno/eval/reliability.py +6 -3
- agno/eval/utils.py +33 -16
- agno/exceptions.py +68 -1
- agno/filters.py +354 -0
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +52 -0
- agno/integrations/discord/client.py +1 -0
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +1 -1
- agno/knowledge/chunking/semantic.py +40 -8
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/aws_bedrock.py +9 -4
- agno/knowledge/embedder/azure_openai.py +54 -0
- agno/knowledge/embedder/base.py +2 -0
- agno/knowledge/embedder/cohere.py +184 -5
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/google.py +79 -1
- agno/knowledge/embedder/huggingface.py +9 -4
- agno/knowledge/embedder/jina.py +63 -0
- agno/knowledge/embedder/mistral.py +78 -11
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +13 -0
- agno/knowledge/embedder/openai.py +37 -65
- agno/knowledge/embedder/sentence_transformer.py +8 -4
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/embedder/voyageai.py +69 -16
- agno/knowledge/knowledge.py +594 -186
- agno/knowledge/reader/base.py +9 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
- agno/knowledge/reader/json_reader.py +6 -5
- agno/knowledge/reader/markdown_reader.py +13 -13
- agno/knowledge/reader/pdf_reader.py +43 -68
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +51 -6
- agno/knowledge/reader/s3_reader.py +3 -15
- agno/knowledge/reader/tavily_reader.py +194 -0
- agno/knowledge/reader/text_reader.py +13 -13
- agno/knowledge/reader/web_search_reader.py +2 -43
- agno/knowledge/reader/website_reader.py +43 -25
- agno/knowledge/reranker/__init__.py +2 -8
- agno/knowledge/types.py +9 -0
- agno/knowledge/utils.py +20 -0
- agno/media.py +72 -0
- agno/memory/manager.py +336 -82
- agno/models/aimlapi/aimlapi.py +2 -2
- agno/models/anthropic/claude.py +183 -37
- agno/models/aws/bedrock.py +52 -112
- agno/models/aws/claude.py +33 -1
- agno/models/azure/ai_foundry.py +33 -15
- agno/models/azure/openai_chat.py +25 -8
- agno/models/base.py +999 -519
- agno/models/cerebras/cerebras.py +19 -13
- agno/models/cerebras/cerebras_openai.py +8 -5
- agno/models/cohere/chat.py +27 -1
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +57 -0
- agno/models/dashscope/dashscope.py +1 -0
- agno/models/deepinfra/deepinfra.py +2 -2
- agno/models/deepseek/deepseek.py +2 -2
- agno/models/fireworks/fireworks.py +2 -2
- agno/models/google/gemini.py +103 -31
- agno/models/groq/groq.py +28 -11
- agno/models/huggingface/huggingface.py +2 -1
- agno/models/internlm/internlm.py +2 -2
- agno/models/langdb/langdb.py +4 -4
- agno/models/litellm/chat.py +18 -1
- agno/models/litellm/litellm_openai.py +2 -2
- agno/models/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- agno/models/message.py +139 -0
- agno/models/meta/llama.py +27 -10
- agno/models/meta/llama_openai.py +5 -17
- agno/models/nebius/nebius.py +6 -6
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +22 -0
- agno/models/nvidia/nvidia.py +2 -2
- agno/models/ollama/chat.py +59 -5
- agno/models/openai/chat.py +69 -29
- agno/models/openai/responses.py +103 -106
- agno/models/openrouter/openrouter.py +41 -3
- agno/models/perplexity/perplexity.py +4 -5
- agno/models/portkey/portkey.py +3 -3
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +52 -0
- agno/models/response.py +77 -1
- agno/models/sambanova/sambanova.py +2 -2
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +25 -0
- agno/models/together/together.py +2 -2
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +2 -2
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +96 -0
- agno/models/vllm/vllm.py +1 -0
- agno/models/xai/xai.py +3 -2
- agno/os/app.py +543 -178
- agno/os/auth.py +24 -14
- agno/os/config.py +1 -0
- agno/os/interfaces/__init__.py +1 -0
- agno/os/interfaces/a2a/__init__.py +3 -0
- agno/os/interfaces/a2a/a2a.py +42 -0
- agno/os/interfaces/a2a/router.py +250 -0
- agno/os/interfaces/a2a/utils.py +924 -0
- agno/os/interfaces/agui/agui.py +23 -7
- agno/os/interfaces/agui/router.py +27 -3
- agno/os/interfaces/agui/utils.py +242 -142
- agno/os/interfaces/base.py +6 -2
- agno/os/interfaces/slack/router.py +81 -23
- agno/os/interfaces/slack/slack.py +29 -14
- agno/os/interfaces/whatsapp/router.py +11 -4
- agno/os/interfaces/whatsapp/whatsapp.py +14 -7
- agno/os/mcp.py +111 -54
- agno/os/middleware/__init__.py +7 -0
- agno/os/middleware/jwt.py +233 -0
- agno/os/router.py +556 -139
- agno/os/routers/evals/evals.py +71 -34
- agno/os/routers/evals/schemas.py +31 -31
- agno/os/routers/evals/utils.py +6 -5
- agno/os/routers/health.py +31 -0
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/knowledge.py +185 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +158 -53
- agno/os/routers/memory/schemas.py +20 -16
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +499 -38
- agno/os/schema.py +308 -198
- agno/os/utils.py +401 -41
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/azure_ai_foundry.py +2 -2
- agno/reasoning/deepseek.py +2 -2
- agno/reasoning/default.py +3 -1
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/groq.py +2 -2
- agno/reasoning/ollama.py +2 -2
- agno/reasoning/openai.py +7 -2
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +248 -94
- agno/run/base.py +44 -5
- agno/run/team.py +238 -97
- agno/run/workflow.py +144 -33
- agno/session/agent.py +105 -89
- agno/session/summary.py +65 -25
- agno/session/team.py +176 -96
- agno/session/workflow.py +406 -40
- agno/team/team.py +3854 -1610
- agno/tools/dalle.py +2 -4
- agno/tools/decorator.py +4 -2
- agno/tools/duckduckgo.py +15 -11
- agno/tools/e2b.py +14 -7
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +350 -0
- agno/tools/firecrawl.py +4 -4
- agno/tools/function.py +250 -30
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +270 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/knowledge.py +3 -3
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/mem0.py +11 -17
- agno/tools/memori.py +1 -53
- agno/tools/memory.py +419 -0
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/scrapegraph.py +58 -31
- agno/tools/searxng.py +2 -2
- agno/tools/serper.py +2 -2
- agno/tools/slack.py +18 -3
- agno/tools/spider.py +2 -2
- agno/tools/tavily.py +146 -0
- agno/tools/whatsapp.py +1 -1
- agno/tools/workflow.py +278 -0
- agno/tools/yfinance.py +12 -11
- agno/utils/agent.py +820 -0
- agno/utils/audio.py +27 -0
- agno/utils/common.py +90 -1
- agno/utils/events.py +217 -2
- agno/utils/gemini.py +180 -22
- agno/utils/hooks.py +57 -0
- agno/utils/http.py +111 -0
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +188 -10
- agno/utils/merge_dict.py +22 -1
- agno/utils/message.py +60 -0
- agno/utils/models/claude.py +40 -11
- agno/utils/print_response/agent.py +105 -21
- agno/utils/print_response/team.py +103 -38
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/reasoning.py +22 -1
- agno/utils/serialize.py +32 -0
- agno/utils/streamlit.py +16 -10
- agno/utils/string.py +41 -0
- agno/utils/team.py +98 -9
- agno/utils/tools.py +1 -1
- agno/vectordb/base.py +23 -4
- agno/vectordb/cassandra/cassandra.py +65 -9
- agno/vectordb/chroma/chromadb.py +182 -38
- agno/vectordb/clickhouse/clickhousedb.py +64 -11
- agno/vectordb/couchbase/couchbase.py +105 -10
- agno/vectordb/lancedb/lance_db.py +124 -133
- agno/vectordb/langchaindb/langchaindb.py +25 -7
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/__init__.py +3 -0
- agno/vectordb/llamaindex/llamaindexdb.py +46 -7
- agno/vectordb/milvus/milvus.py +126 -9
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +112 -7
- agno/vectordb/pgvector/pgvector.py +142 -21
- agno/vectordb/pineconedb/pineconedb.py +80 -8
- agno/vectordb/qdrant/qdrant.py +125 -39
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +694 -0
- agno/vectordb/singlestore/singlestore.py +111 -25
- agno/vectordb/surrealdb/surrealdb.py +31 -5
- agno/vectordb/upstashdb/upstashdb.py +76 -8
- agno/vectordb/weaviate/weaviate.py +86 -15
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +112 -18
- agno/workflow/loop.py +69 -10
- agno/workflow/parallel.py +266 -118
- agno/workflow/router.py +110 -17
- agno/workflow/step.py +638 -129
- agno/workflow/steps.py +65 -6
- agno/workflow/types.py +61 -23
- agno/workflow/workflow.py +2085 -272
- {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/METADATA +182 -58
- agno-2.3.0.dist-info/RECORD +577 -0
- agno/knowledge/reader/url_reader.py +0 -128
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -610
- agno/utils/models/aws_claude.py +0 -170
- agno-2.0.1.dist-info/RECORD +0 -515
- {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
- {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
agno/os/interfaces/agui/agui.py
CHANGED
|
@@ -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__(
|
|
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
|
|
23
|
-
raise ValueError("AGUI requires an agent
|
|
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
|
|
26
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
agno/os/interfaces/agui/utils.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
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) ->
|
|
59
|
-
"""End a tool call
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
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)
|
|
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)
|
|
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")
|
|
223
|
-
events_to_emit.append(step_started_event)
|
|
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
|
-
|
|
226
|
-
events_to_emit.append(
|
|
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
|
-
|
|
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)
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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(
|
|
362
|
+
events_to_emit.append(content_event)
|
|
269
363
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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=
|
|
380
|
+
parent_message_id=assistant_message_id, # Use the assistant message as parent
|
|
294
381
|
)
|
|
295
|
-
events_to_emit.append(start_event)
|
|
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)
|
|
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)
|
|
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)
|
|
398
|
+
events_to_emit.append(run_finished_event)
|
|
312
399
|
|
|
313
|
-
return events_to_emit
|
|
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
|
|
318
|
-
events_to_emit: List[BaseEvent] = []
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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 =
|
|
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
|
-
#
|
|
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
|
-
|
|
382
|
-
|
|
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 =
|
|
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
|
-
#
|
|
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
|
-
|
|
419
|
-
|
|
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
|