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.
- agno/agent/agent.py +1125 -1401
- agno/eval/__init__.py +21 -8
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/models/anthropic/claude.py +4 -1
- agno/models/azure/openai_chat.py +11 -5
- agno/models/base.py +8 -4
- agno/models/openai/chat.py +0 -2
- agno/models/openai/responses.py +2 -2
- agno/os/app.py +112 -5
- agno/os/auth.py +190 -3
- agno/os/config.py +9 -0
- agno/os/interfaces/a2a/router.py +619 -9
- agno/os/interfaces/a2a/utils.py +31 -32
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +670 -108
- agno/os/router.py +0 -1
- agno/os/routers/agents/router.py +22 -4
- agno/os/routers/agents/schema.py +14 -1
- agno/os/routers/teams/router.py +20 -4
- agno/os/routers/teams/schema.py +14 -1
- agno/os/routers/workflows/router.py +88 -9
- agno/os/scopes.py +469 -0
- agno/os/utils.py +86 -53
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +91 -1
- agno/reasoning/gemini.py +81 -1
- agno/reasoning/groq.py +103 -1
- agno/reasoning/manager.py +1244 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +113 -1
- agno/reasoning/vertexai.py +85 -1
- agno/run/agent.py +11 -0
- agno/run/base.py +1 -1
- agno/run/team.py +11 -0
- agno/session/team.py +0 -3
- agno/team/team.py +1204 -1452
- agno/tools/postgres.py +1 -1
- agno/utils/cryptography.py +22 -0
- agno/utils/events.py +69 -2
- agno/utils/hooks.py +4 -10
- agno/utils/print_response/agent.py +52 -2
- agno/utils/print_response/team.py +141 -10
- agno/utils/prompts.py +8 -6
- agno/utils/string.py +46 -0
- agno/utils/team.py +1 -1
- agno/vectordb/chroma/chromadb.py +1 -0
- agno/vectordb/milvus/milvus.py +32 -3
- agno/vectordb/redis/redisdb.py +16 -2
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/METADATA +3 -2
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/RECORD +55 -52
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/WHEEL +0 -0
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/top_level.txt +0 -0
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
|
agno/models/anthropic/claude.py
CHANGED
|
@@ -320,8 +320,11 @@ class Claude(Model):
|
|
|
320
320
|
|
|
321
321
|
return {"type": "json_schema", "schema": schema}
|
|
322
322
|
|
|
323
|
-
# Handle dict format
|
|
323
|
+
# Handle dict format
|
|
324
324
|
elif isinstance(response_format, dict):
|
|
325
|
+
# Claude only supports json_schema, not json_object
|
|
326
|
+
if response_format.get("type") == "json_object":
|
|
327
|
+
return None
|
|
325
328
|
return response_format
|
|
326
329
|
|
|
327
330
|
return None
|
agno/models/azure/openai_chat.py
CHANGED
|
@@ -65,11 +65,17 @@ class AzureOpenAI(OpenAILike):
|
|
|
65
65
|
self.azure_endpoint = self.azure_endpoint or getenv("AZURE_OPENAI_ENDPOINT")
|
|
66
66
|
self.azure_deployment = self.azure_deployment or getenv("AZURE_OPENAI_DEPLOYMENT")
|
|
67
67
|
|
|
68
|
-
if not self.api_key:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
if not (self.api_key or self.azure_ad_token):
|
|
69
|
+
if not self.api_key:
|
|
70
|
+
raise ModelAuthenticationError(
|
|
71
|
+
message="AZURE_OPENAI_API_KEY not set. Please set the AZURE_OPENAI_API_KEY environment variable.",
|
|
72
|
+
model_name=self.name,
|
|
73
|
+
)
|
|
74
|
+
if not self.azure_ad_token:
|
|
75
|
+
raise ModelAuthenticationError(
|
|
76
|
+
message="AZURE_AD_TOKEN not set. Please set the AZURE_AD_TOKEN environment variable.",
|
|
77
|
+
model_name=self.name,
|
|
78
|
+
)
|
|
73
79
|
|
|
74
80
|
params_mapping = {
|
|
75
81
|
"api_key": self.api_key,
|
agno/models/base.py
CHANGED
|
@@ -196,7 +196,8 @@ class Model(ABC):
|
|
|
196
196
|
)
|
|
197
197
|
sleep(delay)
|
|
198
198
|
else:
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/openai/chat.py
CHANGED
|
@@ -768,7 +768,6 @@ class OpenAIChat(Model):
|
|
|
768
768
|
# Add role
|
|
769
769
|
if response_message.role is not None:
|
|
770
770
|
model_response.role = response_message.role
|
|
771
|
-
|
|
772
771
|
# Add content
|
|
773
772
|
if response_message.content is not None:
|
|
774
773
|
model_response.content = response_message.content
|
|
@@ -846,7 +845,6 @@ class OpenAIChat(Model):
|
|
|
846
845
|
|
|
847
846
|
if response_delta.choices and len(response_delta.choices) > 0:
|
|
848
847
|
choice_delta: ChoiceDelta = response_delta.choices[0].delta
|
|
849
|
-
|
|
850
848
|
if choice_delta:
|
|
851
849
|
# Add content
|
|
852
850
|
if choice_delta.content is not None:
|
agno/models/openai/responses.py
CHANGED
|
@@ -234,8 +234,8 @@ class OpenAIResponses(Model):
|
|
|
234
234
|
"strict": self.strict_output,
|
|
235
235
|
}
|
|
236
236
|
else:
|
|
237
|
-
#
|
|
238
|
-
text_params["format"] =
|
|
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
|
@@ -16,6 +16,7 @@ from agno.db.base import AsyncBaseDb, BaseDb
|
|
|
16
16
|
from agno.knowledge.knowledge import Knowledge
|
|
17
17
|
from agno.os.config import (
|
|
18
18
|
AgentOSConfig,
|
|
19
|
+
AuthorizationConfig,
|
|
19
20
|
DatabaseConfig,
|
|
20
21
|
EvalsConfig,
|
|
21
22
|
EvalsDomainConfig,
|
|
@@ -49,11 +50,12 @@ from agno.os.utils import (
|
|
|
49
50
|
collect_mcp_tools_from_workflow,
|
|
50
51
|
find_conflicting_routes,
|
|
51
52
|
load_yaml_config,
|
|
53
|
+
resolve_origins,
|
|
52
54
|
setup_tracing_for_os,
|
|
53
55
|
update_cors_middleware,
|
|
54
56
|
)
|
|
55
57
|
from agno.team.team import Team
|
|
56
|
-
from agno.utils.log import log_debug, log_error, log_warning
|
|
58
|
+
from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
57
59
|
from agno.utils.string import generate_id, generate_id_from_name
|
|
58
60
|
from agno.workflow.workflow import Workflow
|
|
59
61
|
|
|
@@ -118,17 +120,20 @@ class AgentOS:
|
|
|
118
120
|
knowledge: Optional[List[Knowledge]] = None,
|
|
119
121
|
interfaces: Optional[List[BaseInterface]] = None,
|
|
120
122
|
a2a_interface: bool = False,
|
|
123
|
+
authorization: bool = False,
|
|
124
|
+
authorization_config: Optional[AuthorizationConfig] = None,
|
|
125
|
+
cors_allowed_origins: Optional[List[str]] = None,
|
|
121
126
|
config: Optional[Union[str, AgentOSConfig]] = None,
|
|
122
127
|
settings: Optional[AgnoAPISettings] = None,
|
|
123
128
|
lifespan: Optional[Any] = None,
|
|
124
129
|
enable_mcp_server: bool = False,
|
|
125
130
|
base_app: Optional[FastAPI] = None,
|
|
126
131
|
on_route_conflict: Literal["preserve_agentos", "preserve_base_app", "error"] = "preserve_agentos",
|
|
127
|
-
telemetry: bool = True,
|
|
128
132
|
tracing: bool = False,
|
|
129
133
|
tracing_db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
|
|
130
134
|
auto_provision_dbs: bool = True,
|
|
131
135
|
run_hooks_in_background: bool = False,
|
|
136
|
+
telemetry: bool = True,
|
|
132
137
|
):
|
|
133
138
|
"""Initialize AgentOS.
|
|
134
139
|
|
|
@@ -149,11 +154,15 @@ class AgentOS:
|
|
|
149
154
|
enable_mcp_server: Whether to enable MCP (Model Context Protocol)
|
|
150
155
|
base_app: Optional base FastAPI app to use for the AgentOS. All routes and middleware will be added to this app.
|
|
151
156
|
on_route_conflict: What to do when a route conflict is detected in case a custom base_app is provided.
|
|
152
|
-
|
|
157
|
+
auto_provision_dbs: Whether to automatically provision databases
|
|
158
|
+
authorization: Whether to enable authorization
|
|
159
|
+
authorization_config: Configuration for the authorization middleware
|
|
160
|
+
cors_allowed_origins: List of allowed CORS origins (will be merged with default Agno domains)
|
|
153
161
|
tracing: If True, enables OpenTelemetry tracing for all agents and teams in the OS
|
|
154
162
|
tracing_db: Dedicated database for storing and reading traces. Recommended for multi-db setups.
|
|
155
163
|
If not provided and tracing=True, the first available db from agents/teams/workflows is used.
|
|
156
164
|
run_hooks_in_background: If True, run agent/team pre/post hooks as FastAPI background tasks (non-blocking)
|
|
165
|
+
telemetry: Whether to enable telemetry
|
|
157
166
|
|
|
158
167
|
"""
|
|
159
168
|
if not agents and not workflows and not teams and not knowledge:
|
|
@@ -198,6 +207,13 @@ class AgentOS:
|
|
|
198
207
|
self.enable_mcp_server = enable_mcp_server
|
|
199
208
|
self.lifespan = lifespan
|
|
200
209
|
|
|
210
|
+
# RBAC
|
|
211
|
+
self.authorization = authorization
|
|
212
|
+
self.authorization_config = authorization_config
|
|
213
|
+
|
|
214
|
+
# CORS configuration - merge user-provided origins with defaults from settings
|
|
215
|
+
self.cors_allowed_origins = resolve_origins(cors_allowed_origins, self.settings.cors_origin_list)
|
|
216
|
+
|
|
201
217
|
# If True, run agent/team hooks as FastAPI background tasks
|
|
202
218
|
self.run_hooks_in_background = run_hooks_in_background
|
|
203
219
|
|
|
@@ -209,6 +225,9 @@ class AgentOS:
|
|
|
209
225
|
self._initialize_teams()
|
|
210
226
|
self._initialize_workflows()
|
|
211
227
|
|
|
228
|
+
# Check for duplicate IDs
|
|
229
|
+
self._raise_if_duplicate_ids()
|
|
230
|
+
|
|
212
231
|
if self.tracing:
|
|
213
232
|
self._setup_tracing()
|
|
214
233
|
|
|
@@ -250,6 +269,9 @@ class AgentOS:
|
|
|
250
269
|
self._initialize_agents()
|
|
251
270
|
self._initialize_teams()
|
|
252
271
|
self._initialize_workflows()
|
|
272
|
+
|
|
273
|
+
# Check for duplicate IDs
|
|
274
|
+
self._raise_if_duplicate_ids()
|
|
253
275
|
self._auto_discover_databases()
|
|
254
276
|
self._auto_discover_knowledge_instances()
|
|
255
277
|
|
|
@@ -318,6 +340,31 @@ class AgentOS:
|
|
|
318
340
|
self.interfaces.append(a2a_interface)
|
|
319
341
|
self._add_router(app, a2a_interface.get_router())
|
|
320
342
|
|
|
343
|
+
def _raise_if_duplicate_ids(self) -> None:
|
|
344
|
+
"""Check for duplicate IDs within each entity type.
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
ValueError: If duplicate IDs are found within the same entity type
|
|
348
|
+
"""
|
|
349
|
+
duplicate_ids: List[str] = []
|
|
350
|
+
|
|
351
|
+
for entities in [self.agents, self.teams, self.workflows]:
|
|
352
|
+
if not entities:
|
|
353
|
+
continue
|
|
354
|
+
seen_ids: set[str] = set()
|
|
355
|
+
for entity in entities:
|
|
356
|
+
entity_id = entity.id
|
|
357
|
+
if entity_id is None:
|
|
358
|
+
continue
|
|
359
|
+
if entity_id in seen_ids:
|
|
360
|
+
if entity_id not in duplicate_ids:
|
|
361
|
+
duplicate_ids.append(entity_id)
|
|
362
|
+
else:
|
|
363
|
+
seen_ids.add(entity_id)
|
|
364
|
+
|
|
365
|
+
if duplicate_ids:
|
|
366
|
+
raise ValueError(f"Duplicate IDs found in AgentOS: {', '.join(repr(id_) for id_ in duplicate_ids)}")
|
|
367
|
+
|
|
321
368
|
def _make_app(self, lifespan: Optional[Any] = None) -> FastAPI:
|
|
322
369
|
# Adjust the FastAPI app lifespan to handle MCP connections if relevant
|
|
323
370
|
app_lifespan = lifespan
|
|
@@ -558,10 +605,53 @@ class AgentOS:
|
|
|
558
605
|
)
|
|
559
606
|
|
|
560
607
|
# Update CORS middleware
|
|
561
|
-
update_cors_middleware(fastapi_app, self.
|
|
608
|
+
update_cors_middleware(fastapi_app, self.cors_allowed_origins) # type: ignore
|
|
609
|
+
|
|
610
|
+
# Set agent_os_id and cors_allowed_origins on app state
|
|
611
|
+
# This allows middleware (like JWT) to access these values
|
|
612
|
+
fastapi_app.state.agent_os_id = self.id
|
|
613
|
+
fastapi_app.state.cors_allowed_origins = self.cors_allowed_origins
|
|
614
|
+
|
|
615
|
+
# Add JWT middleware if authorization is enabled
|
|
616
|
+
if self.authorization:
|
|
617
|
+
self._add_jwt_middleware(fastapi_app)
|
|
562
618
|
|
|
563
619
|
return fastapi_app
|
|
564
620
|
|
|
621
|
+
def _add_jwt_middleware(self, fastapi_app: FastAPI) -> None:
|
|
622
|
+
from agno.os.middleware.jwt import JWTMiddleware, JWTValidator
|
|
623
|
+
|
|
624
|
+
verify_audience = False
|
|
625
|
+
jwks_file = None
|
|
626
|
+
verification_keys = None
|
|
627
|
+
algorithm = "RS256"
|
|
628
|
+
|
|
629
|
+
if self.authorization_config:
|
|
630
|
+
algorithm = self.authorization_config.algorithm or "RS256"
|
|
631
|
+
verification_keys = self.authorization_config.verification_keys
|
|
632
|
+
jwks_file = self.authorization_config.jwks_file
|
|
633
|
+
verify_audience = self.authorization_config.verify_audience or False
|
|
634
|
+
|
|
635
|
+
log_info(f"Adding JWT middleware for authorization (algorithm: {algorithm})")
|
|
636
|
+
|
|
637
|
+
# Create validator and store on app.state for WebSocket access
|
|
638
|
+
jwt_validator = JWTValidator(
|
|
639
|
+
verification_keys=verification_keys,
|
|
640
|
+
jwks_file=jwks_file,
|
|
641
|
+
algorithm=algorithm,
|
|
642
|
+
)
|
|
643
|
+
fastapi_app.state.jwt_validator = jwt_validator
|
|
644
|
+
|
|
645
|
+
# Add middleware to stack
|
|
646
|
+
fastapi_app.add_middleware(
|
|
647
|
+
JWTMiddleware,
|
|
648
|
+
verification_keys=verification_keys,
|
|
649
|
+
jwks_file=jwks_file,
|
|
650
|
+
algorithm=algorithm,
|
|
651
|
+
authorization=self.authorization,
|
|
652
|
+
verify_audience=verify_audience,
|
|
653
|
+
)
|
|
654
|
+
|
|
565
655
|
def get_routes(self) -> List[Any]:
|
|
566
656
|
"""Retrieve all routes from the FastAPI app.
|
|
567
657
|
|
|
@@ -922,6 +1012,8 @@ class AgentOS:
|
|
|
922
1012
|
host: str = "localhost",
|
|
923
1013
|
port: int = 7777,
|
|
924
1014
|
reload: bool = False,
|
|
1015
|
+
reload_includes: Optional[List[str]] = None,
|
|
1016
|
+
reload_excludes: Optional[List[str]] = None,
|
|
925
1017
|
workers: Optional[int] = None,
|
|
926
1018
|
access_log: bool = False,
|
|
927
1019
|
**kwargs,
|
|
@@ -956,4 +1048,19 @@ class AgentOS:
|
|
|
956
1048
|
)
|
|
957
1049
|
)
|
|
958
1050
|
|
|
959
|
-
|
|
1051
|
+
# Adding *.yaml to reload_includes to reload the app when the yaml config file changes.
|
|
1052
|
+
if reload and reload_includes is not None:
|
|
1053
|
+
reload_includes = ["*.yaml", "*.yml"]
|
|
1054
|
+
|
|
1055
|
+
uvicorn.run(
|
|
1056
|
+
app=app,
|
|
1057
|
+
host=host,
|
|
1058
|
+
port=port,
|
|
1059
|
+
reload=reload,
|
|
1060
|
+
reload_includes=reload_includes,
|
|
1061
|
+
reload_excludes=reload_excludes,
|
|
1062
|
+
workers=workers,
|
|
1063
|
+
access_log=access_log,
|
|
1064
|
+
lifespan="on",
|
|
1065
|
+
**kwargs,
|
|
1066
|
+
)
|
agno/os/auth.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import List, Set
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends, HTTPException, Request
|
|
2
4
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
3
5
|
|
|
6
|
+
from agno.os.scopes import get_accessible_resource_ids
|
|
4
7
|
from agno.os.settings import AgnoAPISettings
|
|
5
8
|
|
|
6
9
|
# Create a global HTTPBearer instance
|
|
@@ -18,7 +21,7 @@ def get_authentication_dependency(settings: AgnoAPISettings):
|
|
|
18
21
|
A dependency function that can be used with FastAPI's Depends()
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
|
-
def auth_dependency(credentials: HTTPAuthorizationCredentials = Depends(security)) -> bool:
|
|
24
|
+
async def auth_dependency(credentials: HTTPAuthorizationCredentials = Depends(security)) -> bool:
|
|
22
25
|
# If no security key is set, skip authentication entirely
|
|
23
26
|
if not settings or not settings.os_security_key:
|
|
24
27
|
return True
|
|
@@ -40,7 +43,7 @@ def get_authentication_dependency(settings: AgnoAPISettings):
|
|
|
40
43
|
|
|
41
44
|
def validate_websocket_token(token: str, settings: AgnoAPISettings) -> bool:
|
|
42
45
|
"""
|
|
43
|
-
Validate a bearer token for WebSocket authentication.
|
|
46
|
+
Validate a bearer token for WebSocket authentication (legacy os_security_key method).
|
|
44
47
|
|
|
45
48
|
Args:
|
|
46
49
|
token: The bearer token to validate
|
|
@@ -55,3 +58,187 @@ def validate_websocket_token(token: str, settings: AgnoAPISettings) -> bool:
|
|
|
55
58
|
|
|
56
59
|
# Verify the token matches the configured security key
|
|
57
60
|
return token == settings.os_security_key
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_accessible_resources(request: Request, resource_type: str) -> Set[str]:
|
|
64
|
+
"""
|
|
65
|
+
Get the set of resource IDs the user has access to based on their scopes.
|
|
66
|
+
|
|
67
|
+
This function is used to filter lists of resources (agents, teams, workflows)
|
|
68
|
+
based on the user's scopes from their JWT token.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
request: The FastAPI request object (contains request.state.scopes)
|
|
72
|
+
resource_type: Type of resource ("agents", "teams", "workflows")
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Set of resource IDs the user can access. Returns {"*"} for wildcard access.
|
|
76
|
+
|
|
77
|
+
Usage:
|
|
78
|
+
accessible_ids = get_accessible_resources(request, "agents")
|
|
79
|
+
if "*" not in accessible_ids:
|
|
80
|
+
agents = [a for a in agents if a.id in accessible_ids]
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
>>> # User with specific agent access
|
|
84
|
+
>>> # Token scopes: ["agent-os:my-os:agents:my-agent:read"]
|
|
85
|
+
>>> get_accessible_resources(request, "agents")
|
|
86
|
+
{'my-agent'}
|
|
87
|
+
|
|
88
|
+
>>> # User with wildcard access
|
|
89
|
+
>>> # Token scopes: ["agent-os:my-os:agents:*:read"] or ["admin"]
|
|
90
|
+
>>> get_accessible_resources(request, "agents")
|
|
91
|
+
{'*'}
|
|
92
|
+
|
|
93
|
+
>>> # User with agent-os level access (global resource scope)
|
|
94
|
+
>>> # Token scopes: ["agent-os:my-os:agents:read"]
|
|
95
|
+
>>> get_accessible_resources(request, "agents")
|
|
96
|
+
{'*'}
|
|
97
|
+
"""
|
|
98
|
+
# Check if accessible_resource_ids is already cached in request state (set by JWT middleware)
|
|
99
|
+
# This happens when user doesn't have global scope but has specific resource scopes
|
|
100
|
+
cached_ids = getattr(request.state, "accessible_resource_ids", None)
|
|
101
|
+
if cached_ids is not None:
|
|
102
|
+
return cached_ids
|
|
103
|
+
|
|
104
|
+
# Get user's scopes from request state (set by JWT middleware)
|
|
105
|
+
user_scopes = getattr(request.state, "scopes", [])
|
|
106
|
+
|
|
107
|
+
# Get accessible resource IDs
|
|
108
|
+
accessible_ids = get_accessible_resource_ids(user_scopes=user_scopes, resource_type=resource_type)
|
|
109
|
+
|
|
110
|
+
return accessible_ids
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def filter_resources_by_access(request: Request, resources: List, resource_type: str) -> List:
|
|
114
|
+
"""
|
|
115
|
+
Filter a list of resources based on user's access permissions.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
request: The FastAPI request object
|
|
119
|
+
resources: List of resource objects (agents, teams, or workflows) with 'id' attribute
|
|
120
|
+
resource_type: Type of resource ("agents", "teams", "workflows")
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Filtered list of resources the user has access to
|
|
124
|
+
|
|
125
|
+
Usage:
|
|
126
|
+
agents = filter_resources_by_access(request, all_agents, "agents")
|
|
127
|
+
teams = filter_resources_by_access(request, all_teams, "teams")
|
|
128
|
+
workflows = filter_resources_by_access(request, all_workflows, "workflows")
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
>>> # User with specific access
|
|
132
|
+
>>> agents = [Agent(id="agent-1"), Agent(id="agent-2"), Agent(id="agent-3")]
|
|
133
|
+
>>> # Token scopes: ["agent-os:my-os:agents:agent-1:read", "agent-os:my-os:agents:agent-2:read"]
|
|
134
|
+
>>> filter_resources_by_access(request, agents, "agents")
|
|
135
|
+
[Agent(id="agent-1"), Agent(id="agent-2")]
|
|
136
|
+
|
|
137
|
+
>>> # User with wildcard access
|
|
138
|
+
>>> # Token scopes: ["admin"]
|
|
139
|
+
>>> filter_resources_by_access(request, agents, "agents")
|
|
140
|
+
[Agent(id="agent-1"), Agent(id="agent-2"), Agent(id="agent-3")]
|
|
141
|
+
"""
|
|
142
|
+
accessible_ids = get_accessible_resources(request, resource_type)
|
|
143
|
+
|
|
144
|
+
# Wildcard access - return all resources
|
|
145
|
+
if "*" in accessible_ids:
|
|
146
|
+
return resources
|
|
147
|
+
|
|
148
|
+
# Filter to only accessible resources
|
|
149
|
+
return [r for r in resources if r.id in accessible_ids]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def check_resource_access(request: Request, resource_id: str, resource_type: str, action: str = "read") -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Check if user has access to a specific resource.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
request: The FastAPI request object
|
|
158
|
+
resource_id: ID of the resource to check
|
|
159
|
+
resource_type: Type of resource ("agents", "teams", "workflows")
|
|
160
|
+
action: Action to check ("read", "run", etc.)
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
True if user has access, False otherwise
|
|
164
|
+
|
|
165
|
+
Usage:
|
|
166
|
+
if not check_resource_access(request, agent_id, "agents", "run"):
|
|
167
|
+
raise HTTPException(status_code=403, detail="Access denied")
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
>>> # Token scopes: ["agent-os:my-os:agents:my-agent:read", "agent-os:my-os:agents:my-agent:run"]
|
|
171
|
+
>>> check_resource_access(request, "my-agent", "agents", "run")
|
|
172
|
+
True
|
|
173
|
+
|
|
174
|
+
>>> check_resource_access(request, "other-agent", "agents", "run")
|
|
175
|
+
False
|
|
176
|
+
"""
|
|
177
|
+
accessible_ids = get_accessible_resources(request, resource_type)
|
|
178
|
+
|
|
179
|
+
# Wildcard access grants all permissions
|
|
180
|
+
if "*" in accessible_ids:
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
# Check if user has access to this specific resource
|
|
184
|
+
return resource_id in accessible_ids
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def require_resource_access(resource_type: str, action: str, resource_id_param: str):
|
|
188
|
+
"""
|
|
189
|
+
Create a dependency that checks if the user has access to a specific resource.
|
|
190
|
+
|
|
191
|
+
This dependency factory creates a FastAPI dependency that automatically checks
|
|
192
|
+
authorization when authorization is enabled. It extracts the resource ID from
|
|
193
|
+
the path parameters and verifies the user has the required access.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
resource_type: Type of resource ("agents", "teams", "workflows")
|
|
197
|
+
action: Action to check ("read", "run")
|
|
198
|
+
resource_id_param: Name of the path parameter containing the resource ID
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
A dependency function for use with FastAPI's Depends()
|
|
202
|
+
|
|
203
|
+
Usage:
|
|
204
|
+
@router.post("/agents/{agent_id}/runs")
|
|
205
|
+
async def create_agent_run(
|
|
206
|
+
agent_id: str,
|
|
207
|
+
request: Request,
|
|
208
|
+
_: None = Depends(require_resource_access("agents", "run", "agent_id")),
|
|
209
|
+
):
|
|
210
|
+
...
|
|
211
|
+
|
|
212
|
+
@router.get("/agents/{agent_id}")
|
|
213
|
+
async def get_agent(
|
|
214
|
+
agent_id: str,
|
|
215
|
+
request: Request,
|
|
216
|
+
_: None = Depends(require_resource_access("agents", "read", "agent_id")),
|
|
217
|
+
):
|
|
218
|
+
...
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
>>> # Creates dependency for checking agent run access
|
|
222
|
+
>>> dep = require_resource_access("agents", "run", "agent_id")
|
|
223
|
+
|
|
224
|
+
>>> # Creates dependency for checking team read access
|
|
225
|
+
>>> dep = require_resource_access("teams", "read", "team_id")
|
|
226
|
+
"""
|
|
227
|
+
# Map resource_type to singular form for error messages
|
|
228
|
+
resource_singular = {
|
|
229
|
+
"agents": "agent",
|
|
230
|
+
"teams": "team",
|
|
231
|
+
"workflows": "workflow",
|
|
232
|
+
}.get(resource_type, resource_type.rstrip("s"))
|
|
233
|
+
|
|
234
|
+
async def dependency(request: Request):
|
|
235
|
+
# Only check authorization if it's enabled
|
|
236
|
+
if not getattr(request.state, "authorization_enabled", False):
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
# Get the resource_id from path parameters
|
|
240
|
+
resource_id = request.path_params.get(resource_id_param)
|
|
241
|
+
if resource_id and not check_resource_access(request, resource_id, resource_type, action):
|
|
242
|
+
raise HTTPException(status_code=403, detail=f"Access denied to {action} this {resource_singular}")
|
|
243
|
+
|
|
244
|
+
return dependency
|
agno/os/config.py
CHANGED
|
@@ -5,6 +5,15 @@ from typing import Generic, List, Optional, TypeVar
|
|
|
5
5
|
from pydantic import BaseModel, field_validator
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
class AuthorizationConfig(BaseModel):
|
|
9
|
+
"""Configuration for the JWT middleware"""
|
|
10
|
+
|
|
11
|
+
verification_keys: Optional[List[str]] = None
|
|
12
|
+
jwks_file: Optional[str] = None
|
|
13
|
+
algorithm: Optional[str] = None
|
|
14
|
+
verify_audience: Optional[bool] = None
|
|
15
|
+
|
|
16
|
+
|
|
8
17
|
class EvalsDomainConfig(BaseModel):
|
|
9
18
|
"""Configuration for the Evals domain of the AgentOS"""
|
|
10
19
|
|