agno 2.3.12__py3-none-any.whl → 2.3.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. agno/agent/agent.py +1125 -1401
  2. agno/eval/__init__.py +21 -8
  3. agno/knowledge/embedder/azure_openai.py +0 -1
  4. agno/knowledge/embedder/google.py +1 -1
  5. agno/models/anthropic/claude.py +4 -1
  6. agno/models/azure/openai_chat.py +11 -5
  7. agno/models/base.py +8 -4
  8. agno/models/openai/chat.py +0 -2
  9. agno/models/openai/responses.py +2 -2
  10. agno/os/app.py +112 -5
  11. agno/os/auth.py +190 -3
  12. agno/os/config.py +9 -0
  13. agno/os/interfaces/a2a/router.py +619 -9
  14. agno/os/interfaces/a2a/utils.py +31 -32
  15. agno/os/middleware/__init__.py +2 -0
  16. agno/os/middleware/jwt.py +670 -108
  17. agno/os/router.py +0 -1
  18. agno/os/routers/agents/router.py +22 -4
  19. agno/os/routers/agents/schema.py +14 -1
  20. agno/os/routers/teams/router.py +20 -4
  21. agno/os/routers/teams/schema.py +14 -1
  22. agno/os/routers/workflows/router.py +88 -9
  23. agno/os/scopes.py +469 -0
  24. agno/os/utils.py +86 -53
  25. agno/reasoning/anthropic.py +85 -1
  26. agno/reasoning/azure_ai_foundry.py +93 -1
  27. agno/reasoning/deepseek.py +91 -1
  28. agno/reasoning/gemini.py +81 -1
  29. agno/reasoning/groq.py +103 -1
  30. agno/reasoning/manager.py +1244 -0
  31. agno/reasoning/ollama.py +93 -1
  32. agno/reasoning/openai.py +113 -1
  33. agno/reasoning/vertexai.py +85 -1
  34. agno/run/agent.py +11 -0
  35. agno/run/base.py +1 -1
  36. agno/run/team.py +11 -0
  37. agno/session/team.py +0 -3
  38. agno/team/team.py +1204 -1452
  39. agno/tools/postgres.py +1 -1
  40. agno/utils/cryptography.py +22 -0
  41. agno/utils/events.py +69 -2
  42. agno/utils/hooks.py +4 -10
  43. agno/utils/print_response/agent.py +52 -2
  44. agno/utils/print_response/team.py +141 -10
  45. agno/utils/prompts.py +8 -6
  46. agno/utils/string.py +46 -0
  47. agno/utils/team.py +1 -1
  48. agno/vectordb/chroma/chromadb.py +1 -0
  49. agno/vectordb/milvus/milvus.py +32 -3
  50. agno/vectordb/redis/redisdb.py +16 -2
  51. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/METADATA +3 -2
  52. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/RECORD +55 -52
  53. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/WHEEL +0 -0
  54. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/licenses/LICENSE +0 -0
  55. {agno-2.3.12.dist-info → agno-2.3.14.dist-info}/top_level.txt +0 -0
agno/os/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 dynamic Pydantic model
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
- dynamic_model = json_schema_to_pydantic_model(schema_dict)
112
- kwargs["output_schema"] = dynamic_model
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
- # Otherwise use the original user message
285
- else:
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
- if run is None:
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
- if not isinstance(run, dict):
328
- run = run.to_dict()
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]: