abstractflow 0.3.0__py3-none-any.whl → 0.3.1__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.
- abstractflow/__init__.py +2 -2
- abstractflow/adapters/agent_adapter.py +2 -121
- abstractflow/adapters/control_adapter.py +2 -612
- abstractflow/adapters/effect_adapter.py +2 -642
- abstractflow/adapters/event_adapter.py +2 -304
- abstractflow/adapters/function_adapter.py +2 -94
- abstractflow/adapters/subflow_adapter.py +2 -71
- abstractflow/adapters/variable_adapter.py +2 -314
- abstractflow/cli.py +73 -28
- abstractflow/compiler.py +18 -2022
- abstractflow/core/flow.py +4 -240
- abstractflow/runner.py +59 -5
- abstractflow/visual/agent_ids.py +2 -26
- abstractflow/visual/builtins.py +2 -786
- abstractflow/visual/code_executor.py +2 -211
- abstractflow/visual/executor.py +319 -2140
- abstractflow/visual/interfaces.py +103 -10
- abstractflow/visual/models.py +26 -1
- abstractflow/visual/session_runner.py +23 -9
- abstractflow/visual/workspace_scoped_tools.py +11 -243
- abstractflow/workflow_bundle.py +290 -0
- abstractflow-0.3.1.dist-info/METADATA +186 -0
- abstractflow-0.3.1.dist-info/RECORD +33 -0
- {abstractflow-0.3.0.dist-info → abstractflow-0.3.1.dist-info}/WHEEL +1 -1
- abstractflow-0.3.0.dist-info/METADATA +0 -413
- abstractflow-0.3.0.dist-info/RECORD +0 -32
- {abstractflow-0.3.0.dist-info → abstractflow-0.3.1.dist-info}/entry_points.txt +0 -0
- {abstractflow-0.3.0.dist-info → abstractflow-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {abstractflow-0.3.0.dist-info → abstractflow-0.3.1.dist-info}/top_level.txt +0 -0
abstractflow/core/flow.py
CHANGED
|
@@ -1,247 +1,11 @@
|
|
|
1
1
|
"""Flow definition classes for AbstractFlow.
|
|
2
2
|
|
|
3
|
-
This module
|
|
4
|
-
|
|
5
|
-
- FlowNode: A node in the flow (agent, function, or nested flow)
|
|
6
|
-
- FlowEdge: An edge connecting two nodes
|
|
3
|
+
This module is a thin re-export of AbstractRuntime's Flow IR so there is a
|
|
4
|
+
single semantics + IR surface shared across hosts.
|
|
7
5
|
"""
|
|
8
6
|
|
|
9
7
|
from __future__ import annotations
|
|
10
8
|
|
|
11
|
-
from
|
|
12
|
-
from typing import Any, Callable, Dict, List, Optional, Union, TYPE_CHECKING
|
|
9
|
+
from abstractruntime.visualflow_compiler.flow import Flow, FlowEdge, FlowNode
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
# Avoid circular import - only used for type hints
|
|
16
|
-
pass
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@dataclass
|
|
20
|
-
class FlowNode:
|
|
21
|
-
"""A node in a flow graph.
|
|
22
|
-
|
|
23
|
-
Attributes:
|
|
24
|
-
id: Unique identifier for the node within the flow
|
|
25
|
-
handler: The handler for this node - can be an agent, function, or nested flow
|
|
26
|
-
input_key: Key in run.vars to read input from (optional)
|
|
27
|
-
output_key: Key in run.vars to write output to (optional)
|
|
28
|
-
effect_type: Effect type for effect nodes (ask_user, wait_until, etc.)
|
|
29
|
-
effect_config: Additional configuration for effect nodes
|
|
30
|
-
"""
|
|
31
|
-
id: str
|
|
32
|
-
handler: Any # Union[BaseAgent, Callable, Flow] - Any to avoid circular imports
|
|
33
|
-
input_key: Optional[str] = None
|
|
34
|
-
output_key: Optional[str] = None
|
|
35
|
-
effect_type: Optional[str] = None # e.g., "ask_user", "wait_until", etc.
|
|
36
|
-
effect_config: Optional[Dict[str, Any]] = None # Effect-specific configuration
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@dataclass
|
|
40
|
-
class FlowEdge:
|
|
41
|
-
"""An edge connecting two nodes in a flow.
|
|
42
|
-
|
|
43
|
-
Attributes:
|
|
44
|
-
source: ID of the source node
|
|
45
|
-
target: ID of the target node
|
|
46
|
-
condition: Optional condition function for conditional routing (future)
|
|
47
|
-
source_handle: Optional execution output handle id (visual flows).
|
|
48
|
-
"""
|
|
49
|
-
source: str
|
|
50
|
-
target: str
|
|
51
|
-
condition: Optional[Callable[[Dict[str, Any]], bool]] = None
|
|
52
|
-
source_handle: Optional[str] = None
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class Flow:
|
|
56
|
-
"""Declarative flow definition that compiles to WorkflowSpec.
|
|
57
|
-
|
|
58
|
-
A Flow represents a directed graph of nodes (agents, functions, or nested flows)
|
|
59
|
-
connected by edges. Flows can be compiled to AbstractRuntime WorkflowSpec for
|
|
60
|
-
durable execution.
|
|
61
|
-
|
|
62
|
-
Example:
|
|
63
|
-
>>> flow = Flow("my_flow")
|
|
64
|
-
>>> flow.add_node("start", my_function, output_key="data")
|
|
65
|
-
>>> flow.add_node("process", process_function, input_key="data")
|
|
66
|
-
>>> flow.add_edge("start", "process")
|
|
67
|
-
>>> flow.set_entry("start")
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
def __init__(self, flow_id: str):
|
|
71
|
-
"""Initialize a new flow.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
flow_id: Unique identifier for this flow
|
|
75
|
-
"""
|
|
76
|
-
self.flow_id = flow_id
|
|
77
|
-
self.nodes: Dict[str, FlowNode] = {}
|
|
78
|
-
self.edges: List[FlowEdge] = []
|
|
79
|
-
self.entry_node: Optional[str] = None
|
|
80
|
-
self.exit_node: Optional[str] = None
|
|
81
|
-
|
|
82
|
-
def add_node(
|
|
83
|
-
self,
|
|
84
|
-
node_id: str,
|
|
85
|
-
handler: Any,
|
|
86
|
-
*,
|
|
87
|
-
input_key: Optional[str] = None,
|
|
88
|
-
output_key: Optional[str] = None,
|
|
89
|
-
effect_type: Optional[str] = None,
|
|
90
|
-
effect_config: Optional[Dict[str, Any]] = None,
|
|
91
|
-
) -> "Flow":
|
|
92
|
-
"""Add a node to the flow.
|
|
93
|
-
|
|
94
|
-
Args:
|
|
95
|
-
node_id: Unique identifier for this node
|
|
96
|
-
handler: The handler - an agent, callable function, or nested Flow
|
|
97
|
-
input_key: Key in run.vars to read input from
|
|
98
|
-
output_key: Key in run.vars to write output to
|
|
99
|
-
effect_type: Effect type for effect nodes (ask_user, wait_until, etc.)
|
|
100
|
-
effect_config: Additional configuration for effect nodes
|
|
101
|
-
|
|
102
|
-
Returns:
|
|
103
|
-
Self for method chaining
|
|
104
|
-
"""
|
|
105
|
-
if node_id in self.nodes:
|
|
106
|
-
raise ValueError(f"Node '{node_id}' already exists in flow '{self.flow_id}'")
|
|
107
|
-
|
|
108
|
-
self.nodes[node_id] = FlowNode(
|
|
109
|
-
id=node_id,
|
|
110
|
-
handler=handler,
|
|
111
|
-
input_key=input_key,
|
|
112
|
-
output_key=output_key,
|
|
113
|
-
effect_type=effect_type,
|
|
114
|
-
effect_config=effect_config,
|
|
115
|
-
)
|
|
116
|
-
return self
|
|
117
|
-
|
|
118
|
-
def add_edge(
|
|
119
|
-
self,
|
|
120
|
-
source: str,
|
|
121
|
-
target: str,
|
|
122
|
-
*,
|
|
123
|
-
condition: Optional[Callable[[Dict[str, Any]], bool]] = None,
|
|
124
|
-
source_handle: Optional[str] = None,
|
|
125
|
-
) -> "Flow":
|
|
126
|
-
"""Add an edge between nodes.
|
|
127
|
-
|
|
128
|
-
Args:
|
|
129
|
-
source: ID of the source node
|
|
130
|
-
target: ID of the target node
|
|
131
|
-
condition: Optional condition function for conditional routing
|
|
132
|
-
|
|
133
|
-
Returns:
|
|
134
|
-
Self for method chaining
|
|
135
|
-
"""
|
|
136
|
-
self.edges.append(
|
|
137
|
-
FlowEdge(
|
|
138
|
-
source=source,
|
|
139
|
-
target=target,
|
|
140
|
-
condition=condition,
|
|
141
|
-
source_handle=source_handle,
|
|
142
|
-
)
|
|
143
|
-
)
|
|
144
|
-
return self
|
|
145
|
-
|
|
146
|
-
def set_entry(self, node_id: str) -> "Flow":
|
|
147
|
-
"""Set the entry node for the flow.
|
|
148
|
-
|
|
149
|
-
Args:
|
|
150
|
-
node_id: ID of the entry node
|
|
151
|
-
|
|
152
|
-
Returns:
|
|
153
|
-
Self for method chaining
|
|
154
|
-
"""
|
|
155
|
-
if node_id not in self.nodes:
|
|
156
|
-
raise ValueError(f"Entry node '{node_id}' not found in flow '{self.flow_id}'")
|
|
157
|
-
self.entry_node = node_id
|
|
158
|
-
return self
|
|
159
|
-
|
|
160
|
-
def set_exit(self, node_id: str) -> "Flow":
|
|
161
|
-
"""Set the exit node for the flow (optional, can be inferred).
|
|
162
|
-
|
|
163
|
-
Args:
|
|
164
|
-
node_id: ID of the exit node
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
Self for method chaining
|
|
168
|
-
"""
|
|
169
|
-
if node_id not in self.nodes:
|
|
170
|
-
raise ValueError(f"Exit node '{node_id}' not found in flow '{self.flow_id}'")
|
|
171
|
-
self.exit_node = node_id
|
|
172
|
-
return self
|
|
173
|
-
|
|
174
|
-
def validate(self) -> List[str]:
|
|
175
|
-
"""Validate the flow definition.
|
|
176
|
-
|
|
177
|
-
Returns:
|
|
178
|
-
List of validation error messages (empty if valid)
|
|
179
|
-
"""
|
|
180
|
-
errors = []
|
|
181
|
-
|
|
182
|
-
if not self.entry_node:
|
|
183
|
-
errors.append("Flow must have an entry node")
|
|
184
|
-
elif self.entry_node not in self.nodes:
|
|
185
|
-
errors.append(f"Entry node '{self.entry_node}' not found")
|
|
186
|
-
|
|
187
|
-
# Check that all edge endpoints exist
|
|
188
|
-
for edge in self.edges:
|
|
189
|
-
if edge.source not in self.nodes:
|
|
190
|
-
errors.append(f"Edge source '{edge.source}' not found")
|
|
191
|
-
if edge.target not in self.nodes:
|
|
192
|
-
errors.append(f"Edge target '{edge.target}' not found")
|
|
193
|
-
|
|
194
|
-
# Check for unreachable nodes
|
|
195
|
-
if self.entry_node:
|
|
196
|
-
reachable = self._find_reachable_nodes(self.entry_node)
|
|
197
|
-
for node_id in self.nodes:
|
|
198
|
-
if node_id not in reachable:
|
|
199
|
-
errors.append(f"Node '{node_id}' is unreachable from entry")
|
|
200
|
-
|
|
201
|
-
return errors
|
|
202
|
-
|
|
203
|
-
def _find_reachable_nodes(self, start: str) -> set:
|
|
204
|
-
"""Find all nodes reachable from a starting node."""
|
|
205
|
-
reachable = set()
|
|
206
|
-
to_visit = [start]
|
|
207
|
-
|
|
208
|
-
while to_visit:
|
|
209
|
-
current = to_visit.pop()
|
|
210
|
-
if current in reachable:
|
|
211
|
-
continue
|
|
212
|
-
reachable.add(current)
|
|
213
|
-
|
|
214
|
-
# Find outgoing edges
|
|
215
|
-
for edge in self.edges:
|
|
216
|
-
if edge.source == current and edge.target not in reachable:
|
|
217
|
-
to_visit.append(edge.target)
|
|
218
|
-
|
|
219
|
-
return reachable
|
|
220
|
-
|
|
221
|
-
def get_next_nodes(self, node_id: str) -> List[str]:
|
|
222
|
-
"""Get the next nodes from a given node.
|
|
223
|
-
|
|
224
|
-
Args:
|
|
225
|
-
node_id: ID of the current node
|
|
226
|
-
|
|
227
|
-
Returns:
|
|
228
|
-
List of target node IDs
|
|
229
|
-
"""
|
|
230
|
-
return [edge.target for edge in self.edges if edge.source == node_id]
|
|
231
|
-
|
|
232
|
-
def get_terminal_nodes(self) -> List[str]:
|
|
233
|
-
"""Get nodes with no outgoing edges (terminal nodes).
|
|
234
|
-
|
|
235
|
-
Returns:
|
|
236
|
-
List of terminal node IDs
|
|
237
|
-
"""
|
|
238
|
-
sources = {edge.source for edge in self.edges}
|
|
239
|
-
return [node_id for node_id in self.nodes if node_id not in sources]
|
|
240
|
-
|
|
241
|
-
def __repr__(self) -> str:
|
|
242
|
-
return (
|
|
243
|
-
f"Flow(id={self.flow_id!r}, "
|
|
244
|
-
f"nodes={len(self.nodes)}, "
|
|
245
|
-
f"edges={len(self.edges)}, "
|
|
246
|
-
f"entry={self.entry_node!r})"
|
|
247
|
-
)
|
|
11
|
+
__all__ = ["Flow", "FlowNode", "FlowEdge"]
|
abstractflow/runner.py
CHANGED
|
@@ -66,7 +66,45 @@ class FlowRunner:
|
|
|
66
66
|
"""Get the current run ID."""
|
|
67
67
|
return self._current_run_id
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _normalize_completed_output(raw: Any) -> Dict[str, Any]:
|
|
71
|
+
"""Normalize workflow completion output for host callers.
|
|
72
|
+
|
|
73
|
+
Runtime-level workflows may complete with various output shapes:
|
|
74
|
+
- VisualFlow On Flow End: {"my_output": ..., "success": True}
|
|
75
|
+
- Terminal node returning scalar: {"response": 123, "success": True}
|
|
76
|
+
- Legacy / explicit: {"result": ..., "success": True}
|
|
77
|
+
|
|
78
|
+
AbstractFlow's public contract is: {"success": bool, "result": Any, ...}.
|
|
79
|
+
"""
|
|
80
|
+
if not isinstance(raw, dict):
|
|
81
|
+
return {"success": True, "result": raw}
|
|
82
|
+
|
|
83
|
+
success = raw.get("success")
|
|
84
|
+
if success is False:
|
|
85
|
+
# Preserve error shape (tests + callers expect top-level "error"/"node", etc).
|
|
86
|
+
return raw
|
|
87
|
+
|
|
88
|
+
# Prefer explicit `result` when present (visual flows may also keep
|
|
89
|
+
# top-level keys for UI/WS convenience).
|
|
90
|
+
if "result" in raw:
|
|
91
|
+
return {"success": True, "result": raw.get("result")}
|
|
92
|
+
|
|
93
|
+
payload = {k: v for k, v in raw.items() if k != "success"}
|
|
94
|
+
if len(payload) == 1:
|
|
95
|
+
(only_key, only_val) = next(iter(payload.items()))
|
|
96
|
+
if only_key in {"result", "response"}:
|
|
97
|
+
return {"success": True, "result": only_val}
|
|
98
|
+
|
|
99
|
+
return {"success": True, "result": payload}
|
|
100
|
+
|
|
101
|
+
def start(
|
|
102
|
+
self,
|
|
103
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
104
|
+
*,
|
|
105
|
+
actor_id: Optional[str] = None,
|
|
106
|
+
session_id: Optional[str] = None,
|
|
107
|
+
) -> str:
|
|
70
108
|
"""Start flow execution.
|
|
71
109
|
|
|
72
110
|
Args:
|
|
@@ -79,6 +117,8 @@ class FlowRunner:
|
|
|
79
117
|
self._current_run_id = self.runtime.start(
|
|
80
118
|
workflow=self.workflow,
|
|
81
119
|
vars=vars_dict,
|
|
120
|
+
actor_id=actor_id,
|
|
121
|
+
session_id=session_id,
|
|
82
122
|
)
|
|
83
123
|
return self._current_run_id
|
|
84
124
|
|
|
@@ -103,7 +143,13 @@ class FlowRunner:
|
|
|
103
143
|
max_steps=max_steps,
|
|
104
144
|
)
|
|
105
145
|
|
|
106
|
-
def run(
|
|
146
|
+
def run(
|
|
147
|
+
self,
|
|
148
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
149
|
+
*,
|
|
150
|
+
actor_id: Optional[str] = None,
|
|
151
|
+
session_id: Optional[str] = None,
|
|
152
|
+
) -> Dict[str, Any]:
|
|
107
153
|
"""Execute flow to completion.
|
|
108
154
|
|
|
109
155
|
This method starts the flow and runs until it completes, fails,
|
|
@@ -121,7 +167,7 @@ class FlowRunner:
|
|
|
121
167
|
"""
|
|
122
168
|
from abstractruntime.core.models import RunStatus, WaitReason
|
|
123
169
|
|
|
124
|
-
self.start(input_data)
|
|
170
|
+
self.start(input_data, actor_id=actor_id, session_id=session_id)
|
|
125
171
|
|
|
126
172
|
while True:
|
|
127
173
|
state = self.runtime.tick(
|
|
@@ -130,7 +176,7 @@ class FlowRunner:
|
|
|
130
176
|
)
|
|
131
177
|
|
|
132
178
|
if state.status == RunStatus.COMPLETED:
|
|
133
|
-
return state.output
|
|
179
|
+
return self._normalize_completed_output(state.output)
|
|
134
180
|
|
|
135
181
|
if state.status == RunStatus.FAILED:
|
|
136
182
|
raise RuntimeError(f"Flow failed: {state.error}")
|
|
@@ -283,13 +329,21 @@ class FlowRunner:
|
|
|
283
329
|
if not self._current_run_id:
|
|
284
330
|
raise ValueError("No active run to resume.")
|
|
285
331
|
|
|
286
|
-
|
|
332
|
+
state = self.runtime.resume(
|
|
287
333
|
workflow=self.workflow,
|
|
288
334
|
run_id=self._current_run_id,
|
|
289
335
|
wait_key=wait_key,
|
|
290
336
|
payload=payload or {},
|
|
291
337
|
max_steps=max_steps,
|
|
292
338
|
)
|
|
339
|
+
try:
|
|
340
|
+
from abstractruntime.core.models import RunStatus
|
|
341
|
+
|
|
342
|
+
if getattr(state, "status", None) == RunStatus.COMPLETED:
|
|
343
|
+
state.output = self._normalize_completed_output(getattr(state, "output", None)) # type: ignore[attr-defined]
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
return state
|
|
293
347
|
|
|
294
348
|
def get_state(self) -> Optional["RunState"]:
|
|
295
349
|
"""Get the current run state.
|
abstractflow/visual/agent_ids.py
CHANGED
|
@@ -1,29 +1,5 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
Visual Agent nodes are compiled into `START_SUBWORKFLOW` effects that reference
|
|
4
|
-
an AbstractAgent ReAct workflow registered in the runtime's WorkflowRegistry.
|
|
5
|
-
|
|
6
|
-
IDs must be stable across hosts so a VisualFlow JSON document can be executed
|
|
7
|
-
outside the web editor (CLI, AbstractCode, third-party apps).
|
|
8
|
-
"""
|
|
1
|
+
"""Re-export: AbstractRuntime VisualFlow compiler agent IDs."""
|
|
9
2
|
|
|
10
3
|
from __future__ import annotations
|
|
11
4
|
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
_SAFE_ID_RE = re.compile(r"[^a-zA-Z0-9_-]+")
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _sanitize(value: str) -> str:
|
|
19
|
-
value = str(value or "").strip()
|
|
20
|
-
if not value:
|
|
21
|
-
return "unknown"
|
|
22
|
-
value = _SAFE_ID_RE.sub("_", value)
|
|
23
|
-
return value or "unknown"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def visual_react_workflow_id(*, flow_id: str, node_id: str) -> str:
|
|
27
|
-
"""Return the workflow_id used for a VisualFlow Agent node's ReAct subworkflow."""
|
|
28
|
-
return f"visual_react_agent_{_sanitize(flow_id)}_{_sanitize(node_id)}"
|
|
29
|
-
|
|
5
|
+
from abstractruntime.visualflow_compiler.visual.agent_ids import * # noqa: F401,F403
|