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.
Files changed (41) hide show
  1. agno/agent/agent.py +74 -85
  2. agno/db/dynamo/dynamo.py +2 -2
  3. agno/db/firestore/firestore.py +3 -2
  4. agno/db/gcs_json/gcs_json_db.py +2 -2
  5. agno/db/json/json_db.py +2 -2
  6. agno/db/migrations/v1_to_v2.py +191 -23
  7. agno/db/mongo/mongo.py +61 -2
  8. agno/db/mysql/mysql.py +5 -5
  9. agno/db/mysql/schemas.py +27 -27
  10. agno/db/postgres/postgres.py +5 -5
  11. agno/db/redis/redis.py +2 -2
  12. agno/db/singlestore/singlestore.py +2 -2
  13. agno/db/sqlite/sqlite.py +6 -5
  14. agno/db/utils.py +0 -14
  15. agno/integrations/discord/client.py +1 -0
  16. agno/knowledge/knowledge.py +7 -7
  17. agno/knowledge/reader/reader_factory.py +7 -3
  18. agno/knowledge/reader/web_search_reader.py +12 -6
  19. agno/models/message.py +109 -0
  20. agno/models/openai/responses.py +6 -0
  21. agno/os/app.py +162 -42
  22. agno/os/interfaces/agui/utils.py +98 -134
  23. agno/os/routers/health.py +0 -1
  24. agno/os/routers/home.py +52 -0
  25. agno/os/routers/knowledge/knowledge.py +2 -2
  26. agno/os/schema.py +21 -0
  27. agno/os/utils.py +0 -8
  28. agno/run/agent.py +3 -3
  29. agno/run/team.py +3 -3
  30. agno/team/team.py +33 -38
  31. agno/tools/duckduckgo.py +15 -11
  32. agno/tools/googlesearch.py +1 -1
  33. agno/utils/string.py +32 -0
  34. agno/utils/tools.py +1 -1
  35. agno/workflow/step.py +4 -3
  36. {agno-2.0.4.dist-info → agno-2.0.5.dist-info}/METADATA +6 -5
  37. {agno-2.0.4.dist-info → agno-2.0.5.dist-info}/RECORD +40 -40
  38. agno/knowledge/reader/url_reader.py +0 -128
  39. {agno-2.0.4.dist-info → agno-2.0.5.dist-info}/WHEEL +0 -0
  40. {agno-2.0.4.dist-info → agno-2.0.5.dist-info}/licenses/LICENSE +0 -0
  41. {agno-2.0.4.dist-info → agno-2.0.5.dist-info}/top_level.txt +0 -0
@@ -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 = generate_deterministic_id(content.content_hash)
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
- config: Optional cloud storage configuration
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 = generate_deterministic_id(file_content.content_hash)
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 = generate_deterministic_id(content.content_hash)
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 = generate_deterministic_id(content_entry.content_hash)
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 = generate_deterministic_id(content_entry.content_hash)
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 URL reader
214
- return cls.create_reader("url")
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 = ["url", "website", "firecrawl", "pdf_url", "csv_url", "youtube", "web_search"]
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("link", ""),
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
- search_results = search(query, num_results=self.max_results, stop=self.max_results)
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
- for result in search_results:
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": getattr(result, "title", ""),
145
- "url": getattr(result, "url", ""),
146
- "description": getattr(result, "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]:
@@ -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: Optional[str] = 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 = generate_id(workflow.name)
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.fastapi_app.include_router(get_base_router(self, settings=self.settings))
212
- self.fastapi_app.include_router(get_websocket_router(self, settings=self.settings))
213
- self.fastapi_app.include_router(get_health_router())
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.fastapi_app.include_router(interface_router)
248
+ self._add_router(interface_router)
218
249
 
219
250
  self._auto_discover_databases()
220
251
  self._auto_discover_knowledge_instances()
221
- self._setup_routers()
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
- dbs[agent.knowledge.contents_db.id] = agent.knowledge.contents_db
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
- dbs[team.knowledge.contents_db.id] = team.knowledge.contents_db
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
- multiple_dbs: bool = len(self.dbs.keys()) > 1
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 db_id in self.dbs.keys():
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 multiple_dbs else "Knowledge in database " + db_id
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
- aligned_endpoint = Align.center(f"[bold cyan]{public_endpoint}[/bold cyan]")
489
- connection_endpoint = f"\n\n[bold dark_orange]Running on:[/bold dark_orange] http://{host}:{port}"
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(aligned_endpoint, connection_endpoint),
614
+ Group(*panel_group),
495
615
  title="AgentOS",
496
616
  expand=False,
497
617
  border_style="dark_orange",