Download tiny_computer_agent/gates.py from hkblockchaintechnology/tiny-computer-agent: direct link, hf CLI and curl.
- Browser
- Download file 7.44 kB
-
https://huggingface.co/hkblockchaintechnology/tiny-computer-agent/resolve/main/tiny_computer_agent/gates.py
- Command line
-
hf download hf://hkblockchaintechnology/tiny-computer-agent/tiny_computer_agent/gates.py
-
curl -L -o gates.py https://huggingface.co/hkblockchaintechnology/tiny-computer-agent/resolve/main/tiny_computer_agent/gates.py
7.44 kB
| """The operator's yes, as a first-class part of the loop. | |
| A :class:`~tiny_computer_agent.safety.Policy` answers "may the harness do this at all?". | |
| A gate answers the other question: "does the person watching want it done *now*?" Both end | |
| in the same place - a refusal that is recorded as a step and stops the episode, never a | |
| traceback and never a silent click. | |
| loop = AgentLoop(screen=..., brain=..., gate=InteractiveGate(), policy=Policy(dry_run=False)) | |
| loop = AgentLoop(..., gate=AllowOnlyLabels(["Save", "Cancel"])) # click nothing else | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from typing import Any, Callable, Protocol, Sequence | |
| from .actions import Action | |
| from .observe import Observation, normalise_label | |
| READ_ONLY_KINDS = ("wait", "done") | |
| """Verbs that cannot change the machine, and so never need an operator's consent.""" | |
| def mutating(action: Action) -> bool: | |
| """True when this action would change something on the machine.""" | |
| return action.kind not in READ_ONLY_KINDS | |
| class Approval: | |
| """A gate's answer, with the reason it will be recorded under.""" | |
| approved: bool | |
| reason: str = "" | |
| def line(self) -> str: | |
| return f"{'approved' if self.approved else 'refused'}: {self.reason}" | |
| class Gate(Protocol): | |
| """Ask before acting. Return an :class:`Approval`; never raise to say no.""" | |
| def approve(self, action: Action, observation: Observation | None = None) -> Approval: | |
| ... | |
| class AllowAll: | |
| """No gate: everything policy already allowed is approved. The default when none is given.""" | |
| reason: str = "no gate configured" | |
| def approve(self, action: Action, observation: Observation | None = None) -> Approval: | |
| return Approval(True, self.reason) | |
| class DenyAll: | |
| """Refuse every action that would change anything - a one-line kill switch for a live loop.""" | |
| reason: str = "the operator said no" | |
| def approve(self, action: Action, observation: Observation | None = None) -> Approval: | |
| if not mutating(action): | |
| return Approval(True, "read-only") | |
| return Approval(False, self.reason) | |
| class AllowOnlyLabels: | |
| """A whitelist of labels: the loop may click these and nothing else. | |
| This is the gate for "watch it work, but it may only touch Save and Cancel" - the | |
| coordinates still come from the index, so a renamed button is refused rather than | |
| clicked at. | |
| """ | |
| labels: Sequence[str] = () | |
| reason: str = "not in the operator's whitelist" | |
| def approve(self, action: Action, observation: Observation | None = None) -> Approval: | |
| if not mutating(action): | |
| return Approval(True, "read-only") | |
| if action.label and self.matches(action.label): | |
| return Approval(True, f"{action.label!r} is whitelisted") | |
| return Approval(False, self.reason) | |
| def matches(self, label: str) -> bool: | |
| target = normalise_label(label) | |
| for allowed in self.labels: | |
| wanted = normalise_label(allowed) | |
| if target and (target == wanted or wanted in target or target in wanted): | |
| return True | |
| return False | |
| class InteractiveGate: | |
| """Ask a person, once per action, and remember what they said. | |
| ``always`` and ``never`` persist for the rest of the run, so a supervised session does not | |
| turn into a keyboard exercise. ``reader``/``writer`` are injectable, so the gate is testable | |
| and a console can route the question through its own widgets. | |
| """ | |
| prompt: str = "run this action?" | |
| reader: Callable[[str], str] = input | |
| writer: Callable[[str], None] = print | |
| _always: bool = field(default=False, init=False, repr=False) | |
| _never: bool = field(default=False, init=False, repr=False) | |
| asked: int = field(default=0, init=False) | |
| def approve(self, action: Action, observation: Observation | None = None) -> Approval: | |
| if not mutating(action): | |
| return Approval(True, "read-only") | |
| if self._never: | |
| return Approval(False, "the operator said never") | |
| if self._always: | |
| return Approval(True, "the operator said always") | |
| self.asked += 1 | |
| question = f"{self.prompt} {action.describe()} [y]es / [n]o / [a]lways / [v]never: " | |
| try: | |
| answer = (self.reader(question) or "").strip().lower() | |
| except (EOFError, KeyboardInterrupt): | |
| return Approval(False, "the operator was not there to answer") | |
| if answer.startswith("a"): | |
| self._always = True | |
| return Approval(True, "the operator said always") | |
| if answer.startswith("v"): | |
| self._never = True | |
| return Approval(False, "the operator said never") | |
| if answer.startswith("y"): | |
| return Approval(True, "the operator said yes") | |
| return Approval(False, "the operator said no") | |
| class AllOf: | |
| """Every gate must approve - the strictest reading of a set of rules.""" | |
| gates: Sequence[Gate] = () | |
| def approve(self, action: Action, observation: Observation | None = None) -> Approval: | |
| for gate in self.gates: | |
| approval = gate.approve(action, observation) | |
| if not approval.approved: | |
| return approval | |
| return Approval(True, "every gate approved") | |
| class AnyOf: | |
| """Any gate may approve - the loosest, for "my colleague or my rulebook".""" | |
| gates: Sequence[Gate] = () | |
| reason: str = "no gate approved" | |
| def approve(self, action: Action, observation: Observation | None = None) -> Approval: | |
| for gate in self.gates: | |
| approval = gate.approve(action, observation) | |
| if approval.approved: | |
| return approval | |
| return Approval(False, self.reason) | |
| GATE_SPECS = ("allow-all", "read-only", "interactive", "labels:") | |
| """The spellings :func:`gate_from_spec` accepts, in the order its error lists them.""" | |
| def gate_from_spec(spec: str) -> Gate: | |
| """Build a gate from a one-line spelling, the one the command line takes. | |
| A gate is the operator's yes, and an operator should not have to write Python to give | |
| one:: | |
| allow-all everything policy already allowed | |
| read-only nothing that changes the machine, whatever policy says | |
| interactive ask me, once per action, and remember "always"/"never" | |
| labels:Save,Cancel only these labels, and nothing else | |
| An unknown spec raises ``ValueError`` naming the accepted spellings, because a typo in | |
| a safety switch must never quietly mean "allow everything". | |
| """ | |
| wanted = (spec or "").strip().lower() | |
| if wanted in ("allow-all", "allow", "none"): | |
| return AllowAll() | |
| if wanted in ("read-only", "readonly"): | |
| return DenyAll(reason="read-only gate: nothing may change the machine") | |
| if wanted in ("interactive", "ask"): | |
| return InteractiveGate() | |
| if wanted.startswith("labels:"): | |
| labels = [part.strip() for part in spec.split(":", 1)[1].split(",") if part.strip()] | |
| if not labels: | |
| raise ValueError("labels: needs at least one label, e.g. labels:Save,Cancel") | |
| return AllowOnlyLabels(tuple(labels)) | |
| accepted = " | ".join(GATE_SPECS) | |
| raise ValueError(f"unknown gate {spec!r}: expected one of {accepted}") | |