generflow-core 0.2.0__tar.gz

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 (38) hide show
  1. generflow_core-0.2.0/PKG-INFO +161 -0
  2. generflow_core-0.2.0/README.md +119 -0
  3. generflow_core-0.2.0/pyproject.toml +91 -0
  4. generflow_core-0.2.0/setup.cfg +4 -0
  5. generflow_core-0.2.0/src/generflow_core/__init__.py +3 -0
  6. generflow_core-0.2.0/src/generflow_core/actions/__init__.py +22 -0
  7. generflow_core-0.2.0/src/generflow_core/actions/dispatcher.py +223 -0
  8. generflow_core-0.2.0/src/generflow_core/adapters/__init__.py +11 -0
  9. generflow_core-0.2.0/src/generflow_core/adapters/llm.py +186 -0
  10. generflow_core-0.2.0/src/generflow_core/api/__init__.py +5 -0
  11. generflow_core-0.2.0/src/generflow_core/api/app.py +494 -0
  12. generflow_core-0.2.0/src/generflow_core/api/prompt.py +64 -0
  13. generflow_core-0.2.0/src/generflow_core/cli.py +241 -0
  14. generflow_core-0.2.0/src/generflow_core/databind/__init__.py +30 -0
  15. generflow_core-0.2.0/src/generflow_core/databind/config.py +183 -0
  16. generflow_core-0.2.0/src/generflow_core/databind/resolver.py +306 -0
  17. generflow_core-0.2.0/src/generflow_core/hitl/__init__.py +22 -0
  18. generflow_core-0.2.0/src/generflow_core/hitl/gates.py +165 -0
  19. generflow_core-0.2.0/src/generflow_core/interop/__init__.py +257 -0
  20. generflow_core-0.2.0/src/generflow_core/observability/__init__.py +208 -0
  21. generflow_core-0.2.0/src/generflow_core/py.typed +0 -0
  22. generflow_core-0.2.0/src/generflow_core/registry/__init__.py +4 -0
  23. generflow_core-0.2.0/src/generflow_core/registry/registry.py +194 -0
  24. generflow_core-0.2.0/src/generflow_core/replay/__init__.py +189 -0
  25. generflow_core-0.2.0/src/generflow_core/spec/__init__.py +21 -0
  26. generflow_core-0.2.0/src/generflow_core/spec/ast.py +61 -0
  27. generflow_core-0.2.0/src/generflow_core/spec/diff.py +177 -0
  28. generflow_core-0.2.0/src/generflow_core/spec/parser.py +332 -0
  29. generflow_core-0.2.0/src/generflow_core/spec/update.py +136 -0
  30. generflow_core-0.2.0/src/generflow_core.egg-info/PKG-INFO +161 -0
  31. generflow_core-0.2.0/src/generflow_core.egg-info/SOURCES.txt +36 -0
  32. generflow_core-0.2.0/src/generflow_core.egg-info/dependency_links.txt +1 -0
  33. generflow_core-0.2.0/src/generflow_core.egg-info/entry_points.txt +3 -0
  34. generflow_core-0.2.0/src/generflow_core.egg-info/requires.txt +17 -0
  35. generflow_core-0.2.0/src/generflow_core.egg-info/top_level.txt +1 -0
  36. generflow_core-0.2.0/tests/test_parser.py +76 -0
  37. generflow_core-0.2.0/tests/test_phase2.py +248 -0
  38. generflow_core-0.2.0/tests/test_phase3.py +288 -0
@@ -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,119 @@
1
+ # generflow-core
2
+
3
+ > The Python runtime for [Generflow](https://generflow.dev) — open-source generative UI.
4
+
5
+ Generflow solves four problems nobody else has solved together:
6
+
7
+ 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).
8
+ 2. **Data-grounded rendering** — components can declare `$ref`s to live data sources (REST, SQL, GraphQL, MCP). No fabricated numbers.
9
+ 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).
10
+ 4. **Cross-platform** — Web Components + React + vanilla TS renderers. Same protocol, any framework.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install generflow-core
16
+ ```
17
+
18
+ Or with all dev extras:
19
+
20
+ ```bash
21
+ pip install generflow-core[dev]
22
+ ```
23
+
24
+ ## Quickstart (no API key)
25
+
26
+ ```python
27
+ from generflow_core.adapters import EchoAdapter
28
+ from generflow_core.spec import GFLangParser
29
+ from generflow_core.registry import Registry
30
+ import asyncio
31
+
32
+ async def main():
33
+ adapter = EchoAdapter()
34
+ registry = Registry()
35
+ parser = GFLangParser()
36
+
37
+ full = ""
38
+ async for chunk in adapter.stream("", "build a dashboard"):
39
+ full += chunk
40
+
41
+ for node in parser.feed_chunk(full):
42
+ if registry.has(node.name):
43
+ print(f" ✓ {node.name}")
44
+
45
+ asyncio.run(main())
46
+ ```
47
+
48
+ ## Quickstart (real LLM)
49
+
50
+ ```bash
51
+ export OPENAI_API_KEY=sk-...
52
+ python -m generflow_core.api.app # → http://localhost:7878
53
+ ```
54
+
55
+ Then send a chat message:
56
+
57
+ ```bash
58
+ curl -X POST http://localhost:7878/v1/stream \
59
+ -H "Content-Type: application/json" \
60
+ -d '{"message": "build a sales dashboard", "session_id": "demo"}'
61
+ ```
62
+
63
+ ## CLI
64
+
65
+ The package ships a CLI for local-only rendering (no LLM, no API key):
66
+
67
+ ```bash
68
+ generflow-render render dashboard.gf -o dashboard.html
69
+ generflow-render validate dashboard.gf
70
+ generflow-render diff before.gf after.gf
71
+ generflow-render to-a2ui dashboard.gf -o dashboard.a2ui.json
72
+ generflow-render from-a2ui incoming.a2ui.json -o spec.gf
73
+ ```
74
+
75
+ ## Architecture
76
+
77
+ ```
78
+ ┌──────────────────┐ SSE ┌──────────────────┐
79
+ │ LLM Adapter │ ─────────▶ │ SSE /v1/stream │
80
+ │ (OpenAI/Anthro/ │ │ │
81
+ │ Echo) │ │ parser (Python) │
82
+ └──────────────────┘ │ │ │
83
+ │ ▼ │
84
+ │ Registry │
85
+ │ (allow-list) │
86
+ │ │ │
87
+ │ ▼ │
88
+ │ DataResolver │
89
+ │ (REST/SQL/GQL) │
90
+ │ │ │
91
+ │ ▼ │
92
+ │ HITL gates │
93
+ │ (PII/conf/etc) │
94
+ │ │ │
95
+ │ ▼ │
96
+ │ Action dispatch │
97
+ │ (intent → HTTP) │
98
+ └──────────────────┘
99
+
100
+
101
+ SSE events: spec.line,
102
+ component.mount, data.fill,
103
+ hitl.request, action.request,
104
+ update, stream.end
105
+ ```
106
+
107
+ ## Protocol
108
+
109
+ 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).
110
+
111
+ ```http
112
+ event: session.start
113
+ data: {"session_id":"demo","adapter":"openai"}
114
+
115
+ event: spec.line
116
+ data: {"path":"line:1","component":"Card","node":{...},"valid":true}
117
+
118
+ event: data.fill
119
+ data: {"ref":"monthly_revenue","ok":true,"value":[...]}
@@ -0,0 +1,91 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel", "setuptools_scm>=8"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "generflow-core"
7
+ version = "0.2.0"
8
+ description = "Generflow Python runtime — streams low-token UI specs over SSE, with HITL checkpoints, live data binding, action gating, and rewind/replay."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Generflow", email = "team@generflow.dev" }] # placeholder
13
+ keywords = [
14
+ "ai",
15
+ "generative-ui",
16
+ "llm",
17
+ "sse",
18
+ "agent",
19
+ "hitl",
20
+ "human-in-the-loop",
21
+ "design-system",
22
+ "streaming",
23
+ "tooling",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Framework :: FastAPI",
28
+ "Intended Audience :: Developers",
29
+ "License :: OSI Approved :: Apache Software License",
30
+ "Operating System :: OS Independent",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Topic :: Software Development :: Libraries :: Python Modules",
36
+ "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
37
+ "Typing :: Typed",
38
+ ]
39
+ dependencies = [
40
+ "fastapi>=0.110",
41
+ "uvicorn[standard]>=0.27",
42
+ "pydantic>=2.5",
43
+ "openai>=1.12",
44
+ "anthropic>=0.18",
45
+ "pyyaml>=6.0",
46
+ "tiktoken>=0.5",
47
+ ]
48
+
49
+ [project.optional-dependencies]
50
+ dev = [
51
+ "pytest>=7.4",
52
+ "pytest-asyncio>=0.23",
53
+ "httpx>=0.26",
54
+ "build>=1.0",
55
+ "twine>=4.0",
56
+ ]
57
+ all = ["generflow-core[dev]"]
58
+
59
+ [project.urls]
60
+ Homepage = "https://generflow.dev"
61
+ Repository = "https://github.com/generflow/generflow"
62
+ Documentation = "https://docs.generflow.dev"
63
+ Issues = "https://github.com/generflow/generflow/issues"
64
+ Changelog = "https://github.com/generflow/generflow/blob/main/CHANGELOG.md"
65
+
66
+ [project.scripts]
67
+ generflow = "generflow_core.api.app:main"
68
+ generflow-render = "generflow_core.cli:main"
69
+
70
+ [tool.setuptools.packages.find]
71
+ where = ["src"]
72
+ include = ["generflow_core*"]
73
+
74
+ [tool.setuptools.package-data]
75
+ generflow_core = ["py.typed"]
76
+
77
+ [tool.setuptools.package-dir]
78
+ "" = "src"
79
+
80
+ [tool.pytest.ini_options]
81
+ testpaths = ["tests"]
82
+ python_files = ["test_*.py"]
83
+ python_functions = ["test_*"]
84
+ addopts = "-v --tb=short"
85
+ filterwarnings = [
86
+ "ignore::DeprecationWarning:datetime.*",
87
+ ]
88
+
89
+ [tool.build_sphinx]
90
+ source-dir = "docs"
91
+ build-dir = "docs/_build"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """generflow-core: Generflow Python backend runtime."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,22 @@
1
+ """Action module: intent dispatch + audit."""
2
+ from .dispatcher import (
3
+ ActionError,
4
+ ActionResult,
5
+ DispatchPlan,
6
+ audit_clear,
7
+ audit_recent,
8
+ audit_record,
9
+ build_dispatch,
10
+ execute_dispatch,
11
+ )
12
+
13
+ __all__ = [
14
+ "ActionError",
15
+ "ActionResult",
16
+ "DispatchPlan",
17
+ "audit_clear",
18
+ "audit_recent",
19
+ "audit_record",
20
+ "build_dispatch",
21
+ "execute_dispatch",
22
+ ]
@@ -0,0 +1,223 @@
1
+ """Action dispatcher: intent → configured endpoint, with HITL confirm.
2
+
3
+ The LLM emits `intent="refund.approve"` on a Button. This module:
4
+ 1. Looks up the intent in the app config
5
+ 2. Merges the bound values (form state) into the body template
6
+ 3. Returns a dispatch plan with confirm-required flag
7
+
8
+ The actual HTTP call is made by the SSE handler after the user
9
+ confirms via the HITL gate. This separation lets us:
10
+ - Audit the intent *before* the call
11
+ - Show the user exactly what will be sent
12
+ - Cancel cleanly if they back out
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import json
18
+ from dataclasses import dataclass, field
19
+ from typing import Any
20
+
21
+ from ..databind import AppConfig, Action
22
+
23
+
24
+ class ActionError(Exception):
25
+ pass
26
+
27
+
28
+ @dataclass
29
+ class DispatchPlan:
30
+ """A fully-resolved plan for an action call, ready for user confirmation.
31
+
32
+ Not yet executed — `executed` is False until the HITL gate (or auto-approve)
33
+ fires the actual request.
34
+ """
35
+ intent: str
36
+ action: Action
37
+ method: str
38
+ url: str
39
+ headers: dict
40
+ body: dict
41
+ payload_hash: str
42
+ confirm_required: bool
43
+ requires_role: str | None
44
+ audit: bool
45
+
46
+ def to_dict(self) -> dict:
47
+ return {
48
+ "intent": self.intent,
49
+ "method": self.method,
50
+ "url": self.url,
51
+ "headers": {k: v for k, v in self.headers.items() if k.lower() != "authorization"},
52
+ "body": self.body,
53
+ "payload_hash": self.payload_hash,
54
+ "confirm_required": self.confirm_required,
55
+ "requires_role": self.requires_role,
56
+ "audit": self.audit,
57
+ }
58
+
59
+
60
+ def build_dispatch(
61
+ config: AppConfig,
62
+ intent: str,
63
+ bindings: dict[str, Any] | None = None,
64
+ ) -> DispatchPlan:
65
+ """Build a dispatch plan from an intent + bound values.
66
+
67
+ `bindings` is the form state or other values the LLM specified via
68
+ `bind=[...]` on the Button. Only keys in the action's allow-listed
69
+ `bind` are accepted — everything else is dropped (field smuggling
70
+ prevention).
71
+ """
72
+ action = config.action(intent)
73
+ if action is None:
74
+ raise ActionError(f"Unknown action intent: {intent!r}")
75
+ bindings = bindings or {}
76
+ # enforce bind allow-list
77
+ allowed = set(action.bind)
78
+ sanitized: dict[str, Any] = {}
79
+ for k, v in bindings.items():
80
+ if k in allowed:
81
+ sanitized[k] = v
82
+ # merge into body template — keep original types (int, str, etc.)
83
+ body = _render_template(action.body_template, sanitized)
84
+ # no-op for now: keep body as-is. The binding type flows through
85
+ # the renderer; downstream JSON serialization handles the rest.
86
+ # interpolate URL params
87
+ url = action.url
88
+ for k, v in sanitized.items():
89
+ url = url.replace(f"${k}", str(v))
90
+ # payload hash (used for audit dedup + render preview)
91
+ payload = json.dumps(body, sort_keys=True, default=str)
92
+ payload_hash = hashlib.sha256(payload.encode()).hexdigest()[:16]
93
+ # auth: bearer is interpolated from env at config-load time,
94
+ # so we just need to copy the configured headers
95
+ return DispatchPlan(
96
+ intent=intent,
97
+ action=action,
98
+ method=action.method,
99
+ url=url,
100
+ headers=dict(action.headers),
101
+ body=body,
102
+ payload_hash=payload_hash,
103
+ confirm_required=action.confirm,
104
+ requires_role=action.requires_role,
105
+ audit=action.audit,
106
+ )
107
+
108
+
109
+ def _render_template(template: Any, values: dict[str, Any]) -> Any:
110
+ """Recursively replace `$key` in strings with `values[key]`."""
111
+ if isinstance(template, str):
112
+ out = template
113
+ for k, v in values.items():
114
+ out = out.replace(f"${k}", str(v))
115
+ return out
116
+ if isinstance(template, dict):
117
+ return {k: _render_template(v, values) for k, v in template.items()}
118
+ if isinstance(template, list):
119
+ return [_render_template(v, values) for v in template]
120
+ return template
121
+
122
+
123
+ def _coerce_types(body: Any) -> Any:
124
+ """If a value came back as a numeric string ("89") but the binding was
125
+ numeric (89), restore the original type. Keeps dispatch payloads
126
+ type-correct for downstream APIs."""
127
+ if isinstance(body, dict):
128
+ return {k: _coerce_types(v) for k, v in body.items()}
129
+ if isinstance(body, list):
130
+ return [_coerce_types(v) for v in body]
131
+ if isinstance(body, str):
132
+ s = body.strip()
133
+ if s and (s[0].isdigit() or (s[0] == "-" and len(s) > 1 and s[1].isdigit())):
134
+ try:
135
+ if "." in s:
136
+ return float(s)
137
+ return int(s)
138
+ except ValueError:
139
+ pass
140
+ return body
141
+
142
+
143
+ # ── Execution ──────────────────────────────────────────────────────────────
144
+
145
+ import asyncio
146
+
147
+
148
+ class ActionResult:
149
+ def __init__(self, status: int, body: Any, ok: bool, error: str | None = None) -> None:
150
+ self.status = status
151
+ self.body = body
152
+ self.ok = ok
153
+ self.error = error
154
+
155
+ def to_dict(self) -> dict:
156
+ return {"status": self.status, "ok": self.ok, "error": self.error, "body": self.body}
157
+
158
+
159
+ async def execute_dispatch(plan: DispatchPlan) -> ActionResult:
160
+ """Execute a confirmed dispatch plan. Sends the actual HTTP request."""
161
+ import httpx
162
+ try:
163
+ async with httpx.AsyncClient(timeout=15.0) as client:
164
+ r = await client.request(
165
+ plan.method,
166
+ plan.url,
167
+ json=plan.body,
168
+ headers=plan.headers,
169
+ )
170
+ try:
171
+ body = r.json()
172
+ except Exception:
173
+ body = r.text
174
+ return ActionResult(status=r.status_code, body=body, ok=r.is_success)
175
+ except Exception as e:
176
+ return ActionResult(status=0, body=None, ok=False, error=str(e))
177
+
178
+
179
+ # ── Audit log ─────────────────────────────────────────────────────────────
180
+
181
+ import datetime
182
+ import threading
183
+
184
+
185
+ _AUDIT_LOG: list[dict] = []
186
+ _AUDIT_LOCK = threading.Lock()
187
+
188
+
189
+ def audit_record(
190
+ intent: str,
191
+ user: str,
192
+ payload_hash: str,
193
+ status: int,
194
+ latency_ms: int,
195
+ extra: dict | None = None,
196
+ ) -> None:
197
+ """Append a record to the in-memory audit log.
198
+
199
+ Production would back this with Postgres / OpenTelemetry / etc.
200
+ For v1, an in-memory list is enough to demonstrate the shape.
201
+ """
202
+ rec = {
203
+ "ts": datetime.datetime.utcnow().isoformat() + "Z",
204
+ "intent": intent,
205
+ "user": user,
206
+ "payload_hash": payload_hash,
207
+ "status": status,
208
+ "latency_ms": latency_ms,
209
+ }
210
+ if extra:
211
+ rec.update(extra)
212
+ with _AUDIT_LOCK:
213
+ _AUDIT_LOG.append(rec)
214
+
215
+
216
+ def audit_recent(limit: int = 50) -> list[dict]:
217
+ with _AUDIT_LOCK:
218
+ return list(_AUDIT_LOG[-limit:])
219
+
220
+
221
+ def audit_clear() -> None:
222
+ with _AUDIT_LOCK:
223
+ _AUDIT_LOG.clear()
@@ -0,0 +1,11 @@
1
+ """LLM adapters — pluggable backends (OpenAI, Anthropic, Echo for local dev)."""
2
+ from .llm import AnthropicAdapter, EchoAdapter, LLMAdapter, LLMError, OpenAIAdapter, get_adapter
3
+
4
+ __all__ = [
5
+ "AnthropicAdapter",
6
+ "EchoAdapter",
7
+ "LLMAdapter",
8
+ "LLMError",
9
+ "OpenAIAdapter",
10
+ "get_adapter",
11
+ ]