hkblockchaintechnology commited on
Commit
2366140
·
verified ·
1 Parent(s): 4cc3d64

Publish tiny_computer_agent_app.py from marimo

Browse files
Files changed (1) hide show
  1. tiny_computer_agent_app.py +150 -18
tiny_computer_agent_app.py CHANGED
@@ -17,10 +17,27 @@ with app.setup:
17
  import marimo as mo
18
  from huggingface_hub import HfApi
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  @app.cell(hide_code=True)
22
  def readme():
23
- readme_md = '''
24
 
25
  ---
26
  title: Tiny Computer Agent
@@ -46,7 +63,9 @@ def readme():
46
 
47
  > **Status 0.3.0: it drives a real desktop — on Linux and on Windows.** Screen capture,
48
  > an OCR element index with real coordinates, a real mouse click, and a *verified*
49
- > outcome. The suite passes on a live Linux desktop and inside a real Windows Server
 
 
50
  > 2022 guest; what is not solved is model reliability — see
51
  > [measured results](#measured-results).
52
 
@@ -106,7 +125,7 @@ def readme():
106
  ```bash
107
  python -m pip install -e ".[desktop,ocr,dev]"
108
  pytest tests -q
109
- # 123 passed, 2 skipped in 18.6s
110
  ```
111
 
112
  The two skips are the live-desktop tests. Give them a display and they run for real:
@@ -176,6 +195,37 @@ def readme():
176
  step 0 | click label='Save' reason="the reply names 'Save'" -> pixel (295, 284)
177
  ```
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  ### Which brain drove which run
180
 
181
  The harness and the brain are separable, and it matters which one a result came from.
@@ -207,7 +257,7 @@ def readme():
207
  `warning: no text was found on screen, so the index is 96 numbered grid cells and the
208
  agent is choosing blind` — instead of silently guessing.
209
 
210
- **125 tests pass**, including real OCR over a real image and (opt-in) live-desktop tests
211
  that click a real button.
212
 
213
  ## Verified on real Windows
@@ -249,7 +299,7 @@ def readme():
249
 
250
  None of the three failures in the first row is a harness defect: two are
251
  `ModuleNotFoundError: numpy` and the third is the guard bug documented below. With the
252
- OCR engine installed, the same suite exits **0**.
253
 
254
  **The emulated-OCR caveat.** onnxruntime requires AVX2, and QEMU's software emulation of
255
  AVX2 is roughly two orders of magnitude slower than native. A suite that finishes in 17
@@ -275,7 +325,7 @@ def readme():
275
 
276
  ## The test suite, in full
277
 
278
- `pytest tests` collects **125 tests**. 123 run headless — no display, no network, no
279
  weights. The last 2 are the opt-in live-desktop tests, skipped unless
280
  `TINY_COMPUTER_AGENT_LIVE_TEST=1`.
281
 
@@ -292,11 +342,12 @@ def readme():
292
  | `tests/test_grid_fallback.py` | 11 | the fallback index announces itself instead of guessing |
293
  | `tests/test_desktop.py` | 10 | display detection, the preflight checks, `doctor` |
294
  | `tests/test_cli.py` | 10 | the command line: `--doctor`, `--image`, `--live`, argument handling |
 
295
  | `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 |
296
 
297
  ```bash
298
- pytest tests -q # 123 passed, 2 skipped
299
- TINY_COMPUTER_AGENT_LIVE_TEST=1 pytest tests -q # 125 passed, on a desktop
300
  ```
301
 
302
  ## Install
@@ -305,7 +356,7 @@ def readme():
305
  pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
306
  pip install -e ".[desktop,ocr]" # + real capture and real input
307
  pip install -e ".[vision,ocr]" # + the tiny model brains
308
- pytest # 125 tests, no display or network needed
309
  ```
310
 
311
  ## Run it on your machine
@@ -382,7 +433,7 @@ def readme():
382
 
383
  # 2. inside the guest, after autologon
384
  # vc_redist.x64.exe is required before onnxruntime will load
385
- curl.exe -s -o C: iny c.exe <vc_redist-url> && C: iny c.exe /install /quiet /norestart
386
  ```
387
 
388
  Serve files to the guest over QEMU's user-mode networking (the host is `10.0.2.2`), which
@@ -406,6 +457,7 @@ def readme():
406
  | `loop.py` | `AgentLoop`, `Episode`, `Step`, the no-progress guard and the verifier |
407
  | `desktop.py` | Preflight checks, `doctor`, and `build_agent` in one call |
408
  | `demo.py` | The synthetic desktop used by tests and `--image example` |
 
409
 
410
  ## Why a tiny model can do this at all
411
 
@@ -438,7 +490,7 @@ def readme():
438
 
439
  **Works:** real capture, a real OCR index with coordinates that land on the right pixel,
440
  a real click an app reacts to, verified success, the closed vocabulary, the policy gates,
441
- the no-progress guard, echo detection, the doctor, the CLI, 125 tests, a 256M VLM that
442
  reads a button label from pixels on a clean screenshot — and the same suite passing inside
443
  a real Windows Server 2022 guest with no KVM (109 passed, exit 0, with the OCR-inference
444
  tests excluded as impractically slow under emulation).
@@ -459,7 +511,7 @@ def readme():
459
  - [ ] Make the OCR guard in `test_cli.py` skip, rather than fail, when no engine is installed
460
  - [ ] Make the VLM reliable on a live screen (it echoes the prompt instead of choosing)
461
  - [ ] Accessibility-tree detector (AT-SPI on Linux, UI Automation on Windows)
462
- - [ ] Score episodes: replay a trace and count how often the choice was right
463
  - [ ] marimo console: live screen preview, action log, pause / resume / step
464
 
465
  ## License
@@ -551,7 +603,7 @@ def repo_files():
551
  '''
552
 
553
  mo.md(f"packaging: {len(pyproject_toml) + len(license_txt) + len(gitignore_txt)} chars, "
554
- f"v0.2.0 with ocr/desktop/vision extras")
555
  return gitignore_txt, license_txt, pyproject_toml
556
 
557
 
@@ -639,11 +691,6 @@ def publish_status(
639
 
640
  @app.cell
641
  def demo():
642
- from tiny_computer_agent import AgentLoop, DryRunExecutor, PlannerBrain, Policy
643
- from tiny_computer_agent.demo import fake_desktop
644
- from tiny_computer_agent.observe import ImageScreen
645
- from tiny_computer_agent.perception import IndexedScreen, OcrDetector
646
-
647
  # A real screenshot -> real OCR -> labelled, clickable elements. No display needed:
648
  # this is the same path --image takes, and it is what makes a tiny model viable.
649
  _detector = OcrDetector()
@@ -677,5 +724,90 @@ def demo():
677
  return
678
 
679
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
  if __name__ == "__main__":
681
  app.run()
 
17
  import marimo as mo
18
  from huggingface_hub import HfApi
19
 
20
+ # The project's public API, imported once so no cell has to duplicate it.
21
+ from tiny_computer_agent import (
22
+ AgentLoop,
23
+ DryRunExecutor,
24
+ Element,
25
+ MockBrain,
26
+ PlannerBrain,
27
+ Policy,
28
+ )
29
+ from tiny_computer_agent.actions import Action
30
+ from tiny_computer_agent.brain import DecisionMenu
31
+ from tiny_computer_agent.demo import fake_desktop
32
+ from tiny_computer_agent.observe import ImageScreen
33
+ from tiny_computer_agent.perception import IndexedScreen, OcrDetector
34
+ from tiny_computer_agent.trace import Scenario, benchmark
35
+
36
+
37
 
38
  @app.cell(hide_code=True)
39
  def readme():
40
+ readme_md = r'''
41
 
42
  ---
43
  title: Tiny Computer Agent
 
63
 
64
  > **Status 0.3.0: it drives a real desktop — on Linux and on Windows.** Screen capture,
65
  > an OCR element index with real coordinates, a real mouse click, and a *verified*
66
+ > outcome. A run can be saved as a portable *trace*, replayed offline and **scored**, so
67
+ > "which brain is right, and how often?" is a number in a table.
68
+ > The suite passes on a live Linux desktop and inside a real Windows Server
69
  > 2022 guest; what is not solved is model reliability — see
70
  > [measured results](#measured-results).
71
 
 
125
  ```bash
126
  python -m pip install -e ".[desktop,ocr,dev]"
127
  pytest tests -q
128
+ # 151 passed, 2 skipped in 18.7s
129
  ```
130
 
131
  The two skips are the live-desktop tests. Give them a display and they run for real:
 
195
  step 0 | click label='Save' reason="the reply names 'Save'" -> pixel (295, 284)
196
  ```
197
 
198
+ ### Scoring a brain, offline
199
+
200
+ "Which brain is right, and how often?" needs no desktop, no model and no network,
201
+ because a **trace** keeps every frame's element index and throws the pixels away. Record a
202
+ run once; replay it against any brain, as often as you like.
203
+
204
+ ```
205
+ $ python -m tiny_computer_agent --image example --trace runs/save.json --expected Save
206
+ ...
207
+ trace: wrote runs/save.json (2 frames)
208
+ score: correct click 'Save' to save the document: expected 'Save', chose 'Save'
209
+ ```
210
+
211
+ A trace is JSON, so it diffs in review, commits next to the tests, and replays on a machine
212
+ with nothing installed. Scoring a whole suite is a table:
213
+
214
+ ```
215
+ scoreboard: 7/9 correct (78%)
216
+ planner (rule-based): 100%
217
+ first-item (baseline): 33%
218
+ oracle (always right): 100%
219
+ correct save the document: expected 'Save', chose 'Save'
220
+ incorrect save the document: expected 'Save', chose 'Files' (chose 'Files', not 'Save')
221
+ ...
222
+ ```
223
+
224
+ The number is the brain's **decision** - what it chose when it had to commit. Whether the
225
+ harness *allowed* that decision to run is a separate axis: a click the policy refused is
226
+ still the choice it was, and it is flagged `(harness refused it)` rather than passed off as
227
+ a clean success. Keeping those two apart is the point.
228
+
229
  ### Which brain drove which run
230
 
231
  The harness and the brain are separable, and it matters which one a result came from.
 
257
  `warning: no text was found on screen, so the index is 96 numbered grid cells and the
258
  agent is choosing blind` — instead of silently guessing.
259
 
260
+ **153 tests pass**, including real OCR over a real image and (opt-in) live-desktop tests
261
  that click a real button.
262
 
263
  ## Verified on real Windows
 
299
 
300
  None of the three failures in the first row is a harness defect: two are
301
  `ModuleNotFoundError: numpy` and the third is the guard bug documented below. With the
302
+ OCR engine installed, the same suite exits **0**. (Those Windows counts predate `trace.py`; the harness suite has since grown to 153 tests.)
303
 
304
  **The emulated-OCR caveat.** onnxruntime requires AVX2, and QEMU's software emulation of
305
  AVX2 is roughly two orders of magnitude slower than native. A suite that finishes in 17
 
325
 
326
  ## The test suite, in full
327
 
328
+ `pytest tests` collects **153 tests**. 151 run headless — no display, no network, no
329
  weights. The last 2 are the opt-in live-desktop tests, skipped unless
330
  `TINY_COMPUTER_AGENT_LIVE_TEST=1`.
331
 
 
342
  | `tests/test_grid_fallback.py` | 11 | the fallback index announces itself instead of guessing |
343
  | `tests/test_desktop.py` | 10 | display detection, the preflight checks, `doctor` |
344
  | `tests/test_cli.py` | 10 | the command line: `--doctor`, `--image`, `--live`, argument handling |
345
+ | `tests/test_trace.py` | 28 | traces: a run recorded with no pixels, replayed against another brain, and scored against ground truth |
346
  | `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 |
347
 
348
  ```bash
349
+ pytest tests -q # 151 passed, 2 skipped
350
+ TINY_COMPUTER_AGENT_LIVE_TEST=1 pytest tests -q # 153 passed, on a desktop
351
  ```
352
 
353
  ## Install
 
356
  pip install -e ".[ocr]" # labels from pixels (RapidOCR: no system binary)
357
  pip install -e ".[desktop,ocr]" # + real capture and real input
358
  pip install -e ".[vision,ocr]" # + the tiny model brains
359
+ pytest # 153 tests, no display or network needed
360
  ```
361
 
362
  ## Run it on your machine
 
433
 
434
  # 2. inside the guest, after autologon
435
  # vc_redist.x64.exe is required before onnxruntime will load
436
+ curl.exe -s -o C:\tiny\vc_redist.x64.exe <vc_redist-url> && C:\tiny\vc_redist.x64.exe /install /quiet /norestart
437
  ```
438
 
439
  Serve files to the guest over QEMU's user-mode networking (the host is `10.0.2.2`), which
 
457
  | `loop.py` | `AgentLoop`, `Episode`, `Step`, the no-progress guard and the verifier |
458
  | `desktop.py` | Preflight checks, `doctor`, and `build_agent` in one call |
459
  | `demo.py` | The synthetic desktop used by tests and `--image example` |
460
+ | `trace.py` | Portable traces: record a run with no pixels, replay it offline, score the choice against ground truth |
461
 
462
  ## Why a tiny model can do this at all
463
 
 
490
 
491
  **Works:** real capture, a real OCR index with coordinates that land on the right pixel,
492
  a real click an app reacts to, verified success, the closed vocabulary, the policy gates,
493
+ the no-progress guard, echo detection, the doctor, the CLI, portable traces that score a brain's choices offline, 153 tests, a 256M VLM that
494
  reads a button label from pixels on a clean screenshot — and the same suite passing inside
495
  a real Windows Server 2022 guest with no KVM (109 passed, exit 0, with the OCR-inference
496
  tests excluded as impractically slow under emulation).
 
511
  - [ ] Make the OCR guard in `test_cli.py` skip, rather than fail, when no engine is installed
512
  - [ ] Make the VLM reliable on a live screen (it echoes the prompt instead of choosing)
513
  - [ ] Accessibility-tree detector (AT-SPI on Linux, UI Automation on Windows)
514
+ - [x] Score episodes: replay a trace and count how often the choice was right - see [scoring a brain, offline](#scoring-a-brain-offline)
515
  - [ ] marimo console: live screen preview, action log, pause / resume / step
516
 
517
  ## License
 
603
  '''
604
 
605
  mo.md(f"packaging: {len(pyproject_toml) + len(license_txt) + len(gitignore_txt)} chars, "
606
+ f"v0.3.0 with ocr/desktop/vision extras")
607
  return gitignore_txt, license_txt, pyproject_toml
608
 
609
 
 
691
 
692
  @app.cell
693
  def demo():
 
 
 
 
 
694
  # A real screenshot -> real OCR -> labelled, clickable elements. No display needed:
695
  # this is the same path --image takes, and it is what makes a tiny model viable.
696
  _detector = OcrDetector()
 
724
  return
725
 
726
 
727
+ @app.cell
728
+ def scoreboard():
729
+ # The brains are separable, so they can be scored against the same tasks with no
730
+ # display, no OCR and no weights: a scenario is an element index plus the answer that
731
+ # is right. Record a run once; replay it against any brain, and count how often right.
732
+
733
+
734
+ class FirstItemBrain:
735
+ """A baseline with no model at all: click whatever is first in reading order."""
736
+
737
+ name = "first-item"
738
+
739
+ def decide(self, goal, observation):
740
+ menu = DecisionMenu(observation)
741
+ if not menu:
742
+ return Action("done", reason="nothing to click")
743
+ return Action("click", label=menu.items[0].label, reason="first item in reading order")
744
+
745
+
746
+ def _bar(label, x):
747
+ return Element(label, x=x, y=6, width=80, height=20)
748
+
749
+
750
+ def _button(label, x, y=270):
751
+ return Element(label, x=x, y=y, width=90, height=30)
752
+
753
+
754
+ score_scenarios = [
755
+ Scenario(
756
+ "click 'Save' to save the document",
757
+ frames=[
758
+ (_bar("Files", 0), _bar("Edit", 100), _bar("View", 200)),
759
+ (Element("Name", x=200, y=195, width=200, height=26), _button("Save", 200)),
760
+ (_bar("Files", 0), _button("Save", 200)),
761
+ ],
762
+ expected="Save", name="save the document",
763
+ ),
764
+ Scenario(
765
+ "click 'Files' to open the file manager",
766
+ frames=[(_bar("Files", 0), _bar("Edit", 100), _button("Save", 200))],
767
+ expected="Files", name="open the file manager",
768
+ ),
769
+ Scenario(
770
+ "click 'Cancel' to discard the change",
771
+ frames=[(_button("Save", 200), _button("Cancel", 400))],
772
+ expected="Cancel", name="discard the change",
773
+ ),
774
+ ]
775
+
776
+ scoreboard = benchmark(
777
+ score_scenarios,
778
+ {
779
+ "planner (rule-based)": lambda scenario: PlannerBrain(),
780
+ "first-item (baseline)": lambda scenario: FirstItemBrain(),
781
+ "oracle (always right)": lambda scenario: MockBrain(
782
+ [
783
+ {"kind": "click", "label": scenario.expected},
784
+ {"kind": "done", "reason": "clicked the answer the task names"},
785
+ ]
786
+ ),
787
+ },
788
+ )
789
+
790
+ mo.vstack(
791
+ [
792
+ mo.md(
793
+ f"**Score the choice, offline.** {len(score_scenarios)} tasks x "
794
+ f"{len(scoreboard.brains())} brains = **{scoreboard.total} decisions**, "
795
+ f"**{scoreboard.earned} of them right ({scoreboard.accuracy:.0%})** - read off "
796
+ f"saved element indexes, with no screen, no model and no network."
797
+ ),
798
+ mo.ui.table(scoreboard.to_rows(), selection=None),
799
+ mo.md(
800
+ "Accuracy per brain: "
801
+ + " · ".join(
802
+ f"**{_brain}** {_accuracy:.0%}"
803
+ for _brain, _accuracy in scoreboard.by_brain().items()
804
+ )
805
+ ),
806
+ ]
807
+ )
808
+
809
+ return
810
+
811
+
812
  if __name__ == "__main__":
813
  app.run()