agno 2.0.3__py3-none-any.whl → 2.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/agent/agent.py +162 -86
- agno/db/dynamo/dynamo.py +8 -0
- agno/db/firestore/firestore.py +8 -1
- agno/db/gcs_json/gcs_json_db.py +9 -0
- agno/db/json/json_db.py +8 -0
- agno/db/mongo/mongo.py +10 -1
- agno/db/mysql/mysql.py +10 -0
- agno/db/postgres/postgres.py +16 -8
- agno/db/redis/redis.py +6 -0
- agno/db/singlestore/schemas.py +1 -1
- agno/db/singlestore/singlestore.py +8 -1
- agno/db/sqlite/sqlite.py +9 -1
- agno/db/utils.py +14 -0
- agno/knowledge/knowledge.py +91 -65
- agno/models/base.py +2 -2
- agno/models/openai/chat.py +3 -0
- agno/models/openai/responses.py +6 -0
- agno/models/response.py +5 -0
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +25 -0
- agno/os/app.py +4 -1
- agno/os/auth.py +24 -14
- agno/os/router.py +128 -55
- agno/os/routers/evals/utils.py +9 -9
- agno/os/routers/health.py +26 -0
- agno/os/routers/knowledge/knowledge.py +11 -11
- agno/os/routers/session/session.py +24 -8
- agno/os/schema.py +8 -2
- agno/run/workflow.py +64 -10
- agno/session/team.py +1 -0
- agno/team/team.py +192 -92
- agno/tools/mem0.py +11 -17
- agno/tools/memory.py +34 -6
- agno/utils/common.py +90 -1
- agno/utils/streamlit.py +14 -8
- agno/vectordb/chroma/chromadb.py +8 -2
- agno/workflow/step.py +111 -13
- agno/workflow/workflow.py +16 -13
- {agno-2.0.3.dist-info → agno-2.0.4.dist-info}/METADATA +1 -1
- {agno-2.0.3.dist-info → agno-2.0.4.dist-info}/RECORD +43 -40
- {agno-2.0.3.dist-info → agno-2.0.4.dist-info}/WHEEL +0 -0
- {agno-2.0.3.dist-info → agno-2.0.4.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.3.dist-info → agno-2.0.4.dist-info}/top_level.txt +0 -0
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,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
|
@@ -27,8 +27,9 @@ from agno.os.config import (
|
|
|
27
27
|
SessionDomainConfig,
|
|
28
28
|
)
|
|
29
29
|
from agno.os.interfaces.base import BaseInterface
|
|
30
|
-
from agno.os.router import get_base_router
|
|
30
|
+
from agno.os.router import get_base_router, get_websocket_router
|
|
31
31
|
from agno.os.routers.evals import get_eval_router
|
|
32
|
+
from agno.os.routers.health import get_health_router
|
|
32
33
|
from agno.os.routers.knowledge import get_knowledge_router
|
|
33
34
|
from agno.os.routers.memory import get_memory_router
|
|
34
35
|
from agno.os.routers.metrics import get_metrics_router
|
|
@@ -208,6 +209,8 @@ class AgentOS:
|
|
|
208
209
|
|
|
209
210
|
# Add routes
|
|
210
211
|
self.fastapi_app.include_router(get_base_router(self, settings=self.settings))
|
|
212
|
+
self.fastapi_app.include_router(get_websocket_router(self, settings=self.settings))
|
|
213
|
+
self.fastapi_app.include_router(get_health_router())
|
|
211
214
|
|
|
212
215
|
for interface in self.interfaces:
|
|
213
216
|
interface_router = interface.get_router()
|
agno/os/auth.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
from
|
|
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(
|
|
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
|
|
27
|
+
if not credentials:
|
|
30
28
|
raise HTTPException(status_code=401, detail="Authorization header required")
|
|
31
29
|
|
|
32
|
-
|
|
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
|
agno/os/router.py
CHANGED
|
@@ -18,13 +18,12 @@ from pydantic import BaseModel
|
|
|
18
18
|
from agno.agent.agent import Agent
|
|
19
19
|
from agno.media import Audio, Image, Video
|
|
20
20
|
from agno.media import File as FileMedia
|
|
21
|
-
from agno.os.auth import get_authentication_dependency
|
|
21
|
+
from agno.os.auth import get_authentication_dependency, validate_websocket_token
|
|
22
22
|
from agno.os.schema import (
|
|
23
23
|
AgentResponse,
|
|
24
24
|
AgentSummaryResponse,
|
|
25
25
|
BadRequestResponse,
|
|
26
26
|
ConfigResponse,
|
|
27
|
-
HealthResponse,
|
|
28
27
|
InterfaceResponse,
|
|
29
28
|
InternalServerErrorResponse,
|
|
30
29
|
Model,
|
|
@@ -49,7 +48,7 @@ from agno.os.utils import (
|
|
|
49
48
|
from agno.run.agent import RunErrorEvent, RunOutput, RunOutputEvent
|
|
50
49
|
from agno.run.team import RunErrorEvent as TeamRunErrorEvent
|
|
51
50
|
from agno.run.team import TeamRunOutputEvent
|
|
52
|
-
from agno.run.workflow import WorkflowErrorEvent, WorkflowRunOutputEvent
|
|
51
|
+
from agno.run.workflow import WorkflowErrorEvent, WorkflowRunOutput, WorkflowRunOutputEvent
|
|
53
52
|
from agno.team.team import Team
|
|
54
53
|
from agno.utils.log import log_debug, log_error, log_warning, logger
|
|
55
54
|
from agno.workflow.workflow import Workflow
|
|
@@ -110,6 +109,7 @@ class WebSocketManager:
|
|
|
110
109
|
"""Manages WebSocket connections for workflow runs"""
|
|
111
110
|
|
|
112
111
|
active_connections: Dict[str, WebSocket] # {run_id: websocket}
|
|
112
|
+
authenticated_connections: Dict[WebSocket, bool] # {websocket: is_authenticated}
|
|
113
113
|
|
|
114
114
|
def __init__(
|
|
115
115
|
self,
|
|
@@ -117,22 +117,51 @@ class WebSocketManager:
|
|
|
117
117
|
):
|
|
118
118
|
# Store active connections: {run_id: websocket}
|
|
119
119
|
self.active_connections = active_connections or {}
|
|
120
|
+
# Track authentication state for each websocket
|
|
121
|
+
self.authenticated_connections = {}
|
|
120
122
|
|
|
121
|
-
async def connect(self, websocket: WebSocket):
|
|
123
|
+
async def connect(self, websocket: WebSocket, requires_auth: bool = True):
|
|
122
124
|
"""Accept WebSocket connection"""
|
|
123
125
|
await websocket.accept()
|
|
124
126
|
logger.debug("WebSocket connected")
|
|
125
127
|
|
|
126
|
-
#
|
|
128
|
+
# If auth is not required, mark as authenticated immediately
|
|
129
|
+
self.authenticated_connections[websocket] = not requires_auth
|
|
130
|
+
|
|
131
|
+
# Send connection confirmation with auth requirement info
|
|
127
132
|
await websocket.send_text(
|
|
128
133
|
json.dumps(
|
|
129
134
|
{
|
|
130
135
|
"event": "connected",
|
|
131
|
-
"message":
|
|
136
|
+
"message": (
|
|
137
|
+
"Connected to workflow events. Please authenticate to continue."
|
|
138
|
+
if requires_auth
|
|
139
|
+
else "Connected to workflow events. Authentication not required."
|
|
140
|
+
),
|
|
141
|
+
"requires_auth": requires_auth,
|
|
132
142
|
}
|
|
133
143
|
)
|
|
134
144
|
)
|
|
135
145
|
|
|
146
|
+
async def authenticate_websocket(self, websocket: WebSocket):
|
|
147
|
+
"""Mark a WebSocket connection as authenticated"""
|
|
148
|
+
self.authenticated_connections[websocket] = True
|
|
149
|
+
logger.debug("WebSocket authenticated")
|
|
150
|
+
|
|
151
|
+
# Send authentication confirmation
|
|
152
|
+
await websocket.send_text(
|
|
153
|
+
json.dumps(
|
|
154
|
+
{
|
|
155
|
+
"event": "authenticated",
|
|
156
|
+
"message": "Authentication successful. You can now send commands.",
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def is_authenticated(self, websocket: WebSocket) -> bool:
|
|
162
|
+
"""Check if a WebSocket connection is authenticated"""
|
|
163
|
+
return self.authenticated_connections.get(websocket, False)
|
|
164
|
+
|
|
136
165
|
async def register_workflow_websocket(self, run_id: str, websocket: WebSocket):
|
|
137
166
|
"""Register a workflow run with its WebSocket connection"""
|
|
138
167
|
self.active_connections[run_id] = websocket
|
|
@@ -141,9 +170,26 @@ class WebSocketManager:
|
|
|
141
170
|
async def disconnect_by_run_id(self, run_id: str):
|
|
142
171
|
"""Remove WebSocket connection by run_id"""
|
|
143
172
|
if run_id in self.active_connections:
|
|
173
|
+
websocket = self.active_connections[run_id]
|
|
144
174
|
del self.active_connections[run_id]
|
|
175
|
+
# Clean up authentication state
|
|
176
|
+
if websocket in self.authenticated_connections:
|
|
177
|
+
del self.authenticated_connections[websocket]
|
|
145
178
|
logger.debug(f"WebSocket disconnected for run_id: {run_id}")
|
|
146
179
|
|
|
180
|
+
async def disconnect_websocket(self, websocket: WebSocket):
|
|
181
|
+
"""Remove WebSocket connection and clean up all associated state"""
|
|
182
|
+
# Remove from authenticated connections
|
|
183
|
+
if websocket in self.authenticated_connections:
|
|
184
|
+
del self.authenticated_connections[websocket]
|
|
185
|
+
|
|
186
|
+
# Remove from active connections
|
|
187
|
+
runs_to_remove = [run_id for run_id, ws in self.active_connections.items() if ws == websocket]
|
|
188
|
+
for run_id in runs_to_remove:
|
|
189
|
+
del self.active_connections[run_id]
|
|
190
|
+
|
|
191
|
+
logger.debug("WebSocket disconnected and cleaned up")
|
|
192
|
+
|
|
147
193
|
async def get_websocket_for_run(self, run_id: str) -> Optional[WebSocket]:
|
|
148
194
|
"""Get WebSocket connection for a workflow run"""
|
|
149
195
|
return self.active_connections.get(run_id)
|
|
@@ -288,7 +334,7 @@ async def handle_workflow_via_websocket(websocket: WebSocket, message: dict, os:
|
|
|
288
334
|
session_id = str(uuid4())
|
|
289
335
|
|
|
290
336
|
# Execute workflow in background with streaming
|
|
291
|
-
await workflow.arun(
|
|
337
|
+
workflow_result = await workflow.arun(
|
|
292
338
|
input=user_message,
|
|
293
339
|
session_id=session_id,
|
|
294
340
|
user_id=user_id,
|
|
@@ -298,6 +344,10 @@ async def handle_workflow_via_websocket(websocket: WebSocket, message: dict, os:
|
|
|
298
344
|
websocket=websocket,
|
|
299
345
|
)
|
|
300
346
|
|
|
347
|
+
workflow_run_output = cast(WorkflowRunOutput, workflow_result)
|
|
348
|
+
|
|
349
|
+
await websocket_manager.register_workflow_websocket(workflow_run_output.run_id, websocket) # type: ignore
|
|
350
|
+
|
|
301
351
|
except Exception as e:
|
|
302
352
|
logger.error(f"Error executing workflow via WebSocket: {e}")
|
|
303
353
|
await websocket.send_text(json.dumps({"event": "error", "error": str(e)}))
|
|
@@ -334,6 +384,77 @@ async def workflow_response_streamer(
|
|
|
334
384
|
return
|
|
335
385
|
|
|
336
386
|
|
|
387
|
+
def get_websocket_router(
|
|
388
|
+
os: "AgentOS",
|
|
389
|
+
settings: AgnoAPISettings = AgnoAPISettings(),
|
|
390
|
+
) -> APIRouter:
|
|
391
|
+
"""
|
|
392
|
+
Create WebSocket router without HTTP authentication dependencies.
|
|
393
|
+
WebSocket endpoints handle authentication internally via message-based auth.
|
|
394
|
+
"""
|
|
395
|
+
ws_router = APIRouter()
|
|
396
|
+
|
|
397
|
+
@ws_router.websocket(
|
|
398
|
+
"/workflows/ws",
|
|
399
|
+
name="workflow_websocket",
|
|
400
|
+
)
|
|
401
|
+
async def workflow_websocket_endpoint(websocket: WebSocket):
|
|
402
|
+
"""WebSocket endpoint for receiving real-time workflow events"""
|
|
403
|
+
requires_auth = bool(settings.os_security_key)
|
|
404
|
+
await websocket_manager.connect(websocket, requires_auth=requires_auth)
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
while True:
|
|
408
|
+
data = await websocket.receive_text()
|
|
409
|
+
message = json.loads(data)
|
|
410
|
+
action = message.get("action")
|
|
411
|
+
|
|
412
|
+
# Handle authentication first
|
|
413
|
+
if action == "authenticate":
|
|
414
|
+
token = message.get("token")
|
|
415
|
+
if not token:
|
|
416
|
+
await websocket.send_text(json.dumps({"event": "auth_error", "error": "Token is required"}))
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
if validate_websocket_token(token, settings):
|
|
420
|
+
await websocket_manager.authenticate_websocket(websocket)
|
|
421
|
+
else:
|
|
422
|
+
await websocket.send_text(json.dumps({"event": "auth_error", "error": "Invalid token"}))
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
# Check authentication for all other actions (only when required)
|
|
426
|
+
elif requires_auth and not websocket_manager.is_authenticated(websocket):
|
|
427
|
+
await websocket.send_text(
|
|
428
|
+
json.dumps(
|
|
429
|
+
{
|
|
430
|
+
"event": "auth_required",
|
|
431
|
+
"error": "Authentication required. Send authenticate action with valid token.",
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
# Handle authenticated actions
|
|
438
|
+
elif action == "ping":
|
|
439
|
+
await websocket.send_text(json.dumps({"event": "pong"}))
|
|
440
|
+
|
|
441
|
+
elif action == "start-workflow":
|
|
442
|
+
# Handle workflow execution directly via WebSocket
|
|
443
|
+
await handle_workflow_via_websocket(websocket, message, os)
|
|
444
|
+
|
|
445
|
+
else:
|
|
446
|
+
await websocket.send_text(json.dumps({"event": "error", "error": f"Unknown action: {action}"}))
|
|
447
|
+
|
|
448
|
+
except Exception as e:
|
|
449
|
+
if "1012" not in str(e):
|
|
450
|
+
logger.error(f"WebSocket error: {e}")
|
|
451
|
+
finally:
|
|
452
|
+
# Clean up the websocket connection
|
|
453
|
+
await websocket_manager.disconnect_websocket(websocket)
|
|
454
|
+
|
|
455
|
+
return ws_router
|
|
456
|
+
|
|
457
|
+
|
|
337
458
|
def get_base_router(
|
|
338
459
|
os: "AgentOS",
|
|
339
460
|
settings: AgnoAPISettings = AgnoAPISettings(),
|
|
@@ -346,7 +467,6 @@ def get_base_router(
|
|
|
346
467
|
- Agent management and execution
|
|
347
468
|
- Team collaboration and coordination
|
|
348
469
|
- Workflow automation and orchestration
|
|
349
|
-
- Real-time WebSocket communications
|
|
350
470
|
|
|
351
471
|
All endpoints include detailed documentation, examples, and proper error handling.
|
|
352
472
|
"""
|
|
@@ -362,24 +482,6 @@ def get_base_router(
|
|
|
362
482
|
)
|
|
363
483
|
|
|
364
484
|
# -- Main Routes ---
|
|
365
|
-
|
|
366
|
-
@router.get(
|
|
367
|
-
"/health",
|
|
368
|
-
tags=["Core"],
|
|
369
|
-
operation_id="health_check",
|
|
370
|
-
summary="Health Check",
|
|
371
|
-
description="Check the health status of the AgentOS API. Returns a simple status indicator.",
|
|
372
|
-
response_model=HealthResponse,
|
|
373
|
-
responses={
|
|
374
|
-
200: {
|
|
375
|
-
"description": "API is healthy and operational",
|
|
376
|
-
"content": {"application/json": {"example": {"status": "ok"}}},
|
|
377
|
-
}
|
|
378
|
-
},
|
|
379
|
-
)
|
|
380
|
-
async def health_check() -> HealthResponse:
|
|
381
|
-
return HealthResponse(status="ok")
|
|
382
|
-
|
|
383
485
|
@router.get(
|
|
384
486
|
"/config",
|
|
385
487
|
response_model=ConfigResponse,
|
|
@@ -1228,35 +1330,6 @@ def get_base_router(
|
|
|
1228
1330
|
|
|
1229
1331
|
# -- Workflow routes ---
|
|
1230
1332
|
|
|
1231
|
-
@router.websocket(
|
|
1232
|
-
"/workflows/ws",
|
|
1233
|
-
name="workflow_websocket",
|
|
1234
|
-
)
|
|
1235
|
-
async def workflow_websocket_endpoint(websocket: WebSocket):
|
|
1236
|
-
"""WebSocket endpoint for receiving real-time workflow events"""
|
|
1237
|
-
await websocket_manager.connect(websocket)
|
|
1238
|
-
|
|
1239
|
-
try:
|
|
1240
|
-
while True:
|
|
1241
|
-
data = await websocket.receive_text()
|
|
1242
|
-
message = json.loads(data)
|
|
1243
|
-
action = message.get("action")
|
|
1244
|
-
|
|
1245
|
-
if action == "ping":
|
|
1246
|
-
await websocket.send_text(json.dumps({"event": "pong"}))
|
|
1247
|
-
|
|
1248
|
-
elif action == "start-workflow":
|
|
1249
|
-
# Handle workflow execution directly via WebSocket
|
|
1250
|
-
await handle_workflow_via_websocket(websocket, message, os)
|
|
1251
|
-
except Exception as e:
|
|
1252
|
-
if "1012" not in str(e):
|
|
1253
|
-
logger.error(f"WebSocket error: {e}")
|
|
1254
|
-
finally:
|
|
1255
|
-
# Clean up any run_ids associated with this websocket
|
|
1256
|
-
runs_to_remove = [run_id for run_id, ws in websocket_manager.active_connections.items() if ws == websocket]
|
|
1257
|
-
for run_id in runs_to_remove:
|
|
1258
|
-
await websocket_manager.disconnect_by_run_id(run_id)
|
|
1259
|
-
|
|
1260
1333
|
@router.get(
|
|
1261
1334
|
"/workflows",
|
|
1262
1335
|
response_model=List[WorkflowSummaryResponse],
|
agno/os/routers/evals/utils.py
CHANGED
|
@@ -35,7 +35,7 @@ async def run_accuracy_eval(
|
|
|
35
35
|
name=eval_run_input.name,
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
-
result = accuracy_eval.
|
|
38
|
+
result = await accuracy_eval.arun(print_results=False, print_summary=False)
|
|
39
39
|
if not result:
|
|
40
40
|
raise HTTPException(status_code=500, detail="Failed to run accuracy evaluation")
|
|
41
41
|
|
|
@@ -60,16 +60,16 @@ async def run_performance_eval(
|
|
|
60
60
|
"""Run a performance evaluation for the given agent or team"""
|
|
61
61
|
if agent:
|
|
62
62
|
|
|
63
|
-
def run_component(): # type: ignore
|
|
64
|
-
return agent.
|
|
63
|
+
async def run_component(): # type: ignore
|
|
64
|
+
return await agent.arun(eval_run_input.input)
|
|
65
65
|
|
|
66
66
|
model_id = agent.model.id if agent and agent.model else None
|
|
67
67
|
model_provider = agent.model.provider if agent and agent.model else None
|
|
68
68
|
|
|
69
69
|
elif team:
|
|
70
70
|
|
|
71
|
-
def run_component():
|
|
72
|
-
return team.
|
|
71
|
+
async def run_component():
|
|
72
|
+
return await team.arun(eval_run_input.input)
|
|
73
73
|
|
|
74
74
|
model_id = team.model.id if team and team.model else None
|
|
75
75
|
model_provider = team.model.provider if team and team.model else None
|
|
@@ -85,7 +85,7 @@ async def run_performance_eval(
|
|
|
85
85
|
model_id=model_id,
|
|
86
86
|
model_provider=model_provider,
|
|
87
87
|
)
|
|
88
|
-
result = performance_eval.
|
|
88
|
+
result = await performance_eval.arun(print_results=False, print_summary=False)
|
|
89
89
|
if not result:
|
|
90
90
|
raise HTTPException(status_code=500, detail="Failed to run performance evaluation")
|
|
91
91
|
|
|
@@ -119,7 +119,7 @@ async def run_reliability_eval(
|
|
|
119
119
|
raise HTTPException(status_code=400, detail="expected_tool_calls is required for reliability evaluations")
|
|
120
120
|
|
|
121
121
|
if agent:
|
|
122
|
-
agent_response = agent.
|
|
122
|
+
agent_response = await agent.arun(eval_run_input.input)
|
|
123
123
|
reliability_eval = ReliabilityEval(
|
|
124
124
|
db=db,
|
|
125
125
|
name=eval_run_input.name,
|
|
@@ -130,7 +130,7 @@ async def run_reliability_eval(
|
|
|
130
130
|
model_provider = agent.model.provider if agent and agent.model else None
|
|
131
131
|
|
|
132
132
|
elif team:
|
|
133
|
-
team_response = team.
|
|
133
|
+
team_response = await team.arun(eval_run_input.input)
|
|
134
134
|
reliability_eval = ReliabilityEval(
|
|
135
135
|
db=db,
|
|
136
136
|
name=eval_run_input.name,
|
|
@@ -140,7 +140,7 @@ async def run_reliability_eval(
|
|
|
140
140
|
model_id = team.model.id if team and team.model else None
|
|
141
141
|
model_provider = team.model.provider if team and team.model else None
|
|
142
142
|
|
|
143
|
-
result = reliability_eval.
|
|
143
|
+
result = await reliability_eval.arun(print_results=False)
|
|
144
144
|
if not result:
|
|
145
145
|
raise HTTPException(status_code=500, detail="Failed to run reliability evaluation")
|
|
146
146
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from agno.os.schema import HealthResponse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_health_router() -> APIRouter:
|
|
7
|
+
router = APIRouter(tags=["Health"])
|
|
8
|
+
|
|
9
|
+
@router.get(
|
|
10
|
+
"/health",
|
|
11
|
+
tags=["Core"],
|
|
12
|
+
operation_id="health_check",
|
|
13
|
+
summary="Health Check",
|
|
14
|
+
description="Check the health status of the AgentOS API. Returns a simple status indicator.",
|
|
15
|
+
response_model=HealthResponse,
|
|
16
|
+
responses={
|
|
17
|
+
200: {
|
|
18
|
+
"description": "API is healthy and operational",
|
|
19
|
+
"content": {"application/json": {"example": {"status": "ok"}}},
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
async def health_check() -> HealthResponse:
|
|
24
|
+
return HealthResponse(status="ok")
|
|
25
|
+
|
|
26
|
+
return router
|
|
@@ -2,10 +2,10 @@ import json
|
|
|
2
2
|
import logging
|
|
3
3
|
import math
|
|
4
4
|
from typing import Dict, List, Optional
|
|
5
|
-
from uuid import uuid4
|
|
6
5
|
|
|
7
6
|
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, Path, Query, UploadFile
|
|
8
7
|
|
|
8
|
+
from agno.db.utils import generate_deterministic_id
|
|
9
9
|
from agno.knowledge.content import Content, FileData
|
|
10
10
|
from agno.knowledge.knowledge import Knowledge
|
|
11
11
|
from agno.knowledge.reader import ReaderFactory
|
|
@@ -102,8 +102,7 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Knowledge]) -> AP
|
|
|
102
102
|
db_id: Optional[str] = Query(default=None, description="Database ID to use for content storage"),
|
|
103
103
|
):
|
|
104
104
|
knowledge = get_knowledge_instance_by_db_id(knowledge_instances, db_id)
|
|
105
|
-
|
|
106
|
-
log_info(f"Adding content: {name}, {description}, {url}, {metadata} with ID: {content_id}")
|
|
105
|
+
log_info(f"Adding content: {name}, {description}, {url}, {metadata}")
|
|
107
106
|
|
|
108
107
|
parsed_metadata = None
|
|
109
108
|
if metadata:
|
|
@@ -166,10 +165,14 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Knowledge]) -> AP
|
|
|
166
165
|
file_data=file_data,
|
|
167
166
|
size=file.size if file else None if text_content else None,
|
|
168
167
|
)
|
|
169
|
-
|
|
168
|
+
content_hash = knowledge._build_content_hash(content)
|
|
169
|
+
content.content_hash = content_hash
|
|
170
|
+
content.id = generate_deterministic_id(content_hash)
|
|
171
|
+
|
|
172
|
+
background_tasks.add_task(process_content, knowledge, content, reader_id, chunker)
|
|
170
173
|
|
|
171
174
|
response = ContentResponseSchema(
|
|
172
|
-
id=
|
|
175
|
+
id=content.id,
|
|
173
176
|
name=name,
|
|
174
177
|
description=description,
|
|
175
178
|
metadata=parsed_metadata,
|
|
@@ -802,15 +805,13 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Knowledge]) -> AP
|
|
|
802
805
|
|
|
803
806
|
async def process_content(
|
|
804
807
|
knowledge: Knowledge,
|
|
805
|
-
content_id: str,
|
|
806
808
|
content: Content,
|
|
807
809
|
reader_id: Optional[str] = None,
|
|
808
810
|
chunker: Optional[str] = None,
|
|
809
811
|
):
|
|
810
812
|
"""Background task to process the content"""
|
|
811
|
-
|
|
813
|
+
|
|
812
814
|
try:
|
|
813
|
-
content.id = content_id
|
|
814
815
|
if reader_id:
|
|
815
816
|
reader = None
|
|
816
817
|
if knowledge.readers and reader_id in knowledge.readers:
|
|
@@ -834,16 +835,15 @@ async def process_content(
|
|
|
834
835
|
|
|
835
836
|
log_debug(f"Using reader: {content.reader.__class__.__name__}")
|
|
836
837
|
await knowledge._load_content(content, upsert=False, skip_if_exists=True)
|
|
837
|
-
log_info(f"Content {
|
|
838
|
+
log_info(f"Content {content.id} processed successfully")
|
|
838
839
|
except Exception as e:
|
|
839
|
-
log_info(f"Error processing content
|
|
840
|
+
log_info(f"Error processing content: {e}")
|
|
840
841
|
# Mark content as failed in the contents DB
|
|
841
842
|
try:
|
|
842
843
|
from agno.knowledge.content import ContentStatus as KnowledgeContentStatus
|
|
843
844
|
|
|
844
845
|
content.status = KnowledgeContentStatus.FAILED
|
|
845
846
|
content.status_message = str(e)
|
|
846
|
-
content.id = content_id
|
|
847
847
|
knowledge.patch_content(content)
|
|
848
848
|
except Exception:
|
|
849
849
|
# Swallow any secondary errors to avoid crashing the background task
|
|
@@ -222,7 +222,9 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
222
222
|
db = get_db(dbs, db_id)
|
|
223
223
|
session = db.get_session(session_id=session_id, session_type=session_type)
|
|
224
224
|
if not session:
|
|
225
|
-
raise HTTPException(
|
|
225
|
+
raise HTTPException(
|
|
226
|
+
status_code=404, detail=f"{session_type.value.title()} Session with id '{session_id}' not found"
|
|
227
|
+
)
|
|
226
228
|
|
|
227
229
|
if session_type == SessionType.AGENT:
|
|
228
230
|
return AgentSessionDetailSchema.from_session(session) # type: ignore
|
|
@@ -233,7 +235,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
233
235
|
|
|
234
236
|
@router.get(
|
|
235
237
|
"/sessions/{session_id}/runs",
|
|
236
|
-
response_model=Union[
|
|
238
|
+
response_model=List[Union[RunSchema, TeamRunSchema, WorkflowRunSchema]],
|
|
237
239
|
status_code=200,
|
|
238
240
|
operation_id="get_session_runs",
|
|
239
241
|
summary="Get Session Runs",
|
|
@@ -251,7 +253,8 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
251
253
|
"summary": "Example completed run",
|
|
252
254
|
"value": {
|
|
253
255
|
"run_id": "fcdf50f0-7c32-4593-b2ef-68a558774340",
|
|
254
|
-
"
|
|
256
|
+
"parent_run_id": "80056af0-c7a5-4d69-b6a2-c3eba9f040e0",
|
|
257
|
+
"agent_id": "basic-agent",
|
|
255
258
|
"user_id": "",
|
|
256
259
|
"run_input": "Which tools do you have access to?",
|
|
257
260
|
"content": "I don't have access to external tools or the internet. However, I can assist you with a wide range of topics by providing information, answering questions, and offering suggestions based on the knowledge I've been trained on. If there's anything specific you need help with, feel free to ask!",
|
|
@@ -351,7 +354,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
351
354
|
default=SessionType.AGENT, description="Session type (agent, team, or workflow)", alias="type"
|
|
352
355
|
),
|
|
353
356
|
db_id: Optional[str] = Query(default=None, description="Database ID to query runs from"),
|
|
354
|
-
) -> Union[
|
|
357
|
+
) -> List[Union[RunSchema, TeamRunSchema, WorkflowRunSchema]]:
|
|
355
358
|
db = get_db(dbs, db_id)
|
|
356
359
|
session = db.get_session(session_id=session_id, session_type=session_type, deserialize=False)
|
|
357
360
|
if not session:
|
|
@@ -365,13 +368,26 @@ def attach_routes(router: APIRouter, dbs: dict[str, BaseDb]) -> APIRouter:
|
|
|
365
368
|
return [RunSchema.from_dict(run) for run in runs]
|
|
366
369
|
|
|
367
370
|
elif session_type == SessionType.TEAM:
|
|
368
|
-
|
|
371
|
+
run_responses: List[Union[RunSchema, TeamRunSchema, WorkflowRunSchema]] = []
|
|
372
|
+
for run in runs:
|
|
373
|
+
if run.get("agent_id") is not None:
|
|
374
|
+
run_responses.append(RunSchema.from_dict(run))
|
|
375
|
+
elif run.get("team_id") is not None:
|
|
376
|
+
run_responses.append(TeamRunSchema.from_dict(run))
|
|
377
|
+
return run_responses
|
|
369
378
|
|
|
370
379
|
elif session_type == SessionType.WORKFLOW:
|
|
371
|
-
|
|
372
|
-
|
|
380
|
+
run_responses: List[Union[RunSchema, TeamRunSchema, WorkflowRunSchema]] = [] # type: ignore
|
|
381
|
+
for run in runs:
|
|
382
|
+
if run.get("workflow_id") is not None:
|
|
383
|
+
run_responses.append(WorkflowRunSchema.from_dict(run))
|
|
384
|
+
elif run.get("team_id") is not None:
|
|
385
|
+
run_responses.append(TeamRunSchema.from_dict(run))
|
|
386
|
+
else:
|
|
387
|
+
run_responses.append(RunSchema.from_dict(run))
|
|
388
|
+
return run_responses
|
|
373
389
|
else:
|
|
374
|
-
|
|
390
|
+
raise HTTPException(status_code=400, detail=f"Invalid session type: {session_type}")
|
|
375
391
|
|
|
376
392
|
@router.delete(
|
|
377
393
|
"/sessions/{session_id}",
|