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
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""WorkflowBundle (.flow) tooling (authoring-side).
|
|
2
|
+
|
|
3
|
+
This module remains for backwards compatibility, but bundling logic lives in
|
|
4
|
+
`abstractruntime.workflow_bundle` so hosts and clients share the same semantics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import zipfile
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
from abstractruntime.workflow_bundle import (
|
|
17
|
+
WORKFLOW_BUNDLE_FORMAT_VERSION_V1,
|
|
18
|
+
WorkflowBundleEntrypoint,
|
|
19
|
+
WorkflowBundleManifest,
|
|
20
|
+
WorkflowBundleError,
|
|
21
|
+
open_workflow_bundle,
|
|
22
|
+
workflow_bundle_manifest_to_dict,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from .visual.models import NodeType, VisualFlow
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class PackedWorkflowBundle:
|
|
30
|
+
path: Path
|
|
31
|
+
manifest: WorkflowBundleManifest
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _now_iso() -> str:
|
|
35
|
+
return datetime.now(timezone.utc).isoformat()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _read_json_bytes(path: Path) -> bytes:
|
|
39
|
+
return path.read_bytes()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _load_visualflow_from_bytes(raw: bytes) -> VisualFlow:
|
|
43
|
+
data = json.loads(raw.decode("utf-8"))
|
|
44
|
+
return VisualFlow.model_validate(data)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _node_type_str(node: Any) -> str:
|
|
48
|
+
t = getattr(node, "type", None)
|
|
49
|
+
return t.value if hasattr(t, "value") else str(t or "")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _reachable_exec_node_ids(flow: VisualFlow) -> set[str]:
|
|
53
|
+
"""Return exec-reachable node ids (Blueprint-style; ignores disconnected exec nodes)."""
|
|
54
|
+
exec_ids: set[str] = set()
|
|
55
|
+
for n in flow.nodes:
|
|
56
|
+
data = dict(n.data) if isinstance(n.data, dict) else {}
|
|
57
|
+
pins = data.get("inputs") if isinstance(data.get("inputs"), list) else []
|
|
58
|
+
pins2 = data.get("outputs") if isinstance(data.get("outputs"), list) else []
|
|
59
|
+
for p in list(pins) + list(pins2):
|
|
60
|
+
if isinstance(p, dict) and p.get("type") == "execution":
|
|
61
|
+
exec_ids.add(str(n.id))
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
if not exec_ids:
|
|
65
|
+
return set()
|
|
66
|
+
|
|
67
|
+
incoming_exec = {e.target for e in flow.edges if getattr(e, "targetHandle", None) == "exec-in"}
|
|
68
|
+
|
|
69
|
+
roots: list[str] = []
|
|
70
|
+
if isinstance(flow.entryNode, str) and flow.entryNode in exec_ids:
|
|
71
|
+
roots.append(flow.entryNode)
|
|
72
|
+
for n in flow.nodes:
|
|
73
|
+
if _node_type_str(n) == str(NodeType.ON_EVENT.value) and n.id in exec_ids:
|
|
74
|
+
roots.append(n.id)
|
|
75
|
+
if not roots:
|
|
76
|
+
for n in flow.nodes:
|
|
77
|
+
if n.id in exec_ids and n.id not in incoming_exec:
|
|
78
|
+
roots.append(n.id)
|
|
79
|
+
break
|
|
80
|
+
if not roots:
|
|
81
|
+
roots.append(next(iter(exec_ids)))
|
|
82
|
+
|
|
83
|
+
adj: Dict[str, list[str]] = {}
|
|
84
|
+
for e in flow.edges:
|
|
85
|
+
if getattr(e, "targetHandle", None) != "exec-in":
|
|
86
|
+
continue
|
|
87
|
+
if e.source not in exec_ids or e.target not in exec_ids:
|
|
88
|
+
continue
|
|
89
|
+
adj.setdefault(e.source, []).append(e.target)
|
|
90
|
+
|
|
91
|
+
reachable: set[str] = set()
|
|
92
|
+
stack = list(dict.fromkeys([r for r in roots if isinstance(r, str) and r]))
|
|
93
|
+
while stack:
|
|
94
|
+
cur = stack.pop()
|
|
95
|
+
if cur in reachable:
|
|
96
|
+
continue
|
|
97
|
+
reachable.add(cur)
|
|
98
|
+
for nxt in adj.get(cur, []):
|
|
99
|
+
if nxt not in reachable:
|
|
100
|
+
stack.append(nxt)
|
|
101
|
+
return reachable
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _collect_reachable_flows(
|
|
105
|
+
*, root_flow: VisualFlow, root_bytes: bytes, flows_dir: Path
|
|
106
|
+
) -> Tuple[List[Tuple[str, VisualFlow, bytes]], List[str]]:
|
|
107
|
+
"""Return [(flow_id, flow, raw_bytes)] in discovery order + list of missing subflow ids."""
|
|
108
|
+
ordered: list[Tuple[str, VisualFlow, bytes]] = []
|
|
109
|
+
visited: set[str] = set()
|
|
110
|
+
missing: list[str] = []
|
|
111
|
+
|
|
112
|
+
# Memoize loaded files by id for reuse.
|
|
113
|
+
cache: Dict[str, Tuple[VisualFlow, bytes]] = {str(root_flow.id): (root_flow, root_bytes)}
|
|
114
|
+
|
|
115
|
+
def _load_by_id(flow_id: str) -> Optional[Tuple[VisualFlow, bytes]]:
|
|
116
|
+
fid = str(flow_id or "").strip()
|
|
117
|
+
if not fid:
|
|
118
|
+
return None
|
|
119
|
+
if fid in cache:
|
|
120
|
+
return cache[fid]
|
|
121
|
+
p = (flows_dir / f"{fid}.json").resolve()
|
|
122
|
+
if not p.exists():
|
|
123
|
+
return None
|
|
124
|
+
raw = _read_json_bytes(p)
|
|
125
|
+
vf = _load_visualflow_from_bytes(raw)
|
|
126
|
+
cache[fid] = (vf, raw)
|
|
127
|
+
return cache[fid]
|
|
128
|
+
|
|
129
|
+
def _dfs(vf: VisualFlow, raw: bytes) -> None:
|
|
130
|
+
fid = str(vf.id)
|
|
131
|
+
if fid in visited:
|
|
132
|
+
return
|
|
133
|
+
visited.add(fid)
|
|
134
|
+
ordered.append((fid, vf, raw))
|
|
135
|
+
|
|
136
|
+
reachable = _reachable_exec_node_ids(vf)
|
|
137
|
+
for n in vf.nodes:
|
|
138
|
+
if _node_type_str(n) != str(NodeType.SUBFLOW.value):
|
|
139
|
+
continue
|
|
140
|
+
if reachable and n.id not in reachable:
|
|
141
|
+
continue
|
|
142
|
+
data = n.data if isinstance(n.data, dict) else {}
|
|
143
|
+
sub_id = data.get("subflowId") or data.get("flowId")
|
|
144
|
+
if not isinstance(sub_id, str) or not sub_id.strip():
|
|
145
|
+
# Match VisualFlow executor behavior: fail if a reachable subflow is malformed.
|
|
146
|
+
missing.append(f"<missing-subflow-id:{fid}:{n.id}>")
|
|
147
|
+
continue
|
|
148
|
+
sub_id = sub_id.strip()
|
|
149
|
+
child = _load_by_id(sub_id)
|
|
150
|
+
if child is None:
|
|
151
|
+
# Self-recursion is valid even if the file isn't duplicated on disk.
|
|
152
|
+
if sub_id == fid:
|
|
153
|
+
_dfs(vf, raw)
|
|
154
|
+
continue
|
|
155
|
+
missing.append(sub_id)
|
|
156
|
+
continue
|
|
157
|
+
_dfs(child[0], child[1])
|
|
158
|
+
|
|
159
|
+
_dfs(root_flow, root_bytes)
|
|
160
|
+
return ordered, missing
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def pack_workflow_bundle(
|
|
164
|
+
*,
|
|
165
|
+
root_flow_json: str | Path,
|
|
166
|
+
out_path: str | Path,
|
|
167
|
+
bundle_id: Optional[str] = None,
|
|
168
|
+
bundle_version: str = "0.0.0",
|
|
169
|
+
flows_dir: Optional[str | Path] = None,
|
|
170
|
+
entrypoints: Optional[List[str]] = None,
|
|
171
|
+
default_entrypoint: Optional[str] = None,
|
|
172
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
173
|
+
) -> PackedWorkflowBundle:
|
|
174
|
+
"""Pack a `.flow` bundle from a root VisualFlow JSON file."""
|
|
175
|
+
root_path = Path(root_flow_json).expanduser().resolve()
|
|
176
|
+
if not root_path.exists():
|
|
177
|
+
raise FileNotFoundError(f"root flow not found: {root_path}")
|
|
178
|
+
root_bytes = _read_json_bytes(root_path)
|
|
179
|
+
root_flow = _load_visualflow_from_bytes(root_bytes)
|
|
180
|
+
|
|
181
|
+
flows_base = Path(flows_dir).expanduser().resolve() if flows_dir is not None else root_path.parent
|
|
182
|
+
if not flows_base.exists() or not flows_base.is_dir():
|
|
183
|
+
raise FileNotFoundError(f"flows_dir does not exist: {flows_base}")
|
|
184
|
+
|
|
185
|
+
ordered, missing = _collect_reachable_flows(root_flow=root_flow, root_bytes=root_bytes, flows_dir=flows_base)
|
|
186
|
+
if missing:
|
|
187
|
+
uniq = sorted(set(missing))
|
|
188
|
+
raise WorkflowBundleError(f"Missing referenced subflows in flows_dir: {uniq}")
|
|
189
|
+
|
|
190
|
+
# Entry points: default to root flow id.
|
|
191
|
+
entry_ids = list(entrypoints) if isinstance(entrypoints, list) and entrypoints else [str(root_flow.id)]
|
|
192
|
+
entry_ids = [str(x).strip() for x in entry_ids if isinstance(x, str) and str(x).strip()]
|
|
193
|
+
if not entry_ids:
|
|
194
|
+
raise WorkflowBundleError("No valid entrypoints specified")
|
|
195
|
+
|
|
196
|
+
de_param = str(default_entrypoint).strip() if isinstance(default_entrypoint, str) and str(default_entrypoint).strip() else ""
|
|
197
|
+
if de_param and de_param not in entry_ids:
|
|
198
|
+
raise WorkflowBundleError(f"default_entrypoint '{de_param}' must be one of: {entry_ids}")
|
|
199
|
+
default_ep = de_param or (str(root_flow.id).strip() if str(root_flow.id).strip() in entry_ids else entry_ids[0])
|
|
200
|
+
|
|
201
|
+
# Compile artifacts for all included flows.
|
|
202
|
+
flows_json: Dict[str, bytes] = {}
|
|
203
|
+
interfaces_by_flow: Dict[str, list[str]] = {}
|
|
204
|
+
name_by_flow: Dict[str, str] = {}
|
|
205
|
+
desc_by_flow: Dict[str, str] = {}
|
|
206
|
+
|
|
207
|
+
for fid, vf, raw in ordered:
|
|
208
|
+
flows_json[fid] = raw
|
|
209
|
+
name_by_flow[fid] = str(getattr(vf, "name", "") or "")
|
|
210
|
+
desc_by_flow[fid] = str(getattr(vf, "description", "") or "")
|
|
211
|
+
interfaces_by_flow[fid] = list(getattr(vf, "interfaces", []) or [])
|
|
212
|
+
|
|
213
|
+
bid = str(bundle_id or "").strip() or str(root_flow.id)
|
|
214
|
+
created_at = _now_iso()
|
|
215
|
+
|
|
216
|
+
eps: list[WorkflowBundleEntrypoint] = []
|
|
217
|
+
for fid in entry_ids:
|
|
218
|
+
fid2 = str(fid or "").strip()
|
|
219
|
+
if not fid2:
|
|
220
|
+
continue
|
|
221
|
+
eps.append(
|
|
222
|
+
WorkflowBundleEntrypoint(
|
|
223
|
+
flow_id=fid2,
|
|
224
|
+
name=name_by_flow.get(fid2) or fid2,
|
|
225
|
+
description=desc_by_flow.get(fid2, ""),
|
|
226
|
+
interfaces=list(interfaces_by_flow.get(fid2, [])),
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
if not eps:
|
|
230
|
+
raise WorkflowBundleError("No valid entrypoints specified")
|
|
231
|
+
|
|
232
|
+
manifest = WorkflowBundleManifest(
|
|
233
|
+
bundle_format_version=WORKFLOW_BUNDLE_FORMAT_VERSION_V1,
|
|
234
|
+
bundle_id=bid,
|
|
235
|
+
bundle_version=str(bundle_version or "0.0.0"),
|
|
236
|
+
created_at=created_at,
|
|
237
|
+
entrypoints=eps,
|
|
238
|
+
default_entrypoint=default_ep,
|
|
239
|
+
flows={fid: f"flows/{fid}.json" for fid in sorted(flows_json.keys())},
|
|
240
|
+
artifacts={},
|
|
241
|
+
assets={},
|
|
242
|
+
metadata=dict(metadata) if isinstance(metadata, dict) else {},
|
|
243
|
+
)
|
|
244
|
+
manifest.validate()
|
|
245
|
+
|
|
246
|
+
out = Path(out_path).expanduser().resolve()
|
|
247
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
248
|
+
|
|
249
|
+
# Deterministic write order for reproducibility.
|
|
250
|
+
with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
251
|
+
zf.writestr("manifest.json", json.dumps(workflow_bundle_manifest_to_dict(manifest), indent=2, ensure_ascii=False))
|
|
252
|
+
for fid in sorted(flows_json.keys()):
|
|
253
|
+
zf.writestr(f"flows/{fid}.json", flows_json[fid])
|
|
254
|
+
|
|
255
|
+
return PackedWorkflowBundle(path=out, manifest=manifest)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def inspect_workflow_bundle(*, bundle_path: str | Path) -> WorkflowBundleManifest:
|
|
259
|
+
b = open_workflow_bundle(bundle_path)
|
|
260
|
+
return b.manifest
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def unpack_workflow_bundle(*, bundle_path: str | Path, out_dir: str | Path) -> Path:
|
|
264
|
+
src = Path(bundle_path).expanduser().resolve()
|
|
265
|
+
out = Path(out_dir).expanduser().resolve()
|
|
266
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
|
|
268
|
+
if src.is_dir():
|
|
269
|
+
# Directory bundle: copy files (best-effort).
|
|
270
|
+
for p in src.rglob("*"):
|
|
271
|
+
if p.is_dir():
|
|
272
|
+
continue
|
|
273
|
+
rel = p.relative_to(src)
|
|
274
|
+
dst = out / rel
|
|
275
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
dst.write_bytes(p.read_bytes())
|
|
277
|
+
return out
|
|
278
|
+
|
|
279
|
+
with zipfile.ZipFile(src, "r") as zf:
|
|
280
|
+
zf.extractall(out)
|
|
281
|
+
return out
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# Backwards-compat: prefer the shared stdlib-only implementation in AbstractRuntime.
|
|
285
|
+
from abstractruntime.workflow_bundle import ( # noqa: E402
|
|
286
|
+
PackedWorkflowBundle as PackedWorkflowBundle,
|
|
287
|
+
inspect_workflow_bundle as inspect_workflow_bundle,
|
|
288
|
+
pack_workflow_bundle as pack_workflow_bundle,
|
|
289
|
+
unpack_workflow_bundle as unpack_workflow_bundle,
|
|
290
|
+
)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abstractflow
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Diagram-based AI workflow generation built on AbstractCore
|
|
5
|
+
Author-email: AbstractFlow Team <contact@abstractflow.ai>
|
|
6
|
+
Maintainer-email: AbstractFlow Team <contact@abstractflow.ai>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/lpalbou/AbstractFlow
|
|
9
|
+
Project-URL: Documentation, https://abstractflow.readthedocs.io
|
|
10
|
+
Project-URL: Repository, https://github.com/lpalbou/AbstractFlow
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/lpalbou/AbstractFlow/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/lpalbou/AbstractFlow/blob/main/CHANGELOG.md
|
|
13
|
+
Keywords: ai,workflow,diagram,llm,automation,visual-programming,abstractcore,machine-learning
|
|
14
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: AbstractRuntime>=0.4.0
|
|
29
|
+
Requires-Dist: abstractcore[tools]>=2.6.8
|
|
30
|
+
Requires-Dist: pydantic>=2.0.0
|
|
31
|
+
Requires-Dist: typing-extensions>=4.0.0
|
|
32
|
+
Provides-Extra: agent
|
|
33
|
+
Requires-Dist: abstractagent>=0.2.0; extra == "agent"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
37
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: isort>=5.12.0; extra == "dev"
|
|
39
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
40
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
42
|
+
Provides-Extra: server
|
|
43
|
+
Requires-Dist: fastapi>=0.100.0; extra == "server"
|
|
44
|
+
Requires-Dist: uvicorn[standard]>=0.23.0; extra == "server"
|
|
45
|
+
Requires-Dist: websockets>=11.0.0; extra == "server"
|
|
46
|
+
Provides-Extra: ui
|
|
47
|
+
Requires-Dist: streamlit>=1.28.0; extra == "ui"
|
|
48
|
+
Requires-Dist: plotly>=5.15.0; extra == "ui"
|
|
49
|
+
Requires-Dist: networkx>=3.1.0; extra == "ui"
|
|
50
|
+
Provides-Extra: all
|
|
51
|
+
Requires-Dist: abstractflow[agent,dev,server,ui]; extra == "all"
|
|
52
|
+
Dynamic: license-file
|
|
53
|
+
|
|
54
|
+
# AbstractFlow
|
|
55
|
+
|
|
56
|
+
Diagram-based, **durable** AI workflows for Python.
|
|
57
|
+
|
|
58
|
+
AbstractFlow provides:
|
|
59
|
+
- A portable workflow format (`VisualFlow` JSON) and helpers to execute it from any host (`abstractflow.visual`).
|
|
60
|
+
- A simple programmatic API (`Flow`, `FlowRunner`) backed by **AbstractRuntime**.
|
|
61
|
+
- A reference visual editor app in `web/` (FastAPI backend + React frontend).
|
|
62
|
+
|
|
63
|
+
Project status: **Pre-alpha** (`pyproject.toml`: `Development Status :: 2 - Pre-Alpha`). Expect breaking changes.
|
|
64
|
+
|
|
65
|
+
## Capabilities (implemented)
|
|
66
|
+
|
|
67
|
+
- Execute programmatic flows (`Flow` → `FlowRunner`) with a default in-memory runtime.
|
|
68
|
+
- Execute portable `VisualFlow` JSON from any host process (`abstractflow.visual`).
|
|
69
|
+
- Durable waits and resumption via AbstractRuntime (e.g. user/event/schedule waits).
|
|
70
|
+
- Package a flow tree as a WorkflowBundle (`.flow`) via the CLI.
|
|
71
|
+
- Author/run VisualFlows in the reference web editor (`web/`).
|
|
72
|
+
|
|
73
|
+
Evidence (code): `abstractflow/runner.py`, `abstractflow/visual/executor.py`, `abstractflow/cli.py`, `web/backend/routes/ws.py`.
|
|
74
|
+
|
|
75
|
+
## Docs
|
|
76
|
+
|
|
77
|
+
- Start here: `docs/getting-started.md`
|
|
78
|
+
- API reference: `docs/api.md`
|
|
79
|
+
- VisualFlow format: `docs/visualflow.md`
|
|
80
|
+
- Visual editor: `docs/web-editor.md`
|
|
81
|
+
- CLI: `docs/cli.md`
|
|
82
|
+
- FAQ: `docs/faq.md`
|
|
83
|
+
- Architecture: `docs/architecture.md`
|
|
84
|
+
- Docs index: `docs/README.md`
|
|
85
|
+
|
|
86
|
+
## Installation
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install abstractflow
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Requirements: Python **3.10+** (`pyproject.toml`: `requires-python`).
|
|
93
|
+
|
|
94
|
+
Optional extras:
|
|
95
|
+
- Agent nodes (ReAct workflows): `pip install "abstractflow[agent]"`
|
|
96
|
+
- Dev tools (tests/formatting): `pip install "abstractflow[dev]"`
|
|
97
|
+
|
|
98
|
+
Notes:
|
|
99
|
+
- `abstractflow` depends on `AbstractRuntime` and `abstractcore[tools]` (see `pyproject.toml`).
|
|
100
|
+
- Some VisualFlow node types require additional packages (e.g. `memory_kg_*` nodes need `abstractmemory`).
|
|
101
|
+
|
|
102
|
+
## Quickstart (programmatic)
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from abstractflow import Flow, FlowRunner
|
|
106
|
+
|
|
107
|
+
flow = Flow("linear")
|
|
108
|
+
flow.add_node("double", lambda x: x * 2, input_key="value", output_key="doubled")
|
|
109
|
+
flow.add_node("add_ten", lambda x: x + 10, input_key="doubled", output_key="final")
|
|
110
|
+
flow.add_edge("double", "add_ten")
|
|
111
|
+
flow.set_entry("double")
|
|
112
|
+
|
|
113
|
+
result = FlowRunner(flow).run({"value": 5})
|
|
114
|
+
print(result) # {"success": True, "result": 20}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Quickstart (execute a VisualFlow JSON)
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import json
|
|
121
|
+
from abstractflow.visual import VisualFlow, execute_visual_flow
|
|
122
|
+
|
|
123
|
+
with open("my-flow.json", "r", encoding="utf-8") as f:
|
|
124
|
+
vf = VisualFlow.model_validate(json.load(f))
|
|
125
|
+
result = execute_visual_flow(vf, {"prompt": "Hello"}, flows={vf.id: vf})
|
|
126
|
+
print(result) # {"success": True, "waiting": False, "result": ...}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If your flow uses subflows, load all referenced `*.json` into the `flows={...}` mapping (see `docs/getting-started.md`).
|
|
130
|
+
|
|
131
|
+
## Visual editor (from source)
|
|
132
|
+
|
|
133
|
+
The visual editor is a dev/reference app in `web/` (not shipped as a Python package on PyPI).
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
git clone https://github.com/lpalbou/AbstractFlow.git
|
|
137
|
+
cd AbstractFlow
|
|
138
|
+
|
|
139
|
+
python -m venv .venv
|
|
140
|
+
source .venv/bin/activate
|
|
141
|
+
pip install -e ".[server,agent]"
|
|
142
|
+
|
|
143
|
+
# Terminal 1: Backend (FastAPI)
|
|
144
|
+
cd web && python -m backend --reload --port 8080
|
|
145
|
+
|
|
146
|
+
# Terminal 2: Frontend (Vite)
|
|
147
|
+
cd web/frontend && npm install && npm run dev
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Open the frontend at http://localhost:3003 (default Vite port). See `docs/web-editor.md`.
|
|
151
|
+
|
|
152
|
+
## CLI (WorkflowBundle `.flow`)
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
abstractflow bundle pack web/flows/ac-echo.json --out /tmp/ac-echo.flow
|
|
156
|
+
abstractflow bundle inspect /tmp/ac-echo.flow
|
|
157
|
+
abstractflow bundle unpack /tmp/ac-echo.flow --dir /tmp/ac-echo
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
See `docs/cli.md` and `abstractflow/cli.py`.
|
|
161
|
+
|
|
162
|
+
## Related projects
|
|
163
|
+
|
|
164
|
+
- AbstractRuntime (durable execution kernel): https://github.com/lpalbou/AbstractRuntime
|
|
165
|
+
- AbstractCore (providers/models/tools): https://github.com/lpalbou/AbstractCore
|
|
166
|
+
- AbstractAgent (ReAct/CodeAct): https://github.com/lpalbou/AbstractAgent
|
|
167
|
+
|
|
168
|
+
## Changelog
|
|
169
|
+
|
|
170
|
+
See `CHANGELOG.md`.
|
|
171
|
+
|
|
172
|
+
## Contributing
|
|
173
|
+
|
|
174
|
+
See `CONTRIBUTING.md`.
|
|
175
|
+
|
|
176
|
+
## Security
|
|
177
|
+
|
|
178
|
+
See `SECURITY.md`.
|
|
179
|
+
|
|
180
|
+
## Acknowledgments
|
|
181
|
+
|
|
182
|
+
See `ACKNOWLEDMENTS.md`.
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
abstractflow/__init__.py,sha256=0c08cMHJC5c-tAjh9up90y7cG-SUa2rTf3Nhf2w718o,2586
|
|
2
|
+
abstractflow/__main__.py,sha256=nl-6C1KVPCcPfrIM67zYnyotsKI8XaNpIsNJ1EZ6S5w,175
|
|
3
|
+
abstractflow/cli.py,sha256=oU-XeqXuYkJxTBVkm-sHksYeYYy658owE9WwRamGAoI,3240
|
|
4
|
+
abstractflow/compiler.py,sha256=E71lsuqZWYlpRh4iPRGs2ahZVyjvwNHlOepgHaTKTxA,634
|
|
5
|
+
abstractflow/py.typed,sha256=QjzTvd0siCmvIPhJL7u61TKvbkV9Ba_WXrDMoyIZ3Fc,76
|
|
6
|
+
abstractflow/runner.py,sha256=97Aepi3NUtygp8NbjLrv-6HZPCRciAytKEix5ZK4QtI,16173
|
|
7
|
+
abstractflow/workflow_bundle.py,sha256=Adv3yT-odPn6bLN4OovJ6OuT8De1JBbxqxVsAt_7uCI,10596
|
|
8
|
+
abstractflow/adapters/__init__.py,sha256=1My7MaQqrg9DRGNR3fq0PSGfs06ZpzuwHJnj7LRqX9A,360
|
|
9
|
+
abstractflow/adapters/agent_adapter.py,sha256=3MsCKK22RuIDEjko5w-FyhdsnA3OuzRf5curyszXUbY,191
|
|
10
|
+
abstractflow/adapters/control_adapter.py,sha256=CZdW1incL-y7Ef6DKlorHbNyucGMpP_0KqpkUeCboOY,193
|
|
11
|
+
abstractflow/adapters/effect_adapter.py,sha256=X0cc5WREn1aU0MLbdSPQHodfEQDvV_3t4KJXC0wfeKE,192
|
|
12
|
+
abstractflow/adapters/event_adapter.py,sha256=NzupBnN_jNQ1moGbQ4NEBw_8L1-a834gjBbMJ2NnHfI,191
|
|
13
|
+
abstractflow/adapters/function_adapter.py,sha256=7EfM22zr0MlVVugaUPmJcoOpixgB_Xm36_J5enH-X4s,194
|
|
14
|
+
abstractflow/adapters/subflow_adapter.py,sha256=R6YI3ZUd1OX3ttzjjh36QS47rUySC1MSA74Wrp3H3WY,193
|
|
15
|
+
abstractflow/adapters/variable_adapter.py,sha256=wa0zPYN2M45iKbVE_W1iW5MtXY2iHCdSqLPhqOq7Rpw,194
|
|
16
|
+
abstractflow/core/__init__.py,sha256=1Qo2qFHgi49nTwGYQaLv9ywwSR6kwRNQQoZFjqXzPmU,120
|
|
17
|
+
abstractflow/core/flow.py,sha256=nQW-KDN7iNbKpAY4ONmB_ZPbMK3gWIVgxJDGwcTf7Ds,335
|
|
18
|
+
abstractflow/visual/__init__.py,sha256=R8pahi0TZ5-mpxR8PXgni_YUiJKQdJZYcNpE9QjnP9I,965
|
|
19
|
+
abstractflow/visual/agent_ids.py,sha256=0L4ngxBxuLaLufA35q9wZTYVgC5FDO1skZPN5a3hvmc,187
|
|
20
|
+
abstractflow/visual/builtins.py,sha256=QVfIRPid4p45bDvj9RBPjnBkynhgcY3547jR7wNUkd8,185
|
|
21
|
+
abstractflow/visual/code_executor.py,sha256=gsYsPGTta5jnKiEourXygjMkRJWmvHYiquNwEFZFSvI,195
|
|
22
|
+
abstractflow/visual/event_ids.py,sha256=NzSry5Omjbxnn163CP5PJ3CQJ2dXo-Flf5sBROLjeYQ,858
|
|
23
|
+
abstractflow/visual/executor.py,sha256=Vt-Luy1z5SvCsq6WZfTFJVeLOg0YujxNdt4HfagG10I,42754
|
|
24
|
+
abstractflow/visual/interfaces.py,sha256=DcGwBKY7VxGH5-uC6MmXJD5CNpdYaqVal6ufjADqa6w,16051
|
|
25
|
+
abstractflow/visual/models.py,sha256=8ze_g4olmP7XFAAugmBcVvoW6jxtzMPLHDvguFRIhkI,8402
|
|
26
|
+
abstractflow/visual/session_runner.py,sha256=egdBXYHCg5esRaOzqIco30AzvoiVAaDvgZQDDNuVSmg,7603
|
|
27
|
+
abstractflow/visual/workspace_scoped_tools.py,sha256=epSnsUvuEtuxg1pep8gnjDSX5-phq3rkZsJlGJCOQ8g,1029
|
|
28
|
+
abstractflow-0.3.1.dist-info/licenses/LICENSE,sha256=_zpmvJ804El0O63VB5CWtADw0TccHWYvCRLm9c05blM,1076
|
|
29
|
+
abstractflow-0.3.1.dist-info/METADATA,sha256=B2jw5IEMR9gqPGsx91CTncZNK1j4FwhsHIO5XzrPTP8,6406
|
|
30
|
+
abstractflow-0.3.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
31
|
+
abstractflow-0.3.1.dist-info/entry_points.txt,sha256=Gc916Xwp7HMEOUlxFYHn7lMRrOT3Ah0Q_3tP9S8LHP0,55
|
|
32
|
+
abstractflow-0.3.1.dist-info/top_level.txt,sha256=bimZZ-20W8CxqozcCSWc_NlDus4gBMlKsMZC7xQxzww,13
|
|
33
|
+
abstractflow-0.3.1.dist-info/RECORD,,
|