agno 2.0.1__py3-none-any.whl → 2.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/agent/agent.py +6015 -2823
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +385 -6
- agno/db/dynamo/dynamo.py +388 -81
- agno/db/dynamo/schemas.py +47 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +435 -64
- agno/db/firestore/schemas.py +11 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +384 -42
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +351 -66
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +339 -48
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +510 -37
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2036 -0
- agno/db/mongo/mongo.py +653 -76
- agno/db/mongo/schemas.py +13 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/mysql.py +687 -25
- agno/db/mysql/schemas.py +61 -37
- agno/db/mysql/utils.py +60 -2
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2001 -0
- agno/db/postgres/postgres.py +676 -57
- agno/db/postgres/schemas.py +43 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +344 -38
- agno/db/redis/schemas.py +18 -0
- agno/db/redis/utils.py +60 -2
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/memory.py +13 -0
- agno/db/singlestore/schemas.py +26 -1
- agno/db/singlestore/singlestore.py +687 -53
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2371 -0
- agno/db/sqlite/schemas.py +24 -0
- agno/db/sqlite/sqlite.py +774 -85
- agno/db/sqlite/utils.py +168 -5
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +309 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1361 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +50 -22
- agno/eval/accuracy.py +50 -43
- agno/eval/performance.py +6 -3
- agno/eval/reliability.py +6 -3
- agno/eval/utils.py +33 -16
- agno/exceptions.py +68 -1
- agno/filters.py +354 -0
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +52 -0
- agno/integrations/discord/client.py +1 -0
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +1 -1
- agno/knowledge/chunking/semantic.py +40 -8
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/aws_bedrock.py +9 -4
- agno/knowledge/embedder/azure_openai.py +54 -0
- agno/knowledge/embedder/base.py +2 -0
- agno/knowledge/embedder/cohere.py +184 -5
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/google.py +79 -1
- agno/knowledge/embedder/huggingface.py +9 -4
- agno/knowledge/embedder/jina.py +63 -0
- agno/knowledge/embedder/mistral.py +78 -11
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +13 -0
- agno/knowledge/embedder/openai.py +37 -65
- agno/knowledge/embedder/sentence_transformer.py +8 -4
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/embedder/voyageai.py +69 -16
- agno/knowledge/knowledge.py +594 -186
- agno/knowledge/reader/base.py +9 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
- agno/knowledge/reader/json_reader.py +6 -5
- agno/knowledge/reader/markdown_reader.py +13 -13
- agno/knowledge/reader/pdf_reader.py +43 -68
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +51 -6
- agno/knowledge/reader/s3_reader.py +3 -15
- agno/knowledge/reader/tavily_reader.py +194 -0
- agno/knowledge/reader/text_reader.py +13 -13
- agno/knowledge/reader/web_search_reader.py +2 -43
- agno/knowledge/reader/website_reader.py +43 -25
- agno/knowledge/reranker/__init__.py +2 -8
- agno/knowledge/types.py +9 -0
- agno/knowledge/utils.py +20 -0
- agno/media.py +72 -0
- agno/memory/manager.py +336 -82
- agno/models/aimlapi/aimlapi.py +2 -2
- agno/models/anthropic/claude.py +183 -37
- agno/models/aws/bedrock.py +52 -112
- agno/models/aws/claude.py +33 -1
- agno/models/azure/ai_foundry.py +33 -15
- agno/models/azure/openai_chat.py +25 -8
- agno/models/base.py +999 -519
- agno/models/cerebras/cerebras.py +19 -13
- agno/models/cerebras/cerebras_openai.py +8 -5
- agno/models/cohere/chat.py +27 -1
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +57 -0
- agno/models/dashscope/dashscope.py +1 -0
- agno/models/deepinfra/deepinfra.py +2 -2
- agno/models/deepseek/deepseek.py +2 -2
- agno/models/fireworks/fireworks.py +2 -2
- agno/models/google/gemini.py +103 -31
- agno/models/groq/groq.py +28 -11
- agno/models/huggingface/huggingface.py +2 -1
- agno/models/internlm/internlm.py +2 -2
- agno/models/langdb/langdb.py +4 -4
- agno/models/litellm/chat.py +18 -1
- agno/models/litellm/litellm_openai.py +2 -2
- agno/models/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- agno/models/message.py +139 -0
- agno/models/meta/llama.py +27 -10
- agno/models/meta/llama_openai.py +5 -17
- agno/models/nebius/nebius.py +6 -6
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +22 -0
- agno/models/nvidia/nvidia.py +2 -2
- agno/models/ollama/chat.py +59 -5
- agno/models/openai/chat.py +69 -29
- agno/models/openai/responses.py +103 -106
- agno/models/openrouter/openrouter.py +41 -3
- agno/models/perplexity/perplexity.py +4 -5
- agno/models/portkey/portkey.py +3 -3
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +52 -0
- agno/models/response.py +77 -1
- agno/models/sambanova/sambanova.py +2 -2
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +25 -0
- agno/models/together/together.py +2 -2
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +2 -2
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +96 -0
- agno/models/vllm/vllm.py +1 -0
- agno/models/xai/xai.py +3 -2
- agno/os/app.py +543 -178
- agno/os/auth.py +24 -14
- agno/os/config.py +1 -0
- agno/os/interfaces/__init__.py +1 -0
- agno/os/interfaces/a2a/__init__.py +3 -0
- agno/os/interfaces/a2a/a2a.py +42 -0
- agno/os/interfaces/a2a/router.py +250 -0
- agno/os/interfaces/a2a/utils.py +924 -0
- agno/os/interfaces/agui/agui.py +23 -7
- agno/os/interfaces/agui/router.py +27 -3
- agno/os/interfaces/agui/utils.py +242 -142
- agno/os/interfaces/base.py +6 -2
- agno/os/interfaces/slack/router.py +81 -23
- agno/os/interfaces/slack/slack.py +29 -14
- agno/os/interfaces/whatsapp/router.py +11 -4
- agno/os/interfaces/whatsapp/whatsapp.py +14 -7
- agno/os/mcp.py +111 -54
- agno/os/middleware/__init__.py +7 -0
- agno/os/middleware/jwt.py +233 -0
- agno/os/router.py +556 -139
- agno/os/routers/evals/evals.py +71 -34
- agno/os/routers/evals/schemas.py +31 -31
- agno/os/routers/evals/utils.py +6 -5
- agno/os/routers/health.py +31 -0
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/knowledge.py +185 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +158 -53
- agno/os/routers/memory/schemas.py +20 -16
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +499 -38
- agno/os/schema.py +308 -198
- agno/os/utils.py +401 -41
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/azure_ai_foundry.py +2 -2
- agno/reasoning/deepseek.py +2 -2
- agno/reasoning/default.py +3 -1
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/groq.py +2 -2
- agno/reasoning/ollama.py +2 -2
- agno/reasoning/openai.py +7 -2
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +248 -94
- agno/run/base.py +44 -5
- agno/run/team.py +238 -97
- agno/run/workflow.py +144 -33
- agno/session/agent.py +105 -89
- agno/session/summary.py +65 -25
- agno/session/team.py +176 -96
- agno/session/workflow.py +406 -40
- agno/team/team.py +3854 -1610
- agno/tools/dalle.py +2 -4
- agno/tools/decorator.py +4 -2
- agno/tools/duckduckgo.py +15 -11
- agno/tools/e2b.py +14 -7
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +350 -0
- agno/tools/firecrawl.py +4 -4
- agno/tools/function.py +250 -30
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +270 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/knowledge.py +3 -3
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/mem0.py +11 -17
- agno/tools/memori.py +1 -53
- agno/tools/memory.py +419 -0
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/scrapegraph.py +58 -31
- agno/tools/searxng.py +2 -2
- agno/tools/serper.py +2 -2
- agno/tools/slack.py +18 -3
- agno/tools/spider.py +2 -2
- agno/tools/tavily.py +146 -0
- agno/tools/whatsapp.py +1 -1
- agno/tools/workflow.py +278 -0
- agno/tools/yfinance.py +12 -11
- agno/utils/agent.py +820 -0
- agno/utils/audio.py +27 -0
- agno/utils/common.py +90 -1
- agno/utils/events.py +217 -2
- agno/utils/gemini.py +180 -22
- agno/utils/hooks.py +57 -0
- agno/utils/http.py +111 -0
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +188 -10
- agno/utils/merge_dict.py +22 -1
- agno/utils/message.py +60 -0
- agno/utils/models/claude.py +40 -11
- agno/utils/print_response/agent.py +105 -21
- agno/utils/print_response/team.py +103 -38
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/reasoning.py +22 -1
- agno/utils/serialize.py +32 -0
- agno/utils/streamlit.py +16 -10
- agno/utils/string.py +41 -0
- agno/utils/team.py +98 -9
- agno/utils/tools.py +1 -1
- agno/vectordb/base.py +23 -4
- agno/vectordb/cassandra/cassandra.py +65 -9
- agno/vectordb/chroma/chromadb.py +182 -38
- agno/vectordb/clickhouse/clickhousedb.py +64 -11
- agno/vectordb/couchbase/couchbase.py +105 -10
- agno/vectordb/lancedb/lance_db.py +124 -133
- agno/vectordb/langchaindb/langchaindb.py +25 -7
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/__init__.py +3 -0
- agno/vectordb/llamaindex/llamaindexdb.py +46 -7
- agno/vectordb/milvus/milvus.py +126 -9
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +112 -7
- agno/vectordb/pgvector/pgvector.py +142 -21
- agno/vectordb/pineconedb/pineconedb.py +80 -8
- agno/vectordb/qdrant/qdrant.py +125 -39
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +694 -0
- agno/vectordb/singlestore/singlestore.py +111 -25
- agno/vectordb/surrealdb/surrealdb.py +31 -5
- agno/vectordb/upstashdb/upstashdb.py +76 -8
- agno/vectordb/weaviate/weaviate.py +86 -15
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +112 -18
- agno/workflow/loop.py +69 -10
- agno/workflow/parallel.py +266 -118
- agno/workflow/router.py +110 -17
- agno/workflow/step.py +638 -129
- agno/workflow/steps.py +65 -6
- agno/workflow/types.py +61 -23
- agno/workflow/workflow.py +2085 -272
- {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/METADATA +182 -58
- agno-2.3.0.dist-info/RECORD +577 -0
- agno/knowledge/reader/url_reader.py +0 -128
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -610
- agno/utils/models/aws_claude.py +0 -170
- agno-2.0.1.dist-info/RECORD +0 -515
- {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
- {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
agno/os/app.py
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
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, Literal, Optional, Tuple, 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
|
-
from starlette.middleware.cors import CORSMiddleware
|
|
12
12
|
from starlette.requests import Request
|
|
13
13
|
|
|
14
14
|
from agno.agent.agent import Agent
|
|
15
|
+
from agno.db.base import AsyncBaseDb, BaseDb
|
|
16
|
+
from agno.knowledge.knowledge import Knowledge
|
|
15
17
|
from agno.os.config import (
|
|
16
18
|
AgentOSConfig,
|
|
17
19
|
DatabaseConfig,
|
|
@@ -27,20 +29,30 @@ from agno.os.config import (
|
|
|
27
29
|
SessionDomainConfig,
|
|
28
30
|
)
|
|
29
31
|
from agno.os.interfaces.base import BaseInterface
|
|
30
|
-
from agno.os.router import get_base_router
|
|
32
|
+
from agno.os.router import get_base_router, get_websocket_router
|
|
31
33
|
from agno.os.routers.evals import get_eval_router
|
|
34
|
+
from agno.os.routers.health import get_health_router
|
|
35
|
+
from agno.os.routers.home import get_home_router
|
|
32
36
|
from agno.os.routers.knowledge import get_knowledge_router
|
|
33
37
|
from agno.os.routers.memory import get_memory_router
|
|
34
38
|
from agno.os.routers.metrics import get_metrics_router
|
|
35
39
|
from agno.os.routers.session import get_session_router
|
|
36
40
|
from agno.os.settings import AgnoAPISettings
|
|
37
|
-
from agno.os.utils import
|
|
41
|
+
from agno.os.utils import (
|
|
42
|
+
collect_mcp_tools_from_team,
|
|
43
|
+
collect_mcp_tools_from_workflow,
|
|
44
|
+
find_conflicting_routes,
|
|
45
|
+
load_yaml_config,
|
|
46
|
+
update_cors_middleware,
|
|
47
|
+
)
|
|
38
48
|
from agno.team.team import Team
|
|
49
|
+
from agno.utils.log import log_debug, log_error, log_warning
|
|
50
|
+
from agno.utils.string import generate_id, generate_id_from_name
|
|
39
51
|
from agno.workflow.workflow import Workflow
|
|
40
52
|
|
|
41
53
|
|
|
42
54
|
@asynccontextmanager
|
|
43
|
-
async def mcp_lifespan(
|
|
55
|
+
async def mcp_lifespan(_, mcp_tools):
|
|
44
56
|
"""Manage MCP connection lifecycle inside a FastAPI app"""
|
|
45
57
|
# Startup logic: connect to all contextual MCP servers
|
|
46
58
|
for tool in mcp_tools:
|
|
@@ -53,104 +65,223 @@ async def mcp_lifespan(app, mcp_tools):
|
|
|
53
65
|
await tool.close()
|
|
54
66
|
|
|
55
67
|
|
|
68
|
+
def _combine_app_lifespans(lifespans: list) -> Any:
|
|
69
|
+
"""Combine multiple FastAPI app lifespan context managers into one."""
|
|
70
|
+
if len(lifespans) == 1:
|
|
71
|
+
return lifespans[0]
|
|
72
|
+
|
|
73
|
+
from contextlib import asynccontextmanager
|
|
74
|
+
|
|
75
|
+
@asynccontextmanager
|
|
76
|
+
async def combined_lifespan(app):
|
|
77
|
+
async def _run_nested(index: int):
|
|
78
|
+
if index >= len(lifespans):
|
|
79
|
+
yield
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
async with lifespans[index](app):
|
|
83
|
+
async for _ in _run_nested(index + 1):
|
|
84
|
+
yield
|
|
85
|
+
|
|
86
|
+
async for _ in _run_nested(0):
|
|
87
|
+
yield
|
|
88
|
+
|
|
89
|
+
return combined_lifespan
|
|
90
|
+
|
|
91
|
+
|
|
56
92
|
class AgentOS:
|
|
57
93
|
def __init__(
|
|
58
94
|
self,
|
|
59
|
-
|
|
95
|
+
id: Optional[str] = None,
|
|
60
96
|
name: Optional[str] = None,
|
|
61
97
|
description: Optional[str] = None,
|
|
62
98
|
version: Optional[str] = None,
|
|
63
99
|
agents: Optional[List[Agent]] = None,
|
|
64
100
|
teams: Optional[List[Team]] = None,
|
|
65
101
|
workflows: Optional[List[Workflow]] = None,
|
|
102
|
+
knowledge: Optional[List[Knowledge]] = None,
|
|
66
103
|
interfaces: Optional[List[BaseInterface]] = None,
|
|
104
|
+
a2a_interface: bool = False,
|
|
67
105
|
config: Optional[Union[str, AgentOSConfig]] = None,
|
|
68
106
|
settings: Optional[AgnoAPISettings] = None,
|
|
69
|
-
fastapi_app: Optional[FastAPI] = None,
|
|
70
107
|
lifespan: Optional[Any] = None,
|
|
71
|
-
|
|
108
|
+
enable_mcp_server: bool = False,
|
|
109
|
+
base_app: Optional[FastAPI] = None,
|
|
110
|
+
on_route_conflict: Literal["preserve_agentos", "preserve_base_app", "error"] = "preserve_agentos",
|
|
72
111
|
telemetry: bool = True,
|
|
112
|
+
auto_provision_dbs: bool = True,
|
|
73
113
|
):
|
|
74
|
-
|
|
75
|
-
|
|
114
|
+
"""Initialize AgentOS.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
id: Unique identifier for this AgentOS instance
|
|
118
|
+
name: Name of the AgentOS instance
|
|
119
|
+
description: Description of the AgentOS instance
|
|
120
|
+
version: Version of the AgentOS instance
|
|
121
|
+
agents: List of agents to include in the OS
|
|
122
|
+
teams: List of teams to include in the OS
|
|
123
|
+
workflows: List of workflows to include in the OS
|
|
124
|
+
knowledge: List of knowledge bases to include in the OS
|
|
125
|
+
interfaces: List of interfaces to include in the OS
|
|
126
|
+
a2a_interface: Whether to expose the OS agents and teams in an A2A server
|
|
127
|
+
config: Configuration file path or AgentOSConfig instance
|
|
128
|
+
settings: API settings for the OS
|
|
129
|
+
lifespan: Optional lifespan context manager for the FastAPI app
|
|
130
|
+
enable_mcp_server: Whether to enable MCP (Model Context Protocol)
|
|
131
|
+
base_app: Optional base FastAPI app to use for the AgentOS. All routes and middleware will be added to this app.
|
|
132
|
+
on_route_conflict: What to do when a route conflict is detected in case a custom base_app is provided.
|
|
133
|
+
telemetry: Whether to enable telemetry
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
if not agents and not workflows and not teams and not knowledge:
|
|
137
|
+
raise ValueError("Either agents, teams, workflows or knowledge bases must be provided.")
|
|
76
138
|
|
|
77
|
-
self.config =
|
|
139
|
+
self.config = load_yaml_config(config) if isinstance(config, str) else config
|
|
78
140
|
|
|
79
141
|
self.agents: Optional[List[Agent]] = agents
|
|
80
142
|
self.workflows: Optional[List[Workflow]] = workflows
|
|
81
143
|
self.teams: Optional[List[Team]] = teams
|
|
82
144
|
self.interfaces = interfaces or []
|
|
83
|
-
|
|
145
|
+
self.a2a_interface = a2a_interface
|
|
146
|
+
self.knowledge = knowledge
|
|
84
147
|
self.settings: AgnoAPISettings = settings or AgnoAPISettings()
|
|
85
|
-
|
|
148
|
+
self.auto_provision_dbs = auto_provision_dbs
|
|
86
149
|
self._app_set = False
|
|
87
|
-
|
|
88
|
-
if
|
|
89
|
-
self.
|
|
150
|
+
|
|
151
|
+
if base_app:
|
|
152
|
+
self.base_app: Optional[FastAPI] = base_app
|
|
90
153
|
self._app_set = True
|
|
154
|
+
self.on_route_conflict = on_route_conflict
|
|
155
|
+
else:
|
|
156
|
+
self.base_app = None
|
|
157
|
+
self._app_set = False
|
|
158
|
+
self.on_route_conflict = on_route_conflict
|
|
91
159
|
|
|
92
160
|
self.interfaces = interfaces or []
|
|
93
161
|
|
|
94
|
-
self.os_id: Optional[str] = os_id
|
|
95
162
|
self.name = name
|
|
163
|
+
|
|
164
|
+
self.id = id
|
|
165
|
+
if not self.id:
|
|
166
|
+
self.id = generate_id(self.name) if self.name else str(uuid4())
|
|
167
|
+
|
|
96
168
|
self.version = version
|
|
97
169
|
self.description = description
|
|
98
170
|
|
|
99
171
|
self.telemetry = telemetry
|
|
100
172
|
|
|
101
|
-
self.
|
|
173
|
+
self.enable_mcp_server = enable_mcp_server
|
|
102
174
|
self.lifespan = lifespan
|
|
103
175
|
|
|
104
176
|
# List of all MCP tools used inside the AgentOS
|
|
105
|
-
self.mcp_tools = []
|
|
106
|
-
|
|
107
|
-
if self.agents:
|
|
108
|
-
for agent in self.agents:
|
|
109
|
-
# Track all MCP tools to later handle their connection
|
|
110
|
-
if agent.tools:
|
|
111
|
-
for tool in agent.tools:
|
|
112
|
-
# Checking if the tool is a MCPTools or MultiMCPTools instance
|
|
113
|
-
type_name = type(tool).__name__
|
|
114
|
-
if type_name in ("MCPTools", "MultiMCPTools"):
|
|
115
|
-
self.mcp_tools.append(tool)
|
|
177
|
+
self.mcp_tools: List[Any] = []
|
|
178
|
+
self._mcp_app: Optional[Any] = None
|
|
116
179
|
|
|
117
|
-
|
|
180
|
+
self._initialize_agents()
|
|
181
|
+
self._initialize_teams()
|
|
182
|
+
self._initialize_workflows()
|
|
118
183
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if self.teams:
|
|
123
|
-
for team in self.teams:
|
|
124
|
-
# Track all MCP tools to later handle their connection
|
|
125
|
-
if team.tools:
|
|
126
|
-
for tool in team.tools:
|
|
127
|
-
# Checking if the tool is a MCPTools or MultiMCPTools instance
|
|
128
|
-
type_name = type(tool).__name__
|
|
129
|
-
if type_name in ("MCPTools", "MultiMCPTools"):
|
|
130
|
-
self.mcp_tools.append(tool)
|
|
184
|
+
if self.telemetry:
|
|
185
|
+
from agno.api.os import OSLaunch, log_os_telemetry
|
|
131
186
|
|
|
132
|
-
|
|
187
|
+
log_os_telemetry(launch=OSLaunch(os_id=self.id, data=self._get_telemetry_data()))
|
|
133
188
|
|
|
134
|
-
|
|
135
|
-
|
|
189
|
+
def _add_agent_os_to_lifespan_function(self, lifespan):
|
|
190
|
+
"""
|
|
191
|
+
Inspect a lifespan function and wrap it to pass agent_os if it accepts it.
|
|
136
192
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
193
|
+
Returns:
|
|
194
|
+
A wrapped lifespan that passes agent_os if the lifespan function expects it.
|
|
195
|
+
"""
|
|
196
|
+
# Getting the actual function inside the lifespan
|
|
197
|
+
lifespan_function = lifespan
|
|
198
|
+
if hasattr(lifespan, "__wrapped__"):
|
|
199
|
+
lifespan_function = lifespan.__wrapped__
|
|
143
200
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# TODO: track MCP tools in workflow members
|
|
147
|
-
if not workflow.id:
|
|
148
|
-
workflow.id = generate_id(workflow.name)
|
|
201
|
+
try:
|
|
202
|
+
from inspect import signature
|
|
149
203
|
|
|
150
|
-
|
|
151
|
-
|
|
204
|
+
# Inspecting the lifespan function signature to find its parameters
|
|
205
|
+
sig = signature(lifespan_function)
|
|
206
|
+
params = list(sig.parameters.keys())
|
|
207
|
+
|
|
208
|
+
# If the lifespan function expects the 'agent_os' parameter, add it
|
|
209
|
+
if "agent_os" in params:
|
|
210
|
+
return partial(lifespan, agent_os=self)
|
|
211
|
+
else:
|
|
212
|
+
return lifespan
|
|
213
|
+
|
|
214
|
+
except (ValueError, TypeError):
|
|
215
|
+
return lifespan
|
|
216
|
+
|
|
217
|
+
def resync(self, app: FastAPI) -> None:
|
|
218
|
+
"""Resync the AgentOS to discover, initialize and configure: agents, teams, workflows, databases and knowledge bases."""
|
|
219
|
+
self._initialize_agents()
|
|
220
|
+
self._initialize_teams()
|
|
221
|
+
self._initialize_workflows()
|
|
222
|
+
self._auto_discover_databases()
|
|
223
|
+
self._auto_discover_knowledge_instances()
|
|
224
|
+
|
|
225
|
+
if self.enable_mcp_server:
|
|
226
|
+
from agno.os.mcp import get_mcp_server
|
|
227
|
+
|
|
228
|
+
self._mcp_app = get_mcp_server(self)
|
|
152
229
|
|
|
153
|
-
|
|
230
|
+
self._reprovision_routers(app=app)
|
|
231
|
+
|
|
232
|
+
def _reprovision_routers(self, app: FastAPI) -> None:
|
|
233
|
+
"""Re-provision all routes for the AgentOS."""
|
|
234
|
+
updated_routers = [
|
|
235
|
+
get_session_router(dbs=self.dbs),
|
|
236
|
+
get_metrics_router(dbs=self.dbs),
|
|
237
|
+
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
238
|
+
get_memory_router(dbs=self.dbs),
|
|
239
|
+
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
# Clear all previously existing routes
|
|
243
|
+
app.router.routes = [
|
|
244
|
+
route
|
|
245
|
+
for route in app.router.routes
|
|
246
|
+
if hasattr(route, "path")
|
|
247
|
+
and route.path in ["/docs", "/redoc", "/openapi.json", "/docs/oauth2-redirect"]
|
|
248
|
+
or route.path.startswith("/mcp") # type: ignore
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
# Add the built-in routes
|
|
252
|
+
self._add_built_in_routes(app=app)
|
|
253
|
+
|
|
254
|
+
# Add the updated routes
|
|
255
|
+
for router in updated_routers:
|
|
256
|
+
self._add_router(app, router)
|
|
257
|
+
|
|
258
|
+
# Mount MCP if needed
|
|
259
|
+
if self.enable_mcp_server and self._mcp_app:
|
|
260
|
+
app.mount("/", self._mcp_app)
|
|
261
|
+
|
|
262
|
+
def _add_built_in_routes(self, app: FastAPI) -> None:
|
|
263
|
+
"""Add all AgentOSbuilt-in routes to the given app."""
|
|
264
|
+
# Add the home router if MCP server is not enabled
|
|
265
|
+
if not self.enable_mcp_server:
|
|
266
|
+
self._add_router(app, get_home_router(self))
|
|
267
|
+
|
|
268
|
+
self._add_router(app, get_health_router(health_endpoint="/health"))
|
|
269
|
+
self._add_router(app, get_base_router(self, settings=self.settings))
|
|
270
|
+
self._add_router(app, get_websocket_router(self, settings=self.settings))
|
|
271
|
+
|
|
272
|
+
# Add A2A interface if relevant
|
|
273
|
+
has_a2a_interface = False
|
|
274
|
+
for interface in self.interfaces:
|
|
275
|
+
if not has_a2a_interface and interface.__class__.__name__ == "A2A":
|
|
276
|
+
has_a2a_interface = True
|
|
277
|
+
interface_router = interface.get_router()
|
|
278
|
+
self._add_router(app, interface_router)
|
|
279
|
+
if self.a2a_interface and not has_a2a_interface:
|
|
280
|
+
from agno.os.interfaces.a2a import A2A
|
|
281
|
+
|
|
282
|
+
a2a_interface = A2A(agents=self.agents, teams=self.teams, workflows=self.workflows)
|
|
283
|
+
self.interfaces.append(a2a_interface)
|
|
284
|
+
self._add_router(app, a2a_interface.get_router())
|
|
154
285
|
|
|
155
286
|
def _make_app(self, lifespan: Optional[Any] = None) -> FastAPI:
|
|
156
287
|
# Adjust the FastAPI app lifespan to handle MCP connections if relevant
|
|
@@ -167,7 +298,7 @@ class AgentOS:
|
|
|
167
298
|
async with mcp_tools_lifespan(app): # type: ignore
|
|
168
299
|
yield
|
|
169
300
|
|
|
170
|
-
app_lifespan = combined_lifespan
|
|
301
|
+
app_lifespan = combined_lifespan
|
|
171
302
|
else:
|
|
172
303
|
app_lifespan = mcp_tools_lifespan
|
|
173
304
|
|
|
@@ -181,77 +312,175 @@ class AgentOS:
|
|
|
181
312
|
lifespan=app_lifespan,
|
|
182
313
|
)
|
|
183
314
|
|
|
315
|
+
def _initialize_agents(self) -> None:
|
|
316
|
+
"""Initialize and configure all agents for AgentOS usage."""
|
|
317
|
+
if not self.agents:
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
for agent in self.agents:
|
|
321
|
+
# Track all MCP tools to later handle their connection
|
|
322
|
+
if agent.tools:
|
|
323
|
+
for tool in agent.tools:
|
|
324
|
+
# Checking if the tool is a MCPTools or MultiMCPTools instance
|
|
325
|
+
type_name = type(tool).__name__
|
|
326
|
+
if type_name in ("MCPTools", "MultiMCPTools"):
|
|
327
|
+
if tool not in self.mcp_tools:
|
|
328
|
+
self.mcp_tools.append(tool)
|
|
329
|
+
|
|
330
|
+
agent.initialize_agent()
|
|
331
|
+
|
|
332
|
+
# Required for the built-in routes to work
|
|
333
|
+
agent.store_events = True
|
|
334
|
+
|
|
335
|
+
def _initialize_teams(self) -> None:
|
|
336
|
+
"""Initialize and configure all teams for AgentOS usage."""
|
|
337
|
+
if not self.teams:
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
for team in self.teams:
|
|
341
|
+
# Track all MCP tools recursively
|
|
342
|
+
collect_mcp_tools_from_team(team, self.mcp_tools)
|
|
343
|
+
|
|
344
|
+
team.initialize_team()
|
|
345
|
+
|
|
346
|
+
for member in team.members:
|
|
347
|
+
if isinstance(member, Agent):
|
|
348
|
+
member.team_id = None
|
|
349
|
+
member.initialize_agent()
|
|
350
|
+
elif isinstance(member, Team):
|
|
351
|
+
member.initialize_team()
|
|
352
|
+
|
|
353
|
+
# Required for the built-in routes to work
|
|
354
|
+
team.store_events = True
|
|
355
|
+
|
|
356
|
+
def _initialize_workflows(self) -> None:
|
|
357
|
+
"""Initialize and configure all workflows for AgentOS usage."""
|
|
358
|
+
if not self.workflows:
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
if self.workflows:
|
|
362
|
+
for workflow in self.workflows:
|
|
363
|
+
# Track MCP tools recursively in workflow members
|
|
364
|
+
collect_mcp_tools_from_workflow(workflow, self.mcp_tools)
|
|
365
|
+
|
|
366
|
+
if not workflow.id:
|
|
367
|
+
workflow.id = generate_id_from_name(workflow.name)
|
|
368
|
+
|
|
369
|
+
# Required for the built-in routes to work
|
|
370
|
+
workflow.store_events = True
|
|
371
|
+
|
|
184
372
|
def get_app(self) -> FastAPI:
|
|
185
|
-
if
|
|
186
|
-
|
|
373
|
+
if self.base_app:
|
|
374
|
+
fastapi_app = self.base_app
|
|
375
|
+
|
|
376
|
+
# Initialize MCP server if enabled
|
|
377
|
+
if self.enable_mcp_server:
|
|
378
|
+
from agno.os.mcp import get_mcp_server
|
|
379
|
+
|
|
380
|
+
self._mcp_app = get_mcp_server(self)
|
|
381
|
+
|
|
382
|
+
# Collect all lifespans that need to be combined
|
|
383
|
+
lifespans = []
|
|
384
|
+
|
|
385
|
+
# The user provided lifespan
|
|
386
|
+
if self.lifespan:
|
|
387
|
+
# Wrap the user lifespan with agent_os parameter
|
|
388
|
+
wrapped_lifespan = self._add_agent_os_to_lifespan_function(self.lifespan)
|
|
389
|
+
lifespans.append(wrapped_lifespan)
|
|
390
|
+
|
|
391
|
+
# The provided app's existing lifespan
|
|
392
|
+
if fastapi_app.router.lifespan_context:
|
|
393
|
+
lifespans.append(fastapi_app.router.lifespan_context)
|
|
394
|
+
|
|
395
|
+
# The MCP tools lifespan
|
|
396
|
+
if self.mcp_tools:
|
|
397
|
+
lifespans.append(partial(mcp_lifespan, mcp_tools=self.mcp_tools))
|
|
398
|
+
|
|
399
|
+
# The /mcp server lifespan
|
|
400
|
+
if self.enable_mcp_server and self._mcp_app:
|
|
401
|
+
lifespans.append(self._mcp_app.lifespan)
|
|
402
|
+
|
|
403
|
+
# Combine lifespans and set them in the app
|
|
404
|
+
if lifespans:
|
|
405
|
+
fastapi_app.router.lifespan_context = _combine_app_lifespans(lifespans)
|
|
406
|
+
|
|
407
|
+
else:
|
|
408
|
+
if self.enable_mcp_server:
|
|
187
409
|
from contextlib import asynccontextmanager
|
|
188
410
|
|
|
189
411
|
from agno.os.mcp import get_mcp_server
|
|
190
412
|
|
|
191
|
-
self.
|
|
413
|
+
self._mcp_app = get_mcp_server(self)
|
|
192
414
|
|
|
193
|
-
final_lifespan = self.
|
|
415
|
+
final_lifespan = self._mcp_app.lifespan # type: ignore
|
|
194
416
|
if self.lifespan is not None:
|
|
417
|
+
# Wrap the user lifespan with agent_os parameter
|
|
418
|
+
wrapped_lifespan = self._add_agent_os_to_lifespan_function(self.lifespan)
|
|
419
|
+
|
|
195
420
|
# Combine both lifespans
|
|
196
421
|
@asynccontextmanager
|
|
197
422
|
async def combined_lifespan(app: FastAPI):
|
|
198
423
|
# Run both lifespans
|
|
199
|
-
async with
|
|
200
|
-
async with self.
|
|
424
|
+
async with wrapped_lifespan(app): # type: ignore
|
|
425
|
+
async with self._mcp_app.lifespan(app): # type: ignore
|
|
201
426
|
yield
|
|
202
427
|
|
|
203
428
|
final_lifespan = combined_lifespan # type: ignore
|
|
204
429
|
|
|
205
|
-
|
|
430
|
+
fastapi_app = self._make_app(lifespan=final_lifespan)
|
|
206
431
|
else:
|
|
207
|
-
|
|
432
|
+
# Wrap the user lifespan with agent_os parameter
|
|
433
|
+
wrapped_user_lifespan = None
|
|
434
|
+
if self.lifespan is not None:
|
|
435
|
+
wrapped_user_lifespan = self._add_agent_os_to_lifespan_function(self.lifespan)
|
|
208
436
|
|
|
209
|
-
|
|
210
|
-
self.fastapi_app.include_router(get_base_router(self, settings=self.settings))
|
|
437
|
+
fastapi_app = self._make_app(lifespan=wrapped_user_lifespan)
|
|
211
438
|
|
|
212
|
-
|
|
213
|
-
interface_router = interface.get_router()
|
|
214
|
-
self.fastapi_app.include_router(interface_router)
|
|
439
|
+
self._add_built_in_routes(app=fastapi_app)
|
|
215
440
|
|
|
216
441
|
self._auto_discover_databases()
|
|
217
442
|
self._auto_discover_knowledge_instances()
|
|
218
|
-
|
|
443
|
+
|
|
444
|
+
routers = [
|
|
445
|
+
get_session_router(dbs=self.dbs),
|
|
446
|
+
get_memory_router(dbs=self.dbs),
|
|
447
|
+
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
448
|
+
get_metrics_router(dbs=self.dbs),
|
|
449
|
+
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
450
|
+
]
|
|
451
|
+
|
|
452
|
+
for router in routers:
|
|
453
|
+
self._add_router(fastapi_app, router)
|
|
219
454
|
|
|
220
455
|
# Mount MCP if needed
|
|
221
|
-
if self.
|
|
222
|
-
|
|
456
|
+
if self.enable_mcp_server and self._mcp_app:
|
|
457
|
+
fastapi_app.mount("/", self._mcp_app)
|
|
223
458
|
|
|
224
|
-
# Add middleware (only if app is not set)
|
|
225
459
|
if not self._app_set:
|
|
226
460
|
|
|
227
|
-
@
|
|
228
|
-
async def http_exception_handler(
|
|
461
|
+
@fastapi_app.exception_handler(HTTPException)
|
|
462
|
+
async def http_exception_handler(_, exc: HTTPException) -> JSONResponse:
|
|
463
|
+
log_error(f"HTTP exception: {exc.status_code} {exc.detail}")
|
|
229
464
|
return JSONResponse(
|
|
230
465
|
status_code=exc.status_code,
|
|
231
466
|
content={"detail": str(exc.detail)},
|
|
232
467
|
)
|
|
233
468
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
except Exception as e:
|
|
238
|
-
return JSONResponse(
|
|
239
|
-
status_code=e.status_code if hasattr(e, "status_code") else 500, # type: ignore
|
|
240
|
-
content={"detail": str(e)},
|
|
241
|
-
)
|
|
469
|
+
@fastapi_app.exception_handler(Exception)
|
|
470
|
+
async def general_exception_handler(_: Request, exc: Exception) -> JSONResponse:
|
|
471
|
+
import traceback
|
|
242
472
|
|
|
243
|
-
|
|
473
|
+
log_error(f"Unhandled exception:\n{traceback.format_exc(limit=5)}")
|
|
244
474
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
)
|
|
475
|
+
return JSONResponse(
|
|
476
|
+
status_code=getattr(exc, "status_code", 500),
|
|
477
|
+
content={"detail": str(exc)},
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Update CORS middleware
|
|
481
|
+
update_cors_middleware(fastapi_app, self.settings.cors_origin_list) # type: ignore
|
|
253
482
|
|
|
254
|
-
return
|
|
483
|
+
return fastapi_app
|
|
255
484
|
|
|
256
485
|
def get_routes(self) -> List[Any]:
|
|
257
486
|
"""Retrieve all routes from the FastAPI app.
|
|
@@ -263,6 +492,62 @@ class AgentOS:
|
|
|
263
492
|
|
|
264
493
|
return app.routes
|
|
265
494
|
|
|
495
|
+
def _add_router(self, fastapi_app: FastAPI, router: APIRouter) -> None:
|
|
496
|
+
"""Add a router to the FastAPI app, avoiding route conflicts.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
router: The APIRouter to add
|
|
500
|
+
"""
|
|
501
|
+
|
|
502
|
+
conflicts = find_conflicting_routes(fastapi_app, router)
|
|
503
|
+
conflicting_routes = [conflict["route"] for conflict in conflicts]
|
|
504
|
+
|
|
505
|
+
if conflicts and self._app_set:
|
|
506
|
+
if self.on_route_conflict == "preserve_base_app":
|
|
507
|
+
# Skip conflicting AgentOS routes, prefer user's existing routes
|
|
508
|
+
for conflict in conflicts:
|
|
509
|
+
methods_str = ", ".join(conflict["methods"]) # type: ignore
|
|
510
|
+
log_debug(
|
|
511
|
+
f"Skipping conflicting AgentOS route: {methods_str} {conflict['path']} - "
|
|
512
|
+
f"Using existing custom route instead"
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Create a new router without the conflicting routes
|
|
516
|
+
filtered_router = APIRouter()
|
|
517
|
+
for route in router.routes:
|
|
518
|
+
if route not in conflicting_routes:
|
|
519
|
+
filtered_router.routes.append(route)
|
|
520
|
+
|
|
521
|
+
# Use the filtered router if it has any routes left
|
|
522
|
+
if filtered_router.routes:
|
|
523
|
+
fastapi_app.include_router(filtered_router)
|
|
524
|
+
|
|
525
|
+
elif self.on_route_conflict == "preserve_agentos":
|
|
526
|
+
# Log warnings but still add all routes (AgentOS routes will override)
|
|
527
|
+
for conflict in conflicts:
|
|
528
|
+
methods_str = ", ".join(conflict["methods"]) # type: ignore
|
|
529
|
+
log_warning(
|
|
530
|
+
f"Route conflict detected: {methods_str} {conflict['path']} - "
|
|
531
|
+
f"AgentOS route will override existing custom route"
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# Remove conflicting routes
|
|
535
|
+
for route in fastapi_app.routes:
|
|
536
|
+
for conflict in conflicts:
|
|
537
|
+
if isinstance(route, APIRoute):
|
|
538
|
+
if route.path == conflict["path"] and list(route.methods) == list(conflict["methods"]): # type: ignore
|
|
539
|
+
fastapi_app.routes.pop(fastapi_app.routes.index(route))
|
|
540
|
+
|
|
541
|
+
fastapi_app.include_router(router)
|
|
542
|
+
|
|
543
|
+
elif self.on_route_conflict == "error":
|
|
544
|
+
conflicting_paths = [conflict["path"] for conflict in conflicts]
|
|
545
|
+
raise ValueError(f"Route conflict detected: {conflicting_paths}")
|
|
546
|
+
|
|
547
|
+
else:
|
|
548
|
+
# No conflicts, add router normally
|
|
549
|
+
fastapi_app.include_router(router)
|
|
550
|
+
|
|
266
551
|
def _get_telemetry_data(self) -> Dict[str, Any]:
|
|
267
552
|
"""Get the telemetry data for the OS"""
|
|
268
553
|
return {
|
|
@@ -272,59 +557,160 @@ class AgentOS:
|
|
|
272
557
|
"interfaces": [interface.type for interface in self.interfaces] if self.interfaces else None,
|
|
273
558
|
}
|
|
274
559
|
|
|
275
|
-
def _load_yaml_config(self, config_file_path: str) -> AgentOSConfig:
|
|
276
|
-
"""Load a YAML config file and return the configuration as an AgentOSConfig instance."""
|
|
277
|
-
from pathlib import Path
|
|
278
|
-
|
|
279
|
-
import yaml
|
|
280
|
-
|
|
281
|
-
# Validate that the path points to a YAML file
|
|
282
|
-
path = Path(config_file_path)
|
|
283
|
-
if path.suffix.lower() not in [".yaml", ".yml"]:
|
|
284
|
-
raise ValueError(f"Config file must have a .yaml or .yml extension, got: {config_file_path}")
|
|
285
|
-
|
|
286
|
-
# Load the YAML file
|
|
287
|
-
with open(config_file_path, "r") as f:
|
|
288
|
-
return AgentOSConfig.model_validate(yaml.safe_load(f))
|
|
289
|
-
|
|
290
560
|
def _auto_discover_databases(self) -> None:
|
|
291
|
-
"""Auto-discover the databases used by all contextual agents, teams and workflows."""
|
|
292
|
-
|
|
561
|
+
"""Auto-discover and initialize the databases used by all contextual agents, teams and workflows."""
|
|
562
|
+
|
|
563
|
+
dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb]]] = {}
|
|
564
|
+
knowledge_dbs: Dict[
|
|
565
|
+
str, List[Union[BaseDb, AsyncBaseDb]]
|
|
566
|
+
] = {} # Track databases specifically used for knowledge
|
|
293
567
|
|
|
294
568
|
for agent in self.agents or []:
|
|
295
569
|
if agent.db:
|
|
296
|
-
dbs
|
|
570
|
+
self._register_db_with_validation(dbs, agent.db)
|
|
297
571
|
if agent.knowledge and agent.knowledge.contents_db:
|
|
298
|
-
|
|
572
|
+
self._register_db_with_validation(knowledge_dbs, agent.knowledge.contents_db)
|
|
299
573
|
|
|
300
574
|
for team in self.teams or []:
|
|
301
575
|
if team.db:
|
|
302
|
-
dbs
|
|
576
|
+
self._register_db_with_validation(dbs, team.db)
|
|
303
577
|
if team.knowledge and team.knowledge.contents_db:
|
|
304
|
-
|
|
578
|
+
self._register_db_with_validation(knowledge_dbs, team.knowledge.contents_db)
|
|
305
579
|
|
|
306
580
|
for workflow in self.workflows or []:
|
|
307
581
|
if workflow.db:
|
|
308
|
-
dbs
|
|
582
|
+
self._register_db_with_validation(dbs, workflow.db)
|
|
583
|
+
|
|
584
|
+
for knowledge_base in self.knowledge or []:
|
|
585
|
+
if knowledge_base.contents_db:
|
|
586
|
+
self._register_db_with_validation(knowledge_dbs, knowledge_base.contents_db)
|
|
309
587
|
|
|
310
588
|
for interface in self.interfaces or []:
|
|
311
589
|
if interface.agent and interface.agent.db:
|
|
312
|
-
dbs
|
|
590
|
+
self._register_db_with_validation(dbs, interface.agent.db)
|
|
313
591
|
elif interface.team and interface.team.db:
|
|
314
|
-
dbs
|
|
592
|
+
self._register_db_with_validation(dbs, interface.team.db)
|
|
315
593
|
|
|
316
594
|
self.dbs = dbs
|
|
595
|
+
self.knowledge_dbs = knowledge_dbs
|
|
596
|
+
|
|
597
|
+
# Initialize/scaffold all discovered databases
|
|
598
|
+
if self.auto_provision_dbs:
|
|
599
|
+
import asyncio
|
|
600
|
+
import concurrent.futures
|
|
601
|
+
|
|
602
|
+
try:
|
|
603
|
+
# If we're already in an event loop, run in a separate thread
|
|
604
|
+
asyncio.get_running_loop()
|
|
605
|
+
|
|
606
|
+
def run_in_new_loop():
|
|
607
|
+
new_loop = asyncio.new_event_loop()
|
|
608
|
+
asyncio.set_event_loop(new_loop)
|
|
609
|
+
try:
|
|
610
|
+
return new_loop.run_until_complete(self._initialize_databases())
|
|
611
|
+
finally:
|
|
612
|
+
new_loop.close()
|
|
613
|
+
|
|
614
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
615
|
+
future = executor.submit(run_in_new_loop)
|
|
616
|
+
future.result() # Wait for completion
|
|
617
|
+
|
|
618
|
+
except RuntimeError:
|
|
619
|
+
# No event loop running, use asyncio.run
|
|
620
|
+
asyncio.run(self._initialize_databases())
|
|
621
|
+
|
|
622
|
+
async def _initialize_databases(self) -> None:
|
|
623
|
+
"""Initialize all discovered databases and create all Agno tables that don't exist yet."""
|
|
624
|
+
from itertools import chain
|
|
625
|
+
|
|
626
|
+
# Collect all database instances and remove duplicates by identity
|
|
627
|
+
unique_dbs = list(
|
|
628
|
+
{
|
|
629
|
+
id(db): db
|
|
630
|
+
for db in chain(
|
|
631
|
+
chain.from_iterable(self.dbs.values()), chain.from_iterable(self.knowledge_dbs.values())
|
|
632
|
+
)
|
|
633
|
+
}.values()
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
# Separate sync and async databases
|
|
637
|
+
sync_dbs: List[Tuple[str, BaseDb]] = []
|
|
638
|
+
async_dbs: List[Tuple[str, AsyncBaseDb]] = []
|
|
639
|
+
|
|
640
|
+
for db in unique_dbs:
|
|
641
|
+
target = async_dbs if isinstance(db, AsyncBaseDb) else sync_dbs
|
|
642
|
+
target.append((db.id, db)) # type: ignore
|
|
643
|
+
|
|
644
|
+
# Initialize sync databases
|
|
645
|
+
for db_id, db in sync_dbs:
|
|
646
|
+
try:
|
|
647
|
+
if hasattr(db, "_create_all_tables") and callable(getattr(db, "_create_all_tables")):
|
|
648
|
+
db._create_all_tables()
|
|
649
|
+
else:
|
|
650
|
+
log_debug(f"No table initialization needed for {db.__class__.__name__}")
|
|
651
|
+
|
|
652
|
+
except Exception as e:
|
|
653
|
+
log_warning(f"Failed to initialize {db.__class__.__name__} (id: {db_id}): {e}")
|
|
654
|
+
|
|
655
|
+
# Initialize async databases
|
|
656
|
+
for db_id, db in async_dbs:
|
|
657
|
+
try:
|
|
658
|
+
log_debug(f"Initializing async {db.__class__.__name__} (id: {db_id})")
|
|
659
|
+
|
|
660
|
+
if hasattr(db, "_create_all_tables") and callable(getattr(db, "_create_all_tables")):
|
|
661
|
+
await db._create_all_tables()
|
|
662
|
+
else:
|
|
663
|
+
log_debug(f"No table initialization needed for async {db.__class__.__name__}")
|
|
664
|
+
|
|
665
|
+
except Exception as e:
|
|
666
|
+
log_warning(f"Failed to initialize async database {db.__class__.__name__} (id: {db_id}): {e}")
|
|
667
|
+
|
|
668
|
+
def _get_db_table_names(self, db: BaseDb) -> Dict[str, str]:
|
|
669
|
+
"""Get the table names for a database"""
|
|
670
|
+
table_names = {
|
|
671
|
+
"session_table_name": db.session_table_name,
|
|
672
|
+
"culture_table_name": db.culture_table_name,
|
|
673
|
+
"memory_table_name": db.memory_table_name,
|
|
674
|
+
"metrics_table_name": db.metrics_table_name,
|
|
675
|
+
"evals_table_name": db.eval_table_name,
|
|
676
|
+
"knowledge_table_name": db.knowledge_table_name,
|
|
677
|
+
}
|
|
678
|
+
return {k: v for k, v in table_names.items() if v is not None}
|
|
679
|
+
|
|
680
|
+
def _register_db_with_validation(
|
|
681
|
+
self, registered_dbs: Dict[str, List[Union[BaseDb, AsyncBaseDb]]], db: Union[BaseDb, AsyncBaseDb]
|
|
682
|
+
) -> None:
|
|
683
|
+
"""Register a database in the contextual OS after validating it is not conflicting with registered databases"""
|
|
684
|
+
if db.id in registered_dbs:
|
|
685
|
+
registered_dbs[db.id].append(db)
|
|
686
|
+
else:
|
|
687
|
+
registered_dbs[db.id] = [db]
|
|
317
688
|
|
|
318
689
|
def _auto_discover_knowledge_instances(self) -> None:
|
|
319
690
|
"""Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
|
|
320
|
-
|
|
691
|
+
seen_ids = set()
|
|
692
|
+
knowledge_instances: List[Knowledge] = []
|
|
693
|
+
|
|
694
|
+
def _add_knowledge_if_not_duplicate(knowledge: "Knowledge") -> None:
|
|
695
|
+
"""Add knowledge instance if it's not already in the list (by object identity or db_id)."""
|
|
696
|
+
# Use database ID if available, otherwise use object ID as fallback
|
|
697
|
+
if not knowledge.contents_db:
|
|
698
|
+
return
|
|
699
|
+
if knowledge.contents_db.id in seen_ids:
|
|
700
|
+
return
|
|
701
|
+
seen_ids.add(knowledge.contents_db.id)
|
|
702
|
+
knowledge_instances.append(knowledge)
|
|
703
|
+
|
|
321
704
|
for agent in self.agents or []:
|
|
322
705
|
if agent.knowledge:
|
|
323
|
-
|
|
706
|
+
_add_knowledge_if_not_duplicate(agent.knowledge)
|
|
324
707
|
|
|
325
708
|
for team in self.teams or []:
|
|
326
709
|
if team.knowledge:
|
|
327
|
-
|
|
710
|
+
_add_knowledge_if_not_duplicate(team.knowledge)
|
|
711
|
+
|
|
712
|
+
for knowledge_base in self.knowledge or []:
|
|
713
|
+
_add_knowledge_if_not_duplicate(knowledge_base)
|
|
328
714
|
|
|
329
715
|
self.knowledge_instances = knowledge_instances
|
|
330
716
|
|
|
@@ -334,17 +720,16 @@ class AgentOS:
|
|
|
334
720
|
if session_config.dbs is None:
|
|
335
721
|
session_config.dbs = []
|
|
336
722
|
|
|
337
|
-
multiple_dbs: bool = len(self.dbs.keys()) > 1
|
|
338
723
|
dbs_with_specific_config = [db.db_id for db in session_config.dbs]
|
|
339
|
-
|
|
340
|
-
for db_id in self.dbs.keys():
|
|
724
|
+
for db_id, dbs in self.dbs.items():
|
|
341
725
|
if db_id not in dbs_with_specific_config:
|
|
726
|
+
# Collect unique table names from all databases with the same id
|
|
727
|
+
unique_tables = list(set(db.session_table_name for db in dbs))
|
|
342
728
|
session_config.dbs.append(
|
|
343
729
|
DatabaseConfig(
|
|
344
730
|
db_id=db_id,
|
|
345
|
-
domain_config=SessionDomainConfig(
|
|
346
|
-
|
|
347
|
-
),
|
|
731
|
+
domain_config=SessionDomainConfig(display_name=db_id),
|
|
732
|
+
tables=unique_tables,
|
|
348
733
|
)
|
|
349
734
|
)
|
|
350
735
|
|
|
@@ -356,17 +741,17 @@ class AgentOS:
|
|
|
356
741
|
if memory_config.dbs is None:
|
|
357
742
|
memory_config.dbs = []
|
|
358
743
|
|
|
359
|
-
multiple_dbs: bool = len(self.dbs.keys()) > 1
|
|
360
744
|
dbs_with_specific_config = [db.db_id for db in memory_config.dbs]
|
|
361
745
|
|
|
362
|
-
for db_id in self.dbs.
|
|
746
|
+
for db_id, dbs in self.dbs.items():
|
|
363
747
|
if db_id not in dbs_with_specific_config:
|
|
748
|
+
# Collect unique table names from all databases with the same id
|
|
749
|
+
unique_tables = list(set(db.memory_table_name for db in dbs))
|
|
364
750
|
memory_config.dbs.append(
|
|
365
751
|
DatabaseConfig(
|
|
366
752
|
db_id=db_id,
|
|
367
|
-
domain_config=MemoryDomainConfig(
|
|
368
|
-
|
|
369
|
-
),
|
|
753
|
+
domain_config=MemoryDomainConfig(display_name=db_id),
|
|
754
|
+
tables=unique_tables,
|
|
370
755
|
)
|
|
371
756
|
)
|
|
372
757
|
|
|
@@ -378,17 +763,15 @@ class AgentOS:
|
|
|
378
763
|
if knowledge_config.dbs is None:
|
|
379
764
|
knowledge_config.dbs = []
|
|
380
765
|
|
|
381
|
-
multiple_dbs: bool = len(self.dbs.keys()) > 1
|
|
382
766
|
dbs_with_specific_config = [db.db_id for db in knowledge_config.dbs]
|
|
383
767
|
|
|
384
|
-
for
|
|
768
|
+
# Only add databases that are actually used for knowledge contents
|
|
769
|
+
for db_id in self.knowledge_dbs.keys():
|
|
385
770
|
if db_id not in dbs_with_specific_config:
|
|
386
771
|
knowledge_config.dbs.append(
|
|
387
772
|
DatabaseConfig(
|
|
388
773
|
db_id=db_id,
|
|
389
|
-
domain_config=KnowledgeDomainConfig(
|
|
390
|
-
display_name="Knowledge" if not multiple_dbs else "Knowledge in database " + db_id
|
|
391
|
-
),
|
|
774
|
+
domain_config=KnowledgeDomainConfig(display_name=db_id),
|
|
392
775
|
)
|
|
393
776
|
)
|
|
394
777
|
|
|
@@ -400,17 +783,17 @@ class AgentOS:
|
|
|
400
783
|
if metrics_config.dbs is None:
|
|
401
784
|
metrics_config.dbs = []
|
|
402
785
|
|
|
403
|
-
multiple_dbs: bool = len(self.dbs.keys()) > 1
|
|
404
786
|
dbs_with_specific_config = [db.db_id for db in metrics_config.dbs]
|
|
405
787
|
|
|
406
|
-
for db_id in self.dbs.
|
|
788
|
+
for db_id, dbs in self.dbs.items():
|
|
407
789
|
if db_id not in dbs_with_specific_config:
|
|
790
|
+
# Collect unique table names from all databases with the same id
|
|
791
|
+
unique_tables = list(set(db.metrics_table_name for db in dbs))
|
|
408
792
|
metrics_config.dbs.append(
|
|
409
793
|
DatabaseConfig(
|
|
410
794
|
db_id=db_id,
|
|
411
|
-
domain_config=MetricsDomainConfig(
|
|
412
|
-
|
|
413
|
-
),
|
|
795
|
+
domain_config=MetricsDomainConfig(display_name=db_id),
|
|
796
|
+
tables=unique_tables,
|
|
414
797
|
)
|
|
415
798
|
)
|
|
416
799
|
|
|
@@ -422,45 +805,22 @@ class AgentOS:
|
|
|
422
805
|
if evals_config.dbs is None:
|
|
423
806
|
evals_config.dbs = []
|
|
424
807
|
|
|
425
|
-
multiple_dbs: bool = len(self.dbs.keys()) > 1
|
|
426
808
|
dbs_with_specific_config = [db.db_id for db in evals_config.dbs]
|
|
427
809
|
|
|
428
|
-
for db_id in self.dbs.
|
|
810
|
+
for db_id, dbs in self.dbs.items():
|
|
429
811
|
if db_id not in dbs_with_specific_config:
|
|
812
|
+
# Collect unique table names from all databases with the same id
|
|
813
|
+
unique_tables = list(set(db.eval_table_name for db in dbs))
|
|
430
814
|
evals_config.dbs.append(
|
|
431
815
|
DatabaseConfig(
|
|
432
816
|
db_id=db_id,
|
|
433
|
-
domain_config=EvalsDomainConfig(
|
|
434
|
-
|
|
435
|
-
),
|
|
817
|
+
domain_config=EvalsDomainConfig(display_name=db_id),
|
|
818
|
+
tables=unique_tables,
|
|
436
819
|
)
|
|
437
820
|
)
|
|
438
821
|
|
|
439
822
|
return evals_config
|
|
440
823
|
|
|
441
|
-
def _setup_routers(self) -> None:
|
|
442
|
-
"""Add all routers to the FastAPI app."""
|
|
443
|
-
if not self.dbs or not self.fastapi_app:
|
|
444
|
-
return
|
|
445
|
-
|
|
446
|
-
routers = [
|
|
447
|
-
get_session_router(dbs=self.dbs),
|
|
448
|
-
get_memory_router(dbs=self.dbs),
|
|
449
|
-
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
450
|
-
get_metrics_router(dbs=self.dbs),
|
|
451
|
-
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
452
|
-
]
|
|
453
|
-
|
|
454
|
-
for router in routers:
|
|
455
|
-
self.fastapi_app.include_router(router)
|
|
456
|
-
|
|
457
|
-
def set_os_id(self) -> str:
|
|
458
|
-
# If os_id is already set, keep it instead of overriding with UUID
|
|
459
|
-
if self.os_id is None:
|
|
460
|
-
self.os_id = str(uuid4())
|
|
461
|
-
|
|
462
|
-
return self.os_id
|
|
463
|
-
|
|
464
824
|
def serve(
|
|
465
825
|
self,
|
|
466
826
|
app: Union[str, FastAPI],
|
|
@@ -469,6 +829,7 @@ class AgentOS:
|
|
|
469
829
|
port: int = 7777,
|
|
470
830
|
reload: bool = False,
|
|
471
831
|
workers: Optional[int] = None,
|
|
832
|
+
access_log: bool = False,
|
|
472
833
|
**kwargs,
|
|
473
834
|
):
|
|
474
835
|
import uvicorn
|
|
@@ -482,13 +843,17 @@ class AgentOS:
|
|
|
482
843
|
from rich.align import Align
|
|
483
844
|
from rich.console import Console, Group
|
|
484
845
|
|
|
485
|
-
|
|
486
|
-
|
|
846
|
+
panel_group = [
|
|
847
|
+
Align.center(f"[bold cyan]{public_endpoint}[/bold cyan]"),
|
|
848
|
+
Align.center(f"\n\n[bold dark_orange]OS running on:[/bold dark_orange] http://{host}:{port}"),
|
|
849
|
+
]
|
|
850
|
+
if bool(self.settings.os_security_key):
|
|
851
|
+
panel_group.append(Align.center("\n\n[bold chartreuse3]:lock: Security Enabled[/bold chartreuse3]"))
|
|
487
852
|
|
|
488
853
|
console = Console()
|
|
489
854
|
console.print(
|
|
490
855
|
Panel(
|
|
491
|
-
Group(
|
|
856
|
+
Group(*panel_group),
|
|
492
857
|
title="AgentOS",
|
|
493
858
|
expand=False,
|
|
494
859
|
border_style="dark_orange",
|
|
@@ -497,4 +862,4 @@ class AgentOS:
|
|
|
497
862
|
)
|
|
498
863
|
)
|
|
499
864
|
|
|
500
|
-
uvicorn.run(app=app, host=host, port=port, reload=reload, workers=workers, **kwargs)
|
|
865
|
+
uvicorn.run(app=app, host=host, port=port, reload=reload, workers=workers, access_log=access_log, **kwargs)
|