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
@@ -555,7 +555,13 @@ def get_workflow_router(
555
555
  db_workflows = get_workflows(db=os.db, registry=os.registry)
556
556
  if db_workflows:
557
557
  for db_workflow in db_workflows:
558
- workflows.append(WorkflowSummaryResponse.from_workflow(workflow=db_workflow))
558
+ try:
559
+ workflows.append(WorkflowSummaryResponse.from_workflow(workflow=db_workflow))
560
+ except Exception as e:
561
+ workflow_id = getattr(db_workflow, "id", "unknown")
562
+ logger.error(f"Error converting workflow {workflow_id} to response: {e}")
563
+ # Continue processing other workflows even if this one fails
564
+ continue
559
565
 
560
566
  return workflows
561
567
 
agno/os/schema.py CHANGED
@@ -618,6 +618,7 @@ class ComponentUpdate(BaseModel):
618
618
  description: Optional[str] = None
619
619
  component_type: Optional[str] = None
620
620
  metadata: Optional[Dict[str, Any]] = None
621
+ current_version: Optional[int] = None
621
622
 
622
623
 
623
624
  class ConfigUpdate(BaseModel):
@@ -626,3 +627,106 @@ class ConfigUpdate(BaseModel):
626
627
  stage: Optional[str] = None
627
628
  notes: Optional[str] = None
628
629
  links: Optional[List[Dict[str, Any]]] = None
630
+
631
+
632
+ class RegistryResourceType(str, Enum):
633
+ """Types of resources that can be stored in a registry."""
634
+
635
+ TOOL = "tool"
636
+ MODEL = "model"
637
+ DB = "db"
638
+ VECTOR_DB = "vector_db"
639
+ SCHEMA = "schema"
640
+ FUNCTION = "function"
641
+
642
+
643
+ class CallableMetadata(BaseModel):
644
+ """Common metadata for callable components (tools, functions)."""
645
+
646
+ name: str = Field(..., description="Callable name")
647
+ description: Optional[str] = Field(None, description="Callable description")
648
+ class_path: str = Field(..., description="Full module path to the class/function")
649
+ module: Optional[str] = Field(None, description="Module where the callable is defined")
650
+ qualname: Optional[str] = Field(None, description="Qualified name of the callable")
651
+ has_entrypoint: bool = Field(..., description="Whether the callable has an executable entrypoint")
652
+ parameters: Dict[str, Any] = Field(default_factory=dict, description="JSON schema of parameters")
653
+ requires_confirmation: Optional[bool] = Field(None, description="Whether execution requires user confirmation")
654
+ external_execution: Optional[bool] = Field(None, description="Whether execution happens externally")
655
+ signature: Optional[str] = Field(None, description="Function signature string")
656
+ return_annotation: Optional[str] = Field(None, description="Return type annotation")
657
+
658
+
659
+ class ToolMetadata(BaseModel):
660
+ """Metadata for tool registry components."""
661
+
662
+ class_path: str = Field(..., description="Full module path to the tool class")
663
+ is_toolkit: bool = Field(False, description="Whether this is a toolkit containing multiple functions")
664
+ functions: Optional[List[CallableMetadata]] = Field(
665
+ None, description="Functions in the toolkit (if is_toolkit=True)"
666
+ )
667
+
668
+ # Fields for non-toolkit tools (Function or raw callable)
669
+ module: Optional[str] = Field(None, description="Module where the callable is defined")
670
+ qualname: Optional[str] = Field(None, description="Qualified name of the callable")
671
+ has_entrypoint: Optional[bool] = Field(None, description="Whether the tool has an executable entrypoint")
672
+ parameters: Optional[Dict[str, Any]] = Field(None, description="JSON schema of parameters")
673
+ requires_confirmation: Optional[bool] = Field(None, description="Whether execution requires user confirmation")
674
+ external_execution: Optional[bool] = Field(None, description="Whether execution happens externally")
675
+ signature: Optional[str] = Field(None, description="Function signature string")
676
+ return_annotation: Optional[str] = Field(None, description="Return type annotation")
677
+
678
+
679
+ class ModelMetadata(BaseModel):
680
+ """Metadata for model registry components."""
681
+
682
+ class_path: str = Field(..., description="Full module path to the model class")
683
+ provider: Optional[str] = Field(None, description="Model provider (e.g., openai, anthropic)")
684
+ model_id: Optional[str] = Field(None, description="Model identifier")
685
+
686
+
687
+ class DbMetadata(BaseModel):
688
+ """Metadata for database registry components."""
689
+
690
+ class_path: str = Field(..., description="Full module path to the database class")
691
+ db_id: Optional[str] = Field(None, description="Database identifier")
692
+
693
+
694
+ class VectorDbMetadata(BaseModel):
695
+ """Metadata for vector database registry components."""
696
+
697
+ class_path: str = Field(..., description="Full module path to the vector database class")
698
+ vector_db_id: Optional[str] = Field(None, description="Vector database identifier")
699
+ collection: Optional[str] = Field(None, description="Collection name")
700
+ table_name: Optional[str] = Field(None, description="Table name (for SQL-based vector stores)")
701
+
702
+
703
+ class SchemaMetadata(BaseModel):
704
+ """Metadata for schema registry components."""
705
+
706
+ class_path: str = Field(..., description="Full module path to the schema class")
707
+ schema_: Optional[Dict[str, Any]] = Field(None, alias="schema", description="JSON schema definition")
708
+ schema_error: Optional[str] = Field(None, description="Error message if schema generation failed")
709
+
710
+
711
+ class FunctionMetadata(CallableMetadata):
712
+ """Metadata for function registry components (workflow conditions, selectors, etc.)."""
713
+
714
+ pass
715
+
716
+
717
+ # Union of all metadata types for type hints
718
+ RegistryMetadata = Union[
719
+ ToolMetadata,
720
+ ModelMetadata,
721
+ DbMetadata,
722
+ VectorDbMetadata,
723
+ SchemaMetadata,
724
+ FunctionMetadata,
725
+ ]
726
+
727
+
728
+ class RegistryContentResponse(BaseModel):
729
+ name: str
730
+ type: RegistryResourceType
731
+ description: Optional[str] = None
732
+ metadata: Optional[Dict[str, Any]] = None
agno/registry/registry.py CHANGED
@@ -26,6 +26,7 @@ class Registry:
26
26
  dbs: List[BaseDb] = field(default_factory=list)
27
27
  vector_dbs: List[VectorDb] = field(default_factory=list)
28
28
  schemas: List[Type[BaseModel]] = field(default_factory=list)
29
+ functions: List[Callable] = field(default_factory=list)
29
30
 
30
31
  @cached_property
31
32
  def _entrypoint_lookup(self) -> Dict[str, Callable]:
@@ -66,3 +67,6 @@ class Registry:
66
67
  if self.dbs:
67
68
  return next((db for db in self.dbs if db.id == db_id), None)
68
69
  return None
70
+
71
+ def get_function(self, name: str) -> Optional[Callable]:
72
+ return next((f for f in self.functions if f.__name__ == name), None)
agno/run/workflow.py CHANGED
@@ -311,6 +311,9 @@ class ConditionExecutionCompletedEvent(BaseWorkflowRunOutputEvent):
311
311
  condition_result: Optional[bool] = None
312
312
  executed_steps: Optional[int] = None
313
313
 
314
+ # Which branch was executed: "if", "else", or None (condition false with no else_steps)
315
+ branch: Optional[str] = None
316
+
314
317
  # Results from executed steps
315
318
  step_results: List[StepOutput] = field(default_factory=list)
316
319
 
agno/session/workflow.py CHANGED
@@ -16,7 +16,7 @@ from agno.utils.log import log_debug, logger
16
16
 
17
17
  @dataclass
18
18
  class WorkflowSession:
19
- """Workflow Session V2 for pipeline-based workflows"""
19
+ """Workflow Session for pipeline-based workflows"""
20
20
 
21
21
  # Session UUID - this is the workflow_session_id that gets set on agents/teams
22
22
  session_id: str
agno/skills/utils.py CHANGED
@@ -1,8 +1,10 @@
1
1
  """Utility functions for the skills module."""
2
2
 
3
3
  import os
4
+ import platform
4
5
  import stat
5
6
  import subprocess
7
+ import sys
6
8
  from dataclasses import dataclass
7
9
  from pathlib import Path
8
10
  from typing import List, Optional
@@ -41,6 +43,95 @@ def ensure_executable(file_path: Path) -> None:
41
43
  os.chmod(file_path, current_mode | stat.S_IXUSR)
42
44
 
43
45
 
46
+ def parse_shebang(script_path: Path) -> Optional[str]:
47
+ """Parse the shebang line from a script file to determine the interpreter.
48
+
49
+ Handles various shebang formats:
50
+ - #!/usr/bin/env python3 -> "python3"
51
+ - #!/usr/bin/python3 -> "python3"
52
+ - #!/bin/bash -> "bash"
53
+ - #!/usr/bin/env -S node -> "node"
54
+
55
+ Args:
56
+ script_path: Path to the script file.
57
+
58
+ Returns:
59
+ The interpreter name (e.g., "python3", "bash") or None if no valid shebang.
60
+ """
61
+ try:
62
+ with open(script_path, "r", encoding="utf-8") as f:
63
+ first_line = f.readline().strip()
64
+ except (OSError, UnicodeDecodeError):
65
+ return None
66
+
67
+ if not first_line.startswith("#!"):
68
+ return None
69
+
70
+ shebang = first_line[2:].strip()
71
+ if not shebang:
72
+ return None
73
+
74
+ parts = shebang.split()
75
+
76
+ # Handle /usr/bin/env style shebangs
77
+ if Path(parts[0]).name == "env":
78
+ # Skip any flags (like -S) and get the interpreter
79
+ for part in parts[1:]:
80
+ if not part.startswith("-"):
81
+ return part
82
+ return None
83
+
84
+ # Handle direct path shebangs like #!/bin/bash or #!/usr/bin/python3
85
+ # Extract the basename of the path
86
+ interpreter_path = parts[0]
87
+ return Path(interpreter_path).name
88
+
89
+
90
+ def get_interpreter_command(interpreter: str) -> List[str]:
91
+ """Map an interpreter name to a Windows-compatible command.
92
+
93
+ Args:
94
+ interpreter: The interpreter name from shebang (e.g., "python3", "bash").
95
+
96
+ Returns:
97
+ A list representing the command to invoke the interpreter.
98
+ """
99
+ # Normalize interpreter name
100
+ interpreter_lower = interpreter.lower()
101
+
102
+ # Python interpreters - use current Python executable
103
+ if interpreter_lower in ("python", "python3", "python2"):
104
+ return [sys.executable]
105
+
106
+ # Other interpreters - pass through as-is
107
+ # This includes: bash, sh, node, ruby, perl, etc.
108
+ # These need to be available in PATH on Windows
109
+ return [interpreter]
110
+
111
+
112
+ def _build_windows_command(script_path: Path, args: List[str]) -> List[str]:
113
+ """Build the command list for executing a script on Windows.
114
+
115
+ On Windows, shebang lines are not processed by the OS, so we need to
116
+ parse the shebang and explicitly invoke the interpreter.
117
+
118
+ Args:
119
+ script_path: Path to the script file.
120
+ args: Arguments to pass to the script.
121
+
122
+ Returns:
123
+ A list representing the full command to execute.
124
+ """
125
+ interpreter = parse_shebang(script_path)
126
+
127
+ if interpreter:
128
+ cmd_prefix = get_interpreter_command(interpreter)
129
+ return [*cmd_prefix, str(script_path), *args]
130
+
131
+ # Fallback: try direct execution (may fail, but provides clear error)
132
+ return [str(script_path), *args]
133
+
134
+
44
135
  @dataclass
45
136
  class ScriptResult:
46
137
  """Result of a script execution."""
@@ -58,6 +149,10 @@ def run_script(
58
149
  ) -> ScriptResult:
59
150
  """Execute a script and return the result.
60
151
 
152
+ On Unix-like systems, scripts are executed directly using their shebang.
153
+ On Windows, the shebang is parsed to determine the interpreter since
154
+ Windows does not natively support shebang lines.
155
+
61
156
  Args:
62
157
  script_path: Path to the script to execute.
63
158
  args: Optional list of arguments to pass to the script.
@@ -71,8 +166,11 @@ def run_script(
71
166
  subprocess.TimeoutExpired: If script exceeds timeout.
72
167
  FileNotFoundError: If script or interpreter not found.
73
168
  """
74
- ensure_executable(script_path)
75
- cmd = [str(script_path), *(args or [])]
169
+ if platform.system() == "Windows":
170
+ cmd = _build_windows_command(script_path, args or [])
171
+ else:
172
+ ensure_executable(script_path)
173
+ cmd = [str(script_path), *(args or [])]
76
174
 
77
175
  result = subprocess.run(
78
176
  cmd,
agno/team/team.py CHANGED
@@ -7094,7 +7094,7 @@ class Team:
7094
7094
  get_chat_history_func = get_chat_history # type: ignore
7095
7095
  return Function.from_callable(get_chat_history_func, name="get_chat_history")
7096
7096
 
7097
- def _update_session_state_tool(self, session_state, session_state_updates: dict) -> str:
7097
+ def _update_session_state_tool(self, run_context: RunContext, session_state_updates: dict) -> str:
7098
7098
  """
7099
7099
  Update the shared session state. Provide any updates as a dictionary of key-value pairs.
7100
7100
  Example:
@@ -7103,6 +7103,9 @@ class Team:
7103
7103
  Args:
7104
7104
  session_state_updates (dict): The updates to apply to the shared session state. Should be a dictionary of key-value pairs.
7105
7105
  """
7106
+ if run_context.session_state is None:
7107
+ run_context.session_state = {}
7108
+ session_state = run_context.session_state
7106
7109
  for key, value in session_state_updates.items():
7107
7110
  session_state[key] = value
7108
7111
 
@@ -8731,8 +8734,8 @@ class Team:
8731
8734
  tool_choice=config.get("tool_choice"),
8732
8735
  get_member_information_tool=config.get("get_member_information_tool", False),
8733
8736
  # --- Schema settings ---
8734
- # input_schema=config.get("input_schema"), # TODO
8735
- # output_schema=config.get("output_schema"), # TODO
8737
+ input_schema=config.get("input_schema"),
8738
+ output_schema=config.get("output_schema"),
8736
8739
  # --- Parser and output settings ---
8737
8740
  # parser_model=config.get("parser_model"), # TODO
8738
8741
  parser_model_prompt=config.get("parser_model_prompt"),
agno/tools/mcp/mcp.py CHANGED
@@ -74,6 +74,13 @@ class MCPTools(Toolkit):
74
74
  Only relevant with HTTP transports (Streamable HTTP or SSE).
75
75
  Creates a new session per agent run with dynamic headers merged into connection config.
76
76
  """
77
+ # Extract these before super().__init__() to bypass early validation
78
+ # (tools aren't available until build_tools() is called)
79
+ requires_confirmation_tools = kwargs.pop("requires_confirmation_tools", None)
80
+ external_execution_required_tools = kwargs.pop("external_execution_required_tools", None)
81
+ stop_after_tool_call_tools = kwargs.pop("stop_after_tool_call_tools", None)
82
+ show_result_tools = kwargs.pop("show_result_tools", None)
83
+
77
84
  super().__init__(name="MCPTools", **kwargs)
78
85
 
79
86
  if url is not None:
@@ -92,6 +99,10 @@ class MCPTools(Toolkit):
92
99
  # because tools are not available until `initialize()` is called.
93
100
  self.include_tools = include_tools
94
101
  self.exclude_tools = exclude_tools
102
+ self.requires_confirmation_tools = requires_confirmation_tools or []
103
+ self.external_execution_required_tools = external_execution_required_tools or []
104
+ self.stop_after_tool_call_tools = stop_after_tool_call_tools or []
105
+ self.show_result_tools = show_result_tools or []
95
106
  self.refresh_connection = refresh_connection
96
107
  self.tool_name_prefix = tool_name_prefix
97
108
 
@@ -575,13 +586,27 @@ class MCPTools(Toolkit):
575
586
  mcp_tools_instance=self,
576
587
  )
577
588
  # Create a Function for the tool
589
+ # Apply toolkit-level settings
590
+ tool_name = tool.name
591
+ stop_after = tool_name in self.stop_after_tool_call_tools
592
+ show_result = tool_name in self.show_result_tools or stop_after
593
+
578
594
  f = Function(
579
- name=tool_name_prefix + tool.name,
595
+ name=tool_name_prefix + tool_name,
580
596
  description=tool.description,
581
597
  parameters=tool.inputSchema,
582
598
  entrypoint=entrypoint,
583
599
  # Set skip_entrypoint_processing to True to avoid processing the entrypoint
584
600
  skip_entrypoint_processing=True,
601
+ # Apply toolkit-level settings for HITL and control flow
602
+ requires_confirmation=tool_name in self.requires_confirmation_tools,
603
+ external_execution=tool_name in self.external_execution_required_tools,
604
+ stop_after_tool_call=stop_after,
605
+ show_result=show_result,
606
+ # Apply toolkit-level cache settings
607
+ cache_results=self.cache_results,
608
+ cache_dir=self.cache_dir,
609
+ cache_ttl=self.cache_ttl,
585
610
  )
586
611
 
587
612
  # Register the Function with the toolkit
@@ -104,7 +104,7 @@ class LanceDb(VectorDb):
104
104
  self.async_connection: Optional[lancedb.AsyncConnection] = async_connection
105
105
  self.async_table: Optional[lancedb.db.AsyncTable] = async_table
106
106
 
107
- if table_name and table_name in self.connection.table_names():
107
+ if table_name and table_name in self._get_table_names(self.connection):
108
108
  # Open the table if it exists
109
109
  try:
110
110
  self.table = self.connection.open_table(name=table_name)
@@ -157,6 +157,21 @@ class LanceDb(VectorDb):
157
157
 
158
158
  log_debug(f"Initialized LanceDb with table: '{self.table_name}'")
159
159
 
160
+ def _get_table_names(self, conn: lancedb.DBConnection) -> List[str]:
161
+ """Get table names with backward compatibility for older LanceDB versions."""
162
+ # Prefer list_tables over table_names (deprecated in new versions)
163
+ if hasattr(conn, "list_tables"):
164
+ return conn.list_tables().tables
165
+ return conn.table_names()
166
+
167
+ async def _get_async_table_names(self, conn: lancedb.AsyncConnection) -> List[str]:
168
+ """Get table names asynchronously with backward compatibility for older LanceDB versions."""
169
+ # Prefer list_tables over table_names (deprecated in new versions)
170
+ if hasattr(conn, "list_tables"):
171
+ table_list = await conn.list_tables()
172
+ return table_list.tables
173
+ return await conn.table_names()
174
+
160
175
  def _prepare_vector(self, embedding) -> List[float]:
161
176
  """Prepare vector embedding for insertion, ensuring correct dimensions and type."""
162
177
  if embedding is not None and len(embedding) > 0:
@@ -186,7 +201,7 @@ class LanceDb(VectorDb):
186
201
  self.async_connection = await lancedb.connect_async(self.uri)
187
202
  # Only try to open table if it exists and we don't have it already
188
203
  if self.async_table is None:
189
- table_names = await self.async_connection.table_names()
204
+ table_names = await self._get_async_table_names(self.async_connection)
190
205
  if self.table_name in table_names:
191
206
  try:
192
207
  self.async_table = await self.async_connection.open_table(self.table_name)
@@ -199,7 +214,7 @@ class LanceDb(VectorDb):
199
214
  """Refresh the sync connection to see changes made by async operations."""
200
215
  try:
201
216
  # Re-establish sync connection to see async changes
202
- if self.connection and self.table_name in self.connection.table_names():
217
+ if self.connection is not None and self.table_name in self._get_table_names(self.connection):
203
218
  self.table = self.connection.open_table(self.table_name)
204
219
  except Exception as e:
205
220
  log_debug(f"Could not refresh sync connection: {e}")
@@ -459,7 +474,7 @@ class LanceDb(VectorDb):
459
474
  Returns:
460
475
  List[Document]: List of matching documents
461
476
  """
462
- if self.connection:
477
+ if self.connection is not None:
463
478
  self.table = self.connection.open_table(name=self.table_name)
464
479
 
465
480
  results = None
@@ -641,8 +656,8 @@ class LanceDb(VectorDb):
641
656
  # If we have an async table that was created, the table exists
642
657
  if self.async_table is not None:
643
658
  return True
644
- if self.connection:
645
- return self.table_name in self.connection.table_names()
659
+ if self.connection is not None:
660
+ return self.table_name in self._get_table_names(self.connection)
646
661
  return False
647
662
 
648
663
  async def async_exists(self) -> bool:
@@ -653,7 +668,7 @@ class LanceDb(VectorDb):
653
668
  # Check if table exists in database without trying to open it
654
669
  if self.async_connection is None:
655
670
  self.async_connection = await lancedb.connect_async(self.uri)
656
- table_names = await self.async_connection.table_names()
671
+ table_names = await self._get_async_table_names(self.async_connection)
657
672
  return self.table_name in table_names
658
673
 
659
674
  async def async_get_count(self) -> int:
agno/workflow/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from agno.workflow.agent import WorkflowAgent
2
+ from agno.workflow.cel import CEL_AVAILABLE, validate_cel_expression
2
3
  from agno.workflow.condition import Condition
3
4
  from agno.workflow.loop import Loop
4
5
  from agno.workflow.parallel import Parallel
@@ -24,4 +25,7 @@ __all__ = [
24
25
  "StepOutput",
25
26
  "get_workflow_by_id",
26
27
  "get_workflows",
28
+ # CEL utilities
29
+ "CEL_AVAILABLE",
30
+ "validate_cel_expression",
27
31
  ]