hkblockchaintechnology's picture
Publish tiny_computer_agent/ from marimo
e187916 verified
Raw History Blame Contribute Delete
4.97 kB
"""The closed action vocabulary.
A tiny model is only dependable when its output space is small and every mistake
is cheap to catch. The harness therefore accepts exactly six verbs, and validates
each field *before* anything touches the machine.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Final, Mapping
ACTION_KINDS: Final[tuple[str, ...]] = ("click", "type", "key", "scroll", "wait", "done")
class ActionError(ValueError):
"""Raised when a brain proposes something outside the vocabulary."""
@dataclass(frozen=True)
class Action:
"""One step of computer use: exactly one verb, validated on construction.
label : click target *by element label* — the preferred form, because it
survives window moves that would break raw pixel coordinates
x, y : pixel fallback, used only when no label matches
text : literal text to type
keys : key chord, e.g. ``ctrl+s``
amount : scroll ticks; negative scrolls up
seconds : how long to wait
reason : the brain's stated rationale — kept for traces, never executed
"""
kind: str
label: str | None = None
x: int | None = None
y: int | None = None
text: str | None = None
keys: str | None = None
amount: int = 0
seconds: float = 0.0
reason: str = ""
def __post_init__(self) -> None:
if self.kind not in ACTION_KINDS:
raise ActionError(f"unknown action {self.kind!r}; expected one of {ACTION_KINDS}")
if self.kind == "click":
if not self.label and not (self.x is not None and self.y is not None):
raise ActionError("click needs a label, or x and y")
if (self.x is not None and self.x < 0) or (self.y is not None and self.y < 0):
raise ActionError("click coordinates must be non-negative")
if self.kind == "type" and not self.text:
raise ActionError("type needs non-empty text")
if self.kind == "key" and not self.keys:
raise ActionError("key needs a chord, e.g. 'ctrl+s'")
if self.kind == "scroll" and self.amount == 0:
raise ActionError("scroll needs a non-zero amount")
if self.kind == "wait" and self.seconds <= 0:
raise ActionError("wait needs a positive number of seconds")
def describe(self) -> str:
"""One-line, human-readable form used in traces and dry-run logs."""
parts: list[str] = [self.kind]
if self.label:
parts.append(f"label={self.label!r}")
if self.x is not None and self.y is not None:
parts.append(f"at=({self.x},{self.y})")
if self.text is not None:
parts.append(f"text={self.text!r}")
if self.keys:
parts.append(f"keys={self.keys!r}")
if self.kind == "scroll":
parts.append(f"amount={self.amount}")
if self.kind == "wait":
parts.append(f"seconds={self.seconds}")
if self.reason:
parts.append(f"reason={self.reason!r}")
return " ".join(parts)
def to_dict(self) -> dict[str, Any]:
return {
"kind": self.kind,
"label": self.label,
"x": self.x,
"y": self.y,
"text": self.text,
"keys": self.keys,
"amount": self.amount,
"seconds": self.seconds,
"reason": self.reason,
}
ACTION_MENU: Final[str] = """\
Reply with exactly one JSON object and nothing else. Allowed verbs:
{"kind": "click", "label": "<text of the element to click>", "reason": "..."}
{"kind": "click", "x": 640, "y": 400, "reason": "..."} // pixels, only if no label fits
{"kind": "type", "text": "...", "reason": "..."}
{"kind": "key", "keys": "ctrl+s", "reason": "..."}
{"kind": "scroll", "amount": -3, "reason": "..."}
{"kind": "wait", "seconds": 1.5, "reason": "..."}
{"kind": "done", "reason": "why the goal is met"}
Rules: one action per reply; prefer a label over guessing pixels; reply "done"
as soon as the goal is met; never invent new verbs."""
_FIELDS: Final[tuple[str, ...]] = (
"label", "x", "y", "text", "keys", "amount", "seconds", "reason",
)
def parse_action(payload: Mapping[str, Any]) -> Action:
"""Turn a brain's raw mapping into a validated :class:`Action`.
Unknown keys are ignored, so a chatty model cannot crash the loop. An unknown
verb or a missing field raises :class:`ActionError`, which the loop records as
a failed step *without executing anything*.
"""
if not isinstance(payload, Mapping):
raise ActionError(f"expected a mapping, got {type(payload).__name__}")
kind = payload.get("kind")
if kind not in ACTION_KINDS:
raise ActionError(f"unknown action {kind!r}; expected one of {ACTION_KINDS}")
kwargs = {k: payload[k] for k in _FIELDS if k in payload}
return Action(kind=str(kind), **kwargs)