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.
Files changed (82) hide show
  1. agno/agent/__init__.py +2 -0
  2. agno/agent/agent.py +4 -53
  3. agno/agent/remote.py +351 -0
  4. agno/client/__init__.py +3 -0
  5. agno/client/os.py +2669 -0
  6. agno/db/base.py +20 -0
  7. agno/db/mongo/async_mongo.py +11 -0
  8. agno/db/mongo/mongo.py +10 -0
  9. agno/db/mysql/async_mysql.py +9 -0
  10. agno/db/mysql/mysql.py +9 -0
  11. agno/db/postgres/async_postgres.py +9 -0
  12. agno/db/postgres/postgres.py +9 -0
  13. agno/db/postgres/utils.py +3 -2
  14. agno/db/sqlite/async_sqlite.py +9 -0
  15. agno/db/sqlite/sqlite.py +11 -1
  16. agno/exceptions.py +23 -0
  17. agno/knowledge/chunking/semantic.py +123 -46
  18. agno/knowledge/reader/csv_reader.py +9 -14
  19. agno/knowledge/reader/docx_reader.py +2 -4
  20. agno/knowledge/reader/field_labeled_csv_reader.py +11 -17
  21. agno/knowledge/reader/json_reader.py +9 -17
  22. agno/knowledge/reader/markdown_reader.py +6 -6
  23. agno/knowledge/reader/pdf_reader.py +14 -11
  24. agno/knowledge/reader/pptx_reader.py +2 -4
  25. agno/knowledge/reader/s3_reader.py +2 -11
  26. agno/knowledge/reader/text_reader.py +6 -18
  27. agno/knowledge/reader/web_search_reader.py +4 -15
  28. agno/os/app.py +104 -23
  29. agno/os/auth.py +25 -1
  30. agno/os/interfaces/a2a/a2a.py +7 -6
  31. agno/os/interfaces/a2a/router.py +13 -13
  32. agno/os/interfaces/agui/agui.py +5 -3
  33. agno/os/interfaces/agui/router.py +23 -16
  34. agno/os/interfaces/base.py +7 -7
  35. agno/os/interfaces/slack/router.py +6 -6
  36. agno/os/interfaces/slack/slack.py +7 -7
  37. agno/os/interfaces/whatsapp/router.py +29 -6
  38. agno/os/interfaces/whatsapp/whatsapp.py +11 -8
  39. agno/os/managers.py +326 -0
  40. agno/os/mcp.py +651 -79
  41. agno/os/router.py +125 -18
  42. agno/os/routers/agents/router.py +65 -22
  43. agno/os/routers/agents/schema.py +16 -4
  44. agno/os/routers/database.py +5 -0
  45. agno/os/routers/evals/evals.py +93 -11
  46. agno/os/routers/evals/utils.py +6 -6
  47. agno/os/routers/knowledge/knowledge.py +104 -16
  48. agno/os/routers/memory/memory.py +124 -7
  49. agno/os/routers/metrics/metrics.py +21 -4
  50. agno/os/routers/session/session.py +141 -12
  51. agno/os/routers/teams/router.py +40 -14
  52. agno/os/routers/teams/schema.py +12 -4
  53. agno/os/routers/traces/traces.py +54 -4
  54. agno/os/routers/workflows/router.py +223 -117
  55. agno/os/routers/workflows/schema.py +65 -1
  56. agno/os/schema.py +38 -12
  57. agno/os/utils.py +87 -166
  58. agno/remote/__init__.py +3 -0
  59. agno/remote/base.py +484 -0
  60. agno/run/workflow.py +1 -0
  61. agno/team/__init__.py +2 -0
  62. agno/team/remote.py +287 -0
  63. agno/team/team.py +25 -54
  64. agno/tracing/exporter.py +10 -6
  65. agno/tracing/setup.py +2 -1
  66. agno/utils/agent.py +58 -1
  67. agno/utils/http.py +68 -20
  68. agno/utils/os.py +0 -0
  69. agno/utils/remote.py +23 -0
  70. agno/vectordb/chroma/chromadb.py +452 -16
  71. agno/vectordb/pgvector/pgvector.py +7 -0
  72. agno/vectordb/redis/redisdb.py +1 -1
  73. agno/workflow/__init__.py +2 -0
  74. agno/workflow/agent.py +2 -2
  75. agno/workflow/remote.py +222 -0
  76. agno/workflow/types.py +0 -73
  77. agno/workflow/workflow.py +119 -68
  78. {agno-2.3.15.dist-info → agno-2.3.17.dist-info}/METADATA +1 -1
  79. {agno-2.3.15.dist-info → agno-2.3.17.dist-info}/RECORD +82 -72
  80. {agno-2.3.15.dist-info → agno-2.3.17.dist-info}/WHEEL +0 -0
  81. {agno-2.3.15.dist-info → agno-2.3.17.dist-info}/licenses/LICENSE +0 -0
  82. {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', 'pptx_file')}")
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
- # Handle BytesIO and other file-like objects that may not have a name attribute
45
- if name:
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
- # Handle BytesIO and other file-like objects that may not have a name attribute
91
- if name:
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
- document = self._create_document_from_url(url, content, result)
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, task in enumerate(tasks):
308
- if i > 0: # Add delay between requests (except for the first one)
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 task
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.agent import 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 get_websocket_router, get_workflow_router
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.team.team import Team
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.workflow import 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": [agent.id for agent in self.agents] if self.agents else None,
739
- "teams": [team.id for team in self.teams] if self.teams else None,
740
- "workflows": [workflow.id for workflow in self.workflows] if self.workflows else None,
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, registered_dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb]]], db: Union[BaseDb, AsyncBaseDb]
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
 
@@ -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
  ):
@@ -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[assignment]
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,
@@ -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
  ):