agno 2.0.4__py3-none-any.whl → 2.0.6__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 +127 -102
- agno/db/dynamo/dynamo.py +9 -7
- agno/db/firestore/firestore.py +7 -4
- agno/db/gcs_json/gcs_json_db.py +6 -4
- agno/db/json/json_db.py +10 -6
- agno/db/migrations/v1_to_v2.py +191 -23
- agno/db/mongo/mongo.py +67 -6
- agno/db/mysql/mysql.py +7 -6
- agno/db/mysql/schemas.py +27 -27
- agno/db/postgres/postgres.py +7 -6
- agno/db/redis/redis.py +3 -3
- agno/db/singlestore/singlestore.py +4 -4
- agno/db/sqlite/sqlite.py +7 -6
- agno/db/utils.py +0 -14
- agno/integrations/discord/client.py +1 -0
- agno/knowledge/embedder/openai.py +19 -11
- agno/knowledge/knowledge.py +11 -10
- agno/knowledge/reader/reader_factory.py +7 -3
- agno/knowledge/reader/web_search_reader.py +12 -6
- agno/knowledge/reader/website_reader.py +33 -16
- agno/media.py +70 -0
- agno/models/aimlapi/aimlapi.py +2 -2
- agno/models/base.py +31 -4
- agno/models/cerebras/cerebras_openai.py +2 -2
- agno/models/deepinfra/deepinfra.py +2 -2
- agno/models/deepseek/deepseek.py +2 -2
- agno/models/fireworks/fireworks.py +2 -2
- agno/models/internlm/internlm.py +2 -2
- agno/models/langdb/langdb.py +4 -4
- agno/models/litellm/litellm_openai.py +2 -2
- agno/models/message.py +135 -0
- agno/models/meta/llama_openai.py +2 -2
- agno/models/nebius/nebius.py +2 -2
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +25 -0
- agno/models/nvidia/nvidia.py +2 -2
- agno/models/openai/responses.py +6 -0
- agno/models/openrouter/openrouter.py +2 -2
- agno/models/perplexity/perplexity.py +2 -2
- agno/models/portkey/portkey.py +3 -3
- agno/models/response.py +2 -1
- agno/models/sambanova/sambanova.py +2 -2
- agno/models/together/together.py +2 -2
- agno/models/vercel/v0.py +2 -2
- agno/models/xai/xai.py +2 -2
- agno/os/app.py +162 -42
- agno/os/interfaces/agui/utils.py +98 -134
- agno/os/router.py +3 -1
- agno/os/routers/health.py +0 -1
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/knowledge.py +2 -2
- agno/os/schema.py +21 -0
- agno/os/utils.py +1 -9
- agno/run/agent.py +19 -3
- agno/run/team.py +18 -3
- agno/run/workflow.py +10 -0
- agno/team/team.py +70 -45
- agno/tools/duckduckgo.py +15 -11
- agno/tools/e2b.py +14 -7
- agno/tools/file_generation.py +350 -0
- agno/tools/function.py +2 -0
- agno/tools/googlesearch.py +1 -1
- agno/utils/gemini.py +24 -4
- agno/utils/string.py +32 -0
- agno/utils/tools.py +1 -1
- agno/vectordb/chroma/chromadb.py +66 -25
- agno/vectordb/lancedb/lance_db.py +15 -4
- agno/vectordb/milvus/milvus.py +6 -0
- agno/workflow/step.py +4 -3
- agno/workflow/workflow.py +4 -0
- {agno-2.0.4.dist-info → agno-2.0.6.dist-info}/METADATA +9 -5
- {agno-2.0.4.dist-info → agno-2.0.6.dist-info}/RECORD +75 -72
- agno/knowledge/reader/url_reader.py +0 -128
- {agno-2.0.4.dist-info → agno-2.0.6.dist-info}/WHEEL +0 -0
- {agno-2.0.4.dist-info → agno-2.0.6.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.4.dist-info → agno-2.0.6.dist-info}/top_level.txt +0 -0
agno/os/app.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from contextlib import asynccontextmanager
|
|
2
2
|
from functools import partial
|
|
3
3
|
from os import getenv
|
|
4
|
-
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
|
5
5
|
from uuid import uuid4
|
|
6
6
|
|
|
7
|
-
from fastapi import FastAPI, HTTPException
|
|
7
|
+
from fastapi import APIRouter, FastAPI, HTTPException
|
|
8
8
|
from fastapi.responses import JSONResponse
|
|
9
|
+
from fastapi.routing import APIRoute
|
|
9
10
|
from rich import box
|
|
10
11
|
from rich.panel import Panel
|
|
11
12
|
from starlette.middleware.cors import CORSMiddleware
|
|
@@ -30,13 +31,15 @@ from agno.os.interfaces.base import BaseInterface
|
|
|
30
31
|
from agno.os.router import get_base_router, get_websocket_router
|
|
31
32
|
from agno.os.routers.evals import get_eval_router
|
|
32
33
|
from agno.os.routers.health import get_health_router
|
|
34
|
+
from agno.os.routers.home import get_home_router
|
|
33
35
|
from agno.os.routers.knowledge import get_knowledge_router
|
|
34
36
|
from agno.os.routers.memory import get_memory_router
|
|
35
37
|
from agno.os.routers.metrics import get_metrics_router
|
|
36
38
|
from agno.os.routers.session import get_session_router
|
|
37
39
|
from agno.os.settings import AgnoAPISettings
|
|
38
|
-
from agno.os.utils import generate_id
|
|
39
40
|
from agno.team.team import Team
|
|
41
|
+
from agno.utils.log import logger
|
|
42
|
+
from agno.utils.string import generate_id, generate_id_from_name
|
|
40
43
|
from agno.workflow.workflow import Workflow
|
|
41
44
|
|
|
42
45
|
|
|
@@ -70,8 +73,30 @@ class AgentOS:
|
|
|
70
73
|
fastapi_app: Optional[FastAPI] = None,
|
|
71
74
|
lifespan: Optional[Any] = None,
|
|
72
75
|
enable_mcp: bool = False,
|
|
76
|
+
replace_routes: bool = True,
|
|
73
77
|
telemetry: bool = True,
|
|
74
78
|
):
|
|
79
|
+
"""Initialize AgentOS.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
os_id: Unique identifier for this AgentOS instance
|
|
83
|
+
name: Name of the AgentOS instance
|
|
84
|
+
description: Description of the AgentOS instance
|
|
85
|
+
version: Version of the AgentOS instance
|
|
86
|
+
agents: List of agents to include in the OS
|
|
87
|
+
teams: List of teams to include in the OS
|
|
88
|
+
workflows: List of workflows to include in the OS
|
|
89
|
+
interfaces: List of interfaces to include in the OS
|
|
90
|
+
config: Configuration file path or AgentOSConfig instance
|
|
91
|
+
settings: API settings for the OS
|
|
92
|
+
fastapi_app: Optional custom FastAPI app to use instead of creating a new one
|
|
93
|
+
lifespan: Optional lifespan context manager for the FastAPI app
|
|
94
|
+
enable_mcp: Whether to enable MCP (Model Context Protocol)
|
|
95
|
+
replace_routes: If False and using a custom fastapi_app, skip AgentOS routes that
|
|
96
|
+
conflict with existing routes, preferring the user's custom routes.
|
|
97
|
+
If True (default), AgentOS routes will override conflicting custom routes.
|
|
98
|
+
telemetry: Whether to enable telemetry
|
|
99
|
+
"""
|
|
75
100
|
if not agents and not workflows and not teams:
|
|
76
101
|
raise ValueError("Either agents, teams or workflows must be provided.")
|
|
77
102
|
|
|
@@ -92,11 +117,13 @@ class AgentOS:
|
|
|
92
117
|
|
|
93
118
|
self.interfaces = interfaces or []
|
|
94
119
|
|
|
95
|
-
self.os_id
|
|
120
|
+
self.os_id = os_id
|
|
96
121
|
self.name = name
|
|
97
122
|
self.version = version
|
|
98
123
|
self.description = description
|
|
99
124
|
|
|
125
|
+
self.replace_routes = replace_routes
|
|
126
|
+
|
|
100
127
|
self.telemetry = telemetry
|
|
101
128
|
|
|
102
129
|
self.enable_mcp = enable_mcp
|
|
@@ -146,7 +173,10 @@ class AgentOS:
|
|
|
146
173
|
for workflow in self.workflows:
|
|
147
174
|
# TODO: track MCP tools in workflow members
|
|
148
175
|
if not workflow.id:
|
|
149
|
-
workflow.id =
|
|
176
|
+
workflow.id = generate_id_from_name(workflow.name)
|
|
177
|
+
|
|
178
|
+
if not self.os_id:
|
|
179
|
+
self.os_id = generate_id(self.name) if self.name else str(uuid4())
|
|
150
180
|
|
|
151
181
|
if self.telemetry:
|
|
152
182
|
from agno.api.os import OSLaunch, log_os_telemetry
|
|
@@ -207,18 +237,29 @@ class AgentOS:
|
|
|
207
237
|
else:
|
|
208
238
|
self.fastapi_app = self._make_app(lifespan=self.lifespan)
|
|
209
239
|
|
|
210
|
-
# Add routes
|
|
211
|
-
self.
|
|
212
|
-
self.
|
|
213
|
-
self.
|
|
240
|
+
# Add routes with conflict detection
|
|
241
|
+
self._add_router(get_base_router(self, settings=self.settings))
|
|
242
|
+
self._add_router(get_websocket_router(self, settings=self.settings))
|
|
243
|
+
self._add_router(get_health_router())
|
|
244
|
+
self._add_router(get_home_router(self))
|
|
214
245
|
|
|
215
246
|
for interface in self.interfaces:
|
|
216
247
|
interface_router = interface.get_router()
|
|
217
|
-
self.
|
|
248
|
+
self._add_router(interface_router)
|
|
218
249
|
|
|
219
250
|
self._auto_discover_databases()
|
|
220
251
|
self._auto_discover_knowledge_instances()
|
|
221
|
-
|
|
252
|
+
|
|
253
|
+
routers = [
|
|
254
|
+
get_session_router(dbs=self.dbs),
|
|
255
|
+
get_memory_router(dbs=self.dbs),
|
|
256
|
+
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
257
|
+
get_metrics_router(dbs=self.dbs),
|
|
258
|
+
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
for router in routers:
|
|
262
|
+
self._add_router(router)
|
|
222
263
|
|
|
223
264
|
# Mount MCP if needed
|
|
224
265
|
if self.enable_mcp and self.mcp_app:
|
|
@@ -266,6 +307,94 @@ class AgentOS:
|
|
|
266
307
|
|
|
267
308
|
return app.routes
|
|
268
309
|
|
|
310
|
+
def _get_existing_route_paths(self) -> Dict[str, List[str]]:
|
|
311
|
+
"""Get all existing route paths and methods from the FastAPI app.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Dict[str, List[str]]: Dictionary mapping paths to list of HTTP methods
|
|
315
|
+
"""
|
|
316
|
+
if not self.fastapi_app:
|
|
317
|
+
return {}
|
|
318
|
+
|
|
319
|
+
existing_paths: Dict[str, Any] = {}
|
|
320
|
+
for route in self.fastapi_app.routes:
|
|
321
|
+
if isinstance(route, APIRoute):
|
|
322
|
+
path = route.path
|
|
323
|
+
methods = list(route.methods) if route.methods else []
|
|
324
|
+
if path in existing_paths:
|
|
325
|
+
existing_paths[path].extend(methods)
|
|
326
|
+
else:
|
|
327
|
+
existing_paths[path] = methods
|
|
328
|
+
return existing_paths
|
|
329
|
+
|
|
330
|
+
def _add_router(self, router: APIRouter) -> None:
|
|
331
|
+
"""Add a router to the FastAPI app, avoiding route conflicts.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
router: The APIRouter to add
|
|
335
|
+
"""
|
|
336
|
+
if not self.fastapi_app:
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
# Get existing routes
|
|
340
|
+
existing_paths = self._get_existing_route_paths()
|
|
341
|
+
|
|
342
|
+
# Check for conflicts
|
|
343
|
+
conflicts = []
|
|
344
|
+
conflicting_routes = []
|
|
345
|
+
|
|
346
|
+
for route in router.routes:
|
|
347
|
+
if isinstance(route, APIRoute):
|
|
348
|
+
full_path = route.path
|
|
349
|
+
route_methods = list(route.methods) if route.methods else []
|
|
350
|
+
|
|
351
|
+
if full_path in existing_paths:
|
|
352
|
+
conflicting_methods: Set[str] = set(route_methods) & set(existing_paths[full_path])
|
|
353
|
+
if conflicting_methods:
|
|
354
|
+
conflicts.append({"path": full_path, "methods": list(conflicting_methods), "route": route})
|
|
355
|
+
conflicting_routes.append(route)
|
|
356
|
+
|
|
357
|
+
if conflicts and self._app_set:
|
|
358
|
+
if self.replace_routes:
|
|
359
|
+
# Log warnings but still add all routes (AgentOS routes will override)
|
|
360
|
+
for conflict in conflicts:
|
|
361
|
+
methods_str = ", ".join(conflict["methods"]) # type: ignore
|
|
362
|
+
logger.warning(
|
|
363
|
+
f"Route conflict detected: {methods_str} {conflict['path']} - "
|
|
364
|
+
f"AgentOS route will override existing custom route"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Remove conflicting routes
|
|
368
|
+
for route in self.fastapi_app.routes:
|
|
369
|
+
for conflict in conflicts:
|
|
370
|
+
if isinstance(route, APIRoute):
|
|
371
|
+
if route.path == conflict["path"] and list(route.methods) == list(conflict["methods"]):
|
|
372
|
+
self.fastapi_app.routes.pop(self.fastapi_app.routes.index(route))
|
|
373
|
+
|
|
374
|
+
self.fastapi_app.include_router(router)
|
|
375
|
+
|
|
376
|
+
else:
|
|
377
|
+
# Skip conflicting AgentOS routes, prefer user's existing routes
|
|
378
|
+
for conflict in conflicts:
|
|
379
|
+
methods_str = ", ".join(conflict["methods"]) # type: ignore
|
|
380
|
+
logger.debug(
|
|
381
|
+
f"Skipping conflicting AgentOS route: {methods_str} {conflict['path']} - "
|
|
382
|
+
f"Using existing custom route instead"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Create a new router without the conflicting routes
|
|
386
|
+
filtered_router = APIRouter()
|
|
387
|
+
for route in router.routes:
|
|
388
|
+
if route not in conflicting_routes:
|
|
389
|
+
filtered_router.routes.append(route)
|
|
390
|
+
|
|
391
|
+
# Use the filtered router if it has any routes left
|
|
392
|
+
if filtered_router.routes:
|
|
393
|
+
self.fastapi_app.include_router(filtered_router)
|
|
394
|
+
else:
|
|
395
|
+
# No conflicts, add router normally
|
|
396
|
+
self.fastapi_app.include_router(router)
|
|
397
|
+
|
|
269
398
|
def _get_telemetry_data(self) -> Dict[str, Any]:
|
|
270
399
|
"""Get the telemetry data for the OS"""
|
|
271
400
|
return {
|
|
@@ -293,18 +422,25 @@ class AgentOS:
|
|
|
293
422
|
def _auto_discover_databases(self) -> None:
|
|
294
423
|
"""Auto-discover the databases used by all contextual agents, teams and workflows."""
|
|
295
424
|
dbs = {}
|
|
425
|
+
knowledge_dbs = {} # Track databases specifically used for knowledge
|
|
296
426
|
|
|
297
427
|
for agent in self.agents or []:
|
|
298
428
|
if agent.db:
|
|
299
429
|
dbs[agent.db.id] = agent.db
|
|
300
430
|
if agent.knowledge and agent.knowledge.contents_db:
|
|
301
|
-
|
|
431
|
+
knowledge_dbs[agent.knowledge.contents_db.id] = agent.knowledge.contents_db
|
|
432
|
+
# Also add to general dbs if it's used for both purposes
|
|
433
|
+
if agent.knowledge.contents_db.id not in dbs:
|
|
434
|
+
dbs[agent.knowledge.contents_db.id] = agent.knowledge.contents_db
|
|
302
435
|
|
|
303
436
|
for team in self.teams or []:
|
|
304
437
|
if team.db:
|
|
305
438
|
dbs[team.db.id] = team.db
|
|
306
439
|
if team.knowledge and team.knowledge.contents_db:
|
|
307
|
-
|
|
440
|
+
knowledge_dbs[team.knowledge.contents_db.id] = team.knowledge.contents_db
|
|
441
|
+
# Also add to general dbs if it's used for both purposes
|
|
442
|
+
if team.knowledge.contents_db.id not in dbs:
|
|
443
|
+
dbs[team.knowledge.contents_db.id] = team.knowledge.contents_db
|
|
308
444
|
|
|
309
445
|
for workflow in self.workflows or []:
|
|
310
446
|
if workflow.db:
|
|
@@ -317,6 +453,7 @@ class AgentOS:
|
|
|
317
453
|
dbs[interface.team.db.id] = interface.team.db
|
|
318
454
|
|
|
319
455
|
self.dbs = dbs
|
|
456
|
+
self.knowledge_dbs = knowledge_dbs
|
|
320
457
|
|
|
321
458
|
def _auto_discover_knowledge_instances(self) -> None:
|
|
322
459
|
"""Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
|
|
@@ -381,16 +518,17 @@ class AgentOS:
|
|
|
381
518
|
if knowledge_config.dbs is None:
|
|
382
519
|
knowledge_config.dbs = []
|
|
383
520
|
|
|
384
|
-
|
|
521
|
+
multiple_knowledge_dbs: bool = len(self.knowledge_dbs.keys()) > 1
|
|
385
522
|
dbs_with_specific_config = [db.db_id for db in knowledge_config.dbs]
|
|
386
523
|
|
|
387
|
-
for
|
|
524
|
+
# Only add databases that are actually used for knowledge contents
|
|
525
|
+
for db_id in self.knowledge_dbs.keys():
|
|
388
526
|
if db_id not in dbs_with_specific_config:
|
|
389
527
|
knowledge_config.dbs.append(
|
|
390
528
|
DatabaseConfig(
|
|
391
529
|
db_id=db_id,
|
|
392
530
|
domain_config=KnowledgeDomainConfig(
|
|
393
|
-
display_name="Knowledge" if not
|
|
531
|
+
display_name="Knowledge" if not multiple_knowledge_dbs else "Knowledge in database " + db_id
|
|
394
532
|
),
|
|
395
533
|
)
|
|
396
534
|
)
|
|
@@ -441,29 +579,6 @@ class AgentOS:
|
|
|
441
579
|
|
|
442
580
|
return evals_config
|
|
443
581
|
|
|
444
|
-
def _setup_routers(self) -> None:
|
|
445
|
-
"""Add all routers to the FastAPI app."""
|
|
446
|
-
if not self.dbs or not self.fastapi_app:
|
|
447
|
-
return
|
|
448
|
-
|
|
449
|
-
routers = [
|
|
450
|
-
get_session_router(dbs=self.dbs),
|
|
451
|
-
get_memory_router(dbs=self.dbs),
|
|
452
|
-
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
453
|
-
get_metrics_router(dbs=self.dbs),
|
|
454
|
-
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
455
|
-
]
|
|
456
|
-
|
|
457
|
-
for router in routers:
|
|
458
|
-
self.fastapi_app.include_router(router)
|
|
459
|
-
|
|
460
|
-
def set_os_id(self) -> str:
|
|
461
|
-
# If os_id is already set, keep it instead of overriding with UUID
|
|
462
|
-
if self.os_id is None:
|
|
463
|
-
self.os_id = str(uuid4())
|
|
464
|
-
|
|
465
|
-
return self.os_id
|
|
466
|
-
|
|
467
582
|
def serve(
|
|
468
583
|
self,
|
|
469
584
|
app: Union[str, FastAPI],
|
|
@@ -485,13 +600,18 @@ class AgentOS:
|
|
|
485
600
|
from rich.align import Align
|
|
486
601
|
from rich.console import Console, Group
|
|
487
602
|
|
|
488
|
-
|
|
489
|
-
|
|
603
|
+
panel_group = []
|
|
604
|
+
panel_group.append(Align.center(f"[bold cyan]{public_endpoint}[/bold cyan]"))
|
|
605
|
+
panel_group.append(
|
|
606
|
+
Align.center(f"\n\n[bold dark_orange]OS running on:[/bold dark_orange] http://{host}:{port}")
|
|
607
|
+
)
|
|
608
|
+
if bool(self.settings.os_security_key):
|
|
609
|
+
panel_group.append(Align.center("\n\n[bold chartreuse3]:lock: Security Enabled[/bold chartreuse3]"))
|
|
490
610
|
|
|
491
611
|
console = Console()
|
|
492
612
|
console.print(
|
|
493
613
|
Panel(
|
|
494
|
-
Group(
|
|
614
|
+
Group(*panel_group),
|
|
495
615
|
title="AgentOS",
|
|
496
616
|
expand=False,
|
|
497
617
|
border_style="dark_orange",
|
agno/os/interfaces/agui/utils.py
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import uuid
|
|
5
|
-
from collections import deque
|
|
6
5
|
from collections.abc import Iterator
|
|
7
6
|
from dataclasses import dataclass
|
|
8
|
-
from typing import AsyncIterator,
|
|
7
|
+
from typing import AsyncIterator, List, Set, Tuple, Union
|
|
9
8
|
|
|
10
9
|
from ag_ui.core import (
|
|
11
10
|
BaseEvent,
|
|
@@ -34,39 +33,22 @@ from agno.utils.message import get_text_from_message
|
|
|
34
33
|
class EventBuffer:
|
|
35
34
|
"""Buffer to manage event ordering constraints, relevant when mapping Agno responses to AG-UI events."""
|
|
36
35
|
|
|
37
|
-
buffer: Deque[BaseEvent]
|
|
38
|
-
blocking_tool_call_id: Optional[str] # The tool call that's currently blocking the buffer
|
|
39
36
|
active_tool_call_ids: Set[str] # All currently active tool calls
|
|
40
37
|
ended_tool_call_ids: Set[str] # All tool calls that have ended
|
|
41
38
|
|
|
42
39
|
def __init__(self):
|
|
43
|
-
self.buffer = deque()
|
|
44
|
-
self.blocking_tool_call_id = None
|
|
45
40
|
self.active_tool_call_ids = set()
|
|
46
41
|
self.ended_tool_call_ids = set()
|
|
47
42
|
|
|
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
|
|
51
|
-
|
|
52
43
|
def start_tool_call(self, tool_call_id: str) -> None:
|
|
53
|
-
"""Start a new tool call
|
|
44
|
+
"""Start a new tool call."""
|
|
54
45
|
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
46
|
|
|
58
|
-
def end_tool_call(self, tool_call_id: str) ->
|
|
59
|
-
"""End a tool call
|
|
47
|
+
def end_tool_call(self, tool_call_id: str) -> None:
|
|
48
|
+
"""End a tool call."""
|
|
60
49
|
self.active_tool_call_ids.discard(tool_call_id)
|
|
61
50
|
self.ended_tool_call_ids.add(tool_call_id)
|
|
62
51
|
|
|
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
|
|
67
|
-
|
|
68
|
-
return False
|
|
69
|
-
|
|
70
52
|
|
|
71
53
|
def convert_agui_messages_to_agno_messages(messages: List[AGUIMessage]) -> List[Message]:
|
|
72
54
|
"""Convert AG-UI messages to Agno messages."""
|
|
@@ -169,6 +151,12 @@ def _create_events_from_chunk(
|
|
|
169
151
|
|
|
170
152
|
# Handle starting a new tool call
|
|
171
153
|
elif chunk.event == RunEvent.tool_call_started:
|
|
154
|
+
# End the current text message if one is active before starting tool calls
|
|
155
|
+
if message_started:
|
|
156
|
+
end_message_event = TextMessageEndEvent(type=EventType.TEXT_MESSAGE_END, message_id=message_id)
|
|
157
|
+
events_to_emit.append(end_message_event)
|
|
158
|
+
message_started = False # Reset message_started state
|
|
159
|
+
|
|
172
160
|
if chunk.tool is not None: # type: ignore
|
|
173
161
|
tool_call = chunk.tool # type: ignore
|
|
174
162
|
start_event = ToolCallStartEvent(
|
|
@@ -195,7 +183,7 @@ def _create_events_from_chunk(
|
|
|
195
183
|
type=EventType.TOOL_CALL_END,
|
|
196
184
|
tool_call_id=tool_call.tool_call_id, # type: ignore
|
|
197
185
|
)
|
|
198
|
-
events_to_emit.append(end_event)
|
|
186
|
+
events_to_emit.append(end_event)
|
|
199
187
|
|
|
200
188
|
if tool_call.result is not None:
|
|
201
189
|
result_event = ToolCallResultEvent(
|
|
@@ -205,27 +193,17 @@ def _create_events_from_chunk(
|
|
|
205
193
|
role="tool",
|
|
206
194
|
message_id=str(uuid.uuid4()),
|
|
207
195
|
)
|
|
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
|
|
196
|
+
events_to_emit.append(result_event)
|
|
219
197
|
|
|
220
198
|
# Handle reasoning
|
|
221
199
|
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)
|
|
200
|
+
step_started_event = StepStartedEvent(type=EventType.STEP_STARTED, step_name="reasoning")
|
|
201
|
+
events_to_emit.append(step_started_event)
|
|
224
202
|
elif chunk.event == RunEvent.reasoning_completed:
|
|
225
|
-
|
|
226
|
-
events_to_emit.append(
|
|
203
|
+
step_finished_event = StepFinishedEvent(type=EventType.STEP_FINISHED, step_name="reasoning")
|
|
204
|
+
events_to_emit.append(step_finished_event)
|
|
227
205
|
|
|
228
|
-
return events_to_emit, message_started
|
|
206
|
+
return events_to_emit, message_started
|
|
229
207
|
|
|
230
208
|
|
|
231
209
|
def _create_completion_events(
|
|
@@ -251,7 +229,7 @@ def _create_completion_events(
|
|
|
251
229
|
# End the message and run, denoting the end of the session
|
|
252
230
|
if message_started:
|
|
253
231
|
end_message_event = TextMessageEndEvent(type=EventType.TEXT_MESSAGE_END, message_id=message_id)
|
|
254
|
-
events_to_emit.append(end_message_event)
|
|
232
|
+
events_to_emit.append(end_message_event)
|
|
255
233
|
|
|
256
234
|
# emit frontend tool calls, i.e. external_execution=True
|
|
257
235
|
if isinstance(chunk, RunPausedEvent) and chunk.tools is not None:
|
|
@@ -265,14 +243,14 @@ def _create_completion_events(
|
|
|
265
243
|
tool_call_name=tool.tool_name,
|
|
266
244
|
parent_message_id=message_id,
|
|
267
245
|
)
|
|
268
|
-
events_to_emit.append(start_event)
|
|
246
|
+
events_to_emit.append(start_event)
|
|
269
247
|
|
|
270
248
|
args_event = ToolCallArgsEvent(
|
|
271
249
|
type=EventType.TOOL_CALL_ARGS,
|
|
272
250
|
tool_call_id=tool.tool_call_id,
|
|
273
251
|
delta=json.dumps(tool.tool_args),
|
|
274
252
|
)
|
|
275
|
-
events_to_emit.append(args_event)
|
|
253
|
+
events_to_emit.append(args_event)
|
|
276
254
|
|
|
277
255
|
end_event = ToolCallEndEvent(
|
|
278
256
|
type=EventType.TOOL_CALL_END,
|
|
@@ -280,85 +258,25 @@ def _create_completion_events(
|
|
|
280
258
|
)
|
|
281
259
|
events_to_emit.append(end_event)
|
|
282
260
|
|
|
283
|
-
# emit frontend tool calls, i.e. external_execution=True
|
|
284
|
-
if isinstance(chunk, RunPausedEvent) and chunk.tools is not None:
|
|
285
|
-
for tool in chunk.tools:
|
|
286
|
-
if tool.tool_call_id is None or tool.tool_name is None:
|
|
287
|
-
continue
|
|
288
|
-
|
|
289
|
-
start_event = ToolCallStartEvent(
|
|
290
|
-
type=EventType.TOOL_CALL_START,
|
|
291
|
-
tool_call_id=tool.tool_call_id,
|
|
292
|
-
tool_call_name=tool.tool_name,
|
|
293
|
-
parent_message_id=message_id,
|
|
294
|
-
)
|
|
295
|
-
events_to_emit.append(start_event) # type: ignore
|
|
296
|
-
|
|
297
|
-
args_event = ToolCallArgsEvent(
|
|
298
|
-
type=EventType.TOOL_CALL_ARGS,
|
|
299
|
-
tool_call_id=tool.tool_call_id,
|
|
300
|
-
delta=json.dumps(tool.tool_args),
|
|
301
|
-
)
|
|
302
|
-
events_to_emit.append(args_event) # type: ignore
|
|
303
|
-
|
|
304
|
-
end_event = ToolCallEndEvent(
|
|
305
|
-
type=EventType.TOOL_CALL_END,
|
|
306
|
-
tool_call_id=tool.tool_call_id,
|
|
307
|
-
)
|
|
308
|
-
events_to_emit.append(end_event) # type: ignore
|
|
309
|
-
|
|
310
261
|
run_finished_event = RunFinishedEvent(type=EventType.RUN_FINISHED, thread_id=thread_id, run_id=run_id)
|
|
311
|
-
events_to_emit.append(run_finished_event)
|
|
262
|
+
events_to_emit.append(run_finished_event)
|
|
312
263
|
|
|
313
|
-
return events_to_emit
|
|
264
|
+
return events_to_emit
|
|
314
265
|
|
|
315
266
|
|
|
316
267
|
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)
|
|
268
|
+
"""Process an event and return events to actually emit."""
|
|
269
|
+
events_to_emit: List[BaseEvent] = [event]
|
|
270
|
+
|
|
271
|
+
# Update the event buffer state for tracking purposes
|
|
272
|
+
if event.type == EventType.TOOL_CALL_START:
|
|
273
|
+
tool_call_id = getattr(event, "tool_call_id", None)
|
|
274
|
+
if tool_call_id:
|
|
275
|
+
event_buffer.start_tool_call(tool_call_id)
|
|
276
|
+
elif event.type == EventType.TOOL_CALL_END:
|
|
277
|
+
tool_call_id = getattr(event, "tool_call_id", None)
|
|
278
|
+
if tool_call_id:
|
|
279
|
+
event_buffer.end_tool_call(tool_call_id)
|
|
362
280
|
|
|
363
281
|
return events_to_emit
|
|
364
282
|
|
|
@@ -370,23 +288,22 @@ def stream_agno_response_as_agui_events(
|
|
|
370
288
|
message_id = str(uuid.uuid4())
|
|
371
289
|
message_started = False
|
|
372
290
|
event_buffer = EventBuffer()
|
|
291
|
+
stream_completed = False
|
|
292
|
+
|
|
293
|
+
completion_chunk = None
|
|
373
294
|
|
|
374
295
|
for chunk in response_stream:
|
|
375
|
-
#
|
|
296
|
+
# Check if this is a completion event
|
|
376
297
|
if (
|
|
377
298
|
chunk.event == RunEvent.run_completed
|
|
378
299
|
or chunk.event == TeamRunEvent.run_completed
|
|
379
300
|
or chunk.event == RunEvent.run_paused
|
|
380
301
|
):
|
|
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
|
|
302
|
+
# Store completion chunk but don't process it yet
|
|
303
|
+
completion_chunk = chunk
|
|
304
|
+
stream_completed = True
|
|
388
305
|
else:
|
|
389
|
-
# Process regular chunk
|
|
306
|
+
# Process regular chunk immediately
|
|
390
307
|
events_from_chunk, message_started = _create_events_from_chunk(
|
|
391
308
|
chunk, message_id, message_started, event_buffer
|
|
392
309
|
)
|
|
@@ -396,6 +313,30 @@ def stream_agno_response_as_agui_events(
|
|
|
396
313
|
for emit_event in events_to_emit:
|
|
397
314
|
yield emit_event
|
|
398
315
|
|
|
316
|
+
# Process ONLY completion cleanup events, not content from completion chunk
|
|
317
|
+
if completion_chunk:
|
|
318
|
+
completion_events = _create_completion_events(
|
|
319
|
+
completion_chunk, event_buffer, message_started, message_id, thread_id, run_id
|
|
320
|
+
)
|
|
321
|
+
for event in completion_events:
|
|
322
|
+
events_to_emit = _emit_event_logic(event_buffer=event_buffer, event=event)
|
|
323
|
+
for emit_event in events_to_emit:
|
|
324
|
+
yield emit_event
|
|
325
|
+
|
|
326
|
+
# Ensure completion events are always emitted even when stream ends naturally
|
|
327
|
+
if not stream_completed:
|
|
328
|
+
# Create a synthetic completion event to ensure proper cleanup
|
|
329
|
+
from agno.run.agent import RunCompletedEvent
|
|
330
|
+
|
|
331
|
+
synthetic_completion = RunCompletedEvent()
|
|
332
|
+
completion_events = _create_completion_events(
|
|
333
|
+
synthetic_completion, event_buffer, message_started, message_id, thread_id, run_id
|
|
334
|
+
)
|
|
335
|
+
for event in completion_events:
|
|
336
|
+
events_to_emit = _emit_event_logic(event_buffer=event_buffer, event=event)
|
|
337
|
+
for emit_event in events_to_emit:
|
|
338
|
+
yield emit_event
|
|
339
|
+
|
|
399
340
|
|
|
400
341
|
# Async version - thin wrapper
|
|
401
342
|
async def async_stream_agno_response_as_agui_events(
|
|
@@ -407,23 +348,22 @@ async def async_stream_agno_response_as_agui_events(
|
|
|
407
348
|
message_id = str(uuid.uuid4())
|
|
408
349
|
message_started = False
|
|
409
350
|
event_buffer = EventBuffer()
|
|
351
|
+
stream_completed = False
|
|
352
|
+
|
|
353
|
+
completion_chunk = None
|
|
410
354
|
|
|
411
355
|
async for chunk in response_stream:
|
|
412
|
-
#
|
|
356
|
+
# Check if this is a completion event
|
|
413
357
|
if (
|
|
414
358
|
chunk.event == RunEvent.run_completed
|
|
415
359
|
or chunk.event == TeamRunEvent.run_completed
|
|
416
360
|
or chunk.event == RunEvent.run_paused
|
|
417
361
|
):
|
|
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
|
|
362
|
+
# Store completion chunk but don't process it yet
|
|
363
|
+
completion_chunk = chunk
|
|
364
|
+
stream_completed = True
|
|
425
365
|
else:
|
|
426
|
-
# Process regular chunk
|
|
366
|
+
# Process regular chunk immediately
|
|
427
367
|
events_from_chunk, message_started = _create_events_from_chunk(
|
|
428
368
|
chunk, message_id, message_started, event_buffer
|
|
429
369
|
)
|
|
@@ -432,3 +372,27 @@ async def async_stream_agno_response_as_agui_events(
|
|
|
432
372
|
events_to_emit = _emit_event_logic(event_buffer=event_buffer, event=event)
|
|
433
373
|
for emit_event in events_to_emit:
|
|
434
374
|
yield emit_event
|
|
375
|
+
|
|
376
|
+
# Process ONLY completion cleanup events, not content from completion chunk
|
|
377
|
+
if completion_chunk:
|
|
378
|
+
completion_events = _create_completion_events(
|
|
379
|
+
completion_chunk, event_buffer, message_started, message_id, thread_id, run_id
|
|
380
|
+
)
|
|
381
|
+
for event in completion_events:
|
|
382
|
+
events_to_emit = _emit_event_logic(event_buffer=event_buffer, event=event)
|
|
383
|
+
for emit_event in events_to_emit:
|
|
384
|
+
yield emit_event
|
|
385
|
+
|
|
386
|
+
# Ensure completion events are always emitted even when stream ends naturally
|
|
387
|
+
if not stream_completed:
|
|
388
|
+
# Create a synthetic completion event to ensure proper cleanup
|
|
389
|
+
from agno.run.agent import RunCompletedEvent
|
|
390
|
+
|
|
391
|
+
synthetic_completion = RunCompletedEvent()
|
|
392
|
+
completion_events = _create_completion_events(
|
|
393
|
+
synthetic_completion, event_buffer, message_started, message_id, thread_id, run_id
|
|
394
|
+
)
|
|
395
|
+
for event in completion_events:
|
|
396
|
+
events_to_emit = _emit_event_logic(event_buffer=event_buffer, event=event)
|
|
397
|
+
for emit_event in events_to_emit:
|
|
398
|
+
yield emit_event
|