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.
Files changed (58) hide show
  1. agno/agent/agent.py +229 -164
  2. agno/db/dynamo/dynamo.py +8 -0
  3. agno/db/firestore/firestore.py +8 -0
  4. agno/db/gcs_json/gcs_json_db.py +9 -0
  5. agno/db/json/json_db.py +8 -0
  6. agno/db/migrations/v1_to_v2.py +191 -23
  7. agno/db/mongo/mongo.py +68 -0
  8. agno/db/mysql/mysql.py +13 -3
  9. agno/db/mysql/schemas.py +27 -27
  10. agno/db/postgres/postgres.py +19 -11
  11. agno/db/redis/redis.py +6 -0
  12. agno/db/singlestore/schemas.py +1 -1
  13. agno/db/singlestore/singlestore.py +8 -1
  14. agno/db/sqlite/sqlite.py +12 -3
  15. agno/integrations/discord/client.py +1 -0
  16. agno/knowledge/knowledge.py +92 -66
  17. agno/knowledge/reader/reader_factory.py +7 -3
  18. agno/knowledge/reader/web_search_reader.py +12 -6
  19. agno/models/base.py +2 -2
  20. agno/models/message.py +109 -0
  21. agno/models/openai/chat.py +3 -0
  22. agno/models/openai/responses.py +12 -0
  23. agno/models/response.py +5 -0
  24. agno/models/siliconflow/__init__.py +5 -0
  25. agno/models/siliconflow/siliconflow.py +25 -0
  26. agno/os/app.py +164 -41
  27. agno/os/auth.py +24 -14
  28. agno/os/interfaces/agui/utils.py +98 -134
  29. agno/os/router.py +128 -55
  30. agno/os/routers/evals/utils.py +9 -9
  31. agno/os/routers/health.py +25 -0
  32. agno/os/routers/home.py +52 -0
  33. agno/os/routers/knowledge/knowledge.py +11 -11
  34. agno/os/routers/session/session.py +24 -8
  35. agno/os/schema.py +29 -2
  36. agno/os/utils.py +0 -8
  37. agno/run/agent.py +3 -3
  38. agno/run/team.py +3 -3
  39. agno/run/workflow.py +64 -10
  40. agno/session/team.py +1 -0
  41. agno/team/team.py +189 -94
  42. agno/tools/duckduckgo.py +15 -11
  43. agno/tools/googlesearch.py +1 -1
  44. agno/tools/mem0.py +11 -17
  45. agno/tools/memory.py +34 -6
  46. agno/utils/common.py +90 -1
  47. agno/utils/streamlit.py +14 -8
  48. agno/utils/string.py +32 -0
  49. agno/utils/tools.py +1 -1
  50. agno/vectordb/chroma/chromadb.py +8 -2
  51. agno/workflow/step.py +115 -16
  52. agno/workflow/workflow.py +16 -13
  53. {agno-2.0.3.dist-info → agno-2.0.5.dist-info}/METADATA +6 -5
  54. {agno-2.0.3.dist-info → agno-2.0.5.dist-info}/RECORD +57 -54
  55. agno/knowledge/reader/url_reader.py +0 -128
  56. {agno-2.0.3.dist-info → agno-2.0.5.dist-info}/WHEEL +0 -0
  57. {agno-2.0.3.dist-info → agno-2.0.5.dist-info}/licenses/LICENSE +0 -0
  58. {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]:
@@ -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
  )
@@ -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,5 @@
1
+ from agno.models.siliconflow.siliconflow import Siliconflow
2
+
3
+ __all__ = [
4
+ "Siliconflow",
5
+ ]
@@ -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: Optional[str] = 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 = 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())
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.fastapi_app.include_router(get_base_router(self, settings=self.settings))
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.fastapi_app.include_router(interface_router)
248
+ self._add_router(interface_router)
215
249
 
216
250
  self._auto_discover_databases()
217
251
  self._auto_discover_knowledge_instances()
218
- 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)
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
- 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
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
- 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
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
- multiple_dbs: bool = len(self.dbs.keys()) > 1
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 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():
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 multiple_dbs else "Knowledge in database " + db_id
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
- aligned_endpoint = Align.center(f"[bold cyan]{public_endpoint}[/bold cyan]")
486
- 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]"))
487
610
 
488
611
  console = Console()
489
612
  console.print(
490
613
  Panel(
491
- Group(aligned_endpoint, connection_endpoint),
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 typing import Optional
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(authorization: Optional[str] = Header(None)) -> bool:
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 authorization:
27
+ if not credentials:
30
28
  raise HTTPException(status_code=401, detail="Authorization header required")
31
29
 
32
- # Check if the authorization header starts with "Bearer "
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