sending an automated command "agent_decision" every second
This commit is contained in:
parent
d880848d78
commit
538801f084
2 changed files with 151 additions and 7 deletions
3
.env
3
.env
|
|
@ -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
155
app.py
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue