sending an automated command "agent_decision" every second

This commit is contained in:
Daniel Eder 2025-09-27 07:52:29 +02:00
parent d880848d78
commit 538801f084
2 changed files with 151 additions and 7 deletions

3
.env
View file

@ -3,4 +3,5 @@ MISTLE_PORT=4711
MISTLE_USER=mistle
MISTLE_PASSWORD=sl-mudbot
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

155
app.py
View file

@ -3,10 +3,113 @@ import select
import sys
import time
from pathlib import Path
from threading import Event, Lock, Thread
from typing import Callable, Optional
from telnetclient import TelnetClient
AgentFunction = Callable[[str], Optional[str]]
class SessionState:
"""Share Telnet session state safely across threads."""
def __init__(self) -> None:
self._send_lock = Lock()
self._output_lock = Lock()
self._output_event = Event()
self._last_output = ""
self._last_agent_send = 0.0
def send(self, client: TelnetClient, message: str) -> None:
with self._send_lock:
client.send(message)
def agent_send(
self,
client: TelnetClient,
message: str,
*,
min_interval: float,
stop_event: Event,
) -> bool:
"""Send on behalf of the agent while respecting a minimum cadence."""
while not stop_event.is_set():
with self._send_lock:
now = time.time()
elapsed = now - self._last_agent_send
if elapsed >= min_interval:
client.send(message)
self._last_agent_send = now
return True
wait_time = min_interval - elapsed
if wait_time <= 0:
continue
if stop_event.wait(wait_time):
break
return False
def update_output(self, text: str) -> None:
if not text:
return
with self._output_lock:
self._last_output = text
self._output_event.set()
def snapshot_output(self) -> str:
with self._output_lock:
return self._last_output
def wait_for_output(self, timeout: float) -> bool:
return self._output_event.wait(timeout)
def clear_output_event(self) -> None:
self._output_event.clear()
def agent_decision(last_output: str) -> Optional[str]:
"""Decide which command to send based on the most recent server output."""
return "schau"
def run_agent_loop(
client: TelnetClient,
state: SessionState,
agent_fn: AgentFunction,
stop_event: Event,
*,
idle_delay: float = 0.5,
min_send_interval: float = 1.0,
) -> None:
"""Invoke *agent_fn* whenever new output arrives and send its response."""
while not stop_event.is_set():
triggered = state.wait_for_output(timeout=idle_delay)
if stop_event.is_set():
break
if not triggered:
continue
state.clear_output_event()
last_output = state.snapshot_output()
if not last_output:
continue
try:
command = agent_fn(last_output)
except Exception as exc: # pragma: no cover - defensive logging
print(f"Agent function failed: {exc}", file=sys.stderr)
continue
if not command:
continue
sent = state.agent_send(
client,
command,
min_interval=min_send_interval,
stop_event=stop_event,
)
if not sent:
break
def load_env_file(path: str = ".env") -> None:
"""Populate ``os.environ`` with key/value pairs from a dotenv file."""
env_path = Path(path)
@ -40,6 +143,7 @@ def login(
login_prompt: str,
banner_timeout: float = 10.0,
response_timeout: float = 2.0,
state: Optional[SessionState] = None,
) -> None:
"""Handle the banner/prompt exchange and send credentials."""
if login_prompt:
@ -48,6 +152,8 @@ def login(
banner = client.receive(timeout=response_timeout)
if banner:
print(banner, end="" if banner.endswith("\n") else "\n")
if state:
state.update_output(banner)
if user:
client.send(user)
@ -58,14 +164,18 @@ def login(
response = client.receive(timeout=response_timeout)
if response:
print(response, end="" if response.endswith("\n") else "\n")
if state:
state.update_output(response)
def interactive_session(
client: TelnetClient,
state: SessionState,
stop_event: Event,
*,
exit_command: str,
poll_interval: float = 0.2,
receive_timeout: float = 0.2,
exit_command: str,
) -> None:
"""Keep the Telnet session running, proxying input/output until interrupted."""
if exit_command:
@ -73,30 +183,42 @@ def interactive_session(
else:
print("Connected. Press Ctrl-C to exit.")
while True:
while not stop_event.is_set():
incoming = client.receive(timeout=receive_timeout)
if incoming:
print(incoming, end="" if incoming.endswith("\n") else "\n")
state.update_output(incoming)
readable, _, _ = select.select([sys.stdin], [], [], poll_interval)
if sys.stdin in readable:
line = sys.stdin.readline()
if line == "":
stop_event.set()
break
line = line.rstrip("\r\n")
if not line:
continue
client.send(line)
state.send(client, line)
def graceful_shutdown(client: TelnetClient, exit_command: str) -> None:
def graceful_shutdown(
client: TelnetClient,
exit_command: str,
*,
state: Optional[SessionState] = None,
) -> None:
if not exit_command:
return
try:
client.send(exit_command)
if state:
state.send(client, exit_command)
else:
client.send(exit_command)
farewell = client.receive(timeout=2.0)
if farewell:
print(farewell, end="" if farewell.endswith("\n") else "\n")
if state:
state.update_output(farewell)
except Exception as exc: # pragma: no cover - best effort logging
print(f"Failed to send exit command: {exc}", file=sys.stderr)
@ -116,6 +238,11 @@ def main() -> int:
password = os.environ.get("MISTLE_PASSWORD", "")
login_prompt = os.environ.get("MISTLE_LOGIN_PROMPT", "")
exit_command = os.environ.get("MISTLE_EXIT_COMMAND", "")
agent_mode = os.environ.get("MISTLE_AGENT_MODE", "").lower() in {"1", "true", "yes", "on"}
state = SessionState()
stop_event = Event()
agent_thread: Optional[Thread] = None
with TelnetClient(host=host, port=port, timeout=10.0) as client:
login(
@ -123,20 +250,36 @@ def main() -> int:
user=user,
password=password,
login_prompt=login_prompt,
state=state,
)
if agent_mode:
agent_thread = Thread(
target=run_agent_loop,
args=(client, state, agent_decision, stop_event),
kwargs={"min_send_interval": 1.0},
daemon=True,
)
agent_thread.start()
interrupted = False
try:
interactive_session(
client,
state=state,
stop_event=stop_event,
exit_command=exit_command,
)
except KeyboardInterrupt:
print()
interrupted = True
finally:
stop_event.set()
if agent_thread:
agent_thread.join(timeout=1.0)
if interrupted:
graceful_shutdown(client, exit_command)
graceful_shutdown(client, exit_command, state=state)
return 0