agno 2.4.7__py3-none-any.whl → 2.4.8__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 (45) hide show
  1. agno/agent/agent.py +5 -1
  2. agno/db/base.py +2 -0
  3. agno/db/postgres/postgres.py +5 -5
  4. agno/db/sqlite/sqlite.py +4 -4
  5. agno/knowledge/knowledge.py +83 -1853
  6. agno/knowledge/loaders/__init__.py +29 -0
  7. agno/knowledge/loaders/azure_blob.py +423 -0
  8. agno/knowledge/loaders/base.py +187 -0
  9. agno/knowledge/loaders/gcs.py +267 -0
  10. agno/knowledge/loaders/github.py +415 -0
  11. agno/knowledge/loaders/s3.py +281 -0
  12. agno/knowledge/loaders/sharepoint.py +439 -0
  13. agno/knowledge/reader/website_reader.py +2 -2
  14. agno/knowledge/remote_knowledge.py +151 -0
  15. agno/learn/stores/session_context.py +10 -2
  16. agno/models/azure/openai_chat.py +6 -11
  17. agno/models/neosantara/__init__.py +5 -0
  18. agno/models/neosantara/neosantara.py +42 -0
  19. agno/models/utils.py +5 -0
  20. agno/os/app.py +4 -1
  21. agno/os/interfaces/agui/router.py +1 -1
  22. agno/os/routers/components/components.py +2 -0
  23. agno/os/routers/knowledge/knowledge.py +0 -1
  24. agno/os/routers/registry/registry.py +340 -192
  25. agno/os/routers/workflows/router.py +7 -1
  26. agno/os/schema.py +104 -0
  27. agno/registry/registry.py +4 -0
  28. agno/session/workflow.py +1 -1
  29. agno/skills/utils.py +100 -2
  30. agno/team/team.py +6 -3
  31. agno/vectordb/lancedb/lance_db.py +22 -7
  32. agno/workflow/__init__.py +4 -0
  33. agno/workflow/cel.py +299 -0
  34. agno/workflow/condition.py +145 -2
  35. agno/workflow/loop.py +177 -46
  36. agno/workflow/parallel.py +75 -4
  37. agno/workflow/router.py +260 -44
  38. agno/workflow/step.py +14 -7
  39. agno/workflow/steps.py +43 -0
  40. agno/workflow/workflow.py +104 -46
  41. {agno-2.4.7.dist-info → agno-2.4.8.dist-info}/METADATA +24 -36
  42. {agno-2.4.7.dist-info → agno-2.4.8.dist-info}/RECORD +45 -34
  43. {agno-2.4.7.dist-info → agno-2.4.8.dist-info}/WHEEL +0 -0
  44. {agno-2.4.7.dist-info → agno-2.4.8.dist-info}/licenses/LICENSE +0 -0
  45. {agno-2.4.7.dist-info → agno-2.4.8.dist-info}/top_level.txt +0 -0
agno/workflow/router.py CHANGED
@@ -3,6 +3,7 @@ from dataclasses import dataclass
3
3
  from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterator, List, Optional, Union
4
4
  from uuid import uuid4
5
5
 
6
+ from agno.registry import Registry
6
7
  from agno.run.agent import RunOutputEvent
7
8
  from agno.run.base import RunContext
8
9
  from agno.run.team import TeamRunOutputEvent
@@ -14,6 +15,7 @@ from agno.run.workflow import (
14
15
  )
15
16
  from agno.session.workflow import WorkflowSession
16
17
  from agno.utils.log import log_debug, logger
18
+ from agno.workflow.cel import CEL_AVAILABLE, evaluate_cel_router_selector, is_cel_expression
17
19
  from agno.workflow.step import Step
18
20
  from agno.workflow.types import StepInput, StepOutput, StepType
19
21
 
@@ -34,20 +36,119 @@ WorkflowSteps = List[
34
36
 
35
37
  @dataclass
36
38
  class Router:
37
- """A router that dynamically selects which step(s) to execute based on input"""
39
+ """A router that dynamically selects which step(s) to execute based on input.
38
40
 
39
- # Router function that returns the step(s) to execute
41
+ The selector can be:
42
+ - A callable function that takes StepInput and returns step(s)
43
+ - A CEL (Common Expression Language) expression string that returns a step name
44
+
45
+ CEL expressions for selector have access to (same as Condition, plus step_choices):
46
+ - input: The workflow input as a string
47
+ - previous_step_content: Content from the previous step
48
+ - previous_step_outputs: Map of step name to content string from all previous steps
49
+ - additional_data: Map of additional data passed to the workflow
50
+ - session_state: Map of session state values
51
+ - step_choices: List of step names available to the selector
52
+
53
+ CEL expressions must return the name of a step from choices.
54
+
55
+ Example CEL expressions:
56
+ - 'input.contains("video") ? "video_step" : "image_step"'
57
+ - 'additional_data.route'
58
+ - 'previous_step_outputs.classifier.contains("billing") ? "Billing" : "Support"'
59
+ """
60
+
61
+ # Router function or CEL expression that selects step(s) to execute
40
62
  selector: Union[
41
63
  Callable[[StepInput], Union[WorkflowSteps, List[WorkflowSteps]]],
42
64
  Callable[[StepInput], Awaitable[Union[WorkflowSteps, List[WorkflowSteps]]]],
65
+ str, # CEL expression returning step name
43
66
  ]
44
67
  choices: WorkflowSteps # Available steps that can be selected
45
68
 
46
69
  name: Optional[str] = None
47
70
  description: Optional[str] = None
48
71
 
49
- def _prepare_steps(self):
50
- """Prepare the steps for execution - mirrors workflow logic"""
72
+ def to_dict(self) -> Dict[str, Any]:
73
+ result: Dict[str, Any] = {
74
+ "type": "Router",
75
+ "name": self.name,
76
+ "description": self.description,
77
+ "choices": [step.to_dict() for step in self.choices if hasattr(step, "to_dict")],
78
+ }
79
+ # Serialize selector
80
+ if callable(self.selector):
81
+ result["selector"] = self.selector.__name__
82
+ result["selector_type"] = "function"
83
+ elif isinstance(self.selector, str):
84
+ result["selector"] = self.selector
85
+ result["selector_type"] = "cel"
86
+ else:
87
+ raise ValueError(f"Invalid selector type: {type(self.selector).__name__}")
88
+
89
+ return result
90
+
91
+ @classmethod
92
+ def from_dict(
93
+ cls,
94
+ data: Dict[str, Any],
95
+ registry: Optional["Registry"] = None,
96
+ db: Optional[Any] = None,
97
+ links: Optional[List[Dict[str, Any]]] = None,
98
+ ) -> "Router":
99
+ from agno.workflow.condition import Condition
100
+ from agno.workflow.loop import Loop
101
+ from agno.workflow.parallel import Parallel
102
+ from agno.workflow.steps import Steps
103
+
104
+ def deserialize_step(step_data: Dict[str, Any]) -> Any:
105
+ step_type = step_data.get("type", "Step")
106
+ if step_type == "Loop":
107
+ return Loop.from_dict(step_data, registry=registry, db=db, links=links)
108
+ elif step_type == "Parallel":
109
+ return Parallel.from_dict(step_data, registry=registry, db=db, links=links)
110
+ elif step_type == "Steps":
111
+ return Steps.from_dict(step_data, registry=registry, db=db, links=links)
112
+ elif step_type == "Condition":
113
+ return Condition.from_dict(step_data, registry=registry, db=db, links=links)
114
+ elif step_type == "Router":
115
+ return cls.from_dict(step_data, registry=registry, db=db, links=links)
116
+ else:
117
+ return Step.from_dict(step_data, registry=registry, db=db, links=links)
118
+
119
+ # Deserialize selector
120
+ selector_data = data.get("selector")
121
+ selector_type = data.get("selector_type")
122
+
123
+ selector: Any = None
124
+ if selector_data is None:
125
+ raise ValueError("Router requires a selector")
126
+ elif isinstance(selector_data, str):
127
+ # Determine if this is a CEL expression or a function name
128
+ if selector_type == "cel" or (selector_type is None and is_cel_expression(selector_data)):
129
+ # CEL expression - use as-is
130
+ selector = selector_data
131
+ else:
132
+ # Function name - look up in registry
133
+ if registry:
134
+ func = registry.get_function(selector_data)
135
+ if func is None:
136
+ raise ValueError(f"Selector function '{selector_data}' not found in registry")
137
+ selector = func
138
+ else:
139
+ raise ValueError(f"Registry required to deserialize selector function '{selector_data}'")
140
+ else:
141
+ raise ValueError(f"Invalid selector type in data: {type(selector_data).__name__}")
142
+
143
+ return cls(
144
+ selector=selector,
145
+ choices=[deserialize_step(step) for step in data.get("choices", [])],
146
+ name=data.get("name"),
147
+ description=data.get("description"),
148
+ )
149
+
150
+ def _prepare_single_step(self, step: Any) -> Any:
151
+ """Prepare a single step for execution."""
51
152
  from agno.agent.agent import Agent
52
153
  from agno.team.team import Team
53
154
  from agno.workflow.condition import Condition
@@ -56,20 +157,41 @@ class Router:
56
157
  from agno.workflow.step import Step
57
158
  from agno.workflow.steps import Steps
58
159
 
160
+ if callable(step) and hasattr(step, "__name__"):
161
+ return Step(name=step.__name__, description="User-defined callable step", executor=step)
162
+ elif isinstance(step, Agent):
163
+ return Step(name=step.name, description=step.description, agent=step)
164
+ elif isinstance(step, Team):
165
+ return Step(name=step.name, description=step.description, team=step)
166
+ elif isinstance(step, (Step, Steps, Loop, Parallel, Condition, Router)):
167
+ return step
168
+ else:
169
+ raise ValueError(f"Invalid step type: {type(step).__name__}")
170
+
171
+ def _prepare_steps(self):
172
+ """Prepare the steps for execution - mirrors workflow logic"""
173
+ from agno.workflow.steps import Steps
174
+
59
175
  prepared_steps: WorkflowSteps = []
60
176
  for step in self.choices:
61
- if callable(step) and hasattr(step, "__name__"):
62
- prepared_steps.append(Step(name=step.__name__, description="User-defined callable step", executor=step))
63
- elif isinstance(step, Agent):
64
- prepared_steps.append(Step(name=step.name, description=step.description, agent=step))
65
- elif isinstance(step, Team):
66
- prepared_steps.append(Step(name=step.name, description=step.description, team=step))
67
- elif isinstance(step, (Step, Steps, Loop, Parallel, Condition, Router)):
68
- prepared_steps.append(step)
177
+ if isinstance(step, list):
178
+ # Handle nested list of steps - wrap in Steps container
179
+ nested_prepared = [self._prepare_single_step(s) for s in step]
180
+ # Create a Steps container with a generated name
181
+ steps_container = Steps(
182
+ name=f"steps_group_{len(prepared_steps)}",
183
+ steps=nested_prepared,
184
+ )
185
+ prepared_steps.append(steps_container)
69
186
  else:
70
- raise ValueError(f"Invalid step type: {type(step).__name__}")
187
+ prepared_steps.append(self._prepare_single_step(step))
71
188
 
72
189
  self.steps = prepared_steps
190
+ # Build name-to-step mapping for string-based selection (used by CEL and callable selectors)
191
+ self._step_name_map: Dict[str, Any] = {}
192
+ for step in self.steps:
193
+ if hasattr(step, "name") and step.name:
194
+ self._step_name_map[step.name] = step
73
195
 
74
196
  def _update_step_input_from_outputs(
75
197
  self,
@@ -109,54 +231,148 @@ class Router:
109
231
  audio=current_audio + all_audio,
110
232
  )
111
233
 
112
- def _route_steps(self, step_input: StepInput, session_state: Optional[Dict[str, Any]] = None) -> List[Step]: # type: ignore[return-value]
113
- """Route to the appropriate steps based on input"""
114
- if callable(self.selector):
115
- if session_state is not None and self._selector_has_session_state_param():
116
- result = self.selector(step_input, session_state) # type: ignore[call-arg]
117
- else:
118
- result = self.selector(step_input)
234
+ def _resolve_selector_result(self, result: Any) -> List[Any]:
235
+ """Resolve selector result to a list of steps, handling strings, Steps, and lists.
119
236
 
120
- # Handle the result based on its type
121
- if isinstance(result, Step):
122
- return [result]
123
- elif isinstance(result, list):
124
- return result # type: ignore
237
+ This unified resolver handles:
238
+ - String step names (from CEL expressions or callable selectors)
239
+ - Step objects directly returned by callable selectors
240
+ - Lists of strings or Steps
241
+ """
242
+ from agno.workflow.condition import Condition
243
+ from agno.workflow.loop import Loop
244
+ from agno.workflow.parallel import Parallel
245
+ from agno.workflow.steps import Steps
246
+
247
+ if result is None:
248
+ return []
249
+
250
+ # Handle string - look up by name in the step_name_map
251
+ if isinstance(result, str):
252
+ if result in self._step_name_map:
253
+ return [self._step_name_map[result]]
125
254
  else:
126
- logger.warning(f"Router function returned unexpected type: {type(result)}")
255
+ available_steps = list(self._step_name_map.keys())
256
+ logger.warning(
257
+ f"Router '{self.name}' selector returned unknown step name: '{result}'. "
258
+ f"Available step names are: {available_steps}. "
259
+ f"Make sure the selector returns one of the available step names."
260
+ )
261
+ return []
262
+
263
+ # Handle step types (Step, Steps, Loop, Parallel, Condition, Router)
264
+ if isinstance(result, (Step, Steps, Loop, Parallel, Condition, Router)):
265
+ # Validate that the returned step is in the router's choices
266
+ step_name = getattr(result, "name", None)
267
+ if step_name and step_name not in self._step_name_map:
268
+ available_steps = list(self._step_name_map.keys())
269
+ logger.warning(
270
+ f"Router '{self.name}' selector returned a Step '{step_name}' that is not in choices. "
271
+ f"Available step names are: {available_steps}. "
272
+ f"The step will still be executed, but this may indicate a configuration error."
273
+ )
274
+ return [result]
275
+
276
+ # Handle list of results (could be strings, Steps, or mixed)
277
+ if isinstance(result, list):
278
+ resolved = []
279
+ for item in result:
280
+ resolved.extend(self._resolve_selector_result(item))
281
+ return resolved
282
+
283
+ logger.warning(f"Router selector returned unexpected type: {type(result)}")
284
+ return []
285
+
286
+ def _selector_has_step_choices_param(self) -> bool:
287
+ """Check if the selector function has a step_choices parameter"""
288
+ if not callable(self.selector):
289
+ return False
290
+
291
+ try:
292
+ sig = inspect.signature(self.selector)
293
+ return "step_choices" in sig.parameters
294
+ except Exception:
295
+ return False
296
+
297
+ def _route_steps(self, step_input: StepInput, session_state: Optional[Dict[str, Any]] = None) -> List[Step]: # type: ignore[return-value]
298
+ """Route to the appropriate steps based on input."""
299
+ # Handle CEL expression selector
300
+ if isinstance(self.selector, str):
301
+ if not CEL_AVAILABLE:
302
+ logger.error(
303
+ "CEL expression used but cel-python is not installed. Install with: pip install cel-python"
304
+ )
305
+ return []
306
+ try:
307
+ step_names = list(self._step_name_map.keys())
308
+ step_name = evaluate_cel_router_selector(
309
+ self.selector, step_input, session_state, step_choices=step_names
310
+ )
311
+ return self._resolve_selector_result(step_name)
312
+ except Exception as e:
313
+ logger.error(f"Router CEL evaluation failed: {e}")
127
314
  return []
128
315
 
316
+ # Handle callable selector
317
+ if callable(self.selector):
318
+ has_session_state = session_state is not None and self._selector_has_session_state_param()
319
+ has_step_choices = self._selector_has_step_choices_param()
320
+
321
+ # Build kwargs based on what parameters the selector accepts
322
+ kwargs: Dict[str, Any] = {}
323
+ if has_session_state:
324
+ kwargs["session_state"] = session_state
325
+ if has_step_choices:
326
+ kwargs["step_choices"] = self.steps
327
+
328
+ result = self.selector(step_input, **kwargs) # type: ignore[call-arg]
329
+
330
+ return self._resolve_selector_result(result)
331
+
129
332
  return []
130
333
 
131
334
  async def _aroute_steps(self, step_input: StepInput, session_state: Optional[Dict[str, Any]] = None) -> List[Step]: # type: ignore[return-value]
132
- """Async version of step routing"""
335
+ """Async version of step routing."""
336
+ # Handle CEL expression selector (CEL evaluation is synchronous)
337
+ if isinstance(self.selector, str):
338
+ if not CEL_AVAILABLE:
339
+ logger.error(
340
+ "CEL expression used but cel-python is not installed. Install with: pip install cel-python"
341
+ )
342
+ return []
343
+ try:
344
+ step_names = list(self._step_name_map.keys())
345
+ step_name = evaluate_cel_router_selector(
346
+ self.selector, step_input, session_state, step_choices=step_names
347
+ )
348
+ return self._resolve_selector_result(step_name)
349
+ except Exception as e:
350
+ logger.error(f"Router CEL evaluation failed: {e}")
351
+ return []
352
+
353
+ # Handle callable selector
133
354
  if callable(self.selector):
134
355
  has_session_state = session_state is not None and self._selector_has_session_state_param()
356
+ has_step_choices = self._selector_has_step_choices_param()
357
+
358
+ # Build kwargs based on what parameters the selector accepts
359
+ kwargs: Dict[str, Any] = {}
360
+ if has_session_state:
361
+ kwargs["session_state"] = session_state
362
+ if has_step_choices:
363
+ kwargs["step_choices"] = self.steps
135
364
 
136
365
  if inspect.iscoroutinefunction(self.selector):
137
- if has_session_state:
138
- result = await self.selector(step_input, session_state) # type: ignore[call-arg]
139
- else:
140
- result = await self.selector(step_input)
366
+ result = await self.selector(step_input, **kwargs) # type: ignore[call-arg]
141
367
  else:
142
- if has_session_state:
143
- result = self.selector(step_input, session_state) # type: ignore[call-arg]
144
- else:
145
- result = self.selector(step_input)
368
+ result = self.selector(step_input, **kwargs) # type: ignore[call-arg]
146
369
 
147
- # Handle the result based on its type
148
- if isinstance(result, Step):
149
- return [result]
150
- elif isinstance(result, list):
151
- return result
152
- else:
153
- logger.warning(f"Router function returned unexpected type: {type(result)}")
154
- return []
370
+ return self._resolve_selector_result(result)
155
371
 
156
372
  return []
157
373
 
158
374
  def _selector_has_session_state_param(self) -> bool:
159
- """Check if the selector function has a session_state parameter"""
375
+ """Check if the selector function has a session_state parameter."""
160
376
  if not callable(self.selector):
161
377
  return False
162
378
 
agno/workflow/step.py CHANGED
@@ -118,6 +118,7 @@ class Step:
118
118
  def to_dict(self) -> Dict[str, Any]:
119
119
  """Convert step to a dictionary representation."""
120
120
  result = {
121
+ "type": "Step",
121
122
  "name": self.name,
122
123
  "step_id": self.step_id,
123
124
  "description": self.description,
@@ -132,7 +133,8 @@ class Step:
132
133
  result["agent_id"] = self.agent.id
133
134
  if self.team is not None:
134
135
  result["team_id"] = self.team.id
135
- # TODO: Add support for custom executors
136
+ if self.executor is not None:
137
+ result["executor_ref"] = self.executor.__name__
136
138
 
137
139
  return result
138
140
 
@@ -171,14 +173,16 @@ class Step:
171
173
  agent = get_agent_by_id(db=db, id=agent_id, registry=registry)
172
174
 
173
175
  # --- Handle Team reconstruction ---
174
- # if "team_id" in config and config["team_id"] and registry:
175
- # from agno.team.team import get_team_by_id
176
- # team = get_team_by_id(db=db, id=config["team_id"])
176
+ if "team_id" in config and config["team_id"] and registry:
177
+ from agno.team.team import get_team_by_id
178
+
179
+ team_id = config.get("team_id")
180
+ if db is not None and team_id is not None:
181
+ team = get_team_by_id(db=db, id=team_id, registry=registry)
177
182
 
178
183
  # --- Handle Executor reconstruction ---
179
- # TODO: Implement executor reconstruction
180
- # if "executor_ref" in config and config["executor_ref"] and registry:
181
- # executor = registry.rehydrate_function(config["executor_ref"])
184
+ if "executor_ref" in config and config["executor_ref"] and registry:
185
+ executor = registry.get_function(config["executor_ref"])
182
186
 
183
187
  return cls(
184
188
  name=config.get("name"),
@@ -559,6 +563,9 @@ class Step:
559
563
  event.workflow_id = workflow_run_response.workflow_id
560
564
  if hasattr(event, "workflow_run_id"):
561
565
  event.workflow_run_id = workflow_run_response.run_id
566
+ # Set session_id to match workflow's session_id for consistent event tracking
567
+ if hasattr(event, "session_id") and workflow_run_response.session_id:
568
+ event.session_id = workflow_run_response.session_id
562
569
  if hasattr(event, "step_id"):
563
570
  event.step_id = self.step_id
564
571
  if hasattr(event, "step_name") and self.name is not None:
agno/workflow/steps.py CHANGED
@@ -2,6 +2,7 @@ from dataclasses import dataclass
2
2
  from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterator, List, Optional, Union
3
3
  from uuid import uuid4
4
4
 
5
+ from agno.registry import Registry
5
6
  from agno.run.agent import RunOutputEvent
6
7
  from agno.run.base import RunContext
7
8
  from agno.run.team import TeamRunOutputEvent
@@ -48,6 +49,48 @@ class Steps:
48
49
  self.description = description
49
50
  self.steps = steps if steps else []
50
51
 
52
+ def to_dict(self) -> Dict[str, Any]:
53
+ return {
54
+ "type": "Steps",
55
+ "name": self.name,
56
+ "description": self.description,
57
+ "steps": [step.to_dict() for step in self.steps if hasattr(step, "to_dict")],
58
+ }
59
+
60
+ @classmethod
61
+ def from_dict(
62
+ cls,
63
+ data: Dict[str, Any],
64
+ registry: Optional["Registry"] = None,
65
+ db: Optional[Any] = None,
66
+ links: Optional[List[Dict[str, Any]]] = None,
67
+ ) -> "Steps":
68
+ from agno.workflow.condition import Condition
69
+ from agno.workflow.loop import Loop
70
+ from agno.workflow.parallel import Parallel
71
+ from agno.workflow.router import Router
72
+
73
+ def deserialize_step(step_data: Dict[str, Any]) -> Any:
74
+ step_type = step_data.get("type", "Step")
75
+ if step_type == "Loop":
76
+ return Loop.from_dict(step_data, registry=registry, db=db, links=links)
77
+ elif step_type == "Parallel":
78
+ return Parallel.from_dict(step_data, registry=registry, db=db, links=links)
79
+ elif step_type == "Steps":
80
+ return cls.from_dict(step_data, registry=registry, db=db, links=links)
81
+ elif step_type == "Condition":
82
+ return Condition.from_dict(step_data, registry=registry, db=db, links=links)
83
+ elif step_type == "Router":
84
+ return Router.from_dict(step_data, registry=registry, db=db, links=links)
85
+ else:
86
+ return Step.from_dict(step_data, registry=registry, db=db, links=links)
87
+
88
+ return cls(
89
+ name=data.get("name"),
90
+ description=data.get("description"),
91
+ steps=[deserialize_step(step) for step in data.get("steps", [])],
92
+ )
93
+
51
94
  def _prepare_steps(self):
52
95
  """Prepare the steps for execution - mirrors workflow logic"""
53
96
  from agno.agent.agent import Agent