agno 2.0.4__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 +74 -85
- agno/db/dynamo/dynamo.py +2 -2
- agno/db/firestore/firestore.py +3 -2
- agno/db/gcs_json/gcs_json_db.py +2 -2
- agno/db/json/json_db.py +2 -2
- agno/db/migrations/v1_to_v2.py +191 -23
- agno/db/mongo/mongo.py +61 -2
- agno/db/mysql/mysql.py +5 -5
- agno/db/mysql/schemas.py +27 -27
- agno/db/postgres/postgres.py +5 -5
- agno/db/redis/redis.py +2 -2
- agno/db/singlestore/singlestore.py +2 -2
- agno/db/sqlite/sqlite.py +6 -5
- agno/db/utils.py +0 -14
- agno/integrations/discord/client.py +1 -0
- agno/knowledge/knowledge.py +7 -7
- agno/knowledge/reader/reader_factory.py +7 -3
- agno/knowledge/reader/web_search_reader.py +12 -6
- agno/models/message.py +109 -0
- agno/models/openai/responses.py +6 -0
- agno/os/app.py +162 -42
- agno/os/interfaces/agui/utils.py +98 -134
- agno/os/routers/health.py +0 -1
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/knowledge.py +2 -2
- agno/os/schema.py +21 -0
- agno/os/utils.py +0 -8
- agno/run/agent.py +3 -3
- agno/run/team.py +3 -3
- agno/team/team.py +33 -38
- agno/tools/duckduckgo.py +15 -11
- agno/tools/googlesearch.py +1 -1
- agno/utils/string.py +32 -0
- agno/utils/tools.py +1 -1
- agno/workflow/step.py +4 -3
- {agno-2.0.4.dist-info → agno-2.0.5.dist-info}/METADATA +6 -5
- {agno-2.0.4.dist-info → agno-2.0.5.dist-info}/RECORD +40 -40
- agno/knowledge/reader/url_reader.py +0 -128
- {agno-2.0.4.dist-info → agno-2.0.5.dist-info}/WHEEL +0 -0
- {agno-2.0.4.dist-info → agno-2.0.5.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.4.dist-info → agno-2.0.5.dist-info}/top_level.txt +0 -0
agno/knowledge/knowledge.py
CHANGED
|
@@ -14,13 +14,13 @@ from httpx import AsyncClient
|
|
|
14
14
|
|
|
15
15
|
from agno.db.base import BaseDb
|
|
16
16
|
from agno.db.schemas.knowledge import KnowledgeRow
|
|
17
|
-
from agno.db.utils import generate_deterministic_id
|
|
18
17
|
from agno.knowledge.content import Content, ContentAuth, ContentStatus, FileData
|
|
19
18
|
from agno.knowledge.document import Document
|
|
20
19
|
from agno.knowledge.reader import Reader, ReaderFactory
|
|
21
20
|
from agno.knowledge.remote_content.remote_content import GCSContent, RemoteContent, S3Content
|
|
22
21
|
from agno.utils.http import async_fetch_with_retry
|
|
23
22
|
from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
23
|
+
from agno.utils.string import generate_id
|
|
24
24
|
from agno.vectordb import VectorDb
|
|
25
25
|
|
|
26
26
|
ContentDict = Dict[str, Union[str, Dict[str, str]]]
|
|
@@ -253,7 +253,7 @@ class Knowledge:
|
|
|
253
253
|
auth=auth,
|
|
254
254
|
)
|
|
255
255
|
content.content_hash = self._build_content_hash(content)
|
|
256
|
-
content.id =
|
|
256
|
+
content.id = generate_id(content.content_hash)
|
|
257
257
|
|
|
258
258
|
await self._load_content(content, upsert, skip_if_exists, include, exclude)
|
|
259
259
|
|
|
@@ -304,7 +304,7 @@ class Knowledge:
|
|
|
304
304
|
text_content: Optional text content to add directly
|
|
305
305
|
metadata: Optional metadata dictionary
|
|
306
306
|
topics: Optional list of topics
|
|
307
|
-
|
|
307
|
+
remote_content: Optional cloud storage configuration
|
|
308
308
|
reader: Optional custom reader for processing the content
|
|
309
309
|
include: Optional list of file patterns to include
|
|
310
310
|
exclude: Optional list of file patterns to exclude
|
|
@@ -431,7 +431,7 @@ class Knowledge:
|
|
|
431
431
|
reader=content.reader,
|
|
432
432
|
)
|
|
433
433
|
file_content.content_hash = self._build_content_hash(file_content)
|
|
434
|
-
file_content.id =
|
|
434
|
+
file_content.id = generate_id(file_content.content_hash)
|
|
435
435
|
|
|
436
436
|
await self._load_from_path(file_content, upsert, skip_if_exists, include, exclude)
|
|
437
437
|
else:
|
|
@@ -680,7 +680,7 @@ class Knowledge:
|
|
|
680
680
|
topics=[topic],
|
|
681
681
|
)
|
|
682
682
|
content.content_hash = self._build_content_hash(content)
|
|
683
|
-
content.id =
|
|
683
|
+
content.id = generate_id(content.content_hash)
|
|
684
684
|
|
|
685
685
|
self._add_to_contents_db(content)
|
|
686
686
|
if self._should_skip(content.content_hash, skip_if_exists):
|
|
@@ -777,7 +777,7 @@ class Knowledge:
|
|
|
777
777
|
|
|
778
778
|
# 3. Hash content and add it to the contents database
|
|
779
779
|
content_entry.content_hash = self._build_content_hash(content_entry)
|
|
780
|
-
content_entry.id =
|
|
780
|
+
content_entry.id = generate_id(content_entry.content_hash)
|
|
781
781
|
self._add_to_contents_db(content_entry)
|
|
782
782
|
if self._should_skip(content_entry.content_hash, skip_if_exists):
|
|
783
783
|
content_entry.status = ContentStatus.COMPLETED
|
|
@@ -859,7 +859,7 @@ class Knowledge:
|
|
|
859
859
|
|
|
860
860
|
# 3. Hash content and add it to the contents database
|
|
861
861
|
content_entry.content_hash = self._build_content_hash(content_entry)
|
|
862
|
-
content_entry.id =
|
|
862
|
+
content_entry.id = generate_id(content_entry.content_hash)
|
|
863
863
|
self._add_to_contents_db(content_entry)
|
|
864
864
|
if self._should_skip(content_entry.content_hash, skip_if_exists):
|
|
865
865
|
content_entry.status = ContentStatus.COMPLETED
|
|
@@ -210,8 +210,8 @@ class ReaderFactory:
|
|
|
210
210
|
if any(domain in url_lower for domain in ["youtube.com", "youtu.be"]):
|
|
211
211
|
return cls.create_reader("youtube")
|
|
212
212
|
|
|
213
|
-
# Default to
|
|
214
|
-
return cls.create_reader("
|
|
213
|
+
# Default to website reader
|
|
214
|
+
return cls.create_reader("website")
|
|
215
215
|
|
|
216
216
|
@classmethod
|
|
217
217
|
def get_all_reader_keys(cls) -> List[str]:
|
|
@@ -228,7 +228,11 @@ class ReaderFactory:
|
|
|
228
228
|
reader_keys.append(reader_key)
|
|
229
229
|
|
|
230
230
|
# Define priority order for URL readers
|
|
231
|
-
url_reader_priority = [
|
|
231
|
+
url_reader_priority = [
|
|
232
|
+
"website",
|
|
233
|
+
"firecrawl",
|
|
234
|
+
"youtube",
|
|
235
|
+
]
|
|
232
236
|
|
|
233
237
|
# Sort with URL readers in priority order, others alphabetically
|
|
234
238
|
def sort_key(reader_key):
|
|
@@ -96,7 +96,7 @@ class WebSearchReader(Reader):
|
|
|
96
96
|
results.append(
|
|
97
97
|
{
|
|
98
98
|
"title": result.get("title", ""),
|
|
99
|
-
"url": result.get("
|
|
99
|
+
"url": result.get("href", ""),
|
|
100
100
|
"description": result.get("body", ""),
|
|
101
101
|
}
|
|
102
102
|
)
|
|
@@ -136,14 +136,20 @@ class WebSearchReader(Reader):
|
|
|
136
136
|
self._respect_rate_limits()
|
|
137
137
|
|
|
138
138
|
results = []
|
|
139
|
-
|
|
139
|
+
# Use the basic search function without unsupported parameters
|
|
140
|
+
# The googlesearch-python library's search function only accepts basic parameters
|
|
141
|
+
search_results = search(query)
|
|
140
142
|
|
|
141
|
-
|
|
143
|
+
# Convert iterator to list and limit results
|
|
144
|
+
result_list = list(search_results)[: self.max_results]
|
|
145
|
+
|
|
146
|
+
for result in result_list:
|
|
147
|
+
# The search function returns URLs as strings
|
|
142
148
|
results.append(
|
|
143
149
|
{
|
|
144
|
-
"title":
|
|
145
|
-
"url":
|
|
146
|
-
"description":
|
|
150
|
+
"title": "", # Google search doesn't provide titles directly
|
|
151
|
+
"url": result,
|
|
152
|
+
"description": "", # Google search doesn't provide descriptions directly
|
|
147
153
|
}
|
|
148
154
|
)
|
|
149
155
|
|
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/responses.py
CHANGED
|
@@ -1088,4 +1088,10 @@ class OpenAIResponses(Model):
|
|
|
1088
1088
|
metrics.output_tokens = response_usage.output_tokens or 0
|
|
1089
1089
|
metrics.total_tokens = response_usage.total_tokens or 0
|
|
1090
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
|
+
|
|
1091
1097
|
return metrics
|
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
|
|
@@ -30,13 +31,15 @@ from agno.os.interfaces.base import BaseInterface
|
|
|
30
31
|
from agno.os.router import get_base_router, get_websocket_router
|
|
31
32
|
from agno.os.routers.evals import get_eval_router
|
|
32
33
|
from agno.os.routers.health import get_health_router
|
|
34
|
+
from agno.os.routers.home import get_home_router
|
|
33
35
|
from agno.os.routers.knowledge import get_knowledge_router
|
|
34
36
|
from agno.os.routers.memory import get_memory_router
|
|
35
37
|
from agno.os.routers.metrics import get_metrics_router
|
|
36
38
|
from agno.os.routers.session import get_session_router
|
|
37
39
|
from agno.os.settings import AgnoAPISettings
|
|
38
|
-
from agno.os.utils import generate_id
|
|
39
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
|
|
40
43
|
from agno.workflow.workflow import Workflow
|
|
41
44
|
|
|
42
45
|
|
|
@@ -70,8 +73,30 @@ class AgentOS:
|
|
|
70
73
|
fastapi_app: Optional[FastAPI] = None,
|
|
71
74
|
lifespan: Optional[Any] = None,
|
|
72
75
|
enable_mcp: bool = False,
|
|
76
|
+
replace_routes: bool = True,
|
|
73
77
|
telemetry: bool = True,
|
|
74
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
|
+
"""
|
|
75
100
|
if not agents and not workflows and not teams:
|
|
76
101
|
raise ValueError("Either agents, teams or workflows must be provided.")
|
|
77
102
|
|
|
@@ -92,11 +117,13 @@ class AgentOS:
|
|
|
92
117
|
|
|
93
118
|
self.interfaces = interfaces or []
|
|
94
119
|
|
|
95
|
-
self.os_id
|
|
120
|
+
self.os_id = os_id
|
|
96
121
|
self.name = name
|
|
97
122
|
self.version = version
|
|
98
123
|
self.description = description
|
|
99
124
|
|
|
125
|
+
self.replace_routes = replace_routes
|
|
126
|
+
|
|
100
127
|
self.telemetry = telemetry
|
|
101
128
|
|
|
102
129
|
self.enable_mcp = enable_mcp
|
|
@@ -146,7 +173,10 @@ class AgentOS:
|
|
|
146
173
|
for workflow in self.workflows:
|
|
147
174
|
# TODO: track MCP tools in workflow members
|
|
148
175
|
if not workflow.id:
|
|
149
|
-
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())
|
|
150
180
|
|
|
151
181
|
if self.telemetry:
|
|
152
182
|
from agno.api.os import OSLaunch, log_os_telemetry
|
|
@@ -207,18 +237,29 @@ class AgentOS:
|
|
|
207
237
|
else:
|
|
208
238
|
self.fastapi_app = self._make_app(lifespan=self.lifespan)
|
|
209
239
|
|
|
210
|
-
# Add routes
|
|
211
|
-
self.
|
|
212
|
-
self.
|
|
213
|
-
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))
|
|
214
245
|
|
|
215
246
|
for interface in self.interfaces:
|
|
216
247
|
interface_router = interface.get_router()
|
|
217
|
-
self.
|
|
248
|
+
self._add_router(interface_router)
|
|
218
249
|
|
|
219
250
|
self._auto_discover_databases()
|
|
220
251
|
self._auto_discover_knowledge_instances()
|
|
221
|
-
|
|
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)
|
|
222
263
|
|
|
223
264
|
# Mount MCP if needed
|
|
224
265
|
if self.enable_mcp and self.mcp_app:
|
|
@@ -266,6 +307,94 @@ class AgentOS:
|
|
|
266
307
|
|
|
267
308
|
return app.routes
|
|
268
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
|
+
|
|
269
398
|
def _get_telemetry_data(self) -> Dict[str, Any]:
|
|
270
399
|
"""Get the telemetry data for the OS"""
|
|
271
400
|
return {
|
|
@@ -293,18 +422,25 @@ class AgentOS:
|
|
|
293
422
|
def _auto_discover_databases(self) -> None:
|
|
294
423
|
"""Auto-discover the databases used by all contextual agents, teams and workflows."""
|
|
295
424
|
dbs = {}
|
|
425
|
+
knowledge_dbs = {} # Track databases specifically used for knowledge
|
|
296
426
|
|
|
297
427
|
for agent in self.agents or []:
|
|
298
428
|
if agent.db:
|
|
299
429
|
dbs[agent.db.id] = agent.db
|
|
300
430
|
if agent.knowledge and agent.knowledge.contents_db:
|
|
301
|
-
|
|
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
|
|
302
435
|
|
|
303
436
|
for team in self.teams or []:
|
|
304
437
|
if team.db:
|
|
305
438
|
dbs[team.db.id] = team.db
|
|
306
439
|
if team.knowledge and team.knowledge.contents_db:
|
|
307
|
-
|
|
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
|
|
308
444
|
|
|
309
445
|
for workflow in self.workflows or []:
|
|
310
446
|
if workflow.db:
|
|
@@ -317,6 +453,7 @@ class AgentOS:
|
|
|
317
453
|
dbs[interface.team.db.id] = interface.team.db
|
|
318
454
|
|
|
319
455
|
self.dbs = dbs
|
|
456
|
+
self.knowledge_dbs = knowledge_dbs
|
|
320
457
|
|
|
321
458
|
def _auto_discover_knowledge_instances(self) -> None:
|
|
322
459
|
"""Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
|
|
@@ -381,16 +518,17 @@ class AgentOS:
|
|
|
381
518
|
if knowledge_config.dbs is None:
|
|
382
519
|
knowledge_config.dbs = []
|
|
383
520
|
|
|
384
|
-
|
|
521
|
+
multiple_knowledge_dbs: bool = len(self.knowledge_dbs.keys()) > 1
|
|
385
522
|
dbs_with_specific_config = [db.db_id for db in knowledge_config.dbs]
|
|
386
523
|
|
|
387
|
-
for
|
|
524
|
+
# Only add databases that are actually used for knowledge contents
|
|
525
|
+
for db_id in self.knowledge_dbs.keys():
|
|
388
526
|
if db_id not in dbs_with_specific_config:
|
|
389
527
|
knowledge_config.dbs.append(
|
|
390
528
|
DatabaseConfig(
|
|
391
529
|
db_id=db_id,
|
|
392
530
|
domain_config=KnowledgeDomainConfig(
|
|
393
|
-
display_name="Knowledge" if not
|
|
531
|
+
display_name="Knowledge" if not multiple_knowledge_dbs else "Knowledge in database " + db_id
|
|
394
532
|
),
|
|
395
533
|
)
|
|
396
534
|
)
|
|
@@ -441,29 +579,6 @@ class AgentOS:
|
|
|
441
579
|
|
|
442
580
|
return evals_config
|
|
443
581
|
|
|
444
|
-
def _setup_routers(self) -> None:
|
|
445
|
-
"""Add all routers to the FastAPI app."""
|
|
446
|
-
if not self.dbs or not self.fastapi_app:
|
|
447
|
-
return
|
|
448
|
-
|
|
449
|
-
routers = [
|
|
450
|
-
get_session_router(dbs=self.dbs),
|
|
451
|
-
get_memory_router(dbs=self.dbs),
|
|
452
|
-
get_eval_router(dbs=self.dbs, agents=self.agents, teams=self.teams),
|
|
453
|
-
get_metrics_router(dbs=self.dbs),
|
|
454
|
-
get_knowledge_router(knowledge_instances=self.knowledge_instances),
|
|
455
|
-
]
|
|
456
|
-
|
|
457
|
-
for router in routers:
|
|
458
|
-
self.fastapi_app.include_router(router)
|
|
459
|
-
|
|
460
|
-
def set_os_id(self) -> str:
|
|
461
|
-
# If os_id is already set, keep it instead of overriding with UUID
|
|
462
|
-
if self.os_id is None:
|
|
463
|
-
self.os_id = str(uuid4())
|
|
464
|
-
|
|
465
|
-
return self.os_id
|
|
466
|
-
|
|
467
582
|
def serve(
|
|
468
583
|
self,
|
|
469
584
|
app: Union[str, FastAPI],
|
|
@@ -485,13 +600,18 @@ class AgentOS:
|
|
|
485
600
|
from rich.align import Align
|
|
486
601
|
from rich.console import Console, Group
|
|
487
602
|
|
|
488
|
-
|
|
489
|
-
|
|
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]"))
|
|
490
610
|
|
|
491
611
|
console = Console()
|
|
492
612
|
console.print(
|
|
493
613
|
Panel(
|
|
494
|
-
Group(
|
|
614
|
+
Group(*panel_group),
|
|
495
615
|
title="AgentOS",
|
|
496
616
|
expand=False,
|
|
497
617
|
border_style="dark_orange",
|