Publish tiny_computer_agent_app.py from marimo
Browse files- tiny_computer_agent_app.py +408 -0
tiny_computer_agent_app.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import marimo
|
| 2 |
+
|
| 3 |
+
__generated_with = "0.25.2"
|
| 4 |
+
app = marimo.App(width="medium", auto_download=["html"])
|
| 5 |
+
|
| 6 |
+
with app.setup:
|
| 7 |
+
import os
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import marimo as mo
|
| 11 |
+
from huggingface_hub import HfApi
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@app.cell(hide_code=True)
|
| 16 |
+
def readme():
|
| 17 |
+
readme_md = '''
|
| 18 |
+
---
|
| 19 |
+
title: Tiny Computer Agent
|
| 20 |
+
emoji: 🖱️
|
| 21 |
+
colorFrom: indigo
|
| 22 |
+
colorTo: green
|
| 23 |
+
license: mit
|
| 24 |
+
tags:
|
| 25 |
+
- computer-use
|
| 26 |
+
- gui-agent
|
| 27 |
+
- agentic-harness
|
| 28 |
+
- local-first
|
| 29 |
+
- tiny-models
|
| 30 |
+
- offline
|
| 31 |
+
- marimo
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
# 🖱️ Tiny Computer Agent
|
| 35 |
+
|
| 36 |
+
**An agentic harness that drives a real computer with a tiny, fully local model.**
|
| 37 |
+
No cloud API, no API keys, no telemetry — and a core that imports nothing outside
|
| 38 |
+
the Python standard library.
|
| 39 |
+
|
| 40 |
+
> **Status: early alpha.** The loop runs end to end today, with a scripted screen,
|
| 41 |
+
> dry-run execution, a CLI and 56 offline tests. The vision backend is a
|
| 42 |
+
> documented seam, not a feature — see [what works](#-what-works-today).
|
| 43 |
+
|
| 44 |
+
## The idea, in one loop
|
| 45 |
+
|
| 46 |
+
```
|
| 47 |
+
observe think act verify
|
| 48 |
+
+---------+ +---------+ +---------+ +---------+
|
| 49 |
+
| screen |--->| tiny |--->| mouse / |--->| next |
|
| 50 |
+
| frame | | model | | keyboard| | frame |
|
| 51 |
+
+---------+ +---------+ +---------+ +----+----+
|
| 52 |
+
|
|
| 53 |
+
<-------------------------------------------------+
|
| 54 |
+
repeat until done, or the step budget runs out
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
1. **Observe** — grab the screen, downscale it, optionally attach OCR text or the
|
| 58 |
+
accessibility tree so the model never has to guess where things are.
|
| 59 |
+
2. **Think** — a tiny (≤ 3B, quantised) vision-language model emits **exactly one
|
| 60 |
+
action** from a closed vocabulary.
|
| 61 |
+
3. **Act** — the harness executes it with OS-level input (mouse, keyboard, scroll).
|
| 62 |
+
4. **Verify** — the next frame shows whether it worked. Repeat, bounded by a step
|
| 63 |
+
budget and a kill switch.
|
| 64 |
+
|
| 65 |
+
## What works today
|
| 66 |
+
|
| 67 |
+
```
|
| 68 |
+
$ python -m tiny_computer_agent
|
| 69 |
+
goal: click 'Save' to save the document
|
| 70 |
+
mode: dry-run | screen: scripted demo | kill switch: create /tmp/tiny-computer-agent.STOP to stop
|
| 71 |
+
|
| 72 |
+
step 0 | wait seconds=0.5 reason="'Save' is not on screen yet" | ok: (dry-run) would wait 0.5s
|
| 73 |
+
step 1 | click label='Save' reason="matched 'Save' in the element index" | ok: (dry-run) would click -> pixel (360, 132)
|
| 74 |
+
step 2 | done reason="already clicked 'Save'; goal assumed met" | ok: goal reported met
|
| 75 |
+
|
| 76 |
+
result: success - brain reported done: already clicked 'Save'; goal assumed met
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
Shipped and tested:
|
| 80 |
+
|
| 81 |
+
- **the loop** — `AgentLoop` runs observe → think → act → verify, **re-reads the
|
| 82 |
+
screen after every action**, and stops on done, on a step budget, on the kill
|
| 83 |
+
switch, on a policy refusal, on a failed action, or on a brain error. Every step
|
| 84 |
+
is recorded in an `Episode` you can render or turn into rows.
|
| 85 |
+
- **a closed action vocabulary** — `click` `type` `key` `scroll` `wait` `done`,
|
| 86 |
+
validated *before* anything executes. A model that invents a verb gets a recorded
|
| 87 |
+
error, not a click.
|
| 88 |
+
- **three brains** — `MockBrain` replays a script; `PlannerBrain` is rule-based and
|
| 89 |
+
genuinely clicks the element matching the goal (it waits for a dialog instead of
|
| 90 |
+
flailing, and refuses to click the same label twice); `VLMBrain` is the seam —
|
| 91 |
+
prompt assembly and reply validation are done, `_generate()` is yours.
|
| 92 |
+
- **two executors** — `DryRunExecutor` is the default: it logs the plan and resolves
|
| 93 |
+
every label to a pixel. `PyAutoGuiExecutor` does real input and **refuses to click
|
| 94 |
+
when a label matches no element**, because a tiny model guessing pixels is how you
|
| 95 |
+
click "Delete account". Dry-run fails exactly where live input refuses, so a
|
| 96 |
+
rehearsal never gives false confidence.
|
| 97 |
+
- **policy gates** — deny-listed text (`sudo`, `rm -rf`, "password", "api_key"),
|
| 98 |
+
deny-listed widgets (terminal, keychain, password manager, wallet, bank), four
|
| 99 |
+
deny-listed key chords, a kill-switch file checked before every action, and a step
|
| 100 |
+
budget.
|
| 101 |
+
- **real input needs two deliberate flags** — `--real-input` *and*
|
| 102 |
+
`TINY_COMPUTER_AGENT_ALLOW_REAL_INPUT=1`. Default is a dry run.
|
| 103 |
+
- **56 tests** needing no display, no model and no network.
|
| 104 |
+
|
| 105 |
+
What does **not** work yet: seeing a real screen *usefully*. `GrabScreen` captures
|
| 106 |
+
pixels through `mss`, but nothing yet turns pixels into an element index (OCR or
|
| 107 |
+
AT-SPI), so a live screen currently yields zero elements — which is exactly what the
|
| 108 |
+
label path depends on. And no model is wired up.
|
| 109 |
+
|
| 110 |
+
## Install and run
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
pip install -e . # core: standard library only, no deps
|
| 114 |
+
pytest # 56 offline tests
|
| 115 |
+
python -m tiny_computer_agent # scripted demo, dry run
|
| 116 |
+
python -m tiny_computer_agent --live # your real screen, still dry run
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
## Put it on your own task
|
| 120 |
+
|
| 121 |
+
```python
|
| 122 |
+
from tiny_computer_agent import AgentLoop, Element, FakeScreen, MockBrain
|
| 123 |
+
|
| 124 |
+
screen = FakeScreen([(Element("Save", x=320, y=120, width=80, height=24),)])
|
| 125 |
+
brain = MockBrain([{"kind": "click", "label": "Save"},
|
| 126 |
+
{"kind": "done", "reason": "saved"}])
|
| 127 |
+
|
| 128 |
+
episode = AgentLoop(screen=screen, brain=brain).run("save the document")
|
| 129 |
+
print(episode.render())
|
| 130 |
+
episode.to_rows() # rows for a dataframe or notebook table
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
Swap `FakeScreen` for `GrabScreen()` and `MockBrain` for your `VLMBrain` subclass and
|
| 134 |
+
that is the whole distance between the tests and a live agent.
|
| 135 |
+
|
| 136 |
+
## Why "tiny" is the whole point
|
| 137 |
+
|
| 138 |
+
The harness does the heavy lifting, so the model does not have to be clever:
|
| 139 |
+
|
| 140 |
+
| Job | Who does it |
|
| 141 |
+
|---|---|
|
| 142 |
+
| Finding *where* a button is | OCR / accessibility tree / element index — not the model |
|
| 143 |
+
| Choosing *what* to do | a tiny VLM over a **closed action menu** (click, type, key, scroll, wait, done) |
|
| 144 |
+
| Knowing whether it worked | the next frame, plus harness-side checks |
|
| 145 |
+
| Recovering from a bad step | the retry policy in `loop.py`, not the weights |
|
| 146 |
+
|
| 147 |
+
That keeps the agent offline, fast to start, and small enough that a laptop GPU
|
| 148 |
+
(or plain CPU) can run it on the same box it is controlling.
|
| 149 |
+
|
| 150 |
+
## Candidate backends
|
| 151 |
+
|
| 152 |
+
Pluggable behind one interface; nothing here needs a network call at run time.
|
| 153 |
+
|
| 154 |
+
- **Moondream 2** / **SmolVLM-256M / 500M** — a few hundred MB, safe default
|
| 155 |
+
- **Qwen2-VL-2B-Instruct**, **Florence-2-base** — stronger grounding, still tiny
|
| 156 |
+
- **OCR + text-only planner** — no VLM at all; the cheapest credible baseline
|
| 157 |
+
- **mock brain** — deterministic scripted actions, used to test the harness itself
|
| 158 |
+
|
| 159 |
+
## Layout
|
| 160 |
+
|
| 161 |
+
```
|
| 162 |
+
tiny-computer-agent/
|
| 163 |
+
├── README.md
|
| 164 |
+
├── LICENSE
|
| 165 |
+
├── pyproject.toml
|
| 166 |
+
├── tiny_computer_agent_app.py # marimo console: run an episode, publish this card
|
| 167 |
+
├── tiny_computer_agent/
|
| 168 |
+
│ ├── actions.py # the closed action vocabulary
|
| 169 |
+
│ ├── observe.py # Observation + element index; FakeScreen, GrabScreen
|
| 170 |
+
│ ├── brain.py # MockBrain, PlannerBrain, VLMBrain (the seam)
|
| 171 |
+
│ ├── act.py # DryRunExecutor (default) + PyAutoGuiExecutor
|
| 172 |
+
│ ├── safety.py # Policy: deny lists, kill switch, step budget
|
| 173 |
+
│ ├── loop.py # AgentLoop, Episode, Step
|
| 174 |
+
│ └── __main__.py # CLI
|
| 175 |
+
└── tests/ # 56 tests: no display, no model, no network
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
## Safety, because this thing has your mouse
|
| 179 |
+
|
| 180 |
+
- Run it in a **VM, container, or throwaway desktop account** — not your real session.
|
| 181 |
+
- Dry run is the default; read the whole plan before letting it near your pointer.
|
| 182 |
+
- Deny-list for shells, terminals, payment flows and password managers.
|
| 183 |
+
- A step budget plus a kill-switch file checked before every action.
|
| 184 |
+
- **Local-first, for real:** screen pixels and keystrokes never leave the machine.
|
| 185 |
+
|
| 186 |
+
## Roadmap
|
| 187 |
+
|
| 188 |
+
- [x] `loop.py` + `act.py` with the mock brain — the harness is testable before any model download
|
| 189 |
+
- [x] policy gates: deny lists, kill switch, step budget, dry run by default
|
| 190 |
+
- [ ] OCR / AT-SPI element index, so a live screen has clickable labels
|
| 191 |
+
- [ ] tiny-VLM grounding backend, behind `VLMBrain._generate()`
|
| 192 |
+
- [ ] marimo console: live screen preview, action log, pause / resume / step
|
| 193 |
+
- [ ] persist episodes, so failed runs can be replayed and scored
|
| 194 |
+
|
| 195 |
+
## License
|
| 196 |
+
|
| 197 |
+
MIT — see [LICENSE](LICENSE).
|
| 198 |
+
'''
|
| 199 |
+
|
| 200 |
+
mo.md(readme_md)
|
| 201 |
+
|
| 202 |
+
return (readme_md,)
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
@app.cell(hide_code=True)
|
| 206 |
+
def repo_files():
|
| 207 |
+
pyproject_toml = '''
|
| 208 |
+
[build-system]
|
| 209 |
+
requires = ["setuptools>=68"]
|
| 210 |
+
build-backend = "setuptools.build_meta"
|
| 211 |
+
|
| 212 |
+
[project]
|
| 213 |
+
name = "tiny-computer-agent"
|
| 214 |
+
version = "0.1.0"
|
| 215 |
+
description = "An agentic harness that drives a real computer with a tiny, fully local model - no cloud API."
|
| 216 |
+
readme = "README.md"
|
| 217 |
+
requires-python = ">=3.10"
|
| 218 |
+
license = { text = "MIT" }
|
| 219 |
+
authors = [{ name = "hkblockchaintechnology" }]
|
| 220 |
+
keywords = ["computer-use", "gui-agent", "agentic-harness", "local-first", "tiny-models"]
|
| 221 |
+
classifiers = [
|
| 222 |
+
"Development Status :: 3 - Alpha",
|
| 223 |
+
"Intended Audience :: Developers",
|
| 224 |
+
"Programming Language :: Python :: 3 :: Only",
|
| 225 |
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
| 226 |
+
]
|
| 227 |
+
dependencies = [] # the harness core imports nothing outside the standard library
|
| 228 |
+
|
| 229 |
+
[project.optional-dependencies]
|
| 230 |
+
desktop = ["mss>=9.0", "pyautogui>=0.9", "pillow>=10"]
|
| 231 |
+
vision = ["transformers>=4.48", "torch>=2.2", "pillow>=10"]
|
| 232 |
+
dev = ["pytest>=8"]
|
| 233 |
+
|
| 234 |
+
[project.scripts]
|
| 235 |
+
tiny-computer-agent = "tiny_computer_agent.__main__:main"
|
| 236 |
+
|
| 237 |
+
[project.urls]
|
| 238 |
+
Homepage = "https://huggingface.co/hkblockchaintechnology/tiny-computer-agent"
|
| 239 |
+
|
| 240 |
+
[tool.setuptools]
|
| 241 |
+
packages = ["tiny_computer_agent"]
|
| 242 |
+
|
| 243 |
+
[tool.pytest.ini_options]
|
| 244 |
+
testpaths = ["tests"]
|
| 245 |
+
'''
|
| 246 |
+
|
| 247 |
+
license_txt = '''MIT License
|
| 248 |
+
|
| 249 |
+
Copyright (c) 2026 hkblockchaintechnology
|
| 250 |
+
|
| 251 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 252 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 253 |
+
in the Software without restriction, including without limitation the rights
|
| 254 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 255 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 256 |
+
furnished to do so, subject to the following conditions:
|
| 257 |
+
|
| 258 |
+
The above copyright notice and this permission notice shall be included in all
|
| 259 |
+
copies or substantial portions of the Software.
|
| 260 |
+
|
| 261 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 262 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 263 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 264 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 265 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 266 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 267 |
+
SOFTWARE.
|
| 268 |
+
'''
|
| 269 |
+
|
| 270 |
+
gitignore_txt = '''__pycache__/
|
| 271 |
+
*.py[cod]
|
| 272 |
+
.venv/
|
| 273 |
+
venv/
|
| 274 |
+
.env
|
| 275 |
+
*.gguf
|
| 276 |
+
models/
|
| 277 |
+
runs/
|
| 278 |
+
traces/
|
| 279 |
+
.ipynb_checkpoints/
|
| 280 |
+
.DS_Store
|
| 281 |
+
'''
|
| 282 |
+
|
| 283 |
+
mo.md(f"{len(pyproject_toml) + len(license_txt) + len(gitignore_txt)} chars of packaging ready to publish")
|
| 284 |
+
|
| 285 |
+
return gitignore_txt, license_txt, pyproject_toml
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
@app.cell
|
| 289 |
+
def publish():
|
| 290 |
+
REPO_ID = "hkblockchaintechnology/tiny-computer-agent"
|
| 291 |
+
|
| 292 |
+
publish_button = mo.ui.button(
|
| 293 |
+
label="⬆️ Publish README.md to the Hub",
|
| 294 |
+
value=0,
|
| 295 |
+
on_click=lambda value: value + 1,
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
publish_button
|
| 299 |
+
|
| 300 |
+
return REPO_ID, publish_button
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
@app.cell
|
| 304 |
+
def publish_status(
|
| 305 |
+
REPO_ID,
|
| 306 |
+
gitignore_txt,
|
| 307 |
+
license_txt,
|
| 308 |
+
publish_button,
|
| 309 |
+
pyproject_toml,
|
| 310 |
+
readme_md,
|
| 311 |
+
):
|
| 312 |
+
_status = mo.md(
|
| 313 |
+
"This notebook owns the project: the card, the packaging files, the package and "
|
| 314 |
+
"the tests. Click **Publish** to push all of it to the Hub."
|
| 315 |
+
).callout(kind="info")
|
| 316 |
+
|
| 317 |
+
if publish_button.value:
|
| 318 |
+
_token = os.environ.get("HF_TOKEN")
|
| 319 |
+
if not _token:
|
| 320 |
+
_status = mo.md(
|
| 321 |
+
"No `HF_TOKEN` in the environment. Run `export HF_TOKEN=...` "
|
| 322 |
+
"(or `hf auth login`), then restart the kernel."
|
| 323 |
+
).callout(kind="warn")
|
| 324 |
+
else:
|
| 325 |
+
_api = HfApi(token=_token)
|
| 326 |
+
_root = Path(__file__).parent
|
| 327 |
+
_notebook = Path(__file__)
|
| 328 |
+
_files = {
|
| 329 |
+
"README.md": readme_md,
|
| 330 |
+
"pyproject.toml": pyproject_toml,
|
| 331 |
+
"LICENSE": license_txt,
|
| 332 |
+
".gitignore": gitignore_txt,
|
| 333 |
+
"tiny_computer_agent_app.py": _notebook.read_text(encoding="utf-8"),
|
| 334 |
+
}
|
| 335 |
+
_missing = [f for f in ("tiny_computer_agent", "tests") if not (_root / f).is_dir()]
|
| 336 |
+
if _missing:
|
| 337 |
+
_status = mo.md(f"Nothing to publish: missing {_missing} next to `{_root}`").callout(kind="danger")
|
| 338 |
+
else:
|
| 339 |
+
try:
|
| 340 |
+
for _name, _text in _files.items():
|
| 341 |
+
_api.upload_file(
|
| 342 |
+
path_or_fileobj=_text.encode("utf-8"),
|
| 343 |
+
path_in_repo=_name,
|
| 344 |
+
repo_id=REPO_ID,
|
| 345 |
+
repo_type="model",
|
| 346 |
+
commit_message=f"Publish {_name} from marimo",
|
| 347 |
+
)
|
| 348 |
+
for _folder in ("tiny_computer_agent", "tests"):
|
| 349 |
+
_api.upload_folder(
|
| 350 |
+
folder_path=str(_root / _folder),
|
| 351 |
+
path_in_repo=_folder,
|
| 352 |
+
repo_id=REPO_ID,
|
| 353 |
+
repo_type="model",
|
| 354 |
+
commit_message=f"Publish {_folder}/ from marimo",
|
| 355 |
+
ignore_patterns=["__pycache__/*", "*.pyc"],
|
| 356 |
+
)
|
| 357 |
+
_status = mo.md(
|
| 358 |
+
f"✅ Published {len(_files)} files and 2 folders to "
|
| 359 |
+
f"[{REPO_ID}](https://huggingface.co/{REPO_ID})"
|
| 360 |
+
).callout(kind="success")
|
| 361 |
+
except Exception as _e:
|
| 362 |
+
_status = mo.md(f"Publish failed: `{type(_e).__name__}: {_e}`").callout(kind="danger")
|
| 363 |
+
|
| 364 |
+
_status
|
| 365 |
+
|
| 366 |
+
return
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
@app.cell
|
| 370 |
+
def demo():
|
| 371 |
+
from tiny_computer_agent import (
|
| 372 |
+
AgentLoop, DryRunExecutor, Element, FakeScreen, PlannerBrain, Policy,
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
_scripted_screen = FakeScreen(
|
| 376 |
+
[
|
| 377 |
+
(Element("Terminal", x=0, y=0, width=40, height=24),
|
| 378 |
+
Element("Files", x=60, y=0, width=40, height=24)),
|
| 379 |
+
(Element("Name", kind="input", x=100, y=120, width=200, height=24),
|
| 380 |
+
Element("Save", x=320, y=120, width=80, height=24)),
|
| 381 |
+
(Element("Save", x=320, y=120, width=80, height=24),),
|
| 382 |
+
],
|
| 383 |
+
text="Untitled document - unsaved changes",
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
demo_episode = AgentLoop(
|
| 387 |
+
screen=_scripted_screen,
|
| 388 |
+
brain=PlannerBrain(),
|
| 389 |
+
executor=DryRunExecutor(),
|
| 390 |
+
policy=Policy(step_budget=6),
|
| 391 |
+
).run("click \'Save\' to save the document")
|
| 392 |
+
|
| 393 |
+
mo.vstack(
|
| 394 |
+
[
|
| 395 |
+
mo.md(
|
| 396 |
+
"**Scripted episode, dry run** — no model, no display, nothing touched. "
|
| 397 |
+
"The planner waits for the dialog, clicks the label, and stops."
|
| 398 |
+
),
|
| 399 |
+
mo.ui.table(demo_episode.to_rows(), selection=None),
|
| 400 |
+
mo.md(f"`{demo_episode.render().splitlines()[1]}`"),
|
| 401 |
+
]
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
return
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
if __name__ == "__main__":
|
| 408 |
+
app.run()
|