Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ improvising or retrying when something is off.
| Capability | Status | Evidence | Where |
| --- | --- | --- | --- |
| Compile one or more demonstrations into a deterministic program | Released | Real app (OpenEMR referral, 62 timed runs) | `openadapt-flow record`, `compile`; [packages/flow](packages/flow/README.md) |
| Try the three acts on a real app: `openadapt quickstart --app openemr` runs the certified OpenEMR referral program in Docker, five referrals at once checked in OpenEMR's record, then a possible duplicate stopped before saving; without Docker or a clone it says so and exits 2 | On main ([#1431](https://github.com/OpenAdaptAI/OpenAdapt/pull/1431)) | Real app (local OpenEMR 8.0.0.3, synthetic patients) | `openadapt quickstart --app openemr`; [docs/quickstart](docs/quickstart/README.md#on-a-real-app---app-openemr) |
| Classify common local edits without a person (select-all, Enter in a field or dropdown, an in-form tab, an informational dialog's dismiss), only when the recording's network capture shows no write and a runtime check confirms only the expected change | On main ([#1366](https://github.com/OpenAdaptAI/OpenAdapt/pull/1366)) | Synthetic; OpenEMR referral recording re-admitted offline (violations 12 to 1 under `permissive`) | [LOCAL_EDIT_CLASSIFICATION.md](packages/flow/docs/design/LOCAL_EDIT_CLASSIFICATION.md), `compiler/local_edit.py` |
| Right-record check before any write (structure, then OCR, then pixels) | Released | Real app and synthetic | [RESOLUTION_LADDER.md](packages/flow/docs/RESOLUTION_LADDER.md), `runtime/identity.py` |
| Read the saved record back from the system of record (API, database, FHIR, separate session) | Released | Real app (OpenEMR 12 of 12 fields) | [VERIFICATION.md](packages/flow/docs/VERIFICATION.md), `runtime/effects/` |
Expand Down
55 changes: 55 additions & 0 deletions docs/quickstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,58 @@
- `timing-2026-10-20.json`: the engine tutorial with `--break-it` (what
`openadapt quickstart --break-it` ran before) against the new quickstart,
three rounds each, interleaved, from `scripts/quickstart_timing.py`.

## On a real app: `--app openemr`

```bash
openadapt quickstart --app openemr
```

The same three acts on OpenEMR 8.0.0.3, a real open-source EHR, instead of the
bundled MockMed. The default stays MockMed: it needs no Docker and finishes in
about 22 seconds.

- **Needs** Docker (Docker Desktop, or Podman with `DOCKER_HOST` pointed at
its socket) and a clone of this repository, because the OpenEMR fixture is
repository-only:

```bash
git clone https://github.com/OpenAdaptAI/OpenAdapt && cd OpenAdapt
pip install -e . -e packages/flow
```

Without either, the command says which one is missing in one line and exits
with status 2. It never falls back to MockMed.
- **Setup.** The pinned fixture from `packages/flow/benchmark/openemr_local/`:
pinned image digests, verified source files, a hashed SQL baseline, loopback
ports 9741 (HTTP) and 9740 (HTTPS) (set `OPENADAPT_QUICKSTART_OPENEMR_HTTP_PORT`
and `OPENADAPT_QUICKSTART_OPENEMR_HTTPS_PORT` to move them), and its own Compose project
(`openadapt-quickstart-openemr`). Its state stays in
`~/.cache/openadapt/quickstart-openemr`. The first run downloads the images
if they're missing (about 2.6 GB unpacked), installs OpenEMR, and adds
synthetic API clients. Later runs reuse the running copy and restore its
baseline.
- **Act 1, show it once.** The referral as it was demonstrated once in
OpenEMR: the recorded actions of the certified referral program, the same
program behind the site's OpenEMR Seals
(`benchmark/openemr_local/referral_program/`). It was compiled with the
shared duplicate rule (`apply_referral_rules`) and qualified under
`clinical-write`.
- **Act 2, run it.** Five synthetic referrals at once, each on its own clerk
account and browser. Each runs through the certified run path (the Standard
run gate, then the REST record check) and is read back from OpenEMR's
database with SQL: "✓ checked in OpenEMR's record".
- **Act 3, break it.** The quickstart adds an existing chart (Rosa Delgado,
born 1958-03-27), then runs a referral for a Rosa Delgado born 1958-03-22.
OpenEMR's duplicate check lists the match, and the program stops before
saving ("Stopped before saving: possible duplicate"). SQL finds 0 new charts.
- **Finale.** The same receipt card and Seal. Every quickstart, in either app,
ends the receipt with "See real Seals from OpenEMR runs:
https://openadapt.ai/seals".

To stop the fixture, run
`docker compose -p openadapt-quickstart-openemr stop`. The next quickstart
starts it again.

The real run is covered by `test_real_openemr_quickstart` (opt in with
`OPENADAPT_REAL_QUICKSTART_OPENEMR=1` on a machine with Docker); CI skips it.
35 changes: 35 additions & 0 deletions docs/quickstart/quickstart-openemr.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Records docs/quickstart/quickstart-openemr.gif with VHS (https://github.com/charmbracelet/vhs, MIT).
#
# PATH="$PWD/.venv/bin:$PATH" vhs docs/quickstart/quickstart-openemr.tape
#
# Run it from a clone with Docker running (with Podman, export DOCKER_HOST
# first; the recording's shell inherits it). The fixture from an earlier run
# is reused, so this records a warm run. The browsers stay hidden (no display
# in the recording). Everything else is the real command, start to finish, at
# real speed.

Output docs/quickstart/quickstart-openemr.gif

Set Shell bash
Set FontSize 15
Set Width 1240
Set Height 1100
Set Padding 24
Set Framerate 12
Set TypingSpeed 45ms
Set Theme "Builtin Dark"

Hide
Type "unset DISPLAY WAYLAND_DISPLAY; export SSH_CONNECTION=recording BROWSER=true OPENADAPT_FLOW_SCRUB=off OPENADAPT_SEAL_HOME=$(mktemp -d) PS1='$ '; cd $(mktemp -d) && clear"
Enter
Show

Sleep 500ms
Type "openadapt quickstart --app openemr"
Sleep 400ms
Enter
Wait+Screen@600s /Press Enter/
Sleep 2s
Enter
Wait+Screen@300s /Give your agent/
Sleep 8s
1 change: 1 addition & 0 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ improvising or retrying when something is off.
| Capability | Status | Evidence | Where |
| --- | --- | --- | --- |
| Compile one or more demonstrations into a deterministic program | Released | Real app (OpenEMR referral, 62 timed runs) | `openadapt-flow record`, `compile`; [packages/flow](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/README.md) |
| Try the three acts on a real app: `openadapt quickstart --app openemr` runs the certified OpenEMR referral program in Docker, five referrals at once checked in OpenEMR's record, then a possible duplicate stopped before saving; without Docker or a clone it says so and exits 2 | On main ([#1431](https://github.com/OpenAdaptAI/OpenAdapt/pull/1431)) | Real app (local OpenEMR 8.0.0.3, synthetic patients) | `openadapt quickstart --app openemr`; [docs/quickstart](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/docs/quickstart/README.md#on-a-real-app---app-openemr) |
| Classify common local edits without a person (select-all, Enter in a field or dropdown, an in-form tab, an informational dialog's dismiss), only when the recording's network capture shows no write and a runtime check confirms only the expected change | On main ([#1366](https://github.com/OpenAdaptAI/OpenAdapt/pull/1366)) | Synthetic; OpenEMR referral recording re-admitted offline (violations 12 to 1 under `permissive`) | [LOCAL_EDIT_CLASSIFICATION.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/design/LOCAL_EDIT_CLASSIFICATION.md), `compiler/local_edit.py` |
| Right-record check before any write (structure, then OCR, then pixels) | Released | Real app and synthetic | [RESOLUTION_LADDER.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/RESOLUTION_LADDER.md), `runtime/identity.py` |
| Read the saved record back from the system of record (API, database, FHIR, separate session) | Released | Real app (OpenEMR 12 of 12 fields) | [VERIFICATION.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/VERIFICATION.md), `runtime/effects/` |
Expand Down
56 changes: 53 additions & 3 deletions openadapt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ def _is_externally_managed_error(error: BaseException) -> bool:
_QUICKSTART_OPTIONS = frozenset(
{
"--out",
"--app",
"--headless",
"--no-break",
"--verbose",
Expand Down Expand Up @@ -548,6 +549,17 @@ def _quickstart_root(out: Optional[Path]) -> Path:
f"{_DEFAULT_QUICKSTART_DIR}, suffixed -2, -3, ... when taken."
),
)
@click.option(
"--app",
type=click.Choice(["mockmed", "openemr"]),
default="mockmed",
show_default=True,
help=(
"The app to run it in. mockmed is bundled (about 22 seconds, no Docker). "
"openemr runs OpenEMR 8.0.0.3 in Docker from a clone of the repository; "
"the first run downloads and installs it, later runs reuse it."
),
)
@click.option(
"--headless",
is_flag=True,
Expand All @@ -570,6 +582,7 @@ def _quickstart_root(out: Optional[Path]) -> Path:
def quickstart(
command_ctx: click.Context,
out: Optional[Path],
app: str,
headless: bool,
no_break: bool,
verbose: bool,
Expand All @@ -584,14 +597,21 @@ def quickstart(
screen says saved, the database has nothing, and OpenAdapt stops. The
program makes no AI calls. Nothing in the output directory is overwritten.

With --app openemr, the same three acts run on OpenEMR 8.0.0.3 in Docker,
with the certified referral program: referrals registered at once and
checked in OpenEMR's record, then a referral that matches an existing
chart, where the program stops before saving. It runs from a clone of the
repository and needs Docker; without either it says so and exits 2.

Other flags of the engine tutorial (for example --guided or
--interactive-record) run that tutorial instead, unchanged.
"""
_require_supported_python()
_refuse_unknown_tutorial_options(command_ctx.args)
openemr = _openemr_checkout(command_ctx.args) if app == "openemr" else None
root = _quickstart_root(out)

if command_ctx.args or not _flow_runs_quickstart():
if openemr is None and (command_ctx.args or not _flow_runs_quickstart()):
# Engine-owned flags (--guided, --interactive-record, and future
# engine additions) run the engine's own tutorial, as before. So does
# a Flow older than the pieces the three-act quickstart uses.
Expand All @@ -605,7 +625,7 @@ def quickstart(
steps = next_steps(
_hint,
_shell_quote,
_agent_serves_tutorial(),
openemr is None and _agent_serves_tutorial(),
keep=_keep_command(),
shadow=_shadowing_openadapt if _under_uvx() else None,
)
Expand All @@ -617,6 +637,7 @@ def quickstart(
break_it=not no_break,
verbose=verbose,
steps=steps,
openemr=openemr,
)
except KeyboardInterrupt:
click.echo(f"\nStopped. Anything written so far is in {root}", err=True)
Expand All @@ -625,7 +646,11 @@ def quickstart(
if _is_externally_managed_error(error):
_echo_python_remedy()
raise click.exceptions.Exit(1) from error
if type(error).__name__ in ("TutorialError", "DurableAuthorityBusy"):
if type(error).__name__ in (
"TutorialError",
"DurableAuthorityBusy",
"FixtureError",
):
raise click.ClickException(
f"The quickstart stopped: {error}\nAnything it wrote is in {root}"
) from error
Expand All @@ -636,6 +661,31 @@ def quickstart(
raise click.exceptions.Exit(1)


def _openemr_checkout(extra: list[str]) -> Path:
"""The clone's packages/flow for ``--app openemr``, or exit 2 saying why.

Each missing piece is one line on stderr. Nothing falls back to MockMed.
"""
from openadapt.quickstart.openemr import (
checkout_problem,
docker_problem,
find_flow_checkout,
)

if extra:
raise click.UsageError(
f"--app openemr runs the three-act quickstart only; {extra[0]} "
"belongs to the engine's MockMed tutorial."
)
problem = docker_problem()
flow = find_flow_checkout() if problem is None else None
problem = problem or checkout_problem(flow)
if problem is not None or flow is None:
click.echo(problem, err=True)
raise click.exceptions.Exit(2)
return flow


# The oldest Flow with every piece the three-act quickstart uses (shared
# browser, record checks bound to run inputs, the side-by-side page).
_QUICKSTART_FLOW_MINIMUM = (2, 2, 0)
Expand Down
24 changes: 18 additions & 6 deletions openadapt/quickstart/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@

import sys
from pathlib import Path
from typing import IO, Callable, Optional
from typing import IO, Any, Callable, Optional

from openadapt.quickstart.scene import MOCKMED
from openadapt.quickstart.story import (
YOUR_APP_URL,
NextSteps,
Expand Down Expand Up @@ -68,21 +69,32 @@ def run_quickstart(
steps: Callable[[Outcome], NextSteps],
stream: Optional[IO[str]] = None,
interactive: Optional[bool] = None,
openemr: Optional[Path] = None,
) -> Outcome:
"""Run the quickstart into ``root`` and print it to ``stream``."""
"""Run the quickstart into ``root`` and print it to ``stream``.

``openemr`` is a clone's ``packages/flow`` folder: the three acts then run
on OpenEMR in Docker instead of the bundled MockMed.
"""

from openadapt.quickstart.engine import FlowEngine, display_available, open_page
from openadapt.quickstart.view import make_view

stream = stream or sys.stdout
if interactive is None:
interactive = bool(sys.stdin.isatty() and getattr(stream, "isatty", bool)())
# A person watching gets a short pause between demonstrated actions; a
# hidden browser runs at full speed.
engine = FlowEngine(root, visible=visible, pace_s=0.3 if visible else 0.0)
engine: Any
if openemr is not None:
from openadapt.quickstart.openemr import OpenEMREngine

engine = OpenEMREngine(root, visible=visible, flow=openemr)
else:
# A person watching gets a short pause between demonstrated actions;
# a hidden browser runs at full speed.
engine = FlowEngine(root, visible=visible, pace_s=0.3 if visible else 0.0)
return run_story(
engine,
make_view(stream),
make_view(stream, scene=getattr(engine, "scene", MOCKMED)),
next_steps=steps,
break_it=break_it,
interactive=interactive,
Expand Down
40 changes: 26 additions & 14 deletions openadapt/quickstart/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from pathlib import Path
from typing import Any, Callable, Optional, Sequence

from openadapt.quickstart.scene import MOCKMED

#: Synthetic notes for the five runs. None of them is clinical advice.
RUN_NOTES = (
"Follow-up in two weeks",
Expand Down Expand Up @@ -336,6 +338,10 @@ def _sha256(path: Path) -> Optional[str]:
class FlowEngine:
"""The bundled tutorial, one stage per call, inside one browser process."""

#: Act 2's inputs: one new note per run.
inputs = RUN_NOTES
scene = MOCKMED

def __init__(self, root: Path, *, visible: bool, pace_s: float = 0.0) -> None:
self.root = root
self.visible = visible
Expand Down Expand Up @@ -595,20 +601,7 @@ def seal(self, runs: list[RunFacts]) -> Optional[SealFacts]:
no Seal and no command to check one.
"""

try:
from openadapt_flow.offline_seal import seal_home, seal_run
except ImportError:
return None
checked = [run for run in runs if run.checked]
if not checked:
return None
key_id = ""
for run in checked:
# The configured Seal key, or one generated under the Seal home
# on first use (the offline-Seal default).
seal = seal_run(run.run_dir, home=seal_home(), enqueue=False)
key_id = str(seal.key_id)
return SealFacts(key_id=key_id, sealed=len(checked))
return seal_runs(runs)

def write_page(self, first: RunFacts, broken: BreakFacts) -> Optional[Path]:
"""Write the side-by-side page (``openadapt flow demo``'s page)."""
Expand Down Expand Up @@ -652,6 +645,25 @@ def write_page(self, first: RunFacts, broken: BreakFacts) -> Optional[Path]:
return demo_proof.write_demo_page(evidence)


def seal_runs(runs: list[RunFacts]) -> Optional[SealFacts]:
"""Sign each checked run's report with this computer's Seal key."""

try:
from openadapt_flow.offline_seal import seal_home, seal_run
except ImportError:
return None
checked = [run for run in runs if run.checked]
if not checked:
return None
key_id = ""
for run in checked:
# The configured Seal key, or one generated under the Seal home on
# first use (the offline-Seal default).
seal = seal_run(run.run_dir, home=seal_home(), enqueue=False)
key_id = str(seal.key_id)
return SealFacts(key_id=key_id, sealed=len(checked))


def return_focus_to_terminal(
*, visible: bool, platform: Optional[str] = None, environ=None, run=None
) -> bool:
Expand Down
Loading
Loading