agno 2.3.26__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.
- agno/agent/__init__.py +4 -0
- agno/agent/agent.py +1368 -541
- agno/agent/remote.py +13 -0
- agno/db/base.py +339 -0
- agno/db/postgres/async_postgres.py +116 -12
- agno/db/postgres/postgres.py +1229 -25
- agno/db/postgres/schemas.py +48 -1
- agno/db/sqlite/async_sqlite.py +119 -4
- agno/db/sqlite/schemas.py +51 -0
- agno/db/sqlite/sqlite.py +1173 -13
- agno/db/utils.py +37 -1
- agno/knowledge/__init__.py +4 -0
- agno/knowledge/chunking/code.py +1 -1
- agno/knowledge/chunking/semantic.py +1 -1
- agno/knowledge/chunking/strategy.py +4 -0
- agno/knowledge/filesystem.py +412 -0
- agno/knowledge/knowledge.py +2767 -2254
- agno/knowledge/protocol.py +134 -0
- agno/knowledge/reader/arxiv_reader.py +2 -2
- agno/knowledge/reader/base.py +9 -7
- agno/knowledge/reader/csv_reader.py +5 -5
- agno/knowledge/reader/docx_reader.py +2 -2
- agno/knowledge/reader/field_labeled_csv_reader.py +2 -2
- agno/knowledge/reader/firecrawl_reader.py +2 -2
- agno/knowledge/reader/json_reader.py +2 -2
- agno/knowledge/reader/markdown_reader.py +2 -2
- agno/knowledge/reader/pdf_reader.py +5 -4
- agno/knowledge/reader/pptx_reader.py +2 -2
- agno/knowledge/reader/reader_factory.py +110 -0
- agno/knowledge/reader/s3_reader.py +2 -2
- agno/knowledge/reader/tavily_reader.py +2 -2
- agno/knowledge/reader/text_reader.py +2 -2
- agno/knowledge/reader/web_search_reader.py +2 -2
- agno/knowledge/reader/website_reader.py +5 -3
- agno/knowledge/reader/wikipedia_reader.py +2 -2
- agno/knowledge/reader/youtube_reader.py +2 -2
- agno/knowledge/utils.py +37 -29
- agno/learn/__init__.py +6 -0
- agno/learn/machine.py +35 -0
- agno/learn/schemas.py +82 -11
- agno/learn/stores/__init__.py +3 -0
- agno/learn/stores/decision_log.py +1156 -0
- agno/learn/stores/learned_knowledge.py +6 -6
- agno/models/anthropic/claude.py +24 -0
- agno/models/aws/bedrock.py +20 -0
- agno/models/base.py +48 -4
- agno/models/cohere/chat.py +25 -0
- agno/models/google/gemini.py +50 -5
- agno/models/litellm/chat.py +38 -0
- agno/models/openai/chat.py +7 -0
- agno/models/openrouter/openrouter.py +46 -0
- agno/models/response.py +16 -0
- agno/os/app.py +83 -44
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/trailing_slash.py +27 -0
- agno/os/router.py +1 -0
- agno/os/routers/agents/router.py +29 -16
- agno/os/routers/agents/schema.py +6 -4
- agno/os/routers/components/__init__.py +3 -0
- agno/os/routers/components/components.py +466 -0
- agno/os/routers/evals/schemas.py +4 -3
- agno/os/routers/health.py +3 -3
- agno/os/routers/knowledge/knowledge.py +3 -3
- agno/os/routers/memory/schemas.py +4 -2
- agno/os/routers/metrics/metrics.py +9 -11
- agno/os/routers/metrics/schemas.py +10 -6
- agno/os/routers/registry/__init__.py +3 -0
- agno/os/routers/registry/registry.py +337 -0
- agno/os/routers/teams/router.py +20 -8
- agno/os/routers/teams/schema.py +6 -4
- agno/os/routers/traces/traces.py +5 -5
- agno/os/routers/workflows/router.py +38 -11
- agno/os/routers/workflows/schema.py +1 -1
- agno/os/schema.py +92 -26
- agno/os/utils.py +84 -19
- agno/reasoning/anthropic.py +2 -2
- agno/reasoning/azure_ai_foundry.py +2 -2
- agno/reasoning/deepseek.py +2 -2
- agno/reasoning/default.py +6 -7
- agno/reasoning/gemini.py +2 -2
- agno/reasoning/helpers.py +6 -7
- agno/reasoning/manager.py +4 -10
- agno/reasoning/ollama.py +2 -2
- agno/reasoning/openai.py +2 -2
- agno/reasoning/vertexai.py +2 -2
- agno/registry/__init__.py +3 -0
- agno/registry/registry.py +68 -0
- agno/run/agent.py +57 -0
- agno/run/base.py +7 -0
- agno/run/team.py +57 -0
- agno/skills/agent_skills.py +10 -3
- agno/team/__init__.py +3 -1
- agno/team/team.py +1145 -326
- agno/tools/duckduckgo.py +25 -71
- agno/tools/exa.py +0 -21
- agno/tools/function.py +35 -83
- agno/tools/knowledge.py +9 -4
- agno/tools/mem0.py +11 -10
- agno/tools/memory.py +47 -46
- agno/tools/parallel.py +0 -7
- agno/tools/reasoning.py +30 -23
- agno/tools/tavily.py +4 -1
- agno/tools/websearch.py +93 -0
- agno/tools/website.py +1 -1
- agno/tools/wikipedia.py +1 -1
- agno/tools/workflow.py +48 -47
- agno/utils/agent.py +42 -5
- agno/utils/events.py +160 -2
- agno/utils/print_response/agent.py +0 -31
- agno/utils/print_response/team.py +0 -2
- agno/utils/print_response/workflow.py +0 -2
- agno/utils/team.py +61 -11
- agno/vectordb/lancedb/lance_db.py +4 -1
- agno/vectordb/mongodb/mongodb.py +1 -1
- agno/vectordb/qdrant/qdrant.py +4 -4
- agno/workflow/__init__.py +3 -1
- agno/workflow/condition.py +0 -21
- agno/workflow/loop.py +0 -21
- agno/workflow/parallel.py +0 -21
- agno/workflow/router.py +0 -21
- agno/workflow/step.py +117 -24
- agno/workflow/steps.py +0 -21
- agno/workflow/workflow.py +427 -63
- {agno-2.3.26.dist-info → agno-2.4.0.dist-info}/METADATA +46 -76
- {agno-2.3.26.dist-info → agno-2.4.0.dist-info}/RECORD +128 -117
- {agno-2.3.26.dist-info → agno-2.4.0.dist-info}/WHEEL +0 -0
- {agno-2.3.26.dist-info → agno-2.4.0.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.26.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
|
agno/os/routers/evals/schemas.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from dataclasses import asdict
|
|
2
|
-
from datetime import datetime
|
|
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=
|
|
78
|
-
updated_at=
|
|
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
|
-
|
|
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":
|
|
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=
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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=
|
|
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
|
|
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:
|
|
83
|
-
"created_at":
|
|
84
|
-
"updated_at":
|
|
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=
|
|
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:
|
|
171
|
-
"created_at":
|
|
172
|
-
"updated_at":
|
|
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:
|
|
24
|
-
updated_at:
|
|
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
|
-
|
|
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
|
-
|
|
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),
|