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/core/flow.py CHANGED
@@ -1,247 +1,11 @@
1
1
  """Flow definition classes for AbstractFlow.
2
2
 
3
- This module provides the core data structures for defining flows:
4
- - Flow: A directed graph of nodes connected by edges
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 dataclasses import dataclass
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
- if TYPE_CHECKING:
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
- def start(self, input_data: Optional[Dict[str, Any]] = None) -> str:
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(self, input_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
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 or {}
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
- return self.runtime.resume(
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.
@@ -1,29 +1,5 @@
1
- """Deterministic workflow IDs for VisualFlow Agent nodes.
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 re
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