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
agno/os/utils.py CHANGED
@@ -7,21 +7,21 @@ from fastapi.routing import APIRoute, APIRouter
7
7
  from pydantic import BaseModel, create_model
8
8
  from starlette.middleware.cors import CORSMiddleware
9
9
 
10
- from agno.agent.agent import Agent
10
+ from agno.agent import Agent, RemoteAgent
11
11
  from agno.db.base import AsyncBaseDb, BaseDb
12
12
  from agno.knowledge.knowledge import Knowledge
13
13
  from agno.media import Audio, Image, Video
14
14
  from agno.media import File as FileMedia
15
15
  from agno.models.message import Message
16
16
  from agno.os.config import AgentOSConfig
17
+ from agno.remote.base import RemoteDb, RemoteKnowledge
17
18
  from agno.run.agent import RunOutputEvent
18
19
  from agno.run.team import TeamRunOutputEvent
19
20
  from agno.run.workflow import WorkflowRunOutputEvent
20
- from agno.team.team import Team
21
- from agno.tools import Toolkit
22
- from agno.tools.function import Function
21
+ from agno.team import RemoteTeam, Team
22
+ from agno.tools import Function, Toolkit
23
23
  from agno.utils.log import log_warning, logger
24
- from agno.workflow.workflow import Workflow
24
+ from agno.workflow import RemoteWorkflow, Workflow
25
25
 
26
26
 
27
27
  async def get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict[str, Any]:
@@ -177,14 +177,14 @@ def format_sse_event(event: Union[RunOutputEvent, TeamRunOutputEvent, WorkflowRu
177
177
 
178
178
 
179
179
  async def get_db(
180
- dbs: dict[str, list[Union[BaseDb, AsyncBaseDb]]], db_id: Optional[str] = None, table: Optional[str] = None
181
- ) -> Union[BaseDb, AsyncBaseDb]:
180
+ dbs: dict[str, list[Union[BaseDb, AsyncBaseDb, RemoteDb]]], db_id: Optional[str] = None, table: Optional[str] = None
181
+ ) -> Union[BaseDb, AsyncBaseDb, RemoteDb]:
182
182
  """Return the database with the given ID and/or table, or the first database if no ID/table is provided."""
183
183
 
184
184
  if table and not db_id:
185
185
  raise HTTPException(status_code=400, detail="The db_id query parameter is required when passing a table")
186
186
 
187
- async def _has_table(db: Union[BaseDb, AsyncBaseDb], table_name: str) -> bool:
187
+ async def _has_table(db: Union[BaseDb, AsyncBaseDb, RemoteDb], table_name: str) -> bool:
188
188
  """Check if this database has the specified table (configured and actually exists)."""
189
189
  # First check if table name is configured
190
190
  is_configured = (
@@ -203,6 +203,10 @@ async def get_db(
203
203
  if not is_configured:
204
204
  return False
205
205
 
206
+ if isinstance(db, RemoteDb):
207
+ # We have to assume remote DBs are always configured and exist
208
+ return True
209
+
206
210
  # Then check if table actually exists in the database
207
211
  try:
208
212
  if isinstance(db, AsyncBaseDb):
@@ -241,7 +245,9 @@ async def get_db(
241
245
  return next(db for dbs in dbs.values() for db in dbs)
242
246
 
243
247
 
244
- def get_knowledge_instance_by_db_id(knowledge_instances: List[Knowledge], db_id: Optional[str] = None) -> Knowledge:
248
+ def get_knowledge_instance_by_db_id(
249
+ knowledge_instances: List[Union[Knowledge, RemoteKnowledge]], db_id: Optional[str] = None
250
+ ) -> Union[Knowledge, RemoteKnowledge]:
245
251
  """Return the knowledge instance with the given ID, or the first knowledge instance if no ID is provided."""
246
252
  if not db_id and len(knowledge_instances) == 1:
247
253
  return next(iter(knowledge_instances))
@@ -350,11 +356,12 @@ def extract_input_media(run_dict: Dict[str, Any]) -> Dict[str, Any]:
350
356
  "files": [],
351
357
  }
352
358
 
353
- input = run_dict.get("input", {})
354
- input_media["images"].extend(input.get("images", []))
355
- input_media["videos"].extend(input.get("videos", []))
356
- input_media["audios"].extend(input.get("audios", []))
357
- input_media["files"].extend(input.get("files", []))
359
+ input_data = run_dict.get("input", {})
360
+ if isinstance(input_data, dict):
361
+ input_media["images"].extend(input_data.get("images", []))
362
+ input_media["videos"].extend(input_data.get("videos", []))
363
+ input_media["audios"].extend(input_data.get("audios", []))
364
+ input_media["files"].extend(input_data.get("files", []))
358
365
 
359
366
  return input_media
360
367
 
@@ -406,37 +413,9 @@ def extract_format(file: UploadFile) -> Optional[str]:
406
413
  return None
407
414
 
408
415
 
409
- def format_tools(agent_tools: List[Union[Dict[str, Any], Toolkit, Function, Callable]]):
410
- formatted_tools: List[Dict] = []
411
- if agent_tools is not None:
412
- for tool in agent_tools:
413
- if isinstance(tool, dict):
414
- formatted_tools.append(tool)
415
- elif isinstance(tool, Toolkit):
416
- for _, f in tool.functions.items():
417
- formatted_tools.append(f.to_dict())
418
- elif isinstance(tool, Function):
419
- formatted_tools.append(tool.to_dict())
420
- elif callable(tool):
421
- func = Function.from_callable(tool)
422
- formatted_tools.append(func.to_dict())
423
- else:
424
- logger.warning(f"Unknown tool type: {type(tool)}")
425
- return formatted_tools
426
-
427
-
428
- def format_team_tools(team_tools: List[Union[Function, dict]]):
429
- formatted_tools: List[Dict] = []
430
- if team_tools is not None:
431
- for tool in team_tools:
432
- if isinstance(tool, dict):
433
- formatted_tools.append(tool)
434
- elif isinstance(tool, Function):
435
- formatted_tools.append(tool.to_dict())
436
- return formatted_tools
437
-
438
-
439
- def get_agent_by_id(agent_id: str, agents: Optional[List[Agent]] = None) -> Optional[Agent]:
416
+ def get_agent_by_id(
417
+ agent_id: str, agents: Optional[List[Union[Agent, RemoteAgent]]] = None
418
+ ) -> Optional[Union[Agent, RemoteAgent]]:
440
419
  if agent_id is None or agents is None:
441
420
  return None
442
421
 
@@ -446,7 +425,9 @@ def get_agent_by_id(agent_id: str, agents: Optional[List[Agent]] = None) -> Opti
446
425
  return None
447
426
 
448
427
 
449
- def get_team_by_id(team_id: str, teams: Optional[List[Team]] = None) -> Optional[Team]:
428
+ def get_team_by_id(
429
+ team_id: str, teams: Optional[List[Union[Team, RemoteTeam]]] = None
430
+ ) -> Optional[Union[Team, RemoteTeam]]:
450
431
  if team_id is None or teams is None:
451
432
  return None
452
433
 
@@ -456,7 +437,9 @@ def get_team_by_id(team_id: str, teams: Optional[List[Team]] = None) -> Optional
456
437
  return None
457
438
 
458
439
 
459
- def get_workflow_by_id(workflow_id: str, workflows: Optional[List[Workflow]] = None) -> Optional[Workflow]:
440
+ def get_workflow_by_id(
441
+ workflow_id: str, workflows: Optional[List[Union[Workflow, RemoteWorkflow]]] = None
442
+ ) -> Optional[Union[Workflow, RemoteWorkflow]]:
460
443
  if workflow_id is None or workflows is None:
461
444
  return None
462
445
 
@@ -466,98 +449,6 @@ def get_workflow_by_id(workflow_id: str, workflows: Optional[List[Workflow]] = N
466
449
  return None
467
450
 
468
451
 
469
- # INPUT SCHEMA VALIDATIONS
470
-
471
-
472
- def get_agent_input_schema_dict(agent: Agent) -> Optional[Dict[str, Any]]:
473
- """Get input schema as dictionary for API responses"""
474
-
475
- if agent.input_schema is not None:
476
- try:
477
- return agent.input_schema.model_json_schema()
478
- except Exception:
479
- return None
480
-
481
- return None
482
-
483
-
484
- def get_team_input_schema_dict(team: Team) -> Optional[Dict[str, Any]]:
485
- """Get input schema as dictionary for API responses"""
486
-
487
- if team.input_schema is not None:
488
- try:
489
- return team.input_schema.model_json_schema()
490
- except Exception:
491
- return None
492
-
493
- return None
494
-
495
-
496
- def get_workflow_input_schema_dict(workflow: Workflow) -> Optional[Dict[str, Any]]:
497
- """Get input schema as dictionary for API responses"""
498
-
499
- # Priority 1: Explicit input_schema (Pydantic model)
500
- if workflow.input_schema is not None:
501
- try:
502
- return workflow.input_schema.model_json_schema()
503
- except Exception:
504
- return None
505
-
506
- # Priority 2: Auto-generate from custom kwargs
507
- if workflow.steps and callable(workflow.steps):
508
- custom_params = workflow.run_parameters
509
- if custom_params and len(custom_params) > 1: # More than just 'message'
510
- return _generate_schema_from_params(custom_params)
511
-
512
- # Priority 3: No schema (expects string message)
513
- return None
514
-
515
-
516
- def _generate_schema_from_params(params: Dict[str, Any]) -> Dict[str, Any]:
517
- """Convert function parameters to JSON schema"""
518
- properties: Dict[str, Any] = {}
519
- required: List[str] = []
520
-
521
- for param_name, param_info in params.items():
522
- # Skip the default 'message' parameter for custom kwargs workflows
523
- if param_name == "message":
524
- continue
525
-
526
- # Map Python types to JSON schema types
527
- param_type = param_info.get("annotation", "str")
528
- default_value = param_info.get("default")
529
- is_required = param_info.get("required", False)
530
-
531
- # Convert Python type annotations to JSON schema types
532
- if param_type == "str":
533
- properties[param_name] = {"type": "string"}
534
- elif param_type == "bool":
535
- properties[param_name] = {"type": "boolean"}
536
- elif param_type == "int":
537
- properties[param_name] = {"type": "integer"}
538
- elif param_type == "float":
539
- properties[param_name] = {"type": "number"}
540
- elif "List" in str(param_type):
541
- properties[param_name] = {"type": "array", "items": {"type": "string"}}
542
- else:
543
- properties[param_name] = {"type": "string"} # fallback
544
-
545
- # Add default value if present
546
- if default_value is not None:
547
- properties[param_name]["default"] = default_value
548
-
549
- # Add to required if no default value
550
- if is_required and default_value is None:
551
- required.append(param_name)
552
-
553
- schema = {"type": "object", "properties": properties}
554
-
555
- if required:
556
- schema["required"] = required
557
-
558
- return schema
559
-
560
-
561
452
  def resolve_origins(user_origins: Optional[List[str]] = None, default_origins: Optional[List[str]] = None) -> List[str]:
562
453
  """
563
454
  Get CORS origins - user-provided origins override defaults.
@@ -783,32 +674,6 @@ def collect_mcp_tools_from_workflow_step(step: Any, mcp_tools: List[Any]) -> Non
783
674
  collect_mcp_tools_from_workflow(step, mcp_tools)
784
675
 
785
676
 
786
- def stringify_input_content(input_content: Union[str, Dict[str, Any], List[Any], BaseModel]) -> str:
787
- """Convert any given input_content into its string representation.
788
-
789
- This handles both serialized (dict) and live (object) input_content formats.
790
- """
791
- import json
792
-
793
- if isinstance(input_content, str):
794
- return input_content
795
- elif isinstance(input_content, Message):
796
- return json.dumps(input_content.to_dict())
797
- elif isinstance(input_content, dict):
798
- return json.dumps(input_content, indent=2, default=str)
799
- elif isinstance(input_content, list):
800
- if input_content:
801
- # Handle live Message objects
802
- if isinstance(input_content[0], Message):
803
- return json.dumps([m.to_dict() for m in input_content])
804
- # Handle serialized Message dicts
805
- elif isinstance(input_content[0], dict) and input_content[0].get("role") == "user":
806
- return input_content[0].get("content", str(input_content))
807
- return str(input_content)
808
- else:
809
- return str(input_content)
810
-
811
-
812
677
  def _get_python_type_from_json_schema(field_schema: Dict[str, Any], field_name: str = "NestedModel") -> Type:
813
678
  """Map JSON schema type to Python type with recursive handling.
814
679
 
@@ -925,7 +790,7 @@ def json_schema_to_pydantic_model(schema: Dict[str, Any]) -> Type[BaseModel]:
925
790
  return create_model(model_name)
926
791
 
927
792
 
928
- def setup_tracing_for_os(db: Union[BaseDb, AsyncBaseDb]) -> None:
793
+ def setup_tracing_for_os(db: Union[BaseDb, AsyncBaseDb, RemoteDb]) -> None:
929
794
  """Set up OpenTelemetry tracing for this agent/team/workflow."""
930
795
  try:
931
796
  from agno.tracing import setup_tracing
@@ -979,3 +844,59 @@ def parse_datetime_to_utc(datetime_str: str, param_name: str = "datetime") -> "d
979
844
  status_code=400,
980
845
  detail=f"Invalid {param_name} format. Use ISO 8601 format (e.g., '2025-11-19T10:00:00Z' or '2025-11-19T10:00:00+05:30'): {e}",
981
846
  )
847
+
848
+
849
+ def format_team_tools(team_tools: List[Union[Function, dict]]):
850
+ formatted_tools: List[Dict] = []
851
+ if team_tools is not None:
852
+ for tool in team_tools:
853
+ if isinstance(tool, dict):
854
+ formatted_tools.append(tool)
855
+ elif isinstance(tool, Function):
856
+ formatted_tools.append(tool.to_dict())
857
+ return formatted_tools
858
+
859
+
860
+ def format_tools(agent_tools: List[Union[Dict[str, Any], Toolkit, Function, Callable]]):
861
+ formatted_tools: List[Dict] = []
862
+ if agent_tools is not None:
863
+ for tool in agent_tools:
864
+ if isinstance(tool, dict):
865
+ formatted_tools.append(tool)
866
+ elif isinstance(tool, Toolkit):
867
+ for _, f in tool.functions.items():
868
+ formatted_tools.append(f.to_dict())
869
+ elif isinstance(tool, Function):
870
+ formatted_tools.append(tool.to_dict())
871
+ elif callable(tool):
872
+ func = Function.from_callable(tool)
873
+ formatted_tools.append(func.to_dict())
874
+ else:
875
+ logger.warning(f"Unknown tool type: {type(tool)}")
876
+ return formatted_tools
877
+
878
+
879
+ def stringify_input_content(input_content: Union[str, Dict[str, Any], List[Any], BaseModel]) -> str:
880
+ """Convert any given input_content into its string representation.
881
+
882
+ This handles both serialized (dict) and live (object) input_content formats.
883
+ """
884
+ import json
885
+
886
+ if isinstance(input_content, str):
887
+ return input_content
888
+ elif isinstance(input_content, Message):
889
+ return json.dumps(input_content.to_dict())
890
+ elif isinstance(input_content, dict):
891
+ return json.dumps(input_content, indent=2, default=str)
892
+ elif isinstance(input_content, list):
893
+ if input_content:
894
+ # Handle live Message objects
895
+ if isinstance(input_content[0], Message):
896
+ return json.dumps([m.to_dict() for m in input_content])
897
+ # Handle serialized Message dicts
898
+ elif isinstance(input_content[0], dict) and input_content[0].get("role") == "user":
899
+ return input_content[0].get("content", str(input_content))
900
+ return str(input_content)
901
+ else:
902
+ return str(input_content)
@@ -0,0 +1,3 @@
1
+ from agno.remote.base import BaseRemote
2
+
3
+ __all__ = ["BaseRemote"]