hkblockchaintechnology commited on
Commit
aa02af1
·
verified ·
1 Parent(s): 598cca6

Publish tiny_computer_agent_app.py from marimo

Browse files
Files changed (1) hide show
  1. tiny_computer_agent_app.py +156 -22
tiny_computer_agent_app.py CHANGED
@@ -18,7 +18,6 @@ with app.setup:
18
  from huggingface_hub import HfApi
19
 
20
 
21
-
22
  @app.cell(hide_code=True)
23
  def readme():
24
  readme_md = '''
@@ -97,6 +96,70 @@ def readme():
97
  verifier confirmed it before the episode was called a success. **Pixels in, a real click
98
  out, and evidence that it worked.**
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  ### On a replayed screenshot
101
 
102
  ```
@@ -112,6 +175,23 @@ def readme():
112
  step 0 | click label='Save' reason="the reply names 'Save'" -> pixel (295, 284)
113
  ```
114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  ### What did *not* work, and what caught it
116
 
117
  - **The 256M VLM echoes the prompt on a busy live screen.** Asked to click Save on the
@@ -126,16 +206,42 @@ def readme():
126
  `warning: no text was found on screen, so the index is 96 numbered grid cells and the
127
  agent is choosing blind` — instead of silently guessing.
128
 
129
- **123 tests pass**, including real OCR over a real image and (opt-in) live-desktop tests
130
  that click a real button.
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  ## Install
133
 
134
  ```bash
135
  pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
136
  pip install -e ".[desktop,ocr]" # + real capture and real input
137
  pip install -e ".[vision,ocr]" # + the tiny model brains
138
- pytest # 123 tests, no display or network needed
139
  ```
140
 
141
  ## Run it on your machine
@@ -175,20 +281,53 @@ def readme():
175
  was actually drawn at, clicks it for real, and checks the app reacted. It is opt-in
176
  because it moves your mouse — run it on a throwaway desktop.
177
 
178
- ### Testing on Windows (or macOS)
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
- This repo's live test was run on Linux/X11, because that is what the build container
181
- had. For real Windows coverage, mirror the repo to GitHub: `.github/workflows/live-desktop.yml`
182
- runs the unit suite **and** the live desktop test on `windows-latest` — a real Windows
183
- desktop session, free for public repositories — plus the same test under Xvfb on Linux,
184
- and a preflight on macOS. Trigger it from the Actions tab.
 
185
 
186
- Nothing in the harness is Linux-specific: pyautogui uses `SendInput` on Windows, and
187
- `GrabScreen` already falls back to Pillow's `ImageGrab` there. Two things can only fail
188
- on a real Windows desktop, which is exactly why the job exists: **DPI scaling** (that is
189
- what `--scale` is for) and the native input backend. At 125% or 150% display scaling,
190
- expect the coordinate assertion in `test_live_desktop.py` to be the first thing that
191
- fails — that is the signal, not a nuisance.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
193
  ## How it works
194
 
@@ -236,7 +375,7 @@ def readme():
236
 
237
  **Works:** real capture, a real OCR index with coordinates that land on the right pixel,
238
  a real click that an app reacts to, verified success, the closed vocabulary, the policy
239
- gates, the no-progress guard, echo detection, the doctor, the CLI, 123 tests — and a
240
  256M VLM that reads a button label from pixels on a clean screenshot.
241
 
242
  **Does not work yet:** reliability on a *live* screen. The 256M VLM echoed the prompt
@@ -250,7 +389,7 @@ def readme():
250
  - [x] A real click on a real window, with a verified outcome
251
  - [x] Blind-grid-click guard, echo detection, no-progress guard
252
  - [ ] Make the VLM reliable on a live screen (it echoes the prompt instead of choosing)
253
- - [ ] Windows run of the live test (workflow included; needs a GitHub mirror)
254
  - [ ] Accessibility-tree detector (AT-SPI on Linux, UI Automation on Windows)
255
  - [ ] Score episodes: replay a trace and count how often the choice was right
256
  - [ ] marimo console: live screen preview, action log, pause / resume / step
@@ -261,7 +400,6 @@ def readme():
261
  '''
262
 
263
  mo.md(readme_md)
264
-
265
  return (readme_md,)
266
 
267
 
@@ -346,7 +484,6 @@ def repo_files():
346
 
347
  mo.md(f"packaging: {len(pyproject_toml) + len(license_txt) + len(gitignore_txt)} chars, "
348
  f"v0.2.0 with ocr/desktop/vision extras")
349
-
350
  return gitignore_txt, license_txt, pyproject_toml
351
 
352
 
@@ -361,7 +498,6 @@ def publish():
361
  )
362
 
363
  publish_button
364
-
365
  return REPO_ID, publish_button
366
 
367
 
@@ -430,7 +566,6 @@ def publish_status(
430
  _status = mo.md(f"Publish failed: `{type(_e).__name__}: {_e}`").callout(kind="danger")
431
 
432
  _status
433
-
434
  return
435
 
436
 
@@ -471,7 +606,6 @@ def demo():
471
  mo.md(f"`{perception_episode.render().splitlines()[1]}`"),
472
  ]
473
  )
474
-
475
  return
476
 
477
 
 
18
  from huggingface_hub import HfApi
19
 
20
 
 
21
  @app.cell(hide_code=True)
22
  def readme():
23
  readme_md = '''
 
96
  verifier confirmed it before the episode was called a success. **Pixels in, a real click
97
  out, and evidence that it worked.**
98
 
99
+ ### Re-confirmed end to end on a headless Linux box
100
+
101
+ The whole path above was re-run from scratch on a headless Debian container (gVisor
102
+ sandbox, 20 vCPU, no GPU, no physical display, no network at run time) behind a virtual
103
+ X server. First the suite, headless:
104
+
105
+ ```bash
106
+ python -m pip install -e ".[desktop,ocr,dev]"
107
+ pytest tests -q
108
+ # 123 passed, 2 skipped in 18.6s
109
+ ```
110
+
111
+ The two skips are the live-desktop tests. Give them a display and they run for real:
112
+
113
+ ```bash
114
+ Xvfb :99 -screen 0 1280x800x24 &
115
+ export DISPLAY=:99
116
+ TINY_COMPUTER_AGENT_LIVE_TEST=1 python -m pytest tests/test_live_desktop.py -q
117
+ # tests/test_live_desktop.py::test_ocr_finds_a_real_button_at_the_right_pixels PASSED
118
+ # tests/test_live_desktop.py::test_a_real_click_lands_on_the_button PASSED
119
+ # 2 passed
120
+ ```
121
+
122
+ Then the same desktop, driven through the CLI with the mouse actually live:
123
+
124
+ ```bash
125
+ export TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT=1
126
+ python -m tiny_computer_agent --live --real-input --verify-text Saved \
127
+ "click 'Save' to save the document"
128
+ ```
129
+
130
+ ```
131
+ goal: click 'Save' to save the document
132
+ mode: REAL INPUT (mouse and keyboard live) | screen: real desktop (monitor 1) | brain: planner | kill switch: create /tmp/tiny-computer-agent.STOP to stop
133
+ index: auto - OCR when it finds text, grid otherwise (rapidocr-onnxruntime ready (CPU, offline))
134
+
135
+ step 0 | click label='Save' reason="matched 'Save' in the element index" | ok: clicked (194, 296)
136
+
137
+ result: success - verified after 1 action(s)
138
+ ```
139
+
140
+ The window was drawn at `+120+120` with its Save button at `x=20, y=155, 110x40`, so the
141
+ button centre is `(195, 295)`. The click at `(194, 296)` is *on the button*, not near it —
142
+ and the app wrote `Save` back to its own marker file. Pixels in, a real click out, and the
143
+ app's own side effect as the proof.
144
+
145
+ `--doctor` on that box, with the virtual display attached:
146
+
147
+ ```
148
+ tiny-computer-agent preflight (Linux x86_64, python 3.13.11)
149
+
150
+ [ok ] display server: DISPLAY=:99
151
+ [ok ] pillow: installed
152
+ [ok ] screen capture: mss (fast, multi-monitor)
153
+ [ok ] input backend (pyautogui): installed
154
+ [ok ] element detector (auto): OCR when it finds text, grid otherwise (rapidocr-onnxruntime ready (CPU, offline))
155
+ [ok ] model runtime: not needed: the planner is rule-based (no weights, no download)
156
+ [ok ] execution mode: dry run (nothing is clicked)
157
+ [ok ] kill switch: create /tmp/tiny-computer-agent.STOP to abort the next action
158
+
159
+ READY: the harness can drive this desktop.
160
+ ```
161
+
162
+
163
  ### On a replayed screenshot
164
 
165
  ```
 
175
  step 0 | click label='Save' reason="the reply names 'Save'" -> pixel (295, 284)
176
  ```
177
 
178
+ ### Which brain drove which run
179
+
180
+ The harness and the brain are separable, and it matters which one a result came from.
181
+ None of the runs below used a cloud API; the two model brains run on CPU:
182
+
183
+ | Run | Brain | Weights | Outcome |
184
+ |---|---|---|---|
185
+ | Live Linux desktop, real click | `PlannerBrain` (rule-based) | none | **success, verified** |
186
+ | `/tmp/saveapp.py` window live | `PlannerBrain` | none | clicked `Save` at `(194, 296)` |
187
+ | `--image example` | `PlannerBrain` | none | picked `Save`, pixel `(295, 284)` |
188
+ | `--image example` | `VLMBrain` (SmolVLM-256M-Instruct) | 256M, CPU | picked `Save` |
189
+ | Live Linux desktop | `VLMBrain` (SmolVLM-256M-Instruct) | 256M, CPU | echoed the prompt; **refused** by echo detection |
190
+ | Live Linux desktop | `TextPlannerBrain` (Qwen2.5-0.5B) | 0.5B, CPU | clicked Cancel, then garbled JSON; budget exhausted |
191
+
192
+ The rule-based planner is what carries the verified end-to-end runs. The tiny model is the
193
+ open problem: it is reliable on a clean screenshot and unreliable on a busy live screen.
194
+
195
  ### What did *not* work, and what caught it
196
 
197
  - **The 256M VLM echoes the prompt on a busy live screen.** Asked to click Save on the
 
206
  `warning: no text was found on screen, so the index is 96 numbered grid cells and the
207
  agent is choosing blind` — instead of silently guessing.
208
 
209
+ **125 tests pass**, including real OCR over a real image and (opt-in) live-desktop tests
210
  that click a real button.
211
 
212
+ ## The test suite, in full
213
+
214
+ `pytest tests` collects **125 tests**. 123 run headless — no display, no network, no
215
+ weights. The last 2 are the opt-in live-desktop tests, skipped unless
216
+ `TINY_COMPUTER_AGENT_LIVE_TEST=1`.
217
+
218
+ | File | Tests | What it proves |
219
+ |---|---:|---|
220
+ | `tests/test_actions.py` | 13 | the closed vocabulary — every action is validated before anything executes |
221
+ | `tests/test_act.py` | 8 | `DryRunExecutor` is inert; the real executor refuses until explicitly armed |
222
+ | `tests/test_observe.py` | 8 | element/observation geometry, `FakeScreen`, `ImageScreen`, the capture fallback |
223
+ | `tests/test_perception.py` | 13 | OCR labels a real image with real coordinates; the grid fallback; `AutoDetector` |
224
+ | `tests/test_brain.py` | 18 | the numbered `DecisionMenu`, lenient parsing, echo detection, `MockBrain`, `PlannerBrain` |
225
+ | `tests/test_loop.py` | 14 | `AgentLoop` / `Episode` / `Step`, the no-progress guard, the step budget |
226
+ | `tests/test_verify.py` | 7 | success is only claimed when a verifier can actually see it |
227
+ | `tests/test_safety.py` | 11 | deny lists, the kill switch, the blind-grid-click guard |
228
+ | `tests/test_grid_fallback.py` | 11 | the fallback index announces itself instead of guessing |
229
+ | `tests/test_desktop.py` | 10 | display detection, the preflight checks, `doctor` |
230
+ | `tests/test_cli.py` | 10 | the command line: `--doctor`, `--image`, `--live`, argument handling |
231
+ | `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 |
232
+
233
+ ```bash
234
+ pytest tests -q # 123 passed, 2 skipped
235
+ TINY_COMPUTER_AGENT_LIVE_TEST=1 pytest tests -q # 125 passed, on a desktop
236
+ ```
237
+
238
  ## Install
239
 
240
  ```bash
241
  pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
242
  pip install -e ".[desktop,ocr]" # + real capture and real input
243
  pip install -e ".[vision,ocr]" # + the tiny model brains
244
+ pytest # 125 tests, no display or network needed
245
  ```
246
 
247
  ## Run it on your machine
 
281
  was actually drawn at, clicks it for real, and checks the app reacted. It is opt-in
282
  because it moves your mouse — run it on a throwaway desktop.
283
 
284
+ ### Testing on Windows - three ways, and which to use
285
+
286
+ Nothing in the harness is Linux-specific: `pyautogui` uses `SendInput` on Windows and
287
+ `GrabScreen` already falls back to Pillow's `ImageGrab` there. Two things can only fail on
288
+ a real Windows desktop, which is why these runs exist: **DPI scaling** (that is what
289
+ `--scale` is for) and the native input backend. At 125% or 150% display scaling, expect the
290
+ coordinate assertion in `test_live_desktop.py` to be the first thing that fails - that is
291
+ the signal, not a nuisance.
292
+
293
+ | Method | Real Windows desktop? | Cost | Status |
294
+ |---|---|---|---|
295
+ | **GitHub Actions `windows-latest`** | yes, an interactive session | free for public repos, ~5 min | workflow committed: `.github/workflows/live-desktop.yml` |
296
+ | **QEMU/TCG VM on a Linux host** | yes, but fully emulated | hours of wall-clock | possible; measured below |
297
+ | **A cloud Windows dev box over RDP** | yes, native | paid by the hour | untested here |
298
 
299
+ **1. GitHub Actions - the recommended path.** `.github/workflows/live-desktop.yml` runs
300
+ the unit suite and the live desktop test on `windows-latest`, the same live test under
301
+ Xvfb on Linux, and a `--doctor` preflight on macOS. The Windows job is
302
+ `continue-on-error` on purpose: the first run tells us whether a hosted runner's session
303
+ accepts injected input. Trigger it from the Actions tab. The repo needs a GitHub mirror -
304
+ Hugging Face does not run GitHub Actions.
305
 
306
+ **2. A local QEMU VM without KVM.** Measured on the headless container above:
307
+
308
+ ```
309
+ /dev/kvm absent -> no hardware virtualization
310
+ qemu-system-x86_64 -accel help -> tcg, kvm
311
+ qemu-system-x86_64 -accel kvm -> Could not access KVM kernel module: No such file or directory
312
+ qemu-system-x86_64 -accel tcg -> boots (reached "No bootable device.")
313
+ ```
314
+
315
+ So a Windows guest is *possible* with no KVM at all, but entirely under software
316
+ emulation. TCG runs roughly 10-50x slower than native, which turns a 20-minute Windows
317
+ install into many hours - and the harness's OCR would then run on an emulated CPU inside
318
+ the guest. Use this only when no CI runner and no cloud box is available:
319
+
320
+ ```bash
321
+ qemu-img create -f qcow2 win.qcow2 64G
322
+ qemu-system-x86_64 -accel tcg -m 8G -smp 4 \
323
+ -drive file=win.qcow2,format=qcow2,if=virtio \
324
+ -cdrom Win11_Eval_x64.iso -boot d \
325
+ -device virtio-vga -vnc :1 # then RDP/VNC in, pip install the harness inside
326
+ ```
327
+
328
+ **3. A cloud Windows box.** Any Windows VM reachable over RDP gives a native desktop, a
329
+ real `SendInput` path and real DPI scaling. Untested here; it is the closest thing to
330
+ "a user's actual laptop".
331
 
332
  ## How it works
333
 
 
375
 
376
  **Works:** real capture, a real OCR index with coordinates that land on the right pixel,
377
  a real click that an app reacts to, verified success, the closed vocabulary, the policy
378
+ gates, the no-progress guard, echo detection, the doctor, the CLI, 125 tests — and a
379
  256M VLM that reads a button label from pixels on a clean screenshot.
380
 
381
  **Does not work yet:** reliability on a *live* screen. The 256M VLM echoed the prompt
 
389
  - [x] A real click on a real window, with a verified outcome
390
  - [x] Blind-grid-click guard, echo detection, no-progress guard
391
  - [ ] Make the VLM reliable on a live screen (it echoes the prompt instead of choosing)
392
+ - [ ] Windows run of the live test - workflow committed; needs a GitHub mirror with Actions on
393
  - [ ] Accessibility-tree detector (AT-SPI on Linux, UI Automation on Windows)
394
  - [ ] Score episodes: replay a trace and count how often the choice was right
395
  - [ ] marimo console: live screen preview, action log, pause / resume / step
 
400
  '''
401
 
402
  mo.md(readme_md)
 
403
  return (readme_md,)
404
 
405
 
 
484
 
485
  mo.md(f"packaging: {len(pyproject_toml) + len(license_txt) + len(gitignore_txt)} chars, "
486
  f"v0.2.0 with ocr/desktop/vision extras")
 
487
  return gitignore_txt, license_txt, pyproject_toml
488
 
489
 
 
498
  )
499
 
500
  publish_button
 
501
  return REPO_ID, publish_button
502
 
503
 
 
566
  _status = mo.md(f"Publish failed: `{type(_e).__name__}: {_e}`").callout(kind="danger")
567
 
568
  _status
 
569
  return
570
 
571
 
 
606
  mo.md(f"`{perception_episode.render().splitlines()[1]}`"),
607
  ]
608
  )
 
609
  return
610
 
611