hkblockchaintechnology commited on
Commit
f321127
Β·
verified Β·
1 Parent(s): 1a398be

Publish tiny_computer_agent_app.py from marimo

Browse files
Files changed (1) hide show
  1. tiny_computer_agent_app.py +121 -110
tiny_computer_agent_app.py CHANGED
@@ -41,12 +41,13 @@ def readme():
41
  # πŸ–±οΈ Tiny Computer Agent
42
 
43
  **An agentic harness that drives a real computer with a tiny, fully local model.**
44
- No cloud API, no API keys, no telemetry β€” and a core that imports nothing outside
45
- the Python standard library.
46
 
47
- > **Status: early alpha.** The loop runs end to end today, with a scripted screen,
48
- > dry-run execution, a CLI and 56 offline tests. The vision backend is a
49
- > documented seam, not a feature β€” see [what works](#-what-works-today).
 
50
 
51
  ## The idea, in one loop
52
 
@@ -61,143 +62,153 @@ def readme():
61
  repeat until done, or the step budget runs out
62
  ```
63
 
64
- 1. **Observe** β€” grab the screen, downscale it, optionally attach OCR text or the
65
- accessibility tree so the model never has to guess where things are.
66
- 2. **Think** β€” a tiny (≀ 3B, quantised) vision-language model emits **exactly one
67
- action** from a closed vocabulary.
68
- 3. **Act** β€” the harness executes it with OS-level input (mouse, keyboard, scroll).
69
- 4. **Verify** β€” the next frame shows whether it worked. Repeat, bounded by a step
70
- budget and a kill switch.
 
71
 
72
- ## What works today
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  ```
75
- $ python -m tiny_computer_agent
76
- goal: click 'Save' to save the document
77
- mode: dry-run | screen: scripted demo | kill switch: create /tmp/tiny-computer-agent.STOP to stop
78
 
79
- step 0 | wait seconds=0.5 reason="'Save' is not on screen yet" | ok: (dry-run) would wait 0.5s
80
- step 1 | click label='Save' reason="matched 'Save' in the element index" | ok: (dry-run) would click -> pixel (360, 132)
81
- step 2 | done reason="already clicked 'Save'; goal assumed met" | ok: goal reported met
82
 
83
- result: success - brain reported done: already clicked 'Save'; goal assumed met
 
 
 
 
 
 
 
84
  ```
85
 
86
- Shipped and tested:
87
-
88
- - **the loop** β€” `AgentLoop` runs observe β†’ think β†’ act β†’ verify, **re-reads the
89
- screen after every action**, and stops on done, on a step budget, on the kill
90
- switch, on a policy refusal, on a failed action, or on a brain error. Every step
91
- is recorded in an `Episode` you can render or turn into rows.
92
- - **a closed action vocabulary** β€” `click` `type` `key` `scroll` `wait` `done`,
93
- validated *before* anything executes. A model that invents a verb gets a recorded
94
- error, not a click.
95
- - **three brains** β€” `MockBrain` replays a script; `PlannerBrain` is rule-based and
96
- genuinely clicks the element matching the goal (it waits for a dialog instead of
97
- flailing, and refuses to click the same label twice); `VLMBrain` is the seam β€”
98
- prompt assembly and reply validation are done, `_generate()` is yours.
99
- - **two executors** β€” `DryRunExecutor` is the default: it logs the plan and resolves
100
- every label to a pixel. `PyAutoGuiExecutor` does real input and **refuses to click
101
- when a label matches no element**, because a tiny model guessing pixels is how you
102
- click "Delete account". Dry-run fails exactly where live input refuses, so a
103
- rehearsal never gives false confidence.
104
- - **policy gates** β€” deny-listed text (`sudo`, `rm -rf`, "password", "api_key"),
105
- deny-listed widgets (terminal, keychain, password manager, wallet, bank), four
106
- deny-listed key chords, a kill-switch file checked before every action, and a step
107
- budget.
108
- - **real input needs two deliberate flags** β€” `--real-input` *and*
109
- `TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT=1`. Default is a dry run.
110
- - **56 tests** needing no display, no model and no network.
111
-
112
- What does **not** work yet: seeing a real screen *usefully*. `GrabScreen` captures
113
- pixels through `mss`, but nothing yet turns pixels into an element index (OCR or
114
- AT-SPI), so a live screen currently yields zero elements β€” which is exactly what the
115
- label path depends on. And no model is wired up.
116
-
117
- ## Install and run
118
 
119
  ```bash
120
- pip install -e . # core: standard library only, no deps
121
- pytest # 56 offline tests
122
- python -m tiny_computer_agent # scripted demo, dry run
123
- python -m tiny_computer_agent --live # your real screen, still dry run
124
  ```
125
 
126
- ## Put it on your own task
127
 
128
- ```python
129
- from tiny_computer_agent import AgentLoop, Element, FakeScreen, MockBrain
 
 
 
 
130
 
131
- screen = FakeScreen([(Element("Save", x=320, y=120, width=80, height=24),)])
132
- brain = MockBrain([{"kind": "click", "label": "Save"},
133
- {"kind": "done", "reason": "saved"}])
134
 
135
- episode = AgentLoop(screen=screen, brain=brain).run("save the document")
136
- print(episode.render())
137
- episode.to_rows() # rows for a dataframe or notebook table
138
  ```
139
 
140
- Swap `FakeScreen` for `GrabScreen()` and `MockBrain` for your `VLMBrain` subclass and
141
- that is the whole distance between the tests and a live agent.
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
- ## Why "tiny" is the whole point
144
 
145
- The harness does the heavy lifting, so the model does not have to be clever:
146
 
147
  | Job | Who does it |
148
  |---|---|
149
- | Finding *where* a button is | OCR / accessibility tree / element index β€” not the model |
150
- | Choosing *what* to do | a tiny VLM over a **closed action menu** (click, type, key, scroll, wait, done) |
151
- | Knowing whether it worked | the next frame, plus harness-side checks |
152
  | Recovering from a bad step | the retry policy in `loop.py`, not the weights |
153
 
154
- That keeps the agent offline, fast to start, and small enough that a laptop GPU
155
- (or plain CPU) can run it on the same box it is controlling.
156
 
157
- ## Candidate backends
158
-
159
- Pluggable behind one interface; nothing here needs a network call at run time.
160
-
161
- - **Moondream 2** / **SmolVLM-256M / 500M** β€” a few hundred MB, safe default
162
- - **Qwen2-VL-2B-Instruct**, **Florence-2-base** β€” stronger grounding, still tiny
163
- - **OCR + text-only planner** β€” no VLM at all; the cheapest credible baseline
164
- - **mock brain** β€” deterministic scripted actions, used to test the harness itself
165
 
166
- ## Layout
 
 
 
 
 
 
 
 
167
 
168
- ```
169
- tiny-computer-agent/
170
- β”œβ”€β”€ README.md
171
- β”œβ”€β”€ LICENSE
172
- β”œβ”€β”€ pyproject.toml
173
- β”œβ”€β”€ tiny_computer_agent_app.py # marimo console: run an episode, publish this card
174
- β”œβ”€β”€ tiny_computer_agent/
175
- β”‚ β”œβ”€β”€ actions.py # the closed action vocabulary
176
- β”‚ β”œβ”€β”€ observe.py # Observation + element index; FakeScreen, GrabScreen
177
- β”‚ β”œβ”€β”€ brain.py # MockBrain, PlannerBrain, VLMBrain (the seam)
178
- β”‚ β”œβ”€β”€ act.py # DryRunExecutor (default) + PyAutoGuiExecutor
179
- β”‚ β”œβ”€β”€ safety.py # Policy: deny lists, kill switch, step budget
180
- β”‚ β”œβ”€β”€ loop.py # AgentLoop, Episode, Step
181
- β”‚ └── __main__.py # CLI
182
- └── tests/ # 56 tests: no display, no model, no network
183
- ```
184
 
185
- ## Safety, because this thing has your mouse
 
 
186
 
187
- - Run it in a **VM, container, or throwaway desktop account** β€” not your real session.
188
- - Dry run is the default; read the whole plan before letting it near your pointer.
189
- - Deny-list for shells, terminals, payment flows and password managers.
190
- - A step budget plus a kill-switch file checked before every action.
191
- - **Local-first, for real:** screen pixels and keystrokes never leave the machine.
192
 
193
  ## Roadmap
194
 
195
- - [x] `loop.py` + `act.py` with the mock brain β€” the harness is testable before any model download
196
- - [x] policy gates: deny lists, kill switch, step budget, dry run by default
197
- - [ ] OCR / AT-SPI element index, so a live screen has clickable labels
198
- - [ ] tiny-VLM grounding backend, behind `VLMBrain._generate()`
 
199
  - [ ] marimo console: live screen preview, action log, pause / resume / step
200
- - [ ] persist episodes, so failed runs can be replayed and scored
201
 
202
  ## License
203
 
 
41
  # πŸ–±οΈ Tiny Computer Agent
42
 
43
  **An agentic harness that drives a real computer with a tiny, fully local model.**
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
 
 
62
  repeat until done, or the step budget runs out
63
  ```
64
 
65
+ 1. **Observe** β€” capture the screen, then build an **element index**: OCR (and,
66
+ optionally, the OS accessibility tree) locates text and widgets and gives each one a
67
+ label and a real screen coordinate.
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
 
127
  ```bash
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
135
 
136
+ ```bash
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 |
155
+ |---|---|
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
 
213
  ## License
214