Spaces:
Runtime error
Runtime error
Commit ·
a684d5c
1
Parent(s): 40ee460
feat(cp5): HumanAgent act path + WASD parsing + reprompt (I/O-injected)
Browse files- proteus/agents/__init__.py +2 -1
- proteus/agents/human.py +83 -0
- tests/agents/test_human.py +60 -0
proteus/agents/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""proteus.agents — slim LLM agent abstraction (no forfeit/stake/risk)."""
|
| 2 |
|
| 3 |
from proteus.agents.base import Agent, ActResult, ProbeResult
|
|
|
|
| 4 |
from proteus.agents.vanilla import VanillaAgent
|
| 5 |
|
| 6 |
-
__all__ = ["Agent", "ActResult", "ProbeResult", "VanillaAgent"]
|
|
|
|
| 1 |
"""proteus.agents — slim LLM agent abstraction (no forfeit/stake/risk)."""
|
| 2 |
|
| 3 |
from proteus.agents.base import Agent, ActResult, ProbeResult
|
| 4 |
+
from proteus.agents.human import HumanAgent
|
| 5 |
from proteus.agents.vanilla import VanillaAgent
|
| 6 |
|
| 7 |
+
__all__ = ["Agent", "ActResult", "ProbeResult", "HumanAgent", "VanillaAgent"]
|
proteus/agents/human.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HumanAgent — a stdin-driven Agent so a person plays through the same SessionRunner.
|
| 2 |
+
|
| 3 |
+
Routing a human through the identical SessionRunner makes the resulting
|
| 4 |
+
SessionTrace schema-identical to an LLM trace, so the two compare directly
|
| 5 |
+
(the human-baseline foundation, spec §10). The agent is I/O-injected
|
| 6 |
+
(input_fn / output_fn) so human play is testable headlessly with a scripted
|
| 7 |
+
input stream — no TTY, no network.
|
| 8 |
+
|
| 9 |
+
Fairness note: the live view is the ASCII observation the LLM also sees; the
|
| 10 |
+
truecolor / PNG renderers are reserved for post-hoc replay (proteus.viz), so a
|
| 11 |
+
human baseline reads exactly what the model reads.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import builtins
|
| 17 |
+
from typing import Callable, Optional
|
| 18 |
+
|
| 19 |
+
from proteus.agents.base import Agent, ActResult, ProbeResult
|
| 20 |
+
|
| 21 |
+
# Single-key shortcuts -> canonical action strings (WASD).
|
| 22 |
+
_SHORTCUTS = {"w": "up", "a": "left", "s": "down", "d": "right"}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class HumanAgent(Agent):
|
| 26 |
+
"""An Agent driven by a person via injected input/output callables.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
input_fn: Reads one line from the human (defaults to builtins.input,
|
| 30 |
+
resolved lazily so tests can monkeypatch builtins.input).
|
| 31 |
+
output_fn: Shows a line to the human (defaults to builtins.print).
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(
|
| 35 |
+
self,
|
| 36 |
+
input_fn: Optional[Callable[[str], str]] = None,
|
| 37 |
+
output_fn: Optional[Callable[[str], None]] = None,
|
| 38 |
+
) -> None:
|
| 39 |
+
# Resolve builtins at construction time (not as default-arg values, which
|
| 40 |
+
# would capture the pre-monkeypatch references at import time).
|
| 41 |
+
self._input = input_fn if input_fn is not None else builtins.input
|
| 42 |
+
self._output = output_fn if output_fn is not None else builtins.print
|
| 43 |
+
|
| 44 |
+
@property
|
| 45 |
+
def name(self) -> str:
|
| 46 |
+
return "human"
|
| 47 |
+
|
| 48 |
+
def act(
|
| 49 |
+
self,
|
| 50 |
+
observation: str,
|
| 51 |
+
available_actions: list[str],
|
| 52 |
+
system_prompt: str,
|
| 53 |
+
) -> ActResult:
|
| 54 |
+
self._output(observation)
|
| 55 |
+
action, raw = self._read_action(available_actions)
|
| 56 |
+
return ActResult(action=action, reasoning="", raw_text=raw)
|
| 57 |
+
|
| 58 |
+
def probe(
|
| 59 |
+
self,
|
| 60 |
+
observation: str,
|
| 61 |
+
question: str,
|
| 62 |
+
system_prompt: str,
|
| 63 |
+
) -> ProbeResult:
|
| 64 |
+
# Implemented in Task 2.
|
| 65 |
+
raise NotImplementedError
|
| 66 |
+
|
| 67 |
+
def reset(self) -> None:
|
| 68 |
+
return None
|
| 69 |
+
|
| 70 |
+
def _read_action(self, available_actions: list[str]) -> tuple[str, str]:
|
| 71 |
+
"""Prompt until a valid action is entered; return (action, raw_input)."""
|
| 72 |
+
valid = {a.lower() for a in available_actions}
|
| 73 |
+
prompt = f"action [{'/'.join(available_actions)} | wasd]> "
|
| 74 |
+
while True:
|
| 75 |
+
raw = self._input(prompt).strip()
|
| 76 |
+
token = raw.lower()
|
| 77 |
+
action = _SHORTCUTS.get(token, token)
|
| 78 |
+
if action in valid:
|
| 79 |
+
return action, raw
|
| 80 |
+
self._output(
|
| 81 |
+
f"invalid input {raw!r}; choose one of "
|
| 82 |
+
f"{', '.join(available_actions)} (or wasd)."
|
| 83 |
+
)
|
tests/agents/test_human.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from proteus.agents import HumanAgent
|
| 2 |
+
from proteus.agents.base import ActResult
|
| 3 |
+
|
| 4 |
+
_ACTIONS = ["up", "down", "left", "right", "stay"]
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _scripted(seq):
|
| 8 |
+
"""Return an input_fn that yields the given strings in order."""
|
| 9 |
+
it = iter(seq)
|
| 10 |
+
return lambda prompt="": next(it)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_act_parses_plain_action():
|
| 14 |
+
out = []
|
| 15 |
+
agent = HumanAgent(input_fn=_scripted(["up"]), output_fn=out.append)
|
| 16 |
+
result = agent.act("OBSERVATION", _ACTIONS, "SYSTEM")
|
| 17 |
+
assert isinstance(result, ActResult)
|
| 18 |
+
assert result.action == "up"
|
| 19 |
+
assert result.reasoning == ""
|
| 20 |
+
assert result.raw_text == "up"
|
| 21 |
+
assert result.input_tokens == 0
|
| 22 |
+
assert result.output_tokens == 0
|
| 23 |
+
assert result.thinking_tokens == 0
|
| 24 |
+
# The observation was shown to the human.
|
| 25 |
+
assert any("OBSERVATION" in line for line in out)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_act_maps_wasd_shortcuts():
|
| 29 |
+
agent = HumanAgent(input_fn=_scripted(["w"]), output_fn=lambda s: None)
|
| 30 |
+
assert agent.act("o", _ACTIONS, "s").action == "up"
|
| 31 |
+
agent = HumanAgent(input_fn=_scripted(["a"]), output_fn=lambda s: None)
|
| 32 |
+
assert agent.act("o", _ACTIONS, "s").action == "left"
|
| 33 |
+
agent = HumanAgent(input_fn=_scripted(["s"]), output_fn=lambda s: None)
|
| 34 |
+
assert agent.act("o", _ACTIONS, "s").action == "down"
|
| 35 |
+
agent = HumanAgent(input_fn=_scripted(["d"]), output_fn=lambda s: None)
|
| 36 |
+
assert agent.act("o", _ACTIONS, "s").action == "right"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_act_is_case_and_whitespace_insensitive():
|
| 40 |
+
agent = HumanAgent(input_fn=_scripted([" UP "]), output_fn=lambda s: None)
|
| 41 |
+
result = agent.act("o", _ACTIONS, "s")
|
| 42 |
+
assert result.action == "up"
|
| 43 |
+
assert result.raw_text == "UP" # stripped, original casing preserved
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_act_reprompts_on_invalid_then_accepts():
|
| 47 |
+
out = []
|
| 48 |
+
agent = HumanAgent(input_fn=_scripted(["nope", "diagonal", "right"]), output_fn=out.append)
|
| 49 |
+
result = agent.act("o", _ACTIONS, "s")
|
| 50 |
+
assert result.action == "right"
|
| 51 |
+
# Two invalid inputs produced two guidance messages.
|
| 52 |
+
assert sum("invalid" in line for line in out) == 2
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_name_is_human():
|
| 56 |
+
assert HumanAgent().name == "human"
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def test_reset_is_noop():
|
| 60 |
+
assert HumanAgent().reset() is None
|