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/os/scopes.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""AgentOS RBAC Scopes
|
|
2
|
+
|
|
3
|
+
This module defines all available permission scopes for AgentOS RBAC (Role-Based Access Control).
|
|
4
|
+
|
|
5
|
+
Scope Format:
|
|
6
|
+
- Global resource scopes: `resource:action`
|
|
7
|
+
- Per-resource scopes: `resource:<resource-id>:action`
|
|
8
|
+
- Wildcards: `resource:*:action` for any resource
|
|
9
|
+
|
|
10
|
+
The AgentOS ID is verified via the JWT `aud` (audience) claim.
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
- `system:read` - Read system config
|
|
14
|
+
- `agents:read` - List all agents
|
|
15
|
+
- `agents:web-agent:read` - Read specific agent
|
|
16
|
+
- `agents:web-agent:run` - Run specific agent
|
|
17
|
+
- `agents:*:run` - Run any agent (wildcard)
|
|
18
|
+
- `agent_os:admin` - Full access to everything
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from typing import Dict, List, Optional, Set
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentOSScope(str, Enum):
|
|
27
|
+
"""
|
|
28
|
+
Enum of all available AgentOS permission scopes.
|
|
29
|
+
|
|
30
|
+
Special Scopes:
|
|
31
|
+
- ADMIN: Grants full access to all endpoints (agent_os:admin)
|
|
32
|
+
|
|
33
|
+
Scope format:
|
|
34
|
+
|
|
35
|
+
Global Resource Scopes:
|
|
36
|
+
- system:read - System configuration and model information
|
|
37
|
+
- agents:read - List all agents
|
|
38
|
+
- teams:read - List all teams
|
|
39
|
+
- workflows:read - List all workflows
|
|
40
|
+
- sessions:read - View session data
|
|
41
|
+
- sessions:write - Create and update sessions
|
|
42
|
+
- sessions:delete - Delete sessions
|
|
43
|
+
- memories:read - View memories
|
|
44
|
+
- memories:write - Create and update memories
|
|
45
|
+
- memories:delete - Delete memories
|
|
46
|
+
- knowledge:read - View and search knowledge
|
|
47
|
+
- knowledge:write - Add and update knowledge
|
|
48
|
+
- knowledge:delete - Delete knowledge
|
|
49
|
+
- metrics:read - View metrics
|
|
50
|
+
- metrics:write - Refresh metrics
|
|
51
|
+
- evals:read - View evaluation runs
|
|
52
|
+
- evals:write - Create and update evaluation runs
|
|
53
|
+
- evals:delete - Delete evaluation runs
|
|
54
|
+
- traces:read - View traces and trace statistics
|
|
55
|
+
|
|
56
|
+
Per-Resource Scopes (with resource ID):
|
|
57
|
+
- agents:<agent-id>:read - Read specific agent
|
|
58
|
+
- agents:<agent-id>:run - Run specific agent
|
|
59
|
+
- teams:<team-id>:read - Read specific team
|
|
60
|
+
- teams:<team-id>:run - Run specific team
|
|
61
|
+
- workflows:<workflow-id>:read - Read specific workflow
|
|
62
|
+
- workflows:<workflow-id>:run - Run specific workflow
|
|
63
|
+
|
|
64
|
+
Wildcards:
|
|
65
|
+
- agents:*:run - Run any agent
|
|
66
|
+
- teams:*:run - Run any team
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Special scopes
|
|
70
|
+
ADMIN = "agent_os:admin"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class ParsedScope:
|
|
75
|
+
"""Represents a parsed scope with its components."""
|
|
76
|
+
|
|
77
|
+
raw: str
|
|
78
|
+
scope_type: str # "admin", "global", "per_resource", or "unknown"
|
|
79
|
+
resource: Optional[str] = None
|
|
80
|
+
resource_id: Optional[str] = None
|
|
81
|
+
action: Optional[str] = None
|
|
82
|
+
is_wildcard_resource: bool = False
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def is_global_resource_scope(self) -> bool:
|
|
86
|
+
"""Check if this scope targets all resources of a type (no resource_id)."""
|
|
87
|
+
return self.scope_type == "global"
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def is_per_resource_scope(self) -> bool:
|
|
91
|
+
"""Check if this scope targets a specific resource (has resource_id)."""
|
|
92
|
+
return self.scope_type == "per_resource"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse_scope(scope: str, admin_scope: Optional[str] = None) -> ParsedScope:
|
|
96
|
+
"""
|
|
97
|
+
Parse a scope string into its components.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
scope: The scope string to parse
|
|
101
|
+
admin_scope: The scope string that grants admin access (default: "agent_os:admin")
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
ParsedScope object with parsed components
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
>>> parse_scope("agent_os:admin")
|
|
108
|
+
ParsedScope(raw="agent_os:admin", scope_type="admin")
|
|
109
|
+
|
|
110
|
+
>>> parse_scope("system:read")
|
|
111
|
+
ParsedScope(raw="system:read", scope_type="global", resource="system", action="read")
|
|
112
|
+
|
|
113
|
+
>>> parse_scope("agents:web-agent:read")
|
|
114
|
+
ParsedScope(raw="...", scope_type="per_resource", resource="agents", resource_id="web-agent", action="read")
|
|
115
|
+
|
|
116
|
+
>>> parse_scope("agents:*:run")
|
|
117
|
+
ParsedScope(raw="...", scope_type="per_resource", resource="agents", resource_id="*", action="run", is_wildcard_resource=True)
|
|
118
|
+
"""
|
|
119
|
+
effective_admin_scope = admin_scope or AgentOSScope.ADMIN.value
|
|
120
|
+
if scope == effective_admin_scope:
|
|
121
|
+
return ParsedScope(raw=scope, scope_type="admin")
|
|
122
|
+
|
|
123
|
+
parts = scope.split(":")
|
|
124
|
+
|
|
125
|
+
# Global resource scope: resource:action (2 parts)
|
|
126
|
+
if len(parts) == 2:
|
|
127
|
+
return ParsedScope(
|
|
128
|
+
raw=scope,
|
|
129
|
+
scope_type="global",
|
|
130
|
+
resource=parts[0],
|
|
131
|
+
action=parts[1],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Per-resource scope: resource:<resource-id>:action (3 parts)
|
|
135
|
+
if len(parts) == 3:
|
|
136
|
+
resource_id = parts[1]
|
|
137
|
+
is_wildcard_resource = resource_id == "*"
|
|
138
|
+
|
|
139
|
+
return ParsedScope(
|
|
140
|
+
raw=scope,
|
|
141
|
+
scope_type="per_resource",
|
|
142
|
+
resource=parts[0],
|
|
143
|
+
resource_id=resource_id,
|
|
144
|
+
action=parts[2],
|
|
145
|
+
is_wildcard_resource=is_wildcard_resource,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Invalid format
|
|
149
|
+
return ParsedScope(raw=scope, scope_type="unknown")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def matches_scope(
|
|
153
|
+
user_scope: ParsedScope,
|
|
154
|
+
required_scope: ParsedScope,
|
|
155
|
+
resource_id: Optional[str] = None,
|
|
156
|
+
) -> bool:
|
|
157
|
+
"""
|
|
158
|
+
Check if a user's scope matches a required scope.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
user_scope: The user's parsed scope
|
|
162
|
+
required_scope: The required parsed scope
|
|
163
|
+
resource_id: The specific resource ID being accessed
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if the user's scope satisfies the required scope
|
|
167
|
+
|
|
168
|
+
Examples:
|
|
169
|
+
>>> user = parse_scope("system:read")
|
|
170
|
+
>>> required = parse_scope("system:read")
|
|
171
|
+
>>> matches_scope(user, required)
|
|
172
|
+
True
|
|
173
|
+
|
|
174
|
+
>>> user = parse_scope("agents:web-agent:run")
|
|
175
|
+
>>> required = parse_scope("agents:<id>:run")
|
|
176
|
+
>>> matches_scope(user, required, resource_id="web-agent")
|
|
177
|
+
True
|
|
178
|
+
|
|
179
|
+
>>> user = parse_scope("agents:*:run")
|
|
180
|
+
>>> required = parse_scope("agents:<id>:run")
|
|
181
|
+
>>> matches_scope(user, required, resource_id="web-agent")
|
|
182
|
+
True
|
|
183
|
+
"""
|
|
184
|
+
# Admin always matches
|
|
185
|
+
if user_scope.scope_type == "admin":
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
# Unknown scopes don't match anything
|
|
189
|
+
if user_scope.scope_type == "unknown" or required_scope.scope_type == "unknown":
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
# Resource type must match
|
|
193
|
+
if user_scope.resource != required_scope.resource:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
# Action must match
|
|
197
|
+
if user_scope.action != required_scope.action:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
# If required scope has a resource_id, check it
|
|
201
|
+
if required_scope.resource_id:
|
|
202
|
+
# User has wildcard resource access
|
|
203
|
+
if user_scope.is_wildcard_resource:
|
|
204
|
+
return True
|
|
205
|
+
# User has global resource access (no resource_id in user scope)
|
|
206
|
+
if not user_scope.resource_id:
|
|
207
|
+
return True
|
|
208
|
+
# User has specific resource access - must match
|
|
209
|
+
return user_scope.resource_id == resource_id
|
|
210
|
+
|
|
211
|
+
# Required scope is global (no resource_id), user scope matches if:
|
|
212
|
+
# - User has global scope (no resource_id), OR
|
|
213
|
+
# - User has wildcard resource scope
|
|
214
|
+
return not user_scope.resource_id or user_scope.is_wildcard_resource
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def has_required_scopes(
|
|
218
|
+
user_scopes: List[str],
|
|
219
|
+
required_scopes: List[str],
|
|
220
|
+
resource_type: Optional[str] = None,
|
|
221
|
+
resource_id: Optional[str] = None,
|
|
222
|
+
admin_scope: Optional[str] = None,
|
|
223
|
+
) -> bool:
|
|
224
|
+
"""
|
|
225
|
+
Check if user has all required scopes.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
user_scopes: List of scope strings the user has
|
|
229
|
+
required_scopes: List of scope strings required
|
|
230
|
+
resource_type: Type of resource being accessed ("agents", "teams", "workflows")
|
|
231
|
+
resource_id: Specific resource ID being accessed
|
|
232
|
+
admin_scope: The scope string that grants admin access (default: "agent_os:admin")
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
True if user has all required scopes
|
|
236
|
+
|
|
237
|
+
Examples:
|
|
238
|
+
>>> has_required_scopes(
|
|
239
|
+
... ["agents:read"],
|
|
240
|
+
... ["agents:read"],
|
|
241
|
+
... )
|
|
242
|
+
True
|
|
243
|
+
|
|
244
|
+
>>> has_required_scopes(
|
|
245
|
+
... ["agents:web-agent:run"],
|
|
246
|
+
... ["agents:run"],
|
|
247
|
+
... resource_type="agents",
|
|
248
|
+
... resource_id="web-agent"
|
|
249
|
+
... )
|
|
250
|
+
True
|
|
251
|
+
|
|
252
|
+
>>> has_required_scopes(
|
|
253
|
+
... ["agents:*:run"],
|
|
254
|
+
... ["agents:run"],
|
|
255
|
+
... resource_type="agents",
|
|
256
|
+
... resource_id="any-agent"
|
|
257
|
+
... )
|
|
258
|
+
True
|
|
259
|
+
"""
|
|
260
|
+
if not required_scopes:
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
# Parse user scopes once
|
|
264
|
+
parsed_user_scopes = [parse_scope(scope, admin_scope=admin_scope) for scope in user_scopes]
|
|
265
|
+
|
|
266
|
+
# Check for admin scope
|
|
267
|
+
if any(s.scope_type == "admin" for s in parsed_user_scopes):
|
|
268
|
+
return True
|
|
269
|
+
|
|
270
|
+
# Check each required scope
|
|
271
|
+
for required_scope_str in required_scopes:
|
|
272
|
+
parts = required_scope_str.split(":")
|
|
273
|
+
if len(parts) == 2:
|
|
274
|
+
resource, action = parts
|
|
275
|
+
# Build the required scope based on context
|
|
276
|
+
if resource_id and resource_type:
|
|
277
|
+
# Per-resource scope required
|
|
278
|
+
full_required_scope = f"{resource_type}:<resource-id>:{action}"
|
|
279
|
+
else:
|
|
280
|
+
# Global resource scope required
|
|
281
|
+
full_required_scope = required_scope_str
|
|
282
|
+
|
|
283
|
+
required = parse_scope(full_required_scope, admin_scope=admin_scope)
|
|
284
|
+
else:
|
|
285
|
+
required = parse_scope(required_scope_str, admin_scope=admin_scope)
|
|
286
|
+
|
|
287
|
+
scope_matched = False
|
|
288
|
+
for user_scope in parsed_user_scopes:
|
|
289
|
+
if matches_scope(user_scope, required, resource_id=resource_id):
|
|
290
|
+
scope_matched = True
|
|
291
|
+
break
|
|
292
|
+
|
|
293
|
+
if not scope_matched:
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
return True
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def get_accessible_resource_ids(
|
|
300
|
+
user_scopes: List[str],
|
|
301
|
+
resource_type: str,
|
|
302
|
+
admin_scope: Optional[str] = None,
|
|
303
|
+
) -> Set[str]:
|
|
304
|
+
"""
|
|
305
|
+
Get the set of resource IDs the user has access to.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
user_scopes: List of scope strings the user has
|
|
309
|
+
resource_type: Type of resource ("agents", "teams", "workflows")
|
|
310
|
+
admin_scope: The scope string that grants admin access (default: "agent_os:admin")
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Set of resource IDs the user can access. Returns {"*"} for wildcard access.
|
|
314
|
+
|
|
315
|
+
Examples:
|
|
316
|
+
>>> get_accessible_resource_ids(
|
|
317
|
+
... ["agents:agent-1:read", "agents:agent-2:read"],
|
|
318
|
+
... "agents"
|
|
319
|
+
... )
|
|
320
|
+
{'agent-1', 'agent-2'}
|
|
321
|
+
|
|
322
|
+
>>> get_accessible_resource_ids(["agents:*:read"], "agents")
|
|
323
|
+
{'*'}
|
|
324
|
+
|
|
325
|
+
>>> get_accessible_resource_ids(["agents:read"], "agents")
|
|
326
|
+
{'*'}
|
|
327
|
+
|
|
328
|
+
>>> get_accessible_resource_ids(["admin"], "agents")
|
|
329
|
+
{'*'}
|
|
330
|
+
"""
|
|
331
|
+
parsed_scopes = [parse_scope(scope, admin_scope=admin_scope) for scope in user_scopes]
|
|
332
|
+
|
|
333
|
+
# Check for admin or global wildcard access
|
|
334
|
+
for scope in parsed_scopes:
|
|
335
|
+
if scope.scope_type == "admin":
|
|
336
|
+
return {"*"}
|
|
337
|
+
|
|
338
|
+
# Check if resource type matches
|
|
339
|
+
if scope.resource == resource_type:
|
|
340
|
+
# Global resource scope (no resource_id) grants access to all
|
|
341
|
+
if not scope.resource_id and scope.action in ["read", "run"]:
|
|
342
|
+
return {"*"}
|
|
343
|
+
# Wildcard resource scope grants access to all
|
|
344
|
+
if scope.is_wildcard_resource and scope.action in ["read", "run"]:
|
|
345
|
+
return {"*"}
|
|
346
|
+
|
|
347
|
+
# Collect specific resource IDs
|
|
348
|
+
accessible_ids: Set[str] = set()
|
|
349
|
+
for scope in parsed_scopes:
|
|
350
|
+
# Check if resource type matches
|
|
351
|
+
if scope.resource == resource_type:
|
|
352
|
+
# Specific resource ID
|
|
353
|
+
if scope.resource_id and not scope.is_wildcard_resource and scope.action in ["read", "run"]:
|
|
354
|
+
accessible_ids.add(scope.resource_id)
|
|
355
|
+
|
|
356
|
+
return accessible_ids
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def get_default_scope_mappings() -> Dict[str, List[str]]:
|
|
360
|
+
"""
|
|
361
|
+
Get default scope mappings for AgentOS endpoints.
|
|
362
|
+
|
|
363
|
+
Returns a dictionary mapping route patterns (with HTTP methods) to required scope templates.
|
|
364
|
+
Format: "METHOD /path/pattern": ["resource:action"]
|
|
365
|
+
"""
|
|
366
|
+
return {
|
|
367
|
+
# System endpoints
|
|
368
|
+
"GET /config": ["system:read"],
|
|
369
|
+
"GET /models": ["system:read"],
|
|
370
|
+
# Agent endpoints
|
|
371
|
+
"GET /agents": ["agents:read"],
|
|
372
|
+
"GET /agents/*": ["agents:read"],
|
|
373
|
+
"POST /agents": ["agents:write"],
|
|
374
|
+
"PATCH /agents/*": ["agents:write"],
|
|
375
|
+
"DELETE /agents/*": ["agents:delete"],
|
|
376
|
+
"POST /agents/*/runs": ["agents:run"],
|
|
377
|
+
"POST /agents/*/runs/*/continue": ["agents:run"],
|
|
378
|
+
"POST /agents/*/runs/*/cancel": ["agents:run"],
|
|
379
|
+
# Team endpoints
|
|
380
|
+
"GET /teams": ["teams:read"],
|
|
381
|
+
"GET /teams/*": ["teams:read"],
|
|
382
|
+
"POST /teams": ["teams:write"],
|
|
383
|
+
"PATCH /teams/*": ["teams:write"],
|
|
384
|
+
"DELETE /teams/*": ["teams:delete"],
|
|
385
|
+
"POST /teams/*/runs": ["teams:run"],
|
|
386
|
+
"POST /teams/*/runs/*/continue": ["teams:run"],
|
|
387
|
+
"POST /teams/*/runs/*/cancel": ["teams:run"],
|
|
388
|
+
# Workflow endpoints
|
|
389
|
+
"GET /workflows": ["workflows:read"],
|
|
390
|
+
"GET /workflows/*": ["workflows:read"],
|
|
391
|
+
"POST /workflows": ["workflows:write"],
|
|
392
|
+
"PATCH /workflows/*": ["workflows:write"],
|
|
393
|
+
"DELETE /workflows/*": ["workflows:delete"],
|
|
394
|
+
"POST /workflows/*/runs": ["workflows:run"],
|
|
395
|
+
"POST /workflows/*/runs/*/continue": ["workflows:run"],
|
|
396
|
+
"POST /workflows/*/runs/*/cancel": ["workflows:run"],
|
|
397
|
+
# Session endpoints
|
|
398
|
+
"GET /sessions": ["sessions:read"],
|
|
399
|
+
"GET /sessions/*": ["sessions:read"],
|
|
400
|
+
"POST /sessions": ["sessions:write"],
|
|
401
|
+
"POST /sessions/*/rename": ["sessions:write"],
|
|
402
|
+
"PATCH /sessions/*": ["sessions:write"],
|
|
403
|
+
"DELETE /sessions": ["sessions:delete"],
|
|
404
|
+
"DELETE /sessions/*": ["sessions:delete"],
|
|
405
|
+
# Memory endpoints
|
|
406
|
+
"GET /memories": ["memories:read"],
|
|
407
|
+
"GET /memories/*": ["memories:read"],
|
|
408
|
+
"GET /memory_topics": ["memories:read"],
|
|
409
|
+
"GET /user_memory_stats": ["memories:read"],
|
|
410
|
+
"POST /memories": ["memories:write"],
|
|
411
|
+
"PATCH /memories/*": ["memories:write"],
|
|
412
|
+
"DELETE /memories": ["memories:delete"],
|
|
413
|
+
"DELETE /memories/*": ["memories:delete"],
|
|
414
|
+
"POST /optimize-memories": ["memories:write"],
|
|
415
|
+
# Knowledge endpoints
|
|
416
|
+
"GET /knowledge/content": ["knowledge:read"],
|
|
417
|
+
"GET /knowledge/content/*": ["knowledge:read"],
|
|
418
|
+
"GET /knowledge/config": ["knowledge:read"],
|
|
419
|
+
"POST /knowledge/content": ["knowledge:write"],
|
|
420
|
+
"PATCH /knowledge/content/*": ["knowledge:write"],
|
|
421
|
+
"POST /knowledge/search": ["knowledge:read"],
|
|
422
|
+
"DELETE /knowledge/content": ["knowledge:delete"],
|
|
423
|
+
"DELETE /knowledge/content/*": ["knowledge:delete"],
|
|
424
|
+
# Metrics endpoints
|
|
425
|
+
"GET /metrics": ["metrics:read"],
|
|
426
|
+
"POST /metrics/refresh": ["metrics:write"],
|
|
427
|
+
# Evaluation endpoints
|
|
428
|
+
"GET /eval-runs": ["evals:read"],
|
|
429
|
+
"GET /eval-runs/*": ["evals:read"],
|
|
430
|
+
"POST /eval-runs": ["evals:write"],
|
|
431
|
+
"PATCH /eval-runs/*": ["evals:write"],
|
|
432
|
+
"DELETE /eval-runs": ["evals:delete"],
|
|
433
|
+
# Trace endpoints
|
|
434
|
+
"GET /traces": ["traces:read"],
|
|
435
|
+
"GET /traces/*": ["traces:read"],
|
|
436
|
+
"GET /trace_session_stats": ["traces:read"],
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def get_scope_value(scope: AgentOSScope) -> str:
|
|
441
|
+
"""
|
|
442
|
+
Get the string value of a scope.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
scope: The AgentOSScope enum value
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
The string value of the scope
|
|
449
|
+
|
|
450
|
+
Example:
|
|
451
|
+
>>> get_scope_value(AgentOSScope.ADMIN)
|
|
452
|
+
'admin'
|
|
453
|
+
"""
|
|
454
|
+
return scope.value
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def get_all_scopes() -> list[str]:
|
|
458
|
+
"""
|
|
459
|
+
Get a list of all available scope strings.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
List of all scope string values
|
|
463
|
+
|
|
464
|
+
Example:
|
|
465
|
+
>>> scopes = get_all_scopes()
|
|
466
|
+
>>> 'admin' in scopes
|
|
467
|
+
True
|
|
468
|
+
"""
|
|
469
|
+
return [scope.value for scope in AgentOSScope]
|
agno/os/utils.py
CHANGED
|
@@ -26,12 +26,21 @@ from agno.workflow.workflow import Workflow
|
|
|
26
26
|
|
|
27
27
|
async def get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict[str, Any]:
|
|
28
28
|
"""Given a Request and an endpoint function, return a dictionary with all extra form data fields.
|
|
29
|
+
|
|
29
30
|
Args:
|
|
30
31
|
request: The FastAPI Request object
|
|
31
32
|
endpoint_func: The function exposing the endpoint that received the request
|
|
32
33
|
|
|
34
|
+
Supported form parameters:
|
|
35
|
+
- session_state: JSON string of session state dict
|
|
36
|
+
- dependencies: JSON string of dependencies dict
|
|
37
|
+
- metadata: JSON string of metadata dict
|
|
38
|
+
- knowledge_filters: JSON string of knowledge filters
|
|
39
|
+
- output_schema: JSON schema string (converted to Pydantic model by default)
|
|
40
|
+
- use_json_schema: If "true", keeps output_schema as dict instead of converting to Pydantic model
|
|
41
|
+
|
|
33
42
|
Returns:
|
|
34
|
-
A dictionary of kwargs
|
|
43
|
+
A dictionary of kwargs to pass to Agent/Team run methods
|
|
35
44
|
"""
|
|
36
45
|
import inspect
|
|
37
46
|
|
|
@@ -101,15 +110,25 @@ async def get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict[
|
|
|
101
110
|
kwargs.pop("knowledge_filters")
|
|
102
111
|
log_warning(f"Invalid FilterExpr in knowledge_filters: {e}")
|
|
103
112
|
|
|
104
|
-
# Handle output_schema - convert JSON schema to
|
|
113
|
+
# Handle output_schema - convert JSON schema to Pydantic model or keep as dict
|
|
114
|
+
# use_json_schema is a control flag consumed here (not passed to Agent/Team)
|
|
115
|
+
# When true, output_schema stays as dict for direct JSON output
|
|
116
|
+
use_json_schema = kwargs.pop("use_json_schema", False)
|
|
117
|
+
if isinstance(use_json_schema, str):
|
|
118
|
+
use_json_schema = use_json_schema.lower() == "true"
|
|
119
|
+
|
|
105
120
|
if output_schema := kwargs.get("output_schema"):
|
|
106
121
|
try:
|
|
107
122
|
if isinstance(output_schema, str):
|
|
108
|
-
from agno.os.utils import json_schema_to_pydantic_model
|
|
109
|
-
|
|
110
123
|
schema_dict = json.loads(output_schema)
|
|
111
|
-
|
|
112
|
-
|
|
124
|
+
|
|
125
|
+
if use_json_schema:
|
|
126
|
+
# Keep as dict schema for direct JSON output
|
|
127
|
+
kwargs["output_schema"] = schema_dict
|
|
128
|
+
else:
|
|
129
|
+
# Convert to Pydantic model (default behavior)
|
|
130
|
+
dynamic_model = json_schema_to_pydantic_model(schema_dict)
|
|
131
|
+
kwargs["output_schema"] = dynamic_model
|
|
113
132
|
except json.JSONDecodeError:
|
|
114
133
|
kwargs.pop("output_schema")
|
|
115
134
|
log_warning(f"Invalid output_schema JSON: {output_schema}")
|
|
@@ -281,56 +300,45 @@ def get_session_name(session: Dict[str, Any]) -> str:
|
|
|
281
300
|
if session_data is not None and session_data.get("session_name") is not None:
|
|
282
301
|
return session_data["session_name"]
|
|
283
302
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
runs = session.get("runs", []) or []
|
|
287
|
-
|
|
288
|
-
# For teams, identify the first Team run and avoid using the first member's run
|
|
289
|
-
if session.get("session_type") == "team":
|
|
290
|
-
run = None
|
|
291
|
-
for r in runs:
|
|
292
|
-
# If agent_id is not present, it's a team run
|
|
293
|
-
if not r.get("agent_id"):
|
|
294
|
-
run = r
|
|
295
|
-
break
|
|
296
|
-
|
|
297
|
-
# Fallback to first run if no team run found
|
|
298
|
-
if run is None and runs:
|
|
299
|
-
run = runs[0]
|
|
300
|
-
|
|
301
|
-
elif session.get("session_type") == "workflow":
|
|
302
|
-
try:
|
|
303
|
-
workflow_run = runs[0]
|
|
304
|
-
workflow_input = workflow_run.get("input")
|
|
305
|
-
if isinstance(workflow_input, str):
|
|
306
|
-
return workflow_input
|
|
307
|
-
elif isinstance(workflow_input, dict):
|
|
308
|
-
try:
|
|
309
|
-
import json
|
|
310
|
-
|
|
311
|
-
return json.dumps(workflow_input)
|
|
312
|
-
except (TypeError, ValueError):
|
|
313
|
-
pass
|
|
314
|
-
|
|
315
|
-
workflow_name = session.get("workflow_data", {}).get("name")
|
|
316
|
-
return f"New {workflow_name} Session" if workflow_name else ""
|
|
317
|
-
except (KeyError, IndexError, TypeError):
|
|
318
|
-
return ""
|
|
319
|
-
|
|
320
|
-
# For agents, use the first run
|
|
321
|
-
else:
|
|
322
|
-
run = runs[0] if runs else None
|
|
303
|
+
runs = session.get("runs", []) or []
|
|
304
|
+
session_type = session.get("session_type")
|
|
323
305
|
|
|
324
|
-
|
|
306
|
+
# Handle workflows separately
|
|
307
|
+
if session_type == "workflow":
|
|
308
|
+
if not runs:
|
|
325
309
|
return ""
|
|
310
|
+
workflow_run = runs[0]
|
|
311
|
+
workflow_input = workflow_run.get("input")
|
|
312
|
+
if isinstance(workflow_input, str):
|
|
313
|
+
return workflow_input
|
|
314
|
+
elif isinstance(workflow_input, dict):
|
|
315
|
+
try:
|
|
316
|
+
return json.dumps(workflow_input)
|
|
317
|
+
except (TypeError, ValueError):
|
|
318
|
+
pass
|
|
319
|
+
workflow_name = session.get("workflow_data", {}).get("name")
|
|
320
|
+
return f"New {workflow_name} Session" if workflow_name else ""
|
|
321
|
+
|
|
322
|
+
# For team, filter to team runs (runs without agent_id); for agents, use all runs
|
|
323
|
+
if session_type == "team":
|
|
324
|
+
runs_to_check = [r for r in runs if not r.get("agent_id")]
|
|
325
|
+
else:
|
|
326
|
+
runs_to_check = runs
|
|
327
|
+
|
|
328
|
+
# Find the first user message across runs
|
|
329
|
+
for r in runs_to_check:
|
|
330
|
+
if r is None:
|
|
331
|
+
continue
|
|
332
|
+
run_dict = r if isinstance(r, dict) else r.to_dict()
|
|
333
|
+
|
|
334
|
+
for message in run_dict.get("messages") or []:
|
|
335
|
+
if message.get("role") == "user" and message.get("content"):
|
|
336
|
+
return message["content"]
|
|
326
337
|
|
|
327
|
-
|
|
328
|
-
|
|
338
|
+
run_input = r.get("input")
|
|
339
|
+
if run_input is not None:
|
|
340
|
+
return run_input.get("input_content")
|
|
329
341
|
|
|
330
|
-
if run and run.get("messages"):
|
|
331
|
-
for message in run["messages"]:
|
|
332
|
-
if message["role"] == "user":
|
|
333
|
-
return message["content"]
|
|
334
342
|
return ""
|
|
335
343
|
|
|
336
344
|
|
|
@@ -550,6 +558,31 @@ def _generate_schema_from_params(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
550
558
|
return schema
|
|
551
559
|
|
|
552
560
|
|
|
561
|
+
def resolve_origins(user_origins: Optional[List[str]] = None, default_origins: Optional[List[str]] = None) -> List[str]:
|
|
562
|
+
"""
|
|
563
|
+
Get CORS origins - user-provided origins override defaults.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
user_origins: Optional list of user-provided CORS origins
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
List of allowed CORS origins (user-provided if set, otherwise defaults)
|
|
570
|
+
"""
|
|
571
|
+
# User-provided origins override defaults
|
|
572
|
+
if user_origins:
|
|
573
|
+
return user_origins
|
|
574
|
+
|
|
575
|
+
# Default Agno domains
|
|
576
|
+
return default_origins or [
|
|
577
|
+
"http://localhost:3000",
|
|
578
|
+
"https://agno.com",
|
|
579
|
+
"https://www.agno.com",
|
|
580
|
+
"https://app.agno.com",
|
|
581
|
+
"https://os-stg.agno.com",
|
|
582
|
+
"https://os.agno.com",
|
|
583
|
+
]
|
|
584
|
+
|
|
585
|
+
|
|
553
586
|
def update_cors_middleware(app: FastAPI, new_origins: list):
|
|
554
587
|
existing_origins: List[str] = []
|
|
555
588
|
|
|
@@ -837,7 +870,7 @@ def _get_python_type_from_json_schema(field_schema: Dict[str, Any], field_name:
|
|
|
837
870
|
# Unknown or unspecified type - fallback to Any
|
|
838
871
|
if json_type:
|
|
839
872
|
logger.warning(f"Unknown JSON schema type '{json_type}' for field '{field_name}', using Any")
|
|
840
|
-
return Any
|
|
873
|
+
return Any # type: ignore
|
|
841
874
|
|
|
842
875
|
|
|
843
876
|
def json_schema_to_pydantic_model(schema: Dict[str, Any]) -> Type[BaseModel]:
|