irregular6612 Claude Sonnet 4.6 commited on
Commit
a684d5c
·
1 Parent(s): 40ee460

feat(cp5): HumanAgent act path + WASD parsing + reprompt (I/O-injected)

Browse files
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