agno 2.3.15__py3-none-any.whl → 2.3.17__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/__init__.py +2 -0
- agno/agent/agent.py +4 -53
- agno/agent/remote.py +351 -0
- agno/client/__init__.py +3 -0
- agno/client/os.py +2669 -0
- agno/db/base.py +20 -0
- agno/db/mongo/async_mongo.py +11 -0
- agno/db/mongo/mongo.py +10 -0
- agno/db/mysql/async_mysql.py +9 -0
- agno/db/mysql/mysql.py +9 -0
- agno/db/postgres/async_postgres.py +9 -0
- agno/db/postgres/postgres.py +9 -0
- agno/db/postgres/utils.py +3 -2
- agno/db/sqlite/async_sqlite.py +9 -0
- agno/db/sqlite/sqlite.py +11 -1
- agno/exceptions.py +23 -0
- agno/knowledge/chunking/semantic.py +123 -46
- agno/knowledge/reader/csv_reader.py +9 -14
- agno/knowledge/reader/docx_reader.py +2 -4
- agno/knowledge/reader/field_labeled_csv_reader.py +11 -17
- agno/knowledge/reader/json_reader.py +9 -17
- agno/knowledge/reader/markdown_reader.py +6 -6
- agno/knowledge/reader/pdf_reader.py +14 -11
- agno/knowledge/reader/pptx_reader.py +2 -4
- agno/knowledge/reader/s3_reader.py +2 -11
- agno/knowledge/reader/text_reader.py +6 -18
- agno/knowledge/reader/web_search_reader.py +4 -15
- agno/os/app.py +104 -23
- agno/os/auth.py +25 -1
- agno/os/interfaces/a2a/a2a.py +7 -6
- agno/os/interfaces/a2a/router.py +13 -13
- agno/os/interfaces/agui/agui.py +5 -3
- agno/os/interfaces/agui/router.py +23 -16
- agno/os/interfaces/base.py +7 -7
- agno/os/interfaces/slack/router.py +6 -6
- agno/os/interfaces/slack/slack.py +7 -7
- agno/os/interfaces/whatsapp/router.py +29 -6
- agno/os/interfaces/whatsapp/whatsapp.py +11 -8
- agno/os/managers.py +326 -0
- agno/os/mcp.py +651 -79
- agno/os/router.py +125 -18
- agno/os/routers/agents/router.py +65 -22
- agno/os/routers/agents/schema.py +16 -4
- agno/os/routers/database.py +5 -0
- agno/os/routers/evals/evals.py +93 -11
- agno/os/routers/evals/utils.py +6 -6
- agno/os/routers/knowledge/knowledge.py +104 -16
- agno/os/routers/memory/memory.py +124 -7
- agno/os/routers/metrics/metrics.py +21 -4
- agno/os/routers/session/session.py +141 -12
- agno/os/routers/teams/router.py +40 -14
- agno/os/routers/teams/schema.py +12 -4
- agno/os/routers/traces/traces.py +54 -4
- agno/os/routers/workflows/router.py +223 -117
- agno/os/routers/workflows/schema.py +65 -1
- agno/os/schema.py +38 -12
- agno/os/utils.py +87 -166
- agno/remote/__init__.py +3 -0
- agno/remote/base.py +484 -0
- agno/run/workflow.py +1 -0
- agno/team/__init__.py +2 -0
- agno/team/remote.py +287 -0
- agno/team/team.py +25 -54
- agno/tracing/exporter.py +10 -6
- agno/tracing/setup.py +2 -1
- agno/utils/agent.py +58 -1
- agno/utils/http.py +68 -20
- agno/utils/os.py +0 -0
- agno/utils/remote.py +23 -0
- agno/vectordb/chroma/chromadb.py +452 -16
- agno/vectordb/pgvector/pgvector.py +7 -0
- agno/vectordb/redis/redisdb.py +1 -1
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +2 -2
- agno/workflow/remote.py +222 -0
- agno/workflow/types.py +0 -73
- agno/workflow/workflow.py +119 -68
- {agno-2.3.15.dist-info → agno-2.3.17.dist-info}/METADATA +1 -1
- {agno-2.3.15.dist-info → agno-2.3.17.dist-info}/RECORD +82 -72
- {agno-2.3.15.dist-info → agno-2.3.17.dist-info}/WHEEL +0 -0
- {agno-2.3.15.dist-info → agno-2.3.17.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.15.dist-info → agno-2.3.17.dist-info}/top_level.txt +0 -0
|
@@ -47,11 +47,9 @@ class PPTXReader(Reader):
|
|
|
47
47
|
presentation = Presentation(str(file))
|
|
48
48
|
doc_name = name or file.stem
|
|
49
49
|
else:
|
|
50
|
-
log_debug(f"Reading uploaded file: {getattr(file, 'name', '
|
|
50
|
+
log_debug(f"Reading uploaded file: {getattr(file, 'name', 'BytesIO')}")
|
|
51
51
|
presentation = Presentation(file)
|
|
52
|
-
doc_name = name or (
|
|
53
|
-
getattr(file, "name", "pptx_file").split(".")[0] if hasattr(file, "name") else "pptx_file"
|
|
54
|
-
)
|
|
52
|
+
doc_name = name or getattr(file, "name", "pptx_file").split(".")[0]
|
|
55
53
|
|
|
56
54
|
# Extract text from all slides
|
|
57
55
|
slide_texts = []
|
|
@@ -53,29 +53,20 @@ class S3Reader(Reader):
|
|
|
53
53
|
try:
|
|
54
54
|
log_debug(f"Reading S3 file: {s3_object.uri}")
|
|
55
55
|
|
|
56
|
+
doc_name = name or s3_object.name.split("/")[-1].split(".")[0].replace("/", "_").replace(" ", "_")
|
|
57
|
+
|
|
56
58
|
# Read PDF files
|
|
57
59
|
if s3_object.uri.endswith(".pdf"):
|
|
58
60
|
object_resource = s3_object.get_resource()
|
|
59
61
|
object_body = object_resource.get()["Body"]
|
|
60
|
-
doc_name = (
|
|
61
|
-
s3_object.name.split("/")[-1].split(".")[0].replace("/", "_").replace(" ", "_")
|
|
62
|
-
if name is None
|
|
63
|
-
else name
|
|
64
|
-
)
|
|
65
62
|
return PDFReader().read(pdf=BytesIO(object_body.read()), name=doc_name)
|
|
66
63
|
|
|
67
64
|
# Read text files
|
|
68
65
|
else:
|
|
69
|
-
doc_name = (
|
|
70
|
-
s3_object.name.split("/")[-1].split(".")[0].replace("/", "_").replace(" ", "_")
|
|
71
|
-
if name is None
|
|
72
|
-
else name
|
|
73
|
-
)
|
|
74
66
|
obj_name = s3_object.name.split("/")[-1]
|
|
75
67
|
temporary_file = Path("storage").joinpath(obj_name)
|
|
76
68
|
s3_object.download(temporary_file)
|
|
77
69
|
documents = TextReader().read(file=temporary_file, name=doc_name)
|
|
78
|
-
|
|
79
70
|
temporary_file.unlink()
|
|
80
71
|
return documents
|
|
81
72
|
|
|
@@ -39,16 +39,10 @@ class TextReader(Reader):
|
|
|
39
39
|
raise FileNotFoundError(f"Could not find file: {file}")
|
|
40
40
|
log_debug(f"Reading: {file}")
|
|
41
41
|
file_name = name or file.stem
|
|
42
|
-
file_contents = file.read_text(self.encoding or "utf-8")
|
|
42
|
+
file_contents = file.read_text(encoding=self.encoding or "utf-8")
|
|
43
43
|
else:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
file_name = name
|
|
47
|
-
elif hasattr(file, "name") and file.name is not None:
|
|
48
|
-
file_name = file.name.split(".")[0]
|
|
49
|
-
else:
|
|
50
|
-
file_name = "text_file"
|
|
51
|
-
log_debug(f"Reading uploaded file: {file_name}")
|
|
44
|
+
log_debug(f"Reading uploaded file: {getattr(file, 'name', 'BytesIO')}")
|
|
45
|
+
file_name = name or getattr(file, "name", "text_file").split(".")[0]
|
|
52
46
|
file.seek(0)
|
|
53
47
|
file_contents = file.read().decode(self.encoding or "utf-8")
|
|
54
48
|
|
|
@@ -85,16 +79,10 @@ class TextReader(Reader):
|
|
|
85
79
|
file_contents = await f.read()
|
|
86
80
|
except ImportError:
|
|
87
81
|
log_warning("aiofiles not installed, using synchronous file I/O")
|
|
88
|
-
file_contents = file.read_text(self.encoding or "utf-8")
|
|
82
|
+
file_contents = file.read_text(encoding=self.encoding or "utf-8")
|
|
89
83
|
else:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
file_name = name
|
|
93
|
-
elif hasattr(file, "name") and file.name is not None:
|
|
94
|
-
file_name = file.name.split(".")[0]
|
|
95
|
-
else:
|
|
96
|
-
file_name = "text_file"
|
|
97
|
-
log_debug(f"Reading uploaded file asynchronously: {file_name}")
|
|
84
|
+
log_debug(f"Reading uploaded file asynchronously: {getattr(file, 'name', 'BytesIO')}")
|
|
85
|
+
file_name = name or getattr(file, "name", "text_file").split(".")[0]
|
|
98
86
|
file.seek(0)
|
|
99
87
|
file_contents = file.read().decode(self.encoding or "utf-8")
|
|
100
88
|
|
|
@@ -263,21 +263,17 @@ class WebSearchReader(Reader):
|
|
|
263
263
|
|
|
264
264
|
log_debug(f"Starting async web search reader for query: {query}")
|
|
265
265
|
|
|
266
|
-
# Perform web search (synchronous operation)
|
|
267
266
|
search_results = self._perform_web_search(query)
|
|
268
267
|
if not search_results:
|
|
269
268
|
logger.warning(f"No search results found for query: {query}")
|
|
270
269
|
return []
|
|
271
270
|
|
|
272
|
-
# Create tasks for fetching content from each URL
|
|
273
271
|
async def fetch_url_async(result: Dict[str, str]) -> Optional[Document]:
|
|
274
272
|
url = result.get("url", "")
|
|
275
273
|
|
|
276
|
-
# Skip if URL is invalid or already visited
|
|
277
274
|
if not self._is_valid_url(url):
|
|
278
275
|
return None
|
|
279
276
|
|
|
280
|
-
# Mark URL as visited
|
|
281
277
|
self._visited_urls.add(url)
|
|
282
278
|
|
|
283
279
|
try:
|
|
@@ -292,32 +288,25 @@ class WebSearchReader(Reader):
|
|
|
292
288
|
else:
|
|
293
289
|
content = response.text
|
|
294
290
|
|
|
295
|
-
|
|
296
|
-
return document
|
|
291
|
+
return self._create_document_from_url(url, content, result)
|
|
297
292
|
|
|
298
293
|
except Exception as e:
|
|
299
294
|
logger.warning(f"Error fetching {url}: {e}")
|
|
300
295
|
return None
|
|
301
296
|
|
|
302
|
-
# Create tasks for all URLs
|
|
303
|
-
tasks = [fetch_url_async(result) for result in search_results]
|
|
304
|
-
|
|
305
|
-
# Execute all tasks concurrently with delays
|
|
306
297
|
documents = []
|
|
307
|
-
for i,
|
|
308
|
-
if i > 0:
|
|
298
|
+
for i, result in enumerate(search_results):
|
|
299
|
+
if i > 0:
|
|
309
300
|
await asyncio.sleep(self.delay_between_requests)
|
|
310
301
|
|
|
311
|
-
doc = await
|
|
302
|
+
doc = await fetch_url_async(result)
|
|
312
303
|
if doc is not None:
|
|
313
|
-
# Apply chunking if enabled
|
|
314
304
|
if self.chunk:
|
|
315
305
|
chunked_docs = await self.chunk_documents_async([doc])
|
|
316
306
|
documents.extend(chunked_docs)
|
|
317
307
|
else:
|
|
318
308
|
documents.append(doc)
|
|
319
309
|
|
|
320
|
-
# Stop if we've reached max_results
|
|
321
310
|
if len(documents) >= self.max_results:
|
|
322
311
|
break
|
|
323
312
|
|
agno/os/app.py
CHANGED
|
@@ -5,13 +5,15 @@ from typing import Any, Dict, List, Literal, Optional, Union
|
|
|
5
5
|
from uuid import uuid4
|
|
6
6
|
|
|
7
7
|
from fastapi import APIRouter, FastAPI, HTTPException
|
|
8
|
+
from fastapi.exceptions import RequestValidationError
|
|
8
9
|
from fastapi.responses import JSONResponse
|
|
9
10
|
from fastapi.routing import APIRoute
|
|
11
|
+
from httpx import HTTPStatusError
|
|
10
12
|
from rich import box
|
|
11
13
|
from rich.panel import Panel
|
|
12
14
|
from starlette.requests import Request
|
|
13
15
|
|
|
14
|
-
from agno.agent
|
|
16
|
+
from agno.agent import Agent, RemoteAgent
|
|
15
17
|
from agno.db.base import AsyncBaseDb, BaseDb
|
|
16
18
|
from agno.knowledge.knowledge import Knowledge
|
|
17
19
|
from agno.os.config import (
|
|
@@ -32,7 +34,7 @@ from agno.os.config import (
|
|
|
32
34
|
TracesDomainConfig,
|
|
33
35
|
)
|
|
34
36
|
from agno.os.interfaces.base import BaseInterface
|
|
35
|
-
from agno.os.router import get_base_router
|
|
37
|
+
from agno.os.router import get_base_router, get_websocket_router
|
|
36
38
|
from agno.os.routers.agents import get_agent_router
|
|
37
39
|
from agno.os.routers.database import get_database_router
|
|
38
40
|
from agno.os.routers.evals import get_eval_router
|
|
@@ -44,7 +46,7 @@ from agno.os.routers.metrics import get_metrics_router
|
|
|
44
46
|
from agno.os.routers.session import get_session_router
|
|
45
47
|
from agno.os.routers.teams import get_team_router
|
|
46
48
|
from agno.os.routers.traces import get_traces_router
|
|
47
|
-
from agno.os.routers.workflows import
|
|
49
|
+
from agno.os.routers.workflows import get_workflow_router
|
|
48
50
|
from agno.os.settings import AgnoAPISettings
|
|
49
51
|
from agno.os.utils import (
|
|
50
52
|
collect_mcp_tools_from_team,
|
|
@@ -55,34 +57,46 @@ from agno.os.utils import (
|
|
|
55
57
|
setup_tracing_for_os,
|
|
56
58
|
update_cors_middleware,
|
|
57
59
|
)
|
|
58
|
-
from agno.
|
|
60
|
+
from agno.remote.base import RemoteDb, RemoteKnowledge
|
|
61
|
+
from agno.team import RemoteTeam, Team
|
|
59
62
|
from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
60
63
|
from agno.utils.string import generate_id, generate_id_from_name
|
|
61
|
-
from agno.workflow
|
|
64
|
+
from agno.workflow import RemoteWorkflow, Workflow
|
|
62
65
|
|
|
63
66
|
|
|
64
67
|
@asynccontextmanager
|
|
65
68
|
async def mcp_lifespan(_, mcp_tools):
|
|
66
69
|
"""Manage MCP connection lifecycle inside a FastAPI app"""
|
|
67
|
-
# Startup logic: connect to all contextual MCP servers
|
|
68
70
|
for tool in mcp_tools:
|
|
69
71
|
await tool.connect()
|
|
70
72
|
|
|
71
73
|
yield
|
|
72
74
|
|
|
73
|
-
# Shutdown logic: Close all contextual MCP connections
|
|
74
75
|
for tool in mcp_tools:
|
|
75
76
|
await tool.close()
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
@asynccontextmanager
|
|
80
|
+
async def http_client_lifespan(_):
|
|
81
|
+
"""Manage httpx client lifecycle for proper connection pool cleanup."""
|
|
82
|
+
from agno.utils.http import aclose_default_clients
|
|
83
|
+
|
|
84
|
+
yield
|
|
85
|
+
|
|
86
|
+
await aclose_default_clients()
|
|
87
|
+
|
|
88
|
+
|
|
78
89
|
@asynccontextmanager
|
|
79
90
|
async def db_lifespan(app: FastAPI, agent_os: "AgentOS"):
|
|
80
|
-
"""Initializes databases in the event loop"""
|
|
91
|
+
"""Initializes databases in the event loop and closes them on shutdown."""
|
|
81
92
|
if agent_os.auto_provision_dbs:
|
|
82
93
|
agent_os._initialize_sync_databases()
|
|
83
94
|
await agent_os._initialize_async_databases()
|
|
95
|
+
|
|
84
96
|
yield
|
|
85
97
|
|
|
98
|
+
await agent_os._close_databases()
|
|
99
|
+
|
|
86
100
|
|
|
87
101
|
def _combine_app_lifespans(lifespans: list) -> Any:
|
|
88
102
|
"""Combine multiple FastAPI app lifespan context managers into one."""
|
|
@@ -115,9 +129,9 @@ class AgentOS:
|
|
|
115
129
|
name: Optional[str] = None,
|
|
116
130
|
description: Optional[str] = None,
|
|
117
131
|
version: Optional[str] = None,
|
|
118
|
-
agents: Optional[List[Agent]] = None,
|
|
119
|
-
teams: Optional[List[Team]] = None,
|
|
120
|
-
workflows: Optional[List[Workflow]] = None,
|
|
132
|
+
agents: Optional[List[Union[Agent, RemoteAgent]]] = None,
|
|
133
|
+
teams: Optional[List[Union[Team, RemoteTeam]]] = None,
|
|
134
|
+
workflows: Optional[List[Union[Workflow, RemoteWorkflow]]] = None,
|
|
121
135
|
knowledge: Optional[List[Knowledge]] = None,
|
|
122
136
|
interfaces: Optional[List[BaseInterface]] = None,
|
|
123
137
|
a2a_interface: bool = False,
|
|
@@ -171,9 +185,9 @@ class AgentOS:
|
|
|
171
185
|
|
|
172
186
|
self.config = load_yaml_config(config) if isinstance(config, str) else config
|
|
173
187
|
|
|
174
|
-
self.agents: Optional[List[Agent]] = agents
|
|
175
|
-
self.workflows: Optional[List[Workflow]] = workflows
|
|
176
|
-
self.teams: Optional[List[Team]] = teams
|
|
188
|
+
self.agents: Optional[List[Union[Agent, RemoteAgent]]] = agents
|
|
189
|
+
self.workflows: Optional[List[Union[Workflow, RemoteWorkflow]]] = workflows
|
|
190
|
+
self.teams: Optional[List[Union[Team, RemoteTeam]]] = teams
|
|
177
191
|
self.interfaces = interfaces or []
|
|
178
192
|
self.a2a_interface = a2a_interface
|
|
179
193
|
self.knowledge = knowledge
|
|
@@ -400,6 +414,8 @@ class AgentOS:
|
|
|
400
414
|
if not self.agents:
|
|
401
415
|
return
|
|
402
416
|
for agent in self.agents:
|
|
417
|
+
if isinstance(agent, RemoteAgent):
|
|
418
|
+
continue
|
|
403
419
|
# Track all MCP tools to later handle their connection
|
|
404
420
|
if agent.tools:
|
|
405
421
|
for tool in agent.tools:
|
|
@@ -424,6 +440,8 @@ class AgentOS:
|
|
|
424
440
|
return
|
|
425
441
|
|
|
426
442
|
for team in self.teams:
|
|
443
|
+
if isinstance(team, RemoteTeam):
|
|
444
|
+
continue
|
|
427
445
|
# Track all MCP tools recursively
|
|
428
446
|
collect_mcp_tools_from_team(team, self.mcp_tools)
|
|
429
447
|
|
|
@@ -449,6 +467,8 @@ class AgentOS:
|
|
|
449
467
|
|
|
450
468
|
if self.workflows:
|
|
451
469
|
for workflow in self.workflows:
|
|
470
|
+
if isinstance(workflow, RemoteWorkflow):
|
|
471
|
+
continue
|
|
452
472
|
# Track MCP tools recursively in workflow members
|
|
453
473
|
collect_mcp_tools_from_workflow(workflow, self.mcp_tools)
|
|
454
474
|
|
|
@@ -473,7 +493,7 @@ class AgentOS:
|
|
|
473
493
|
return
|
|
474
494
|
|
|
475
495
|
# Fall back to finding the first available database
|
|
476
|
-
db: Optional[Union[BaseDb, AsyncBaseDb]] = None
|
|
496
|
+
db: Optional[Union[BaseDb, AsyncBaseDb, RemoteDb]] = None
|
|
477
497
|
|
|
478
498
|
for agent in self.agents or []:
|
|
479
499
|
if agent.db:
|
|
@@ -535,6 +555,9 @@ class AgentOS:
|
|
|
535
555
|
# The async database lifespan
|
|
536
556
|
lifespans.append(partial(db_lifespan, agent_os=self))
|
|
537
557
|
|
|
558
|
+
# The httpx client cleanup lifespan (should be last to close after other lifespans)
|
|
559
|
+
lifespans.append(http_client_lifespan)
|
|
560
|
+
|
|
538
561
|
# Combine lifespans and set them in the app
|
|
539
562
|
if lifespans:
|
|
540
563
|
fastapi_app.router.lifespan_context = _combine_app_lifespans(lifespans)
|
|
@@ -560,6 +583,9 @@ class AgentOS:
|
|
|
560
583
|
# Async database initialization lifespan
|
|
561
584
|
lifespans.append(partial(db_lifespan, agent_os=self)) # type: ignore
|
|
562
585
|
|
|
586
|
+
# The httpx client cleanup lifespan (should be last to close after other lifespans)
|
|
587
|
+
lifespans.append(http_client_lifespan)
|
|
588
|
+
|
|
563
589
|
final_lifespan = _combine_app_lifespans(lifespans) if lifespans else None
|
|
564
590
|
fastapi_app = self._make_app(lifespan=final_lifespan)
|
|
565
591
|
|
|
@@ -587,6 +613,14 @@ class AgentOS:
|
|
|
587
613
|
|
|
588
614
|
if not self._app_set:
|
|
589
615
|
|
|
616
|
+
@fastapi_app.exception_handler(RequestValidationError)
|
|
617
|
+
async def validation_exception_handler(_: Request, exc: RequestValidationError) -> JSONResponse:
|
|
618
|
+
log_error(f"Validation error (422): {exc.errors()}")
|
|
619
|
+
return JSONResponse(
|
|
620
|
+
status_code=422,
|
|
621
|
+
content={"detail": exc.errors()},
|
|
622
|
+
)
|
|
623
|
+
|
|
590
624
|
@fastapi_app.exception_handler(HTTPException)
|
|
591
625
|
async def http_exception_handler(_, exc: HTTPException) -> JSONResponse:
|
|
592
626
|
log_error(f"HTTP exception: {exc.status_code} {exc.detail}")
|
|
@@ -595,6 +629,16 @@ class AgentOS:
|
|
|
595
629
|
content={"detail": str(exc.detail)},
|
|
596
630
|
)
|
|
597
631
|
|
|
632
|
+
@fastapi_app.exception_handler(HTTPStatusError)
|
|
633
|
+
async def http_status_error_handler(_: Request, exc: HTTPStatusError) -> JSONResponse:
|
|
634
|
+
status_code = exc.response.status_code
|
|
635
|
+
detail = exc.response.text
|
|
636
|
+
log_error(f"Downstream server returned HTTP status error: {status_code} {detail}")
|
|
637
|
+
return JSONResponse(
|
|
638
|
+
status_code=status_code,
|
|
639
|
+
content={"detail": detail},
|
|
640
|
+
)
|
|
641
|
+
|
|
598
642
|
@fastapi_app.exception_handler(Exception)
|
|
599
643
|
async def general_exception_handler(_: Request, exc: Exception) -> JSONResponse:
|
|
600
644
|
import traceback
|
|
@@ -734,19 +778,28 @@ class AgentOS:
|
|
|
734
778
|
|
|
735
779
|
def _get_telemetry_data(self) -> Dict[str, Any]:
|
|
736
780
|
"""Get the telemetry data for the OS"""
|
|
781
|
+
agent_ids = []
|
|
782
|
+
team_ids = []
|
|
783
|
+
workflow_ids = []
|
|
784
|
+
for agent in self.agents or []:
|
|
785
|
+
agent_ids.append(agent.id)
|
|
786
|
+
for team in self.teams or []:
|
|
787
|
+
team_ids.append(team.id)
|
|
788
|
+
for workflow in self.workflows or []:
|
|
789
|
+
workflow_ids.append(workflow.id)
|
|
737
790
|
return {
|
|
738
|
-
"agents":
|
|
739
|
-
"teams":
|
|
740
|
-
"workflows":
|
|
791
|
+
"agents": agent_ids,
|
|
792
|
+
"teams": team_ids,
|
|
793
|
+
"workflows": workflow_ids,
|
|
741
794
|
"interfaces": [interface.type for interface in self.interfaces] if self.interfaces else None,
|
|
742
795
|
}
|
|
743
796
|
|
|
744
797
|
def _auto_discover_databases(self) -> None:
|
|
745
798
|
"""Auto-discover and initialize the databases used by all contextual agents, teams and workflows."""
|
|
746
799
|
|
|
747
|
-
dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb]]] = {}
|
|
800
|
+
dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb, RemoteDb]]] = {}
|
|
748
801
|
knowledge_dbs: Dict[
|
|
749
|
-
str, List[Union[BaseDb, AsyncBaseDb]]
|
|
802
|
+
str, List[Union[BaseDb, AsyncBaseDb, RemoteDb]]
|
|
750
803
|
] = {} # Track databases specifically used for knowledge
|
|
751
804
|
|
|
752
805
|
for agent in self.agents or []:
|
|
@@ -833,6 +886,32 @@ class AgentOS:
|
|
|
833
886
|
except Exception as e:
|
|
834
887
|
log_warning(f"Failed to initialize async {db.__class__.__name__} (id: {db.id}): {e}")
|
|
835
888
|
|
|
889
|
+
async def _close_databases(self) -> None:
|
|
890
|
+
"""Close all database connections and release connection pools."""
|
|
891
|
+
from itertools import chain
|
|
892
|
+
|
|
893
|
+
if not hasattr(self, "dbs") or not hasattr(self, "knowledge_dbs"):
|
|
894
|
+
return
|
|
895
|
+
|
|
896
|
+
unique_dbs = list(
|
|
897
|
+
{
|
|
898
|
+
id(db): db
|
|
899
|
+
for db in chain(
|
|
900
|
+
chain.from_iterable(self.dbs.values()), chain.from_iterable(self.knowledge_dbs.values())
|
|
901
|
+
)
|
|
902
|
+
}.values()
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
for db in unique_dbs:
|
|
906
|
+
try:
|
|
907
|
+
if hasattr(db, "close") and callable(db.close):
|
|
908
|
+
if isinstance(db, AsyncBaseDb):
|
|
909
|
+
await db.close()
|
|
910
|
+
else:
|
|
911
|
+
db.close()
|
|
912
|
+
except Exception as e:
|
|
913
|
+
log_warning(f"Failed to close {db.__class__.__name__} (id: {db.id}): {e}")
|
|
914
|
+
|
|
836
915
|
def _get_db_table_names(self, db: BaseDb) -> Dict[str, str]:
|
|
837
916
|
"""Get the table names for a database"""
|
|
838
917
|
table_names = {
|
|
@@ -846,7 +925,9 @@ class AgentOS:
|
|
|
846
925
|
return {k: v for k, v in table_names.items() if v is not None}
|
|
847
926
|
|
|
848
927
|
def _register_db_with_validation(
|
|
849
|
-
self,
|
|
928
|
+
self,
|
|
929
|
+
registered_dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb, RemoteDb]]],
|
|
930
|
+
db: Union[BaseDb, AsyncBaseDb, RemoteDb],
|
|
850
931
|
) -> None:
|
|
851
932
|
"""Register a database in the contextual OS after validating it is not conflicting with registered databases"""
|
|
852
933
|
if db.id in registered_dbs:
|
|
@@ -857,9 +938,9 @@ class AgentOS:
|
|
|
857
938
|
def _auto_discover_knowledge_instances(self) -> None:
|
|
858
939
|
"""Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
|
|
859
940
|
seen_ids = set()
|
|
860
|
-
knowledge_instances: List[Knowledge] = []
|
|
941
|
+
knowledge_instances: List[Union[Knowledge, RemoteKnowledge]] = []
|
|
861
942
|
|
|
862
|
-
def _add_knowledge_if_not_duplicate(knowledge: "Knowledge") -> None:
|
|
943
|
+
def _add_knowledge_if_not_duplicate(knowledge: Union["Knowledge", RemoteKnowledge]) -> None:
|
|
863
944
|
"""Add knowledge instance if it's not already in the list (by object identity or db_id)."""
|
|
864
945
|
# Use database ID if available, otherwise use object ID as fallback
|
|
865
946
|
if not knowledge.contents_db:
|
agno/os/auth.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from os import getenv
|
|
2
|
-
from typing import List, Set
|
|
2
|
+
from typing import List, Optional, Set
|
|
3
3
|
|
|
4
4
|
from fastapi import Depends, HTTPException, Request
|
|
5
5
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
@@ -11,6 +11,30 @@ from agno.os.settings import AgnoAPISettings
|
|
|
11
11
|
security = HTTPBearer(auto_error=False)
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def get_auth_token_from_request(request: Request) -> Optional[str]:
|
|
15
|
+
"""
|
|
16
|
+
Extract the JWT/Bearer token from the Authorization header.
|
|
17
|
+
|
|
18
|
+
This is used to forward the auth token to remote agents/teams/workflows
|
|
19
|
+
when making requests through the gateway.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
request: The FastAPI request object
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The bearer token string if present, None otherwise
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
auth_token = get_auth_token_from_request(request)
|
|
29
|
+
if auth_token and isinstance(agent, RemoteAgent):
|
|
30
|
+
await agent.arun(message, auth_token=auth_token)
|
|
31
|
+
"""
|
|
32
|
+
auth_header = request.headers.get("Authorization")
|
|
33
|
+
if auth_header and auth_header.lower().startswith("bearer "):
|
|
34
|
+
return auth_header[7:] # Remove "Bearer " prefix
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
14
38
|
def _is_jwt_configured() -> bool:
|
|
15
39
|
"""Check if JWT authentication is configured via environment variables.
|
|
16
40
|
|
agno/os/interfaces/a2a/a2a.py
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"""Main class for the A2A app, used to expose an Agno Agent, Team, or Workflow in an A2A compatible format."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
3
|
+
from typing import Optional, Union
|
|
4
4
|
|
|
5
5
|
from fastapi.routing import APIRouter
|
|
6
6
|
from typing_extensions import List
|
|
7
7
|
|
|
8
8
|
from agno.agent import Agent
|
|
9
|
+
from agno.agent.remote import RemoteAgent
|
|
9
10
|
from agno.os.interfaces.a2a.router import attach_routes
|
|
10
11
|
from agno.os.interfaces.base import BaseInterface
|
|
11
|
-
from agno.team import Team
|
|
12
|
-
from agno.workflow import Workflow
|
|
12
|
+
from agno.team import RemoteTeam, Team
|
|
13
|
+
from agno.workflow import RemoteWorkflow, Workflow
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class A2A(BaseInterface):
|
|
@@ -19,9 +20,9 @@ class A2A(BaseInterface):
|
|
|
19
20
|
|
|
20
21
|
def __init__(
|
|
21
22
|
self,
|
|
22
|
-
agents: Optional[List[Agent]] = None,
|
|
23
|
-
teams: Optional[List[Team]] = None,
|
|
24
|
-
workflows: Optional[List[Workflow]] = None,
|
|
23
|
+
agents: Optional[List[Union[Agent, RemoteAgent]]] = None,
|
|
24
|
+
teams: Optional[List[Union[Team, RemoteTeam]]] = None,
|
|
25
|
+
workflows: Optional[List[Union[Workflow, RemoteWorkflow]]] = None,
|
|
25
26
|
prefix: str = "/a2a",
|
|
26
27
|
tags: Optional[List[str]] = None,
|
|
27
28
|
):
|
agno/os/interfaces/a2a/router.py
CHANGED
|
@@ -23,22 +23,22 @@ except ImportError as e:
|
|
|
23
23
|
|
|
24
24
|
import warnings
|
|
25
25
|
|
|
26
|
-
from agno.agent import Agent
|
|
26
|
+
from agno.agent import Agent, RemoteAgent
|
|
27
27
|
from agno.os.interfaces.a2a.utils import (
|
|
28
28
|
map_a2a_request_to_run_input,
|
|
29
29
|
map_run_output_to_a2a_task,
|
|
30
30
|
stream_a2a_response_with_error_handling,
|
|
31
31
|
)
|
|
32
32
|
from agno.os.utils import get_agent_by_id, get_request_kwargs, get_team_by_id, get_workflow_by_id
|
|
33
|
-
from agno.team import Team
|
|
34
|
-
from agno.workflow import Workflow
|
|
33
|
+
from agno.team import RemoteTeam, Team
|
|
34
|
+
from agno.workflow import RemoteWorkflow, Workflow
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def attach_routes(
|
|
38
38
|
router: APIRouter,
|
|
39
|
-
agents: Optional[List[Agent]] = None,
|
|
40
|
-
teams: Optional[List[Team]] = None,
|
|
41
|
-
workflows: Optional[List[Workflow]] = None,
|
|
39
|
+
agents: Optional[List[Union[Agent, RemoteAgent]]] = None,
|
|
40
|
+
teams: Optional[List[Union[Team, RemoteTeam]]] = None,
|
|
41
|
+
workflows: Optional[List[Union[Workflow, RemoteWorkflow]]] = None,
|
|
42
42
|
) -> APIRouter:
|
|
43
43
|
if agents is None and teams is None and workflows is None:
|
|
44
44
|
raise ValueError("Agents, Teams, or Workflows are required to setup the A2A interface.")
|
|
@@ -687,7 +687,7 @@ def attach_routes(
|
|
|
687
687
|
status_code=400,
|
|
688
688
|
detail="Entity ID required. Provide it via 'agentId' in params.message or 'X-Agent-ID' header.",
|
|
689
689
|
)
|
|
690
|
-
entity: Optional[Union[Agent, Team, Workflow]] = None
|
|
690
|
+
entity: Optional[Union[Agent, RemoteAgent, Team, RemoteTeam, Workflow, RemoteWorkflow]] = None
|
|
691
691
|
if agents:
|
|
692
692
|
entity = get_agent_by_id(agent_id, agents)
|
|
693
693
|
if not entity and teams:
|
|
@@ -720,10 +720,10 @@ def attach_routes(
|
|
|
720
720
|
else:
|
|
721
721
|
response = entity.arun(
|
|
722
722
|
input=run_input.input_content,
|
|
723
|
-
images=run_input.images,
|
|
724
|
-
videos=run_input.videos,
|
|
725
|
-
audio=run_input.audios,
|
|
726
|
-
files=run_input.files,
|
|
723
|
+
images=run_input.images, # type: ignore
|
|
724
|
+
videos=run_input.videos, # type: ignore
|
|
725
|
+
audio=run_input.audios, # type: ignore
|
|
726
|
+
files=run_input.files, # type: ignore
|
|
727
727
|
session_id=context_id,
|
|
728
728
|
user_id=user_id,
|
|
729
729
|
**kwargs,
|
|
@@ -801,7 +801,7 @@ def attach_routes(
|
|
|
801
801
|
status_code=400,
|
|
802
802
|
detail="Entity ID required. Provide 'agentId' in params.message or 'X-Agent-ID' header.",
|
|
803
803
|
)
|
|
804
|
-
entity: Optional[Union[Agent, Team, Workflow]] = None
|
|
804
|
+
entity: Optional[Union[Agent, RemoteAgent, Team, RemoteTeam, Workflow, RemoteWorkflow]] = None
|
|
805
805
|
if agents:
|
|
806
806
|
entity = get_agent_by_id(agent_id, agents)
|
|
807
807
|
if not entity and teams:
|
|
@@ -834,7 +834,7 @@ def attach_routes(
|
|
|
834
834
|
**kwargs,
|
|
835
835
|
)
|
|
836
836
|
else:
|
|
837
|
-
event_stream = entity.arun( # type: ignore
|
|
837
|
+
event_stream = entity.arun( # type: ignore
|
|
838
838
|
input=run_input.input_content,
|
|
839
839
|
images=run_input.images,
|
|
840
840
|
videos=run_input.videos,
|
agno/os/interfaces/agui/agui.py
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
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 List, Optional
|
|
3
|
+
from typing import List, Optional, Union
|
|
4
4
|
|
|
5
5
|
from fastapi.routing import APIRouter
|
|
6
6
|
|
|
7
7
|
from agno.agent import Agent
|
|
8
|
+
from agno.agent.remote import RemoteAgent
|
|
8
9
|
from agno.os.interfaces.agui.router import attach_routes
|
|
9
10
|
from agno.os.interfaces.base import BaseInterface
|
|
10
11
|
from agno.team import Team
|
|
12
|
+
from agno.team.remote import RemoteTeam
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class AGUI(BaseInterface):
|
|
@@ -17,8 +19,8 @@ class AGUI(BaseInterface):
|
|
|
17
19
|
|
|
18
20
|
def __init__(
|
|
19
21
|
self,
|
|
20
|
-
agent: Optional[Agent] = None,
|
|
21
|
-
team: Optional[Team] = None,
|
|
22
|
+
agent: Optional[Union[Agent, RemoteAgent]] = None,
|
|
23
|
+
team: Optional[Union[Team, RemoteTeam]] = None,
|
|
22
24
|
prefix: str = "",
|
|
23
25
|
tags: Optional[List[str]] = None,
|
|
24
26
|
):
|