agno 2.4.6__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 (51) 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/singlestore/singlestore.py +4 -5
  5. agno/db/sqlite/sqlite.py +4 -4
  6. agno/knowledge/embedder/aws_bedrock.py +325 -106
  7. agno/knowledge/knowledge.py +83 -1853
  8. agno/knowledge/loaders/__init__.py +29 -0
  9. agno/knowledge/loaders/azure_blob.py +423 -0
  10. agno/knowledge/loaders/base.py +187 -0
  11. agno/knowledge/loaders/gcs.py +267 -0
  12. agno/knowledge/loaders/github.py +415 -0
  13. agno/knowledge/loaders/s3.py +281 -0
  14. agno/knowledge/loaders/sharepoint.py +439 -0
  15. agno/knowledge/reader/website_reader.py +2 -2
  16. agno/knowledge/remote_knowledge.py +151 -0
  17. agno/knowledge/reranker/aws_bedrock.py +299 -0
  18. agno/learn/machine.py +5 -6
  19. agno/learn/stores/session_context.py +10 -2
  20. agno/models/azure/openai_chat.py +6 -11
  21. agno/models/neosantara/__init__.py +5 -0
  22. agno/models/neosantara/neosantara.py +42 -0
  23. agno/models/utils.py +5 -0
  24. agno/os/app.py +4 -1
  25. agno/os/interfaces/agui/router.py +1 -1
  26. agno/os/routers/components/components.py +2 -0
  27. agno/os/routers/knowledge/knowledge.py +0 -1
  28. agno/os/routers/registry/registry.py +340 -192
  29. agno/os/routers/workflows/router.py +7 -1
  30. agno/os/schema.py +104 -0
  31. agno/registry/registry.py +4 -0
  32. agno/run/workflow.py +3 -0
  33. agno/session/workflow.py +1 -1
  34. agno/skills/utils.py +100 -2
  35. agno/team/team.py +6 -3
  36. agno/tools/mcp/mcp.py +26 -1
  37. agno/vectordb/lancedb/lance_db.py +22 -7
  38. agno/workflow/__init__.py +4 -0
  39. agno/workflow/cel.py +299 -0
  40. agno/workflow/condition.py +280 -58
  41. agno/workflow/loop.py +177 -46
  42. agno/workflow/parallel.py +75 -4
  43. agno/workflow/router.py +260 -44
  44. agno/workflow/step.py +14 -7
  45. agno/workflow/steps.py +43 -0
  46. agno/workflow/workflow.py +104 -46
  47. {agno-2.4.6.dist-info → agno-2.4.8.dist-info}/METADATA +25 -37
  48. {agno-2.4.6.dist-info → agno-2.4.8.dist-info}/RECORD +51 -39
  49. {agno-2.4.6.dist-info → agno-2.4.8.dist-info}/WHEEL +0 -0
  50. {agno-2.4.6.dist-info → agno-2.4.8.dist-info}/licenses/LICENSE +0 -0
  51. {agno-2.4.6.dist-info → agno-2.4.8.dist-info}/top_level.txt +0 -0
agno/workflow/loop.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
@@ -16,6 +17,7 @@ from agno.run.workflow import (
16
17
  )
17
18
  from agno.session.workflow import WorkflowSession
18
19
  from agno.utils.log import log_debug, logger
20
+ from agno.workflow.cel import CEL_AVAILABLE, evaluate_cel_loop_end_condition, is_cel_expression
19
21
  from agno.workflow.step import Step
20
22
  from agno.workflow.types import StepInput, StepOutput, StepType
21
23
 
@@ -36,7 +38,27 @@ WorkflowSteps = List[
36
38
 
37
39
  @dataclass
38
40
  class Loop:
39
- """A loop of steps that execute in order"""
41
+ """A loop of steps that execute in order.
42
+
43
+ The end_condition can be:
44
+ - A callable function that takes List[StepOutput] and returns bool
45
+ - A CEL (Common Expression Language) expression string
46
+ - None (loop runs for max_iterations)
47
+
48
+ CEL expressions for end_condition have access to:
49
+ - current_iteration: Current iteration number (1-indexed, after completion)
50
+ - max_iterations: Maximum iterations configured for the loop
51
+ - all_success: Boolean - True if all steps in this iteration succeeded
52
+ - last_step_content: Content string from the last step in the iteration
53
+ - step_outputs: Map of step name to content string from the current iteration
54
+
55
+ Example CEL expressions:
56
+ - 'current_iteration >= 2'
57
+ - 'current_iteration >= max_iterations'
58
+ - 'all_success'
59
+ - 'last_step_content.contains("DONE")'
60
+ - 'all_success && current_iteration >= 2'
61
+ """
40
62
 
41
63
  steps: WorkflowSteps
42
64
 
@@ -44,7 +66,7 @@ class Loop:
44
66
  description: Optional[str] = None
45
67
 
46
68
  max_iterations: int = 3 # Default to 3
47
- end_condition: Optional[Callable[[List[StepOutput]], bool]] = None
69
+ end_condition: Optional[Union[Callable[[List[StepOutput]], bool], str]] = None
48
70
 
49
71
  def __init__(
50
72
  self,
@@ -52,7 +74,7 @@ class Loop:
52
74
  name: Optional[str] = None,
53
75
  description: Optional[str] = None,
54
76
  max_iterations: int = 3,
55
- end_condition: Optional[Callable[[List[StepOutput]], bool]] = None,
77
+ end_condition: Optional[Union[Callable[[List[StepOutput]], bool], str]] = None,
56
78
  ):
57
79
  self.steps = steps
58
80
  self.name = name
@@ -60,6 +82,142 @@ class Loop:
60
82
  self.max_iterations = max_iterations
61
83
  self.end_condition = end_condition
62
84
 
85
+ def to_dict(self) -> Dict[str, Any]:
86
+ result: Dict[str, Any] = {
87
+ "type": "Loop",
88
+ "name": self.name,
89
+ "description": self.description,
90
+ "steps": [step.to_dict() for step in self.steps if hasattr(step, "to_dict")],
91
+ "max_iterations": self.max_iterations,
92
+ }
93
+ # Serialize end_condition
94
+ if self.end_condition is None:
95
+ result["end_condition"] = None
96
+ result["end_condition_type"] = None
97
+ elif isinstance(self.end_condition, str):
98
+ result["end_condition"] = self.end_condition
99
+ result["end_condition_type"] = "cel"
100
+ elif callable(self.end_condition):
101
+ result["end_condition"] = self.end_condition.__name__
102
+ result["end_condition_type"] = "function"
103
+ else:
104
+ raise ValueError(f"Invalid end_condition type: {type(self.end_condition).__name__}")
105
+
106
+ return result
107
+
108
+ @classmethod
109
+ def from_dict(
110
+ cls,
111
+ data: Dict[str, Any],
112
+ registry: Optional["Registry"] = None,
113
+ db: Optional[Any] = None,
114
+ links: Optional[List[Dict[str, Any]]] = None,
115
+ ) -> "Loop":
116
+ from agno.workflow.condition import Condition
117
+ from agno.workflow.parallel import Parallel
118
+ from agno.workflow.router import Router
119
+ from agno.workflow.steps import Steps
120
+
121
+ def deserialize_step(step_data: Dict[str, Any]) -> Any:
122
+ step_type = step_data.get("type", "Step")
123
+ if step_type == "Loop":
124
+ return cls.from_dict(step_data, registry=registry, db=db, links=links)
125
+ elif step_type == "Parallel":
126
+ return Parallel.from_dict(step_data, registry=registry, db=db, links=links)
127
+ elif step_type == "Steps":
128
+ return Steps.from_dict(step_data, registry=registry, db=db, links=links)
129
+ elif step_type == "Condition":
130
+ return Condition.from_dict(step_data, registry=registry, db=db, links=links)
131
+ elif step_type == "Router":
132
+ return Router.from_dict(step_data, registry=registry, db=db, links=links)
133
+ else:
134
+ return Step.from_dict(step_data, registry=registry, db=db, links=links)
135
+
136
+ # Deserialize end_condition
137
+ end_condition_data = data.get("end_condition")
138
+ end_condition_type = data.get("end_condition_type")
139
+ end_condition: Optional[Union[Callable[[List[StepOutput]], bool], str]] = None
140
+
141
+ if end_condition_data is None:
142
+ end_condition = None
143
+ elif isinstance(end_condition_data, str):
144
+ if end_condition_type == "cel" or (end_condition_type is None and is_cel_expression(end_condition_data)):
145
+ end_condition = end_condition_data
146
+ else:
147
+ if registry:
148
+ end_condition = registry.get_function(end_condition_data)
149
+ if end_condition is None:
150
+ raise ValueError(f"End condition function '{end_condition_data}' not found in registry")
151
+ else:
152
+ raise ValueError(f"Registry required to deserialize end_condition function '{end_condition_data}'")
153
+
154
+ return cls(
155
+ name=data.get("name"),
156
+ description=data.get("description"),
157
+ steps=[deserialize_step(step) for step in data.get("steps", [])],
158
+ max_iterations=data.get("max_iterations", 3),
159
+ end_condition=end_condition,
160
+ )
161
+
162
+ def _evaluate_end_condition(self, iteration_results: List[StepOutput], current_iteration: int = 0) -> bool:
163
+ """Evaluate the end condition and return whether the loop should stop."""
164
+ if self.end_condition is None:
165
+ return False
166
+
167
+ if isinstance(self.end_condition, str):
168
+ if not CEL_AVAILABLE:
169
+ logger.error(
170
+ "CEL expression used but cel-python is not installed. Install with: pip install cel-python"
171
+ )
172
+ return False
173
+ try:
174
+ return evaluate_cel_loop_end_condition(
175
+ self.end_condition, iteration_results, current_iteration, self.max_iterations
176
+ )
177
+ except Exception as e:
178
+ logger.warning(f"CEL end condition evaluation failed: {e}")
179
+ return False
180
+
181
+ if callable(self.end_condition):
182
+ try:
183
+ return self.end_condition(iteration_results)
184
+ except Exception as e:
185
+ logger.warning(f"End condition evaluation failed: {e}")
186
+ return False
187
+
188
+ return False
189
+
190
+ async def _aevaluate_end_condition(self, iteration_results: List[StepOutput], current_iteration: int = 0) -> bool:
191
+ """Async evaluate the end condition."""
192
+ if self.end_condition is None:
193
+ return False
194
+
195
+ if isinstance(self.end_condition, str):
196
+ if not CEL_AVAILABLE:
197
+ logger.error(
198
+ "CEL expression used but cel-python is not installed. Install with: pip install cel-python"
199
+ )
200
+ return False
201
+ try:
202
+ return evaluate_cel_loop_end_condition(
203
+ self.end_condition, iteration_results, current_iteration, self.max_iterations
204
+ )
205
+ except Exception as e:
206
+ logger.warning(f"CEL end condition evaluation failed: {e}")
207
+ return False
208
+
209
+ if callable(self.end_condition):
210
+ try:
211
+ if inspect.iscoroutinefunction(self.end_condition):
212
+ return await self.end_condition(iteration_results)
213
+ else:
214
+ return self.end_condition(iteration_results)
215
+ except Exception as e:
216
+ logger.warning(f"End condition evaluation failed: {e}")
217
+ return False
218
+
219
+ return False
220
+
63
221
  def _prepare_steps(self):
64
222
  """Prepare the steps for execution - mirrors workflow logic"""
65
223
  from agno.agent.agent import Agent
@@ -204,13 +362,8 @@ class Loop:
204
362
  iteration += 1
205
363
 
206
364
  # Check end condition
207
- if self.end_condition and callable(self.end_condition):
208
- try:
209
- should_break = self.end_condition(iteration_results)
210
- if should_break:
211
- break
212
- except Exception as e:
213
- logger.warning(f"End condition evaluation failed: {e}")
365
+ if self._evaluate_end_condition(iteration_results, iteration):
366
+ break
214
367
 
215
368
  # Break out of iteration loop if early termination was requested
216
369
  if early_termination:
@@ -365,16 +518,13 @@ class Loop:
365
518
  )
366
519
 
367
520
  all_results.append(iteration_results)
521
+ iteration += 1
368
522
 
369
523
  # Check end condition
370
524
  should_continue = True
371
- if self.end_condition and callable(self.end_condition):
372
- try:
373
- should_break = self.end_condition(iteration_results)
374
- should_continue = not should_break
375
- log_debug(f"End condition returned: {should_break}, should_continue: {should_continue}")
376
- except Exception as e:
377
- logger.warning(f"End condition evaluation failed: {e}")
525
+ if self._evaluate_end_condition(iteration_results, iteration):
526
+ should_continue = False
527
+ log_debug("End condition met, loop will stop")
378
528
 
379
529
  if early_termination:
380
530
  should_continue = False
@@ -389,7 +539,7 @@ class Loop:
389
539
  session_id=workflow_run_response.session_id or "",
390
540
  step_name=self.name,
391
541
  step_index=step_index,
392
- iteration=iteration + 1,
542
+ iteration=iteration,
393
543
  max_iterations=self.max_iterations,
394
544
  iteration_results=iteration_results,
395
545
  should_continue=should_continue,
@@ -397,10 +547,8 @@ class Loop:
397
547
  parent_step_id=parent_step_id,
398
548
  )
399
549
 
400
- iteration += 1
401
-
402
550
  if not should_continue:
403
- log_debug(f"Loop ending early due to end_condition at iteration {iteration}")
551
+ log_debug(f"Loop ending early at iteration {iteration}")
404
552
  break
405
553
 
406
554
  log_debug(f"Loop End: {self.name} ({iteration} iterations)", center=True, symbol="=")
@@ -515,23 +663,14 @@ class Loop:
515
663
  iteration += 1
516
664
 
517
665
  # Check end condition
518
- if self.end_condition and callable(self.end_condition):
519
- try:
520
- if inspect.iscoroutinefunction(self.end_condition):
521
- should_break = await self.end_condition(iteration_results)
522
- else:
523
- should_break = self.end_condition(iteration_results)
524
- if should_break:
525
- break
526
- except Exception as e:
527
- logger.warning(f"End condition evaluation failed: {e}")
666
+ if await self._aevaluate_end_condition(iteration_results, iteration):
667
+ break
528
668
 
529
669
  # Break out of iteration loop if early termination was requested
530
670
  if early_termination:
531
671
  log_debug(f"Loop ending early due to step termination request at iteration {iteration}")
532
672
  break
533
673
 
534
- # Use workflow logger for async loop completion
535
674
  log_debug(f"Async Loop End: {self.name} ({iteration} iterations)", center=True, symbol="=")
536
675
 
537
676
  # Return flattened results from all iterations
@@ -680,19 +819,13 @@ class Loop:
680
819
  )
681
820
 
682
821
  all_results.append(iteration_results)
822
+ iteration += 1
683
823
 
684
824
  # Check end condition
685
825
  should_continue = True
686
- if self.end_condition and callable(self.end_condition):
687
- try:
688
- if inspect.iscoroutinefunction(self.end_condition):
689
- should_break = await self.end_condition(iteration_results)
690
- else:
691
- should_break = self.end_condition(iteration_results)
692
- should_continue = not should_break
693
- log_debug(f"End condition returned: {should_break}, should_continue: {should_continue}")
694
- except Exception as e:
695
- logger.warning(f"End condition evaluation failed: {e}")
826
+ if await self._aevaluate_end_condition(iteration_results, iteration):
827
+ should_continue = False
828
+ log_debug("End condition met, loop will stop")
696
829
 
697
830
  if early_termination:
698
831
  should_continue = False
@@ -707,7 +840,7 @@ class Loop:
707
840
  session_id=workflow_run_response.session_id or "",
708
841
  step_name=self.name,
709
842
  step_index=step_index,
710
- iteration=iteration + 1,
843
+ iteration=iteration,
711
844
  max_iterations=self.max_iterations,
712
845
  iteration_results=iteration_results,
713
846
  should_continue=should_continue,
@@ -715,10 +848,8 @@ class Loop:
715
848
  parent_step_id=parent_step_id,
716
849
  )
717
850
 
718
- iteration += 1
719
-
720
851
  if not should_continue:
721
- log_debug(f"Loop ending early due to end_condition at iteration {iteration}")
852
+ log_debug(f"Loop ending early at iteration {iteration}")
722
853
  break
723
854
 
724
855
  log_debug(f"Loop End: {self.name} ({iteration} iterations)", center=True, symbol="=")
agno/workflow/parallel.py CHANGED
@@ -7,6 +7,7 @@ from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterator, List
7
7
  from uuid import uuid4
8
8
 
9
9
  from agno.models.metrics import Metrics
10
+ from agno.registry import Registry
10
11
  from agno.run.agent import RunOutputEvent
11
12
  from agno.run.base import RunContext
12
13
  from agno.run.team import TeamRunOutputEvent
@@ -40,7 +41,16 @@ WorkflowSteps = List[
40
41
 
41
42
  @dataclass
42
43
  class Parallel:
43
- """A list of steps that execute in parallel"""
44
+ """A list of steps that execute in parallel.
45
+
46
+ Unlike sequential constructs (Steps, Loop), Parallel uses variadic arguments
47
+ to emphasize that the steps are independent and unordered.
48
+
49
+ Supports flexible calling conventions:
50
+ Parallel(step1, step2, step3) # Steps only (name defaults to "Parallel")
51
+ Parallel(step1, step2, name="my_parallel") # Name as keyword (at end)
52
+ Parallel("my_parallel", step1, step2) # Name as first positional arg
53
+ """
44
54
 
45
55
  steps: WorkflowSteps
46
56
 
@@ -49,14 +59,75 @@ class Parallel:
49
59
 
50
60
  def __init__(
51
61
  self,
52
- *steps: WorkflowSteps,
62
+ *args: Union[str, WorkflowSteps],
53
63
  name: Optional[str] = None,
54
64
  description: Optional[str] = None,
55
65
  ):
56
- self.steps = list(steps)
57
- self.name = name
66
+ resolved_name = name
67
+ resolved_steps: List[Any] = []
68
+
69
+ if args:
70
+ first_arg = args[0]
71
+ # Check if first argument is a plain string (likely a name, not a step)
72
+ if isinstance(first_arg, str):
73
+ # First arg is the name, rest are steps
74
+ resolved_name = first_arg
75
+ resolved_steps = list(args[1:])
76
+ else:
77
+ # All args are steps
78
+ resolved_steps = list(args)
79
+
80
+ # If name was provided as keyword, it takes precedence
81
+ if name is not None:
82
+ resolved_name = name
83
+
84
+ self.steps = resolved_steps
85
+ self.name = resolved_name
58
86
  self.description = description
59
87
 
88
+ def to_dict(self) -> Dict[str, Any]:
89
+ return {
90
+ "type": "Parallel",
91
+ "name": self.name,
92
+ "description": self.description,
93
+ "steps": [step.to_dict() for step in self.steps if hasattr(step, "to_dict")],
94
+ }
95
+
96
+ @classmethod
97
+ def from_dict(
98
+ cls,
99
+ data: Dict[str, Any],
100
+ registry: Optional["Registry"] = None,
101
+ db: Optional[Any] = None,
102
+ links: Optional[List[Dict[str, Any]]] = None,
103
+ ) -> "Parallel":
104
+ from agno.workflow.condition import Condition
105
+ from agno.workflow.loop import Loop
106
+ from agno.workflow.router import Router
107
+ from agno.workflow.steps import Steps
108
+
109
+ def deserialize_step(step_data: Dict[str, Any]) -> Any:
110
+ step_type = step_data.get("type", "Step")
111
+ if step_type == "Loop":
112
+ return Loop.from_dict(step_data, registry=registry, db=db, links=links)
113
+ elif step_type == "Parallel":
114
+ return cls.from_dict(step_data, registry=registry, db=db, links=links)
115
+ elif step_type == "Steps":
116
+ return Steps.from_dict(step_data, registry=registry, db=db, links=links)
117
+ elif step_type == "Condition":
118
+ return Condition.from_dict(step_data, registry=registry, db=db, links=links)
119
+ elif step_type == "Router":
120
+ return Router.from_dict(step_data, registry=registry, db=db, links=links)
121
+ else:
122
+ return Step.from_dict(step_data, registry=registry, db=db, links=links)
123
+
124
+ deserialized_steps = [deserialize_step(step) for step in data.get("steps", [])]
125
+ return cls(
126
+ *deserialized_steps,
127
+ name=data.get("name"),
128
+ description=data.get("description"),
129
+ )
130
+
60
131
  def _prepare_steps(self):
61
132
  """Prepare the steps for execution - mirrors workflow logic"""
62
133
  from agno.agent.agent import Agent