Publish showcase_catalog.py from marimo
Browse files- showcase_catalog.py +231 -2
showcase_catalog.py
CHANGED
|
@@ -14,11 +14,14 @@ from showcase_core import (
|
|
| 14 |
)
|
| 15 |
import os
|
| 16 |
import subprocess
|
|
|
|
|
|
|
| 17 |
|
| 18 |
|
| 19 |
-
def term(sid, category, title, shows, note="", argv=None, code=None, env=None
|
|
|
|
| 20 |
CATALOG.append(Shot(id=sid, category=category, title=title, shows=shows, note=note,
|
| 21 |
-
kind="terminal", argv=argv, code=code, env=env))
|
| 22 |
|
| 23 |
|
| 24 |
def desk(sid, category, title, shows, note="", scene_name=None, make=None):
|
|
@@ -1491,3 +1494,229 @@ _edit("exec-refuses-unresolved",
|
|
| 1491 |
'print(" ", InputUnavailable("real input needs pyautogui and a live display: '
|
| 1492 |
'pip install \'tiny-computer-agent[desktop]\'"))\nprint()\n'
|
| 1493 |
'resolved = executor.execute(Action("done", reason="nothing to do"), frame)')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
)
|
| 15 |
import os
|
| 16 |
import subprocess
|
| 17 |
+
import time
|
| 18 |
+
from pathlib import Path
|
| 19 |
|
| 20 |
|
| 21 |
+
def term(sid, category, title, shows, note="", argv=None, code=None, env=None,
|
| 22 |
+
prepare=None):
|
| 23 |
CATALOG.append(Shot(id=sid, category=category, title=title, shows=shows, note=note,
|
| 24 |
+
kind="terminal", argv=argv, code=code, env=env, prepare=prepare))
|
| 25 |
|
| 26 |
|
| 27 |
def desk(sid, category, title, shows, note="", scene_name=None, make=None):
|
|
|
|
| 1494 |
'print(" ", InputUnavailable("real input needs pyautogui and a live display: '
|
| 1495 |
'pip install \'tiny-computer-agent[desktop]\'"))\nprint()\n'
|
| 1496 |
'resolved = executor.execute(Action("done", reason="nothing to do"), frame)')
|
| 1497 |
+
|
| 1498 |
+
# =========================================================================== #
|
| 1499 |
+
# 15. the stepwise loop, the masked decoder, and the accessibility tree
|
| 1500 |
+
# =========================================================================== #
|
| 1501 |
+
|
| 1502 |
+
term("stepwise", "The loop",
|
| 1503 |
+
"The same loop, one turn at a time, with pause and resume",
|
| 1504 |
+
["AgentLoop", "AgentLoop.begin", "AgentLoop.step_once", "AgentLoop.pause",
|
| 1505 |
+
"AgentLoop.resume", "AgentLoop.finished", "AgentLoop.run", "Step.line"],
|
| 1506 |
+
note="run() is batch; a console needs the caller to own the loop. These are the same rules.",
|
| 1507 |
+
code="""
|
| 1508 |
+
from tiny_computer_agent import AgentLoop, Element, FakeScreen, MockBrain, Policy
|
| 1509 |
+
|
| 1510 |
+
screen = FakeScreen([
|
| 1511 |
+
(Element("Files", x=10, y=6, width=60, height=20),),
|
| 1512 |
+
(Element("Save", x=320, y=120, width=80, height=24),),
|
| 1513 |
+
])
|
| 1514 |
+
loop = AgentLoop(
|
| 1515 |
+
screen=screen,
|
| 1516 |
+
brain=MockBrain([{"kind": "wait", "seconds": 0.1},
|
| 1517 |
+
{"kind": "click", "label": "Save"},
|
| 1518 |
+
{"kind": "done", "reason": "saved"}]),
|
| 1519 |
+
policy=Policy(step_budget=6),
|
| 1520 |
+
)
|
| 1521 |
+
loop.begin("save the report")
|
| 1522 |
+
print("after begin(): finished =", loop.finished, "| steps =", len(loop.episode.steps))
|
| 1523 |
+
|
| 1524 |
+
print("step_once() -> ", loop.step_once().line())
|
| 1525 |
+
print(" finished =", loop.finished, "| steps =", len(loop.episode.steps))
|
| 1526 |
+
|
| 1527 |
+
loop.pause()
|
| 1528 |
+
loop.run() # returns at once: the caller said pause
|
| 1529 |
+
print("after pause(): paused =", loop.paused, "| steps =", len(loop.episode.steps))
|
| 1530 |
+
|
| 1531 |
+
loop.resume()
|
| 1532 |
+
episode = loop.run()
|
| 1533 |
+
print("after resume(): finished =", loop.finished, "| success =", episode.success)
|
| 1534 |
+
print()
|
| 1535 |
+
print(episode.render())
|
| 1536 |
+
""")
|
| 1537 |
+
|
| 1538 |
+
term("decoding", "Models",
|
| 1539 |
+
"Constrained decoding: make an echo impossible, not merely detected",
|
| 1540 |
+
["MenuMask", "digit_token_ids", "choice_token_ids", "pair_token_ids", "read_choice",
|
| 1541 |
+
"read_verdict", "brevity_ladder", "majority_choice", "DIGITS"],
|
| 1542 |
+
note="The 256M model's failure was answering with the prompt. A mask removes that option.",
|
| 1543 |
+
code="""
|
| 1544 |
+
from tiny_computer_agent.decoding import (DIGITS, MenuMask, brevity_ladder,
|
| 1545 |
+
choice_token_ids, digit_token_ids,
|
| 1546 |
+
majority_choice, pair_token_ids, read_choice,
|
| 1547 |
+
read_verdict)
|
| 1548 |
+
from tiny_computer_agent.models import DEFAULT_VISION_MODEL
|
| 1549 |
+
|
| 1550 |
+
try:
|
| 1551 |
+
from transformers import AutoTokenizer
|
| 1552 |
+
tokenizer = AutoTokenizer.from_pretrained(DEFAULT_VISION_MODEL)
|
| 1553 |
+
source = DEFAULT_VISION_MODEL
|
| 1554 |
+
except Exception as exc: # pragma: no cover - env
|
| 1555 |
+
class _StandIn:
|
| 1556 |
+
table = {str(d): 100 + d for d in range(10)}
|
| 1557 |
+
def encode(self, text, add_special_tokens=False):
|
| 1558 |
+
return [self.table[text]] if text in self.table else [7, 7]
|
| 1559 |
+
tokenizer, source = _StandIn(), f"a stand-in ({type(exc).__name__})"
|
| 1560 |
+
|
| 1561 |
+
print("tokenizer:", source)
|
| 1562 |
+
print("digits we allow :", DIGITS)
|
| 1563 |
+
print("digit token ids :", digit_token_ids(tokenizer)[:10], "...")
|
| 1564 |
+
print("ids for a 4-item menu:", choice_token_ids(tokenizer, 4))
|
| 1565 |
+
yes_ids, no_ids = pair_token_ids(tokenizer)
|
| 1566 |
+
print("yes / no token ids :", yes_ids[:3], "/", no_ids[:3])
|
| 1567 |
+
mask = MenuMask(choice_token_ids(tokenizer, 4), eos_token_id=2)
|
| 1568 |
+
print("the mask allows :", mask.allowed_ids, "(everything else is -inf)")
|
| 1569 |
+
print()
|
| 1570 |
+
print("reading an answer, and refusing anything that is not one:")
|
| 1571 |
+
for reply in ["3", " 3 ", "1000", "The goal is to click Save", "3 or 4", ""]:
|
| 1572 |
+
print(f" read_choice({reply[:26]!r:30}) -> {read_choice(reply, 4)}")
|
| 1573 |
+
for reply in ["Yes", " no", "maybe", ""]:
|
| 1574 |
+
print(f" read_verdict({reply!r:9}) -> {read_verdict(reply)!r}")
|
| 1575 |
+
print()
|
| 1576 |
+
print("the retry ladder (echoing grows with prompt length):", brevity_ladder(3))
|
| 1577 |
+
print("a vote over samples:", majority_choice([2, 3, 2]), "| ties go to the first seen:",
|
| 1578 |
+
majority_choice([3, 2]))
|
| 1579 |
+
""")
|
| 1580 |
+
|
| 1581 |
+
# --- the accessibility half needs a real GTK window and a session bus --------
|
| 1582 |
+
_GTK = {"process": None}
|
| 1583 |
+
GTK_BOX = (140, 140, 660, 420)
|
| 1584 |
+
|
| 1585 |
+
|
| 1586 |
+
def open_gtk_scene():
|
| 1587 |
+
"""A session bus plus the GTK window, so AT-SPI has something to read."""
|
| 1588 |
+
if _GTK["process"] is not None:
|
| 1589 |
+
return
|
| 1590 |
+
address = subprocess.run(["dbus-daemon", "--session", "--fork", "--print-address=1"],
|
| 1591 |
+
capture_output=True, text=True).stdout.strip()
|
| 1592 |
+
os.environ["DBUS_SESSION_BUS_ADDRESS"] = address
|
| 1593 |
+
os.environ["NO_AT_BRIDGE"] = "0"
|
| 1594 |
+
ready = Path("/tmp/gallery-gtk.ready")
|
| 1595 |
+
ready.unlink(missing_ok=True)
|
| 1596 |
+
_GTK["process"] = subprocess.Popen(
|
| 1597 |
+
[PY, str(ROOT / "showcase_scenes" / "gtk_dialog.py"),
|
| 1598 |
+
"/tmp/gallery-gtk.marker", str(ready)],
|
| 1599 |
+
env=dict(os.environ), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 1600 |
+
for _ in range(60):
|
| 1601 |
+
if ready.exists():
|
| 1602 |
+
break
|
| 1603 |
+
time.sleep(0.25)
|
| 1604 |
+
time.sleep(2.5)
|
| 1605 |
+
|
| 1606 |
+
|
| 1607 |
+
def close_gtk_scene():
|
| 1608 |
+
process, _GTK["process"] = _GTK["process"], None
|
| 1609 |
+
if process is not None:
|
| 1610 |
+
process.terminate()
|
| 1611 |
+
try:
|
| 1612 |
+
process.wait(timeout=10)
|
| 1613 |
+
except subprocess.TimeoutExpired: # pragma: no cover
|
| 1614 |
+
process.kill()
|
| 1615 |
+
time.sleep(0.5)
|
| 1616 |
+
|
| 1617 |
+
|
| 1618 |
+
def _with_gtk(tmp=None):
|
| 1619 |
+
open_gtk_scene()
|
| 1620 |
+
return close_gtk_scene
|
| 1621 |
+
|
| 1622 |
+
|
| 1623 |
+
term("atspi-index", "Perception",
|
| 1624 |
+
"The accessibility tree: elements the OS already knows about",
|
| 1625 |
+
["AtspiDetector", "AtspiDetector.detect", "AtspiDetector.text", "AtspiReader",
|
| 1626 |
+
"AtspiReader.read", "AccessibleNode", "TreeReader", "TreeReader.read",
|
| 1627 |
+
"accessibility_available",
|
| 1628 |
+
"INTERACTIVE_ROLES", "TEXT_ROLES"],
|
| 1629 |
+
note="No pixels are read: GTK publishes every button's name and screen extents.",
|
| 1630 |
+
prepare=_with_gtk,
|
| 1631 |
+
code="""
|
| 1632 |
+
from tiny_computer_agent import (INTERACTIVE_ROLES, TEXT_ROLES, AtspiDetector, AtspiReader,
|
| 1633 |
+
accessibility_available)
|
| 1634 |
+
|
| 1635 |
+
available, detail = accessibility_available("atspi")
|
| 1636 |
+
print("accessibility_available('atspi') ->", available)
|
| 1637 |
+
print(" ", detail)
|
| 1638 |
+
print()
|
| 1639 |
+
print("interactive roles offered to a model:")
|
| 1640 |
+
print(" ", ", ".join(sorted(INTERACTIVE_ROLES)))
|
| 1641 |
+
print("text roles - readable, but not clickable:")
|
| 1642 |
+
print(" ", ", ".join(sorted(TEXT_ROLES)))
|
| 1643 |
+
print()
|
| 1644 |
+
|
| 1645 |
+
reader = AtspiReader()
|
| 1646 |
+
nodes = reader.read()
|
| 1647 |
+
print(f"AtspiReader().read() -> {len(nodes)} AccessibleNode(s) on the live desktop")
|
| 1648 |
+
print()
|
| 1649 |
+
|
| 1650 |
+
detector = AtspiDetector(reader=reader)
|
| 1651 |
+
elements = detector.detect()
|
| 1652 |
+
print(f"AtspiDetector().detect() -> {len(elements)} clickable elements, with no OCR at all:")
|
| 1653 |
+
for element in elements:
|
| 1654 |
+
print(f" {element.label!r:12} kind={element.kind:8} "
|
| 1655 |
+
f"centre={str(element.center):12} box={element.width}x{element.height}")
|
| 1656 |
+
print()
|
| 1657 |
+
print("the words on screen, straight from the tree:")
|
| 1658 |
+
print(" ", detector.text()[:150])
|
| 1659 |
+
""")
|
| 1660 |
+
|
| 1661 |
+
term("models-decision-modes", "Models",
|
| 1662 |
+
"Four ways to ask a tiny model, and what each one costs",
|
| 1663 |
+
["ModelBrain", "ModelBrain.loaded", "TextPlannerBrain", "VLMBrain", "make_model_brain"],
|
| 1664 |
+
note="json=free text, choice=one masked number, scan=yes/no per item, rank=log-odds order.",
|
| 1665 |
+
code="""
|
| 1666 |
+
from tiny_computer_agent import ModelBrain, make_model_brain
|
| 1667 |
+
from tiny_computer_agent.models import DEFAULT_TEXT_MODEL, DEFAULT_VISION_MODEL
|
| 1668 |
+
|
| 1669 |
+
print("decision_mode decides how the model is asked:")
|
| 1670 |
+
print(" json - reply with JSON, parsed leniently (the original behaviour)")
|
| 1671 |
+
print(" choice - one generation, masked to a menu number")
|
| 1672 |
+
print(" scan - one masked yes/no question per item, first yes wins")
|
| 1673 |
+
print(" rank - the same questions, ranked by the yes/no log-odds margin")
|
| 1674 |
+
print()
|
| 1675 |
+
for name in ("text-planner", "vlm"):
|
| 1676 |
+
brain = make_model_brain(name, decision_mode="scan")
|
| 1677 |
+
print(f"{name:13} -> {type(brain).__name__}")
|
| 1678 |
+
print(f" model : {brain.model_id}")
|
| 1679 |
+
print(f" supports masking : {brain.supports_constrained}")
|
| 1680 |
+
print(f" decision_mode : {brain.decision_mode} | retries {brain.retries} | votes {brain.votes}")
|
| 1681 |
+
print(f" weights loaded : {brain.loaded}")
|
| 1682 |
+
brain.unload()
|
| 1683 |
+
print()
|
| 1684 |
+
print("defaults are unchanged:", make_model_brain("vlm").decision_mode)
|
| 1685 |
+
""")
|
| 1686 |
+
|
| 1687 |
+
# --- the accessibility tree, on a real desktop, with a real click ------------
|
| 1688 |
+
def make_atspi_click(tmp):
|
| 1689 |
+
from tiny_computer_agent import (AgentLoop, AtspiDetector, GrabScreen, IndexedScreen,
|
| 1690 |
+
PlannerBrain, Policy, PyAutoGuiExecutor)
|
| 1691 |
+
open_gtk_scene()
|
| 1692 |
+
try:
|
| 1693 |
+
elements = AtspiDetector().detect()
|
| 1694 |
+
before = crop(annotate(grab(), elements), GTK_BOX)
|
| 1695 |
+
episode = AgentLoop(
|
| 1696 |
+
screen=IndexedScreen(GrabScreen(monitor=1), AtspiDetector()),
|
| 1697 |
+
brain=PlannerBrain(), executor=PyAutoGuiExecutor(),
|
| 1698 |
+
policy=Policy(dry_run=False, step_budget=3),
|
| 1699 |
+
).run("click 'Save' to save the document")
|
| 1700 |
+
target = next((e for e in elements if e.matches("Save")), None)
|
| 1701 |
+
after = crop(annotate(grab(), [], click=target.center if target else None), GTK_BOX)
|
| 1702 |
+
marker = Path("/tmp/gallery-gtk.marker")
|
| 1703 |
+
lines = [
|
| 1704 |
+
"the window's own status label now reads 'last action: Save',",
|
| 1705 |
+
"and it wrote " + (repr(marker.read_text()) if marker.exists() else "nothing")
|
| 1706 |
+
+ " to " + marker.name,
|
| 1707 |
+
"result: " + ("success" if episode.success else "failure")
|
| 1708 |
+
+ " - " + episode.stop_reason,
|
| 1709 |
+
]
|
| 1710 |
+
return overlay_lines(side_by_side(
|
| 1711 |
+
before, after,
|
| 1712 |
+
title="AT-SPI index -> a real click: not one pixel was read to find the button"), lines)
|
| 1713 |
+
finally:
|
| 1714 |
+
close_gtk_scene()
|
| 1715 |
+
|
| 1716 |
+
|
| 1717 |
+
desk("desk-atspi", "Real desktop",
|
| 1718 |
+
"Indexed by the accessibility tree, acted on with the real mouse",
|
| 1719 |
+
["AtspiDetector", "AtspiDetector.detect", "AtspiReader", "IndexedScreen",
|
| 1720 |
+
"PyAutoGuiExecutor", "PlannerBrain"],
|
| 1721 |
+
note="Roles and extents come from GTK itself; the click is a real one and the app reacts.",
|
| 1722 |
+
scene_name="gtk", make=make_atspi_click)
|