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.
Files changed (63) hide show
  1. agno/agent/agent.py +164 -87
  2. agno/db/dynamo/dynamo.py +8 -0
  3. agno/db/firestore/firestore.py +8 -1
  4. agno/db/gcs_json/gcs_json_db.py +9 -0
  5. agno/db/json/json_db.py +8 -0
  6. agno/db/mongo/mongo.py +10 -1
  7. agno/db/mysql/mysql.py +10 -0
  8. agno/db/postgres/postgres.py +16 -8
  9. agno/db/redis/redis.py +6 -0
  10. agno/db/singlestore/schemas.py +1 -1
  11. agno/db/singlestore/singlestore.py +8 -1
  12. agno/db/sqlite/sqlite.py +9 -1
  13. agno/db/utils.py +14 -0
  14. agno/knowledge/chunking/fixed.py +1 -1
  15. agno/knowledge/knowledge.py +91 -65
  16. agno/knowledge/reader/base.py +3 -0
  17. agno/knowledge/reader/csv_reader.py +1 -1
  18. agno/knowledge/reader/json_reader.py +1 -1
  19. agno/knowledge/reader/markdown_reader.py +5 -5
  20. agno/knowledge/reader/s3_reader.py +0 -12
  21. agno/knowledge/reader/text_reader.py +5 -5
  22. agno/models/base.py +2 -2
  23. agno/models/cerebras/cerebras.py +5 -3
  24. agno/models/cerebras/cerebras_openai.py +5 -3
  25. agno/models/google/gemini.py +33 -11
  26. agno/models/litellm/chat.py +1 -1
  27. agno/models/openai/chat.py +3 -0
  28. agno/models/openai/responses.py +81 -40
  29. agno/models/response.py +5 -0
  30. agno/models/siliconflow/__init__.py +5 -0
  31. agno/models/siliconflow/siliconflow.py +25 -0
  32. agno/os/app.py +4 -1
  33. agno/os/auth.py +24 -14
  34. agno/os/interfaces/slack/router.py +1 -1
  35. agno/os/interfaces/whatsapp/router.py +2 -0
  36. agno/os/router.py +187 -76
  37. agno/os/routers/evals/utils.py +9 -9
  38. agno/os/routers/health.py +26 -0
  39. agno/os/routers/knowledge/knowledge.py +11 -11
  40. agno/os/routers/session/session.py +24 -8
  41. agno/os/schema.py +8 -2
  42. agno/run/agent.py +5 -2
  43. agno/run/base.py +6 -3
  44. agno/run/team.py +11 -3
  45. agno/run/workflow.py +69 -12
  46. agno/session/team.py +1 -0
  47. agno/team/team.py +196 -93
  48. agno/tools/mcp.py +1 -0
  49. agno/tools/mem0.py +11 -17
  50. agno/tools/memory.py +419 -0
  51. agno/tools/workflow.py +279 -0
  52. agno/utils/audio.py +27 -0
  53. agno/utils/common.py +90 -1
  54. agno/utils/print_response/agent.py +6 -2
  55. agno/utils/streamlit.py +14 -8
  56. agno/vectordb/chroma/chromadb.py +8 -2
  57. agno/workflow/step.py +111 -13
  58. agno/workflow/workflow.py +16 -13
  59. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/METADATA +1 -1
  60. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/RECORD +63 -58
  61. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/WHEEL +0 -0
  62. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/licenses/LICENSE +0 -0
  63. {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.workflow import WorkflowErrorEvent
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 format_sse_event(json_data: str) -> str:
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
- json_data: JSON string containing the event data
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
- data = json.loads(json_data)
79
- event_type = data.get("event", "message")
97
+ event_type = event.event or "message"
80
98
 
81
- # Format as SSE: event: <event_type>\ndata: <json_data>\n\n
82
- return f"event: {event_type}\ndata: {json_data}\n\n"
83
- except (json.JSONDecodeError, KeyError):
84
- # Fallback to generic message event if parsing fails
85
- return f"event: message\ndata: {json_data}\n\n"
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
- # Send connection confirmation
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": "Connected to workflow events",
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.to_json())
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.to_json())
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.to_json())
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.to_json())
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.to_json())
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.to_json())
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.to_json())
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.to_json())
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
- logger.debug(f"Creating team run: {message} {session_id} {monitor} {user_id} {team_id} {files}")
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:
@@ -35,7 +35,7 @@ async def run_accuracy_eval(
35
35
  name=eval_run_input.name,
36
36
  )
37
37
 
38
- result = accuracy_eval.run(print_results=False, print_summary=False)
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.run(eval_run_input.input)
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.run(eval_run_input.input)
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.run(print_results=False, print_summary=False)
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.run(eval_run_input.input)
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.run(eval_run_input.input)
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.run(print_results=False)
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
- content_id = str(uuid4())
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
- background_tasks.add_task(process_content, knowledge, content_id, content, reader_id, chunker)
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=content_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
- log_info(f"Processing content {content_id}")
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 {content_id} processed successfully")
838
+ log_info(f"Content {content.id} processed successfully")
838
839
  except Exception as e:
839
- log_info(f"Error processing content {content_id}: {e}")
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