agno 2.3.13__py3-none-any.whl → 2.3.15__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 (49) hide show
  1. agno/agent/agent.py +1149 -1392
  2. agno/db/migrations/manager.py +3 -3
  3. agno/eval/__init__.py +21 -8
  4. agno/knowledge/embedder/azure_openai.py +0 -1
  5. agno/knowledge/embedder/google.py +1 -1
  6. agno/models/anthropic/claude.py +9 -4
  7. agno/models/base.py +8 -4
  8. agno/models/metrics.py +12 -0
  9. agno/models/openai/chat.py +2 -0
  10. agno/models/openai/responses.py +2 -2
  11. agno/os/app.py +59 -2
  12. agno/os/auth.py +40 -3
  13. agno/os/interfaces/a2a/router.py +619 -9
  14. agno/os/interfaces/a2a/utils.py +31 -32
  15. agno/os/middleware/jwt.py +5 -5
  16. agno/os/router.py +1 -57
  17. agno/os/routers/agents/schema.py +14 -1
  18. agno/os/routers/database.py +150 -0
  19. agno/os/routers/teams/schema.py +14 -1
  20. agno/os/settings.py +3 -0
  21. agno/os/utils.py +61 -53
  22. agno/reasoning/anthropic.py +85 -1
  23. agno/reasoning/azure_ai_foundry.py +93 -1
  24. agno/reasoning/deepseek.py +91 -1
  25. agno/reasoning/gemini.py +81 -1
  26. agno/reasoning/groq.py +103 -1
  27. agno/reasoning/manager.py +1244 -0
  28. agno/reasoning/ollama.py +93 -1
  29. agno/reasoning/openai.py +113 -1
  30. agno/reasoning/vertexai.py +85 -1
  31. agno/run/agent.py +21 -0
  32. agno/run/base.py +20 -1
  33. agno/run/team.py +21 -0
  34. agno/session/team.py +0 -3
  35. agno/team/team.py +1211 -1445
  36. agno/tools/toolkit.py +119 -8
  37. agno/utils/events.py +99 -4
  38. agno/utils/hooks.py +4 -10
  39. agno/utils/print_response/agent.py +26 -0
  40. agno/utils/print_response/team.py +11 -0
  41. agno/utils/prompts.py +8 -6
  42. agno/utils/string.py +46 -0
  43. agno/utils/team.py +1 -1
  44. agno/vectordb/milvus/milvus.py +32 -3
  45. {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/METADATA +3 -2
  46. {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/RECORD +49 -47
  47. {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/WHEEL +0 -0
  48. {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/licenses/LICENSE +0 -0
  49. {agno-2.3.13.dist-info → agno-2.3.15.dist-info}/top_level.txt +0 -0
@@ -95,7 +95,6 @@ async def map_a2a_request_to_run_input(request_body: dict, stream: bool = True)
95
95
  ```json
96
96
  {
97
97
  "jsonrpc": "2.0",
98
- "method": "message/send",
99
98
  "id": "id",
100
99
  "params": {
101
100
  "message": {
@@ -325,7 +324,7 @@ async def stream_a2a_response(
325
324
  final=False,
326
325
  )
327
326
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
328
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
327
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
329
328
 
330
329
  # 2. Send all content and secondary events
331
330
 
@@ -341,7 +340,7 @@ async def stream_a2a_response(
341
340
  metadata={"agno_content_category": "content"},
342
341
  )
343
342
  response = SendStreamingMessageSuccessResponse(id=request_id, result=message)
344
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
343
+ yield f"event: Message\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
345
344
 
346
345
  # Send tool call events
347
346
  elif isinstance(event, (ToolCallStartedEvent, TeamToolCallStartedEvent)):
@@ -361,7 +360,7 @@ async def stream_a2a_response(
361
360
  metadata=metadata,
362
361
  )
363
362
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
364
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
363
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
365
364
 
366
365
  elif isinstance(event, (ToolCallCompletedEvent, TeamToolCallCompletedEvent)):
367
366
  metadata = {"agno_event_type": "tool_call_completed"}
@@ -380,7 +379,7 @@ async def stream_a2a_response(
380
379
  metadata=metadata,
381
380
  )
382
381
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
383
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
382
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
384
383
 
385
384
  # Send reasoning events
386
385
  elif isinstance(event, (ReasoningStartedEvent, TeamReasoningStartedEvent)):
@@ -392,7 +391,7 @@ async def stream_a2a_response(
392
391
  metadata={"agno_event_type": "reasoning_started"},
393
392
  )
394
393
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
395
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
394
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
396
395
 
397
396
  elif isinstance(event, (ReasoningStepEvent, TeamReasoningStepEvent)):
398
397
  if event.reasoning_content:
@@ -415,7 +414,7 @@ async def stream_a2a_response(
415
414
  metadata={"agno_content_category": "reasoning", "agno_event_type": "reasoning_step"},
416
415
  )
417
416
  response = SendStreamingMessageSuccessResponse(id=request_id, result=reasoning_message)
418
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
417
+ yield f"event: Message\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
419
418
 
420
419
  elif isinstance(event, (ReasoningCompletedEvent, TeamReasoningCompletedEvent)):
421
420
  status_event = TaskStatusUpdateEvent(
@@ -426,7 +425,7 @@ async def stream_a2a_response(
426
425
  metadata={"agno_event_type": "reasoning_completed"},
427
426
  )
428
427
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
429
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
428
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
430
429
 
431
430
  # Send memory update events
432
431
  elif isinstance(event, (MemoryUpdateStartedEvent, TeamMemoryUpdateStartedEvent)):
@@ -438,7 +437,7 @@ async def stream_a2a_response(
438
437
  metadata={"agno_event_type": "memory_update_started"},
439
438
  )
440
439
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
441
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
440
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
442
441
 
443
442
  elif isinstance(event, (MemoryUpdateCompletedEvent, TeamMemoryUpdateCompletedEvent)):
444
443
  status_event = TaskStatusUpdateEvent(
@@ -449,7 +448,7 @@ async def stream_a2a_response(
449
448
  metadata={"agno_event_type": "memory_update_completed"},
450
449
  )
451
450
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
452
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
451
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
453
452
 
454
453
  # Send workflow events
455
454
  elif isinstance(event, WorkflowStepStartedEvent):
@@ -465,7 +464,7 @@ async def stream_a2a_response(
465
464
  metadata=metadata,
466
465
  )
467
466
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
468
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
467
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
469
468
 
470
469
  elif isinstance(event, WorkflowStepCompletedEvent):
471
470
  metadata = {"agno_event_type": "workflow_step_completed"}
@@ -480,7 +479,7 @@ async def stream_a2a_response(
480
479
  metadata=metadata,
481
480
  )
482
481
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
483
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
482
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
484
483
 
485
484
  elif isinstance(event, WorkflowStepErrorEvent):
486
485
  metadata = {"agno_event_type": "workflow_step_error"}
@@ -497,7 +496,7 @@ async def stream_a2a_response(
497
496
  metadata=metadata,
498
497
  )
499
498
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
500
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
499
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
501
500
 
502
501
  # Send loop events
503
502
  elif isinstance(event, LoopExecutionStartedEvent):
@@ -515,7 +514,7 @@ async def stream_a2a_response(
515
514
  metadata=metadata,
516
515
  )
517
516
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
518
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
517
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
519
518
 
520
519
  elif isinstance(event, LoopIterationStartedEvent):
521
520
  metadata = {"agno_event_type": "loop_iteration_started"}
@@ -534,7 +533,7 @@ async def stream_a2a_response(
534
533
  metadata=metadata,
535
534
  )
536
535
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
537
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
536
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
538
537
 
539
538
  elif isinstance(event, LoopIterationCompletedEvent):
540
539
  metadata = {"agno_event_type": "loop_iteration_completed"}
@@ -553,7 +552,7 @@ async def stream_a2a_response(
553
552
  metadata=metadata,
554
553
  )
555
554
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
556
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
555
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
557
556
 
558
557
  elif isinstance(event, LoopExecutionCompletedEvent):
559
558
  metadata = {"agno_event_type": "loop_execution_completed"}
@@ -570,7 +569,7 @@ async def stream_a2a_response(
570
569
  metadata=metadata,
571
570
  )
572
571
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
573
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
572
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
574
573
 
575
574
  # Send parallel events
576
575
  elif isinstance(event, ParallelExecutionStartedEvent):
@@ -588,7 +587,7 @@ async def stream_a2a_response(
588
587
  metadata=metadata,
589
588
  )
590
589
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
591
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
590
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
592
591
 
593
592
  elif isinstance(event, ParallelExecutionCompletedEvent):
594
593
  metadata = {"agno_event_type": "parallel_execution_completed"}
@@ -605,7 +604,7 @@ async def stream_a2a_response(
605
604
  metadata=metadata,
606
605
  )
607
606
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
608
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
607
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
609
608
 
610
609
  # Send condition events
611
610
  elif isinstance(event, ConditionExecutionStartedEvent):
@@ -623,7 +622,7 @@ async def stream_a2a_response(
623
622
  metadata=metadata,
624
623
  )
625
624
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
626
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
625
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
627
626
 
628
627
  elif isinstance(event, ConditionExecutionCompletedEvent):
629
628
  metadata = {"agno_event_type": "condition_execution_completed"}
@@ -642,7 +641,7 @@ async def stream_a2a_response(
642
641
  metadata=metadata,
643
642
  )
644
643
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
645
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
644
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
646
645
 
647
646
  # Send router events
648
647
  elif isinstance(event, RouterExecutionStartedEvent):
@@ -660,7 +659,7 @@ async def stream_a2a_response(
660
659
  metadata=metadata,
661
660
  )
662
661
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
663
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
662
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
664
663
 
665
664
  elif isinstance(event, RouterExecutionCompletedEvent):
666
665
  metadata = {"agno_event_type": "router_execution_completed"}
@@ -679,7 +678,7 @@ async def stream_a2a_response(
679
678
  metadata=metadata,
680
679
  )
681
680
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
682
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
681
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
683
682
 
684
683
  # Send steps events
685
684
  elif isinstance(event, StepsExecutionStartedEvent):
@@ -697,7 +696,7 @@ async def stream_a2a_response(
697
696
  metadata=metadata,
698
697
  )
699
698
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
700
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
699
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
701
700
 
702
701
  elif isinstance(event, StepsExecutionCompletedEvent):
703
702
  metadata = {"agno_event_type": "steps_execution_completed"}
@@ -716,7 +715,7 @@ async def stream_a2a_response(
716
715
  metadata=metadata,
717
716
  )
718
717
  response = SendStreamingMessageSuccessResponse(id=request_id, result=status_event)
719
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
718
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
720
719
 
721
720
  # Capture completion event for final task construction
722
721
  elif isinstance(event, (RunCompletedEvent, TeamRunCompletedEvent, WorkflowCompletedEvent)):
@@ -748,7 +747,7 @@ async def stream_a2a_response(
748
747
  final=True,
749
748
  )
750
749
  response = SendStreamingMessageSuccessResponse(id=request_id, result=final_status_event)
751
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
750
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
752
751
 
753
752
  # 4. Send final task
754
753
  # Handle cancelled case
@@ -778,7 +777,7 @@ async def stream_a2a_response(
778
777
  history=[final_message],
779
778
  )
780
779
  response = SendStreamingMessageSuccessResponse(id=request_id, result=task)
781
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
780
+ yield f"event: Task\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
782
781
  return
783
782
 
784
783
  # Build from completion_event if available, otherwise use accumulated content
@@ -846,8 +845,8 @@ async def stream_a2a_response(
846
845
 
847
846
  # Handle all other data as Message metadata
848
847
  final_metadata: Dict[str, Any] = {}
849
- if hasattr(completion_event, "metrics") and completion_event.metrics:
850
- final_metadata["metrics"] = completion_event.metrics.__dict__
848
+ if hasattr(completion_event, "metrics") and completion_event.metrics: # type: ignore
849
+ final_metadata["metrics"] = completion_event.metrics.to_dict() # type: ignore
851
850
  if hasattr(completion_event, "metadata") and completion_event.metadata:
852
851
  final_metadata.update(completion_event.metadata)
853
852
 
@@ -880,7 +879,7 @@ async def stream_a2a_response(
880
879
  artifacts=artifacts if artifacts else None,
881
880
  )
882
881
  response = SendStreamingMessageSuccessResponse(id=request_id, result=task)
883
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
882
+ yield f"event: Task\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
884
883
 
885
884
 
886
885
  async def stream_a2a_response_with_error_handling(
@@ -904,7 +903,7 @@ async def stream_a2a_response_with_error_handling(
904
903
  final=True,
905
904
  )
906
905
  response = SendStreamingMessageSuccessResponse(id=request_id, result=failed_status_event)
907
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
906
+ yield f"event: TaskStatusUpdateEvent\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
908
907
 
909
908
  # Send failed Task
910
909
  error_message = A2AMessage(
@@ -921,4 +920,4 @@ async def stream_a2a_response_with_error_handling(
921
920
  )
922
921
 
923
922
  response = SendStreamingMessageSuccessResponse(id=request_id, result=failed_task)
924
- yield json.dumps(response.model_dump(exclude_none=True)) + "\n"
923
+ yield f"event: Task\ndata: {json.dumps(response.model_dump(exclude_none=True))}\n\n"
agno/os/middleware/jwt.py CHANGED
@@ -389,8 +389,9 @@ class JWTMiddleware(BaseHTTPMiddleware):
389
389
  or JWT_JWKS env var for inline JWKS JSON content.
390
390
  secret_key: (deprecated) Use verification_keys instead. If provided, will be added to verification_keys.
391
391
  algorithm: JWT algorithm (default: RS256). Common options: RS256 (asymmetric), HS256 (symmetric).
392
- validate: Whether to validate the JWT token (default: True). If False, tokens are decoded
393
- without signature verification and no verification key is required.
392
+ validate: Whether to validate the JWT signature (default: True). If False, tokens are decoded
393
+ without signature verification and no verification key is required. Useful when
394
+ JWT verification is handled upstream (API Gateway, etc.).
394
395
  authorization: Whether to add authorization checks to the request (i.e. validation of scopes)
395
396
  token_source: Where to extract JWT token from (header, cookie, or both)
396
397
  token_header_key: Header key for Authorization (default: "Authorization")
@@ -642,9 +643,8 @@ class JWTMiddleware(BaseHTTPMiddleware):
642
643
  # Extract JWT token
643
644
  token = self._extract_token(request)
644
645
  if not token:
645
- if self.validate:
646
- error_msg = self._get_missing_token_error_message()
647
- return self._create_error_response(401, error_msg, origin, cors_allowed_origins)
646
+ error_msg = self._get_missing_token_error_message()
647
+ return self._create_error_response(401, error_msg, origin, cors_allowed_origins)
648
648
 
649
649
  try:
650
650
  # Validate token and extract claims (with audience verification if configured)
agno/os/router.py CHANGED
@@ -1,16 +1,11 @@
1
- from typing import TYPE_CHECKING, List, Optional, Union, cast
1
+ from typing import TYPE_CHECKING, List, Union, cast
2
2
 
3
3
  from fastapi import (
4
4
  APIRouter,
5
5
  Depends,
6
- HTTPException,
7
6
  )
8
- from fastapi.responses import JSONResponse
9
- from packaging import version
10
7
 
11
8
  from agno.agent.agent import Agent
12
- from agno.db.base import AsyncBaseDb
13
- from agno.db.migrations.manager import MigrationManager
14
9
  from agno.os.auth import get_authentication_dependency
15
10
  from agno.os.schema import (
16
11
  AgentSummaryResponse,
@@ -26,9 +21,6 @@ from agno.os.schema import (
26
21
  WorkflowSummaryResponse,
27
22
  )
28
23
  from agno.os.settings import AgnoAPISettings
29
- from agno.os.utils import (
30
- get_db,
31
- )
32
24
  from agno.team.team import Team
33
25
 
34
26
  if TYPE_CHECKING:
@@ -207,52 +199,4 @@ def get_base_router(
207
199
 
208
200
  return list(unique_models.values())
209
201
 
210
- # -- Database Migration routes ---
211
- @router.post(
212
- "/databases/{db_id}/migrate",
213
- tags=["Database"],
214
- operation_id="migrate_database",
215
- summary="Migrate Database",
216
- description=(
217
- "Migrate the given database schema to the given target version. "
218
- "If a target version is not provided, the database will be migrated to the latest version. "
219
- ),
220
- responses={
221
- 200: {
222
- "description": "Database migrated successfully",
223
- "content": {
224
- "application/json": {
225
- "example": {"message": "Database migrated successfully to version 3.0.0"},
226
- }
227
- },
228
- },
229
- 404: {"description": "Database not found", "model": NotFoundResponse},
230
- 500: {"description": "Failed to migrate database", "model": InternalServerErrorResponse},
231
- },
232
- )
233
- async def migrate_database(db_id: str, target_version: Optional[str] = None):
234
- db = await get_db(os.dbs, db_id)
235
- if not db:
236
- raise HTTPException(status_code=404, detail="Database not found")
237
-
238
- if target_version:
239
- # Use the session table as proxy for the database schema version
240
- if isinstance(db, AsyncBaseDb):
241
- current_version = await db.get_latest_schema_version(db.session_table_name)
242
- else:
243
- current_version = db.get_latest_schema_version(db.session_table_name)
244
-
245
- if version.parse(target_version) > version.parse(current_version): # type: ignore
246
- MigrationManager(db).up(target_version) # type: ignore
247
- else:
248
- MigrationManager(db).down(target_version) # type: ignore
249
-
250
- # If the target version is not provided, migrate to the latest version
251
- else:
252
- MigrationManager(db).up() # type: ignore
253
-
254
- return JSONResponse(
255
- content={"message": f"Database migrated successfully to version {target_version}"}, status_code=200
256
- )
257
-
258
202
  return router
@@ -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,
@@ -0,0 +1,150 @@
1
+ from typing import TYPE_CHECKING, Optional
2
+
3
+ from fastapi import (
4
+ APIRouter,
5
+ Depends,
6
+ HTTPException,
7
+ )
8
+ from fastapi.responses import JSONResponse
9
+ from packaging import version
10
+
11
+ from agno.db.base import AsyncBaseDb
12
+ from agno.db.migrations.manager import MigrationManager
13
+ from agno.os.auth import get_authentication_dependency
14
+ from agno.os.schema import (
15
+ BadRequestResponse,
16
+ InternalServerErrorResponse,
17
+ NotFoundResponse,
18
+ UnauthenticatedResponse,
19
+ ValidationErrorResponse,
20
+ )
21
+ from agno.os.settings import AgnoAPISettings
22
+ from agno.os.utils import (
23
+ get_db,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from agno.os.app import AgentOS
28
+
29
+
30
+ def get_database_router(
31
+ os: "AgentOS",
32
+ settings: AgnoAPISettings = AgnoAPISettings(),
33
+ ) -> APIRouter:
34
+ """Create the database router with comprehensive OpenAPI documentation."""
35
+ router = APIRouter(
36
+ dependencies=[Depends(get_authentication_dependency(settings))],
37
+ responses={
38
+ 400: {"description": "Bad Request", "model": BadRequestResponse},
39
+ 401: {"description": "Unauthorized", "model": UnauthenticatedResponse},
40
+ 404: {"description": "Not Found", "model": NotFoundResponse},
41
+ 422: {"description": "Validation Error", "model": ValidationErrorResponse},
42
+ 500: {"description": "Internal Server Error", "model": InternalServerErrorResponse},
43
+ },
44
+ )
45
+
46
+ async def _migrate_single_db(db, target_version: Optional[str] = None) -> None:
47
+ """Migrate a single database."""
48
+ if target_version:
49
+ # Use the session table as proxy for the database schema version
50
+ if isinstance(db, AsyncBaseDb):
51
+ current_version = await db.get_latest_schema_version(db.session_table_name)
52
+ else:
53
+ current_version = db.get_latest_schema_version(db.session_table_name)
54
+
55
+ if version.parse(target_version) > version.parse(current_version): # type: ignore
56
+ await MigrationManager(db).up(target_version) # type: ignore
57
+ else:
58
+ await MigrationManager(db).down(target_version) # type: ignore
59
+ else:
60
+ # If the target version is not provided, migrate to the latest version
61
+ await MigrationManager(db).up() # type: ignore
62
+
63
+ @router.post(
64
+ "/databases/all/migrate",
65
+ tags=["Database"],
66
+ operation_id="migrate_all_databases",
67
+ summary="Migrate All Databases",
68
+ description=(
69
+ "Migrate all database schemas to the given target version. "
70
+ "If a target version is not provided, all databases will be migrated to the latest version."
71
+ ),
72
+ responses={
73
+ 200: {
74
+ "description": "All databases migrated successfully",
75
+ "content": {
76
+ "application/json": {
77
+ "example": {"message": "All databases migrated successfully to version 3.0.0"},
78
+ }
79
+ },
80
+ },
81
+ 500: {"description": "Failed to migrate databases", "model": InternalServerErrorResponse},
82
+ },
83
+ )
84
+ async def migrate_all_databases(target_version: Optional[str] = None):
85
+ """Migrate all databases."""
86
+ all_dbs = {db.id: db for db_id, dbs in os.dbs.items() for db in dbs}
87
+ failed_dbs: dict[str, str] = {}
88
+
89
+ for db_id, db in all_dbs.items():
90
+ try:
91
+ await _migrate_single_db(db, target_version)
92
+ except Exception as e:
93
+ failed_dbs[db_id] = str(e)
94
+
95
+ version_msg = f"version {target_version}" if target_version else "latest version"
96
+ migrated_count = len(all_dbs) - len(failed_dbs)
97
+
98
+ if failed_dbs:
99
+ return JSONResponse(
100
+ content={
101
+ "message": f"Migrated {migrated_count}/{len(all_dbs)} databases to {version_msg}",
102
+ "failed": failed_dbs,
103
+ },
104
+ status_code=207, # Multi-Status
105
+ )
106
+
107
+ return JSONResponse(
108
+ content={"message": f"All databases migrated successfully to {version_msg}"}, status_code=200
109
+ )
110
+
111
+ @router.post(
112
+ "/databases/{db_id}/migrate",
113
+ tags=["Database"],
114
+ operation_id="migrate_database",
115
+ summary="Migrate Database",
116
+ description=(
117
+ "Migrate the given database schema to the given target version. "
118
+ "If a target version is not provided, the database will be migrated to the latest version."
119
+ ),
120
+ responses={
121
+ 200: {
122
+ "description": "Database migrated successfully",
123
+ "content": {
124
+ "application/json": {
125
+ "example": {"message": "Database migrated successfully to version 3.0.0"},
126
+ }
127
+ },
128
+ },
129
+ 404: {"description": "Database not found", "model": NotFoundResponse},
130
+ 500: {"description": "Failed to migrate database", "model": InternalServerErrorResponse},
131
+ },
132
+ )
133
+ async def migrate_database(db_id: str, target_version: Optional[str] = None):
134
+ db = await get_db(os.dbs, db_id)
135
+ if not db:
136
+ raise HTTPException(status_code=404, detail="Database not found")
137
+
138
+ try:
139
+ await _migrate_single_db(db, target_version)
140
+
141
+ version_msg = f"version {target_version}" if target_version else "latest version"
142
+ return JSONResponse(
143
+ content={"message": f"Database migrated successfully to {version_msg}"}, status_code=200
144
+ )
145
+ except HTTPException:
146
+ raise
147
+ except Exception as e:
148
+ raise HTTPException(status_code=500, detail=f"Failed to migrate database: {str(e)}")
149
+
150
+ return router
@@ -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,
agno/os/settings.py CHANGED
@@ -20,6 +20,9 @@ class AgnoAPISettings(BaseSettings):
20
20
  # Authentication settings
21
21
  os_security_key: Optional[str] = Field(default=None, description="Bearer token for API authentication")
22
22
 
23
+ # Authorization flag - when True, JWT middleware handles auth and security key validation is skipped
24
+ authorization_enabled: bool = Field(default=False, description="Whether JWT authorization is enabled")
25
+
23
26
  # Cors origin list to allow requests from.
24
27
  # This list is set using the set_cors_origin_list validator
25
28
  cors_origin_list: Optional[List[str]] = Field(default=None, validate_default=True)