agno 2.0.2__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 +164 -87
- 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/chunking/fixed.py +1 -1
- agno/knowledge/knowledge.py +91 -65
- agno/knowledge/reader/base.py +3 -0
- agno/knowledge/reader/csv_reader.py +1 -1
- agno/knowledge/reader/json_reader.py +1 -1
- agno/knowledge/reader/markdown_reader.py +5 -5
- agno/knowledge/reader/s3_reader.py +0 -12
- agno/knowledge/reader/text_reader.py +5 -5
- agno/models/base.py +2 -2
- agno/models/cerebras/cerebras.py +5 -3
- agno/models/cerebras/cerebras_openai.py +5 -3
- agno/models/google/gemini.py +33 -11
- agno/models/litellm/chat.py +1 -1
- agno/models/openai/chat.py +3 -0
- agno/models/openai/responses.py +81 -40
- 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/interfaces/slack/router.py +1 -1
- agno/os/interfaces/whatsapp/router.py +2 -0
- agno/os/router.py +187 -76
- 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/agent.py +5 -2
- agno/run/base.py +6 -3
- agno/run/team.py +11 -3
- agno/run/workflow.py +69 -12
- agno/session/team.py +1 -0
- agno/team/team.py +196 -93
- agno/tools/mcp.py +1 -0
- agno/tools/mem0.py +11 -17
- agno/tools/memory.py +419 -0
- agno/tools/workflow.py +279 -0
- agno/utils/audio.py +27 -0
- agno/utils/common.py +90 -1
- agno/utils/print_response/agent.py +6 -2
- 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.2.dist-info → agno-2.0.4.dist-info}/METADATA +1 -1
- {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/RECORD +63 -58
- {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/WHEEL +0 -0
- {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/top_level.txt +0 -0
agno/os/router.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Optional, Union, cast
|
|
2
|
+
from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Dict, List, Optional, Union, cast
|
|
3
3
|
from uuid import uuid4
|
|
4
4
|
|
|
5
5
|
from fastapi import (
|
|
@@ -8,6 +8,7 @@ from fastapi import (
|
|
|
8
8
|
File,
|
|
9
9
|
Form,
|
|
10
10
|
HTTPException,
|
|
11
|
+
Request,
|
|
11
12
|
UploadFile,
|
|
12
13
|
WebSocket,
|
|
13
14
|
)
|
|
@@ -17,13 +18,12 @@ from pydantic import BaseModel
|
|
|
17
18
|
from agno.agent.agent import Agent
|
|
18
19
|
from agno.media import Audio, Image, Video
|
|
19
20
|
from agno.media import File as FileMedia
|
|
20
|
-
from agno.os.auth import get_authentication_dependency
|
|
21
|
+
from agno.os.auth import get_authentication_dependency, validate_websocket_token
|
|
21
22
|
from agno.os.schema import (
|
|
22
23
|
AgentResponse,
|
|
23
24
|
AgentSummaryResponse,
|
|
24
25
|
BadRequestResponse,
|
|
25
26
|
ConfigResponse,
|
|
26
|
-
HealthResponse,
|
|
27
27
|
InterfaceResponse,
|
|
28
28
|
InternalServerErrorResponse,
|
|
29
29
|
Model,
|
|
@@ -45,9 +45,10 @@ from agno.os.utils import (
|
|
|
45
45
|
process_image,
|
|
46
46
|
process_video,
|
|
47
47
|
)
|
|
48
|
-
from agno.run.agent import RunErrorEvent, RunOutput
|
|
48
|
+
from agno.run.agent import RunErrorEvent, RunOutput, RunOutputEvent
|
|
49
49
|
from agno.run.team import RunErrorEvent as TeamRunErrorEvent
|
|
50
|
-
from agno.run.
|
|
50
|
+
from agno.run.team import TeamRunOutputEvent
|
|
51
|
+
from agno.run.workflow import WorkflowErrorEvent, WorkflowRunOutput, WorkflowRunOutputEvent
|
|
51
52
|
from agno.team.team import Team
|
|
52
53
|
from agno.utils.log import log_debug, log_error, log_warning, logger
|
|
53
54
|
from agno.workflow.workflow import Workflow
|
|
@@ -56,11 +57,29 @@ if TYPE_CHECKING:
|
|
|
56
57
|
from agno.os.app import AgentOS
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def
|
|
60
|
+
async def _get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict[str, Any]:
|
|
61
|
+
"""Given a Request and an endpoint function, return a dictionary with all extra form data fields.
|
|
62
|
+
Args:
|
|
63
|
+
request: The FastAPI Request object
|
|
64
|
+
endpoint_func: The function exposing the endpoint that received the request
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
A dictionary of kwargs
|
|
68
|
+
"""
|
|
69
|
+
import inspect
|
|
70
|
+
|
|
71
|
+
form_data = await request.form()
|
|
72
|
+
sig = inspect.signature(endpoint_func)
|
|
73
|
+
known_fields = set(sig.parameters.keys())
|
|
74
|
+
kwargs = {key: value for key, value in form_data.items() if key not in known_fields}
|
|
75
|
+
return kwargs
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def format_sse_event(event: Union[RunOutputEvent, TeamRunOutputEvent, WorkflowRunOutputEvent]) -> str:
|
|
60
79
|
"""Parse JSON data into SSE-compliant format.
|
|
61
80
|
|
|
62
81
|
Args:
|
|
63
|
-
|
|
82
|
+
event_dict: Dictionary containing the event data
|
|
64
83
|
|
|
65
84
|
Returns:
|
|
66
85
|
SSE-formatted response:
|
|
@@ -75,20 +94,22 @@ def format_sse_event(json_data: str) -> str:
|
|
|
75
94
|
"""
|
|
76
95
|
try:
|
|
77
96
|
# Parse the JSON to extract the event type
|
|
78
|
-
|
|
79
|
-
event_type = data.get("event", "message")
|
|
97
|
+
event_type = event.event or "message"
|
|
80
98
|
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
99
|
+
# Serialize to valid JSON with double quotes and no newlines
|
|
100
|
+
clean_json = event.to_json(separators=(",", ":"), indent=None)
|
|
101
|
+
|
|
102
|
+
return f"event: {event_type}\ndata: {clean_json}\n\n"
|
|
103
|
+
except json.JSONDecodeError:
|
|
104
|
+
clean_json = event.to_json(separators=(",", ":"), indent=None)
|
|
105
|
+
return f"event: message\ndata: {clean_json}\n\n"
|
|
86
106
|
|
|
87
107
|
|
|
88
108
|
class WebSocketManager:
|
|
89
109
|
"""Manages WebSocket connections for workflow runs"""
|
|
90
110
|
|
|
91
111
|
active_connections: Dict[str, WebSocket] # {run_id: websocket}
|
|
112
|
+
authenticated_connections: Dict[WebSocket, bool] # {websocket: is_authenticated}
|
|
92
113
|
|
|
93
114
|
def __init__(
|
|
94
115
|
self,
|
|
@@ -96,22 +117,51 @@ class WebSocketManager:
|
|
|
96
117
|
):
|
|
97
118
|
# Store active connections: {run_id: websocket}
|
|
98
119
|
self.active_connections = active_connections or {}
|
|
120
|
+
# Track authentication state for each websocket
|
|
121
|
+
self.authenticated_connections = {}
|
|
99
122
|
|
|
100
|
-
async def connect(self, websocket: WebSocket):
|
|
123
|
+
async def connect(self, websocket: WebSocket, requires_auth: bool = True):
|
|
101
124
|
"""Accept WebSocket connection"""
|
|
102
125
|
await websocket.accept()
|
|
103
126
|
logger.debug("WebSocket connected")
|
|
104
127
|
|
|
105
|
-
#
|
|
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
|
|
106
132
|
await websocket.send_text(
|
|
107
133
|
json.dumps(
|
|
108
134
|
{
|
|
109
135
|
"event": "connected",
|
|
110
|
-
"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,
|
|
111
142
|
}
|
|
112
143
|
)
|
|
113
144
|
)
|
|
114
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
|
+
|
|
115
165
|
async def register_workflow_websocket(self, run_id: str, websocket: WebSocket):
|
|
116
166
|
"""Register a workflow run with its WebSocket connection"""
|
|
117
167
|
self.active_connections[run_id] = websocket
|
|
@@ -120,9 +170,26 @@ class WebSocketManager:
|
|
|
120
170
|
async def disconnect_by_run_id(self, run_id: str):
|
|
121
171
|
"""Remove WebSocket connection by run_id"""
|
|
122
172
|
if run_id in self.active_connections:
|
|
173
|
+
websocket = self.active_connections[run_id]
|
|
123
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]
|
|
124
178
|
logger.debug(f"WebSocket disconnected for run_id: {run_id}")
|
|
125
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
|
+
|
|
126
193
|
async def get_websocket_for_run(self, run_id: str) -> Optional[WebSocket]:
|
|
127
194
|
"""Get WebSocket connection for a workflow run"""
|
|
128
195
|
return self.active_connections.get(run_id)
|
|
@@ -143,6 +210,7 @@ async def agent_response_streamer(
|
|
|
143
210
|
audio: Optional[List[Audio]] = None,
|
|
144
211
|
videos: Optional[List[Video]] = None,
|
|
145
212
|
files: Optional[List[FileMedia]] = None,
|
|
213
|
+
**kwargs: Any,
|
|
146
214
|
) -> AsyncGenerator:
|
|
147
215
|
try:
|
|
148
216
|
run_response = agent.arun(
|
|
@@ -155,9 +223,10 @@ async def agent_response_streamer(
|
|
|
155
223
|
files=files,
|
|
156
224
|
stream=True,
|
|
157
225
|
stream_intermediate_steps=True,
|
|
226
|
+
**kwargs,
|
|
158
227
|
)
|
|
159
228
|
async for run_response_chunk in run_response:
|
|
160
|
-
yield format_sse_event(run_response_chunk
|
|
229
|
+
yield format_sse_event(run_response_chunk) # type: ignore
|
|
161
230
|
|
|
162
231
|
except Exception as e:
|
|
163
232
|
import traceback
|
|
@@ -166,7 +235,7 @@ async def agent_response_streamer(
|
|
|
166
235
|
error_response = RunErrorEvent(
|
|
167
236
|
content=str(e),
|
|
168
237
|
)
|
|
169
|
-
yield format_sse_event(error_response
|
|
238
|
+
yield format_sse_event(error_response)
|
|
170
239
|
|
|
171
240
|
|
|
172
241
|
async def agent_continue_response_streamer(
|
|
@@ -186,7 +255,7 @@ async def agent_continue_response_streamer(
|
|
|
186
255
|
stream_intermediate_steps=True,
|
|
187
256
|
)
|
|
188
257
|
async for run_response_chunk in continue_response:
|
|
189
|
-
yield format_sse_event(run_response_chunk
|
|
258
|
+
yield format_sse_event(run_response_chunk) # type: ignore
|
|
190
259
|
|
|
191
260
|
except Exception as e:
|
|
192
261
|
import traceback
|
|
@@ -195,7 +264,7 @@ async def agent_continue_response_streamer(
|
|
|
195
264
|
error_response = RunErrorEvent(
|
|
196
265
|
content=str(e),
|
|
197
266
|
)
|
|
198
|
-
yield format_sse_event(error_response
|
|
267
|
+
yield format_sse_event(error_response)
|
|
199
268
|
return
|
|
200
269
|
|
|
201
270
|
|
|
@@ -208,6 +277,7 @@ async def team_response_streamer(
|
|
|
208
277
|
audio: Optional[List[Audio]] = None,
|
|
209
278
|
videos: Optional[List[Video]] = None,
|
|
210
279
|
files: Optional[List[FileMedia]] = None,
|
|
280
|
+
**kwargs: Any,
|
|
211
281
|
) -> AsyncGenerator:
|
|
212
282
|
"""Run the given team asynchronously and yield its response"""
|
|
213
283
|
try:
|
|
@@ -221,9 +291,10 @@ async def team_response_streamer(
|
|
|
221
291
|
files=files,
|
|
222
292
|
stream=True,
|
|
223
293
|
stream_intermediate_steps=True,
|
|
294
|
+
**kwargs,
|
|
224
295
|
)
|
|
225
296
|
async for run_response_chunk in run_response:
|
|
226
|
-
yield format_sse_event(run_response_chunk
|
|
297
|
+
yield format_sse_event(run_response_chunk) # type: ignore
|
|
227
298
|
|
|
228
299
|
except Exception as e:
|
|
229
300
|
import traceback
|
|
@@ -232,7 +303,7 @@ async def team_response_streamer(
|
|
|
232
303
|
error_response = TeamRunErrorEvent(
|
|
233
304
|
content=str(e),
|
|
234
305
|
)
|
|
235
|
-
yield format_sse_event(error_response
|
|
306
|
+
yield format_sse_event(error_response)
|
|
236
307
|
return
|
|
237
308
|
|
|
238
309
|
|
|
@@ -263,7 +334,7 @@ async def handle_workflow_via_websocket(websocket: WebSocket, message: dict, os:
|
|
|
263
334
|
session_id = str(uuid4())
|
|
264
335
|
|
|
265
336
|
# Execute workflow in background with streaming
|
|
266
|
-
await workflow.arun(
|
|
337
|
+
workflow_result = await workflow.arun(
|
|
267
338
|
input=user_message,
|
|
268
339
|
session_id=session_id,
|
|
269
340
|
user_id=user_id,
|
|
@@ -273,6 +344,10 @@ async def handle_workflow_via_websocket(websocket: WebSocket, message: dict, os:
|
|
|
273
344
|
websocket=websocket,
|
|
274
345
|
)
|
|
275
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
|
+
|
|
276
351
|
except Exception as e:
|
|
277
352
|
logger.error(f"Error executing workflow via WebSocket: {e}")
|
|
278
353
|
await websocket.send_text(json.dumps({"event": "error", "error": str(e)}))
|
|
@@ -296,7 +371,7 @@ async def workflow_response_streamer(
|
|
|
296
371
|
)
|
|
297
372
|
|
|
298
373
|
async for run_response_chunk in run_response:
|
|
299
|
-
yield format_sse_event(run_response_chunk
|
|
374
|
+
yield format_sse_event(run_response_chunk) # type: ignore
|
|
300
375
|
|
|
301
376
|
except Exception as e:
|
|
302
377
|
import traceback
|
|
@@ -305,10 +380,81 @@ async def workflow_response_streamer(
|
|
|
305
380
|
error_response = WorkflowErrorEvent(
|
|
306
381
|
error=str(e),
|
|
307
382
|
)
|
|
308
|
-
yield format_sse_event(error_response
|
|
383
|
+
yield format_sse_event(error_response)
|
|
309
384
|
return
|
|
310
385
|
|
|
311
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
|
+
|
|
312
458
|
def get_base_router(
|
|
313
459
|
os: "AgentOS",
|
|
314
460
|
settings: AgnoAPISettings = AgnoAPISettings(),
|
|
@@ -321,7 +467,6 @@ def get_base_router(
|
|
|
321
467
|
- Agent management and execution
|
|
322
468
|
- Team collaboration and coordination
|
|
323
469
|
- Workflow automation and orchestration
|
|
324
|
-
- Real-time WebSocket communications
|
|
325
470
|
|
|
326
471
|
All endpoints include detailed documentation, examples, and proper error handling.
|
|
327
472
|
"""
|
|
@@ -337,24 +482,6 @@ def get_base_router(
|
|
|
337
482
|
)
|
|
338
483
|
|
|
339
484
|
# -- Main Routes ---
|
|
340
|
-
|
|
341
|
-
@router.get(
|
|
342
|
-
"/health",
|
|
343
|
-
tags=["Core"],
|
|
344
|
-
operation_id="health_check",
|
|
345
|
-
summary="Health Check",
|
|
346
|
-
description="Check the health status of the AgentOS API. Returns a simple status indicator.",
|
|
347
|
-
response_model=HealthResponse,
|
|
348
|
-
responses={
|
|
349
|
-
200: {
|
|
350
|
-
"description": "API is healthy and operational",
|
|
351
|
-
"content": {"application/json": {"example": {"status": "ok"}}},
|
|
352
|
-
}
|
|
353
|
-
},
|
|
354
|
-
)
|
|
355
|
-
async def health_check() -> HealthResponse:
|
|
356
|
-
return HealthResponse(status="ok")
|
|
357
|
-
|
|
358
485
|
@router.get(
|
|
359
486
|
"/config",
|
|
360
487
|
response_model=ConfigResponse,
|
|
@@ -538,12 +665,15 @@ def get_base_router(
|
|
|
538
665
|
)
|
|
539
666
|
async def create_agent_run(
|
|
540
667
|
agent_id: str,
|
|
668
|
+
request: Request,
|
|
541
669
|
message: str = Form(...),
|
|
542
670
|
stream: bool = Form(False),
|
|
543
671
|
session_id: Optional[str] = Form(None),
|
|
544
672
|
user_id: Optional[str] = Form(None),
|
|
545
673
|
files: Optional[List[UploadFile]] = File(None),
|
|
546
674
|
):
|
|
675
|
+
kwargs = await _get_request_kwargs(request, create_agent_run)
|
|
676
|
+
|
|
547
677
|
agent = get_agent_by_id(agent_id, os.agents)
|
|
548
678
|
if agent is None:
|
|
549
679
|
raise HTTPException(status_code=404, detail="Agent not found")
|
|
@@ -620,6 +750,7 @@ def get_base_router(
|
|
|
620
750
|
audio=base64_audios if base64_audios else None,
|
|
621
751
|
videos=base64_videos if base64_videos else None,
|
|
622
752
|
files=input_files if input_files else None,
|
|
753
|
+
**kwargs,
|
|
623
754
|
),
|
|
624
755
|
media_type="text/event-stream",
|
|
625
756
|
)
|
|
@@ -635,6 +766,7 @@ def get_base_router(
|
|
|
635
766
|
videos=base64_videos if base64_videos else None,
|
|
636
767
|
files=input_files if input_files else None,
|
|
637
768
|
stream=False,
|
|
769
|
+
**kwargs,
|
|
638
770
|
),
|
|
639
771
|
)
|
|
640
772
|
return run_response.to_dict()
|
|
@@ -880,6 +1012,7 @@ def get_base_router(
|
|
|
880
1012
|
)
|
|
881
1013
|
async def create_team_run(
|
|
882
1014
|
team_id: str,
|
|
1015
|
+
request: Request,
|
|
883
1016
|
message: str = Form(...),
|
|
884
1017
|
stream: bool = Form(True),
|
|
885
1018
|
monitor: bool = Form(True),
|
|
@@ -887,7 +1020,10 @@ def get_base_router(
|
|
|
887
1020
|
user_id: Optional[str] = Form(None),
|
|
888
1021
|
files: Optional[List[UploadFile]] = File(None),
|
|
889
1022
|
):
|
|
890
|
-
|
|
1023
|
+
kwargs = await _get_request_kwargs(request, create_team_run)
|
|
1024
|
+
|
|
1025
|
+
logger.debug(f"Creating team run: {message=} {session_id=} {monitor=} {user_id=} {team_id=} {files=} {kwargs=}")
|
|
1026
|
+
|
|
891
1027
|
team = get_team_by_id(team_id, os.teams)
|
|
892
1028
|
if team is None:
|
|
893
1029
|
raise HTTPException(status_code=404, detail="Team not found")
|
|
@@ -962,6 +1098,7 @@ def get_base_router(
|
|
|
962
1098
|
audio=base64_audios if base64_audios else None,
|
|
963
1099
|
videos=base64_videos if base64_videos else None,
|
|
964
1100
|
files=document_files if document_files else None,
|
|
1101
|
+
**kwargs,
|
|
965
1102
|
),
|
|
966
1103
|
media_type="text/event-stream",
|
|
967
1104
|
)
|
|
@@ -975,6 +1112,7 @@ def get_base_router(
|
|
|
975
1112
|
videos=base64_videos if base64_videos else None,
|
|
976
1113
|
files=document_files if document_files else None,
|
|
977
1114
|
stream=False,
|
|
1115
|
+
**kwargs,
|
|
978
1116
|
)
|
|
979
1117
|
return run_response.to_dict()
|
|
980
1118
|
|
|
@@ -1192,35 +1330,6 @@ def get_base_router(
|
|
|
1192
1330
|
|
|
1193
1331
|
# -- Workflow routes ---
|
|
1194
1332
|
|
|
1195
|
-
@router.websocket(
|
|
1196
|
-
"/workflows/ws",
|
|
1197
|
-
name="workflow_websocket",
|
|
1198
|
-
)
|
|
1199
|
-
async def workflow_websocket_endpoint(websocket: WebSocket):
|
|
1200
|
-
"""WebSocket endpoint for receiving real-time workflow events"""
|
|
1201
|
-
await websocket_manager.connect(websocket)
|
|
1202
|
-
|
|
1203
|
-
try:
|
|
1204
|
-
while True:
|
|
1205
|
-
data = await websocket.receive_text()
|
|
1206
|
-
message = json.loads(data)
|
|
1207
|
-
action = message.get("action")
|
|
1208
|
-
|
|
1209
|
-
if action == "ping":
|
|
1210
|
-
await websocket.send_text(json.dumps({"event": "pong"}))
|
|
1211
|
-
|
|
1212
|
-
elif action == "start-workflow":
|
|
1213
|
-
# Handle workflow execution directly via WebSocket
|
|
1214
|
-
await handle_workflow_via_websocket(websocket, message, os)
|
|
1215
|
-
except Exception as e:
|
|
1216
|
-
if "1012" not in str(e):
|
|
1217
|
-
logger.error(f"WebSocket error: {e}")
|
|
1218
|
-
finally:
|
|
1219
|
-
# Clean up any run_ids associated with this websocket
|
|
1220
|
-
runs_to_remove = [run_id for run_id, ws in websocket_manager.active_connections.items() if ws == websocket]
|
|
1221
|
-
for run_id in runs_to_remove:
|
|
1222
|
-
await websocket_manager.disconnect_by_run_id(run_id)
|
|
1223
|
-
|
|
1224
1333
|
@router.get(
|
|
1225
1334
|
"/workflows",
|
|
1226
1335
|
response_model=List[WorkflowSummaryResponse],
|
|
@@ -1328,12 +1437,14 @@ def get_base_router(
|
|
|
1328
1437
|
)
|
|
1329
1438
|
async def create_workflow_run(
|
|
1330
1439
|
workflow_id: str,
|
|
1440
|
+
request: Request,
|
|
1331
1441
|
message: str = Form(...),
|
|
1332
1442
|
stream: bool = Form(True),
|
|
1333
1443
|
session_id: Optional[str] = Form(None),
|
|
1334
1444
|
user_id: Optional[str] = Form(None),
|
|
1335
|
-
**kwargs: Any,
|
|
1336
1445
|
):
|
|
1446
|
+
kwargs = await _get_request_kwargs(request, create_workflow_run)
|
|
1447
|
+
|
|
1337
1448
|
# Retrieve the workflow by ID
|
|
1338
1449
|
workflow = get_workflow_by_id(workflow_id, os.workflows)
|
|
1339
1450
|
if workflow is None:
|
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
|