agno 2.3.25__py3-none-any.whl → 2.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. agno/agent/__init__.py +4 -0
  2. agno/agent/agent.py +1428 -558
  3. agno/agent/remote.py +13 -0
  4. agno/db/base.py +339 -0
  5. agno/db/postgres/async_postgres.py +116 -12
  6. agno/db/postgres/postgres.py +1229 -25
  7. agno/db/postgres/schemas.py +48 -1
  8. agno/db/sqlite/async_sqlite.py +119 -4
  9. agno/db/sqlite/schemas.py +51 -0
  10. agno/db/sqlite/sqlite.py +1173 -13
  11. agno/db/utils.py +37 -1
  12. agno/knowledge/__init__.py +4 -0
  13. agno/knowledge/chunking/code.py +1 -1
  14. agno/knowledge/chunking/semantic.py +1 -1
  15. agno/knowledge/chunking/strategy.py +4 -0
  16. agno/knowledge/filesystem.py +412 -0
  17. agno/knowledge/knowledge.py +2767 -2254
  18. agno/knowledge/protocol.py +134 -0
  19. agno/knowledge/reader/arxiv_reader.py +2 -2
  20. agno/knowledge/reader/base.py +9 -7
  21. agno/knowledge/reader/csv_reader.py +5 -5
  22. agno/knowledge/reader/docx_reader.py +2 -2
  23. agno/knowledge/reader/field_labeled_csv_reader.py +2 -2
  24. agno/knowledge/reader/firecrawl_reader.py +2 -2
  25. agno/knowledge/reader/json_reader.py +2 -2
  26. agno/knowledge/reader/markdown_reader.py +2 -2
  27. agno/knowledge/reader/pdf_reader.py +5 -4
  28. agno/knowledge/reader/pptx_reader.py +2 -2
  29. agno/knowledge/reader/reader_factory.py +110 -0
  30. agno/knowledge/reader/s3_reader.py +2 -2
  31. agno/knowledge/reader/tavily_reader.py +2 -2
  32. agno/knowledge/reader/text_reader.py +2 -2
  33. agno/knowledge/reader/web_search_reader.py +2 -2
  34. agno/knowledge/reader/website_reader.py +5 -3
  35. agno/knowledge/reader/wikipedia_reader.py +2 -2
  36. agno/knowledge/reader/youtube_reader.py +2 -2
  37. agno/knowledge/utils.py +37 -29
  38. agno/learn/__init__.py +6 -0
  39. agno/learn/machine.py +35 -0
  40. agno/learn/schemas.py +82 -11
  41. agno/learn/stores/__init__.py +3 -0
  42. agno/learn/stores/decision_log.py +1156 -0
  43. agno/learn/stores/learned_knowledge.py +6 -6
  44. agno/models/anthropic/claude.py +24 -0
  45. agno/models/aws/bedrock.py +20 -0
  46. agno/models/base.py +48 -4
  47. agno/models/cohere/chat.py +25 -0
  48. agno/models/google/gemini.py +50 -5
  49. agno/models/litellm/chat.py +38 -0
  50. agno/models/openai/chat.py +7 -0
  51. agno/models/openrouter/openrouter.py +46 -0
  52. agno/models/response.py +16 -0
  53. agno/os/app.py +83 -44
  54. agno/os/middleware/__init__.py +2 -0
  55. agno/os/middleware/trailing_slash.py +27 -0
  56. agno/os/router.py +1 -0
  57. agno/os/routers/agents/router.py +29 -16
  58. agno/os/routers/agents/schema.py +6 -4
  59. agno/os/routers/components/__init__.py +3 -0
  60. agno/os/routers/components/components.py +466 -0
  61. agno/os/routers/evals/schemas.py +4 -3
  62. agno/os/routers/health.py +3 -3
  63. agno/os/routers/knowledge/knowledge.py +3 -3
  64. agno/os/routers/memory/schemas.py +4 -2
  65. agno/os/routers/metrics/metrics.py +9 -11
  66. agno/os/routers/metrics/schemas.py +10 -6
  67. agno/os/routers/registry/__init__.py +3 -0
  68. agno/os/routers/registry/registry.py +337 -0
  69. agno/os/routers/teams/router.py +20 -8
  70. agno/os/routers/teams/schema.py +6 -4
  71. agno/os/routers/traces/traces.py +5 -5
  72. agno/os/routers/workflows/router.py +38 -11
  73. agno/os/routers/workflows/schema.py +1 -1
  74. agno/os/schema.py +92 -26
  75. agno/os/utils.py +133 -16
  76. agno/reasoning/anthropic.py +2 -2
  77. agno/reasoning/azure_ai_foundry.py +2 -2
  78. agno/reasoning/deepseek.py +2 -2
  79. agno/reasoning/default.py +6 -7
  80. agno/reasoning/gemini.py +2 -2
  81. agno/reasoning/helpers.py +6 -7
  82. agno/reasoning/manager.py +4 -10
  83. agno/reasoning/ollama.py +2 -2
  84. agno/reasoning/openai.py +2 -2
  85. agno/reasoning/vertexai.py +2 -2
  86. agno/registry/__init__.py +3 -0
  87. agno/registry/registry.py +68 -0
  88. agno/run/agent.py +57 -0
  89. agno/run/base.py +7 -0
  90. agno/run/team.py +57 -0
  91. agno/skills/agent_skills.py +10 -3
  92. agno/team/__init__.py +3 -1
  93. agno/team/team.py +1276 -326
  94. agno/tools/duckduckgo.py +25 -71
  95. agno/tools/exa.py +0 -21
  96. agno/tools/function.py +35 -83
  97. agno/tools/knowledge.py +9 -4
  98. agno/tools/mem0.py +11 -10
  99. agno/tools/memory.py +47 -46
  100. agno/tools/parallel.py +0 -7
  101. agno/tools/reasoning.py +30 -23
  102. agno/tools/tavily.py +4 -1
  103. agno/tools/websearch.py +93 -0
  104. agno/tools/website.py +1 -1
  105. agno/tools/wikipedia.py +1 -1
  106. agno/tools/workflow.py +48 -47
  107. agno/utils/agent.py +42 -5
  108. agno/utils/events.py +160 -2
  109. agno/utils/print_response/agent.py +0 -31
  110. agno/utils/print_response/team.py +0 -2
  111. agno/utils/print_response/workflow.py +0 -2
  112. agno/utils/team.py +61 -11
  113. agno/vectordb/lancedb/lance_db.py +4 -1
  114. agno/vectordb/mongodb/mongodb.py +1 -1
  115. agno/vectordb/qdrant/qdrant.py +4 -4
  116. agno/workflow/__init__.py +3 -1
  117. agno/workflow/condition.py +0 -21
  118. agno/workflow/loop.py +0 -21
  119. agno/workflow/parallel.py +0 -21
  120. agno/workflow/router.py +0 -21
  121. agno/workflow/step.py +117 -24
  122. agno/workflow/steps.py +0 -21
  123. agno/workflow/workflow.py +625 -63
  124. {agno-2.3.25.dist-info → agno-2.4.0.dist-info}/METADATA +46 -76
  125. {agno-2.3.25.dist-info → agno-2.4.0.dist-info}/RECORD +128 -117
  126. {agno-2.3.25.dist-info → agno-2.4.0.dist-info}/WHEEL +0 -0
  127. {agno-2.3.25.dist-info → agno-2.4.0.dist-info}/licenses/LICENSE +0 -0
  128. {agno-2.3.25.dist-info → agno-2.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,466 @@
1
+ import logging
2
+ import time
3
+ from typing import Any, Dict, List, Optional, Union
4
+
5
+ from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
6
+
7
+ from agno.db.base import AsyncBaseDb, BaseDb
8
+ from agno.db.base import ComponentType as DbComponentType
9
+ from agno.os.auth import get_authentication_dependency
10
+ from agno.os.schema import (
11
+ BadRequestResponse,
12
+ ComponentConfigResponse,
13
+ ComponentCreate,
14
+ ComponentResponse,
15
+ ComponentType,
16
+ ComponentUpdate,
17
+ ConfigCreate,
18
+ ConfigUpdate,
19
+ InternalServerErrorResponse,
20
+ NotFoundResponse,
21
+ PaginatedResponse,
22
+ PaginationInfo,
23
+ UnauthenticatedResponse,
24
+ ValidationErrorResponse,
25
+ )
26
+ from agno.os.settings import AgnoAPISettings
27
+ from agno.registry import Registry
28
+ from agno.utils.log import log_error
29
+ from agno.utils.string import generate_id_from_name
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def _resolve_db_in_config(
35
+ config: Dict[str, Any],
36
+ os_db: BaseDb,
37
+ registry: Optional[Registry] = None,
38
+ ) -> Dict[str, Any]:
39
+ """
40
+ Resolve db reference in config by looking up in registry or OS db.
41
+
42
+ If config contains a db dict with an id, this function will:
43
+ 1. Check if the id matches the OS db
44
+ 2. Check if the id exists in the registry
45
+ 3. Convert the found db to a dict for serialization
46
+
47
+ Args:
48
+ config: The config dict that may contain a db reference
49
+ os_db: The OS database instance
50
+ registry: Optional registry containing registered databases
51
+
52
+ Returns:
53
+ Updated config dict with resolved db
54
+ """
55
+ component_db = config.get("db")
56
+ if component_db is not None and isinstance(component_db, dict):
57
+ component_db_id = component_db.get("id")
58
+ if component_db_id is not None:
59
+ resolved_db = None
60
+ # First check if it matches the OS db
61
+ if component_db_id == os_db.id:
62
+ resolved_db = os_db
63
+ # Then check the registry
64
+ elif registry is not None:
65
+ resolved_db = registry.get_db(component_db_id)
66
+
67
+ # Store the full db dict for serialization
68
+ if resolved_db is not None:
69
+ config["db"] = resolved_db.to_dict()
70
+ else:
71
+ log_error(f"Could not resolve db with id: {component_db_id}")
72
+ elif component_db is None and "db" in config:
73
+ # Explicitly set to None, remove the key
74
+ config.pop("db", None)
75
+
76
+ return config
77
+
78
+
79
+ def get_components_router(
80
+ os_db: Union[BaseDb, AsyncBaseDb],
81
+ settings: AgnoAPISettings = AgnoAPISettings(),
82
+ registry: Optional[Registry] = None,
83
+ ) -> APIRouter:
84
+ """Create components router."""
85
+ router = APIRouter(
86
+ dependencies=[Depends(get_authentication_dependency(settings))],
87
+ tags=["Components"],
88
+ responses={
89
+ 400: {"description": "Bad Request", "model": BadRequestResponse},
90
+ 401: {"description": "Unauthorized", "model": UnauthenticatedResponse},
91
+ 404: {"description": "Not Found", "model": NotFoundResponse},
92
+ 422: {"description": "Validation Error", "model": ValidationErrorResponse},
93
+ 500: {"description": "Internal Server Error", "model": InternalServerErrorResponse},
94
+ },
95
+ )
96
+ return attach_routes(router=router, os_db=os_db, registry=registry)
97
+
98
+
99
+ def attach_routes(
100
+ router: APIRouter, os_db: Union[BaseDb, AsyncBaseDb], registry: Optional[Registry] = None
101
+ ) -> APIRouter:
102
+ # Component routes require sync database
103
+ if not isinstance(os_db, BaseDb):
104
+ raise ValueError("Component routes require a sync database (BaseDb), not an async database.")
105
+ db: BaseDb = os_db # Type narrowed after isinstance check
106
+
107
+ @router.get(
108
+ "/components",
109
+ response_model=PaginatedResponse[ComponentResponse],
110
+ response_model_exclude_none=True,
111
+ status_code=200,
112
+ operation_id="list_components",
113
+ summary="List Components",
114
+ description="Retrieve a paginated list of components with optional filtering by type.",
115
+ )
116
+ async def list_components(
117
+ component_type: Optional[ComponentType] = Query(None, description="Filter by type: agent, team, workflow"),
118
+ page: int = Query(1, ge=1, description="Page number"),
119
+ limit: int = Query(20, ge=1, le=100, description="Items per page"),
120
+ ) -> PaginatedResponse[ComponentResponse]:
121
+ try:
122
+ start_time_ms = time.time() * 1000
123
+ offset = (page - 1) * limit
124
+
125
+ components, total_count = db.list_components(
126
+ component_type=DbComponentType(component_type.value) if component_type else None,
127
+ limit=limit,
128
+ offset=offset,
129
+ )
130
+
131
+ total_pages = (total_count + limit - 1) // limit if limit > 0 else 0
132
+
133
+ return PaginatedResponse(
134
+ data=[ComponentResponse(**c) for c in components],
135
+ meta=PaginationInfo(
136
+ page=page,
137
+ limit=limit,
138
+ total_pages=total_pages,
139
+ total_count=total_count,
140
+ search_time_ms=round(time.time() * 1000 - start_time_ms, 2),
141
+ ),
142
+ )
143
+ except Exception as e:
144
+ log_error(f"Error listing components: {e}")
145
+ raise HTTPException(status_code=500, detail="Internal server error")
146
+
147
+ @router.post(
148
+ "/components",
149
+ response_model=ComponentResponse,
150
+ response_model_exclude_none=True,
151
+ status_code=201,
152
+ operation_id="create_component",
153
+ summary="Create Component",
154
+ description="Create a new component (agent, team, or workflow) with initial config.",
155
+ )
156
+ async def create_component(
157
+ body: ComponentCreate,
158
+ ) -> ComponentResponse:
159
+ try:
160
+ component_id = body.component_id
161
+ if component_id is None:
162
+ component_id = generate_id_from_name(body.name)
163
+
164
+ # TODO: Create links from config
165
+
166
+ # Prepare config - ensure it's a dict and resolve db reference
167
+ config = body.config or {}
168
+ config = _resolve_db_in_config(config, db, registry)
169
+
170
+ component, _config = db.create_component_with_config(
171
+ component_id=component_id,
172
+ component_type=DbComponentType(body.component_type.value),
173
+ name=body.name,
174
+ description=body.description,
175
+ metadata=body.metadata,
176
+ config=config,
177
+ label=body.label,
178
+ stage=body.stage or "draft",
179
+ notes=body.notes,
180
+ )
181
+
182
+ return ComponentResponse(**component)
183
+ except ValueError as e:
184
+ raise HTTPException(status_code=400, detail=str(e))
185
+ except Exception as e:
186
+ log_error(f"Error creating component: {e}")
187
+ raise HTTPException(status_code=500, detail="Internal server error")
188
+
189
+ @router.get(
190
+ "/components/{component_id}",
191
+ response_model=ComponentResponse,
192
+ response_model_exclude_none=True,
193
+ status_code=200,
194
+ operation_id="get_component",
195
+ summary="Get Component",
196
+ description="Retrieve a component by ID.",
197
+ )
198
+ async def get_component(
199
+ component_id: str = Path(description="Component ID"),
200
+ ) -> ComponentResponse:
201
+ try:
202
+ component = db.get_component(component_id)
203
+ if component is None:
204
+ raise HTTPException(status_code=404, detail=f"Component {component_id} not found")
205
+ return ComponentResponse(**component)
206
+ except HTTPException:
207
+ raise
208
+ except Exception as e:
209
+ log_error(f"Error getting component: {e}")
210
+ raise HTTPException(status_code=500, detail="Internal server error")
211
+
212
+ @router.patch(
213
+ "/components/{component_id}",
214
+ response_model=ComponentResponse,
215
+ response_model_exclude_none=True,
216
+ status_code=200,
217
+ operation_id="update_component",
218
+ summary="Update Component",
219
+ description="Partially update a component by ID.",
220
+ )
221
+ async def update_component(
222
+ component_id: str = Path(description="Component ID"),
223
+ body: ComponentUpdate = Body(description="Component fields to update"),
224
+ ) -> ComponentResponse:
225
+ try:
226
+ existing = db.get_component(component_id)
227
+ if existing is None:
228
+ raise HTTPException(status_code=404, detail=f"Component {component_id} not found")
229
+
230
+ update_kwargs: Dict[str, Any] = {"component_id": component_id}
231
+ if body.name is not None:
232
+ update_kwargs["name"] = body.name
233
+ if body.description is not None:
234
+ update_kwargs["description"] = body.description
235
+ if body.metadata is not None:
236
+ update_kwargs["metadata"] = body.metadata
237
+ if body.component_type is not None:
238
+ update_kwargs["component_type"] = DbComponentType(body.component_type)
239
+
240
+ component = db.upsert_component(**update_kwargs)
241
+ return ComponentResponse(**component)
242
+ except HTTPException:
243
+ raise
244
+ except ValueError as e:
245
+ raise HTTPException(status_code=400, detail=str(e))
246
+ except Exception as e:
247
+ log_error(f"Error updating component: {e}")
248
+ raise HTTPException(status_code=500, detail="Internal server error")
249
+
250
+ @router.delete(
251
+ "/components/{component_id}",
252
+ status_code=204,
253
+ operation_id="delete_component",
254
+ summary="Delete Component",
255
+ description="Delete a component by ID.",
256
+ )
257
+ async def delete_component(
258
+ component_id: str = Path(description="Component ID"),
259
+ ) -> None:
260
+ try:
261
+ deleted = db.delete_component(component_id)
262
+ if not deleted:
263
+ raise HTTPException(status_code=404, detail=f"Component {component_id} not found")
264
+ except HTTPException:
265
+ raise
266
+ except Exception as e:
267
+ log_error(f"Error deleting component: {e}")
268
+ raise HTTPException(status_code=500, detail="Internal server error")
269
+
270
+ @router.get(
271
+ "/components/{component_id}/configs",
272
+ response_model=List[ComponentConfigResponse],
273
+ response_model_exclude_none=True,
274
+ status_code=200,
275
+ operation_id="list_configs",
276
+ summary="List Configs",
277
+ description="List all configs for a component.",
278
+ )
279
+ async def list_configs(
280
+ component_id: str = Path(description="Component ID"),
281
+ include_config: bool = Query(True, description="Include full config blob"),
282
+ ) -> List[ComponentConfigResponse]:
283
+ try:
284
+ configs = db.list_configs(component_id, include_config=include_config)
285
+ return [ComponentConfigResponse(**c) for c in configs]
286
+ except Exception as e:
287
+ log_error(f"Error listing configs: {e}")
288
+ raise HTTPException(status_code=500, detail="Internal server error")
289
+
290
+ @router.post(
291
+ "/components/{component_id}/configs",
292
+ response_model=ComponentConfigResponse,
293
+ response_model_exclude_none=True,
294
+ status_code=201,
295
+ operation_id="create_config",
296
+ summary="Create Config Version",
297
+ description="Create a new config version for a component.",
298
+ )
299
+ async def create_config(
300
+ component_id: str = Path(description="Component ID"),
301
+ body: ConfigCreate = Body(description="Config data"),
302
+ ) -> ComponentConfigResponse:
303
+ try:
304
+ # Resolve db from config if present
305
+ config_data = body.config or {}
306
+ config_data = _resolve_db_in_config(config_data, db, registry)
307
+
308
+ config = db.upsert_config(
309
+ component_id=component_id,
310
+ version=None, # Always create new
311
+ config=config_data,
312
+ label=body.label,
313
+ stage=body.stage,
314
+ notes=body.notes,
315
+ links=body.links,
316
+ )
317
+ return ComponentConfigResponse(**config)
318
+ except ValueError as e:
319
+ raise HTTPException(status_code=400, detail=str(e))
320
+ except Exception as e:
321
+ log_error(f"Error creating config: {e}")
322
+ raise HTTPException(status_code=500, detail="Internal server error")
323
+
324
+ @router.patch(
325
+ "/components/{component_id}/configs/{version}",
326
+ response_model=ComponentConfigResponse,
327
+ response_model_exclude_none=True,
328
+ status_code=200,
329
+ operation_id="update_config",
330
+ summary="Update Draft Config",
331
+ description="Update an existing draft config. Cannot update published configs.",
332
+ )
333
+ async def update_config(
334
+ component_id: str = Path(description="Component ID"),
335
+ version: int = Path(description="Version number"),
336
+ body: ConfigUpdate = Body(description="Config fields to update"),
337
+ ) -> ComponentConfigResponse:
338
+ try:
339
+ # Resolve db from config if present
340
+ config_data = body.config
341
+ if config_data is not None:
342
+ config_data = _resolve_db_in_config(config_data, db, registry)
343
+
344
+ config = db.upsert_config(
345
+ component_id=component_id,
346
+ version=version, # Always update existing
347
+ config=config_data,
348
+ label=body.label,
349
+ stage=body.stage,
350
+ notes=body.notes,
351
+ links=body.links,
352
+ )
353
+ return ComponentConfigResponse(**config)
354
+ except ValueError as e:
355
+ raise HTTPException(status_code=400, detail=str(e))
356
+ except Exception as e:
357
+ log_error(f"Error updating config: {e}")
358
+ raise HTTPException(status_code=500, detail="Internal server error")
359
+
360
+ @router.get(
361
+ "/components/{component_id}/configs/current",
362
+ response_model=ComponentConfigResponse,
363
+ response_model_exclude_none=True,
364
+ status_code=200,
365
+ operation_id="get_current_config",
366
+ summary="Get Current Config",
367
+ description="Get the current config version for a component.",
368
+ )
369
+ async def get_current_config(
370
+ component_id: str = Path(description="Component ID"),
371
+ ) -> ComponentConfigResponse:
372
+ try:
373
+ config = db.get_config(component_id)
374
+ if config is None:
375
+ raise HTTPException(status_code=404, detail=f"No current config for {component_id}")
376
+ return ComponentConfigResponse(**config)
377
+ except HTTPException:
378
+ raise
379
+ except Exception as e:
380
+ log_error(f"Error getting config: {e}")
381
+ raise HTTPException(status_code=500, detail="Internal server error")
382
+
383
+ @router.get(
384
+ "/components/{component_id}/configs/{version}",
385
+ response_model=ComponentConfigResponse,
386
+ response_model_exclude_none=True,
387
+ status_code=200,
388
+ operation_id="get_config",
389
+ summary="Get Config Version",
390
+ description="Get a specific config version by number.",
391
+ )
392
+ async def get_config_version(
393
+ component_id: str = Path(description="Component ID"),
394
+ version: int = Path(description="Version number"),
395
+ ) -> ComponentConfigResponse:
396
+ try:
397
+ config = db.get_config(component_id, version=version)
398
+
399
+ if config is None:
400
+ raise HTTPException(status_code=404, detail=f"Config {component_id} v{version} not found")
401
+ return ComponentConfigResponse(**config)
402
+ except HTTPException:
403
+ raise
404
+ except Exception as e:
405
+ log_error(f"Error getting config: {e}")
406
+ raise HTTPException(status_code=500, detail="Internal server error")
407
+
408
+ @router.delete(
409
+ "/components/{component_id}/configs/{version}",
410
+ status_code=204,
411
+ operation_id="delete_config",
412
+ summary="Delete Config Version",
413
+ description="Delete a specific draft config version. Cannot delete published or current configs.",
414
+ )
415
+ async def delete_config_version(
416
+ component_id: str = Path(description="Component ID"),
417
+ version: int = Path(description="Version number"),
418
+ ) -> None:
419
+ try:
420
+ # Resolve version number
421
+ deleted = db.delete_config(component_id, version=version)
422
+ if not deleted:
423
+ raise HTTPException(status_code=404, detail=f"Config {component_id} v{version} not found")
424
+ except HTTPException:
425
+ raise
426
+ except ValueError as e:
427
+ raise HTTPException(status_code=400, detail=str(e))
428
+ except Exception as e:
429
+ log_error(f"Error deleting config: {e}")
430
+ raise HTTPException(status_code=500, detail="Internal server error")
431
+
432
+ @router.post(
433
+ "/components/{component_id}/configs/{version}/set-current",
434
+ response_model=ComponentResponse,
435
+ response_model_exclude_none=True,
436
+ status_code=200,
437
+ operation_id="set_current_config",
438
+ summary="Set Current Config Version",
439
+ description="Set a published config version as current (for rollback).",
440
+ )
441
+ async def set_current_config(
442
+ component_id: str = Path(description="Component ID"),
443
+ version: int = Path(description="Version number"),
444
+ ) -> ComponentResponse:
445
+ try:
446
+ success = db.set_current_version(component_id, version=version)
447
+ if not success:
448
+ raise HTTPException(
449
+ status_code=404, detail=f"Component {component_id} or config version {version} not found"
450
+ )
451
+
452
+ # Fetch and return updated component
453
+ component = db.get_component(component_id)
454
+ if component is None:
455
+ raise HTTPException(status_code=404, detail=f"Component {component_id} not found")
456
+
457
+ return ComponentResponse(**component)
458
+ except HTTPException:
459
+ raise
460
+ except ValueError as e:
461
+ raise HTTPException(status_code=400, detail=str(e))
462
+ except Exception as e:
463
+ log_error(f"Error setting current config: {e}")
464
+ raise HTTPException(status_code=500, detail="Internal server error")
465
+
466
+ return router
@@ -1,5 +1,5 @@
1
1
  from dataclasses import asdict
2
- from datetime import datetime, timezone
2
+ from datetime import datetime
3
3
  from typing import Any, Dict, List, Literal, Optional
4
4
 
5
5
  from pydantic import BaseModel, Field
@@ -10,6 +10,7 @@ from agno.eval.accuracy import AccuracyEval
10
10
  from agno.eval.agent_as_judge import AgentAsJudgeEval
11
11
  from agno.eval.performance import PerformanceEval
12
12
  from agno.eval.reliability import ReliabilityEval
13
+ from agno.os.utils import to_utc_datetime
13
14
 
14
15
 
15
16
  class EvalRunInput(BaseModel):
@@ -74,8 +75,8 @@ class EvalSchema(BaseModel):
74
75
  eval_type=eval_run["eval_type"],
75
76
  eval_data=eval_run["eval_data"],
76
77
  eval_input=eval_run.get("eval_input"),
77
- created_at=datetime.fromtimestamp(eval_run["created_at"], tz=timezone.utc),
78
- updated_at=datetime.fromtimestamp(eval_run["updated_at"], tz=timezone.utc),
78
+ created_at=to_utc_datetime(eval_run.get("created_at")),
79
+ updated_at=to_utc_datetime(eval_run.get("updated_at")),
79
80
  )
80
81
 
81
82
  @classmethod
agno/os/routers/health.py CHANGED
@@ -8,7 +8,7 @@ from agno.os.schema import HealthResponse
8
8
  def get_health_router(health_endpoint: str = "/health") -> APIRouter:
9
9
  router = APIRouter(tags=["Health"])
10
10
 
11
- started_time_stamp = datetime.now(timezone.utc).timestamp()
11
+ started_at = datetime.now(timezone.utc)
12
12
 
13
13
  @router.get(
14
14
  health_endpoint,
@@ -20,12 +20,12 @@ def get_health_router(health_endpoint: str = "/health") -> APIRouter:
20
20
  200: {
21
21
  "description": "API is healthy and operational",
22
22
  "content": {
23
- "application/json": {"example": {"status": "ok", "instantiated_at": str(started_time_stamp)}}
23
+ "application/json": {"example": {"status": "ok", "instantiated_at": "2025-06-10T12:00:00Z"}}
24
24
  },
25
25
  }
26
26
  },
27
27
  )
28
28
  async def health_check() -> HealthResponse:
29
- return HealthResponse(status="ok", instantiated_at=str(started_time_stamp))
29
+ return HealthResponse(status="ok", instantiated_at=started_at)
30
30
 
31
31
  return router
@@ -671,7 +671,7 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Union[Knowledge,
671
671
  # Use max_results if specified, otherwise use a higher limit for search then paginate
672
672
  search_limit = request.max_results
673
673
 
674
- results = await knowledge.async_search(
674
+ results = await knowledge.asearch(
675
675
  query=request.query, max_results=search_limit, filters=request.filters, search_type=request.search_type
676
676
  )
677
677
 
@@ -1047,7 +1047,7 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Union[Knowledge,
1047
1047
  search_types=search_types,
1048
1048
  )
1049
1049
  )
1050
- filters = await knowledge.async_get_valid_filters()
1050
+ filters = await knowledge.aget_valid_filters()
1051
1051
  return ConfigResponseSchema(
1052
1052
  readers=reader_schemas,
1053
1053
  vector_dbs=vector_dbs,
@@ -1098,7 +1098,7 @@ async def process_content(
1098
1098
  log_debug(f"Set chunking strategy: {chunker}")
1099
1099
 
1100
1100
  log_debug(f"Using reader: {content.reader.__class__.__name__}")
1101
- await knowledge._load_content_async(content, upsert=False, skip_if_exists=True)
1101
+ await knowledge._aload_content(content, upsert=False, skip_if_exists=True)
1102
1102
  log_info(f"Content {content.id} processed successfully")
1103
1103
  except Exception as e:
1104
1104
  log_info(f"Error processing content: {e}")
@@ -1,9 +1,11 @@
1
1
  import json
2
- from datetime import datetime, timezone
2
+ from datetime import datetime
3
3
  from typing import Any, Dict, List, Optional
4
4
 
5
5
  from pydantic import BaseModel, Field
6
6
 
7
+ from agno.os.utils import to_utc_datetime
8
+
7
9
 
8
10
  class DeleteMemoriesRequest(BaseModel):
9
11
  memory_ids: List[str] = Field(..., description="List of memory IDs to delete", min_length=1)
@@ -71,7 +73,7 @@ class UserStatsSchema(BaseModel):
71
73
  return cls(
72
74
  user_id=str(user_stats_dict["user_id"]),
73
75
  total_memories=user_stats_dict["total_memories"],
74
- last_memory_updated_at=datetime.fromtimestamp(updated_at, tz=timezone.utc) if updated_at else None,
76
+ last_memory_updated_at=to_utc_datetime(updated_at),
75
77
  )
76
78
 
77
79
 
@@ -1,5 +1,5 @@
1
1
  import logging
2
- from datetime import date, datetime, timezone
2
+ from datetime import date
3
3
  from typing import List, Optional, Union, cast
4
4
 
5
5
  from fastapi import Depends, HTTPException, Query, Request
@@ -16,7 +16,7 @@ from agno.os.schema import (
16
16
  ValidationErrorResponse,
17
17
  )
18
18
  from agno.os.settings import AgnoAPISettings
19
- from agno.os.utils import get_db
19
+ from agno.os.utils import get_db, to_utc_datetime
20
20
  from agno.remote.base import RemoteDb
21
21
 
22
22
  logger = logging.getLogger(__name__)
@@ -79,9 +79,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
79
79
  "reasoning_tokens": 0,
80
80
  },
81
81
  "model_metrics": [{"model_id": "gpt-4o", "model_provider": "OpenAI", "count": 5}],
82
- "date": "2025-07-31T00:00:00",
83
- "created_at": 1753993132,
84
- "updated_at": 1753993741,
82
+ "date": "2025-07-31T00:00:00Z",
83
+ "created_at": "2025-07-31T12:38:52Z",
84
+ "updated_at": "2025-07-31T12:49:01Z",
85
85
  }
86
86
  ]
87
87
  }
@@ -121,9 +121,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
121
121
 
122
122
  return MetricsResponse(
123
123
  metrics=[DayAggregatedMetrics.from_dict(metric) for metric in metrics],
124
- updated_at=datetime.fromtimestamp(latest_updated_at, tz=timezone.utc)
125
- if latest_updated_at is not None
126
- else None,
124
+ updated_at=to_utc_datetime(latest_updated_at),
127
125
  )
128
126
 
129
127
  except Exception as e:
@@ -167,9 +165,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
167
165
  "reasoning_tokens": 0,
168
166
  },
169
167
  "model_metrics": [{"model_id": "gpt-4o", "model_provider": "OpenAI", "count": 2}],
170
- "date": "2025-08-12T00:00:00",
171
- "created_at": 1755016907,
172
- "updated_at": 1755016907,
168
+ "date": "2025-08-12T00:00:00Z",
169
+ "created_at": "2025-08-12T08:01:47Z",
170
+ "updated_at": "2025-08-12T08:01:47Z",
173
171
  }
174
172
  ]
175
173
  }
@@ -1,8 +1,10 @@
1
- from datetime import datetime
1
+ from datetime import datetime, timezone
2
2
  from typing import Any, Dict, List, Optional
3
3
 
4
4
  from pydantic import BaseModel, Field
5
5
 
6
+ from agno.os.utils import to_utc_datetime
7
+
6
8
 
7
9
  class DayAggregatedMetrics(BaseModel):
8
10
  """Aggregated metrics for a given day"""
@@ -20,22 +22,24 @@ class DayAggregatedMetrics(BaseModel):
20
22
  model_metrics: List[Dict[str, Any]] = Field(..., description="Metrics grouped by model (model_id, provider, count)")
21
23
 
22
24
  date: datetime = Field(..., description="Date for which these metrics are aggregated")
23
- created_at: int = Field(..., description="Unix timestamp when metrics were created", ge=0)
24
- updated_at: int = Field(..., description="Unix timestamp when metrics were last updated", ge=0)
25
+ created_at: datetime = Field(..., description="Timestamp when metrics were created")
26
+ updated_at: datetime = Field(..., description="Timestamp when metrics were last updated")
25
27
 
26
28
  @classmethod
27
29
  def from_dict(cls, metrics_dict: Dict[str, Any]) -> "DayAggregatedMetrics":
30
+ created_at = to_utc_datetime(metrics_dict.get("created_at")) or datetime.now(timezone.utc)
31
+ updated_at = to_utc_datetime(metrics_dict.get("updated_at", created_at)) or created_at
28
32
  return cls(
29
33
  agent_runs_count=metrics_dict.get("agent_runs_count", 0),
30
34
  agent_sessions_count=metrics_dict.get("agent_sessions_count", 0),
31
- created_at=metrics_dict.get("created_at", 0),
32
- date=metrics_dict.get("date", datetime.now()),
35
+ date=metrics_dict.get("date", datetime.now(timezone.utc)),
33
36
  id=metrics_dict.get("id", ""),
34
37
  model_metrics=metrics_dict.get("model_metrics", {}),
35
38
  team_runs_count=metrics_dict.get("team_runs_count", 0),
36
39
  team_sessions_count=metrics_dict.get("team_sessions_count", 0),
37
40
  token_metrics=metrics_dict.get("token_metrics", {}),
38
- updated_at=metrics_dict.get("updated_at", metrics_dict.get("created_at", 0)),
41
+ created_at=created_at,
42
+ updated_at=updated_at,
39
43
  users_count=metrics_dict.get("users_count", 0),
40
44
  workflow_runs_count=metrics_dict.get("workflow_runs_count", 0),
41
45
  workflow_sessions_count=metrics_dict.get("workflow_sessions_count", 0),
@@ -0,0 +1,3 @@
1
+ from agno.os.routers.registry.registry import get_registry_router
2
+
3
+ __all__ = ["get_registry_router"]