generflow-core 0.2.0__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.
@@ -0,0 +1,332 @@
1
+ """GF-Lang parser with partial-stream recovery.
2
+
3
+ Designed for streaming: feed lines one at a time via feed_line(), get
4
+ back the parsed nodes that completed. Lines that don't parse cleanly
5
+ are flagged as invalid but never crash the stream.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from typing import Iterator
11
+
12
+ from .ast import Assignment, Component, Literal, Ref
13
+
14
+
15
+ _IDENT = re.compile(r"[a-zA-Z_][a-zA-Z0-9_\-]*")
16
+ _STRING_DQ = re.compile(r'"((?:[^"\\]|\\.)*)"')
17
+ _STRING_SQ = re.compile(r"'((?:[^'\\]|\\.)*)'")
18
+ _NUMBER = re.compile(r"-?\d+(?:\.\d+)?")
19
+ _BOOL = re.compile(r"(true|false)\b")
20
+
21
+
22
+ class ParseError(Exception):
23
+ """Raised when a complete line cannot be parsed."""
24
+
25
+
26
+ class GFLangParser:
27
+ """Incremental, partial-recovery parser for GF-Lang.
28
+
29
+ Usage:
30
+ parser = GFLangParser()
31
+ for event in stream:
32
+ nodes = parser.feed(event.line)
33
+ for node in nodes:
34
+ render(node)
35
+ leftover = parser.flush() # anything left in buffer
36
+ """
37
+
38
+ def __init__(self) -> None:
39
+ self._buf = ""
40
+
41
+ def feed_line(self, line: str) -> list[Assignment | Component]:
42
+ """DEPRECATED: use feed_chunk."""
43
+ return self.feed_chunk(line + "\n")
44
+
45
+ def feed_chunk(self, chunk: str) -> list[Assignment | Component]:
46
+ """Feed a raw chunk (may contain partial lines, partial expressions).
47
+
48
+ State machine:
49
+ - Buffer accumulates text until the parser can extract a complete
50
+ top-level node (a balanced `Component(...)` or `root = Component(...)`).
51
+ - We retry parsing the buffer in a loop; if a complete node is
52
+ found, it's emitted and removed from the buffer.
53
+ - If the buffer is mid-construct (open parens/brackets), we hold it
54
+ and wait for more input.
55
+ - If the buffer starts with non-identifier garbage, drop everything
56
+ up to the next newline so a bad prefix doesn't block us.
57
+ """
58
+ out: list[Assignment | Component] = []
59
+ self._buf += chunk
60
+ # safety counter to prevent infinite loops
61
+ max_iter = 1000
62
+ for _ in range(max_iter):
63
+ node, consumed = self._try_parse_top(self._buf)
64
+ if node is None:
65
+ break
66
+ self._buf = self._buf[consumed:]
67
+ out.append(node)
68
+ # partial-recovery loop: drop garbage prefixes and try again
69
+ for _ in range(10):
70
+ if not self._buf or self._is_mid_construct(self._buf):
71
+ break
72
+ before = self._buf
73
+ self._drop_garbage_line()
74
+ if self._buf == before:
75
+ break
76
+ # try parse again after drop
77
+ for _ in range(max_iter):
78
+ node, consumed = self._try_parse_top(self._buf)
79
+ if node is None:
80
+ break
81
+ self._buf = self._buf[consumed:]
82
+ out.append(node)
83
+ return out
84
+
85
+ def _is_mid_construct(self, s: str) -> bool:
86
+ """True if the buffer has unbalanced parens/brackets (more opens than closes)."""
87
+ parens = s.count("(") - s.count(")")
88
+ brackets = s.count("[") - s.count("]")
89
+ return parens > 0 or brackets > 0
90
+
91
+ def _drop_garbage_line(self) -> None:
92
+ """If the buffer doesn't start with a valid start, drop the first line."""
93
+ s = self._buf.lstrip()
94
+ if not s:
95
+ return
96
+ # valid start: an alpha/underscore character (identifier) — could be
97
+ # a component, an assignment, or an Update( expression
98
+ if s[0].isalpha() or s[0] == "_":
99
+ return
100
+ # garbage: drop everything up to and including the first newline
101
+ if "\n" in self._buf:
102
+ self._buf = self._buf.split("\n", 1)[1]
103
+ else:
104
+ self._buf = ""
105
+
106
+ def flush(self) -> list[Assignment | Component]:
107
+ """Force-parse anything left in the buffer (e.g., at end of stream)."""
108
+ out: list[Assignment | Component] = []
109
+ if self._buf.strip():
110
+ out.extend(self.feed_line(self._buf))
111
+ self._buf = ""
112
+ return out
113
+
114
+ # --- internal parsing -------------------------------------------------
115
+
116
+ def _try_parse_top(self, src: str) -> tuple[Assignment | Component | None, int]:
117
+ """Try to parse one top-level node from `src`. Returns (node, chars_consumed) or (None, 0)."""
118
+ src = src.lstrip()
119
+ if not src:
120
+ return None, 0
121
+ # assignment: identifier = ...
122
+ m = _IDENT.match(src)
123
+ if not m:
124
+ return None, 0
125
+ ident = m.group(0)
126
+ rest_start = m.end()
127
+ # peek next non-space char
128
+ ws = re.match(r"[ \t]*", src[rest_start:])
129
+ rest_start += ws.end() if ws else 0
130
+ if rest_start < len(src) and src[rest_start] == "=":
131
+ # assignment
132
+ after_eq = rest_start + 1
133
+ try:
134
+ node, end = self._parse_node(src, after_eq)
135
+ except ParseError:
136
+ return None, 0
137
+ if end is None:
138
+ return None, 0
139
+ consumed = end - (m.start() if False else 0)
140
+ consumed = end
141
+ return Assignment(identifier=ident, value=node), end
142
+ # component
143
+ try:
144
+ node, end = self._parse_component(src, 0)
145
+ except ParseError:
146
+ return None, 0
147
+ if end is None:
148
+ return None, 0
149
+ return node, end
150
+
151
+ def _skip_ws(self, src: str, i: int) -> int:
152
+ while i < len(src) and src[i] in " \t\n":
153
+ i += 1
154
+ return i
155
+
156
+ def _parse_node(
157
+ self, src: str, i: int
158
+ ) -> tuple[Assignment | Component | Literal | Ref | None, int | None]:
159
+ i = self._skip_ws(src, i)
160
+ if i >= len(src):
161
+ return None, None
162
+ c = src[i]
163
+ if c == '"' or c == "'":
164
+ return self._parse_string(src, i)
165
+ if c == "-" or c.isdigit():
166
+ return self._parse_number(src, i)
167
+ if c == "[":
168
+ return self._parse_list(src, i)
169
+ if c == "$":
170
+ return self._parse_ref(src, i)
171
+ if c.isalpha() or c == "_":
172
+ # could be bool, component, or bare identifier (string shorthand)
173
+ m = _BOOL.match(src, i)
174
+ if m:
175
+ return Literal(value=(m.group(1) == "true")), m.end()
176
+ # peek: if followed by '=', it's a named arg's value, not a component
177
+ # BUT we don't know that here — we just check if the char after the
178
+ # ident is whitespace+')' or ',' or whitespace+ident. If so, it's
179
+ # actually a component. Otherwise we still treat as component (caller
180
+ # distinguishes). For bare ident shorthand: if the next char is
181
+ # NOT '(' it's a string-literal shorthand.
182
+ m_id = _IDENT.match(src, i)
183
+ if m_id:
184
+ after = m_id.end()
185
+ if after >= len(src) or src[after] != "(":
186
+ # bare identifier → treat as string literal
187
+ return Literal(value=m_id.group(0)), after
188
+ # try component
189
+ try:
190
+ node, end = self._parse_component(src, i)
191
+ except ParseError:
192
+ return None, None
193
+ return node, end
194
+ raise ParseError(f"unexpected char {c!r} at {i}")
195
+
196
+ def _parse_string(self, src: str, i: int) -> tuple[Literal, int]:
197
+ quote = src[i]
198
+ end = i + 1
199
+ buf = []
200
+ while end < len(src):
201
+ ch = src[end]
202
+ if ch == "\\" and end + 1 < len(src):
203
+ buf.append(src[end + 1])
204
+ end += 2
205
+ continue
206
+ if ch == quote:
207
+ return Literal(value="".join(buf)), end + 1
208
+ buf.append(ch)
209
+ end += 1
210
+ raise ParseError("unterminated string")
211
+
212
+ def _parse_number(self, src: str, i: int) -> tuple[Literal, int]:
213
+ m = _NUMBER.match(src, i)
214
+ if not m:
215
+ raise ParseError(f"bad number at {i}")
216
+ s = m.group(0)
217
+ val: float | int = float(s) if "." in s else int(s)
218
+ return Literal(value=val), m.end()
219
+
220
+ def _parse_list(self, src: str, i: int) -> tuple[Literal, int]:
221
+ assert src[i] == "["
222
+ items = []
223
+ i += 1
224
+ i = self._skip_ws(src, i)
225
+ if i < len(src) and src[i] == "]":
226
+ return Literal(value=[]), i + 1
227
+ while i < len(src):
228
+ i = self._skip_ws(src, i)
229
+ if i >= len(src):
230
+ raise ParseError("unterminated list")
231
+ if src[i] == "]":
232
+ return Literal(value=items), i + 1
233
+ node, end = self._parse_node(src, i)
234
+ if end is None:
235
+ raise ParseError("incomplete list element")
236
+ items.append(node)
237
+ i = end
238
+ i = self._skip_ws(src, i)
239
+ if i < len(src) and src[i] == ",":
240
+ i += 1
241
+ elif i < len(src) and src[i] == "]":
242
+ return Literal(value=items), i + 1
243
+ raise ParseError("unterminated list")
244
+
245
+ def _parse_ref(self, src: str, i: int) -> tuple[Ref, int]:
246
+ assert src[i] == "$"
247
+ i += 1
248
+ m = _IDENT.match(src, i)
249
+ if not m:
250
+ raise ParseError("bad ref")
251
+ return Ref(name=m.group(0)), m.end()
252
+
253
+ def _parse_component(
254
+ self, src: str, i: int
255
+ ) -> tuple[Component, int | None]:
256
+ m = _IDENT.match(src, i)
257
+ if not m:
258
+ raise ParseError(f"expected identifier at {i}")
259
+ name = m.group(0)
260
+ i = m.end()
261
+ i = self._skip_ws(src, i)
262
+ if i >= len(src) or src[i] != "(":
263
+ # bare identifier — treat as no-arg component
264
+ return Component(name=name, args=[], kwargs={}), i
265
+ i += 1
266
+ args: list = []
267
+ kwargs: dict = {}
268
+ positional_idx = 0
269
+ def _finalize(args, kwargs):
270
+ """Move trailing list-literal of Components into children."""
271
+ if args and isinstance(args[-1], Literal) and isinstance(args[-1].value, list):
272
+ if all(isinstance(c, Component) for c in args[-1].value):
273
+ return args[:-1], kwargs, args[-1].value
274
+ return args, kwargs, []
275
+ # parse args
276
+ i = self._skip_ws(src, i)
277
+ if i < len(src) and src[i] == ")":
278
+ args, kwargs, children = _finalize(args, kwargs)
279
+ return Component(name=name, args=args, kwargs=kwargs, children=children), i + 1
280
+ while i < len(src):
281
+ i = self._skip_ws(src, i)
282
+ if i >= len(src):
283
+ return Component(name=name), None # incomplete
284
+ if src[i] == ")":
285
+ args, kwargs, children = _finalize(args, kwargs)
286
+ return Component(name=name, args=args, kwargs=kwargs, children=children), i + 1
287
+ # check for named arg: ident =
288
+ save_i = i
289
+ m2 = _IDENT.match(src, i)
290
+ if m2:
291
+ after_ident = m2.end()
292
+ ws_m = re.match(r"[ \t]*", src[after_ident:])
293
+ peek = after_ident + (ws_m.end() if ws_m else 0)
294
+ if peek < len(src) and src[peek] == "=":
295
+ key = m2.group(0)
296
+ val, end = self._parse_node(src, peek + 1)
297
+ if end is None:
298
+ return Component(name=name), None
299
+ kwargs[key] = val
300
+ i = end
301
+ else:
302
+ # positional
303
+ val, end = self._parse_node(src, i)
304
+ if end is None:
305
+ return Component(name=name), None
306
+ args.append(val)
307
+ i = end
308
+ else:
309
+ val, end = self._parse_node(src, i)
310
+ if end is None:
311
+ return Component(name=name), None
312
+ args.append(val)
313
+ i = end
314
+ i = self._skip_ws(src, i)
315
+ if i < len(src) and src[i] == ",":
316
+ i += 1
317
+ elif i < len(src) and src[i] == ")":
318
+ args, kwargs, children = _finalize(args, kwargs)
319
+ return Component(name=name, args=args, kwargs=kwargs, children=children), i + 1
320
+ else:
321
+ # maybe end of input — let caller retry
322
+ return Component(name=name), None
323
+ raise ParseError("unterminated component args")
324
+
325
+
326
+ def count_tokens(spec: str) -> int:
327
+ """Approximate token count (whitespace-split, treating punctuation as
328
+ separate tokens). Good enough for benchmarking. For real numbers use
329
+ tiktoken in benchmarks/run.py."""
330
+ # split on whitespace and common punctuation
331
+ tokens = re.findall(r"[a-zA-Z_][a-zA-Z0-9_\-]*|[(){}\[\],]|\d+|\S", spec)
332
+ return len(tokens)
@@ -0,0 +1,136 @@
1
+ """Surgical updates: emit `Update(path=..., value=...)` instead of full re-render.
2
+
3
+ The LLM emits `Update(...)` as a top-level line. The renderer applies the
4
+ patch to its in-memory spec tree and re-renders only the affected nodes.
5
+ This is the multi-turn efficiency win — turn 2+ shouldn't pay the full
6
+ spec cost just to change one number.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import dataclass
12
+ from typing import Any
13
+
14
+ from .ast import Assignment, Component, Literal, Ref
15
+
16
+
17
+ @dataclass
18
+ class Update:
19
+ path: str # e.g. "root.children[2].kwargs.value" or "header.title"
20
+ value: Any # the new value (Literal, Ref, or nested Component)
21
+ raw_line: str = ""
22
+
23
+ def to_dict(self) -> dict:
24
+ v = self.value
25
+ if hasattr(v, "to_dict"):
26
+ v = v.to_dict()
27
+ return {"type": "update", "path": self.path, "value": v}
28
+
29
+
30
+ # Match: Update(path="root.children[2].kwargs.value", value=42)
31
+ _UPDATE_RE = re.compile(
32
+ r'Update\s*\(\s*path\s*=\s*("[^"]+"|[A-Za-z_][\w\-.]*)\s*,\s*value\s*=\s*(.+?)\s*\)\s*$',
33
+ re.DOTALL,
34
+ )
35
+
36
+
37
+ def parse_update(line: str, parse_value) -> Update | None:
38
+ """Try to parse a line as an Update expression.
39
+
40
+ `parse_value` is a callable that parses an arbitrary value expression
41
+ (we pass our spec parser to avoid circular imports).
42
+ """
43
+ m = _UPDATE_RE.match(line.strip())
44
+ if not m:
45
+ return None
46
+ path = m.group(1).strip('"').strip("'")
47
+ raw_value = m.group(2)
48
+ try:
49
+ val = parse_value(raw_value)
50
+ except Exception:
51
+ return None
52
+ return Update(path=path, value=val, raw_line=line)
53
+
54
+
55
+ def apply_update(spec_tree: Any, update: Update) -> bool:
56
+ """Apply an update to a spec tree (mutates in place).
57
+
58
+ `spec_tree` is a Component (root) or a dict (when stored as JSON).
59
+
60
+ Path syntax: dot-separated, with optional [index] for children.
61
+ Examples:
62
+ "kwargs.value" → set tree.kwargs["value"]
63
+ "children[2]" → tree.children[2] = value
64
+ "children[2].kwargs.x" → tree.children[2].kwargs["x"]
65
+ """
66
+ parts = _tokenize_path(update.path)
67
+ if not parts:
68
+ return False
69
+ node = spec_tree
70
+ for part in parts[:-1]:
71
+ node = _descend(node, part)
72
+ if node is None:
73
+ return False
74
+ last = parts[-1]
75
+ val = update.value
76
+ if hasattr(val, "to_dict"):
77
+ val = val.to_dict()
78
+ if last["kind"] == "key":
79
+ if not isinstance(node, dict):
80
+ return False
81
+ node[last["name"]] = val
82
+ elif last["kind"] == "index":
83
+ if not isinstance(node, list):
84
+ return False
85
+ idx = last["index"]
86
+ if idx < len(node):
87
+ node[idx] = val
88
+ return True
89
+
90
+
91
+ def _tokenize_path(path: str) -> list[dict]:
92
+ """Tokenize "a.b[2].c" into [{'kind':'key','name':'a'}, {'kind':'key','name':'b'}, ...]"""
93
+ out: list[dict] = []
94
+ i = 0
95
+ name = ""
96
+ while i < len(path):
97
+ c = path[i]
98
+ if c == ".":
99
+ if name:
100
+ out.append({"kind": "key", "name": name})
101
+ name = ""
102
+ elif c == "[":
103
+ if name:
104
+ out.append({"kind": "key", "name": name})
105
+ name = ""
106
+ j = path.find("]", i)
107
+ if j < 0:
108
+ return []
109
+ try:
110
+ idx = int(path[i + 1 : j])
111
+ except ValueError:
112
+ return []
113
+ out.append({"kind": "index", "index": idx})
114
+ i = j
115
+ else:
116
+ name += c
117
+ i += 1
118
+ if name:
119
+ out.append({"kind": "key", "name": name})
120
+ return out
121
+
122
+
123
+ def _descend(node: Any, part: dict) -> Any:
124
+ if part["kind"] == "key":
125
+ if isinstance(node, dict):
126
+ return node.get(part["name"])
127
+ if hasattr(node, "kwargs") and part["name"] in node.kwargs:
128
+ return node.kwargs[part["name"]]
129
+ if hasattr(node, "__dict__") and part["name"] in node.__dict__:
130
+ return getattr(node, part["name"])
131
+ elif part["kind"] == "index":
132
+ if isinstance(node, list):
133
+ idx = part["index"]
134
+ if idx < len(node):
135
+ return node[idx]
136
+ return None
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: generflow-core
3
+ Version: 0.2.0
4
+ Summary: Generflow Python runtime — streams low-token UI specs over SSE, with HITL checkpoints, live data binding, action gating, and rewind/replay.
5
+ Author-email: Generflow <team@generflow.dev>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://generflow.dev
8
+ Project-URL: Repository, https://github.com/generflow/generflow
9
+ Project-URL: Documentation, https://docs.generflow.dev
10
+ Project-URL: Issues, https://github.com/generflow/generflow/issues
11
+ Project-URL: Changelog, https://github.com/generflow/generflow/blob/main/CHANGELOG.md
12
+ Keywords: ai,generative-ui,llm,sse,agent,hitl,human-in-the-loop,design-system,streaming,tooling
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: Apache Software License
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 :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ Requires-Dist: fastapi>=0.110
28
+ Requires-Dist: uvicorn[standard]>=0.27
29
+ Requires-Dist: pydantic>=2.5
30
+ Requires-Dist: openai>=1.12
31
+ Requires-Dist: anthropic>=0.18
32
+ Requires-Dist: pyyaml>=6.0
33
+ Requires-Dist: tiktoken>=0.5
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=7.4; extra == "dev"
36
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
37
+ Requires-Dist: httpx>=0.26; extra == "dev"
38
+ Requires-Dist: build>=1.0; extra == "dev"
39
+ Requires-Dist: twine>=4.0; extra == "dev"
40
+ Provides-Extra: all
41
+ Requires-Dist: generflow-core[dev]; extra == "all"
42
+
43
+ # generflow-core
44
+
45
+ > The Python runtime for [Generflow](https://generflow.dev) — open-source generative UI.
46
+
47
+ Generflow solves four problems nobody else has solved together:
48
+
49
+ 1. **Token-efficient UI description** — GF-Lang is a streaming-friendly DSL that uses **57.6% fewer tokens** than equivalent JSON (benchmarked, beats JSON on 10/10 widgets).
50
+ 2. **Data-grounded rendering** — components can declare `$ref`s to live data sources (REST, SQL, GraphQL, MCP). No fabricated numbers.
51
+ 3. **HITL hallucination mitigation** — every render passes through a security boundary (component allow-list), and every action is gated by HITL checks (PII scan, missing source, ambiguity, low confidence).
52
+ 4. **Cross-platform** — Web Components + React + vanilla TS renderers. Same protocol, any framework.
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install generflow-core
58
+ ```
59
+
60
+ Or with all dev extras:
61
+
62
+ ```bash
63
+ pip install generflow-core[dev]
64
+ ```
65
+
66
+ ## Quickstart (no API key)
67
+
68
+ ```python
69
+ from generflow_core.adapters import EchoAdapter
70
+ from generflow_core.spec import GFLangParser
71
+ from generflow_core.registry import Registry
72
+ import asyncio
73
+
74
+ async def main():
75
+ adapter = EchoAdapter()
76
+ registry = Registry()
77
+ parser = GFLangParser()
78
+
79
+ full = ""
80
+ async for chunk in adapter.stream("", "build a dashboard"):
81
+ full += chunk
82
+
83
+ for node in parser.feed_chunk(full):
84
+ if registry.has(node.name):
85
+ print(f" ✓ {node.name}")
86
+
87
+ asyncio.run(main())
88
+ ```
89
+
90
+ ## Quickstart (real LLM)
91
+
92
+ ```bash
93
+ export OPENAI_API_KEY=sk-...
94
+ python -m generflow_core.api.app # → http://localhost:7878
95
+ ```
96
+
97
+ Then send a chat message:
98
+
99
+ ```bash
100
+ curl -X POST http://localhost:7878/v1/stream \
101
+ -H "Content-Type: application/json" \
102
+ -d '{"message": "build a sales dashboard", "session_id": "demo"}'
103
+ ```
104
+
105
+ ## CLI
106
+
107
+ The package ships a CLI for local-only rendering (no LLM, no API key):
108
+
109
+ ```bash
110
+ generflow-render render dashboard.gf -o dashboard.html
111
+ generflow-render validate dashboard.gf
112
+ generflow-render diff before.gf after.gf
113
+ generflow-render to-a2ui dashboard.gf -o dashboard.a2ui.json
114
+ generflow-render from-a2ui incoming.a2ui.json -o spec.gf
115
+ ```
116
+
117
+ ## Architecture
118
+
119
+ ```
120
+ ┌──────────────────┐ SSE ┌──────────────────┐
121
+ │ LLM Adapter │ ─────────▶ │ SSE /v1/stream │
122
+ │ (OpenAI/Anthro/ │ │ │
123
+ │ Echo) │ │ parser (Python) │
124
+ └──────────────────┘ │ │ │
125
+ │ ▼ │
126
+ │ Registry │
127
+ │ (allow-list) │
128
+ │ │ │
129
+ │ ▼ │
130
+ │ DataResolver │
131
+ │ (REST/SQL/GQL) │
132
+ │ │ │
133
+ │ ▼ │
134
+ │ HITL gates │
135
+ │ (PII/conf/etc) │
136
+ │ │ │
137
+ │ ▼ │
138
+ │ Action dispatch │
139
+ │ (intent → HTTP) │
140
+ └──────────────────┘
141
+
142
+
143
+ SSE events: spec.line,
144
+ component.mount, data.fill,
145
+ hitl.request, action.request,
146
+ update, stream.end
147
+ ```
148
+
149
+ ## Protocol
150
+
151
+ Generflow's protocol is a stream of typed SSE events. See the [GF-Lang grammar](https://github.com/generflow/generflow/blob/main/docs/gf-lang.ebnf) and the [event reference](https://docs.generflow.dev/protocol).
152
+
153
+ ```http
154
+ event: session.start
155
+ data: {"session_id":"demo","adapter":"openai"}
156
+
157
+ event: spec.line
158
+ data: {"path":"line:1","component":"Card","node":{...},"valid":true}
159
+
160
+ event: data.fill
161
+ data: {"ref":"monthly_revenue","ok":true,"value":[...]}
@@ -0,0 +1,30 @@
1
+ generflow_core/__init__.py,sha256=9_wGxsl3sTZAnOO5vHaBUh6Fuv1cI4vjeiJVF8hFAEg,79
2
+ generflow_core/cli.py,sha256=XbqsMBoYJ0-0MGZkpcTr2RPGotPBoW7XURfh296YlTY,10547
3
+ generflow_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ generflow_core/actions/__init__.py,sha256=mGy1yad7jxhCWxw01jOntgx77SO95PXMDvkU0eK4cWw,401
5
+ generflow_core/actions/dispatcher.py,sha256=O5wA_Ex45r5EZawZNyJuipCtnj1UFfAyPhpRrY7PZC8,7102
6
+ generflow_core/adapters/__init__.py,sha256=Q6xYvblGccMLT3o-YZ3m6onGYh0AMCf7S4o9dJtFAO0,312
7
+ generflow_core/adapters/llm.py,sha256=J9N6MLvNJWzOg3khrMGi_FQenfVWmg1KPfihcOoB3BE,5665
8
+ generflow_core/api/__init__.py,sha256=gS04y9nwp8fHbgV4ZXPly8ULHXDhyZ_-nSMwLeM_H4Q,170
9
+ generflow_core/api/app.py,sha256=oSYwWPNZ1Cq6eQpgGUHpnx8Nww-6e1AEH2KlzADvIjo,18662
10
+ generflow_core/api/prompt.py,sha256=rkgsqkrA1FGYlkGFkptXtg46P8AUSRm44K97WVef7Q4,2900
11
+ generflow_core/databind/__init__.py,sha256=DiSEDrLggOrdK8i9oaChFOdykrrvowjj82EOZ_e0lso,577
12
+ generflow_core/databind/config.py,sha256=ME-1aIFD-WEWFJhqX08WFAQmmDZazHTGzM60QY07fJY,6546
13
+ generflow_core/databind/resolver.py,sha256=WElpGRKCxk6iRIcEXIM0au24PvHhAtJ0N2JeNj1vLI0,11635
14
+ generflow_core/hitl/__init__.py,sha256=M5YJOtyFOjFmFAEJhbOPzWuJzI2kNOu6iN_ncdBP4zg,418
15
+ generflow_core/hitl/gates.py,sha256=TTqi6pbhFdzF7t5WBLcYc-8b-fLvC9xBvjk6c-Hkd4M,6119
16
+ generflow_core/interop/__init__.py,sha256=rE-DV3pnpX5aX8tyDcDMhlMX_ME1spZ7K9lcEyJhRmk,8773
17
+ generflow_core/observability/__init__.py,sha256=BEKiFiWW3G3Qh87T2WFdL0rgLBQqXifHaJ49vDNK0vA,8145
18
+ generflow_core/registry/__init__.py,sha256=1bNFobFtUwkVfRiMnBWZbdEOLHGu17R9bymSIt8963M,253
19
+ generflow_core/registry/registry.py,sha256=LhFAVHUZfT4BldSbnve0jrGWVlFlSW3WuumjqPZQPas,8891
20
+ generflow_core/replay/__init__.py,sha256=6NiTo6j4I9BG466rrWvNlBTJCbo4x2oas7NyQeg55CQ,6724
21
+ generflow_core/spec/__init__.py,sha256=oJtqqgkL_tr6INxZ39HJQ1QClRDe9sBQ2vxeRY6X-x0,510
22
+ generflow_core/spec/ast.py,sha256=GcQGMVDwZoZNh24APmF9wge6BtjOtDySV3OjsXKb5wY,1493
23
+ generflow_core/spec/diff.py,sha256=amfAvP1kYKYV4Ol4xXjPKGDgoYqlnyt0E-KGCnF9ubw,6823
24
+ generflow_core/spec/parser.py,sha256=Y6Qc08m0uayciO9UxVmsipN4lacZj10HBqSOPocYw6c,12811
25
+ generflow_core/spec/update.py,sha256=XM-tJMPYEvaGij-Up8P9I4YDkwUrlnGr5Z4I9tkckVw,4181
26
+ generflow_core-0.2.0.dist-info/METADATA,sha256=B-rCYy4UZruDx3dVWcvl4603_79h_yryJPjOOHY3Ows,6199
27
+ generflow_core-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
28
+ generflow_core-0.2.0.dist-info/entry_points.txt,sha256=we2c_hJ4DOq3jFZxfkXRXijx3fLU-zKrcD6N7GgFgWI,101
29
+ generflow_core-0.2.0.dist-info/top_level.txt,sha256=BD7V2_uvP1hK4AG78t58JF6-ignzxGYIasrHIfG-m64,15
30
+ generflow_core-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ generflow = generflow_core.api.app:main
3
+ generflow-render = generflow_core.cli:main
@@ -0,0 +1 @@
1
+ generflow_core