agno 2.3.12__py3-none-any.whl → 2.3.14__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 (55) hide show
  1. agno/agent/agent.py +1125 -1401
  2. agno/eval/__init__.py +21 -8
  3. agno/knowledge/embedder/azure_openai.py +0 -1
  4. agno/knowledge/embedder/google.py +1 -1
  5. agno/models/anthropic/claude.py +4 -1
  6. agno/models/azure/openai_chat.py +11 -5
  7. agno/models/base.py +8 -4
  8. agno/models/openai/chat.py +0 -2
  9. agno/models/openai/responses.py +2 -2
  10. agno/os/app.py +112 -5
  11. agno/os/auth.py +190 -3
  12. agno/os/config.py +9 -0
  13. agno/os/interfaces/a2a/router.py +619 -9
  14. agno/os/interfaces/a2a/utils.py +31 -32
  15. agno/os/middleware/__init__.py +2 -0
  16. agno/os/middleware/jwt.py +670 -108
  17. agno/os/router.py +0 -1
  18. agno/os/routers/agents/router.py +22 -4
  19. agno/os/routers/agents/schema.py +14 -1
  20. agno/os/routers/teams/router.py +20 -4
  21. agno/os/routers/teams/schema.py +14 -1
  22. agno/os/routers/workflows/router.py +88 -9
  23. agno/os/scopes.py +469 -0
  24. agno/os/utils.py +86 -53
  25. agno/reasoning/anthropic.py +85 -1
  26. agno/reasoning/azure_ai_foundry.py +93 -1
  27. agno/reasoning/deepseek.py +91 -1
  28. agno/reasoning/gemini.py +81 -1
  29. agno/reasoning/groq.py +103 -1
  30. agno/reasoning/manager.py +1244 -0
  31. agno/reasoning/ollama.py +93 -1
  32. agno/reasoning/openai.py +113 -1
  33. agno/reasoning/vertexai.py +85 -1
  34. agno/run/agent.py +11 -0
  35. agno/run/base.py +1 -1
  36. agno/run/team.py +11 -0
  37. agno/session/team.py +0 -3
  38. agno/team/team.py +1204 -1452
  39. agno/tools/postgres.py +1 -1
  40. agno/utils/cryptography.py +22 -0
  41. agno/utils/events.py +69 -2
  42. agno/utils/hooks.py +4 -10
  43. agno/utils/print_response/agent.py +52 -2
  44. agno/utils/print_response/team.py +141 -10
  45. agno/utils/prompts.py +8 -6
  46. agno/utils/string.py +46 -0
  47. agno/utils/team.py +1 -1
  48. agno/vectordb/chroma/chromadb.py +1 -0
  49. agno/vectordb/milvus/milvus.py +32 -3
  50. agno/vectordb/redis/redisdb.py +16 -2
  51. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/METADATA +3 -2
  52. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/RECORD +55 -52
  53. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/WHEEL +0 -0
  54. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/licenses/LICENSE +0 -0
  55. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/top_level.txt +0 -0
agno/os/router.py CHANGED
@@ -208,7 +208,6 @@ def get_base_router(
208
208
  return list(unique_models.values())
209
209
 
210
210
  # -- Database Migration routes ---
211
-
212
211
  @router.post(
213
212
  "/databases/{db_id}/migrate",
214
213
  tags=["Database"],
@@ -18,7 +18,7 @@ from agno.agent.agent import Agent
18
18
  from agno.exceptions import InputCheckError, OutputCheckError
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, require_resource_access
22
22
  from agno.os.routers.agents.schema import AgentResponse
23
23
  from agno.os.schema import (
24
24
  BadRequestResponse,
@@ -187,6 +187,7 @@ def get_agent_router(
187
187
  400: {"description": "Invalid request or unsupported file type", "model": BadRequestResponse},
188
188
  404: {"description": "Agent not found", "model": NotFoundResponse},
189
189
  },
190
+ dependencies=[Depends(require_resource_access("agents", "run", "agent_id"))],
190
191
  )
191
192
  async def create_agent_run(
192
193
  agent_id: str,
@@ -372,6 +373,7 @@ def get_agent_router(
372
373
  404: {"description": "Agent not found", "model": NotFoundResponse},
373
374
  500: {"description": "Failed to cancel run", "model": InternalServerErrorResponse},
374
375
  },
376
+ dependencies=[Depends(require_resource_access("agents", "run", "agent_id"))],
375
377
  )
376
378
  async def cancel_agent_run(
377
379
  agent_id: str,
@@ -412,6 +414,7 @@ def get_agent_router(
412
414
  400: {"description": "Invalid JSON in tools field or invalid tool structure", "model": BadRequestResponse},
413
415
  404: {"description": "Agent not found", "model": NotFoundResponse},
414
416
  },
417
+ dependencies=[Depends(require_resource_access("agents", "run", "agent_id"))],
415
418
  )
416
419
  async def continue_agent_run(
417
420
  agent_id: str,
@@ -521,13 +524,27 @@ def get_agent_router(
521
524
  }
522
525
  },
523
526
  )
524
- async def get_agents() -> List[AgentResponse]:
527
+ async def get_agents(request: Request) -> List[AgentResponse]:
525
528
  """Return the list of all Agents present in the contextual OS"""
526
529
  if os.agents is None:
527
530
  return []
528
531
 
532
+ # Filter agents based on user's scopes (only if authorization is enabled)
533
+ if getattr(request.state, "authorization_enabled", False):
534
+ from agno.os.auth import filter_resources_by_access, get_accessible_resources
535
+
536
+ # Check if user has any agent scopes at all
537
+ accessible_ids = get_accessible_resources(request, "agents")
538
+ if not accessible_ids:
539
+ raise HTTPException(status_code=403, detail="Insufficient permissions")
540
+
541
+ # Limit results based on the user's access/scopes
542
+ accessible_agents = filter_resources_by_access(request, os.agents, "agents")
543
+ else:
544
+ accessible_agents = os.agents
545
+
529
546
  agents = []
530
- for agent in os.agents:
547
+ for agent in accessible_agents:
531
548
  agent_response = await AgentResponse.from_agent(agent=agent)
532
549
  agents.append(agent_response)
533
550
 
@@ -570,8 +587,9 @@ def get_agent_router(
570
587
  },
571
588
  404: {"description": "Agent not found", "model": NotFoundResponse},
572
589
  },
590
+ dependencies=[Depends(require_resource_access("agents", "read", "agent_id"))],
573
591
  )
574
- async def get_agent(agent_id: str) -> AgentResponse:
592
+ async def get_agent(agent_id: str, request: Request) -> AgentResponse:
575
593
  agent = get_agent_by_id(agent_id, os.agents)
576
594
  if agent is None:
577
595
  raise HTTPException(status_code=404, detail="Agent not found")
@@ -215,11 +215,24 @@ class AgentResponse(BaseModel):
215
215
  "build_user_context": agent.build_user_context,
216
216
  }
217
217
 
218
+ # Handle output_schema name for both Pydantic models and JSON schemas
219
+ output_schema_name = None
220
+ if agent.output_schema is not None:
221
+ if isinstance(agent.output_schema, dict):
222
+ if "json_schema" in agent.output_schema:
223
+ output_schema_name = agent.output_schema["json_schema"].get("name", "JSONSchema")
224
+ elif "schema" in agent.output_schema and isinstance(agent.output_schema["schema"], dict):
225
+ output_schema_name = agent.output_schema["schema"].get("title", "JSONSchema")
226
+ else:
227
+ output_schema_name = agent.output_schema.get("title", "JSONSchema")
228
+ elif hasattr(agent.output_schema, "__name__"):
229
+ output_schema_name = agent.output_schema.__name__
230
+
218
231
  response_settings_info: Dict[str, Any] = {
219
232
  "retries": agent.retries,
220
233
  "delay_between_retries": agent.delay_between_retries,
221
234
  "exponential_backoff": agent.exponential_backoff,
222
- "output_schema_name": agent.output_schema.__name__ if agent.output_schema else None,
235
+ "output_schema_name": output_schema_name,
223
236
  "parser_model_prompt": agent.parser_model_prompt,
224
237
  "parse_response": agent.parse_response,
225
238
  "structured_outputs": agent.structured_outputs,
@@ -16,7 +16,7 @@ from fastapi.responses import JSONResponse, StreamingResponse
16
16
  from agno.exceptions import InputCheckError, OutputCheckError
17
17
  from agno.media import Audio, Image, Video
18
18
  from agno.media import File as FileMedia
19
- from agno.os.auth import get_authentication_dependency
19
+ from agno.os.auth import get_authentication_dependency, require_resource_access
20
20
  from agno.os.routers.teams.schema import TeamResponse
21
21
  from agno.os.schema import (
22
22
  BadRequestResponse,
@@ -142,6 +142,7 @@ def get_team_router(
142
142
  400: {"description": "Invalid request or unsupported file type", "model": BadRequestResponse},
143
143
  404: {"description": "Team not found", "model": NotFoundResponse},
144
144
  },
145
+ dependencies=[Depends(require_resource_access("teams", "run", "team_id"))],
145
146
  )
146
147
  async def create_team_run(
147
148
  team_id: str,
@@ -295,6 +296,7 @@ def get_team_router(
295
296
  404: {"description": "Team not found", "model": NotFoundResponse},
296
297
  500: {"description": "Failed to cancel team run", "model": InternalServerErrorResponse},
297
298
  },
299
+ dependencies=[Depends(require_resource_access("teams", "run", "team_id"))],
298
300
  )
299
301
  async def cancel_team_run(
300
302
  team_id: str,
@@ -390,13 +392,26 @@ def get_team_router(
390
392
  }
391
393
  },
392
394
  )
393
- async def get_teams() -> List[TeamResponse]:
395
+ async def get_teams(request: Request) -> List[TeamResponse]:
394
396
  """Return the list of all Teams present in the contextual OS"""
395
397
  if os.teams is None:
396
398
  return []
397
399
 
400
+ # Filter teams based on user's scopes (only if authorization is enabled)
401
+ if getattr(request.state, "authorization_enabled", False):
402
+ from agno.os.auth import filter_resources_by_access, get_accessible_resources
403
+
404
+ # Check if user has any team scopes at all
405
+ accessible_ids = get_accessible_resources(request, "teams")
406
+ if not accessible_ids:
407
+ raise HTTPException(status_code=403, detail="Insufficient permissions")
408
+
409
+ accessible_teams = filter_resources_by_access(request, os.teams, "teams")
410
+ else:
411
+ accessible_teams = os.teams
412
+
398
413
  teams = []
399
- for team in os.teams:
414
+ for team in accessible_teams:
400
415
  team_response = await TeamResponse.from_team(team=team)
401
416
  teams.append(team_response)
402
417
 
@@ -485,8 +500,9 @@ def get_team_router(
485
500
  },
486
501
  404: {"description": "Team not found", "model": NotFoundResponse},
487
502
  },
503
+ dependencies=[Depends(require_resource_access("teams", "read", "team_id"))],
488
504
  )
489
- async def get_team(team_id: str) -> TeamResponse:
505
+ async def get_team(team_id: str, request: Request) -> TeamResponse:
490
506
  team = get_team_by_id(team_id, os.teams)
491
507
  if team is None:
492
508
  raise HTTPException(status_code=404, detail="Team not found")
@@ -197,8 +197,21 @@ class TeamResponse(BaseModel):
197
197
  "resolve_in_context": team.resolve_in_context,
198
198
  }
199
199
 
200
+ # Handle output_schema name for both Pydantic models and JSON schemas
201
+ output_schema_name = None
202
+ if team.output_schema is not None:
203
+ if isinstance(team.output_schema, dict):
204
+ if "json_schema" in team.output_schema:
205
+ output_schema_name = team.output_schema["json_schema"].get("name", "JSONSchema")
206
+ elif "schema" in team.output_schema and isinstance(team.output_schema["schema"], dict):
207
+ output_schema_name = team.output_schema["schema"].get("title", "JSONSchema")
208
+ else:
209
+ output_schema_name = team.output_schema.get("title", "JSONSchema")
210
+ elif hasattr(team.output_schema, "__name__"):
211
+ output_schema_name = team.output_schema.__name__
212
+
200
213
  response_settings_info: Dict[str, Any] = {
201
- "output_schema_name": team.output_schema.__name__ if team.output_schema else None,
214
+ "output_schema_name": output_schema_name,
202
215
  "parser_model_prompt": team.parser_model_prompt,
203
216
  "parse_response": team.parse_response,
204
217
  "use_json_mode": team.use_json_mode,
@@ -15,7 +15,7 @@ from fastapi.responses import JSONResponse, StreamingResponse
15
15
  from pydantic import BaseModel
16
16
 
17
17
  from agno.exceptions import InputCheckError, OutputCheckError
18
- from agno.os.auth import get_authentication_dependency, validate_websocket_token
18
+ from agno.os.auth import get_authentication_dependency, require_resource_access, validate_websocket_token
19
19
  from agno.os.routers.workflows.schema import WorkflowResponse
20
20
  from agno.os.schema import (
21
21
  BadRequestResponse,
@@ -252,8 +252,21 @@ def get_websocket_router(
252
252
  settings: AgnoAPISettings = AgnoAPISettings(),
253
253
  ) -> APIRouter:
254
254
  """
255
- Create WebSocket router without HTTP authentication dependencies.
255
+ Create WebSocket router with support for both legacy (os_security_key) and JWT authentication.
256
+
256
257
  WebSocket endpoints handle authentication internally via message-based auth.
258
+ Authentication methods (in order of precedence):
259
+ 1. JWT tokens - if JWTMiddleware is configured (via app.state.jwt_middleware)
260
+ 2. Legacy bearer token - if settings.os_security_key is set
261
+ 3. No authentication - if neither is configured
262
+
263
+ The JWT middleware instance is accessed from app.state.jwt_middleware, which is set
264
+ by AgentOS when authorization is enabled. This allows reusing the same validation
265
+ logic and loaded keys as the HTTP middleware.
266
+
267
+ Args:
268
+ os: The AgentOS instance
269
+ settings: API settings (includes os_security_key for legacy auth)
257
270
  """
258
271
  ws_router = APIRouter()
259
272
 
@@ -263,9 +276,18 @@ def get_websocket_router(
263
276
  )
264
277
  async def workflow_websocket_endpoint(websocket: WebSocket):
265
278
  """WebSocket endpoint for receiving real-time workflow events"""
266
- requires_auth = bool(settings.os_security_key)
279
+ # Check if JWT validator is configured (set by AgentOS when authorization=True)
280
+ jwt_validator = getattr(websocket.app.state, "jwt_validator", None)
281
+ jwt_auth_enabled = jwt_validator is not None
282
+
283
+ # Determine auth requirements - JWT takes precedence over legacy
284
+ requires_auth = jwt_auth_enabled or bool(settings.os_security_key)
285
+
267
286
  await websocket_manager.connect(websocket, requires_auth=requires_auth)
268
287
 
288
+ # Store user context from JWT auth
289
+ websocket_user_context: Dict[str, Any] = {}
290
+
269
291
  try:
270
292
  while True:
271
293
  data = await websocket.receive_text()
@@ -279,19 +301,56 @@ def get_websocket_router(
279
301
  await websocket.send_text(json.dumps({"event": "auth_error", "error": "Token is required"}))
280
302
  continue
281
303
 
282
- if validate_websocket_token(token, settings):
304
+ if jwt_auth_enabled and jwt_validator:
305
+ # Use JWT validator for token validation
306
+ try:
307
+ payload = jwt_validator.validate_token(token)
308
+ claims = jwt_validator.extract_claims(payload)
309
+ await websocket_manager.authenticate_websocket(websocket)
310
+
311
+ # Store user context from JWT
312
+ websocket_user_context["user_id"] = claims["user_id"]
313
+ websocket_user_context["scopes"] = claims["scopes"]
314
+ websocket_user_context["payload"] = payload
315
+
316
+ # Include user info in auth success message
317
+ await websocket.send_text(
318
+ json.dumps(
319
+ {
320
+ "event": "authenticated",
321
+ "message": "JWT authentication successful.",
322
+ "user_id": claims["user_id"],
323
+ }
324
+ )
325
+ )
326
+ except Exception as e:
327
+ error_msg = str(e) if str(e) else "Invalid token"
328
+ error_type = "expired" if "expired" in error_msg.lower() else "invalid_token"
329
+ await websocket.send_text(
330
+ json.dumps(
331
+ {
332
+ "event": "auth_error",
333
+ "error": error_msg,
334
+ "error_type": error_type,
335
+ }
336
+ )
337
+ )
338
+ continue
339
+ elif validate_websocket_token(token, settings):
340
+ # Legacy os_security_key authentication
283
341
  await websocket_manager.authenticate_websocket(websocket)
284
342
  else:
285
343
  await websocket.send_text(json.dumps({"event": "auth_error", "error": "Invalid token"}))
286
- continue
344
+ continue
287
345
 
288
346
  # Check authentication for all other actions (only when required)
289
347
  elif requires_auth and not websocket_manager.is_authenticated(websocket):
348
+ auth_type = "JWT" if jwt_auth_enabled else "bearer token"
290
349
  await websocket.send_text(
291
350
  json.dumps(
292
351
  {
293
352
  "event": "auth_required",
294
- "error": "Authentication required. Send authenticate action with valid token.",
353
+ "error": f"Authentication required. Send authenticate action with valid {auth_type}.",
295
354
  }
296
355
  )
297
356
  )
@@ -302,6 +361,10 @@ def get_websocket_router(
302
361
  await websocket.send_text(json.dumps({"event": "pong"}))
303
362
 
304
363
  elif action == "start-workflow":
364
+ # Add user context to message if available from JWT auth
365
+ if websocket_user_context:
366
+ if "user_id" not in message and websocket_user_context.get("user_id"):
367
+ message["user_id"] = websocket_user_context["user_id"]
305
368
  # Handle workflow execution directly via WebSocket
306
369
  await handle_workflow_via_websocket(websocket, message, os)
307
370
 
@@ -367,11 +430,24 @@ def get_workflow_router(
367
430
  }
368
431
  },
369
432
  )
370
- async def get_workflows() -> List[WorkflowSummaryResponse]:
433
+ async def get_workflows(request: Request) -> List[WorkflowSummaryResponse]:
371
434
  if os.workflows is None:
372
435
  return []
373
436
 
374
- return [WorkflowSummaryResponse.from_workflow(workflow) for workflow in os.workflows]
437
+ # Filter workflows based on user's scopes (only if authorization is enabled)
438
+ if getattr(request.state, "authorization_enabled", False):
439
+ from agno.os.auth import filter_resources_by_access, get_accessible_resources
440
+
441
+ # Check if user has any workflow scopes at all
442
+ accessible_ids = get_accessible_resources(request, "workflows")
443
+ if not accessible_ids:
444
+ raise HTTPException(status_code=403, detail="Insufficient permissions")
445
+
446
+ accessible_workflows = filter_resources_by_access(request, os.workflows, "workflows")
447
+ else:
448
+ accessible_workflows = os.workflows
449
+
450
+ return [WorkflowSummaryResponse.from_workflow(workflow) for workflow in accessible_workflows]
375
451
 
376
452
  @router.get(
377
453
  "/workflows/{workflow_id}",
@@ -397,8 +473,9 @@ def get_workflow_router(
397
473
  },
398
474
  404: {"description": "Workflow not found", "model": NotFoundResponse},
399
475
  },
476
+ dependencies=[Depends(require_resource_access("workflows", "read", "workflow_id"))],
400
477
  )
401
- async def get_workflow(workflow_id: str) -> WorkflowResponse:
478
+ async def get_workflow(workflow_id: str, request: Request) -> WorkflowResponse:
402
479
  workflow = get_workflow_by_id(workflow_id, os.workflows)
403
480
  if workflow is None:
404
481
  raise HTTPException(status_code=404, detail="Workflow not found")
@@ -438,6 +515,7 @@ def get_workflow_router(
438
515
  404: {"description": "Workflow not found", "model": NotFoundResponse},
439
516
  500: {"description": "Workflow execution error", "model": InternalServerErrorResponse},
440
517
  },
518
+ dependencies=[Depends(require_resource_access("workflows", "run", "workflow_id"))],
441
519
  )
442
520
  async def create_workflow_run(
443
521
  workflow_id: str,
@@ -530,6 +608,7 @@ def get_workflow_router(
530
608
  404: {"description": "Workflow or run not found", "model": NotFoundResponse},
531
609
  500: {"description": "Failed to cancel workflow run", "model": InternalServerErrorResponse},
532
610
  },
611
+ dependencies=[Depends(require_resource_access("workflows", "run", "workflow_id"))],
533
612
  )
534
613
  async def cancel_workflow_run(workflow_id: str, run_id: str):
535
614
  workflow = get_workflow_by_id(workflow_id, os.workflows)