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/cel.py ADDED
@@ -0,0 +1,299 @@
1
+ """CEL (Common Expression Language) support for workflow steps.
2
+
3
+ CEL spec: https://github.com/google/cel-spec
4
+ """
5
+
6
+ import json
7
+ import re
8
+ from typing import Any, Dict, List, Optional, Union
9
+
10
+ from agno.utils.log import logger
11
+
12
+ try:
13
+ import celpy
14
+ from celpy import celtypes
15
+
16
+ CEL_AVAILABLE = True
17
+ CelValue = Union[
18
+ celtypes.BoolType,
19
+ celtypes.IntType,
20
+ celtypes.DoubleType,
21
+ celtypes.StringType,
22
+ celtypes.ListType,
23
+ celtypes.MapType,
24
+ ]
25
+ except ImportError:
26
+ CEL_AVAILABLE = False
27
+ celpy = None # type: ignore
28
+ celtypes = None # type: ignore
29
+ CelValue = Any # type: ignore
30
+
31
+ # Type alias for Python values that can be converted to CEL
32
+ PythonValue = Union[None, bool, int, float, str, List[Any], Dict[str, Any]]
33
+
34
+ # Regex for simple Python identifiers (function names)
35
+ _IDENTIFIER_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
36
+
37
+ # Characters/tokens that indicate a CEL expression rather than a function name
38
+ _CEL_INDICATORS = [
39
+ ".",
40
+ "(",
41
+ ")",
42
+ "[",
43
+ "]",
44
+ "==",
45
+ "!=",
46
+ "<=",
47
+ ">=",
48
+ "<",
49
+ ">",
50
+ "&&",
51
+ "||",
52
+ "!",
53
+ "+",
54
+ "-",
55
+ "*",
56
+ "/",
57
+ "%",
58
+ "?",
59
+ ":",
60
+ '"',
61
+ "'",
62
+ "true",
63
+ "false",
64
+ " in ",
65
+ ]
66
+
67
+
68
+ # ********** Public Functions **********
69
+ def validate_cel_expression(expression: str) -> bool:
70
+ """Validate a CEL expression without evaluating it.
71
+
72
+ Useful for UI validation before saving a workflow configuration.
73
+ """
74
+ if not CEL_AVAILABLE:
75
+ logger.warning("cel-python is not installed. Install with: pip install cel-python")
76
+ return False
77
+
78
+ try:
79
+ env = celpy.Environment()
80
+ env.compile(expression)
81
+ return True
82
+ except Exception as e:
83
+ logger.debug(f"CEL expression validation failed: {e}")
84
+ return False
85
+
86
+
87
+ def is_cel_expression(value: str) -> bool:
88
+ """Determine if a string is a CEL expression vs a function name.
89
+
90
+ Simple identifiers like ``my_evaluator`` return False.
91
+ Anything containing operators, dots, parens, etc. returns True.
92
+ """
93
+ if _IDENTIFIER_RE.match(value):
94
+ return False
95
+
96
+ return any(indicator in value for indicator in _CEL_INDICATORS)
97
+
98
+
99
+ def evaluate_cel_condition_evaluator(
100
+ expression: str,
101
+ step_input: "StepInput", # type: ignore # noqa: F821
102
+ session_state: Optional[Dict[str, Any]] = None,
103
+ ) -> bool:
104
+ """Evaluate a CEL expression for a Condition evaluator.
105
+
106
+ Context variables:
107
+ - input: The workflow input as a string
108
+ - previous_step_content: Content from the previous step
109
+ - previous_step_outputs: Map of step name to content string from all previous steps
110
+ - additional_data: Map of additional data passed to the workflow
111
+ - session_state: Map of session state values
112
+ """
113
+ return _evaluate_cel(expression, _build_step_input_context(step_input, session_state))
114
+
115
+
116
+ def evaluate_cel_loop_end_condition(
117
+ expression: str,
118
+ iteration_results: "List[StepOutput]", # type: ignore # noqa: F821
119
+ current_iteration: int = 0,
120
+ max_iterations: int = 3,
121
+ ) -> bool:
122
+ """Evaluate a CEL expression as a Loop end condition.
123
+
124
+ Context variables:
125
+ - current_iteration: Current iteration number (1-indexed, after completion)
126
+ - max_iterations: Maximum iterations configured for the loop
127
+ - all_success: True if all steps in this iteration succeeded
128
+ - last_step_content: Content string from the last step in this iteration
129
+ - step_outputs: Map of step name to content string from the current iteration
130
+ """
131
+ return _evaluate_cel(
132
+ expression, _build_loop_step_output_context(iteration_results, current_iteration, max_iterations)
133
+ )
134
+
135
+
136
+ def evaluate_cel_router_selector(
137
+ expression: str,
138
+ step_input: "StepInput", # type: ignore # noqa: F821
139
+ session_state: Optional[Dict[str, Any]] = None,
140
+ step_choices: Optional[List[str]] = None,
141
+ ) -> str:
142
+ """Evaluate a CEL expression for a Router selector.
143
+
144
+ Returns the name of the step to execute as a string.
145
+
146
+ Context variables (same as Condition, plus step_choices):
147
+ - input: The workflow input as a string
148
+ - previous_step_content: Content from the previous step
149
+ - previous_step_outputs: Map of step name to content string from all previous steps
150
+ - additional_data: Map of additional data passed to the workflow
151
+ - session_state: Map of session state values
152
+ - step_choices: List of step names available to the selector
153
+ """
154
+ context = _build_step_input_context(step_input, session_state)
155
+ context["step_choices"] = step_choices or []
156
+ return _evaluate_cel_string(expression, context)
157
+
158
+
159
+ # ********** Internal Functions **********
160
+ def _evaluate_cel_raw(expression: str, context: Dict[str, Any]) -> Any:
161
+ """Core CEL evaluation: compile, run, and return the raw result."""
162
+ if not CEL_AVAILABLE:
163
+ raise RuntimeError("cel-python is not installed. Install with: pip install cel-python")
164
+
165
+ try:
166
+ env = celpy.Environment()
167
+ prog = env.program(env.compile(expression))
168
+ return prog.evaluate({k: _to_cel(v) for k, v in context.items()})
169
+ except Exception as e:
170
+ logger.error(f"CEL evaluation failed for '{expression}': {e}")
171
+ raise ValueError(f"Failed to evaluate CEL expression '{expression}': {e}") from e
172
+
173
+
174
+ def _evaluate_cel(expression: str, context: Dict[str, Any]) -> bool:
175
+ """CEL evaluation that coerces the result to bool."""
176
+ result = _evaluate_cel_raw(expression, context)
177
+
178
+ if isinstance(result, celtypes.BoolType):
179
+ return bool(result)
180
+ if isinstance(result, bool):
181
+ return result
182
+
183
+ logger.warning(f"CEL expression '{expression}' returned {type(result).__name__}, converting to bool")
184
+ return bool(result)
185
+
186
+
187
+ def _evaluate_cel_string(expression: str, context: Dict[str, Any]) -> str:
188
+ """CEL evaluation that coerces the result to string (for Router selector)."""
189
+ result = _evaluate_cel_raw(expression, context)
190
+
191
+ if isinstance(result, celtypes.StringType):
192
+ return str(result)
193
+ if isinstance(result, str):
194
+ return result
195
+
196
+ logger.warning(f"CEL expression '{expression}' returned {type(result).__name__}, converting to string")
197
+ return str(result)
198
+
199
+
200
+ def _to_cel(value: PythonValue) -> Union["CelValue", None]:
201
+ """Convert a Python value to a CEL-compatible type.
202
+
203
+ Args:
204
+ value: A Python value (None, bool, int, float, str, list, or dict)
205
+
206
+ Returns:
207
+ The corresponding CEL type, or None if input is None
208
+ """
209
+ if value is None:
210
+ return None
211
+
212
+ if isinstance(value, bool):
213
+ return celtypes.BoolType(value)
214
+ if isinstance(value, int):
215
+ return celtypes.IntType(value)
216
+ if isinstance(value, float):
217
+ return celtypes.DoubleType(value)
218
+ if isinstance(value, str):
219
+ return celtypes.StringType(value)
220
+ if isinstance(value, list):
221
+ return celtypes.ListType([_to_cel(item) for item in value])
222
+ if isinstance(value, dict):
223
+ return celtypes.MapType({celtypes.StringType(k): _to_cel(v) for k, v in value.items()})
224
+
225
+ # Fallback for any other type - convert to string
226
+ return celtypes.StringType(str(value))
227
+
228
+
229
+ def _build_step_input_context(
230
+ step_input: "StepInput", # type: ignore # noqa: F821
231
+ session_state: Optional[Dict[str, Any]] = None,
232
+ ) -> Dict[str, Any]:
233
+ """Build context for CEL evaluation of step input.
234
+
235
+ Maps directly to StepInput fields:
236
+ - input: from step_input.input (as string)
237
+ - previous_step_content: from step_input.previous_step_content (as string)
238
+ - previous_step_outputs: from step_input.previous_step_outputs (map of step name -> content string)
239
+ - additional_data: from step_input.additional_data
240
+ - session_state: passed separately
241
+ """
242
+ input_str = ""
243
+ if step_input.input is not None:
244
+ input_str = step_input.get_input_as_string() or ""
245
+
246
+ previous_content = ""
247
+ if step_input.previous_step_content is not None:
248
+ if hasattr(step_input.previous_step_content, "model_dump_json"):
249
+ previous_content = step_input.previous_step_content.model_dump_json()
250
+ elif isinstance(step_input.previous_step_content, dict):
251
+ previous_content = json.dumps(step_input.previous_step_content, default=str)
252
+ else:
253
+ previous_content = str(step_input.previous_step_content)
254
+
255
+ previous_step_outputs: Dict[str, str] = {}
256
+ if step_input.previous_step_outputs:
257
+ for name, output in step_input.previous_step_outputs.items():
258
+ previous_step_outputs[name] = str(output.content) if output.content else ""
259
+
260
+ return {
261
+ "input": input_str,
262
+ "previous_step_content": previous_content,
263
+ "previous_step_outputs": previous_step_outputs,
264
+ "additional_data": step_input.additional_data or {},
265
+ "session_state": session_state or {},
266
+ }
267
+
268
+
269
+ def _build_loop_step_output_context(
270
+ iteration_results: "List[StepOutput]", # type: ignore # noqa: F821
271
+ current_iteration: int = 0,
272
+ max_iterations: int = 3,
273
+ ) -> Dict[str, Any]:
274
+ """Build context for CEL evaluation of loop end condition from iteration results.
275
+
276
+ Maps to StepOutput fields:
277
+ - step_outputs: map of StepOutput.step_name -> str(StepOutput.content)
278
+ - all_success: derived from StepOutput.success
279
+ - last_step_content: content from the last StepOutput of the current loop iteration
280
+ """
281
+ all_success = True
282
+ outputs: Dict[str, str] = {}
283
+ last_content = ""
284
+
285
+ for result in iteration_results:
286
+ content = str(result.content) if result.content else ""
287
+ name = result.step_name or f"step_{len(outputs)}"
288
+ outputs[name] = content
289
+ last_content = content
290
+ if not result.success:
291
+ all_success = False
292
+
293
+ return {
294
+ "current_iteration": current_iteration,
295
+ "max_iterations": max_iterations,
296
+ "all_success": all_success,
297
+ "last_step_content": last_content,
298
+ "step_outputs": outputs,
299
+ }
@@ -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_condition_evaluator, is_cel_expression
17
19
  from agno.workflow.step import Step
18
20
  from agno.workflow.types import StepInput, StepOutput, StepType
19
21
 
@@ -43,13 +45,33 @@ class Condition:
43
45
  If the condition evaluates to True, the `steps` are executed.
44
46
  If the condition evaluates to False and `else_steps` is provided (and not empty),
45
47
  the `else_steps` are executed instead.
48
+
49
+ The evaluator can be:
50
+ - A callable function that returns bool
51
+ - A boolean literal (True/False)
52
+ - A CEL (Common Expression Language) expression string
53
+
54
+ CEL expressions have access to these variables:
55
+ - input: The workflow input as a string
56
+ - previous_step_content: Content from the previous step
57
+ - previous_step_outputs: Map of step name to content string from all previous steps
58
+ - additional_data: Map of additional data passed to the workflow
59
+ - session_state: Map of session state values
60
+
61
+ Example CEL expressions:
62
+ - 'input.contains("urgent")'
63
+ - 'session_state.retry_count < 3'
64
+ - 'additional_data.priority > 5'
65
+ - 'previous_step_outputs.research.contains("error")'
46
66
  """
47
67
 
48
68
  # Evaluator should only return boolean
69
+ # Can be a callable, a bool, or a CEL expression string
49
70
  evaluator: Union[
50
71
  Callable[[StepInput], bool],
51
72
  Callable[[StepInput], Awaitable[bool]],
52
73
  bool,
74
+ str, # CEL expression
53
75
  ]
54
76
  steps: WorkflowSteps
55
77
 
@@ -59,6 +81,89 @@ class Condition:
59
81
  name: Optional[str] = None
60
82
  description: Optional[str] = None
61
83
 
84
+ def to_dict(self) -> Dict[str, Any]:
85
+ result: Dict[str, Any] = {
86
+ "type": "Condition",
87
+ "name": self.name,
88
+ "description": self.description,
89
+ "steps": [step.to_dict() for step in self.steps if hasattr(step, "to_dict")],
90
+ "else_steps": [step.to_dict() for step in (self.else_steps or []) if hasattr(step, "to_dict")],
91
+ }
92
+ if callable(self.evaluator):
93
+ result["evaluator"] = self.evaluator.__name__
94
+ result["evaluator_type"] = "function"
95
+ elif isinstance(self.evaluator, bool):
96
+ result["evaluator"] = self.evaluator
97
+ result["evaluator_type"] = "bool"
98
+ elif isinstance(self.evaluator, str):
99
+ # CEL expression string
100
+ result["evaluator"] = self.evaluator
101
+ result["evaluator_type"] = "cel"
102
+ else:
103
+ raise ValueError(f"Invalid evaluator type: {type(self.evaluator).__name__}")
104
+
105
+ return result
106
+
107
+ @classmethod
108
+ def from_dict(
109
+ cls,
110
+ data: Dict[str, Any],
111
+ registry: Optional["Registry"] = None,
112
+ db: Optional[Any] = None,
113
+ links: Optional[List[Dict[str, Any]]] = None,
114
+ ) -> "Condition":
115
+ from agno.workflow.loop import Loop
116
+ from agno.workflow.parallel import Parallel
117
+ from agno.workflow.router import Router
118
+ from agno.workflow.steps import Steps
119
+
120
+ def deserialize_step(step_data: Dict[str, Any]) -> Any:
121
+ step_type = step_data.get("type", "Step")
122
+ if step_type == "Loop":
123
+ return Loop.from_dict(step_data, registry=registry, db=db, links=links)
124
+ elif step_type == "Parallel":
125
+ return Parallel.from_dict(step_data, registry=registry, db=db, links=links)
126
+ elif step_type == "Steps":
127
+ return Steps.from_dict(step_data, registry=registry, db=db, links=links)
128
+ elif step_type == "Condition":
129
+ return cls.from_dict(step_data, registry=registry, db=db, links=links)
130
+ elif step_type == "Router":
131
+ return Router.from_dict(step_data, registry=registry, db=db, links=links)
132
+ else:
133
+ return Step.from_dict(step_data, registry=registry, db=db, links=links)
134
+
135
+ evaluator_data = data.get("evaluator", True)
136
+ evaluator_type = data.get("evaluator_type")
137
+ evaluator: Union[Callable[[StepInput], bool], Callable[[StepInput], Awaitable[bool]], bool, str]
138
+
139
+ if isinstance(evaluator_data, bool):
140
+ evaluator = evaluator_data
141
+ elif isinstance(evaluator_data, str):
142
+ # Determine if this is a CEL expression or a function name
143
+ # Use evaluator_type if provided, otherwise detect
144
+ if evaluator_type == "cel" or (evaluator_type is None and is_cel_expression(evaluator_data)):
145
+ # CEL expression - use as-is
146
+ evaluator = evaluator_data
147
+ else:
148
+ # Function name - look up in registry
149
+ if registry:
150
+ func = registry.get_function(evaluator_data)
151
+ if func is None:
152
+ raise ValueError(f"Evaluator function '{evaluator_data}' not found in registry")
153
+ evaluator = func
154
+ else:
155
+ raise ValueError(f"Registry required to deserialize evaluator function '{evaluator_data}'")
156
+ else:
157
+ raise ValueError(f"Invalid evaluator type in data: {type(evaluator_data).__name__}")
158
+
159
+ return cls(
160
+ evaluator=evaluator,
161
+ steps=[deserialize_step(step) for step in data.get("steps", [])],
162
+ else_steps=[deserialize_step(step) for step in data.get("else_steps", [])],
163
+ name=data.get("name"),
164
+ description=data.get("description"),
165
+ )
166
+
62
167
  def _prepare_steps(self):
63
168
  """Prepare the steps for execution - mirrors workflow logic"""
64
169
  from agno.agent.agent import Agent
@@ -132,10 +237,29 @@ class Condition:
132
237
  )
133
238
 
134
239
  def _evaluate_condition(self, step_input: StepInput, session_state: Optional[Dict[str, Any]] = None) -> bool:
135
- """Evaluate the condition and return boolean result"""
240
+ """Evaluate the condition and return boolean result.
241
+
242
+ Supports:
243
+ - Boolean literals (True/False)
244
+ - Callable functions
245
+ - CEL expression strings
246
+ """
136
247
  if isinstance(self.evaluator, bool):
137
248
  return self.evaluator
138
249
 
250
+ if isinstance(self.evaluator, str):
251
+ # CEL expression
252
+ if not CEL_AVAILABLE:
253
+ logger.error(
254
+ "CEL expression used but cel-python is not installed. Install with: pip install cel-python"
255
+ )
256
+ return False
257
+ try:
258
+ return evaluate_cel_condition_evaluator(self.evaluator, step_input, session_state)
259
+ except Exception as e:
260
+ logger.error(f"CEL expression evaluation failed: {e}")
261
+ return False
262
+
139
263
  if callable(self.evaluator):
140
264
  if session_state is not None and self._evaluator_has_session_state_param():
141
265
  result = self.evaluator(step_input, session_state=session_state) # type: ignore[call-arg]
@@ -151,10 +275,29 @@ class Condition:
151
275
  return False
152
276
 
153
277
  async def _aevaluate_condition(self, step_input: StepInput, session_state: Optional[Dict[str, Any]] = None) -> bool:
154
- """Async version of condition evaluation"""
278
+ """Async version of condition evaluation.
279
+
280
+ Supports:
281
+ - Boolean literals (True/False)
282
+ - Callable functions (sync and async)
283
+ - CEL expression strings
284
+ """
155
285
  if isinstance(self.evaluator, bool):
156
286
  return self.evaluator
157
287
 
288
+ if isinstance(self.evaluator, str):
289
+ # CEL expression - CEL evaluation is synchronous
290
+ if not CEL_AVAILABLE:
291
+ logger.error(
292
+ "CEL expression used but cel-python is not installed. Install with: pip install cel-python"
293
+ )
294
+ return False
295
+ try:
296
+ return evaluate_cel_condition_evaluator(self.evaluator, step_input, session_state)
297
+ except Exception as e:
298
+ logger.error(f"CEL expression evaluation failed: {e}")
299
+ return False
300
+
158
301
  if callable(self.evaluator):
159
302
  has_session_state = session_state is not None and self._evaluator_has_session_state_param()
160
303