Skip to content

Commit 8a5d995

Browse files
authored
Merge pull request #1251 from OpenAdaptAI/benchmark/openemr-referral-timing-data
benchmark: commit the OpenEMR referral timing data and its figures
2 parents 3bc7cdc + 5fff42c commit 8a5d995

9 files changed

Lines changed: 1171 additions & 8 deletions

File tree

‎README.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,17 +133,19 @@ screen. [Connect an agent](https://docs.openadapt.ai/how-to/connect-an-agent/) c
133133
## Measured so far
134134

135135
On a local OpenEMR 8.0.0.3 test copy with synthetic referrals (14 and
136-
15 October 2026), OpenAdapt and a Claude computer-use agent each entered
137-
referrals in 62 timed runs. Median time was 29.7 seconds for OpenAdapt and 36.5
138-
seconds for the agent. OpenAdapt made no model calls; the agent cost about
136+
15 October 2026), OpenAdapt and a Claude computer-use agent each entered 63
137+
new-patient referrals in timed runs. Median time over the correct runs was 29.7 seconds for
138+
OpenAdapt and 36.5 seconds for the agent. OpenAdapt made no model calls; the agent cost about
139139
$0.08 a run at list price. Both saved 62 of 63 normal referrals correctly,
140140
judged by the same database read-back, and the agent matched or beat OpenAdapt
141141
in three other tests. [Every result](https://openadapt.ai/compare/computer-use-agents)
142-
is published. OpenAdapt isn't affiliated with the OpenEMR project.
142+
is published, and the [run data](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/openemr_referral_timing/README.md)
143+
is in this repository. OpenAdapt isn't affiliated with the OpenEMR project.
143144

144145
In a fault test through the real replayer, 72 of 90 runs left a wrong record
145146
behind a success screen. A check of the success banner reported success on 54
146-
of those 72; a full read of the database caught all 72. [How a save is checked](https://docs.openadapt.ai/explanation/verification/) has the method.
147+
of those 72; a full read of the database caught all 72. The faults were a fixed
148+
list injected into our own engine: a coverage test, not a real-world error rate. [How a save is checked](https://docs.openadapt.ai/explanation/verification/) has the method.
147149

148150
<details>
149151
<summary>Where it runs</summary>

‎packages/flow/README.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,15 @@ behavior: we haven't timed a codegen script on the same task or run one
129129
through our UI-change fixtures.
130130

131131
A newer test on a local OpenEMR copy (14 and 15 Oct 2026, synthetic
132-
referrals) compared compiled replay with a Claude Sonnet agent over 62 timed
133-
warm runs each. Replay took 29.7 s median from sign-in to saved record, the
132+
referrals) compared compiled replay with a Claude Sonnet agent over 63
133+
new-patient referrals each, in timed warm runs; each saved 62 correctly. Over those correct runs, replay took
134+
29.7 s median from sign-in to saved record, the
134135
agent 36.5 s, so replay was about 19% faster, with no model calls against the
135136
agent's median of about $0.08 per run. A separate database read-back matched
136137
12 of 12 fields on each correct save; engine certification was off, so the
137138
engine labeled these runs finished, not checked. Method:
138-
[openadapt.ai/compare/computer-use-agents](https://openadapt.ai/compare/computer-use-agents).
139+
[openadapt.ai/compare/computer-use-agents](https://openadapt.ai/compare/computer-use-agents);
140+
run data: [benchmark/openemr_referral_timing](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/benchmark/openemr_referral_timing/README.md).
139141
A side-by-side comparison with UiPath, Power
140142
Automate, and browser agents is at
141143
[openadapt.ai/compare](https://openadapt.ai/compare).
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
## Caveats (honest limits)
3+
- Fixture: a private copy of the realrun OpenEMR fixture (same pinned images, compose file, baseline SQL, reset and Rosa seed; own podman project `openadapt-openemr-bench` on ports 9311/9310, with `site_addr_oath` changed to the new port). It was needed because another session was resetting the shared fixture during the first smoke runs (those runs are kept in runs/smoke-shared-fixture-invalid and excluded). CPU is still shared: load is recorded per run; other sessions' probe processes seen during a run are recorded in result.json (`foreign_after`).
4+
- Sign-in and opening the New Patient form are inside the clock here (the prior study kept them off camera), so absolute numbers are a few seconds higher than the prior study's engine/agent times. The phase table lets you subtract them.
5+
- Screen recording (CDP screencast) runs during every timed run in all arms, as in the prior study; its attach time is a separate phase and `wall_ex_recorder` excludes it, but its CPU cost during the task is not removed.
6+
- The replay outcome on normal referrals is `COMPLETED_UNVERIFIED` (the engine's own check); correctness here is the harness's independent SQL read-back.
7+
- Agent time depends on Anthropic API latency at the time of each run; runs were interleaved with the replay arms to spread drift, not to remove it. Agent was given the same "do not create a duplicate" instruction as the prior study.
8+
- Within a block the patient list grows by one chart per run (same as prior batches); positions are rotated across blocks.
9+
- Warm replay clears the OCR memo at the start of every run (conservative); a real long-lived runner might hit it more.
10+
- 22:10-23:15 ET window: another process ran replays and reset the shared realrun fixture. This benchmark never used that fixture after the first smoke runs (private `openadapt-openemr-bench` fixture; every run checks its container start time, and no run was invalidated), so no timed run was fixture-contaminated. That process did compete for CPU: block 1 of every arm (22:27-23:50) overlaps it. `figures.json` reports the figures both for blocks 1 to 10 and excluding block 1.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# OpenEMR referral timing benchmark: protocol (written 2026-10-14, before any timed run)
2+
3+
## Question
4+
How long does it take, end to end, to register a referred patient in the local OpenEMR test copy and have the result verified, for (a) OpenAdapt replay of a compiled bundle and (b) a Claude Sonnet computer-use agent; and is the result correct?
5+
6+
## Fixed conditions
7+
- Machine: MacBook-Pro-5.local (12 cores), shared with other sessions. Load is sampled every 2 s during each run and recorded; each run waits for the prior study's load gate (1-min load < 8 for 4 consecutive 5 s samples, give up after 20 min and start anyway, flagged).
8+
- Fixture: the `realrun` OpenEMR podman fixture (state under site-v2/realrun/openadapt-flow/benchmark/openemr_local/state). Same reset as the prior study: `OpenEMRFixture.reset()` (drop + restore verified baseline, restart app) then seed the existing made-up chart Rosa Delgado 1958-03-27 through the REST API.
9+
- Patients: the 8 referrals in site-v2/visuals/referral/harness/referrals.py: 7 real (normal) referrals r1, r2, r3, r4, r5, r7, r8, plus the mismatch r6 (Rosa Delgado, different DOB) as the duplicate-halt check.
10+
- Browser: headless Chromium, 1280x800, DPR 1, fresh browser context per run (no cookies carried over), loopback CDP port for the screen recorder.
11+
- Footage: CDP screencast JPEG q95 for every run (same recorder as the prior study), encoded to MP4 after each block (outside timing); frames then deleted to save disk.
12+
13+
## Unit of work: a block
14+
A block = one fixture reset + Rosa seed, then the 8 referrals run back to back with one arm and one start mode, no reset between them (same start state as the prior study's batches). Each run creates at most one chart for a different person, so runs in a block do not interfere, except that the patient list grows by one per run. Referral order is rotated by one position per block (block k starts at referral k mod 8), so each referral appears in each position about equally. Arms and modes are interleaved in time (round-robin over arm x mode) so slow drift in machine load or API latency hits all arms.
15+
16+
## Arms
17+
1. `replay-main`: OpenAdapt engine at pinned main, bundle compiled 2026-10-10 (site-v2/visuals/referral/bundle), 0 model calls.
18+
2. `replay-dom`: same engine, bundle re-recorded/recompiled with DOM structured identity (scratch/replay-speed/dom-identity/out/bundle), 0 model calls.
19+
3. `agent-sonnet`: claude-sonnet-5-5 via the prior study's harness code (site-v2/visuals/agent/harness/cua.py: same toolset, prompt, instruction, effort `medium`, caps, settle-before-screenshot). Spend ledger is separate (scratch/benchmark/spend.json), total cap $15, stop starting new runs at $14; per-run cap $0.75 (prior runs cost $0.07-0.12).
20+
21+
Engine commits: start pin = origin/main 0c642bc0 (#503 merged). At the end, the replay arms are re-run on the newest merged main (if it differs) as a separate, labelled dataset; pinned commit SHA is recorded per run.
22+
23+
## Start modes
24+
- `cold`: a new Python process per run. Clock starts in the parent immediately before spawning; includes interpreter start, imports, OCR model load (lazy, on first use), Chromium launch, sign-in, opening the form, the task, read-back.
25+
- `warm`: one long-lived Python process per block; modules imported, Chromium launched and the OCR engine (or the API client) initialised once before the block, untimed (recorded as `warm_setup_s`). Clock starts at the call for that run; includes new browser context, sign-in, opening the form, the task, read-back.
26+
Fixture reset/seed is a test precondition, never inside the clock (recorded separately).
27+
28+
## Wall-clock definition
29+
`wall_s` = clock start (above) -> independent read-back finished and verdict computed. Measured with time.time() in one process (cold: child writes absolute marks, parent spawn time is the start). Phases (absolute marks, so they sum to wall):
30+
- `proc_start` (cold only): spawn -> main() entered
31+
- `imports` (cold only): engine/harness imports
32+
- `browser_launch`: Playwright start + Chromium launch (cold) or new context/page (warm)
33+
- `signin_form`: OpenEMR sign-in + open blank New Patient form + stable pre-trial count snapshot (the setup the prior study kept "off camera"; here it is counted)
34+
- `recorder_start`: screen recorder attach (measurement apparatus; reported, and `wall_ex_recorder_s` = wall - recorder_start is also reported)
35+
- `task`: replay `Replayer.run` (engine) or the agent loop (first model call -> final report)
36+
- `readback`: 1.0 s oracle settle (same as prior) + SQL/REST read-back + table-count deltas + verdict
37+
Teardown (recorder stop, browser close) is after the clock and recorded.
38+
Agent task is further split into model-wait and browser-action seconds (from cua.py); replay task into per-step elapsed (engine report).
39+
40+
## Correctness (per run)
41+
- Normal referral: correct iff exactly 1 new chart and the read-back of that chart matches 12/12 referral fields.
42+
- r6 mismatch: correct iff 0 new charts (the run halted or cancelled). Engine outcome / agent stop reason recorded.
43+
Read-back is by the test setup (direct SQL on the fixture DB), never by the arm under test.
44+
A run also records: engine outcome or agent stop reason, actions (replay: steps executed; agent: input actions), screens sent to a model (replay 0), model calls, tokens, list-price cost, local screenshots (replay).
45+
Validity guard: the OpenEMR container start time is checked before and after each run; a change (someone else reset the shared fixture) marks the run `invalid` and it is excluded and reported.
46+
47+
## N
48+
Target: 10 blocks per arm per mode = 10 runs per patient per arm per mode (plus 10 r6 checks). If time on the shared machine does not permit 10 cold blocks for the replay arms, or the agent spend cap binds, the achieved N is reported. Agent cold vs warm differ only in process start (the model dominates), so the agent arm runs 10 warm blocks and as many cold blocks as budget allows (target 3).
49+
50+
## Statistics
51+
Per arm x mode, over correct normal runs (7 patients pooled and per patient): median, p10/p90 (linear interpolation), min/max, mean, and a 95% CI for the median (bootstrap, 10,000 resamples, percentile) and for the mean (t). Arm differences: median of differences is not paired by design, so the difference of medians with a bootstrap CI. Raw per-run CSV with every phase. Failed/incorrect runs are listed, not dropped silently.
52+
53+
## Not in scope / caveats fixed in advance
54+
- No website edits. Healthcare prospect not named anywhere.
55+
- Agent timing depends on Anthropic API latency at run time; it is reported with the time of day.
56+
- The agent is given the same "do not create a duplicate" instruction as the prior study.
57+
58+
## Amendment (2026-10-14 23:45 ET, after block 1; before any analysis of differences)
59+
Other sessions kept the 12-core machine's 1-min load at ~9-10 (test suites, the other executor's probes), so the load gate at 8 stalled runs 5-8 min each; the full plan would have taken ~17 h. From block 2 on: gate threshold 12 (= core count; load is still recorded per run and reported), and replay cold blocks capped at 5 (N=5 per patient per replay arm cold). Warm arms keep 10 blocks; agent cold keeps 3.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# OpenEMR referral timing: OpenAdapt and a computer-use agent
2+
3+
Raw data and derived figures for the timed referral comparison published on
4+
openadapt.ai, run on 14 and 15 October 2026. Every figure the website, docs or
5+
blog cite for this comparison must be in [`figures.json`](figures.json), which
6+
[`compute_figures.py`](compute_figures.py) derives from [`runs.csv`](runs.csv).
7+
8+
| File | What it is |
9+
| --- | --- |
10+
| `runs.csv` | One row per timed run, 424 rows, every phase and count |
11+
| `figures.json` | The figures, derived from `runs.csv` |
12+
| `compute_figures.py` | Derives `figures.json`; `--check` fails when it is stale |
13+
| `PROTOCOL.md` | The protocol, written before the first timed run, and its one amendment |
14+
| `CAVEATS.md` | Known limits of the run |
15+
16+
All patients, clinicians, addresses, phone numbers (555-01xx) and email
17+
addresses (`example.invalid`) are made up. The rows hold timings, counts and
18+
costs only. The `dir` column points at per-run evidence (screenshots, screen
19+
recordings, engine reports, agent transcripts) that is kept outside the
20+
repository.
21+
22+
`runs.csv` is the harness's `data/runs.csv` with two changes: `dir` was made
23+
relative to the harness folder, and line endings were normalized from CRLF to
24+
LF. That is why its sha256 differs from the original file; every value is the
25+
same.
26+
27+
## What was measured
28+
29+
The task registers a referred patient in a local OpenEMR 8.0.0.3 test copy. Eight
30+
synthetic referrals: seven new patients and one possible duplicate of an
31+
existing patient (r6), where the correct result is no new chart.
32+
33+
- **OpenAdapt** (`replay-dom` in `runs.csv`): the referral program compiled with
34+
DOM element identity, replayed by engine commit 0c642bc0. 0 model calls.
35+
- **Agent** (`agent-sonnet`): claude-sonnet-5-5 with a capped computer-use
36+
harness, effort `medium`, list-price token costs.
37+
- `replay-main` is the same task from the 10 October bundle on the same engine.
38+
It is in the data and in `figures.json` (`openadapt_main_bundle`), and is not
39+
the published OpenAdapt figure.
40+
41+
A block is one fixture reset followed by the eight referrals back to back. Arms
42+
were interleaved in time. **Warm** means the browser process was already running;
43+
the clock starts at a new browser context and covers sign-in, opening the New
44+
Patient form, the screen-recorder attach, the task and the read-back.
45+
46+
A run is **correct** when the test setup's own database read-back finds exactly
47+
one new chart with 12 of 12 fields matching, or, for r6, no new chart. The arm
48+
under test never judges itself.
49+
50+
## Published figures
51+
52+
The published set (`sets.published`) is warm runs in blocks 2 to 10. Block 1 is
53+
left out because another process competed for the CPU during it. Blocks 11 to 15
54+
exist only for OpenAdapt; they were run later, paired with a different engine
55+
build, without the agent.
56+
57+
| Figure | OpenAdapt | Agent |
58+
| --- | --- | --- |
59+
| New-patient referrals saved and read back 12 of 12 | 62 of 63 | 62 of 63 |
60+
| Possible duplicate: no new chart | 9 of 9 | 9 of 9 |
61+
| Median time, correct runs (n = 62 each) | 29.7 s | 36.5 s |
62+
| p10 to p90 | 26.8 to 32.5 s | 32.4 to 46.1 s |
63+
| Steps or actions per correct run | 41 in every run | median 49 (42 to 76) |
64+
| Screens sent to a model, median | 0 | 10 |
65+
| Model cost per run | $0 | median $0.083 ($0.02 to $0.20) |
66+
67+
OpenAdapt's median was 6.8 s lower (bootstrap 95% CI 5.3 to 9.5 s), 18.6% less
68+
time than the agent's. The one miss in each arm saved nothing:
69+
70+
- OpenAdapt, block 9, r4: the engine stopped before saving because the duplicate
71+
search did not show the expected "No matches were found." text.
72+
- Agent, block 6, r5: the model reported that its browser tool was unavailable
73+
and stopped without entering data.
74+
75+
With block 1 included (blocks 1 to 10): 69 of 70 correct for both, medians 29.7 s
76+
(OpenAdapt) and 37.6 s (agent). The `replay-main` bundle: 62 of 63, median 38.6 s
77+
(blocks 2 to 10).
78+
79+
## Rebuild the figures
80+
81+
```bash
82+
python packages/flow/benchmark/openemr_referral_timing/compute_figures.py
83+
```
84+
85+
The script uses only the standard library and a fixed bootstrap seed, so the
86+
output is identical on every machine. A published figure that is not in
87+
`figures.json` has no source and should not be published.

0 commit comments

Comments
 (0)