agno 2.0.3__py3-none-any.whl → 2.0.5__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 +229 -164
- agno/db/dynamo/dynamo.py +8 -0
- agno/db/firestore/firestore.py +8 -0
- agno/db/gcs_json/gcs_json_db.py +9 -0
- agno/db/json/json_db.py +8 -0
- agno/db/migrations/v1_to_v2.py +191 -23
- agno/db/mongo/mongo.py +68 -0
- agno/db/mysql/mysql.py +13 -3
- agno/db/mysql/schemas.py +27 -27
- agno/db/postgres/postgres.py +19 -11
- agno/db/redis/redis.py +6 -0
- agno/db/singlestore/schemas.py +1 -1
- agno/db/singlestore/singlestore.py +8 -1
- agno/db/sqlite/sqlite.py +12 -3
- agno/integrations/discord/client.py +1 -0
- agno/knowledge/knowledge.py +92 -66
- agno/knowledge/reader/reader_factory.py +7 -3
- agno/knowledge/reader/web_search_reader.py +12 -6
- agno/models/base.py +2 -2
- agno/models/message.py +109 -0
- agno/models/openai/chat.py +3 -0
- agno/models/openai/responses.py +12 -0
- agno/models/response.py +5 -0
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +25 -0
- agno/os/app.py +164 -41
- agno/os/auth.py +24 -14
- agno/os/interfaces/agui/utils.py +98 -134
- agno/os/router.py +128 -55
- agno/os/routers/evals/utils.py +9 -9
- agno/os/routers/health.py +25 -0
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/knowledge.py +11 -11
- agno/os/routers/session/session.py +24 -8
- agno/os/schema.py +29 -2
- agno/os/utils.py +0 -8
- agno/run/agent.py +3 -3
- agno/run/team.py +3 -3
- agno/run/workflow.py +64 -10
- agno/session/team.py +1 -0
- agno/team/team.py +189 -94
- agno/tools/duckduckgo.py +15 -11
- agno/tools/googlesearch.py +1 -1
- agno/tools/mem0.py +11 -17
- agno/tools/memory.py +34 -6
- agno/utils/common.py +90 -1
- agno/utils/streamlit.py +14 -8
- agno/utils/string.py +32 -0
- agno/utils/tools.py +1 -1
- agno/vectordb/chroma/chromadb.py +8 -2
- agno/workflow/step.py +115 -16
- agno/workflow/workflow.py +16 -13
- {agno-2.0.3.dist-info → agno-2.0.5.dist-info}/METADATA +6 -5
- {agno-2.0.3.dist-info → agno-2.0.5.dist-info}/RECORD +57 -54
- agno/knowledge/reader/url_reader.py +0 -128
- {agno-2.0.3.dist-info → agno-2.0.5.dist-info}/WHEEL +0 -0
- {agno-2.0.3.dist-info → agno-2.0.5.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.3.dist-info → agno-2.0.5.dist-info}/top_level.txt +0 -0
agno/models/message.py
CHANGED
|
@@ -121,6 +121,115 @@ class Message(BaseModel):
|
|
|
121
121
|
|
|
122
122
|
@classmethod
|
|
123
123
|
def from_dict(cls, data: Dict[str, Any]) -> "Message":
|
|
124
|
+
# Handle image reconstruction properly
|
|
125
|
+
if "images" in data and data["images"]:
|
|
126
|
+
reconstructed_images = []
|
|
127
|
+
for i, img_data in enumerate(data["images"]):
|
|
128
|
+
if isinstance(img_data, dict):
|
|
129
|
+
# If content is base64, decode it back to bytes
|
|
130
|
+
if "content" in img_data and isinstance(img_data["content"], str):
|
|
131
|
+
reconstructed_images.append(
|
|
132
|
+
Image.from_base64(
|
|
133
|
+
img_data["content"],
|
|
134
|
+
id=img_data.get("id"),
|
|
135
|
+
mime_type=img_data.get("mime_type"),
|
|
136
|
+
format=img_data.get("format"),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
# Regular image (filepath/url)
|
|
141
|
+
reconstructed_images.append(Image(**img_data))
|
|
142
|
+
else:
|
|
143
|
+
reconstructed_images.append(img_data)
|
|
144
|
+
data["images"] = reconstructed_images
|
|
145
|
+
|
|
146
|
+
# Handle audio reconstruction properly
|
|
147
|
+
if "audio" in data and data["audio"]:
|
|
148
|
+
reconstructed_audio = []
|
|
149
|
+
for i, aud_data in enumerate(data["audio"]):
|
|
150
|
+
if isinstance(aud_data, dict):
|
|
151
|
+
# If content is base64, decode it back to bytes
|
|
152
|
+
if "content" in aud_data and isinstance(aud_data["content"], str):
|
|
153
|
+
reconstructed_audio.append(
|
|
154
|
+
Audio.from_base64(
|
|
155
|
+
aud_data["content"],
|
|
156
|
+
id=aud_data.get("id"),
|
|
157
|
+
mime_type=aud_data.get("mime_type"),
|
|
158
|
+
transcript=aud_data.get("transcript"),
|
|
159
|
+
expires_at=aud_data.get("expires_at"),
|
|
160
|
+
sample_rate=aud_data.get("sample_rate", 24000),
|
|
161
|
+
channels=aud_data.get("channels", 1),
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
reconstructed_audio.append(Audio(**aud_data))
|
|
166
|
+
else:
|
|
167
|
+
reconstructed_audio.append(aud_data)
|
|
168
|
+
data["audio"] = reconstructed_audio
|
|
169
|
+
|
|
170
|
+
# Handle video reconstruction properly
|
|
171
|
+
if "videos" in data and data["videos"]:
|
|
172
|
+
reconstructed_videos = []
|
|
173
|
+
for i, vid_data in enumerate(data["videos"]):
|
|
174
|
+
if isinstance(vid_data, dict):
|
|
175
|
+
# If content is base64, decode it back to bytes
|
|
176
|
+
if "content" in vid_data and isinstance(vid_data["content"], str):
|
|
177
|
+
reconstructed_videos.append(
|
|
178
|
+
Video.from_base64(
|
|
179
|
+
vid_data["content"],
|
|
180
|
+
id=vid_data.get("id"),
|
|
181
|
+
mime_type=vid_data.get("mime_type"),
|
|
182
|
+
format=vid_data.get("format"),
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
else:
|
|
186
|
+
reconstructed_videos.append(Video(**vid_data))
|
|
187
|
+
else:
|
|
188
|
+
reconstructed_videos.append(vid_data)
|
|
189
|
+
data["videos"] = reconstructed_videos
|
|
190
|
+
|
|
191
|
+
if "audio_output" in data and data["audio_output"]:
|
|
192
|
+
aud_data = data["audio_output"]
|
|
193
|
+
if isinstance(aud_data, dict):
|
|
194
|
+
if "content" in aud_data and isinstance(aud_data["content"], str):
|
|
195
|
+
data["audio_output"] = Audio.from_base64(
|
|
196
|
+
aud_data["content"],
|
|
197
|
+
id=aud_data.get("id"),
|
|
198
|
+
mime_type=aud_data.get("mime_type"),
|
|
199
|
+
transcript=aud_data.get("transcript"),
|
|
200
|
+
expires_at=aud_data.get("expires_at"),
|
|
201
|
+
sample_rate=aud_data.get("sample_rate", 24000),
|
|
202
|
+
channels=aud_data.get("channels", 1),
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
data["audio_output"] = Audio(**aud_data)
|
|
206
|
+
|
|
207
|
+
if "image_output" in data and data["image_output"]:
|
|
208
|
+
img_data = data["image_output"]
|
|
209
|
+
if isinstance(img_data, dict):
|
|
210
|
+
if "content" in img_data and isinstance(img_data["content"], str):
|
|
211
|
+
data["image_output"] = Image.from_base64(
|
|
212
|
+
img_data["content"],
|
|
213
|
+
id=img_data.get("id"),
|
|
214
|
+
mime_type=img_data.get("mime_type"),
|
|
215
|
+
format=img_data.get("format"),
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
data["image_output"] = Image(**img_data)
|
|
219
|
+
|
|
220
|
+
if "video_output" in data and data["video_output"]:
|
|
221
|
+
vid_data = data["video_output"]
|
|
222
|
+
if isinstance(vid_data, dict):
|
|
223
|
+
if "content" in vid_data and isinstance(vid_data["content"], str):
|
|
224
|
+
data["video_output"] = Video.from_base64(
|
|
225
|
+
vid_data["content"],
|
|
226
|
+
id=vid_data.get("id"),
|
|
227
|
+
mime_type=vid_data.get("mime_type"),
|
|
228
|
+
format=vid_data.get("format"),
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
data["video_output"] = Video(**vid_data)
|
|
232
|
+
|
|
124
233
|
return cls(**data)
|
|
125
234
|
|
|
126
235
|
def to_dict(self) -> Dict[str, Any]:
|
agno/models/openai/chat.py
CHANGED
|
@@ -70,6 +70,7 @@ class OpenAIChat(Model):
|
|
|
70
70
|
service_tier: Optional[str] = None # "auto" | "default" | "flex" | "priority", defaults to "auto" when not set
|
|
71
71
|
extra_headers: Optional[Any] = None
|
|
72
72
|
extra_query: Optional[Any] = None
|
|
73
|
+
extra_body: Optional[Any] = None
|
|
73
74
|
request_params: Optional[Dict[str, Any]] = None
|
|
74
75
|
role_map: Optional[Dict[str, str]] = None
|
|
75
76
|
|
|
@@ -191,6 +192,7 @@ class OpenAIChat(Model):
|
|
|
191
192
|
"top_p": self.top_p,
|
|
192
193
|
"extra_headers": self.extra_headers,
|
|
193
194
|
"extra_query": self.extra_query,
|
|
195
|
+
"extra_body": self.extra_body,
|
|
194
196
|
"metadata": self.metadata,
|
|
195
197
|
"service_tier": self.service_tier,
|
|
196
198
|
}
|
|
@@ -270,6 +272,7 @@ class OpenAIChat(Model):
|
|
|
270
272
|
"user": self.user,
|
|
271
273
|
"extra_headers": self.extra_headers,
|
|
272
274
|
"extra_query": self.extra_query,
|
|
275
|
+
"extra_body": self.extra_body,
|
|
273
276
|
"service_tier": self.service_tier,
|
|
274
277
|
}
|
|
275
278
|
)
|
agno/models/openai/responses.py
CHANGED
|
@@ -56,6 +56,9 @@ class OpenAIResponses(Model):
|
|
|
56
56
|
truncation: Optional[Literal["auto", "disabled"]] = None
|
|
57
57
|
user: Optional[str] = None
|
|
58
58
|
service_tier: Optional[Literal["auto", "default", "flex", "priority"]] = None
|
|
59
|
+
extra_headers: Optional[Any] = None
|
|
60
|
+
extra_query: Optional[Any] = None
|
|
61
|
+
extra_body: Optional[Any] = None
|
|
59
62
|
request_params: Optional[Dict[str, Any]] = None
|
|
60
63
|
|
|
61
64
|
# Client parameters
|
|
@@ -202,6 +205,9 @@ class OpenAIResponses(Model):
|
|
|
202
205
|
"truncation": self.truncation,
|
|
203
206
|
"user": self.user,
|
|
204
207
|
"service_tier": self.service_tier,
|
|
208
|
+
"extra_headers": self.extra_headers,
|
|
209
|
+
"extra_query": self.extra_query,
|
|
210
|
+
"extra_body": self.extra_body,
|
|
205
211
|
}
|
|
206
212
|
# Populate the reasoning parameter
|
|
207
213
|
base_params = self._set_reasoning_request_param(base_params)
|
|
@@ -1082,4 +1088,10 @@ class OpenAIResponses(Model):
|
|
|
1082
1088
|
metrics.output_tokens = response_usage.output_tokens or 0
|
|
1083
1089
|
metrics.total_tokens = response_usage.total_tokens or 0
|
|
1084
1090
|
|
|
1091
|
+
if input_tokens_details := response_usage.input_tokens_details:
|
|
1092
|
+
metrics.cache_read_tokens = input_tokens_details.cached_tokens
|
|
1093
|
+
|
|
1094
|
+
if output_tokens_details := response_usage.output_tokens_details:
|
|
1095
|
+
metrics.reasoning_tokens = output_tokens_details.reasoning_tokens
|
|
1096
|
+
|
|
1085
1097
|
return metrics
|
agno/models/response.py
CHANGED
|
@@ -29,11 +29,15 @@ class ToolExecution:
|
|
|
29
29
|
result: Optional[str] = None
|
|
30
30
|
metrics: Optional[Metrics] = None
|
|
31
31
|
|
|
32
|
+
# In the case where a tool call creates a run of an agent/team/workflow
|
|
33
|
+
child_run_id: Optional[str] = None
|
|
34
|
+
|
|
32
35
|
# If True, the agent will stop executing after this tool call.
|
|
33
36
|
stop_after_tool_call: bool = False
|
|
34
37
|
|
|
35
38
|
created_at: int = int(time())
|
|
36
39
|
|
|
40
|
+
# User control flow requirements
|
|
37
41
|
requires_confirmation: Optional[bool] = None
|
|
38
42
|
confirmed: Optional[bool] = None
|
|
39
43
|
confirmation_note: Optional[str] = None
|
|
@@ -66,6 +70,7 @@ class ToolExecution:
|
|
|
66
70
|
tool_args=data.get("tool_args"),
|
|
67
71
|
tool_call_error=data.get("tool_call_error"),
|
|
68
72
|
result=data.get("result"),
|
|
73
|
+
child_run_id=data.get("child_run_id"),
|
|
69
74
|
stop_after_tool_call=data.get("stop_after_tool_call", False),
|
|
70
75
|
requires_confirmation=data.get("requires_confirmation"),
|
|
71
76
|
confirmed=data.get("confirmed"),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from os import getenv
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from agno.models.openai.like import OpenAILike
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Siliconflow(OpenAILike):
|
|
10
|
+
"""
|
|
11
|
+
A class for interacting with Siliconflow API.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
id (str): The id of the Siliconflow model to use. Default is "Qwen/QwQ-32B".
|
|
15
|
+
name (str): The name of this chat model instance. Default is "Siliconflow".
|
|
16
|
+
provider (str): The provider of the model. Default is "Siliconflow".
|
|
17
|
+
api_key (str): The api key to authorize request to Siliconflow.
|
|
18
|
+
base_url (str): The base url to which the requests are sent. Defaults to "https://api.siliconflow.cn/v1".
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
id: str = "Qwen/QwQ-32B"
|
|
22
|
+
name: str = "Siliconflow"
|
|
23
|
+
provider: str = "Siliconflow"
|
|
24
|
+
api_key: Optional[str] = getenv("SILICONFLOW_API_KEY")
|
|
25
|
+
base_url: str = "https://api.siliconflow.com/v1"
|
agno/os/app.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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, Optional, Set, 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
12
|
from starlette.middleware.cors import CORSMiddleware
|
|
@@ -27,15 +28,18 @@ from agno.os.config import (
|
|
|
27
28
|
SessionDomainConfig,
|
|
28
29
|
)
|
|
29
30
|
from agno.os.interfaces.base import BaseInterface
|
|
30
|
-
from agno.os.router import get_base_router
|
|
31
|
+
from agno.os.router import get_base_router, get_websocket_router
|
|
31
32
|
from agno.os.routers.evals import get_eval_router
|
|
33
|
+
from agno.os.routers.health import get_health_router
|
|
34
|
+
from agno.os.routers.home import get_home_router
|
|
32
35
|
from agno.os.routers.knowledge import get_knowledge_router
|
|
33
36
|
from agno.os.routers.memory import get_memory_router
|
|
34
37
|
from agno.os.routers.metrics import get_metrics_router
|
|
35
38
|
from agno.os.routers.session import get_session_router
|
|
36
39
|
from agno.os.settings import AgnoAPISettings
|
|
37
|
-
from agno.os.utils import generate_id
|
|
38
40
|
from agno.team.team import Team
|
|
41
|
+
from agno.utils.log import logger
|
|
42
|
+
from agno.utils.string import generate_id, generate_id_from_name
|
|
39
43
|
from agno.workflow.workflow import Workflow
|
|
40
44
|
|
|
41
45
|
|
|
@@ -69,8 +73,30 @@ class AgentOS:
|
|
|
69
73
|
fastapi_app: Optional[FastAPI] = None,
|
|
70
74
|
lifespan: Optional[Any] = None,
|
|
71
75
|
enable_mcp: bool = False,
|
|
76
|
+
replace_routes: bool = True,
|
|
72
77
|
telemetry: bool = True,
|
|
73
78
|
):
|
|
79
|
+
"""Initialize AgentOS.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
os_id: Unique identifier for this AgentOS instance
|
|
83
|
+
name: Name of the AgentOS instance
|
|
84
|
+
description: Description of the AgentOS instance
|
|
85
|
+
version: Version of the AgentOS instance
|
|
86
|
+
agents: List of agents to include in the OS
|
|
87
|
+
teams: List of teams to include in the OS
|
|
88
|
+
workflows: List of workflows to include in the OS
|
|
89
|
+
interfaces: List of interfaces to include in the OS
|
|
90
|
+
config: Configuration file path or AgentOSConfig instance
|
|
91
|
+
settings: API settings for the OS
|
|
92
|
+
fastapi_app: Optional custom FastAPI app to use instead of creating a new one
|
|
93
|
+
lifespan: Optional lifespan context manager for the FastAPI app
|
|
94
|
+
enable_mcp: Whether to enable MCP (Model Context Protocol)
|
|
95
|
+
replace_routes: If False and using a custom fastapi_app, skip AgentOS routes that
|
|
96
|
+
conflict with existing routes, preferring the user's custom routes.
|
|
97
|
+
If True (default), AgentOS routes will override conflicting custom routes.
|
|
98
|
+
telemetry: Whether to enable telemetry
|
|
99
|
+
"""
|
|
74
100
|
if not agents and not workflows and not teams:
|
|
75
101
|
raise ValueError("Either agents, teams or workflows must be provided.")
|
|
76
102
|
|
|
@@ -91,11 +117,13 @@ class AgentOS:
|
|
|
91
117
|
|
|
92
118
|
self.interfaces = interfaces or []
|
|
93
119
|
|
|
94
|
-
self.os_id
|
|
120
|
+
self.os_id = os_id
|
|
95
121
|
self.name = name
|
|
96
122
|
self.version = version
|
|
97
123
|
self.description = description
|
|
98
124
|
|
|
125
|
+
self.replace_routes = replace_routes
|
|
126
|
+
|
|
99
127
|
self.telemetry = telemetry
|
|
100
128
|
|
|
101
129
|
self.enable_mcp = enable_mcp
|
|
@@ -145,7 +173,10 @@ class AgentOS:
|
|
|
145
173
|
for workflow in self.workflows:
|
|
146
174
|
# TODO: track MCP tools in workflow members
|
|
147
175
|
if not workflow.id:
|
|
148
|
-
workflow.id =
|
|
176
|
+
workflow.id = generate_id_from_name(workflow.name)
|
|
177
|
+
|
|
178
|
+
if not self.os_id:
|
|
179
|
+
self.os_id = generate_id(self.name) if self.name else str(uuid4())
|
|
149
180
|
|
|
150
181
|
if self.telemetry:
|
|
151
182
|
from agno.api.os import OSLaunch, log_os_telemetry
|
|
@@ -206,16 +237,29 @@ class AgentOS:
|
|
|
206
237
|
else:
|
|
207
238
|
self.fastapi_app = self._make_app(lifespan=self.lifespan)
|
|
208
239
|
|
|
209
|
-
# Add routes
|
|
210
|
-
self.
|
|
240
|
+
# Add routes with conflict detection
|
|
241
|
+
self._add_router(get_base_router(self, settings=self.settings))
|
|
242
|
+
self._add_router(get_websocket_router(self, settings=self.settings))
|
|
243
|
+
self._add_router(get_health_router())
|
|
244
|
+
self._add_router(get_home_router(self))
|
|
211
245
|
|
|
212
246
|
for interface in self.interfaces:
|
|
213
247
|
interface_router = interface.get_router()
|
|
214
|
-
self.
|
|
248
|
+
self._add_router(interface_router)
|
|
215
249
|
|
|
216
250
|
self._auto_discover_databases()
|
|
217
251
|
self._auto_discover_knowledge_instances()
|
|
218
|
-
|
|
252
|
+
|
|
253
|
+
routers = [
|
|
254
|
+
get_session_router(dbs=self.dbs),
|
|
255
|
+
get_memory_router(dbs=self.dbs),
|
|
256
|
+
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
257
|
+
get_metrics_router(dbs=self.dbs),
|
|
258
|
+
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
for router in routers:
|
|
262
|
+
self._add_router(router)
|
|
219
263
|
|
|
220
264
|
# Mount MCP if needed
|
|
221
265
|
if self.enable_mcp and self.mcp_app:
|
|
@@ -263,6 +307,94 @@ class AgentOS:
|
|
|
263
307
|
|
|
264
308
|
return app.routes
|
|
265
309
|
|
|
310
|
+
def _get_existing_route_paths(self) -> Dict[str, List[str]]:
|
|
311
|
+
"""Get all existing route paths and methods from the FastAPI app.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Dict[str, List[str]]: Dictionary mapping paths to list of HTTP methods
|
|
315
|
+
"""
|
|
316
|
+
if not self.fastapi_app:
|
|
317
|
+
return {}
|
|
318
|
+
|
|
319
|
+
existing_paths: Dict[str, Any] = {}
|
|
320
|
+
for route in self.fastapi_app.routes:
|
|
321
|
+
if isinstance(route, APIRoute):
|
|
322
|
+
path = route.path
|
|
323
|
+
methods = list(route.methods) if route.methods else []
|
|
324
|
+
if path in existing_paths:
|
|
325
|
+
existing_paths[path].extend(methods)
|
|
326
|
+
else:
|
|
327
|
+
existing_paths[path] = methods
|
|
328
|
+
return existing_paths
|
|
329
|
+
|
|
330
|
+
def _add_router(self, router: APIRouter) -> None:
|
|
331
|
+
"""Add a router to the FastAPI app, avoiding route conflicts.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
router: The APIRouter to add
|
|
335
|
+
"""
|
|
336
|
+
if not self.fastapi_app:
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
# Get existing routes
|
|
340
|
+
existing_paths = self._get_existing_route_paths()
|
|
341
|
+
|
|
342
|
+
# Check for conflicts
|
|
343
|
+
conflicts = []
|
|
344
|
+
conflicting_routes = []
|
|
345
|
+
|
|
346
|
+
for route in router.routes:
|
|
347
|
+
if isinstance(route, APIRoute):
|
|
348
|
+
full_path = route.path
|
|
349
|
+
route_methods = list(route.methods) if route.methods else []
|
|
350
|
+
|
|
351
|
+
if full_path in existing_paths:
|
|
352
|
+
conflicting_methods: Set[str] = set(route_methods) & set(existing_paths[full_path])
|
|
353
|
+
if conflicting_methods:
|
|
354
|
+
conflicts.append({"path": full_path, "methods": list(conflicting_methods), "route": route})
|
|
355
|
+
conflicting_routes.append(route)
|
|
356
|
+
|
|
357
|
+
if conflicts and self._app_set:
|
|
358
|
+
if self.replace_routes:
|
|
359
|
+
# Log warnings but still add all routes (AgentOS routes will override)
|
|
360
|
+
for conflict in conflicts:
|
|
361
|
+
methods_str = ", ".join(conflict["methods"]) # type: ignore
|
|
362
|
+
logger.warning(
|
|
363
|
+
f"Route conflict detected: {methods_str} {conflict['path']} - "
|
|
364
|
+
f"AgentOS route will override existing custom route"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# Remove conflicting routes
|
|
368
|
+
for route in self.fastapi_app.routes:
|
|
369
|
+
for conflict in conflicts:
|
|
370
|
+
if isinstance(route, APIRoute):
|
|
371
|
+
if route.path == conflict["path"] and list(route.methods) == list(conflict["methods"]):
|
|
372
|
+
self.fastapi_app.routes.pop(self.fastapi_app.routes.index(route))
|
|
373
|
+
|
|
374
|
+
self.fastapi_app.include_router(router)
|
|
375
|
+
|
|
376
|
+
else:
|
|
377
|
+
# Skip conflicting AgentOS routes, prefer user's existing routes
|
|
378
|
+
for conflict in conflicts:
|
|
379
|
+
methods_str = ", ".join(conflict["methods"]) # type: ignore
|
|
380
|
+
logger.debug(
|
|
381
|
+
f"Skipping conflicting AgentOS route: {methods_str} {conflict['path']} - "
|
|
382
|
+
f"Using existing custom route instead"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Create a new router without the conflicting routes
|
|
386
|
+
filtered_router = APIRouter()
|
|
387
|
+
for route in router.routes:
|
|
388
|
+
if route not in conflicting_routes:
|
|
389
|
+
filtered_router.routes.append(route)
|
|
390
|
+
|
|
391
|
+
# Use the filtered router if it has any routes left
|
|
392
|
+
if filtered_router.routes:
|
|
393
|
+
self.fastapi_app.include_router(filtered_router)
|
|
394
|
+
else:
|
|
395
|
+
# No conflicts, add router normally
|
|
396
|
+
self.fastapi_app.include_router(router)
|
|
397
|
+
|
|
266
398
|
def _get_telemetry_data(self) -> Dict[str, Any]:
|
|
267
399
|
"""Get the telemetry data for the OS"""
|
|
268
400
|
return {
|
|
@@ -290,18 +422,25 @@ class AgentOS:
|
|
|
290
422
|
def _auto_discover_databases(self) -> None:
|
|
291
423
|
"""Auto-discover the databases used by all contextual agents, teams and workflows."""
|
|
292
424
|
dbs = {}
|
|
425
|
+
knowledge_dbs = {} # Track databases specifically used for knowledge
|
|
293
426
|
|
|
294
427
|
for agent in self.agents or []:
|
|
295
428
|
if agent.db:
|
|
296
429
|
dbs[agent.db.id] = agent.db
|
|
297
430
|
if agent.knowledge and agent.knowledge.contents_db:
|
|
298
|
-
|
|
431
|
+
knowledge_dbs[agent.knowledge.contents_db.id] = agent.knowledge.contents_db
|
|
432
|
+
# Also add to general dbs if it's used for both purposes
|
|
433
|
+
if agent.knowledge.contents_db.id not in dbs:
|
|
434
|
+
dbs[agent.knowledge.contents_db.id] = agent.knowledge.contents_db
|
|
299
435
|
|
|
300
436
|
for team in self.teams or []:
|
|
301
437
|
if team.db:
|
|
302
438
|
dbs[team.db.id] = team.db
|
|
303
439
|
if team.knowledge and team.knowledge.contents_db:
|
|
304
|
-
|
|
440
|
+
knowledge_dbs[team.knowledge.contents_db.id] = team.knowledge.contents_db
|
|
441
|
+
# Also add to general dbs if it's used for both purposes
|
|
442
|
+
if team.knowledge.contents_db.id not in dbs:
|
|
443
|
+
dbs[team.knowledge.contents_db.id] = team.knowledge.contents_db
|
|
305
444
|
|
|
306
445
|
for workflow in self.workflows or []:
|
|
307
446
|
if workflow.db:
|
|
@@ -314,6 +453,7 @@ class AgentOS:
|
|
|
314
453
|
dbs[interface.team.db.id] = interface.team.db
|
|
315
454
|
|
|
316
455
|
self.dbs = dbs
|
|
456
|
+
self.knowledge_dbs = knowledge_dbs
|
|
317
457
|
|
|
318
458
|
def _auto_discover_knowledge_instances(self) -> None:
|
|
319
459
|
"""Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
|
|
@@ -378,16 +518,17 @@ class AgentOS:
|
|
|
378
518
|
if knowledge_config.dbs is None:
|
|
379
519
|
knowledge_config.dbs = []
|
|
380
520
|
|
|
381
|
-
|
|
521
|
+
multiple_knowledge_dbs: bool = len(self.knowledge_dbs.keys()) > 1
|
|
382
522
|
dbs_with_specific_config = [db.db_id for db in knowledge_config.dbs]
|
|
383
523
|
|
|
384
|
-
for
|
|
524
|
+
# Only add databases that are actually used for knowledge contents
|
|
525
|
+
for db_id in self.knowledge_dbs.keys():
|
|
385
526
|
if db_id not in dbs_with_specific_config:
|
|
386
527
|
knowledge_config.dbs.append(
|
|
387
528
|
DatabaseConfig(
|
|
388
529
|
db_id=db_id,
|
|
389
530
|
domain_config=KnowledgeDomainConfig(
|
|
390
|
-
display_name="Knowledge" if not
|
|
531
|
+
display_name="Knowledge" if not multiple_knowledge_dbs else "Knowledge in database " + db_id
|
|
391
532
|
),
|
|
392
533
|
)
|
|
393
534
|
)
|
|
@@ -438,29 +579,6 @@ class AgentOS:
|
|
|
438
579
|
|
|
439
580
|
return evals_config
|
|
440
581
|
|
|
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
582
|
def serve(
|
|
465
583
|
self,
|
|
466
584
|
app: Union[str, FastAPI],
|
|
@@ -482,13 +600,18 @@ class AgentOS:
|
|
|
482
600
|
from rich.align import Align
|
|
483
601
|
from rich.console import Console, Group
|
|
484
602
|
|
|
485
|
-
|
|
486
|
-
|
|
603
|
+
panel_group = []
|
|
604
|
+
panel_group.append(Align.center(f"[bold cyan]{public_endpoint}[/bold cyan]"))
|
|
605
|
+
panel_group.append(
|
|
606
|
+
Align.center(f"\n\n[bold dark_orange]OS running on:[/bold dark_orange] http://{host}:{port}")
|
|
607
|
+
)
|
|
608
|
+
if bool(self.settings.os_security_key):
|
|
609
|
+
panel_group.append(Align.center("\n\n[bold chartreuse3]:lock: Security Enabled[/bold chartreuse3]"))
|
|
487
610
|
|
|
488
611
|
console = Console()
|
|
489
612
|
console.print(
|
|
490
613
|
Panel(
|
|
491
|
-
Group(
|
|
614
|
+
Group(*panel_group),
|
|
492
615
|
title="AgentOS",
|
|
493
616
|
expand=False,
|
|
494
617
|
border_style="dark_orange",
|
agno/os/auth.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
from fastapi import Header, HTTPException
|
|
4
|
-
from fastapi.security import HTTPBearer
|
|
1
|
+
from fastapi import Depends, HTTPException
|
|
2
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
5
3
|
|
|
6
4
|
from agno.os.settings import AgnoAPISettings
|
|
7
5
|
|
|
@@ -20,23 +18,16 @@ def get_authentication_dependency(settings: AgnoAPISettings):
|
|
|
20
18
|
A dependency function that can be used with FastAPI's Depends()
|
|
21
19
|
"""
|
|
22
20
|
|
|
23
|
-
def auth_dependency(
|
|
21
|
+
def auth_dependency(credentials: HTTPAuthorizationCredentials = Depends(security)) -> bool:
|
|
24
22
|
# If no security key is set, skip authentication entirely
|
|
25
23
|
if not settings or not settings.os_security_key:
|
|
26
24
|
return True
|
|
27
25
|
|
|
28
26
|
# If security is enabled but no authorization header provided, fail
|
|
29
|
-
if not
|
|
27
|
+
if not credentials:
|
|
30
28
|
raise HTTPException(status_code=401, detail="Authorization header required")
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
if not authorization.startswith("Bearer "):
|
|
34
|
-
raise HTTPException(
|
|
35
|
-
status_code=401, detail="Invalid authorization header format. Expected 'Bearer <token>'"
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
# Extract the token from the authorization header
|
|
39
|
-
token = authorization[7:] # Remove "Bearer " prefix
|
|
30
|
+
token = credentials.credentials
|
|
40
31
|
|
|
41
32
|
# Verify the token
|
|
42
33
|
if token != settings.os_security_key:
|
|
@@ -45,3 +36,22 @@ def get_authentication_dependency(settings: AgnoAPISettings):
|
|
|
45
36
|
return True
|
|
46
37
|
|
|
47
38
|
return auth_dependency
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_websocket_token(token: str, settings: AgnoAPISettings) -> bool:
|
|
42
|
+
"""
|
|
43
|
+
Validate a bearer token for WebSocket authentication.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
token: The bearer token to validate
|
|
47
|
+
settings: The API settings containing the security key
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if the token is valid or authentication is disabled, False otherwise
|
|
51
|
+
"""
|
|
52
|
+
# If no security key is set, skip authentication entirely
|
|
53
|
+
if not settings or not settings.os_security_key:
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
# Verify the token matches the configured security key
|
|
57
|
+
return token == settings.os_security_key
|