agno 2.0.0rc2__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 +6009 -2874
- 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 +595 -187
- 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 +3 -0
- agno/knowledge/types.py +9 -0
- agno/knowledge/utils.py +20 -0
- agno/media.py +339 -266
- 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 +1011 -566
- 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 +110 -37
- 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 +143 -4
- 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 +60 -6
- agno/models/openai/chat.py +102 -43
- 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 +81 -5
- 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 -175
- 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 +266 -112
- agno/run/base.py +53 -24
- agno/run/team.py +252 -111
- agno/run/workflow.py +156 -45
- 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 -1692
- agno/tools/brightdata.py +3 -3
- agno/tools/cartesia.py +3 -5
- agno/tools/dalle.py +9 -8
- agno/tools/decorator.py +4 -2
- agno/tools/desi_vocal.py +2 -2
- agno/tools/duckduckgo.py +15 -11
- agno/tools/e2b.py +20 -13
- agno/tools/eleven_labs.py +26 -28
- agno/tools/exa.py +21 -16
- agno/tools/fal.py +4 -4
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +350 -0
- agno/tools/firecrawl.py +4 -4
- agno/tools/function.py +257 -37
- agno/tools/giphy.py +2 -2
- 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/lumalab.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/azure_openai.py +2 -2
- agno/tools/models/gemini.py +3 -3
- agno/tools/models/groq.py +3 -5
- agno/tools/models/nebius.py +7 -7
- agno/tools/models_labs.py +25 -15
- agno/tools/notion.py +204 -0
- agno/tools/openai.py +4 -9
- agno/tools/opencv.py +3 -3
- agno/tools/parallel.py +314 -0
- agno/tools/replicate.py +7 -7
- 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 +222 -7
- agno/utils/gemini.py +181 -23
- 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 +95 -5
- 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/models/cohere.py +1 -1
- agno/utils/models/watsonx.py +1 -1
- agno/utils/openai.py +1 -1
- 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 +183 -135
- 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 +645 -136
- agno/workflow/steps.py +65 -6
- agno/workflow/types.py +71 -33
- agno/workflow/workflow.py +2113 -300
- agno-2.3.0.dist-info/METADATA +618 -0
- agno-2.3.0.dist-info/RECORD +577 -0
- agno-2.3.0.dist-info/licenses/LICENSE +201 -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.0rc2.dist-info/METADATA +0 -355
- agno-2.0.0rc2.dist-info/RECORD +0 -515
- agno-2.0.0rc2.dist-info/licenses/LICENSE +0 -375
- {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
- {agno-2.0.0rc2.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,21 +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
|
|
39
|
-
from agno.
|
|
49
|
+
from agno.utils.log import log_debug, log_error, log_warning
|
|
50
|
+
from agno.utils.string import generate_id, generate_id_from_name
|
|
40
51
|
from agno.workflow.workflow import Workflow
|
|
41
52
|
|
|
42
53
|
|
|
43
54
|
@asynccontextmanager
|
|
44
|
-
async def mcp_lifespan(
|
|
55
|
+
async def mcp_lifespan(_, mcp_tools):
|
|
45
56
|
"""Manage MCP connection lifecycle inside a FastAPI app"""
|
|
46
57
|
# Startup logic: connect to all contextual MCP servers
|
|
47
58
|
for tool in mcp_tools:
|
|
@@ -54,100 +65,223 @@ async def mcp_lifespan(app, mcp_tools: List[Union[MCPTools, MultiMCPTools]]):
|
|
|
54
65
|
await tool.close()
|
|
55
66
|
|
|
56
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
|
+
|
|
57
92
|
class AgentOS:
|
|
58
93
|
def __init__(
|
|
59
94
|
self,
|
|
60
|
-
|
|
95
|
+
id: Optional[str] = None,
|
|
61
96
|
name: Optional[str] = None,
|
|
62
97
|
description: Optional[str] = None,
|
|
63
98
|
version: Optional[str] = None,
|
|
64
99
|
agents: Optional[List[Agent]] = None,
|
|
65
100
|
teams: Optional[List[Team]] = None,
|
|
66
101
|
workflows: Optional[List[Workflow]] = None,
|
|
102
|
+
knowledge: Optional[List[Knowledge]] = None,
|
|
67
103
|
interfaces: Optional[List[BaseInterface]] = None,
|
|
104
|
+
a2a_interface: bool = False,
|
|
68
105
|
config: Optional[Union[str, AgentOSConfig]] = None,
|
|
69
106
|
settings: Optional[AgnoAPISettings] = None,
|
|
70
|
-
fastapi_app: Optional[FastAPI] = None,
|
|
71
107
|
lifespan: Optional[Any] = None,
|
|
72
|
-
|
|
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",
|
|
73
111
|
telemetry: bool = True,
|
|
112
|
+
auto_provision_dbs: bool = True,
|
|
74
113
|
):
|
|
75
|
-
|
|
76
|
-
|
|
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.")
|
|
77
138
|
|
|
78
|
-
self.config =
|
|
139
|
+
self.config = load_yaml_config(config) if isinstance(config, str) else config
|
|
79
140
|
|
|
80
141
|
self.agents: Optional[List[Agent]] = agents
|
|
81
142
|
self.workflows: Optional[List[Workflow]] = workflows
|
|
82
143
|
self.teams: Optional[List[Team]] = teams
|
|
83
144
|
self.interfaces = interfaces or []
|
|
84
|
-
|
|
145
|
+
self.a2a_interface = a2a_interface
|
|
146
|
+
self.knowledge = knowledge
|
|
85
147
|
self.settings: AgnoAPISettings = settings or AgnoAPISettings()
|
|
86
|
-
|
|
148
|
+
self.auto_provision_dbs = auto_provision_dbs
|
|
87
149
|
self._app_set = False
|
|
88
|
-
|
|
89
|
-
if
|
|
90
|
-
self.
|
|
150
|
+
|
|
151
|
+
if base_app:
|
|
152
|
+
self.base_app: Optional[FastAPI] = base_app
|
|
91
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
|
|
92
159
|
|
|
93
160
|
self.interfaces = interfaces or []
|
|
94
161
|
|
|
95
|
-
self.os_id: Optional[str] = os_id
|
|
96
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
|
+
|
|
97
168
|
self.version = version
|
|
98
169
|
self.description = description
|
|
99
170
|
|
|
100
171
|
self.telemetry = telemetry
|
|
101
172
|
|
|
102
|
-
self.
|
|
173
|
+
self.enable_mcp_server = enable_mcp_server
|
|
103
174
|
self.lifespan = lifespan
|
|
104
175
|
|
|
105
176
|
# List of all MCP tools used inside the AgentOS
|
|
106
|
-
self.mcp_tools: List[
|
|
107
|
-
|
|
108
|
-
if self.agents:
|
|
109
|
-
for agent in self.agents:
|
|
110
|
-
# Track all MCP tools to later handle their connection
|
|
111
|
-
if agent.tools:
|
|
112
|
-
for tool in agent.tools:
|
|
113
|
-
if isinstance(tool, MCPTools) or isinstance(tool, MultiMCPTools):
|
|
114
|
-
self.mcp_tools.append(tool)
|
|
177
|
+
self.mcp_tools: List[Any] = []
|
|
178
|
+
self._mcp_app: Optional[Any] = None
|
|
115
179
|
|
|
116
|
-
|
|
180
|
+
self._initialize_agents()
|
|
181
|
+
self._initialize_teams()
|
|
182
|
+
self._initialize_workflows()
|
|
117
183
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if self.teams:
|
|
122
|
-
for team in self.teams:
|
|
123
|
-
# Track all MCP tools to later handle their connection
|
|
124
|
-
if team.tools:
|
|
125
|
-
for tool in team.tools:
|
|
126
|
-
if isinstance(tool, MCPTools) or isinstance(tool, MultiMCPTools):
|
|
127
|
-
self.mcp_tools.append(tool)
|
|
184
|
+
if self.telemetry:
|
|
185
|
+
from agno.api.os import OSLaunch, log_os_telemetry
|
|
128
186
|
|
|
129
|
-
|
|
187
|
+
log_os_telemetry(launch=OSLaunch(os_id=self.id, data=self._get_telemetry_data()))
|
|
130
188
|
|
|
131
|
-
|
|
132
|
-
|
|
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.
|
|
133
192
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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__
|
|
140
200
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
# TODO: track MCP tools in workflow members
|
|
144
|
-
if not workflow.id:
|
|
145
|
-
workflow.id = generate_id(workflow.name)
|
|
201
|
+
try:
|
|
202
|
+
from inspect import signature
|
|
146
203
|
|
|
147
|
-
|
|
148
|
-
|
|
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)
|
|
149
229
|
|
|
150
|
-
|
|
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())
|
|
151
285
|
|
|
152
286
|
def _make_app(self, lifespan: Optional[Any] = None) -> FastAPI:
|
|
153
287
|
# Adjust the FastAPI app lifespan to handle MCP connections if relevant
|
|
@@ -164,7 +298,7 @@ class AgentOS:
|
|
|
164
298
|
async with mcp_tools_lifespan(app): # type: ignore
|
|
165
299
|
yield
|
|
166
300
|
|
|
167
|
-
app_lifespan = combined_lifespan
|
|
301
|
+
app_lifespan = combined_lifespan
|
|
168
302
|
else:
|
|
169
303
|
app_lifespan = mcp_tools_lifespan
|
|
170
304
|
|
|
@@ -178,77 +312,175 @@ class AgentOS:
|
|
|
178
312
|
lifespan=app_lifespan,
|
|
179
313
|
)
|
|
180
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
|
+
|
|
181
372
|
def get_app(self) -> FastAPI:
|
|
182
|
-
if
|
|
183
|
-
|
|
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:
|
|
184
409
|
from contextlib import asynccontextmanager
|
|
185
410
|
|
|
186
411
|
from agno.os.mcp import get_mcp_server
|
|
187
412
|
|
|
188
|
-
self.
|
|
413
|
+
self._mcp_app = get_mcp_server(self)
|
|
189
414
|
|
|
190
|
-
final_lifespan = self.
|
|
415
|
+
final_lifespan = self._mcp_app.lifespan # type: ignore
|
|
191
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
|
+
|
|
192
420
|
# Combine both lifespans
|
|
193
421
|
@asynccontextmanager
|
|
194
422
|
async def combined_lifespan(app: FastAPI):
|
|
195
423
|
# Run both lifespans
|
|
196
|
-
async with
|
|
197
|
-
async with self.
|
|
424
|
+
async with wrapped_lifespan(app): # type: ignore
|
|
425
|
+
async with self._mcp_app.lifespan(app): # type: ignore
|
|
198
426
|
yield
|
|
199
427
|
|
|
200
428
|
final_lifespan = combined_lifespan # type: ignore
|
|
201
429
|
|
|
202
|
-
|
|
430
|
+
fastapi_app = self._make_app(lifespan=final_lifespan)
|
|
203
431
|
else:
|
|
204
|
-
|
|
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)
|
|
205
436
|
|
|
206
|
-
|
|
207
|
-
self.fastapi_app.include_router(get_base_router(self, settings=self.settings))
|
|
437
|
+
fastapi_app = self._make_app(lifespan=wrapped_user_lifespan)
|
|
208
438
|
|
|
209
|
-
|
|
210
|
-
interface_router = interface.get_router()
|
|
211
|
-
self.fastapi_app.include_router(interface_router)
|
|
439
|
+
self._add_built_in_routes(app=fastapi_app)
|
|
212
440
|
|
|
213
441
|
self._auto_discover_databases()
|
|
214
442
|
self._auto_discover_knowledge_instances()
|
|
215
|
-
|
|
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)
|
|
216
454
|
|
|
217
455
|
# Mount MCP if needed
|
|
218
|
-
if self.
|
|
219
|
-
|
|
456
|
+
if self.enable_mcp_server and self._mcp_app:
|
|
457
|
+
fastapi_app.mount("/", self._mcp_app)
|
|
220
458
|
|
|
221
|
-
# Add middleware (only if app is not set)
|
|
222
459
|
if not self._app_set:
|
|
223
460
|
|
|
224
|
-
@
|
|
225
|
-
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}")
|
|
226
464
|
return JSONResponse(
|
|
227
465
|
status_code=exc.status_code,
|
|
228
466
|
content={"detail": str(exc.detail)},
|
|
229
467
|
)
|
|
230
468
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
except Exception as e:
|
|
235
|
-
return JSONResponse(
|
|
236
|
-
status_code=e.status_code if hasattr(e, "status_code") else 500, # type: ignore
|
|
237
|
-
content={"detail": str(e)},
|
|
238
|
-
)
|
|
469
|
+
@fastapi_app.exception_handler(Exception)
|
|
470
|
+
async def general_exception_handler(_: Request, exc: Exception) -> JSONResponse:
|
|
471
|
+
import traceback
|
|
239
472
|
|
|
240
|
-
|
|
473
|
+
log_error(f"Unhandled exception:\n{traceback.format_exc(limit=5)}")
|
|
241
474
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
)
|
|
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
|
|
250
482
|
|
|
251
|
-
return
|
|
483
|
+
return fastapi_app
|
|
252
484
|
|
|
253
485
|
def get_routes(self) -> List[Any]:
|
|
254
486
|
"""Retrieve all routes from the FastAPI app.
|
|
@@ -260,6 +492,62 @@ class AgentOS:
|
|
|
260
492
|
|
|
261
493
|
return app.routes
|
|
262
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
|
+
|
|
263
551
|
def _get_telemetry_data(self) -> Dict[str, Any]:
|
|
264
552
|
"""Get the telemetry data for the OS"""
|
|
265
553
|
return {
|
|
@@ -269,59 +557,160 @@ class AgentOS:
|
|
|
269
557
|
"interfaces": [interface.type for interface in self.interfaces] if self.interfaces else None,
|
|
270
558
|
}
|
|
271
559
|
|
|
272
|
-
def _load_yaml_config(self, config_file_path: str) -> AgentOSConfig:
|
|
273
|
-
"""Load a YAML config file and return the configuration as an AgentOSConfig instance."""
|
|
274
|
-
from pathlib import Path
|
|
275
|
-
|
|
276
|
-
import yaml
|
|
277
|
-
|
|
278
|
-
# Validate that the path points to a YAML file
|
|
279
|
-
path = Path(config_file_path)
|
|
280
|
-
if path.suffix.lower() not in [".yaml", ".yml"]:
|
|
281
|
-
raise ValueError(f"Config file must have a .yaml or .yml extension, got: {config_file_path}")
|
|
282
|
-
|
|
283
|
-
# Load the YAML file
|
|
284
|
-
with open(config_file_path, "r") as f:
|
|
285
|
-
return AgentOSConfig.model_validate(yaml.safe_load(f))
|
|
286
|
-
|
|
287
560
|
def _auto_discover_databases(self) -> None:
|
|
288
|
-
"""Auto-discover the databases used by all contextual agents, teams and workflows."""
|
|
289
|
-
|
|
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
|
|
290
567
|
|
|
291
568
|
for agent in self.agents or []:
|
|
292
569
|
if agent.db:
|
|
293
|
-
dbs
|
|
570
|
+
self._register_db_with_validation(dbs, agent.db)
|
|
294
571
|
if agent.knowledge and agent.knowledge.contents_db:
|
|
295
|
-
|
|
572
|
+
self._register_db_with_validation(knowledge_dbs, agent.knowledge.contents_db)
|
|
296
573
|
|
|
297
574
|
for team in self.teams or []:
|
|
298
575
|
if team.db:
|
|
299
|
-
dbs
|
|
576
|
+
self._register_db_with_validation(dbs, team.db)
|
|
300
577
|
if team.knowledge and team.knowledge.contents_db:
|
|
301
|
-
|
|
578
|
+
self._register_db_with_validation(knowledge_dbs, team.knowledge.contents_db)
|
|
302
579
|
|
|
303
580
|
for workflow in self.workflows or []:
|
|
304
581
|
if workflow.db:
|
|
305
|
-
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)
|
|
306
587
|
|
|
307
588
|
for interface in self.interfaces or []:
|
|
308
589
|
if interface.agent and interface.agent.db:
|
|
309
|
-
dbs
|
|
590
|
+
self._register_db_with_validation(dbs, interface.agent.db)
|
|
310
591
|
elif interface.team and interface.team.db:
|
|
311
|
-
dbs
|
|
592
|
+
self._register_db_with_validation(dbs, interface.team.db)
|
|
312
593
|
|
|
313
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]
|
|
314
688
|
|
|
315
689
|
def _auto_discover_knowledge_instances(self) -> None:
|
|
316
690
|
"""Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
|
|
317
|
-
|
|
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
|
+
|
|
318
704
|
for agent in self.agents or []:
|
|
319
705
|
if agent.knowledge:
|
|
320
|
-
|
|
706
|
+
_add_knowledge_if_not_duplicate(agent.knowledge)
|
|
321
707
|
|
|
322
708
|
for team in self.teams or []:
|
|
323
709
|
if team.knowledge:
|
|
324
|
-
|
|
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)
|
|
325
714
|
|
|
326
715
|
self.knowledge_instances = knowledge_instances
|
|
327
716
|
|
|
@@ -331,17 +720,16 @@ class AgentOS:
|
|
|
331
720
|
if session_config.dbs is None:
|
|
332
721
|
session_config.dbs = []
|
|
333
722
|
|
|
334
|
-
multiple_dbs: bool = len(self.dbs.keys()) > 1
|
|
335
723
|
dbs_with_specific_config = [db.db_id for db in session_config.dbs]
|
|
336
|
-
|
|
337
|
-
for db_id in self.dbs.keys():
|
|
724
|
+
for db_id, dbs in self.dbs.items():
|
|
338
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))
|
|
339
728
|
session_config.dbs.append(
|
|
340
729
|
DatabaseConfig(
|
|
341
730
|
db_id=db_id,
|
|
342
|
-
domain_config=SessionDomainConfig(
|
|
343
|
-
|
|
344
|
-
),
|
|
731
|
+
domain_config=SessionDomainConfig(display_name=db_id),
|
|
732
|
+
tables=unique_tables,
|
|
345
733
|
)
|
|
346
734
|
)
|
|
347
735
|
|
|
@@ -353,17 +741,17 @@ class AgentOS:
|
|
|
353
741
|
if memory_config.dbs is None:
|
|
354
742
|
memory_config.dbs = []
|
|
355
743
|
|
|
356
|
-
multiple_dbs: bool = len(self.dbs.keys()) > 1
|
|
357
744
|
dbs_with_specific_config = [db.db_id for db in memory_config.dbs]
|
|
358
745
|
|
|
359
|
-
for db_id in self.dbs.
|
|
746
|
+
for db_id, dbs in self.dbs.items():
|
|
360
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))
|
|
361
750
|
memory_config.dbs.append(
|
|
362
751
|
DatabaseConfig(
|
|
363
752
|
db_id=db_id,
|
|
364
|
-
domain_config=MemoryDomainConfig(
|
|
365
|
-
|
|
366
|
-
),
|
|
753
|
+
domain_config=MemoryDomainConfig(display_name=db_id),
|
|
754
|
+
tables=unique_tables,
|
|
367
755
|
)
|
|
368
756
|
)
|
|
369
757
|
|
|
@@ -375,17 +763,15 @@ class AgentOS:
|
|
|
375
763
|
if knowledge_config.dbs is None:
|
|
376
764
|
knowledge_config.dbs = []
|
|
377
765
|
|
|
378
|
-
multiple_dbs: bool = len(self.dbs.keys()) > 1
|
|
379
766
|
dbs_with_specific_config = [db.db_id for db in knowledge_config.dbs]
|
|
380
767
|
|
|
381
|
-
for
|
|
768
|
+
# Only add databases that are actually used for knowledge contents
|
|
769
|
+
for db_id in self.knowledge_dbs.keys():
|
|
382
770
|
if db_id not in dbs_with_specific_config:
|
|
383
771
|
knowledge_config.dbs.append(
|
|
384
772
|
DatabaseConfig(
|
|
385
773
|
db_id=db_id,
|
|
386
|
-
domain_config=KnowledgeDomainConfig(
|
|
387
|
-
display_name="Knowledge" if not multiple_dbs else "Knowledge in database " + db_id
|
|
388
|
-
),
|
|
774
|
+
domain_config=KnowledgeDomainConfig(display_name=db_id),
|
|
389
775
|
)
|
|
390
776
|
)
|
|
391
777
|
|
|
@@ -397,17 +783,17 @@ class AgentOS:
|
|
|
397
783
|
if metrics_config.dbs is None:
|
|
398
784
|
metrics_config.dbs = []
|
|
399
785
|
|
|
400
|
-
multiple_dbs: bool = len(self.dbs.keys()) > 1
|
|
401
786
|
dbs_with_specific_config = [db.db_id for db in metrics_config.dbs]
|
|
402
787
|
|
|
403
|
-
for db_id in self.dbs.
|
|
788
|
+
for db_id, dbs in self.dbs.items():
|
|
404
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))
|
|
405
792
|
metrics_config.dbs.append(
|
|
406
793
|
DatabaseConfig(
|
|
407
794
|
db_id=db_id,
|
|
408
|
-
domain_config=MetricsDomainConfig(
|
|
409
|
-
|
|
410
|
-
),
|
|
795
|
+
domain_config=MetricsDomainConfig(display_name=db_id),
|
|
796
|
+
tables=unique_tables,
|
|
411
797
|
)
|
|
412
798
|
)
|
|
413
799
|
|
|
@@ -419,45 +805,22 @@ class AgentOS:
|
|
|
419
805
|
if evals_config.dbs is None:
|
|
420
806
|
evals_config.dbs = []
|
|
421
807
|
|
|
422
|
-
multiple_dbs: bool = len(self.dbs.keys()) > 1
|
|
423
808
|
dbs_with_specific_config = [db.db_id for db in evals_config.dbs]
|
|
424
809
|
|
|
425
|
-
for db_id in self.dbs.
|
|
810
|
+
for db_id, dbs in self.dbs.items():
|
|
426
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))
|
|
427
814
|
evals_config.dbs.append(
|
|
428
815
|
DatabaseConfig(
|
|
429
816
|
db_id=db_id,
|
|
430
|
-
domain_config=EvalsDomainConfig(
|
|
431
|
-
|
|
432
|
-
),
|
|
817
|
+
domain_config=EvalsDomainConfig(display_name=db_id),
|
|
818
|
+
tables=unique_tables,
|
|
433
819
|
)
|
|
434
820
|
)
|
|
435
821
|
|
|
436
822
|
return evals_config
|
|
437
823
|
|
|
438
|
-
def _setup_routers(self) -> None:
|
|
439
|
-
"""Add all routers to the FastAPI app."""
|
|
440
|
-
if not self.dbs or not self.fastapi_app:
|
|
441
|
-
return
|
|
442
|
-
|
|
443
|
-
routers = [
|
|
444
|
-
get_session_router(dbs=self.dbs),
|
|
445
|
-
get_memory_router(dbs=self.dbs),
|
|
446
|
-
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
447
|
-
get_metrics_router(dbs=self.dbs),
|
|
448
|
-
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
449
|
-
]
|
|
450
|
-
|
|
451
|
-
for router in routers:
|
|
452
|
-
self.fastapi_app.include_router(router)
|
|
453
|
-
|
|
454
|
-
def set_os_id(self) -> str:
|
|
455
|
-
# If os_id is already set, keep it instead of overriding with UUID
|
|
456
|
-
if self.os_id is None:
|
|
457
|
-
self.os_id = str(uuid4())
|
|
458
|
-
|
|
459
|
-
return self.os_id
|
|
460
|
-
|
|
461
824
|
def serve(
|
|
462
825
|
self,
|
|
463
826
|
app: Union[str, FastAPI],
|
|
@@ -466,6 +829,7 @@ class AgentOS:
|
|
|
466
829
|
port: int = 7777,
|
|
467
830
|
reload: bool = False,
|
|
468
831
|
workers: Optional[int] = None,
|
|
832
|
+
access_log: bool = False,
|
|
469
833
|
**kwargs,
|
|
470
834
|
):
|
|
471
835
|
import uvicorn
|
|
@@ -479,13 +843,17 @@ class AgentOS:
|
|
|
479
843
|
from rich.align import Align
|
|
480
844
|
from rich.console import Console, Group
|
|
481
845
|
|
|
482
|
-
|
|
483
|
-
|
|
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]"))
|
|
484
852
|
|
|
485
853
|
console = Console()
|
|
486
854
|
console.print(
|
|
487
855
|
Panel(
|
|
488
|
-
Group(
|
|
856
|
+
Group(*panel_group),
|
|
489
857
|
title="AgentOS",
|
|
490
858
|
expand=False,
|
|
491
859
|
border_style="dark_orange",
|
|
@@ -494,4 +862,4 @@ class AgentOS:
|
|
|
494
862
|
)
|
|
495
863
|
)
|
|
496
864
|
|
|
497
|
-
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)
|