Compare commits
2 commits
f5fc6dbd14
...
905e3c91ab
| Author | SHA1 | Date | |
|---|---|---|---|
| 905e3c91ab | |||
| e166b5d0a8 |
4 changed files with 152 additions and 2 deletions
|
|
@ -8,6 +8,7 @@ Python-based Telnet helper for connecting to MUD servers, handling login flows,
|
||||||
- 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 tool mode plus an on-demand `#execute <tool>` escape hatch for ad-hoc automations.
|
- Optional always-on tool mode plus an on-demand `#execute <tool>` escape hatch for ad-hoc automations.
|
||||||
|
- Higher-level agents (`FixedStrategyAgent` so far) that can string multiple tools together via `#agent <spec>`.
|
||||||
- Built-in tools (`SimpleTool`, `ExploreTool`, `CommunicationTool`, `MovementTool`, `IntelligentCommunicationTool`) with a pluggable interface for custom behaviours.
|
- Built-in tools (`SimpleTool`, `ExploreTool`, `CommunicationTool`, `MovementTool`, `IntelligentCommunicationTool`) with a pluggable interface for custom behaviours.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
@ -41,6 +42,14 @@ Python-based Telnet helper for connecting to MUD servers, handling login flows,
|
||||||
|
|
||||||
The command remains interactive while the tool 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.
|
||||||
|
|
||||||
|
6. To run an agent that orchestrates several tools, use:
|
||||||
|
|
||||||
|
```text
|
||||||
|
#agent fixed move,explore
|
||||||
|
```
|
||||||
|
|
||||||
|
This example uses the fixed strategy agent to run `move` and then `explore` once. The first token after `#agent` selects the agent type (`fixed` today, more to come), and any remaining text is passed as that agent's configuration.
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
All variables can be placed in the `.env` file (one `KEY=value` per line) or provided through the shell environment.
|
All variables can be placed in the `.env` file (one `KEY=value` per line) or provided through the shell environment.
|
||||||
|
|
|
||||||
37
agent_runtime.py
Normal file
37
agent_runtime.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from threading import Event
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from agents import Agent
|
||||||
|
from tools import Tool
|
||||||
|
|
||||||
|
ToolBuilder = Callable[[str], Tool]
|
||||||
|
ToolRunner = Callable[[Tool], None]
|
||||||
|
|
||||||
|
|
||||||
|
def run_agent(
|
||||||
|
agent: Agent,
|
||||||
|
*,
|
||||||
|
build_tool: ToolBuilder,
|
||||||
|
run_tool: ToolRunner,
|
||||||
|
stop_event: Event,
|
||||||
|
) -> None:
|
||||||
|
"""Execute *agent* by wiring it to the tool runtime."""
|
||||||
|
|
||||||
|
def invoke_tool(spec: str) -> bool:
|
||||||
|
name = spec.strip()
|
||||||
|
if not name:
|
||||||
|
print("[Agent] Ignoring empty tool spec", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
tool = build_tool(name)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(f"[Agent] Failed to load tool '{name}': {exc}", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
print(f"[Agent] Running tool '{name}'")
|
||||||
|
run_tool(tool)
|
||||||
|
return True
|
||||||
|
|
||||||
|
agent.run(invoke_tool=invoke_tool, stop_event=stop_event)
|
||||||
54
agents.py
Normal file
54
agents.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from threading import Event
|
||||||
|
from typing import Callable, Iterable
|
||||||
|
|
||||||
|
|
||||||
|
ToolInvoker = Callable[[str], bool]
|
||||||
|
|
||||||
|
|
||||||
|
class Agent(ABC):
|
||||||
|
"""Interface for higher-level behaviours that orchestrate tools."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def run(self, *, invoke_tool: ToolInvoker, stop_event: Event) -> None:
|
||||||
|
"""Execute the agent strategy until finished or *stop_event* is set."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FixedStrategyAgent(Agent):
|
||||||
|
"""Run a comma-separated list of tool specs in order."""
|
||||||
|
|
||||||
|
plan: str
|
||||||
|
|
||||||
|
def run(self, *, invoke_tool: ToolInvoker, stop_event: Event) -> None:
|
||||||
|
steps = [part.strip() for part in self.plan.split(",") if part.strip()]
|
||||||
|
if not steps:
|
||||||
|
print("[Agent] No tools configured for fixed strategy", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
for step in steps:
|
||||||
|
if stop_event.is_set():
|
||||||
|
break
|
||||||
|
success = invoke_tool(step)
|
||||||
|
if not success:
|
||||||
|
# Abort on first failure to avoid confusing behaviour
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def build_agent(spec: str) -> Agent:
|
||||||
|
normalized = spec.strip()
|
||||||
|
if not normalized:
|
||||||
|
raise RuntimeError("Agent specification must not be empty")
|
||||||
|
|
||||||
|
parts = normalized.split(maxsplit=1)
|
||||||
|
kind = parts[0].lower()
|
||||||
|
config = parts[1].strip() if len(parts) > 1 else ""
|
||||||
|
|
||||||
|
if kind in {"fixed", "strategy", "fixedstrategy"}:
|
||||||
|
return FixedStrategyAgent(config)
|
||||||
|
|
||||||
|
raise RuntimeError(f"Unknown agent type '{kind}'")
|
||||||
54
app.py
54
app.py
|
|
@ -8,6 +8,8 @@ from threading import Event, Lock, Thread
|
||||||
from typing import Callable, Optional, Type
|
from typing import Callable, Optional, Type
|
||||||
|
|
||||||
from tools import Tool, SimpleTool
|
from tools import Tool, SimpleTool
|
||||||
|
from agents import Agent, build_agent
|
||||||
|
from agent_runtime import run_agent
|
||||||
from telnetclient import TelnetClient
|
from telnetclient import TelnetClient
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -126,7 +128,6 @@ def run_tool_loop(
|
||||||
continue
|
continue
|
||||||
maybe_send()
|
maybe_send()
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
@ -257,6 +258,7 @@ def interactive_session(
|
||||||
receive_timeout: float = 0.2,
|
receive_timeout: float = 0.2,
|
||||||
exit_command: str,
|
exit_command: str,
|
||||||
tool_command: Optional[Callable[[str], None]] = None,
|
tool_command: Optional[Callable[[str], None]] = None,
|
||||||
|
agent_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,7 +281,15 @@ def interactive_session(
|
||||||
line = line.rstrip("\r\n")
|
line = line.rstrip("\r\n")
|
||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
if tool_command and line.lower().startswith("#execute"):
|
lowered = line.lower()
|
||||||
|
if agent_command and lowered.startswith("#agent"):
|
||||||
|
parts = line.split(maxsplit=1)
|
||||||
|
if len(parts) == 1:
|
||||||
|
print("[Agent] Usage: #agent <agent_spec>")
|
||||||
|
else:
|
||||||
|
agent_command(parts[1])
|
||||||
|
continue
|
||||||
|
if tool_command and lowered.startswith("#execute"):
|
||||||
parts = line.split(maxsplit=1)
|
parts = line.split(maxsplit=1)
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
print("[Tool] Usage: #execute <tool_spec>")
|
print("[Tool] Usage: #execute <tool_spec>")
|
||||||
|
|
@ -336,6 +346,7 @@ def main() -> int:
|
||||||
tool_thread: Optional[Thread] = None
|
tool_thread: Optional[Thread] = None
|
||||||
tool: Optional[Tool] = None
|
tool: Optional[Tool] = None
|
||||||
ephemeral_tools: list[Thread] = []
|
ephemeral_tools: list[Thread] = []
|
||||||
|
agent_threads: 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(
|
||||||
|
|
@ -380,6 +391,42 @@ def main() -> int:
|
||||||
print(f"[Tool] Executing {spec!r} once")
|
print(f"[Tool] Executing {spec!r} once")
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
def run_ephemeral_agent(spec: str) -> None:
|
||||||
|
spec = spec.strip()
|
||||||
|
if not spec:
|
||||||
|
print("[Agent] Usage: #agent <agent_spec>")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
temp_agent = build_agent(spec)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(f"[Agent] Failed to configure '{spec}': {exc}", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
def run_tool_instance(tool: Tool) -> None:
|
||||||
|
run_tool_loop(
|
||||||
|
client,
|
||||||
|
state,
|
||||||
|
tool,
|
||||||
|
stop_event,
|
||||||
|
min_send_interval=1.0,
|
||||||
|
auto_stop=True,
|
||||||
|
auto_stop_idle=2.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
thread = Thread(
|
||||||
|
target=run_agent,
|
||||||
|
args=(temp_agent,),
|
||||||
|
kwargs={
|
||||||
|
"build_tool": build_tool,
|
||||||
|
"run_tool": run_tool_instance,
|
||||||
|
"stop_event": stop_event,
|
||||||
|
},
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
agent_threads.append(thread)
|
||||||
|
print(f"[Agent] Executing {spec!r}")
|
||||||
|
thread.start()
|
||||||
|
|
||||||
interrupted = False
|
interrupted = False
|
||||||
try:
|
try:
|
||||||
interactive_session(
|
interactive_session(
|
||||||
|
|
@ -388,6 +435,7 @@ def main() -> int:
|
||||||
stop_event=stop_event,
|
stop_event=stop_event,
|
||||||
exit_command=exit_command,
|
exit_command=exit_command,
|
||||||
tool_command=None if tool_mode else run_ephemeral_tool,
|
tool_command=None if tool_mode else run_ephemeral_tool,
|
||||||
|
agent_command=run_ephemeral_agent,
|
||||||
)
|
)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print()
|
print()
|
||||||
|
|
@ -398,6 +446,8 @@ def main() -> int:
|
||||||
tool_thread.join(timeout=1.0)
|
tool_thread.join(timeout=1.0)
|
||||||
for thread in ephemeral_tools:
|
for thread in ephemeral_tools:
|
||||||
thread.join(timeout=1.0)
|
thread.join(timeout=1.0)
|
||||||
|
for thread in agent_threads:
|
||||||
|
thread.join(timeout=1.0)
|
||||||
|
|
||||||
if interrupted:
|
if interrupted:
|
||||||
graceful_shutdown(client, exit_command, state=state)
|
graceful_shutdown(client, exit_command, state=state)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue