refactor: renamed agents to tools

This commit is contained in:
Daniel Eder 2025-09-28 10:31:42 +02:00
parent 9b8584561e
commit ca493b8551
5 changed files with 126 additions and 121 deletions

View file

@ -1,14 +1,14 @@
# Mistle Mudbot # Mistle Mudbot
Python-based Telnet helper for connecting to MUD servers, handling login flows, and optionally running automated "agents" alongside interactive play. Python-based Telnet helper for connecting to MUD servers, handling login flows, and optionally running automated "tools" alongside interactive play.
## Features ## Features
- Lightweight wrapper (`TelnetClient`) around `telnetlib` with sane defaults and context-manager support. - Lightweight wrapper (`TelnetClient`) around `telnetlib` with sane defaults and context-manager support.
- Loads credentials and connection settings from a local `.env` file. - Loads credentials and connection settings from a local `.env` file.
- Interactive console session that mirrors server output and lets you type commands directly. - Interactive console session that mirrors server output and lets you type commands directly.
- Optional always-on agent mode plus an on-demand `#execute <agent>` escape hatch for ad-hoc automations. - Optional always-on tool mode plus an on-demand `#execute <tool>` escape hatch for ad-hoc automations.
- Built-in agents (`SimpleAgent`, `ExploreAgent`, `CommunicationAgent`, `MovementAgent`, `IntelligentCommunicationAgent`) with a pluggable interface for custom behaviours. - Built-in tools (`SimpleTool`, `ExploreTool`, `CommunicationTool`, `MovementTool`, `IntelligentCommunicationTool`) with a pluggable interface for custom behaviours.
## Requirements ## Requirements
@ -33,13 +33,13 @@ Python-based Telnet helper for connecting to MUD servers, handling login flows,
4. Type commands directly into the console. Press `Ctrl-C` to exit; the client will send any configured shutdown command to the MUD. 4. Type commands directly into the console. Press `Ctrl-C` to exit; the client will send any configured shutdown command to the MUD.
5. When *not* running in agent mode you can kick off one-off automations from the prompt: 5. When *not* running in tool mode you can kick off one-off automations from the prompt:
```text ```text
#execute explore #execute explore
``` ```
The command remains interactive while the agent works in the background and stops automatically a few seconds after things quiet down. The command remains interactive while the tool works in the background and stops automatically a few seconds after things quiet down.
## Environment Variables ## Environment Variables
@ -53,37 +53,37 @@ All variables can be placed in the `.env` file (one `KEY=value` per line) or pro
| `MISTLE_PASSWORD` | ❌ | Password sent after the username. Leave blank for manual entry. | | `MISTLE_PASSWORD` | ❌ | Password sent after the username. Leave blank for manual entry. |
| `MISTLE_LOGIN_PROMPT` | ❌ | Prompt string that signals the client to send credentials (e.g., `"Name:"`). When omitted, the client just waits for the initial banner. | | `MISTLE_LOGIN_PROMPT` | ❌ | Prompt string that signals the client to send credentials (e.g., `"Name:"`). When omitted, the client just waits for the initial banner. |
| `MISTLE_EXIT_COMMAND` | ❌ | Command issued during graceful shutdown (after pressing `Ctrl-C`). Useful for `quit`/`save` macros. | | `MISTLE_EXIT_COMMAND` | ❌ | Command issued during graceful shutdown (after pressing `Ctrl-C`). Useful for `quit`/`save` macros. |
| `MISTLE_AGENT_MODE` | ❌ | Enable full-time agent thread when set to truthy values (`1`, `true`, `yes`, `on`). Defaults to interactive-only mode. | | `MISTLE_TOOL_MODE` | ❌ | Enable full-time tool thread when set to truthy values (`1`, `true`, `yes`, `on`). Defaults to interactive-only mode. (`MISTLE_AGENT_MODE` still works as a backwards-compatible alias.) |
| `MISTLE_AGENT` | ❌ | Select which agent class to instantiate when agent mode is active. Accepted values: `simple` (default), `explore`, `communication`, `movement`, `intelligent`/`intelligentcommunication` (LLM-backed), or custom spec `module:ClassName`. | | `MISTLE_TOOL` | ❌ | Select which tool class to instantiate when tool mode is active. Accepted values: `simple` (default), `explore`, `communication`, `movement`, `intelligent`/`intelligentcommunication` (LLM-backed), or custom spec `module:ClassName`. (`MISTLE_AGENT` remains as a legacy alias.) |
| `MISTLE_LLM_MODEL` | ❌ | Override the `litellm` model used by the intelligent agent (defaults to `mistral/mistral-small-2407`). | | `MISTLE_LLM_MODEL` | ❌ | Override the `litellm` model used by the intelligent tool (defaults to `mistral/mistral-small-2407`). |
| `MISTRAL_API_KEY` | ❌ | API key used by `IntelligentCommunicationAgent` (via `litellm`) when calling the `mistral/mistral-small-2407` model. | | `MISTRAL_API_KEY` | ❌ | API key used by `IntelligentCommunicationTool` (via `litellm`) when calling the `mistral/mistral-small-2407` model. |
## Agent Development ## Tool Development
- Implement new agents by subclassing `agent.Agent` and overriding `observe()` and `decide()`. - Implement new tools by subclassing `tools.Tool` and overriding `observe()` and `decide()`.
- Register the agent by either: - Register the tool by either:
- Adding the class to `agent.py` and referencing it in `MISTLE_AGENT` (e.g., `explore` for `ExploreAgent`). - Adding the class to `tools.py` and referencing it in `MISTLE_TOOL` (e.g., `explore` for `ExploreTool`).
- Placing the class elsewhere and configuring `MISTLE_AGENT` to `your_module:YourAgent`. - Placing the class elsewhere and configuring `MISTLE_TOOL` to `your_module:YourTool`.
- `observe(output)` receives the latest server text; `decide()` returns the next command string or `None` to stay idle. - `observe(output)` receives the latest server text; `decide()` returns the next command string or `None` to stay idle.
- Commands issued by the agent are throttled to one per second so manual commands can still interleave smoothly. - Commands issued by the tool are throttled to one per second so manual commands can still interleave smoothly.
- `ExploreAgent` showcases a richer workflow: it sends `schau`, identifies German nouns, inspects each with `untersuche`, and prints `[Agent]` progress updates like `Explored 3/7 — untersuche Tisch`. - `ExploreTool` showcases a richer workflow: it sends `schau`, identifies German nouns, inspects each with `untersuche`, and prints `[Tool]` progress updates like `Explored 3/7 — untersuche Tisch`.
- `MovementAgent` parses room descriptions/exits and issues direction commands (prefers `n`, `e`, `s`, `w`, diagonals, then vertical moves) while tracking which exits were already tried. Use `#execute move` for ad-hoc pathing or set `MISTLE_AGENT=movement` for continuous roaming. - `MovementTool` parses room descriptions/exits and issues a single direction command, preferring unvisited exits and randomising choices to avoid oscillation. Trigger it via `#execute move` (or set `MISTLE_TOOL=movement` for continuous roaming).
- `CommunicationAgent` auto-replies to every direct tell with a canned greeting, while `IntelligentCommunicationAgent` routes each tell through `litellm` (default model `mistral/mistral-small-2407`) to craft a contextual answer. - `CommunicationTool` auto-replies to every direct tell with a canned greeting, while `IntelligentCommunicationTool` routes each tell through `litellm` (default model `mistral/mistral-small-2407`) to craft a contextual answer via the configured LLM.
## On-Demand Agents ## On-Demand Tools
- When `MISTLE_AGENT_MODE` is **off**, you can trigger an ephemeral agent at any time with `#execute <agent_spec>`. - When `MISTLE_TOOL_MODE` is **off**, you can trigger an ephemeral tool at any time with `#execute <tool_spec>`.
- The syntax accepts the same values as `MISTLE_AGENT` and reuses the `build_agent` helper, so `#execute simple`, `#execute explore`, `#execute move`, `#execute intelligent`, or `#execute mypackage.mymodule:CustomAgent` are all valid. - The syntax accepts the same values as `MISTLE_TOOL` and reuses the `build_tool` helper, so `#execute simple`, `#execute explore`, `#execute move`, `#execute intelligent`, or `#execute mypackage.mymodule:CustomTool` are all valid.
- On-demand runs share the current session, respect the one-command-per-second limit, and stop automatically after a few seconds of inactivity. - On-demand runs share the current session, respect the one-command-per-second limit, and stop automatically after a few seconds of inactivity.
## Danger Zone ## Danger Zone
- The Telnet session runs until you interrupt it. Make sure the terminal is in a state where `Ctrl-C` is available. - The Telnet session runs until you interrupt it. Make sure the terminal is in a state where `Ctrl-C` is available.
- When adding new agents, guard any long-running logic to avoid blocking the agent thread. - When adding new tools, guard any long-running logic to avoid blocking the tool thread.
## Contributing ## Contributing
Feel free to open issues or submit pull requests for additional MUD-specific helpers, new agents, or quality-of-life improvements. Feel free to open issues or submit pull requests for additional MUD-specific helpers, new tools, or quality-of-life improvements.
--- ---

152
app.py
View file

@ -7,7 +7,7 @@ from pathlib import Path
from threading import Event, Lock, Thread from threading import Event, Lock, Thread
from typing import Callable, Optional, Type from typing import Callable, Optional, Type
from agent import Agent, SimpleAgent from tools import Tool, SimpleTool
from telnetclient import TelnetClient from telnetclient import TelnetClient
@ -19,13 +19,13 @@ class SessionState:
self._output_lock = Lock() self._output_lock = Lock()
self._output_event = Event() self._output_event = Event()
self._last_output = "" self._last_output = ""
self._last_agent_send = 0.0 self._last_tool_send = 0.0
def send(self, client: TelnetClient, message: str) -> None: def send(self, client: TelnetClient, message: str) -> None:
with self._send_lock: with self._send_lock:
client.send(message) client.send(message)
def agent_send( def tool_send(
self, self,
client: TelnetClient, client: TelnetClient,
message: str, message: str,
@ -33,14 +33,14 @@ class SessionState:
min_interval: float, min_interval: float,
stop_event: Event, stop_event: Event,
) -> bool: ) -> bool:
"""Send on behalf of the agent while respecting a minimum cadence.""" """Send on behalf of the tool while respecting a minimum cadence."""
while not stop_event.is_set(): while not stop_event.is_set():
with self._send_lock: with self._send_lock:
now = time.time() now = time.time()
elapsed = now - self._last_agent_send elapsed = now - self._last_tool_send
if elapsed >= min_interval: if elapsed >= min_interval:
client.send(message) client.send(message)
self._last_agent_send = now self._last_tool_send = now
return True return True
wait_time = min_interval - elapsed wait_time = min_interval - elapsed
if wait_time <= 0: if wait_time <= 0:
@ -67,10 +67,10 @@ class SessionState:
self._output_event.clear() self._output_event.clear()
def run_agent_loop( def run_tool_loop(
client: TelnetClient, client: TelnetClient,
state: SessionState, state: SessionState,
agent: Agent, tool: Tool,
stop_event: Event, stop_event: Event,
*, *,
idle_delay: float = 0.5, idle_delay: float = 0.5,
@ -78,19 +78,19 @@ def run_agent_loop(
auto_stop: bool = False, auto_stop: bool = False,
auto_stop_idle: float = 2.0, auto_stop_idle: float = 2.0,
) -> None: ) -> None:
"""Invoke *agent* whenever new output arrives and send its response.""" """Invoke *tool* whenever new output arrives and send its response."""
idle_started: Optional[float] = None idle_started: Optional[float] = None
def maybe_send() -> None: def maybe_send() -> None:
nonlocal idle_started nonlocal idle_started
try: try:
command = agent.decide() command = tool.decide()
except Exception as exc: # pragma: no cover - defensive logging except Exception as exc: # pragma: no cover - defensive logging
print(f"Agent failed: {exc}", file=sys.stderr) print(f"[Tool] Failed: {exc}", file=sys.stderr)
return return
if not command: if not command:
return return
sent = state.agent_send( sent = state.tool_send(
client, client,
command, command,
min_interval=min_send_interval, min_interval=min_send_interval,
@ -120,9 +120,9 @@ def run_agent_loop(
if not last_output: if not last_output:
continue continue
try: try:
agent.observe(last_output) tool.observe(last_output)
except Exception as exc: # pragma: no cover - defensive logging except Exception as exc: # pragma: no cover - defensive logging
print(f"Agent failed during observe: {exc}", file=sys.stderr) print(f"[Tool] Failed during observe: {exc}", file=sys.stderr)
continue continue
maybe_send() maybe_send()
@ -152,67 +152,67 @@ def require_env(key: str) -> str:
return value return value
def build_agent(agent_spec: str) -> Agent: def build_tool(spec: str) -> Tool:
"""Instantiate an agent based on ``MISTLE_AGENT`` contents.""" """Instantiate a tool based on configuration."""
normalized = agent_spec.strip() normalized = spec.strip()
if not normalized: if not normalized:
return SimpleAgent() return SimpleTool()
key = normalized.lower() key = normalized.lower()
if key == "simple": if key == "simple":
return SimpleAgent() return SimpleTool()
builtin_agents = { builtin_tools = {
"explore": ("agent", "ExploreAgent", {}), "explore": ("tools", "ExploreTool", {}),
"communication": ("agent", "CommunicationAgent", {}), "communication": ("tools", "CommunicationTool", {}),
"movement": ("movement_agent", "MovementAgent", {}), "movement": ("movement_tool", "MovementTool", {}),
"move": ("movement_agent", "MovementAgent", {}), "move": ("movement_tool", "MovementTool", {}),
"intelligent": ( "intelligent": (
"intelligent_agent", "intelligent_tool",
"IntelligentCommunicationAgent", "IntelligentCommunicationTool",
{"model": os.environ.get("MISTLE_LLM_MODEL", "mistral/mistral-small-2407")}, {"model": os.environ.get("MISTLE_LLM_MODEL", "mistral/mistral-small-2407")},
), ),
"intelligentcommunication": ( "intelligentcommunication": (
"intelligent_agent", "intelligent_tool",
"IntelligentCommunicationAgent", "IntelligentCommunicationTool",
{"model": os.environ.get("MISTLE_LLM_MODEL", "mistral/mistral-small-2407")}, {"model": os.environ.get("MISTLE_LLM_MODEL", "mistral/mistral-small-2407")},
), ),
} }
if key in builtin_agents: if key in builtin_tools:
module_name, class_name, kwargs = builtin_agents[key] module_name, class_name, kwargs = builtin_tools[key]
try: try:
module = import_module(module_name) module = import_module(module_name)
agent_cls = getattr(module, class_name) tool_cls = getattr(module, class_name)
except AttributeError as exc: # pragma: no cover - optional dependency except AttributeError as exc: # pragma: no cover - optional dependency
raise RuntimeError(f"{class_name} is not available in agent module") from exc raise RuntimeError(f"{class_name} is not available in tools module") from exc
agent = _instantiate_agent(agent_cls, normalized, kwargs) tool = _instantiate_tool(tool_cls, normalized, kwargs)
model_name = kwargs.get("model") if kwargs else None model_name = kwargs.get("model") if kwargs else None
if model_name: if model_name:
print(f"[Agent] Using LLM model: {model_name}") print(f"[Tool] Using LLM model: {model_name}")
return agent return tool
if ":" in normalized: if ":" in normalized:
module_name, class_name = normalized.split(":", 1) module_name, class_name = normalized.split(":", 1)
if not module_name or not class_name: if not module_name or not class_name:
raise RuntimeError("MISTLE_AGENT must be in 'module:ClassName' format") raise RuntimeError("MISTLE_TOOL must be in 'module:ClassName' format")
module = import_module(module_name) module = import_module(module_name)
agent_cls = getattr(module, class_name) tool_cls = getattr(module, class_name)
return _instantiate_agent(agent_cls, normalized) return _instantiate_tool(tool_cls, normalized)
raise RuntimeError(f"Unknown agent spec '{agent_spec}'.") raise RuntimeError(f"Unknown tool spec '{spec}'.")
def _instantiate_agent( def _instantiate_tool(
agent_cls: Type[Agent], agent_spec: str, kwargs: Optional[dict] = None tool_cls: Type[Tool], tool_spec: str, kwargs: Optional[dict] = None
) -> Agent: ) -> Tool:
if not issubclass(agent_cls, Agent): if not issubclass(tool_cls, Tool):
raise RuntimeError(f"{agent_spec} is not an Agent subclass") raise RuntimeError(f"{tool_spec} is not a Tool subclass")
try: try:
kwargs = kwargs or {} kwargs = kwargs or {}
return agent_cls(**kwargs) return tool_cls(**kwargs)
except TypeError as exc: except TypeError as exc:
raise RuntimeError(f"Failed to instantiate {agent_spec}: {exc}") from exc raise RuntimeError(f"Failed to instantiate {tool_spec}: {exc}") from exc
def login( def login(
@ -256,7 +256,7 @@ def interactive_session(
poll_interval: float = 0.2, poll_interval: float = 0.2,
receive_timeout: float = 0.2, receive_timeout: float = 0.2,
exit_command: str, exit_command: str,
agent_command: Optional[Callable[[str], None]] = None, tool_command: Optional[Callable[[str], None]] = None,
) -> None: ) -> None:
"""Keep the Telnet session running, proxying input/output until interrupted.""" """Keep the Telnet session running, proxying input/output until interrupted."""
if exit_command: if exit_command:
@ -279,12 +279,12 @@ def interactive_session(
line = line.rstrip("\r\n") line = line.rstrip("\r\n")
if not line: if not line:
continue continue
if agent_command and line.lower().startswith("#execute"): if tool_command and line.lower().startswith("#execute"):
parts = line.split(maxsplit=1) parts = line.split(maxsplit=1)
if len(parts) == 1: if len(parts) == 1:
print("[Agent] Usage: #execute <agent_spec>") print("[Tool] Usage: #execute <tool_spec>")
else: else:
agent_command(parts[1]) tool_command(parts[1])
continue continue
state.send(client, line) state.send(client, line)
@ -326,14 +326,20 @@ def main() -> int:
password = os.environ.get("MISTLE_PASSWORD", "") password = os.environ.get("MISTLE_PASSWORD", "")
login_prompt = os.environ.get("MISTLE_LOGIN_PROMPT", "") login_prompt = os.environ.get("MISTLE_LOGIN_PROMPT", "")
exit_command = os.environ.get("MISTLE_EXIT_COMMAND", "") exit_command = os.environ.get("MISTLE_EXIT_COMMAND", "")
agent_mode = os.environ.get("MISTLE_AGENT_MODE", "").lower() in {"1", "true", "yes", "on"} tool_mode_env = os.environ.get("MISTLE_TOOL_MODE")
agent_spec = os.environ.get("MISTLE_AGENT", "") if tool_mode_env is None:
tool_mode_env = os.environ.get("MISTLE_AGENT_MODE", "")
tool_mode = tool_mode_env.lower() in {"1", "true", "yes", "on"}
tool_spec = os.environ.get("MISTLE_TOOL")
if tool_spec is None:
tool_spec = os.environ.get("MISTLE_AGENT", "")
state = SessionState() state = SessionState()
stop_event = Event() stop_event = Event()
agent_thread: Optional[Thread] = None tool_thread: Optional[Thread] = None
agent: Optional[Agent] = None tool: Optional[Tool] = None
ephemeral_agents: list[Thread] = [] ephemeral_tools: list[Thread] = []
with TelnetClient(host=host, port=port, timeout=10.0) as client: with TelnetClient(host=host, port=port, timeout=10.0) as client:
login( login(
@ -344,29 +350,29 @@ def main() -> int:
state=state, state=state,
) )
if agent_mode: if tool_mode:
agent = build_agent(agent_spec) tool = build_tool(tool_spec)
agent_thread = Thread( tool_thread = Thread(
target=run_agent_loop, target=run_tool_loop,
args=(client, state, agent, stop_event), args=(client, state, tool, stop_event),
kwargs={"min_send_interval": 1.0}, kwargs={"min_send_interval": 1.0},
daemon=True, daemon=True,
) )
agent_thread.start() tool_thread.start()
def run_ephemeral_agent(spec: str) -> None: def run_ephemeral_tool(spec: str) -> None:
spec = spec.strip() spec = spec.strip()
if not spec: if not spec:
print("[Agent] Usage: #execute <agent_spec>") print("[Tool] Usage: #execute <tool_spec>")
return return
try: try:
temp_agent = build_agent(spec) temp_tool = build_tool(spec)
except RuntimeError as exc: except RuntimeError as exc:
print(f"[Agent] Failed to load '{spec}': {exc}", file=sys.stderr) print(f"[Tool] Failed to load '{spec}': {exc}", file=sys.stderr)
return return
thread = Thread( thread = Thread(
target=run_agent_loop, target=run_tool_loop,
args=(client, state, temp_agent, stop_event), args=(client, state, temp_tool, stop_event),
kwargs={ kwargs={
"min_send_interval": 1.0, "min_send_interval": 1.0,
"auto_stop": True, "auto_stop": True,
@ -374,8 +380,8 @@ def main() -> int:
}, },
daemon=True, daemon=True,
) )
ephemeral_agents.append(thread) ephemeral_tools.append(thread)
print(f"[Agent] Executing {spec!r} once") print(f"[Tool] Executing {spec!r} once")
thread.start() thread.start()
interrupted = False interrupted = False
@ -385,16 +391,16 @@ def main() -> int:
state=state, state=state,
stop_event=stop_event, stop_event=stop_event,
exit_command=exit_command, exit_command=exit_command,
agent_command=None if agent_mode else run_ephemeral_agent, tool_command=None if tool_mode else run_ephemeral_tool,
) )
except KeyboardInterrupt: except KeyboardInterrupt:
print() print()
interrupted = True interrupted = True
finally: finally:
stop_event.set() stop_event.set()
if agent_thread: if tool_thread:
agent_thread.join(timeout=1.0) tool_thread.join(timeout=1.0)
for thread in ephemeral_agents: for thread in ephemeral_tools:
thread.join(timeout=1.0) thread.join(timeout=1.0)
if interrupted: if interrupted:

View file

@ -11,12 +11,12 @@ try:
except ImportError: # pragma: no cover - optional dependency except ImportError: # pragma: no cover - optional dependency
completion = None # type: ignore[assignment] completion = None # type: ignore[assignment]
from agent import Agent from tools import Tool
@dataclass @dataclass
class IntelligentCommunicationAgent(Agent): class IntelligentCommunicationTool(Tool):
"""Agent that uses a language model to answer private tells.""" """Tool that uses a language model to answer private tells."""
model: str = "mistral/mistral-tiny" model: str = "mistral/mistral-tiny"
system_prompt: str = ( system_prompt: str = (
@ -47,7 +47,7 @@ class IntelligentCommunicationAgent(Agent):
if not player: if not player:
continue continue
self.pending_replies.append((player, message)) self.pending_replies.append((player, message))
print(f"[Agent] Received message from {player}: {message}") print(f"[Tool] Received message from {player}: {message}")
self._append_history(player, "user", message) self._append_history(player, "user", message)
def decide(self) -> Optional[str]: def decide(self) -> Optional[str]:
@ -57,13 +57,13 @@ class IntelligentCommunicationAgent(Agent):
reply_text = self._sanitize_reply(self._generate_reply(player)) reply_text = self._sanitize_reply(self._generate_reply(player))
self._append_history(player, "assistant", reply_text) self._append_history(player, "assistant", reply_text)
reply = f"teile {player} mit {reply_text}" reply = f"teile {player} mit {reply_text}"
print(f"[Agent] Replying to {player} with model output") print(f"[Tool] Replying to {player} with model output")
return reply return reply
def _generate_reply(self, player: str) -> str: def _generate_reply(self, player: str) -> str:
if completion is None: if completion is None:
print( print(
"[Agent] litellm is not installed; falling back to default reply", "[Tool] litellm is not installed; falling back to default reply",
file=sys.stderr, file=sys.stderr,
) )
return self.fallback_reply return self.fallback_reply
@ -76,7 +76,7 @@ class IntelligentCommunicationAgent(Agent):
max_tokens=self.max_output_tokens, max_tokens=self.max_output_tokens,
) )
except Exception as exc: # pragma: no cover - network/runtime errors except Exception as exc: # pragma: no cover - network/runtime errors
print(f"[Agent] Model call failed: {exc}", file=sys.stderr) print(f"[Tool] Model call failed: {exc}", file=sys.stderr)
return self.fallback_reply return self.fallback_reply
try: try:

View file

@ -5,12 +5,12 @@ import random
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, Optional, Pattern, Sequence, Set, Tuple from typing import Dict, Optional, Pattern, Sequence, Set, Tuple
from agent import Agent from tools import Tool
@dataclass @dataclass
class MovementAgent(Agent): class MovementTool(Tool):
"""Agent that chooses movement commands based on room descriptions.""" """Tool that issues a single movement based on the room description."""
look_command: str = "schau" look_command: str = "schau"
command_format: str = "{direction}" command_format: str = "{direction}"
@ -102,7 +102,7 @@ class MovementAgent(Agent):
return None return None
if self.needs_look: if self.needs_look:
self.needs_look = False self.needs_look = False
print("[Agent] Requesting room description via look command") print("[Tool] Requesting room description via look command")
return self.look_command return self.look_command
if self.selected_direction is None: if self.selected_direction is None:
return None return None
@ -112,7 +112,7 @@ class MovementAgent(Agent):
direction direction
) )
command = self.command_format.format(direction=direction) command = self.command_format.format(direction=direction)
print(f"[Agent] Moving via {direction}") print(f"[Tool] Moving via {direction}")
self.executed = True self.executed = True
self.selected_direction = None self.selected_direction = None
return command return command

View file

@ -8,8 +8,8 @@ from dataclasses import dataclass, field
from typing import Deque, Optional, Pattern, Set, Tuple from typing import Deque, Optional, Pattern, Set, Tuple
class Agent(ABC): class Tool(ABC):
"""Interface for autonomous Telnet actors.""" """Interface for autonomous Telnet tools."""
@abstractmethod @abstractmethod
def observe(self, output: str) -> None: def observe(self, output: str) -> None:
@ -21,8 +21,8 @@ class Agent(ABC):
@dataclass @dataclass
class SimpleAgent(Agent): class SimpleTool(Tool):
"""Minimal agent that always returns the same command.""" """Minimal tool that always returns the same command."""
default_command: str = "schau" default_command: str = "schau"
last_output: str = field(default="", init=False) last_output: str = field(default="", init=False)
@ -36,8 +36,8 @@ class SimpleAgent(Agent):
@dataclass @dataclass
class ExploreAgent(Agent): class ExploreTool(Tool):
"""Agent that inspects every noun it discovers in the room description.""" """Tool that inspects every noun it discovers in the room description."""
look_command: str = "schau" look_command: str = "schau"
inspect_command: str = "untersuche" inspect_command: str = "untersuche"
@ -68,14 +68,14 @@ class ExploreAgent(Agent):
def decide(self) -> Optional[str]: def decide(self) -> Optional[str]:
if not self.look_sent: if not self.look_sent:
self.look_sent = True self.look_sent = True
print("[Agent] Exploring room, sending 'schau'") print("[Tool] Exploring room, sending 'schau'")
return self.look_command return self.look_command
if self.pending_targets: if self.pending_targets:
target = self.pending_targets.popleft() target = self.pending_targets.popleft()
key = target.lower() key = target.lower()
self.inspected_targets.add(key) self.inspected_targets.add(key)
progress = f"[Agent] Explored {len(self.inspected_targets)}/{len(self.seen_targets)} — untersuche {target}" progress = f"[Tool] Explored {len(self.inspected_targets)}/{len(self.seen_targets)} — untersuche {target}"
print(progress) print(progress)
return f"{self.inspect_command} {target}" return f"{self.inspect_command} {target}"
@ -83,8 +83,8 @@ class ExploreAgent(Agent):
@dataclass @dataclass
class CommunicationAgent(Agent): class CommunicationTool(Tool):
"""Agent that replies to private tells.""" """Tool that replies to private tells."""
reply_template: str = "teile {player} mit Hallo! Ich bin Mistle und ein Bot." reply_template: str = "teile {player} mit Hallo! Ich bin Mistle und ein Bot."
tell_pattern: Pattern[str] = field( tell_pattern: Pattern[str] = field(
@ -106,13 +106,12 @@ class CommunicationAgent(Agent):
if not player: if not player:
continue continue
self.pending_replies.append((player, message)) self.pending_replies.append((player, message))
print(f"[Agent] Received message from {player}: {message}") print(f"[Tool] Received message from {player}: {message}")
def decide(self) -> Optional[str]: def decide(self) -> Optional[str]:
if not self.pending_replies: if not self.pending_replies:
return None return None
player, _ = self.pending_replies.popleft() player, _ = self.pending_replies.popleft()
reply = self.reply_template.format(player=player) reply = self.reply_template.format(player=player)
print(f"[Agent] Replying to {player}") print(f"[Tool] Replying to {player}")
return reply return reply