feat: support agent selection via environment variables
This commit is contained in:
parent
0c88bd5a2a
commit
5f3ea6a781
5 changed files with 134 additions and 4 deletions
3
.env
3
.env
|
|
@ -4,4 +4,5 @@ MISTLE_USER=mistle
|
||||||
MISTLE_PASSWORD=sl-mudbot
|
MISTLE_PASSWORD=sl-mudbot
|
||||||
MISTLE_LOGIN_PROMPT=Wie heisst Du denn ("neu" fuer neuen Spieler) ?
|
MISTLE_LOGIN_PROMPT=Wie heisst Du denn ("neu" fuer neuen Spieler) ?
|
||||||
MISTLE_EXIT_COMMAND=schlaf ein
|
MISTLE_EXIT_COMMAND=schlaf ein
|
||||||
MISTLE_AGENT_MODE=true
|
MISTLE_AGENT_MODE=true
|
||||||
|
MISTLE_AGENT=simple
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -8,4 +8,7 @@ wheels/
|
||||||
|
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
|
||||||
|
# config
|
||||||
|
.env
|
||||||
71
README.md
71
README.md
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Mistle Mudbot
|
||||||
|
|
||||||
|
Python-based Telnet helper for connecting to MUD servers, handling login flows, and optionally running an automated "agent" alongside interactive play.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Lightweight wrapper (`TelnetClient`) around `telnetlib` with sane defaults and context-manager support.
|
||||||
|
- Loads credentials and connection settings from a local `.env` file.
|
||||||
|
- Interactive console session that mirrors server output and lets you type commands directly.
|
||||||
|
- Optional agent mode that runs on a background thread and can dispatch automated commands without blocking manual input.
|
||||||
|
- Pluggable agent selection via environment variables (built-in `SimpleAgent`, extendable with custom classes).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.10+
|
||||||
|
- A reachable Telnet-based MUD server
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
1. Create a virtual environment and install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Copy `.env.example` (if available) or create a `.env` file in the project root, then fill in the connection details described below.
|
||||||
|
|
||||||
|
3. Run the client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Type commands directly into the console. Press `Ctrl-C` to exit; the client will send any configured shutdown command to the MUD.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
All variables can be placed in the `.env` file (one `KEY=value` per line) or provided through the shell environment.
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `MISTLE_HOST` | ✅ | DNS name or IP of the target MUD server. |
|
||||||
|
| `MISTLE_PORT` | ✅ | Telnet port number (will be cast to `int`). |
|
||||||
|
| `MISTLE_USER` | ❌ | Username or character name; sent automatically after login banner, if provided. |
|
||||||
|
| `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_EXIT_COMMAND` | ❌ | Command issued during graceful shutdown (after pressing `Ctrl-C`). Useful for `quit`/`save` macros. |
|
||||||
|
| `MISTLE_AGENT_MODE` | ❌ | Enable agent thread when set to truthy values (`1`, `true`, `yes`, `on`). Defaults to interactive-only mode. |
|
||||||
|
| `MISTLE_AGENT` | ❌ | Select which agent class to instantiate when agent mode is active. Accepted values: `simple` (default), `explore` (requires `ExploreAgent` inside `agent.py`), or custom spec `module:ClassName`. |
|
||||||
|
|
||||||
|
## Agent Development
|
||||||
|
|
||||||
|
- Implement new agents by subclassing `agent.Agent` and overriding `observe()` and `decide()`.
|
||||||
|
- Register the agent by either:
|
||||||
|
- Adding the class to `agent.py` and referencing it in `MISTLE_AGENT` (e.g., `explore` if the class is named `ExploreAgent`).
|
||||||
|
- Placing the class elsewhere and configuring `MISTLE_AGENT` to `your_module:YourAgent`.
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
## Danger Zone
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Feel free to open issues or submit pull requests for additional MUD-specific helpers, new agents, or quality-of-life improvements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Happy MUDding!
|
||||||
15
agent.py
15
agent.py
|
|
@ -30,3 +30,18 @@ class SimpleAgent(Agent):
|
||||||
|
|
||||||
def decide(self) -> Optional[str]:
|
def decide(self) -> Optional[str]:
|
||||||
return self.default_command
|
return self.default_command
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ExploreAgent(Agent):
|
||||||
|
"""Very small stateful agent that decides which command to run next."""
|
||||||
|
|
||||||
|
default_command: str = "untersuche boden"
|
||||||
|
last_output: str = field(default="", init=False)
|
||||||
|
|
||||||
|
def observe(self, output: str) -> None:
|
||||||
|
if output:
|
||||||
|
self.last_output = output
|
||||||
|
|
||||||
|
def decide(self) -> Optional[str]:
|
||||||
|
return self.default_command
|
||||||
44
app.py
44
app.py
|
|
@ -2,9 +2,10 @@ import os
|
||||||
import select
|
import select
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from importlib import import_module
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import Event, Lock, Thread
|
from threading import Event, Lock, Thread
|
||||||
from typing import Optional
|
from typing import Optional, Type
|
||||||
|
|
||||||
from agent import Agent, SimpleAgent
|
from agent import Agent, SimpleAgent
|
||||||
from telnetclient import TelnetClient
|
from telnetclient import TelnetClient
|
||||||
|
|
@ -129,6 +130,44 @@ def require_env(key: str) -> str:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def build_agent(agent_spec: str) -> Agent:
|
||||||
|
"""Instantiate an agent based on ``MISTLE_AGENT`` contents."""
|
||||||
|
normalized = agent_spec.strip()
|
||||||
|
if not normalized:
|
||||||
|
return SimpleAgent()
|
||||||
|
|
||||||
|
key = normalized.lower()
|
||||||
|
if key == "simple":
|
||||||
|
return SimpleAgent()
|
||||||
|
|
||||||
|
if key == "explore":
|
||||||
|
try:
|
||||||
|
module = import_module("agent")
|
||||||
|
agent_cls = getattr(module, "ExploreAgent")
|
||||||
|
except AttributeError as exc: # pragma: no cover - optional dependency
|
||||||
|
raise RuntimeError("ExploreAgent is not available in agent module") from exc
|
||||||
|
return _instantiate_agent(agent_cls, normalized)
|
||||||
|
|
||||||
|
if ":" in normalized:
|
||||||
|
module_name, class_name = normalized.split(":", 1)
|
||||||
|
if not module_name or not class_name:
|
||||||
|
raise RuntimeError("MISTLE_AGENT must be in 'module:ClassName' format")
|
||||||
|
module = import_module(module_name)
|
||||||
|
agent_cls = getattr(module, class_name)
|
||||||
|
return _instantiate_agent(agent_cls, normalized)
|
||||||
|
|
||||||
|
raise RuntimeError(f"Unknown agent spec '{agent_spec}'.")
|
||||||
|
|
||||||
|
|
||||||
|
def _instantiate_agent(agent_cls: Type[Agent], agent_spec: str) -> Agent:
|
||||||
|
if not issubclass(agent_cls, Agent):
|
||||||
|
raise RuntimeError(f"{agent_spec} is not an Agent subclass")
|
||||||
|
try:
|
||||||
|
return agent_cls()
|
||||||
|
except TypeError as exc:
|
||||||
|
raise RuntimeError(f"Failed to instantiate {agent_spec}: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
def login(
|
def login(
|
||||||
client: TelnetClient,
|
client: TelnetClient,
|
||||||
*,
|
*,
|
||||||
|
|
@ -233,6 +272,7 @@ def main() -> int:
|
||||||
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"}
|
agent_mode = os.environ.get("MISTLE_AGENT_MODE", "").lower() in {"1", "true", "yes", "on"}
|
||||||
|
agent_spec = os.environ.get("MISTLE_AGENT", "")
|
||||||
|
|
||||||
state = SessionState()
|
state = SessionState()
|
||||||
stop_event = Event()
|
stop_event = Event()
|
||||||
|
|
@ -249,7 +289,7 @@ def main() -> int:
|
||||||
)
|
)
|
||||||
|
|
||||||
if agent_mode:
|
if agent_mode:
|
||||||
agent = SimpleAgent()
|
agent = build_agent(agent_spec)
|
||||||
agent_thread = Thread(
|
agent_thread = Thread(
|
||||||
target=run_agent_loop,
|
target=run_agent_loop,
|
||||||
args=(client, state, agent, stop_event),
|
args=(client, state, agent, stop_event),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue