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.
Files changed (43) hide show
  1. agno/agent/agent.py +162 -86
  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/knowledge.py +91 -65
  15. agno/models/base.py +2 -2
  16. agno/models/openai/chat.py +3 -0
  17. agno/models/openai/responses.py +6 -0
  18. agno/models/response.py +5 -0
  19. agno/models/siliconflow/__init__.py +5 -0
  20. agno/models/siliconflow/siliconflow.py +25 -0
  21. agno/os/app.py +4 -1
  22. agno/os/auth.py +24 -14
  23. agno/os/router.py +128 -55
  24. agno/os/routers/evals/utils.py +9 -9
  25. agno/os/routers/health.py +26 -0
  26. agno/os/routers/knowledge/knowledge.py +11 -11
  27. agno/os/routers/session/session.py +24 -8
  28. agno/os/schema.py +8 -2
  29. agno/run/workflow.py +64 -10
  30. agno/session/team.py +1 -0
  31. agno/team/team.py +192 -92
  32. agno/tools/mem0.py +11 -17
  33. agno/tools/memory.py +34 -6
  34. agno/utils/common.py +90 -1
  35. agno/utils/streamlit.py +14 -8
  36. agno/vectordb/chroma/chromadb.py +8 -2
  37. agno/workflow/step.py +111 -13
  38. agno/workflow/workflow.py +16 -13
  39. {agno-2.0.3.dist-info → agno-2.0.4.dist-info}/METADATA +1 -1
  40. {agno-2.0.3.dist-info → agno-2.0.4.dist-info}/RECORD +43 -40
  41. {agno-2.0.3.dist-info → agno-2.0.4.dist-info}/WHEEL +0 -0
  42. {agno-2.0.3.dist-info → agno-2.0.4.dist-info}/licenses/LICENSE +0 -0
  43. {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,5 @@
1
+ from agno.models.siliconflow.siliconflow import Siliconflow
2
+
3
+ __all__ = [
4
+ "Siliconflow",
5
+ ]
@@ -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 typing import Optional
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(authorization: Optional[str] = Header(None)) -> bool:
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 authorization:
27
+ if not credentials:
30
28
  raise HTTPException(status_code=401, detail="Authorization header required")
31
29
 
32
- # Check if the authorization header starts with "Bearer "
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
- # 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
127
132
  await websocket.send_text(
128
133
  json.dumps(
129
134
  {
130
135
  "event": "connected",
131
- "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,
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],
@@ -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
@@ -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(status_code=404, detail=f"Session with id '{session_id}' not found")
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[List[RunSchema], List[TeamRunSchema], List[WorkflowRunSchema]],
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
- "agent_session_id": "80056af0-c7a5-4d69-b6a2-c3eba9f040e0",
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[List[RunSchema], List[TeamRunSchema], List[WorkflowRunSchema]]:
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
- return [TeamRunSchema.from_dict(run) for run in runs]
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
- return [WorkflowRunSchema.from_dict(run) for run in runs]
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
- return [RunSchema.from_dict(run) for run in runs]
390
+ raise HTTPException(status_code=400, detail=f"Invalid session type: {session_type}")
375
391
 
376
392
  @router.delete(
377
393
  "/sessions/{session_id}",