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
@@ -66,13 +66,13 @@ class MigrationManager:
66
66
  current_version = packaging_version.parse(self.db.get_latest_schema_version(table_name))
67
67
 
68
68
  if current_version is None:
69
- log_warning(f"Skipping up migration: No version found for table {table_name}.")
69
+ log_info(f"Skipping migration: No version found for table {table_name}.")
70
70
  continue
71
71
 
72
72
  # If the target version is less or equal to the current version, no migrations needed
73
73
  if _target_version <= current_version and not force:
74
- log_warning(
75
- f"Skipping up migration: the version of table '{table_name}' ({current_version}) is less or equal to the target version ({_target_version})."
74
+ log_info(
75
+ f"Skipping migration: the version of table '{table_name}' ({current_version}) is less or equal to the target version ({_target_version})."
76
76
  )
77
77
  continue
78
78
 
agno/eval/__init__.py CHANGED
@@ -1,12 +1,4 @@
1
- from agno.eval.accuracy import AccuracyAgentResponse, AccuracyEval, AccuracyEvaluation, AccuracyResult
2
- from agno.eval.agent_as_judge import (
3
- AgentAsJudgeEval,
4
- AgentAsJudgeEvaluation,
5
- AgentAsJudgeResult,
6
- )
7
1
  from agno.eval.base import BaseEval
8
- from agno.eval.performance import PerformanceEval, PerformanceResult
9
- from agno.eval.reliability import ReliabilityEval, ReliabilityResult
10
2
 
11
3
  __all__ = [
12
4
  "AccuracyAgentResponse",
@@ -22,3 +14,24 @@ __all__ = [
22
14
  "ReliabilityEval",
23
15
  "ReliabilityResult",
24
16
  ]
17
+
18
+
19
+ def __getattr__(name: str):
20
+ """Lazy import for eval implementations to avoid circular imports with Agent."""
21
+ if name in ("AccuracyAgentResponse", "AccuracyEval", "AccuracyEvaluation", "AccuracyResult"):
22
+ from agno.eval import accuracy
23
+
24
+ return getattr(accuracy, name)
25
+ elif name in ("AgentAsJudgeEval", "AgentAsJudgeEvaluation", "AgentAsJudgeResult"):
26
+ from agno.eval import agent_as_judge
27
+
28
+ return getattr(agent_as_judge, name)
29
+ elif name in ("PerformanceEval", "PerformanceResult"):
30
+ from agno.eval import performance
31
+
32
+ return getattr(performance, name)
33
+ elif name in ("ReliabilityEval", "ReliabilityResult"):
34
+ from agno.eval import reliability
35
+
36
+ return getattr(reliability, name)
37
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
@@ -18,7 +18,6 @@ except ImportError:
18
18
  @dataclass
19
19
  class AzureOpenAIEmbedder(Embedder):
20
20
  id: str = "text-embedding-3-small" # This has to match the model that you deployed at the provided URL
21
-
22
21
  dimensions: int = 1536
23
22
  encoding_format: Literal["float", "base64"] = "float"
24
23
  user: Optional[str] = None
@@ -15,7 +15,7 @@ except ImportError:
15
15
 
16
16
  @dataclass
17
17
  class GeminiEmbedder(Embedder):
18
- id: str = "gemini-embedding-exp-03-07"
18
+ id: str = "gemini-embedding-001"
19
19
  task_type: str = "RETRIEVAL_QUERY"
20
20
  title: Optional[str] = None
21
21
  dimensions: Optional[int] = 1536
@@ -102,8 +102,8 @@ class Claude(Model):
102
102
  # Claude Sonnet 4.x family (versions before 4.5)
103
103
  "claude-sonnet-4-20250514",
104
104
  "claude-sonnet-4",
105
- # Claude Opus 4.x family (versions before 4.1)
106
- # (Add any Opus 4.x models released before 4.1 if they exist)
105
+ # Claude Opus 4.x family (versions before 4.1 and 4.5)
106
+ # (Add any Opus 4.x models released before 4.1/4.5 if they exist)
107
107
  }
108
108
 
109
109
  id: str = "claude-sonnet-4-5-20250929"
@@ -191,7 +191,9 @@ class Claude(Model):
191
191
  return False
192
192
  if self.id.startswith("claude-sonnet-4-") and not self.id.startswith("claude-sonnet-4-5"):
193
193
  return False
194
- if self.id.startswith("claude-opus-4-") and not self.id.startswith("claude-opus-4-1"):
194
+ if self.id.startswith("claude-opus-4-") and not (
195
+ self.id.startswith("claude-opus-4-1") or self.id.startswith("claude-opus-4-5")
196
+ ):
195
197
  return False
196
198
 
197
199
  return True
@@ -320,8 +322,11 @@ class Claude(Model):
320
322
 
321
323
  return {"type": "json_schema", "schema": schema}
322
324
 
323
- # Handle dict format (already in correct structure)
325
+ # Handle dict format
324
326
  elif isinstance(response_format, dict):
327
+ # Claude only supports json_schema, not json_object
328
+ if response_format.get("type") == "json_object":
329
+ return None
325
330
  return response_format
326
331
 
327
332
  return None
agno/models/base.py CHANGED
@@ -196,7 +196,8 @@ class Model(ABC):
196
196
  )
197
197
  sleep(delay)
198
198
  else:
199
- log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
199
+ if self.retries > 0:
200
+ log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
200
201
  except RetryableModelProviderError as e:
201
202
  current_count = retries_with_guidance_count
202
203
  if current_count >= self.retry_with_guidance_limit:
@@ -238,7 +239,8 @@ class Model(ABC):
238
239
  )
239
240
  await asyncio.sleep(delay)
240
241
  else:
241
- log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
242
+ if self.retries > 0:
243
+ log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
242
244
  except RetryableModelProviderError as e:
243
245
  current_count = retries_with_guidance_count
244
246
  if current_count >= self.retry_with_guidance_limit:
@@ -283,7 +285,8 @@ class Model(ABC):
283
285
  )
284
286
  sleep(delay)
285
287
  else:
286
- log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
288
+ if self.retries > 0:
289
+ log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
287
290
  except RetryableModelProviderError as e:
288
291
  current_count = retries_with_guidance_count
289
292
  if current_count >= self.retry_with_guidance_limit:
@@ -330,7 +333,8 @@ class Model(ABC):
330
333
  )
331
334
  await asyncio.sleep(delay)
332
335
  else:
333
- log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
336
+ if self.retries > 0:
337
+ log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
334
338
  except RetryableModelProviderError as e:
335
339
  current_count = retries_with_guidance_count
336
340
  if current_count >= self.retry_with_guidance_limit:
agno/models/metrics.py CHANGED
@@ -13,6 +13,10 @@ class Metrics:
13
13
  output_tokens: int = 0
14
14
  total_tokens: int = 0
15
15
 
16
+ # Cost of the run
17
+ # Currently only supported by some providers
18
+ cost: Optional[float] = None
19
+
16
20
  # Audio token usage
17
21
  audio_input_tokens: int = 0
18
22
  audio_output_tokens: int = 0
@@ -43,6 +47,7 @@ class Metrics:
43
47
  metrics_dict = asdict(self)
44
48
  # Remove the timer util if present
45
49
  metrics_dict.pop("timer", None)
50
+ # Remove any None, 0, or empty dict values
46
51
  metrics_dict = {
47
52
  k: v
48
53
  for k, v in metrics_dict.items()
@@ -65,6 +70,13 @@ class Metrics:
65
70
  reasoning_tokens=self.reasoning_tokens + other.reasoning_tokens,
66
71
  )
67
72
 
73
+ if self.cost is not None and other.cost is not None:
74
+ result.cost = self.cost + other.cost
75
+ elif self.cost is not None:
76
+ result.cost = self.cost
77
+ elif other.cost is not None:
78
+ result.cost = other.cost
79
+
68
80
  # Handle provider_metrics
69
81
  if self.provider_metrics or other.provider_metrics:
70
82
  result.provider_metrics = {}
@@ -945,4 +945,6 @@ class OpenAIChat(Model):
945
945
  metrics.audio_output_tokens = completion_tokens_details.audio_tokens or 0
946
946
  metrics.reasoning_tokens = completion_tokens_details.reasoning_tokens or 0
947
947
 
948
+ metrics.cost = getattr(response_usage, "cost", None)
949
+
948
950
  return metrics
@@ -234,8 +234,8 @@ class OpenAIResponses(Model):
234
234
  "strict": self.strict_output,
235
235
  }
236
236
  else:
237
- # JSON mode
238
- text_params["format"] = {"type": "json_object"}
237
+ # Pass through directly, user handles everything
238
+ text_params["format"] = response_format
239
239
 
240
240
  # Add text parameter if there are any text-level params
241
241
  if text_params:
agno/os/app.py CHANGED
@@ -34,6 +34,7 @@ from agno.os.config import (
34
34
  from agno.os.interfaces.base import BaseInterface
35
35
  from agno.os.router import get_base_router
36
36
  from agno.os.routers.agents import get_agent_router
37
+ from agno.os.routers.database import get_database_router
37
38
  from agno.os.routers.evals import get_eval_router
38
39
  from agno.os.routers.health import get_health_router
39
40
  from agno.os.routers.home import get_home_router
@@ -225,6 +226,9 @@ class AgentOS:
225
226
  self._initialize_teams()
226
227
  self._initialize_workflows()
227
228
 
229
+ # Check for duplicate IDs
230
+ self._raise_if_duplicate_ids()
231
+
228
232
  if self.tracing:
229
233
  self._setup_tracing()
230
234
 
@@ -266,6 +270,9 @@ class AgentOS:
266
270
  self._initialize_agents()
267
271
  self._initialize_teams()
268
272
  self._initialize_workflows()
273
+
274
+ # Check for duplicate IDs
275
+ self._raise_if_duplicate_ids()
269
276
  self._auto_discover_databases()
270
277
  self._auto_discover_knowledge_instances()
271
278
 
@@ -334,6 +341,31 @@ class AgentOS:
334
341
  self.interfaces.append(a2a_interface)
335
342
  self._add_router(app, a2a_interface.get_router())
336
343
 
344
+ def _raise_if_duplicate_ids(self) -> None:
345
+ """Check for duplicate IDs within each entity type.
346
+
347
+ Raises:
348
+ ValueError: If duplicate IDs are found within the same entity type
349
+ """
350
+ duplicate_ids: List[str] = []
351
+
352
+ for entities in [self.agents, self.teams, self.workflows]:
353
+ if not entities:
354
+ continue
355
+ seen_ids: set[str] = set()
356
+ for entity in entities:
357
+ entity_id = entity.id
358
+ if entity_id is None:
359
+ continue
360
+ if entity_id in seen_ids:
361
+ if entity_id not in duplicate_ids:
362
+ duplicate_ids.append(entity_id)
363
+ else:
364
+ seen_ids.add(entity_id)
365
+
366
+ if duplicate_ids:
367
+ raise ValueError(f"Duplicate IDs found in AgentOS: {', '.join(repr(id_) for id_ in duplicate_ids)}")
368
+
337
369
  def _make_app(self, lifespan: Optional[Any] = None) -> FastAPI:
338
370
  # Adjust the FastAPI app lifespan to handle MCP connections if relevant
339
371
  app_lifespan = lifespan
@@ -543,6 +575,7 @@ class AgentOS:
543
575
  get_metrics_router(dbs=self.dbs),
544
576
  get_knowledge_router(knowledge_instances=self.knowledge_instances),
545
577
  get_traces_router(dbs=self.dbs),
578
+ get_database_router(self, settings=self.settings),
546
579
  ]
547
580
 
548
581
  for router in routers:
@@ -583,6 +616,18 @@ class AgentOS:
583
616
 
584
617
  # Add JWT middleware if authorization is enabled
585
618
  if self.authorization:
619
+ # Set authorization_enabled flag on settings so security key validation is skipped
620
+ self.settings.authorization_enabled = True
621
+
622
+ jwt_configured = bool(getenv("JWT_VERIFICATION_KEY") or getenv("JWT_JWKS_FILE"))
623
+ security_key_set = bool(self.settings.os_security_key)
624
+ if jwt_configured and security_key_set:
625
+ log_warning(
626
+ "Both JWT configuration (JWT_VERIFICATION_KEY or JWT_JWKS_FILE) and OS_SECURITY_KEY are set. "
627
+ "With authorization=True, only JWT authorization will be used. "
628
+ "Consider removing OS_SECURITY_KEY from your environment."
629
+ )
630
+
586
631
  self._add_jwt_middleware(fastapi_app)
587
632
 
588
633
  return fastapi_app
@@ -981,6 +1026,8 @@ class AgentOS:
981
1026
  host: str = "localhost",
982
1027
  port: int = 7777,
983
1028
  reload: bool = False,
1029
+ reload_includes: Optional[List[str]] = None,
1030
+ reload_excludes: Optional[List[str]] = None,
984
1031
  workers: Optional[int] = None,
985
1032
  access_log: bool = False,
986
1033
  **kwargs,
@@ -1000,8 +1047,12 @@ class AgentOS:
1000
1047
  Align.center(f"[bold cyan]{public_endpoint}[/bold cyan]"),
1001
1048
  Align.center(f"\n\n[bold dark_orange]OS running on:[/bold dark_orange] http://{host}:{port}"),
1002
1049
  ]
1003
- if bool(self.settings.os_security_key):
1004
- panel_group.append(Align.center("\n\n[bold chartreuse3]:lock: Security Enabled[/bold chartreuse3]"))
1050
+ if self.authorization:
1051
+ panel_group.append(
1052
+ Align.center("\n\n[bold chartreuse3]:lock: JWT Authorization Enabled[/bold chartreuse3]")
1053
+ )
1054
+ elif bool(self.settings.os_security_key):
1055
+ panel_group.append(Align.center("\n\n[bold chartreuse3]:lock: Security Key Enabled[/bold chartreuse3]"))
1005
1056
 
1006
1057
  console = Console()
1007
1058
  console.print(
@@ -1015,11 +1066,17 @@ class AgentOS:
1015
1066
  )
1016
1067
  )
1017
1068
 
1069
+ # Adding *.yaml to reload_includes to reload the app when the yaml config file changes.
1070
+ if reload and reload_includes is not None:
1071
+ reload_includes = ["*.yaml", "*.yml"]
1072
+
1018
1073
  uvicorn.run(
1019
1074
  app=app,
1020
1075
  host=host,
1021
1076
  port=port,
1022
1077
  reload=reload,
1078
+ reload_includes=reload_includes,
1079
+ reload_excludes=reload_excludes,
1023
1080
  workers=workers,
1024
1081
  access_log=access_log,
1025
1082
  lifespan="on",
agno/os/auth.py CHANGED
@@ -1,3 +1,4 @@
1
+ from os import getenv
1
2
  from typing import List, Set
2
3
 
3
4
  from fastapi import Depends, HTTPException, Request
@@ -10,18 +11,43 @@ from agno.os.settings import AgnoAPISettings
10
11
  security = HTTPBearer(auto_error=False)
11
12
 
12
13
 
14
+ def _is_jwt_configured() -> bool:
15
+ """Check if JWT authentication is configured via environment variables.
16
+
17
+ This covers cases where JWT middleware is set up manually (not via authorization=True).
18
+ """
19
+ return bool(getenv("JWT_VERIFICATION_KEY") or getenv("JWT_JWKS_FILE"))
20
+
21
+
13
22
  def get_authentication_dependency(settings: AgnoAPISettings):
14
23
  """
15
24
  Create an authentication dependency function for FastAPI routes.
16
25
 
26
+ This handles security key authentication (OS_SECURITY_KEY).
27
+ When JWT authorization is enabled (via authorization=True, JWT environment variables,
28
+ or manually added JWT middleware), this dependency is skipped as JWT middleware
29
+ handles authentication.
30
+
17
31
  Args:
18
- settings: The API settings containing the security key
32
+ settings: The API settings containing the security key and authorization flag
19
33
 
20
34
  Returns:
21
35
  A dependency function that can be used with FastAPI's Depends()
22
36
  """
23
37
 
24
- async def auth_dependency(credentials: HTTPAuthorizationCredentials = Depends(security)) -> bool:
38
+ async def auth_dependency(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)) -> bool:
39
+ # If JWT authorization is enabled via settings (authorization=True on AgentOS)
40
+ if settings and settings.authorization_enabled:
41
+ return True
42
+
43
+ # Check if JWT middleware has already handled authentication
44
+ if getattr(request.state, "authenticated", False):
45
+ return True
46
+
47
+ # Also skip if JWT is configured via environment variables
48
+ if _is_jwt_configured():
49
+ return True
50
+
25
51
  # If no security key is set, skip authentication entirely
26
52
  if not settings or not settings.os_security_key:
27
53
  return True
@@ -45,13 +71,24 @@ def validate_websocket_token(token: str, settings: AgnoAPISettings) -> bool:
45
71
  """
46
72
  Validate a bearer token for WebSocket authentication (legacy os_security_key method).
47
73
 
74
+ When JWT authorization is enabled (via authorization=True or JWT environment variables),
75
+ this validation is skipped as JWT middleware handles authentication.
76
+
48
77
  Args:
49
78
  token: The bearer token to validate
50
- settings: The API settings containing the security key
79
+ settings: The API settings containing the security key and authorization flag
51
80
 
52
81
  Returns:
53
82
  True if the token is valid or authentication is disabled, False otherwise
54
83
  """
84
+ # If JWT authorization is enabled, skip security key validation
85
+ if settings and settings.authorization_enabled:
86
+ return True
87
+
88
+ # Also skip if JWT is configured via environment variables (manual JWT middleware setup)
89
+ if _is_jwt_configured():
90
+ return True
91
+
55
92
  # If no security key is set, skip authentication entirely
56
93
  if not settings or not settings.os_security_key:
57
94
  return True