openadapt-flow 0.1.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.
- openadapt_flow-0.1.0/.github/workflows/ci.yml +40 -0
- openadapt_flow-0.1.0/.github/workflows/release.yml +47 -0
- openadapt_flow-0.1.0/.gitignore +11 -0
- openadapt_flow-0.1.0/DESIGN.md +246 -0
- openadapt_flow-0.1.0/LICENSE +21 -0
- openadapt_flow-0.1.0/PKG-INFO +138 -0
- openadapt_flow-0.1.0/README.md +116 -0
- openadapt_flow-0.1.0/docs/L1_INTEGRATION.md +76 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/REPORT.md +55 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/report.json +213 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_000_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_000_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_001_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_001_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_002_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_002_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_003_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_003_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_004_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_004_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_005_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_005_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_006_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_006_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_007_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_007_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_008_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_008_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_009_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_009_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_010_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/baseline-run/steps/step_010_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_000.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_002.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_004.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_005.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_006.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_007.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_008.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/bundle/templates/step_010.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/bundle/workflow.json +581 -0
- openadapt_flow-0.1.0/docs/showcase/bundle/workflow.py +74 -0
- openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_000.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_002.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_004.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_005.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_006.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_007.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_008.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/healed-bundle/templates/step_010.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/healed-bundle/workflow.json +581 -0
- openadapt_flow-0.1.0/docs/showcase/recording/events.jsonl +11 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0000_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0000_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0001_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0001_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0002_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0002_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0003_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0003_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0004_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0004_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0005_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0005_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0006_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0006_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0007_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0007_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0008_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0008_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0009_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0009_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0010_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/frames/0010_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/recording/meta.json +12 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/REPORT.md +161 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_000/heal.json +69 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_000/screen.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_000/template.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_002/heal.json +69 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_002/screen.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_002/template.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_004/heal.json +69 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_004/screen.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_004/template.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_005/heal.json +69 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_005/screen.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_005/template.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_006/heal.json +69 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_006/screen.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_006/template.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_007/heal.json +69 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_007/screen.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_007/template.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_008/heal.json +69 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_008/screen.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_008/template.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_010/heal.json +69 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_010/screen.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/heals/step_010/template.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/report.json +758 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_000_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_000_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_001_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_001_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_002_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_002_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_003_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_003_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_004_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_004_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_005_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_005_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_006_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_006_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_007_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_007_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_008_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_008_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_009_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_009_before.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_010_after.png +0 -0
- openadapt_flow-0.1.0/docs/showcase/theme-drift-run/steps/step_010_before.png +0 -0
- openadapt_flow-0.1.0/openadapt_flow/__init__.py +17 -0
- openadapt_flow-0.1.0/openadapt_flow/__main__.py +349 -0
- openadapt_flow-0.1.0/openadapt_flow/backend.py +33 -0
- openadapt_flow-0.1.0/openadapt_flow/backends/__init__.py +24 -0
- openadapt_flow-0.1.0/openadapt_flow/backends/playwright_backend.py +152 -0
- openadapt_flow-0.1.0/openadapt_flow/bench.py +162 -0
- openadapt_flow-0.1.0/openadapt_flow/compiler/__init__.py +6 -0
- openadapt_flow-0.1.0/openadapt_flow/compiler/codegen.py +88 -0
- openadapt_flow-0.1.0/openadapt_flow/compiler/compile.py +604 -0
- openadapt_flow-0.1.0/openadapt_flow/demo_driver.py +78 -0
- openadapt_flow-0.1.0/openadapt_flow/emit/__init__.py +13 -0
- openadapt_flow-0.1.0/openadapt_flow/emit/l1_artifact.py +151 -0
- openadapt_flow-0.1.0/openadapt_flow/emit/mcp_tool.py +174 -0
- openadapt_flow-0.1.0/openadapt_flow/emit/skill.py +145 -0
- openadapt_flow-0.1.0/openadapt_flow/ir.py +196 -0
- openadapt_flow-0.1.0/openadapt_flow/mockmed/__init__.py +9 -0
- openadapt_flow-0.1.0/openadapt_flow/mockmed/server.py +52 -0
- openadapt_flow-0.1.0/openadapt_flow/mockmed/static/app.js +209 -0
- openadapt_flow-0.1.0/openadapt_flow/mockmed/static/index.html +18 -0
- openadapt_flow-0.1.0/openadapt_flow/mockmed/static/styles.css +213 -0
- openadapt_flow-0.1.0/openadapt_flow/recorder.py +169 -0
- openadapt_flow-0.1.0/openadapt_flow/report.py +247 -0
- openadapt_flow-0.1.0/openadapt_flow/runtime/__init__.py +47 -0
- openadapt_flow-0.1.0/openadapt_flow/runtime/grounder.py +190 -0
- openadapt_flow-0.1.0/openadapt_flow/runtime/heal.py +262 -0
- openadapt_flow-0.1.0/openadapt_flow/runtime/replayer.py +419 -0
- openadapt_flow-0.1.0/openadapt_flow/runtime/resolver.py +297 -0
- openadapt_flow-0.1.0/openadapt_flow/vision/__init__.py +25 -0
- openadapt_flow-0.1.0/openadapt_flow/vision/hashing.py +55 -0
- openadapt_flow-0.1.0/openadapt_flow/vision/match.py +157 -0
- openadapt_flow-0.1.0/openadapt_flow/vision/ocr.py +147 -0
- openadapt_flow-0.1.0/openadapt_flow/vision/settle.py +61 -0
- openadapt_flow-0.1.0/pyproject.toml +37 -0
- openadapt_flow-0.1.0/tests/conftest.py +11 -0
- openadapt_flow-0.1.0/tests/e2e/__init__.py +0 -0
- openadapt_flow-0.1.0/tests/e2e/conftest.py +129 -0
- openadapt_flow-0.1.0/tests/e2e/test_record_compile_replay.py +468 -0
- openadapt_flow-0.1.0/tests/test_compiler.py +480 -0
- openadapt_flow-0.1.0/tests/test_emit.py +267 -0
- openadapt_flow-0.1.0/tests/test_heal.py +315 -0
- openadapt_flow-0.1.0/tests/test_l1_artifact.py +96 -0
- openadapt_flow-0.1.0/tests/test_mockmed.py +256 -0
- openadapt_flow-0.1.0/tests/test_recorder.py +208 -0
- openadapt_flow-0.1.0/tests/test_replayer.py +460 -0
- openadapt_flow-0.1.0/tests/test_report.py +379 -0
- openadapt_flow-0.1.0/tests/test_resolver.py +324 -0
- openadapt_flow-0.1.0/tests/test_vision.py +323 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
|
|
17
|
+
- name: Install
|
|
18
|
+
run: pip install -e .[dev]
|
|
19
|
+
|
|
20
|
+
- name: Install Playwright browser
|
|
21
|
+
run: playwright install --with-deps chromium
|
|
22
|
+
|
|
23
|
+
# --basetemp pins pytest's tmp_path factory inside the workspace so
|
|
24
|
+
# run dirs (REPORT.md + step/heal PNGs) survive for artifact upload;
|
|
25
|
+
# by default they land in the system temp dir and are unrecoverable
|
|
26
|
+
# when a drift/heal test fails in CI.
|
|
27
|
+
- name: Test
|
|
28
|
+
run: mkdir -p runs && pytest -q --basetemp=runs/ci
|
|
29
|
+
|
|
30
|
+
- name: Upload run artifacts
|
|
31
|
+
if: always()
|
|
32
|
+
uses: actions/upload-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: runs
|
|
35
|
+
path: |
|
|
36
|
+
runs/**/REPORT.md
|
|
37
|
+
runs/**/BENCH.md
|
|
38
|
+
runs/**/report.json
|
|
39
|
+
runs/**/*.png
|
|
40
|
+
if-no-files-found: warn
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Release and PyPI Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # PyPI Trusted Publishing (OIDC)
|
|
14
|
+
contents: write # GitHub Release
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout repository
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.12"
|
|
24
|
+
|
|
25
|
+
- name: Verify tag matches package version
|
|
26
|
+
run: |
|
|
27
|
+
PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
|
|
28
|
+
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
29
|
+
if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
|
|
30
|
+
echo "Tag $GITHUB_REF_NAME does not match pyproject version $PKG_VERSION" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
- name: Build package
|
|
35
|
+
run: |
|
|
36
|
+
python -m pip install --quiet build
|
|
37
|
+
python -m build
|
|
38
|
+
|
|
39
|
+
- name: Publish to PyPI
|
|
40
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
41
|
+
|
|
42
|
+
- name: Publish GitHub Release
|
|
43
|
+
env:
|
|
44
|
+
GH_TOKEN: ${{ github.token }}
|
|
45
|
+
run: |
|
|
46
|
+
gh release create "$GITHUB_REF_NAME" dist/* \
|
|
47
|
+
--title "$GITHUB_REF_NAME" --generate-notes
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# openadapt-flow — Design & Module Contracts (v0)
|
|
2
|
+
|
|
3
|
+
**Product:** record a workflow demonstration once → compile it into a
|
|
4
|
+
deterministic, vision-anchored script → replay it locally with a resolution
|
|
5
|
+
ladder → when the UI drifts, heal the script (as a reviewable diff) instead of
|
|
6
|
+
re-reasoning every run.
|
|
7
|
+
|
|
8
|
+
The runtime is **vision-only**: it consumes PNG bytes and emits clicks/keys at
|
|
9
|
+
pixel coordinates through the `Backend` protocol (`openadapt_flow/backend.py`).
|
|
10
|
+
The reference backend is Playwright-driven (headless-capable, CI-friendly,
|
|
11
|
+
permission-free); native OS / RDP backends are future adapters.
|
|
12
|
+
|
|
13
|
+
## Frozen contracts (do not change without updating this doc)
|
|
14
|
+
|
|
15
|
+
- `openadapt_flow/ir.py` — Workflow/Step/Anchor/Postcondition + runtime result
|
|
16
|
+
models. FROZEN for v0. Additive changes only, and only by the integrator.
|
|
17
|
+
- `openadapt_flow/backend.py` — Backend protocol. FROZEN.
|
|
18
|
+
|
|
19
|
+
## Bundle & recording formats
|
|
20
|
+
|
|
21
|
+
**Workflow bundle** (compiler output, replayer input):
|
|
22
|
+
```
|
|
23
|
+
<bundle>/
|
|
24
|
+
workflow.json # ir.Workflow
|
|
25
|
+
templates/*.png # anchor crops (paths referenced from workflow.json)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Recording** (recorder output, compiler input):
|
|
29
|
+
```
|
|
30
|
+
<recording>/
|
|
31
|
+
meta.json # {"id", "created_at", "viewport": [w,h], "app_url",
|
|
32
|
+
# "params": {"<param_name>": "<value typed during demo>"}}
|
|
33
|
+
events.jsonl # one JSON object per line, in order:
|
|
34
|
+
# {"i":0,"kind":"click","x":123,"y":45,"t":1.20}
|
|
35
|
+
# {"i":1,"kind":"type","text":"...","param":"note","t":2.03}
|
|
36
|
+
# {"i":2,"kind":"key","key":"Enter","t":3.10}
|
|
37
|
+
# "param" present iff the typed value is a parameter
|
|
38
|
+
frames/{i:04d}_before.png
|
|
39
|
+
frames/{i:04d}_after.png # after the action settled
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Run directory** (replayer output):
|
|
43
|
+
```
|
|
44
|
+
<run>/
|
|
45
|
+
report.json # ir.RunReport
|
|
46
|
+
steps/{step_id}_before.png / _after.png
|
|
47
|
+
heals/{step_id}/… # heal crops + heal.json per heal event
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Module ownership (build phase — do not edit files outside your area)
|
|
51
|
+
|
|
52
|
+
| Area | Owner | Files |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Mock app + backend + recorder | Agent A | `openadapt_flow/mockmed/**`, `openadapt_flow/backends/playwright_backend.py`, `openadapt_flow/recorder.py`, `openadapt_flow/demo_driver.py`, `tests/test_mockmed.py`, `tests/test_recorder.py` |
|
|
55
|
+
| Vision + compiler | Agent B | `openadapt_flow/vision/**`, `openadapt_flow/compiler/**`, `tests/test_vision.py`, `tests/test_compiler.py` |
|
|
56
|
+
| Runtime (ladder/heal/verify) | Agent C | `openadapt_flow/runtime/**`, `tests/test_resolver.py`, `tests/test_replayer.py`, `tests/test_heal.py` |
|
|
57
|
+
| Report, emit, CLI, bench, CI | Agent D | `openadapt_flow/report.py`, `openadapt_flow/bench.py`, `openadapt_flow/emit/**`, `openadapt_flow/__main__.py`, `.github/workflows/ci.yml`, `tests/test_report.py`, `tests/test_emit.py` |
|
|
58
|
+
| E2E integration | Integrator | `tests/e2e/**`, cross-module fixes anywhere |
|
|
59
|
+
|
|
60
|
+
## Vision API (Agent B implements; Agent C consumes via injection)
|
|
61
|
+
|
|
62
|
+
`openadapt_flow/vision/__init__.py` re-exports:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
class Match(BaseModel):
|
|
66
|
+
point: Point # click/center point, screen coords
|
|
67
|
+
region: Region # matched region, screen coords
|
|
68
|
+
confidence: float
|
|
69
|
+
|
|
70
|
+
# match.py
|
|
71
|
+
def find_template(screen_png: bytes, template_png: bytes, *,
|
|
72
|
+
search_region: Region | None = None,
|
|
73
|
+
scales: tuple[float, ...] = (0.85, 1.0, 1.18),
|
|
74
|
+
threshold: float = 0.82) -> Match | None
|
|
75
|
+
|
|
76
|
+
# ocr.py (rapidocr_onnxruntime; instantiate the engine once, module-level lazy)
|
|
77
|
+
class OcrLine(BaseModel):
|
|
78
|
+
text: str; region: Region; confidence: float
|
|
79
|
+
def ocr(screen_png: bytes, *, region: Region | None = None) -> list[OcrLine]
|
|
80
|
+
def find_text(screen_png: bytes, text: str, *,
|
|
81
|
+
region: Region | None = None, min_ratio: float = 0.8) -> Match | None
|
|
82
|
+
# normalized fuzzy match (difflib ratio on lowercased/stripped text)
|
|
83
|
+
|
|
84
|
+
# hashing.py
|
|
85
|
+
def phash_png(png: bytes, region: Region | None = None) -> str
|
|
86
|
+
def phash_distance(a: str, b: str) -> int
|
|
87
|
+
|
|
88
|
+
# settle.py
|
|
89
|
+
def wait_settled(backend, *, interval_s: float = 0.1,
|
|
90
|
+
stable_frames: int = 2, timeout_s: float = 3.0) -> bytes
|
|
91
|
+
# poll screenshots until N consecutive identical (phash dist 0) or timeout;
|
|
92
|
+
# return the last PNG
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Compiler (Agent B)
|
|
96
|
+
|
|
97
|
+
`openadapt_flow.compiler.compile_recording(recording_dir, out_bundle_dir, *, name) -> Workflow`
|
|
98
|
+
|
|
99
|
+
Per click event (and `double_click`, compiled identically with action
|
|
100
|
+
DOUBLE_CLICK): crop a template around the click point (target-sized crop,
|
|
101
|
+
e.g. 160x64 clamped to frame, centered on click), OCR the crop for `ocr_text`,
|
|
102
|
+
extract up to 2 landmarks (nearest OCR lines outside the crop, carrying both
|
|
103
|
+
relation/distance and exact `dx_px`/`dy_px` offsets to the click point), set
|
|
104
|
+
`click_point`, `region`. Per type event: TYPE step with `text` or `param` (from
|
|
105
|
+
events.jsonl). Postconditions from the after-frame: pick the largest changed
|
|
106
|
+
region between before/after (cv2.absdiff + threshold + bounding rect),
|
|
107
|
+
REGION_STABLE with its phash; plus TEXT_PRESENT for the most distinctive new
|
|
108
|
+
OCR text (text in after, not in before — compared whitespace-insensitively
|
|
109
|
+
so OCR jitter cannot make permanently visible chrome look "new"; prefer
|
|
110
|
+
longest). Parameterized typed values vary per run and are NEVER asserted in
|
|
111
|
+
any step's postconditions — including downstream steps whose after-frames
|
|
112
|
+
embed the typed value (e.g. a save-confirmation banner). Click target labels
|
|
113
|
+
(any anchor's `ocr_text`) are likewise never asserted: they are mutable
|
|
114
|
+
evidence the resolution ladder heals through under rename drift, not
|
|
115
|
+
invariants. Intent: rule-based `"click '<ocr_text>'"` /
|
|
116
|
+
`"type <param or text preview>"` (VLM annotation is a later enhancement —
|
|
117
|
+
design for it, don't call any API).
|
|
118
|
+
|
|
119
|
+
Also emit a readable Python *rendering* of the workflow (`workflow.py` in the
|
|
120
|
+
bundle, generated, not parsed back) so humans can code-review the automation.
|
|
121
|
+
|
|
122
|
+
## Runtime (Agent C)
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
class Replayer:
|
|
126
|
+
def __init__(self, backend, *, vision=None, grounder=None): ...
|
|
127
|
+
def run(self, workflow: Workflow, *, params: dict[str, str] | None = None,
|
|
128
|
+
bundle_dir: Path, run_dir: Path,
|
|
129
|
+
save_healed_to: Path | None = None) -> RunReport
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Parameters not supplied in `params` fall back to the recorded example/default
|
|
133
|
+
values in `workflow.params`, so a bundle replays without any explicit params.
|
|
134
|
+
|
|
135
|
+
Resolution ladder per step with an anchor (record rung + confidence + ms):
|
|
136
|
+
1. `template` — find_template within anchor.region padded by search_pad
|
|
137
|
+
2. `template_global` — find_template full frame
|
|
138
|
+
3. `ocr` — find_text(anchor.ocr_text) full frame
|
|
139
|
+
4. `geometry` — landmarks: locate landmark text, offset by the exact
|
|
140
|
+
`dx_px`/`dy_px` offsets when recorded, else by relation/distance
|
|
141
|
+
5. `grounder` — optional injected `Grounder.locate(png, intent) -> Match|None`
|
|
142
|
+
(protocol in `runtime/grounder.py`; ship a `NullGrounder`; an Anthropic
|
|
143
|
+
implementation goes behind the `grounder` extra and is NOT used in tests)
|
|
144
|
+
|
|
145
|
+
Click point = matched region origin + (anchor.click_point - anchor.region
|
|
146
|
+
origin), scaled by match scale. After acting: `wait_settled`, then check
|
|
147
|
+
postconditions (poll until each passes or times out). Postcondition failure →
|
|
148
|
+
re-settle and re-check once → fail the step and abort the run (semantic drift
|
|
149
|
+
halts; the report must name the step and embed before/after screenshots).
|
|
150
|
+
|
|
151
|
+
**Heal:** when a step succeeds via any rung other than `template`, emit a
|
|
152
|
+
HealEvent: new template crop at the resolved region from the live frame,
|
|
153
|
+
updated region/click_point/ocr_text (re-OCR). Apply to the in-memory workflow;
|
|
154
|
+
if `save_healed_to` is set, write the healed bundle (updated workflow.json +
|
|
155
|
+
new crops). Record heal crops + heal.json under `run_dir/heals/<step_id>/`.
|
|
156
|
+
Irreversible steps: if resolution needed a rung below `ocr`, do NOT act — fail
|
|
157
|
+
with a clear "needs human confirmation" error (v0 policy).
|
|
158
|
+
|
|
159
|
+
## MockMed (Agent A)
|
|
160
|
+
|
|
161
|
+
Static single-page app (`openadapt_flow/mockmed/static/index.html` + app.js +
|
|
162
|
+
styles.css, no external resources, no CSS transitions/animations, font-size
|
|
163
|
+
≥ 14px). Served by `openadapt_flow.mockmed.server.serve(port=0) -> (url, stop)`
|
|
164
|
+
(threaded http.server). Hash-routed screens:
|
|
165
|
+
|
|
166
|
+
- `#login` — Username, Password fields; "Sign In" button
|
|
167
|
+
- `#tasks` — "Referral Tasks" table, rows with fake patient names
|
|
168
|
+
(obviously fake: "Jane Sample", "Alex Testcase"), reason, priority, an
|
|
169
|
+
"Open" button per row
|
|
170
|
+
- `#patient/<id>` — patient banner, "New Encounter" button, Encounters list
|
|
171
|
+
- `#encounter` — Type chooser as segmented BUTTONS ("Triage" / "Consult" —
|
|
172
|
+
no native <select>), Note textarea, "Save Encounter" button
|
|
173
|
+
- after save → back to patient screen with a banner `Encounter saved — <first
|
|
174
|
+
40 chars of note>` and the encounter listed
|
|
175
|
+
|
|
176
|
+
Drift modes via query string `?drift=a,b` (applied before render):
|
|
177
|
+
- `theme` — dark palette (breaks template matching for every anchor; OCR
|
|
178
|
+
still works for labeled targets, while unlabeled input-field anchors fall
|
|
179
|
+
through to the geometry rung)
|
|
180
|
+
- `move` — "New Encounter" and "Save Encounter" buttons relocated to the
|
|
181
|
+
opposite side of their container (breaks local template search)
|
|
182
|
+
- `rename` — "Save Encounter"→"Submit Encounter", "Open"→"View" (breaks
|
|
183
|
+
template + OCR; geometry rung must resolve — keep the button in the SAME
|
|
184
|
+
position as default so landmarks/geometry succeed)
|
|
185
|
+
- `modal` — after clicking Save, a blocking "Survey" modal appears instead of
|
|
186
|
+
the banner (semantic drift: replay must FAIL gracefully)
|
|
187
|
+
|
|
188
|
+
`demo_driver.py`: `record_triage_demo(url, out_dir, *, note_text, param_name
|
|
189
|
+
= "note") -> Path` — drives the canonical demo via Playwright locators (record
|
|
190
|
+
time may cheat with selectors; replay never does): login → tasks → Open first
|
|
191
|
+
referral → New Encounter → click "Triage" → click Note field → type note
|
|
192
|
+
(param) → click Save Encounter → done. It performs every action through
|
|
193
|
+
`Recorder` so frames/events are captured (before frame, act, wait settle,
|
|
194
|
+
after frame).
|
|
195
|
+
|
|
196
|
+
`PlaywrightBackend(page)` implements `Backend` (chromium, fixed viewport
|
|
197
|
+
1280x800, deviceScaleFactor=1). `Recorder(backend, out_dir)` wraps a backend
|
|
198
|
+
with the same action methods plus `type_text(text, param=None)` and
|
|
199
|
+
`finish() -> recording dir`.
|
|
200
|
+
|
|
201
|
+
## Report / emit / CLI / bench / CI (Agent D)
|
|
202
|
+
|
|
203
|
+
- `report.py`: `render_run_report(run_dir) -> Path` — REPORT.md in run_dir:
|
|
204
|
+
outcome, per-step table (intent, rung, confidence, ms, heal?), embedded
|
|
205
|
+
relative-path images for key steps and every heal (before/after side by
|
|
206
|
+
side), rung histogram, totals (ms, model calls, est cost).
|
|
207
|
+
`render_bench_report(bench.json, out) -> Path`.
|
|
208
|
+
- `bench.py`: `run_bench(workflow_bundle, backend_factory, n) -> dict` —
|
|
209
|
+
replay N times; success rate, p50/p95 total ms, rung histogram, model calls
|
|
210
|
+
(0 in v0), cost 0; serialize bench.json.
|
|
211
|
+
- `emit/skill.py`: workflow bundle → `SKILL.md` folder (Agent Skills format:
|
|
212
|
+
name, description, when-to-use, `openadapt-flow replay bundle --param k=v`
|
|
213
|
+
invocation). The bundle is copied into the skill folder (`bundle/`) so the
|
|
214
|
+
artifact is self-contained and portable. `emit/mcp_tool.py`: generate a
|
|
215
|
+
standalone `server.py` exposing the workflow as an MCP tool (string
|
|
216
|
+
template; must `ast.parse`; do not import mcp at generation time); the
|
|
217
|
+
bundle is copied next to `server.py` and referenced relative to
|
|
218
|
+
`__file__`, never by an emitting-machine absolute path.
|
|
219
|
+
- `__main__.py` CLI: `demo-record`, `compile`, `replay`, `bench`, `emit-skill`,
|
|
220
|
+
`emit-mcp` (thin wrappers over the module APIs above). `replay --run-dir`
|
|
221
|
+
is optional and defaults to `runs/replay-<UTC timestamp>`.
|
|
222
|
+
- CI (`.github/workflows/ci.yml`): ubuntu-latest, py3.12,
|
|
223
|
+
`pip install -e .[dev]`, `playwright install --with-deps chromium`,
|
|
224
|
+
`pytest -q --basetemp=runs/ci` (temp dirs pinned inside the workspace so
|
|
225
|
+
run artifacts survive), upload `runs/**/REPORT.md` + PNGs as artifacts.
|
|
226
|
+
|
|
227
|
+
## Test policy
|
|
228
|
+
|
|
229
|
+
- Unit tests per module, cross-module deps faked/injected (Agent C must not
|
|
230
|
+
import Agent B's vision in unit tests — inject fakes; the integrator wires
|
|
231
|
+
real parts in `tests/e2e/`).
|
|
232
|
+
- Deterministic: no sleeps besides settle polling; server on ephemeral port.
|
|
233
|
+
- No network beyond localhost. No API keys required anywhere in tests.
|
|
234
|
+
- E2E matrix (integrator): baseline ×3 all-template zero-heal; theme / move /
|
|
235
|
+
rename each succeed WITH heals then replay-healed all-template; modal fails
|
|
236
|
+
gracefully naming the step; params substitution verified via banner OCR
|
|
237
|
+
with a note value DIFFERENT from the recorded one (the identity case
|
|
238
|
+
cannot distinguish substitution from replaying the baked-in literal); the
|
|
239
|
+
irreversible-step risk gate exercised end-to-end (step marked irreversible
|
|
240
|
+
+ drift forcing a below-ocr rung must refuse to act).
|
|
241
|
+
|
|
242
|
+
## Repo policies
|
|
243
|
+
|
|
244
|
+
- Fake data only; no real patient/clinic/person names anywhere.
|
|
245
|
+
- No references to customers or design partners in this repo.
|
|
246
|
+
- Conventional commits; feature branches; never push to main.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenAdapt.AI (MLDSAI Inc.)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openadapt-flow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Demonstration compiler: record a desktop workflow once, compile it into a deterministic, self-healing, locally-run script.
|
|
5
|
+
Author: OpenAdapt.AI
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: imagehash>=4.3
|
|
10
|
+
Requires-Dist: numpy>=1.26
|
|
11
|
+
Requires-Dist: opencv-python-headless>=4.9
|
|
12
|
+
Requires-Dist: pillow>=10.0
|
|
13
|
+
Requires-Dist: playwright>=1.44
|
|
14
|
+
Requires-Dist: pydantic>=2.5
|
|
15
|
+
Requires-Dist: rapidocr-onnxruntime>=1.3
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest-timeout>=2.2; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
19
|
+
Provides-Extra: grounder
|
|
20
|
+
Requires-Dist: anthropic>=0.40; extra == 'grounder'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# openadapt-flow
|
|
24
|
+
|
|
25
|
+
Record a GUI workflow once. Replay it deterministically, locally, for free.
|
|
26
|
+
A model only touches the script to repair it.
|
|
27
|
+
|
|
28
|
+
Computer-use agents re-reason on every run: screenshot, think, click, repeat.
|
|
29
|
+
That's the right shape for a task nobody has automated before, and the wrong
|
|
30
|
+
one for the 500th referral this month. openadapt-flow makes the other bet. It
|
|
31
|
+
compiles a human demonstration into a plain, reviewable script with vision
|
|
32
|
+
anchors and per-step assertions. A healthy script replays in milliseconds with
|
|
33
|
+
zero model calls. When the UI shifts, a fallback ladder still finds the
|
|
34
|
+
target, and the fix gets written back into the script as a diff you can
|
|
35
|
+
review. The automation gets cheaper over time instead of billing you to
|
|
36
|
+
re-think the same eleven clicks.
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
|
|
40
|
+
A recording captures screenshots and input events while a person does the
|
|
41
|
+
task once. The compiler turns that into a workflow bundle: for every click it
|
|
42
|
+
stores a template crop, the OCR'd label, and offsets to nearby landmark text,
|
|
43
|
+
then derives postconditions from what actually changed on screen after the
|
|
44
|
+
action. Typed values can be tagged as parameters, so one demo yields a
|
|
45
|
+
reusable, parameterized automation plus a generated `workflow.py` rendering a
|
|
46
|
+
human can code-review.
|
|
47
|
+
|
|
48
|
+
At replay time, each step walks a resolution ladder:
|
|
49
|
+
|
|
50
|
+
1. template match near the recorded location (milliseconds, no model)
|
|
51
|
+
2. template match across the whole screen
|
|
52
|
+
3. OCR match on the label
|
|
53
|
+
4. geometry from surrounding landmarks
|
|
54
|
+
5. optionally, a grounding model (local or API)
|
|
55
|
+
|
|
56
|
+
Resolving below the top rung triggers a heal: fresh crop, updated
|
|
57
|
+
coordinates, re-read label, saved as a reviewable change to the bundle.
|
|
58
|
+
Postconditions check that each step actually worked. If the screen stops
|
|
59
|
+
matching expectations entirely (a surprise dialog, a changed process), the
|
|
60
|
+
run halts with an illustrated report rather than guessing. Steps tagged
|
|
61
|
+
irreversible refuse to act on low-confidence resolutions at all.
|
|
62
|
+
|
|
63
|
+
The runtime is vision-only: PNG in, clicks and keys out, behind a small
|
|
64
|
+
`Backend` protocol. The reference backend drives a headless browser, which is
|
|
65
|
+
what lets the entire record→compile→replay→heal loop run in CI with no OS
|
|
66
|
+
permissions. Native desktop and RDP backends are planned adapters behind the
|
|
67
|
+
same protocol.
|
|
68
|
+
|
|
69
|
+
## What it looks like
|
|
70
|
+
|
|
71
|
+
One demonstration was recorded on the light UI (left). The compiled workflow
|
|
72
|
+
then ran unattended against a dark theme it had never seen (right): all 11
|
|
73
|
+
steps succeeded, every anchor re-resolved through the OCR or geometry rungs,
|
|
74
|
+
and all 8 anchors healed themselves back onto the fast path. No human
|
|
75
|
+
involved.
|
|
76
|
+
|
|
77
|
+
| Recorded UI → baseline replay (all `template` rung, 0 model calls) | Theme-drifted UI → replay + self-heal (8/8 anchors healed) |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
|  |  |
|
|
80
|
+
|
|
81
|
+
Every replay generates an illustrated run report:
|
|
82
|
+
[baseline](docs/showcase/baseline-run/REPORT.md) ·
|
|
83
|
+
[theme drift with heals](docs/showcase/theme-drift-run/REPORT.md).
|
|
84
|
+
|
|
85
|
+
## Quickstart
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install -e '.[dev]'
|
|
89
|
+
playwright install chromium
|
|
90
|
+
|
|
91
|
+
# 1. Record a demonstration (drives the bundled MockMed demo app):
|
|
92
|
+
openadapt-flow demo-record --out /tmp/rec
|
|
93
|
+
|
|
94
|
+
# 2. Compile it into a workflow bundle:
|
|
95
|
+
openadapt-flow compile /tmp/rec --out /tmp/bundle --name triage-demo
|
|
96
|
+
|
|
97
|
+
# 3. Replay it — deterministic, local, zero model calls:
|
|
98
|
+
openadapt-flow replay /tmp/bundle --run-dir /tmp/run
|
|
99
|
+
open /tmp/run/REPORT.md
|
|
100
|
+
|
|
101
|
+
# 4. Drift the UI (dark theme it has never seen) and watch it heal:
|
|
102
|
+
openadapt-flow replay /tmp/bundle --drift theme \
|
|
103
|
+
--run-dir /tmp/run-drift --save-healed-to /tmp/healed
|
|
104
|
+
open /tmp/run-drift/REPORT.md
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Steps 3–4 serve the bundled MockMed app automatically. To replay against your
|
|
108
|
+
own running app, pass `--url` (parameters default to the values recorded
|
|
109
|
+
during the demo; `--param` overrides them):
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
openadapt-flow replay /tmp/bundle --url <APP_URL> \
|
|
113
|
+
--run-dir /tmp/run --param note="Booking 3 months"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`bench` replays a bundle N times and aggregates success rate, latency
|
|
117
|
+
percentiles, and cost: `openadapt-flow bench /tmp/bundle --n 10 --run-root
|
|
118
|
+
/tmp/bench`.
|
|
119
|
+
|
|
120
|
+
The test suite includes the full drift matrix (theme, moved buttons, renamed
|
|
121
|
+
buttons, surprise modal) end to end:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
pytest -q
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Status
|
|
128
|
+
|
|
129
|
+
v0. The record→compile→replay→heal loop runs end to end against MockMed, the
|
|
130
|
+
bundled mock clinical app, under four kinds of deliberate UI drift. The suite
|
|
131
|
+
(124 tests) runs headlessly in GitHub Actions and uploads run reports as
|
|
132
|
+
build artifacts. Compiled workflows can also be emitted as Agent Skills
|
|
133
|
+
(`SKILL.md`) and MCP servers, so other agents can invoke them.
|
|
134
|
+
|
|
135
|
+
Solid for the reference browser backend; everything else should be treated as
|
|
136
|
+
a design seam, not a finished feature. See `DESIGN.md` for module contracts
|
|
137
|
+
and `docs/L1_INTEGRATION.md` for feeding layered clinical-data platforms
|
|
138
|
+
(L1→L2 acquisition contracts). MIT license.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# openadapt-flow
|
|
2
|
+
|
|
3
|
+
Record a GUI workflow once. Replay it deterministically, locally, for free.
|
|
4
|
+
A model only touches the script to repair it.
|
|
5
|
+
|
|
6
|
+
Computer-use agents re-reason on every run: screenshot, think, click, repeat.
|
|
7
|
+
That's the right shape for a task nobody has automated before, and the wrong
|
|
8
|
+
one for the 500th referral this month. openadapt-flow makes the other bet. It
|
|
9
|
+
compiles a human demonstration into a plain, reviewable script with vision
|
|
10
|
+
anchors and per-step assertions. A healthy script replays in milliseconds with
|
|
11
|
+
zero model calls. When the UI shifts, a fallback ladder still finds the
|
|
12
|
+
target, and the fix gets written back into the script as a diff you can
|
|
13
|
+
review. The automation gets cheaper over time instead of billing you to
|
|
14
|
+
re-think the same eleven clicks.
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
A recording captures screenshots and input events while a person does the
|
|
19
|
+
task once. The compiler turns that into a workflow bundle: for every click it
|
|
20
|
+
stores a template crop, the OCR'd label, and offsets to nearby landmark text,
|
|
21
|
+
then derives postconditions from what actually changed on screen after the
|
|
22
|
+
action. Typed values can be tagged as parameters, so one demo yields a
|
|
23
|
+
reusable, parameterized automation plus a generated `workflow.py` rendering a
|
|
24
|
+
human can code-review.
|
|
25
|
+
|
|
26
|
+
At replay time, each step walks a resolution ladder:
|
|
27
|
+
|
|
28
|
+
1. template match near the recorded location (milliseconds, no model)
|
|
29
|
+
2. template match across the whole screen
|
|
30
|
+
3. OCR match on the label
|
|
31
|
+
4. geometry from surrounding landmarks
|
|
32
|
+
5. optionally, a grounding model (local or API)
|
|
33
|
+
|
|
34
|
+
Resolving below the top rung triggers a heal: fresh crop, updated
|
|
35
|
+
coordinates, re-read label, saved as a reviewable change to the bundle.
|
|
36
|
+
Postconditions check that each step actually worked. If the screen stops
|
|
37
|
+
matching expectations entirely (a surprise dialog, a changed process), the
|
|
38
|
+
run halts with an illustrated report rather than guessing. Steps tagged
|
|
39
|
+
irreversible refuse to act on low-confidence resolutions at all.
|
|
40
|
+
|
|
41
|
+
The runtime is vision-only: PNG in, clicks and keys out, behind a small
|
|
42
|
+
`Backend` protocol. The reference backend drives a headless browser, which is
|
|
43
|
+
what lets the entire record→compile→replay→heal loop run in CI with no OS
|
|
44
|
+
permissions. Native desktop and RDP backends are planned adapters behind the
|
|
45
|
+
same protocol.
|
|
46
|
+
|
|
47
|
+
## What it looks like
|
|
48
|
+
|
|
49
|
+
One demonstration was recorded on the light UI (left). The compiled workflow
|
|
50
|
+
then ran unattended against a dark theme it had never seen (right): all 11
|
|
51
|
+
steps succeeded, every anchor re-resolved through the OCR or geometry rungs,
|
|
52
|
+
and all 8 anchors healed themselves back onto the fast path. No human
|
|
53
|
+
involved.
|
|
54
|
+
|
|
55
|
+
| Recorded UI → baseline replay (all `template` rung, 0 model calls) | Theme-drifted UI → replay + self-heal (8/8 anchors healed) |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
|  |  |
|
|
58
|
+
|
|
59
|
+
Every replay generates an illustrated run report:
|
|
60
|
+
[baseline](docs/showcase/baseline-run/REPORT.md) ·
|
|
61
|
+
[theme drift with heals](docs/showcase/theme-drift-run/REPORT.md).
|
|
62
|
+
|
|
63
|
+
## Quickstart
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install -e '.[dev]'
|
|
67
|
+
playwright install chromium
|
|
68
|
+
|
|
69
|
+
# 1. Record a demonstration (drives the bundled MockMed demo app):
|
|
70
|
+
openadapt-flow demo-record --out /tmp/rec
|
|
71
|
+
|
|
72
|
+
# 2. Compile it into a workflow bundle:
|
|
73
|
+
openadapt-flow compile /tmp/rec --out /tmp/bundle --name triage-demo
|
|
74
|
+
|
|
75
|
+
# 3. Replay it — deterministic, local, zero model calls:
|
|
76
|
+
openadapt-flow replay /tmp/bundle --run-dir /tmp/run
|
|
77
|
+
open /tmp/run/REPORT.md
|
|
78
|
+
|
|
79
|
+
# 4. Drift the UI (dark theme it has never seen) and watch it heal:
|
|
80
|
+
openadapt-flow replay /tmp/bundle --drift theme \
|
|
81
|
+
--run-dir /tmp/run-drift --save-healed-to /tmp/healed
|
|
82
|
+
open /tmp/run-drift/REPORT.md
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Steps 3–4 serve the bundled MockMed app automatically. To replay against your
|
|
86
|
+
own running app, pass `--url` (parameters default to the values recorded
|
|
87
|
+
during the demo; `--param` overrides them):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
openadapt-flow replay /tmp/bundle --url <APP_URL> \
|
|
91
|
+
--run-dir /tmp/run --param note="Booking 3 months"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`bench` replays a bundle N times and aggregates success rate, latency
|
|
95
|
+
percentiles, and cost: `openadapt-flow bench /tmp/bundle --n 10 --run-root
|
|
96
|
+
/tmp/bench`.
|
|
97
|
+
|
|
98
|
+
The test suite includes the full drift matrix (theme, moved buttons, renamed
|
|
99
|
+
buttons, surprise modal) end to end:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pytest -q
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Status
|
|
106
|
+
|
|
107
|
+
v0. The record→compile→replay→heal loop runs end to end against MockMed, the
|
|
108
|
+
bundled mock clinical app, under four kinds of deliberate UI drift. The suite
|
|
109
|
+
(124 tests) runs headlessly in GitHub Actions and uploads run reports as
|
|
110
|
+
build artifacts. Compiled workflows can also be emitted as Agent Skills
|
|
111
|
+
(`SKILL.md`) and MCP servers, so other agents can invoke them.
|
|
112
|
+
|
|
113
|
+
Solid for the reference browser backend; everything else should be treated as
|
|
114
|
+
a design seam, not a finished feature. See `DESIGN.md` for module contracts
|
|
115
|
+
and `docs/L1_INTEGRATION.md` for feeding layered clinical-data platforms
|
|
116
|
+
(L1→L2 acquisition contracts). MIT license.
|