Publish README.md from marimo
Browse files
README.md
CHANGED
|
@@ -24,7 +24,11 @@ Python standard library.
|
|
| 24 |
|
| 25 |
> **Status 0.3.0: it drives a real desktop — on Linux and on Windows.** Screen capture,
|
| 26 |
> an OCR element index with real coordinates, a real mouse click, and a *verified*
|
| 27 |
-
> outcome. **All
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
> "which brain is right, and how often?" is a number in a table.
|
| 29 |
> The suite passes on a live Linux desktop and inside a real Windows Server
|
| 30 |
> 2022 guest; what is not solved is model reliability — see
|
|
@@ -86,7 +90,7 @@ X server. First the suite, headless:
|
|
| 86 |
```bash
|
| 87 |
python -m pip install -e ".[desktop,ocr,dev]"
|
| 88 |
pytest tests -q
|
| 89 |
-
#
|
| 90 |
```
|
| 91 |
|
| 92 |
The two skips are the live-desktop tests. Give them a display and they run for real:
|
|
@@ -187,6 +191,46 @@ harness *allowed* that decision to run is a separate axis: a click the policy re
|
|
| 187 |
still the choice it was, and it is flagged `(harness refused it)` rather than passed off as
|
| 188 |
a clean success. Keeping those two apart is the point.
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
### Which brain drove which run
|
| 191 |
|
| 192 |
The harness and the brain are separable, and it matters which one a result came from.
|
|
@@ -218,7 +262,7 @@ open problem: it is reliable on a clean screenshot and unreliable on a busy live
|
|
| 218 |
`warning: no text was found on screen, so the index is 96 numbered grid cells and the
|
| 219 |
agent is choosing blind` — instead of silently guessing.
|
| 220 |
|
| 221 |
-
**
|
| 222 |
that click a real button.
|
| 223 |
|
| 224 |
## Verified on real Windows
|
|
@@ -260,7 +304,7 @@ the harness itself, installed into site-packages
|
|
| 260 |
|
| 261 |
None of the three failures in the first row is a harness defect: two are
|
| 262 |
`ModuleNotFoundError: numpy` and the third is the guard bug documented below. With the
|
| 263 |
-
OCR engine installed, the same suite exits **0**. (Those Windows counts predate `trace.py`; the harness suite has since grown to
|
| 264 |
|
| 265 |
**The emulated-OCR caveat.** onnxruntime requires AVX2, and QEMU's software emulation of
|
| 266 |
AVX2 is roughly two orders of magnitude slower than native. A suite that finishes in 17
|
|
@@ -286,7 +330,7 @@ exactly the kind of thing that only surfaces on a machine without an OCR engine.
|
|
| 286 |
|
| 287 |
## The test suite, in full
|
| 288 |
|
| 289 |
-
`pytest tests` collects **
|
| 290 |
weights. The last 2 are the opt-in live-desktop tests, skipped unless
|
| 291 |
`TINY_COMPUTER_AGENT_LIVE_TEST=1`.
|
| 292 |
|
|
@@ -306,11 +350,15 @@ weights. The last 2 are the opt-in live-desktop tests, skipped unless
|
|
| 306 |
| `tests/test_trace.py` | 28 | traces: a run recorded with no pixels, replayed against another brain, and scored against ground truth |
|
| 307 |
| `tests/test_models.py` | 7 | the model backends: lazy loading, device choice, honest refusals |
|
| 308 |
| `tests/test_api_surface.py` | 4 | the public API is a promise: everything advertised is importable, and nothing public is left out |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
| `tests/test_live_desktop.py` | 2 | **needs a display, opt-in**: OCR finds a real button at the pixels it was drawn at, and a real click makes the app react |
|
| 310 |
|
| 311 |
```bash
|
| 312 |
-
pytest tests -q #
|
| 313 |
-
TINY_COMPUTER_AGENT_LIVE_TEST=1 pytest tests -q #
|
| 314 |
```
|
| 315 |
|
| 316 |
## Install
|
|
@@ -319,7 +367,15 @@ TINY_COMPUTER_AGENT_LIVE_TEST=1 pytest tests -q # 164 passed, on a desktop
|
|
| 319 |
pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
|
| 320 |
pip install -e ".[desktop,ocr]" # + real capture and real input
|
| 321 |
pip install -e ".[vision,ocr]" # + the tiny model brains
|
| 322 |
-
pytest #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
```
|
| 324 |
|
| 325 |
## Run it on your machine
|
|
@@ -421,7 +477,61 @@ this: an answer file with non-canonical XML namespaces is **silently ignored** b
|
|
| 421 |
| `desktop.py` | Preflight checks, `doctor`, and `build_agent` in one call |
|
| 422 |
| `demo.py` | The synthetic desktop used by tests and `--image example` |
|
| 423 |
| `trace.py` | Portable traces: record a run with no pixels, replay it offline, score the choice against ground truth |
|
|
|
|
|
|
|
| 424 |
| `showcase.py` | The capability gallery: one real screenshot per capability, checked against the public API |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
|
| 426 |
## Capability gallery
|
| 427 |
|
|
@@ -1068,15 +1178,22 @@ when no verifier is given.
|
|
| 1068 |
- [x] A real click on a real window, with a verified outcome
|
| 1069 |
- [x] Blind-grid-click guard, echo detection, no-progress guard
|
| 1070 |
- [x] The harness suite running on real Windows (Server 2022 guest, no KVM — see above)
|
| 1071 |
-
- [ ] Windows run of the *live desktop* test: the workflow is committed, but it needs a
|
| 1072 |
-
GitHub mirror with Actions enabled — Hugging Face does not run GitHub Actions
|
| 1073 |
-
- [ ] Make the OCR guard in `test_cli.py` skip, rather than fail, when no engine is installed
|
| 1074 |
-
- [ ] Make the VLM reliable on a live screen (it echoes the prompt instead of choosing)
|
| 1075 |
-
- [ ] Accessibility-tree detector (AT-SPI on Linux, UI Automation on Windows)
|
| 1076 |
- [x] Score episodes: replay a trace and count how often the choice was right - see [scoring a brain, offline](#scoring-a-brain-offline)
|
| 1077 |
- [x] A capability gallery: one real screenshot per capability, generated by `showcase.py`
|
| 1078 |
-
- [
|
| 1079 |
-
- [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1080 |
|
| 1081 |
## License
|
| 1082 |
|
|
|
|
| 24 |
|
| 25 |
> **Status 0.3.0: it drives a real desktop — on Linux and on Windows.** Screen capture,
|
| 26 |
> an OCR element index with real coordinates, a real mouse click, and a *verified*
|
| 27 |
+
> outcome. **All 73 capabilities have a real screenshot** in the [capability gallery](#capability-gallery).
|
| 28 |
+
> The loop can be driven from a [marimo console](#the-marimo-console) with a live preview, an
|
| 29 |
+
> action log and pause / resume / step; elements can come from the
|
| 30 |
+
> [accessibility tree](#what-the-machine-already-knows-the-accessibility-tree) instead of OCR;
|
| 31 |
+
> and a run can be saved as a portable *trace*, replayed offline and **scored**, so
|
| 32 |
> "which brain is right, and how often?" is a number in a table.
|
| 33 |
> The suite passes on a live Linux desktop and inside a real Windows Server
|
| 34 |
> 2022 guest; what is not solved is model reliability — see
|
|
|
|
| 90 |
```bash
|
| 91 |
python -m pip install -e ".[desktop,ocr,dev]"
|
| 92 |
pytest tests -q
|
| 93 |
+
# 217 passed, 2 skipped in 18.7s
|
| 94 |
```
|
| 95 |
|
| 96 |
The two skips are the live-desktop tests. Give them a display and they run for real:
|
|
|
|
| 191 |
still the choice it was, and it is flagged `(harness refused it)` rather than passed off as
|
| 192 |
a clean success. Keeping those two apart is the point.
|
| 193 |
|
| 194 |
+
### Asking a tiny model in four ways (measured)
|
| 195 |
+
|
| 196 |
+
The card used to say the 256M VLM "echoes the prompt instead of choosing". That is
|
| 197 |
+
reproducible - and so is the fix. A live GTK window, a menu of four exact widgets
|
| 198 |
+
(Files, Terminal, Save, Cancel), and the goal `click 'Save' to save the document`:
|
| 199 |
+
|
| 200 |
+
```
|
| 201 |
+
mode echoed seconds outcome
|
| 202 |
+
json no 8.9 BrainError: the reply mentions 4 menu entries, so it looks like an echo
|
| 203 |
+
choice no 1.9 BrainError: the constrained reply was not a menu number: '1000'
|
| 204 |
+
scan no 1.4 click label='Files' (the model said yes to the first candidate)
|
| 205 |
+
rank no 9.4 click label='Files' (highest yes/no log-odds margin, +0.94)
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
Four ways to ask, all of them the same weights:
|
| 209 |
+
|
| 210 |
+
| mode | how the model is asked | answer space |
|
| 211 |
+
|---|---|---|
|
| 212 |
+
| `json` | reply with JSON, parsed leniently | the whole vocabulary |
|
| 213 |
+
| `choice` | one masked generation for a menu number | a handful of digits |
|
| 214 |
+
| `scan` | one masked yes/no question per item, first yes wins | two tokens |
|
| 215 |
+
| `rank` | the same questions, ranked by the yes/no log-odds margin | two tokens |
|
| 216 |
+
|
| 217 |
+
What the numbers actually say:
|
| 218 |
+
|
| 219 |
+
- **The echo is fixed, structurally.** In `json` mode the model hands the prompt
|
| 220 |
+
back (`"The goal is to click 'Save' to save the document..."`) and the harness
|
| 221 |
+
refuses it. In the three masked modes the reply *cannot* be the prompt, because
|
| 222 |
+
every one of its tokens is masked out of the logits. The failure stopped being
|
| 223 |
+
*detected* and started being *not representable*.
|
| 224 |
+
- **It is about six times faster** - 8.9 s of free text becomes 1.4 s for a
|
| 225 |
+
one-token answer - because the model is no longer asked to write.
|
| 226 |
+
- **Accuracy is not fixed, and the numbers say why.** The model answers now, and
|
| 227 |
+
answers *wrongly*: it accepts `Files` for a goal that names `Save`, even when the
|
| 228 |
+
answer space is two tokens and each candidate is judged on its own. A 256M model
|
| 229 |
+
cannot do this task. That is a finding about the weights, not about the harness -
|
| 230 |
+
which is why the rule-based planner is still the default brain.
|
| 231 |
+
|
| 232 |
+
Reproduce it with `python measure_vlm.py --screen live --detector best --open-gtk`.
|
| 233 |
+
|
| 234 |
### Which brain drove which run
|
| 235 |
|
| 236 |
The harness and the brain are separable, and it matters which one a result came from.
|
|
|
|
| 262 |
`warning: no text was found on screen, so the index is 96 numbered grid cells and the
|
| 263 |
agent is choosing blind` — instead of silently guessing.
|
| 264 |
|
| 265 |
+
**219 tests pass**, including real OCR over a real image and (opt-in) live-desktop tests
|
| 266 |
that click a real button.
|
| 267 |
|
| 268 |
## Verified on real Windows
|
|
|
|
| 304 |
|
| 305 |
None of the three failures in the first row is a harness defect: two are
|
| 306 |
`ModuleNotFoundError: numpy` and the third is the guard bug documented below. With the
|
| 307 |
+
OCR engine installed, the same suite exits **0**. (Those Windows counts predate `trace.py`; the harness suite has since grown to 219 tests.)
|
| 308 |
|
| 309 |
**The emulated-OCR caveat.** onnxruntime requires AVX2, and QEMU's software emulation of
|
| 310 |
AVX2 is roughly two orders of magnitude slower than native. A suite that finishes in 17
|
|
|
|
| 330 |
|
| 331 |
## The test suite, in full
|
| 332 |
|
| 333 |
+
`pytest tests` collects **219 tests**. 217 run headless — no display, no network, no
|
| 334 |
weights. The last 2 are the opt-in live-desktop tests, skipped unless
|
| 335 |
`TINY_COMPUTER_AGENT_LIVE_TEST=1`.
|
| 336 |
|
|
|
|
| 350 |
| `tests/test_trace.py` | 28 | traces: a run recorded with no pixels, replayed against another brain, and scored against ground truth |
|
| 351 |
| `tests/test_models.py` | 7 | the model backends: lazy loading, device choice, honest refusals |
|
| 352 |
| `tests/test_api_surface.py` | 4 | the public API is a promise: everything advertised is importable, and nothing public is left out |
|
| 353 |
+
| `tests/test_decoding.py` | 24 | the mask, the answer space, the retry ladder, the vote - and that an echo is refused |
|
| 354 |
+
| `tests/test_accessibility.py` | 16 | role filtering, the coordinate contract, the fallback chain, and real AT-SPI |
|
| 355 |
+
| `tests/test_stepwise.py` | 11 | begin / step / pause / resume, and that it equals the batch loop |
|
| 356 |
+
| `tests/test_ocr_guard.py` | 4 | the OCR guard really skips with no engine - the guard that never fired |
|
| 357 |
| `tests/test_live_desktop.py` | 2 | **needs a display, opt-in**: OCR finds a real button at the pixels it was drawn at, and a real click makes the app react |
|
| 358 |
|
| 359 |
```bash
|
| 360 |
+
pytest tests -q # 217 passed, 2 skipped
|
| 361 |
+
TINY_COMPUTER_AGENT_LIVE_TEST=1 pytest tests -q # 219 passed, on a desktop
|
| 362 |
```
|
| 363 |
|
| 364 |
## Install
|
|
|
|
| 367 |
pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
|
| 368 |
pip install -e ".[desktop,ocr]" # + real capture and real input
|
| 369 |
pip install -e ".[vision,ocr]" # + the tiny model brains
|
| 370 |
+
pytest # 219 tests, no display or network needed
|
| 371 |
+
```
|
| 372 |
+
|
| 373 |
+
The accessibility detector needs system packages rather than an extra:
|
| 374 |
+
|
| 375 |
+
```bash
|
| 376 |
+
apt install gir1.2-atspi-2.0 at-spi2-core gir1.2-gtk-3.0
|
| 377 |
+
pip install PyGObject
|
| 378 |
+
python -m tiny_computer_agent --doctor --detector best # is the tree reachable?
|
| 379 |
```
|
| 380 |
|
| 381 |
## Run it on your machine
|
|
|
|
| 477 |
| `desktop.py` | Preflight checks, `doctor`, and `build_agent` in one call |
|
| 478 |
| `demo.py` | The synthetic desktop used by tests and `--image example` |
|
| 479 |
| `trace.py` | Portable traces: record a run with no pixels, replay it offline, score the choice against ground truth |
|
| 480 |
+
| `decoding.py` | Constrained decoding: mask the logits to a menu number or a yes/no, so an echo is impossible |
|
| 481 |
+
| `accessibility.py` | The accessibility tree: `AtspiReader` and `AtspiDetector`, roles and real extents |
|
| 482 |
| `showcase.py` | The capability gallery: one real screenshot per capability, checked against the public API |
|
| 483 |
+
| `measure_vlm.py` | Measures the four ways to ask a model, on a real screen, and prints the table |
|
| 484 |
+
|
| 485 |
+
## The marimo console
|
| 486 |
+
|
| 487 |
+
The loop is a library, but it is also a *session* you can drive one turn at a time -
|
| 488 |
+
which is what a console needs: a live screen preview, an action log, and
|
| 489 |
+
step / pause / resume.
|
| 490 |
+
|
| 491 |
+
<img src="gallery/console.png" width="900">
|
| 492 |
+
|
| 493 |
+
`AgentLoop.run()` is the batch form; `begin()`, `step_once()`, `pause()`, `resume()`
|
| 494 |
+
and `finished` are the stepwise one. `tests/test_stepwise.py` asserts the two cannot
|
| 495 |
+
drift apart - the same scenario has to produce the same episode either way. The
|
| 496 |
+
console cell in this notebook wires that to marimo widgets:
|
| 497 |
+
|
| 498 |
+
- **live preview** - the current screen with the element index drawn over it, so you
|
| 499 |
+
can see what the agent sees and what it thinks is clickable
|
| 500 |
+
- **action log** - one row per step: the action, and the outcome or the refusal
|
| 501 |
+
- **step / run / reset** - `mo.ui.run_button`, so an auto-refresh can never fire a
|
| 502 |
+
step by accident
|
| 503 |
+
- **pause** - respected *between* steps: the episode stays open, and a single step
|
| 504 |
+
can still be taken through it
|
| 505 |
+
- **panic** - creates the same kill-switch file the CLI checks, so the next step
|
| 506 |
+
refuses to run
|
| 507 |
+
|
| 508 |
+
Switch the screen to "this machine, live" and it is the real desktop, indexed through
|
| 509 |
+
OCR or through the accessibility tree.
|
| 510 |
+
|
| 511 |
+
## What the machine already knows: the accessibility tree
|
| 512 |
+
|
| 513 |
+
OCR reads pixels and *guesses* which marks are widgets. An operating system's
|
| 514 |
+
accessibility tree simply knows: every application that publishes one advertises each
|
| 515 |
+
button with a role, a name, and its real screen extents.
|
| 516 |
+
|
| 517 |
+
<img src="gallery/atspi-index.png" width="860">
|
| 518 |
+
|
| 519 |
+
`AtspiDetector` reads that tree through PyGObject's `Atspi` typelib and keeps only the
|
| 520 |
+
roles worth clicking - a frame and a panel have names too, and offering them to a
|
| 521 |
+
model is how you get an agent that clicks the window furniture. The coordinates come
|
| 522 |
+
back as absolute screen pixels, so `absolute_coordinates` tells `IndexedScreen` not to
|
| 523 |
+
re-map them through a downscale.
|
| 524 |
+
|
| 525 |
+
<img src="gallery/desk-atspi.png" width="860">
|
| 526 |
+
|
| 527 |
+
That click was found **without reading a single pixel**: the window's own status label
|
| 528 |
+
reads `last action: Save` afterwards, and the app wrote `Save` to its marker file.
|
| 529 |
+
`--detector best` tries the tree first, then OCR, then the numbered grid.
|
| 530 |
+
|
| 531 |
+
**Honest limits.** AT-SPI is Linux-only here; **UI Automation is not implemented**, and
|
| 532 |
+
`accessibility_available("uia")` says exactly that rather than pretending - so an agent
|
| 533 |
+
on Windows keeps using OCR. Tk barely publishes a tree and a canvas publishes none,
|
| 534 |
+
which is why this is a detector and not a replacement.
|
| 535 |
|
| 536 |
## Capability gallery
|
| 537 |
|
|
|
|
| 1178 |
- [x] A real click on a real window, with a verified outcome
|
| 1179 |
- [x] Blind-grid-click guard, echo detection, no-progress guard
|
| 1180 |
- [x] The harness suite running on real Windows (Server 2022 guest, no KVM — see above)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1181 |
- [x] Score episodes: replay a trace and count how often the choice was right - see [scoring a brain, offline](#scoring-a-brain-offline)
|
| 1182 |
- [x] A capability gallery: one real screenshot per capability, generated by `showcase.py`
|
| 1183 |
+
- [x] The OCR guard skips instead of failing: one tested guard, `conftest.require_ocr`
|
| 1184 |
+
- [x] Stop the 256M VLM echoing the prompt - masked decoding, measured in
|
| 1185 |
+
[asking a tiny model in four ways](#asking-a-tiny-model-in-four-ways-measured)
|
| 1186 |
+
- [x] The marimo console: live screen preview, action log, pause / resume / step
|
| 1187 |
+
- [x] An accessibility-tree detector on Linux (AT-SPI), driving a real click with no OCR
|
| 1188 |
+
- [ ] **Make the 256M VLM choose correctly.** Masking fixed the echo and the latency; the model
|
| 1189 |
+
still picks the wrong item from a four-item menu of exact widgets. That is now a question
|
| 1190 |
+
about weights, or about a better default (the rule-based planner), not about prompt shape.
|
| 1191 |
+
- [ ] **UI Automation** (Windows) for the accessibility detector. The interface is here and
|
| 1192 |
+
`accessibility_available("uia")` reports it as not implemented rather than pretending.
|
| 1193 |
+
- [ ] Windows run of the *live desktop* test: the workflow is committed, but it needs a
|
| 1194 |
+
GitHub mirror with Actions enabled — Hugging Face does not run GitHub Actions
|
| 1195 |
+
- [ ] A **Windows** run of the gallery: the workflow builds it on `windows-latest` (job
|
| 1196 |
+
`gallery-windows`), with the same GitHub-mirror caveat
|
| 1197 |
|
| 1198 |
## License
|
| 1199 |
|