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_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

155
app.py
View file

@ -3,10 +3,113 @@ import select
import sys import sys
import time import time
from pathlib import Path from pathlib import Path
from threading import Event, Lock, Thread
from typing import Callable, Optional
from telnetclient import TelnetClient 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: def load_env_file(path: str = ".env") -> None:
"""Populate ``os.environ`` with key/value pairs from a dotenv file.""" """Populate ``os.environ`` with key/value pairs from a dotenv file."""
env_path = Path(path) env_path = Path(path)
@ -40,6 +143,7 @@ def login(
login_prompt: str, login_prompt: str,
banner_timeout: float = 10.0, banner_timeout: float = 10.0,
response_timeout: float = 2.0, response_timeout: float = 2.0,
state: Optional[SessionState] = None,
) -> None: ) -> None:
"""Handle the banner/prompt exchange and send credentials.""" """Handle the banner/prompt exchange and send credentials."""
if login_prompt: if login_prompt:
@ -48,6 +152,8 @@ def login(
banner = client.receive(timeout=response_timeout) banner = client.receive(timeout=response_timeout)
if banner: if banner:
print(banner, end="" if banner.endswith("\n") else "\n") print(banner, end="" if banner.endswith("\n") else "\n")
if state:
state.update_output(banner)
if user: if user:
client.send(user) client.send(user)
@ -58,14 +164,18 @@ def login(
response = client.receive(timeout=response_timeout) response = client.receive(timeout=response_timeout)
if response: if response:
print(response, end="" if response.endswith("\n") else "\n") print(response, end="" if response.endswith("\n") else "\n")
if state:
state.update_output(response)
def interactive_session( def interactive_session(
client: TelnetClient, client: TelnetClient,
state: SessionState,
stop_event: Event,
*, *,
exit_command: str,
poll_interval: float = 0.2, poll_interval: float = 0.2,
receive_timeout: float = 0.2, receive_timeout: float = 0.2,
exit_command: str,
) -> 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:
@ -73,30 +183,42 @@ def interactive_session(
else: else:
print("Connected. Press Ctrl-C to exit.") print("Connected. Press Ctrl-C to exit.")
while True: while not stop_event.is_set():
incoming = client.receive(timeout=receive_timeout) incoming = client.receive(timeout=receive_timeout)
if incoming: if incoming:
print(incoming, end="" if incoming.endswith("\n") else "\n") print(incoming, end="" if incoming.endswith("\n") else "\n")
state.update_output(incoming)
readable, _, _ = select.select([sys.stdin], [], [], poll_interval) readable, _, _ = select.select([sys.stdin], [], [], poll_interval)
if sys.stdin in readable: if sys.stdin in readable:
line = sys.stdin.readline() line = sys.stdin.readline()
if line == "": if line == "":
stop_event.set()
break break
line = line.rstrip("\r\n") line = line.rstrip("\r\n")
if not line: if not line:
continue 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: if not exit_command:
return return
try: try:
client.send(exit_command) if state:
state.send(client, exit_command)
else:
client.send(exit_command)
farewell = client.receive(timeout=2.0) farewell = client.receive(timeout=2.0)
if farewell: if farewell:
print(farewell, end="" if farewell.endswith("\n") else "\n") 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 except Exception as exc: # pragma: no cover - best effort logging
print(f"Failed to send exit command: {exc}", file=sys.stderr) print(f"Failed to send exit command: {exc}", file=sys.stderr)
@ -116,6 +238,11 @@ 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"}
state = SessionState()
stop_event = Event()
agent_thread: Optional[Thread] = None
with TelnetClient(host=host, port=port, timeout=10.0) as client: with TelnetClient(host=host, port=port, timeout=10.0) as client:
login( login(
@ -123,20 +250,36 @@ def main() -> int:
user=user, user=user,
password=password, password=password,
login_prompt=login_prompt, 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 interrupted = False
try: try:
interactive_session( interactive_session(
client, client,
state=state,
stop_event=stop_event,
exit_command=exit_command, exit_command=exit_command,
) )
except KeyboardInterrupt: except KeyboardInterrupt:
print() print()
interrupted = True interrupted = True
finally:
stop_event.set()
if agent_thread:
agent_thread.join(timeout=1.0)
if interrupted: if interrupted:
graceful_shutdown(client, exit_command) graceful_shutdown(client, exit_command, state=state)
return 0 return 0