hkblockchaintechnology commited on
Commit
29b362f
Β·
verified Β·
1 Parent(s): 618d21f

Publish tiny_computer_agent_app.py from marimo

Browse files
Files changed (1) hide show
  1. tiny_computer_agent_app.py +93 -65
tiny_computer_agent_app.py CHANGED
@@ -44,10 +44,10 @@ def readme():
44
  No cloud API, no API keys, no telemetry β€” and a core that imports nothing outside the
45
  Python standard library.
46
 
47
- > **Status 0.2.0: it runs, on pixels.** The screen is captured for real, OCR turns it
48
- > into a short labelled menu, a **256M** vision model picks from that menu, and the
49
- > harness resolves the choice to a real pixel and (behind two locks) clicks it.
50
- > What is *not* solved is model accuracy β€” see [measured results](#measured-results).
51
 
52
  ## The idea, in one loop
53
 
@@ -68,59 +68,65 @@ def readme():
68
  2. **Think** β€” the model is shown a *short numbered menu* built from that index and
69
  answers with one number (plus a verb). It never emits coordinates.
70
  3. **Act** β€” the harness maps the chosen label back to pixels and executes it.
71
- 4. **Verify** β€” re-read the screen, and stop if the same action repeats against an
72
- unchanged screen, because that is a stuck agent, not a persistent one.
73
 
74
  ## Measured results
75
 
76
- Everything below was run from this repo, **CPU only, dry-run**, against the synthetic
77
- screenshot (`--image example`), because the build machine has no desktop. Nothing here
78
- is aspirational.
79
 
80
- **A rule-based planner, end to end:** OCR read five labels off the screenshot, matched
81
- the goal, and resolved it to a pixel.
 
 
 
82
 
83
  ```
84
- $ python -m tiny_computer_agent --image example --brain planner
 
85
  index: auto - OCR when it finds text, grid otherwise (rapidocr-onnxruntime ready (CPU, offline))
86
- elements detected: 5
87
 
88
- step 0 | click label='Save' reason="matched 'Save' in the element index" | ok: (dry-run) would click -> pixel (295, 284)
89
- step 1 | done reason="already clicked 'Save'; goal assumed met" | ok: goal reported met
 
90
  ```
91
 
92
- **A 256M vision model β€” SmolVLM-256M-Instruct, CPU β€” read the button off the pixels:**
 
 
 
 
93
 
94
  ```
95
- $ python -m tiny_computer_agent --image example --brain vlm
96
  elements detected: 5
97
-
98
- step 0 | click label='Save' reason="the reply names 'Save'" | ok: (dry-run) would click -> pixel (295, 284)
99
- step 1 | click label='Save' ...
100
- step 2 | ... ERROR no progress: repeating click label='Save' against an unchanged screen
101
  ```
102
 
103
- It named the correct target from pixels alone. The episode is recorded as a *failure*
104
- only because the replayed screenshot never changes, so the no-progress guard stops a
105
- loop that a real desktop would have moved past.
106
-
107
- **A 0.5B text planner over the OCR index β€” Qwen2.5-0.5B-Instruct β€” is not reliable:**
108
 
109
  ```
110
- $ python -m tiny_computer_agent --image example --brain text-planner
111
- step 0 | click label='Cancel' reason='menu index 5'
112
- step 1 | click label='Files' reason="recovered 'Files' from the model's reply: {\"kind\": \"click\", \"index\": 0, ...}"
113
- step 2 | click label='Save' reason='menu index 4'
114
- result: failure - step budget of 3 exhausted
115
  ```
116
 
117
- It clicked Cancel before Save, then wandered. The harness held up regardless: every
118
- action was validated against the closed vocabulary, a garbled JSON reply was recovered
119
- into a usable action, and the budget stopped it. **A tiny VLM beats a tiny text model
120
- here** β€” which is why the vision path is the recommended one.
 
 
 
 
 
 
 
 
 
121
 
122
- **116 tests pass**, including real OCR over a real image asserting the `Save` button's
123
- coordinates, and a screenshot replay that runs the whole pipeline with no display.
124
 
125
  ## Install
126
 
@@ -128,7 +134,7 @@ def readme():
128
  pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
129
  pip install -e ".[desktop,ocr]" # + real capture and real input
130
  pip install -e ".[vision,ocr]" # + the tiny model brains
131
- pytest # 116 tests, no display or network needed
132
  ```
133
 
134
  ## Run it on your machine
@@ -137,18 +143,37 @@ def readme():
137
  python -m tiny_computer_agent --doctor # what can this machine do?
138
  python -m tiny_computer_agent --image example # replay a screenshot, dry run
139
  python -m tiny_computer_agent --live # your real screen, dry run
140
- python -m tiny_computer_agent --live --real-input "click 'OK'" # needs the env var too
 
141
  ```
142
 
143
  `--doctor` is the first thing to run on a new machine: it checks the display session,
144
- capture backend, pyautogui, the OCR engine, the model runtime, and the kill switch, and
145
- prints the exact fix for anything missing. Real input needs **two** deliberate acts:
146
 
147
  ```bash
148
  export TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT=1
149
- python -m tiny_computer_agent --live --real-input "click 'Save'"
150
  ```
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  ## How it works
153
 
154
  | Module | Job |
@@ -156,57 +181,60 @@ def readme():
156
  | `actions.py` | The closed vocabulary β€” `click` `type` `key` `scroll` `wait` `done`, validated before anything executes |
157
  | `observe.py` | `Element`/`Observation`, `FakeScreen`, `ImageScreen`, `GrabScreen` (mss β†’ Pillow fallback) |
158
  | `perception.py` | Detectors: `OcrDetector` (RapidOCR), `GridDetector`, `AutoDetector`, `IndexedScreen` |
159
- | `brain.py` | `DecisionMenu` (the numbered menu), `ModelBrain`, lenient `parse_decision`, `MockBrain`, `PlannerBrain` |
160
  | `models.py` | `VLMBrain` (SmolVLM-256M) and `TextPlannerBrain` (Qwen2.5-0.5B), lazily loaded |
161
  | `act.py` | `DryRunExecutor` (the default) and `PyAutoGuiExecutor` |
162
  | `safety.py` | `Policy`: deny lists, kill switch, step budget, the blind-grid-click guard |
163
- | `loop.py` | `AgentLoop`, `Episode`, `Step`, and the no-progress guard |
164
  | `desktop.py` | Preflight checks, `doctor`, and `build_agent` in one call |
165
  | `demo.py` | The synthetic desktop used by tests and `--image example` |
166
 
167
  ## Why a tiny model can do this at all
168
 
169
- The harness does the heavy lifting, so the model never has to be clever:
170
-
171
  | Job | Who does it |
172
  |---|---|
173
  | Finding *where* a button is | OCR / accessibility tree β€” **not** the model |
174
  | Choosing *what* to do | the model, but only by picking a number from a short menu |
175
- | Knowing whether it worked | the next frame, plus the no-progress guard |
176
  | Recovering from a bad step | the retry policy in `loop.py`, not the weights |
177
 
178
- The menu is the trick: a 256M model cannot emit `(295, 284)` reliably, but it can pick
179
- "4" out of five options β€” and the harness owns the coordinate lookup.
 
180
 
181
  ## Safety, because this thing has your mouse
182
 
183
  - Run it in a **VM, container, or throwaway desktop account**, not your real session.
184
  - **Dry run is the default**, so you can read a whole episode before anything clicks.
185
- - Deny lists for typing (`sudo`, `rm -rf`, "password"), for widgets (terminal, keychain,
186
- wallet, bank) and for key chords.
187
  - A **kill-switch file** checked before every action, plus a hard step budget.
188
- - **Blind grid clicks are refused with real input** unless you pass `--allow-grid`. When
189
- OCR finds nothing on screen, the fallback index is a numbered grid β€” coordinates with
190
- no meaning β€” and a model choosing one is guessing about your desktop.
 
 
191
  - **Local-first, for real:** pixels and keystrokes never leave the machine.
192
 
193
  ## What works, and what does not
194
 
195
- **Works:** real capture, a real OCR index with real coordinates, a 256M VLM choosing the
196
- right labelled target, the closed vocabulary, the policy gates, the no-progress guard,
197
- the doctor, the CLI, 116 tests.
 
198
 
199
- **Does not work yet:** accuracy. A 0.5B text planner picks wrong actions; a 256M VLM can
200
- answer "done" before doing anything, and the harness currently trusts that; the
201
- accessibility-tree detector (AT-SPI / UI Automation) is not implemented; and nothing here
202
- has been run against a live multi-window desktop, only against a synthetic screenshot.
203
 
204
  ## Roadmap
205
 
206
  - [x] Capture, OCR index, closed vocabulary, dry-run default, policy gates, doctor
207
- - [x] A 256M VLM that picks a labelled target from pixels
208
- - [ ] Make the harness distrust a `done` that arrives before any action has been taken
209
- - [ ] Accessibility-tree detector (AT-SPI on Linux, UI Automation on Windows) for exact widgets
 
210
  - [ ] Score episodes: replay a trace and count how often the choice was right
211
  - [ ] marimo console: live screen preview, action log, pause / resume / step
212
 
@@ -229,7 +257,7 @@ def repo_files():
229
 
230
  [project]
231
  name = "tiny-computer-agent"
232
- version = "0.2.0"
233
  description = "An agentic harness that drives a real computer with a tiny, fully local model - no cloud API."
234
  readme = "README.md"
235
  requires-python = ">=3.10"
 
44
  No cloud API, no API keys, no telemetry β€” and a core that imports nothing outside the
45
  Python standard library.
46
 
47
+ > **Status 0.3.0: it drives a real desktop.** Screen capture, an OCR element index with
48
+ > real coordinates, a real mouse click, and a *verified* outcome β€” all through the
49
+ > harness, in one episode. What is not solved is model reliability: see
50
+ > [measured results](#measured-results).
51
 
52
  ## The idea, in one loop
53
 
 
68
  2. **Think** β€” the model is shown a *short numbered menu* built from that index and
69
  answers with one number (plus a verb). It never emits coordinates.
70
  3. **Act** β€” the harness maps the chosen label back to pixels and executes it.
71
+ 4. **Verify** β€” re-read the screen; stop if the same action repeats against an unchanged
72
+ screen, and only call the episode a success if a verifier can see it worked.
73
 
74
  ## Measured results
75
 
76
+ Everything below was run from this repo on CPU, dry-run unless stated. Nothing is
77
+ aspirational.
 
78
 
79
+ ### On a live desktop, with a real click
80
+
81
+ A real Tk window (a Save dialog with Save/Cancel buttons) on a **real X server**
82
+ (`Xvfb :99`), driven with real mouse input. The button sits at window `+120+120`,
83
+ `x=20, y=155`, `110x40`, so its centre is `(195, 295)`:
84
 
85
  ```
86
+ $ python -m tiny_computer_agent --live --real-input --verify-text Saved "click 'Save' to save the document"
87
+ mode: REAL INPUT (mouse and keyboard live) | screen: real desktop (monitor 1) | brain: planner
88
  index: auto - OCR when it finds text, grid otherwise (rapidocr-onnxruntime ready (CPU, offline))
 
89
 
90
+ step 0 | click label='Save' reason="matched 'Save' in the element index" | ok: clicked (195, 296)
91
+
92
+ result: success - verified after 1 action(s)
93
  ```
94
 
95
+ The click landed on the button, the app changed its own window to "Saved", and the
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
+ ### On a replayed screenshot
100
 
101
  ```
102
+ $ python -m tiny_computer_agent --image example --brain planner
103
  elements detected: 5
104
+ step 0 | click label='Save' ... -> pixel (295, 284) # OCR found the button
 
 
 
105
  ```
106
 
107
+ **SmolVLM-256M-Instruct** (256M parameters, CPU) read the button off the pixels and
108
+ named the right target:
 
 
 
109
 
110
  ```
111
+ step 0 | click label='Save' reason="the reply names 'Save'" -> pixel (295, 284)
 
 
 
 
112
  ```
113
 
114
+ ### What did *not* work, and what caught it
115
+
116
+ - **The 256M VLM echoes the prompt on a busy live screen.** Asked to click Save on the
117
+ live Tk window it replied with the prompt text back. The harness's echo detection
118
+ refused it (`BrainError: the reply mentions 4 menu entries, so it looks like an echo of
119
+ the prompt`) and stopped β€” **no blind click**. On the cleaner synthetic screenshot the
120
+ same model chose correctly, so this is a prompt/robustness problem, not a wiring one.
121
+ - **Qwen2.5-0.5B text planner over the OCR index is not reliable**: it clicked Cancel
122
+ before Save, then recovered a garbled JSON reply, then found Save β€” budget exhausted.
123
+ - **The grid fallback announced itself.** After the dialog closed, OCR found no text, the
124
+ index degraded to 96 numbered cells, and the run printed
125
+ `warning: no text was found on screen, so the index is 96 numbered grid cells and the
126
+ agent is choosing blind` β€” instead of silently guessing.
127
 
128
+ **123 tests pass**, including real OCR over a real image and (opt-in) live-desktop tests
129
+ that click a real button.
130
 
131
  ## Install
132
 
 
134
  pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
135
  pip install -e ".[desktop,ocr]" # + real capture and real input
136
  pip install -e ".[vision,ocr]" # + the tiny model brains
137
+ pytest # 123 tests, no display or network needed
138
  ```
139
 
140
  ## Run it on your machine
 
143
  python -m tiny_computer_agent --doctor # what can this machine do?
144
  python -m tiny_computer_agent --image example # replay a screenshot, dry run
145
  python -m tiny_computer_agent --live # your real screen, dry run
146
+ python -m tiny_computer_agent --live --real-input "click 'OK'"
147
+ python -m tiny_computer_agent --live --real-input --verify-text "Saved" "click 'Save'"
148
  ```
149
 
150
  `--doctor` is the first thing to run on a new machine: it checks the display session,
151
+ capture backend, pyautogui, the OCR engine, the model runtime and the kill switch, and
152
+ prints the fix for anything missing. Real input needs **two** deliberate acts:
153
 
154
  ```bash
155
  export TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT=1
 
156
  ```
157
 
158
+ **Pass `--verify-text`.** Without it, a successful action that removes its own target
159
+ (clicking Save closes the dialog) is indistinguishable from a failure, and the episode is
160
+ recorded as one. With it, the harness only reports success when it can see the text it
161
+ was given.
162
+
163
+ ### No desktop? Use a virtual one
164
+
165
+ Xvfb is enough for everything except a real model's hallucinations:
166
+
167
+ ```bash
168
+ Xvfb :99 -screen 0 1280x800x24 &
169
+ export DISPLAY=:99
170
+ TINY_COMPUTER_AGENT_LIVE_TEST=1 python -m pytest tests/test_live_desktop.py
171
+ ```
172
+
173
+ That test starts its own Tk window, asserts OCR finds the button at the coordinates it
174
+ was actually drawn at, clicks it for real, and checks the app reacted. It is opt-in
175
+ because it moves your mouse β€” run it on a throwaway desktop.
176
+
177
  ## How it works
178
 
179
  | Module | Job |
 
181
  | `actions.py` | The closed vocabulary β€” `click` `type` `key` `scroll` `wait` `done`, validated before anything executes |
182
  | `observe.py` | `Element`/`Observation`, `FakeScreen`, `ImageScreen`, `GrabScreen` (mss β†’ Pillow fallback) |
183
  | `perception.py` | Detectors: `OcrDetector` (RapidOCR), `GridDetector`, `AutoDetector`, `IndexedScreen` |
184
+ | `brain.py` | `DecisionMenu` (the numbered menu), `ModelBrain`, lenient `parse_decision`, echo detection, `MockBrain`, `PlannerBrain` |
185
  | `models.py` | `VLMBrain` (SmolVLM-256M) and `TextPlannerBrain` (Qwen2.5-0.5B), lazily loaded |
186
  | `act.py` | `DryRunExecutor` (the default) and `PyAutoGuiExecutor` |
187
  | `safety.py` | `Policy`: deny lists, kill switch, step budget, the blind-grid-click guard |
188
+ | `loop.py` | `AgentLoop`, `Episode`, `Step`, the no-progress guard and the verifier |
189
  | `desktop.py` | Preflight checks, `doctor`, and `build_agent` in one call |
190
  | `demo.py` | The synthetic desktop used by tests and `--image example` |
191
 
192
  ## Why a tiny model can do this at all
193
 
 
 
194
  | Job | Who does it |
195
  |---|---|
196
  | Finding *where* a button is | OCR / accessibility tree β€” **not** the model |
197
  | Choosing *what* to do | the model, but only by picking a number from a short menu |
198
+ | Knowing whether it worked | the verifier and the no-progress guard, not the model's word |
199
  | Recovering from a bad step | the retry policy in `loop.py`, not the weights |
200
 
201
+ A 256M model cannot emit `(295, 284)` reliably, but it can pick "4" out of five options β€”
202
+ and the harness owns the coordinate lookup, the vocabulary check and the definition of
203
+ success.
204
 
205
  ## Safety, because this thing has your mouse
206
 
207
  - Run it in a **VM, container, or throwaway desktop account**, not your real session.
208
  - **Dry run is the default**, so you can read a whole episode before anything clicks.
209
+ - Deny lists for typing (`sudo`, `rm -rf`, "password"), widgets (terminal, keychain,
210
+ wallet, bank) and key chords.
211
  - A **kill-switch file** checked before every action, plus a hard step budget.
212
+ - **Blind grid clicks are refused with real input** unless you pass `--allow-grid`: when
213
+ OCR finds nothing, the fallback index is coordinates with no meaning, and a model
214
+ choosing one is guessing about your desktop.
215
+ - **A brain's "done" is a claim, not evidence**: pass `--verify-text` and the harness
216
+ decides success itself.
217
  - **Local-first, for real:** pixels and keystrokes never leave the machine.
218
 
219
  ## What works, and what does not
220
 
221
+ **Works:** real capture, a real OCR index with coordinates that land on the right pixel,
222
+ a real click that an app reacts to, verified success, the closed vocabulary, the policy
223
+ gates, the no-progress guard, echo detection, the doctor, the CLI, 123 tests β€” and a
224
+ 256M VLM that reads a button label from pixels on a clean screenshot.
225
 
226
+ **Does not work yet:** reliability on a *live* screen. The 256M VLM echoed the prompt
227
+ there; the 0.5B text planner picks wrong actions; the accessibility-tree detector is not
228
+ implemented, so labels come from OCR only; and the harness still trusts a model's `done`
229
+ when no verifier is given.
230
 
231
  ## Roadmap
232
 
233
  - [x] Capture, OCR index, closed vocabulary, dry-run default, policy gates, doctor
234
+ - [x] A real click on a real window, with a verified outcome
235
+ - [x] Blind-grid-click guard, echo detection, no-progress guard
236
+ - [ ] Make the VLM reliable on a live screen (it echoes the prompt instead of choosing)
237
+ - [ ] Accessibility-tree detector (AT-SPI on Linux, UI Automation on Windows)
238
  - [ ] Score episodes: replay a trace and count how often the choice was right
239
  - [ ] marimo console: live screen preview, action log, pause / resume / step
240
 
 
257
 
258
  [project]
259
  name = "tiny-computer-agent"
260
+ version = "0.3.0"
261
  description = "An agentic harness that drives a real computer with a tiny, fully local model - no cloud API."
262
  readme = "README.md"
263
  requires-python = ">=3.10"