Compare commits
No commits in common. "ffcebeb801b0ef8014580987eabc9d81ee154eec" and "f95b824a94588ec437dc081335ca55dcd0fcbbc6" have entirely different histories.
ffcebeb801
...
f95b824a94
5 changed files with 98 additions and 135 deletions
|
|
@ -9,7 +9,7 @@ Python-based Telnet helper for connecting to MUD servers, handling login flows,
|
|||
- 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.
|
||||
- Higher-level agents (`fixed`, `loop`, `intelligent`) that can string multiple tools together via `#agent <spec>`.
|
||||
- Built-in tools (`LookTool`, `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
|
||||
|
||||
|
|
@ -64,7 +64,7 @@ Python-based Telnet helper for connecting to MUD servers, handling login flows,
|
|||
#agent intelligent
|
||||
```
|
||||
|
||||
Add guidance text after the type and optional modifiers separated by `|`, e.g. `#agent intelligent explore carefully|model=mistral/mistral-large-2407|delay=2`. The agent only calls built-in tools (`look`, `move`, `movement`, `explore`, `communication`, `intelligentcommunication`) and refuses unknown names.
|
||||
Add guidance text after the type and optional modifiers separated by `|`, e.g. `#agent intelligent explore carefully|model=mistral/mistral-large-2407|delay=2`. The agent only calls built-in tools (`simple`, `move`, `movement`, `explore`, `communication`, `intelligentcommunication`) and refuses unknown names.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ All variables can be placed in the `.env` file (one `KEY=value` per line) or pro
|
|||
| `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_TOOL_MODE` | ❌ | Enable full-time tool thread when set to truthy values (`1`, `true`, `yes`, `on`). Defaults to interactive-only mode. |
|
||||
| `MISTLE_TOOL` | ❌ | Select which tool class to instantiate when tool mode is active. Accepted values: `look` (default, alias `simple`), `explore`, `communication`, `movement`, `intelligent`/`intelligentcommunication` (LLM-backed), or custom spec `module:ClassName`. |
|
||||
| `MISTLE_TOOL` | ❌ | Select which tool class to instantiate when tool mode is active. Accepted values: `simple` (default), `explore`, `communication`, `movement`, `intelligent`/`intelligentcommunication` (LLM-backed), or custom spec `module:ClassName`. |
|
||||
| `MISTLE_LLM_MODEL` | ❌ | Override the `litellm` model used by the intelligent tool (defaults to `mistral/mistral-small-2407`). |
|
||||
| `MISTRAL_API_KEY` | ❌ | API key used by `IntelligentCommunicationTool` (via `litellm`) when calling the `mistral/mistral-small-2407` model. |
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ All variables can be placed in the `.env` file (one `KEY=value` per line) or pro
|
|||
## On-Demand Tools
|
||||
|
||||
- When `MISTLE_TOOL_MODE` is **off**, you can trigger an ephemeral tool at any time with `#execute <tool_spec>`.
|
||||
- The syntax accepts the same values as `MISTLE_TOOL` and reuses the `build_tool` helper, so `#execute look`, `#execute explore`, `#execute move`, `#execute intelligent`, or `#execute mypackage.mymodule:CustomTool` are all valid.
|
||||
- The syntax accepts the same values as `MISTLE_TOOL` and reuses the `build_tool` helper, so `#execute simple`, `#execute explore`, `#execute move`, `#execute intelligent`, or `#execute mypackage.mymodule:CustomTool` are all valid.
|
||||
- On-demand runs share the current session, respect the one-command-per-second limit, and stop automatically after a few seconds of inactivity.
|
||||
|
||||
## Danger Zone
|
||||
|
|
|
|||
13
agents.py
13
agents.py
|
|
@ -5,7 +5,7 @@ import sys
|
|||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from threading import Event
|
||||
from typing import Callable, Mapping, Optional
|
||||
from typing import Callable, Optional, Sequence
|
||||
|
||||
|
||||
ToolInvoker = Callable[[str], bool]
|
||||
|
|
@ -83,9 +83,7 @@ class LoopAgent(Agent):
|
|||
break
|
||||
|
||||
|
||||
def build_agent(
|
||||
spec: str, *, allowed_tools: Optional[Mapping[str, str]] = None
|
||||
) -> Agent:
|
||||
def build_agent(spec: str, *, allowed_tools: Optional[Sequence[str]] = None) -> Agent:
|
||||
normalized = spec.strip()
|
||||
if not normalized:
|
||||
raise RuntimeError("Agent specification must not be empty")
|
||||
|
|
@ -125,11 +123,8 @@ def build_agent(
|
|||
else:
|
||||
instruction = segment
|
||||
|
||||
tool_map = {
|
||||
name: description
|
||||
for name, description in (allowed_tools or {}).items()
|
||||
}
|
||||
agent = IntelligentAgent(instruction=instruction, allowed_tools=tool_map)
|
||||
whitelist = tuple(sorted({tool.lower() for tool in (allowed_tools or [])}))
|
||||
agent = IntelligentAgent(instruction=instruction, allowed_tools=whitelist)
|
||||
if model:
|
||||
agent.model = model
|
||||
agent.turn_delay = turn_delay if turn_delay > 0 else 1.0
|
||||
|
|
|
|||
110
app.py
110
app.py
|
|
@ -7,62 +7,36 @@ from pathlib import Path
|
|||
from threading import Event, Lock, Thread
|
||||
from typing import Callable, Optional, Type
|
||||
|
||||
from tools import Tool, LookTool
|
||||
from tools import Tool, SimpleTool
|
||||
from agents import Agent, build_agent
|
||||
from agent_runtime import run_agent
|
||||
|
||||
TOOL_REGISTRY = {
|
||||
"look": {
|
||||
"module": "tools",
|
||||
"class": "LookTool",
|
||||
"kwargs": {},
|
||||
"description": "Sends the 'schau' look command to refresh the room description.",
|
||||
},
|
||||
"move": {
|
||||
"module": "movement_tool",
|
||||
"class": "MovementTool",
|
||||
"kwargs": {},
|
||||
"description": "Looks around and moves in one available direction, chosen randomly among unvisited exits.",
|
||||
},
|
||||
"movement": {
|
||||
"module": "movement_tool",
|
||||
"class": "MovementTool",
|
||||
"kwargs": {},
|
||||
"description": "Alias of 'move'. Looks around and moves in one available direction, chosen randomly among unvisited exits.",
|
||||
},
|
||||
"explore": {
|
||||
"module": "tools",
|
||||
"class": "ExploreTool",
|
||||
"kwargs": {},
|
||||
"description": "Sends 'schau' once, then 'untersuche <noun>' for each noun found in the room description.",
|
||||
},
|
||||
"communication": {
|
||||
"module": "tools",
|
||||
"class": "CommunicationTool",
|
||||
"kwargs": {},
|
||||
"description": "Responds to private tells with a friendly greeting via 'teile <player> mit ...'.",
|
||||
},
|
||||
"intelligent": {
|
||||
"module": "intelligent_tool",
|
||||
"class": "IntelligentCommunicationTool",
|
||||
"kwargs": {
|
||||
"model": os.environ.get("MISTLE_LLM_MODEL", "mistral/mistral-small-2407")
|
||||
},
|
||||
"description": "Uses an LLM to craft a polite reply to private tells.",
|
||||
},
|
||||
"intelligentcommunication": {
|
||||
"module": "intelligent_tool",
|
||||
"class": "IntelligentCommunicationTool",
|
||||
"kwargs": {
|
||||
"model": os.environ.get("MISTLE_LLM_MODEL", "mistral/mistral-small-2407")
|
||||
},
|
||||
"description": "Alias of 'intelligent'. Uses an LLM to craft a polite reply to private tells.",
|
||||
},
|
||||
BUILTIN_TOOLS = {
|
||||
"simple": ("tools", "SimpleTool", {}),
|
||||
"explore": ("tools", "ExploreTool", {}),
|
||||
"communication": ("tools", "CommunicationTool", {}),
|
||||
"movement": ("movement_tool", "MovementTool", {}),
|
||||
"move": ("movement_tool", "MovementTool", {}),
|
||||
"intelligent": (
|
||||
"intelligent_tool",
|
||||
"IntelligentCommunicationTool",
|
||||
{"model": os.environ.get("MISTLE_LLM_MODEL", "mistral/mistral-small-2407")},
|
||||
),
|
||||
"intelligentcommunication": (
|
||||
"intelligent_tool",
|
||||
"IntelligentCommunicationTool",
|
||||
{"model": os.environ.get("MISTLE_LLM_MODEL", "mistral/mistral-small-2407")},
|
||||
),
|
||||
}
|
||||
|
||||
TOOL_DESCRIPTIONS = {
|
||||
name: meta["description"] for name, meta in TOOL_REGISTRY.items()
|
||||
}
|
||||
AVAILABLE_TOOL_NAMES = [
|
||||
"move",
|
||||
"movement",
|
||||
"explore",
|
||||
"communication",
|
||||
"intelligentcommunication",
|
||||
"simple",
|
||||
]
|
||||
from telnetclient import TelnetClient
|
||||
|
||||
|
||||
|
|
@ -210,17 +184,14 @@ def build_tool(spec: str) -> Tool:
|
|||
"""Instantiate a tool based on configuration."""
|
||||
normalized = spec.strip()
|
||||
if not normalized:
|
||||
return LookTool()
|
||||
return SimpleTool()
|
||||
|
||||
key = normalized.lower()
|
||||
if key in {"simple", "look"}:
|
||||
return LookTool()
|
||||
if key == "simple":
|
||||
return SimpleTool()
|
||||
|
||||
if key in TOOL_REGISTRY:
|
||||
meta = TOOL_REGISTRY[key]
|
||||
module_name = meta["module"]
|
||||
class_name = meta["class"]
|
||||
kwargs = meta.get("kwargs", {})
|
||||
if key in BUILTIN_TOOLS:
|
||||
module_name, class_name, kwargs = BUILTIN_TOOLS[key]
|
||||
try:
|
||||
module = import_module(module_name)
|
||||
tool_cls = getattr(module, class_name)
|
||||
|
|
@ -436,7 +407,7 @@ def main() -> int:
|
|||
print("[Agent] Usage: #agent <agent_spec>")
|
||||
return
|
||||
try:
|
||||
temp_agent = build_agent(spec, allowed_tools=TOOL_DESCRIPTIONS)
|
||||
temp_agent = build_agent(spec, allowed_tools=AVAILABLE_TOOL_NAMES)
|
||||
except RuntimeError as exc:
|
||||
print(f"[Agent] Failed to configure '{spec}': {exc}", file=sys.stderr)
|
||||
return
|
||||
|
|
@ -515,3 +486,22 @@ def main() -> int:
|
|||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
BUILTIN_TOOLS = {
|
||||
"simple": ("tools", "SimpleTool", {}),
|
||||
"explore": ("tools", "ExploreTool", {}),
|
||||
"communication": ("tools", "CommunicationTool", {}),
|
||||
"movement": ("movement_tool", "MovementTool", {}),
|
||||
"move": ("movement_tool", "MovementTool", {}),
|
||||
"intelligent": (
|
||||
"intelligent_tool",
|
||||
"IntelligentCommunicationTool",
|
||||
{"model": os.environ.get("MISTLE_LLM_MODEL", "mistral/mistral-small-2407")},
|
||||
),
|
||||
"intelligentcommunication": (
|
||||
"intelligent_tool",
|
||||
"IntelligentCommunicationTool",
|
||||
{"model": os.environ.get("MISTLE_LLM_MODEL", "mistral/mistral-small-2407")},
|
||||
),
|
||||
}
|
||||
|
||||
AVAILABLE_TOOL_NAMES = sorted(set(BUILTIN_TOOLS.keys()))
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class IntelligentAgent(Agent):
|
|||
max_output_tokens: int = 200
|
||||
instruction: str = ""
|
||||
turn_delay: float = 0.0
|
||||
allowed_tools: dict[str, str] = field(default_factory=dict)
|
||||
allowed_tools: tuple[str, ...] = ()
|
||||
history: Deque[dict[str, str]] = field(default_factory=deque, init=False)
|
||||
|
||||
def observe(self, message: str) -> None:
|
||||
|
|
@ -60,15 +60,13 @@ class IntelligentAgent(Agent):
|
|||
messages.append({"role": "system", "content": self.instruction})
|
||||
messages.extend(self.history)
|
||||
if self.allowed_tools:
|
||||
tool_list = "\n".join(
|
||||
f"- {name}: {desc}" for name, desc in self.allowed_tools.items()
|
||||
)
|
||||
tool_list = ", ".join(self.allowed_tools)
|
||||
messages.append(
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Available tools (only use these names when type=tool):\n"
|
||||
f"{tool_list}"
|
||||
"Available tools: "
|
||||
f"{tool_list}. Use only these tool names when type=tool; do not invent new tools."
|
||||
),
|
||||
}
|
||||
)
|
||||
|
|
@ -131,20 +129,14 @@ class IntelligentAgent(Agent):
|
|||
print("[Agent] LLM returned empty action", file=sys.stderr)
|
||||
return
|
||||
|
||||
allowed_map = {
|
||||
name.lower(): (name, desc)
|
||||
for name, desc in self.allowed_tools.items()
|
||||
}
|
||||
allowed_map = {tool.lower(): tool for tool in self.allowed_tools}
|
||||
|
||||
if action_type == "tool":
|
||||
lower = value.lower()
|
||||
if allowed_map and lower not in allowed_map:
|
||||
print(
|
||||
f"[Agent] Tool '{value}' not in allowed list {list(self.allowed_tools)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(f"[Agent] Tool '{value}' not in allowed list {self.allowed_tools}", file=sys.stderr)
|
||||
return
|
||||
canonical, _ = allowed_map.get(lower, (value, ""))
|
||||
canonical = allowed_map.get(lower, value)
|
||||
success = invoke_tool(canonical)
|
||||
print(f"[Agent] Executed tool: {canonical} (success={success})")
|
||||
self.history.append({"role": "assistant", "content": f"TOOL {canonical}"})
|
||||
|
|
@ -156,60 +148,52 @@ class IntelligentAgent(Agent):
|
|||
print(f"[Agent] Sent command: {value}")
|
||||
self.history.append({"role": "assistant", "content": f"COMMAND {value}"})
|
||||
self._trim_history()
|
||||
elif action_type == "end":
|
||||
print("[Agent] LLM requested to end the session.")
|
||||
self.history.append({"role": "assistant", "content": "END"})
|
||||
self._trim_history()
|
||||
stop_event.set()
|
||||
break
|
||||
else:
|
||||
print(f"[Agent] Unknown action type '{action_type}'", file=sys.stderr)
|
||||
return
|
||||
|
||||
if self.turn_delay > 0 and stop_event.wait(self.turn_delay):
|
||||
break
|
||||
if self.turn_delay > 0 and stop_event.wait(self.turn_delay):
|
||||
return
|
||||
|
||||
messages = [{"role": "system", "content": self.system_prompt}]
|
||||
if self.instruction:
|
||||
messages.append({"role": "system", "content": self.instruction})
|
||||
messages.extend(self.history)
|
||||
if self.allowed_tools:
|
||||
tool_list = "\n".join(
|
||||
f"- {name}: {desc}" for name, desc in self.allowed_tools.items()
|
||||
)
|
||||
messages.append(
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Available tools (only use these names when type=tool):\n"
|
||||
f"{tool_list}"
|
||||
),
|
||||
}
|
||||
)
|
||||
messages = [{"role": "system", "content": self.system_prompt}]
|
||||
if self.instruction:
|
||||
messages.append({"role": "system", "content": self.instruction})
|
||||
messages.extend(self.history)
|
||||
if self.allowed_tools:
|
||||
tool_list = ", ".join(self.allowed_tools)
|
||||
messages.append(
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Respond with JSON only. Schema: {\n"
|
||||
" \"type\": \"tool\" or \"command\" or \"end\",\n"
|
||||
" \"value\": string,\n"
|
||||
" \"notes\": optional string\n}"""
|
||||
"Available tools: "
|
||||
f"{tool_list}. Use only these tool names when type=tool; do not invent new tools."
|
||||
),
|
||||
}
|
||||
)
|
||||
if not self.history or self.history[-1]["role"] != "assistant":
|
||||
messages.append(
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "I will decide the next action now.",
|
||||
}
|
||||
)
|
||||
messages.append(
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"Respond with JSON only. Schema: {\n"
|
||||
" \"type\": \"tool\" or \"command\",\n"
|
||||
" \"value\": string,\n"
|
||||
" \"notes\": optional string\n}"""
|
||||
),
|
||||
}
|
||||
)
|
||||
if not self.history or self.history[-1]["role"] != "assistant":
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the next action you will take?",
|
||||
"role": "assistant",
|
||||
"content": "I will decide the next action now.",
|
||||
}
|
||||
)
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the next action you will take?",
|
||||
}
|
||||
)
|
||||
|
||||
print("[Agent] Intelligent agent finished.")
|
||||
|
||||
|
|
|
|||
12
tools.py
12
tools.py
|
|
@ -21,24 +21,18 @@ class Tool(ABC):
|
|||
|
||||
|
||||
@dataclass
|
||||
class LookTool(Tool):
|
||||
"""Tool that sends a look command to refresh the room description."""
|
||||
class SimpleTool(Tool):
|
||||
"""Minimal tool that always returns the same command."""
|
||||
|
||||
default_command: str = "schau"
|
||||
last_output: str = field(default="", init=False)
|
||||
needs_look: bool = field(default=True, init=False)
|
||||
|
||||
def observe(self, output: str) -> None:
|
||||
if output:
|
||||
self.last_output = output
|
||||
# Once we received output, we consider the look complete.
|
||||
self.needs_look = False
|
||||
|
||||
def decide(self) -> Optional[str]:
|
||||
if self.needs_look:
|
||||
self.needs_look = False
|
||||
return self.default_command
|
||||
return None
|
||||
return self.default_command
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue