Compare commits
2 commits
9af879eadc
...
5c4d00fb2a
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c4d00fb2a | |||
| 348398616a |
10 changed files with 225 additions and 23 deletions
7
GOAL.md
Normal file
7
GOAL.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
Explore cautiously and map nearby rooms.
|
||||||
|
|
||||||
|
Priorities:
|
||||||
|
- Avoid obvious combat when possible.
|
||||||
|
- Prefer gathering room information before moving.
|
||||||
|
- Respond politely to direct tells.
|
||||||
|
- Keep actions short and reversible.
|
||||||
15
README.md
15
README.md
|
|
@ -65,7 +65,7 @@ Python-based Telnet helper for connecting to MUD servers, handling login flows,
|
||||||
#agent intelligent
|
#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 (`look`, `move`, `movement`, `explore`, `communication`, `intelligentcommunication`) and refuses unknown names. It can call `explore` in both forms: `explore` and `explore <target1>,<target2>`.
|
||||||
|
|
||||||
9. Prefer a TUI? Run `python textual_ui.py` to open a two-pane interface with a bottom input field:
|
9. Prefer a TUI? Run `python textual_ui.py` to open a two-pane interface with a bottom input field:
|
||||||
- Left pane: live MUD stream (server output plus outgoing actions from you, tools, and agents).
|
- Left pane: live MUD stream (server output plus outgoing actions from you, tools, and agents).
|
||||||
|
|
@ -85,8 +85,9 @@ 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_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_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_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: `look` (default, alias `simple`), `explore`, `communication`, `movement`, `intelligent`/`intelligentcommunication` (LLM-backed), or custom spec `module:ClassName`. `explore` accepts optional targets via `explore <target1>,<target2>` (or `explore:<target1>,<target2>`). |
|
||||||
| `MISTLE_SIDELOAD_TOOL` | ❌ | Comma-separated list of tool specs that should always run in the background (same syntax as `MISTLE_TOOL`). Useful for running `intelligent` alongside another primary tool. |
|
| `MISTLE_SIDELOAD_TOOL` | ❌ | Comma-separated list of tool specs that should always run in the background (same syntax as `MISTLE_TOOL`). Useful for running `intelligent` alongside another primary tool. |
|
||||||
|
| `MISTLE_AUTOPLAY` | ❌ | When truthy (`1`, `true`, `yes`, `on`), starts an intelligent agent automatically at startup and injects instructions from `GOAL.md`. |
|
||||||
| `MISTLE_LLM_MODEL` | ❌ | Override the `litellm` model used by the intelligent tool (defaults to `mistral/mistral-small-2407`). |
|
| `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. |
|
| `MISTRAL_API_KEY` | ❌ | API key used by `IntelligentCommunicationTool` (via `litellm`) when calling the `mistral/mistral-small-2407` model. |
|
||||||
|
|
||||||
|
|
@ -96,6 +97,12 @@ All variables can be placed in the `.env` file (one `KEY=value` per line) or pro
|
||||||
- This applies to both the intelligent agent and the intelligent communication tool.
|
- This applies to both the intelligent agent and the intelligent communication tool.
|
||||||
- If `SYSTEM.md` is missing or empty, the app falls back to built-in defaults and logs a warning.
|
- If `SYSTEM.md` is missing or empty, the app falls back to built-in defaults and logs a warning.
|
||||||
|
|
||||||
|
## Goal File
|
||||||
|
|
||||||
|
- `GOAL.md` is used when autoplay is enabled (`MISTLE_AUTOPLAY=true`).
|
||||||
|
- On startup, the app launches the intelligent agent automatically and passes the contents of `GOAL.md` as runtime instructions.
|
||||||
|
- If `GOAL.md` is missing or empty, autoplay still starts but without extra instructions.
|
||||||
|
|
||||||
## Tool Development
|
## Tool Development
|
||||||
|
|
||||||
- Implement new tools by subclassing `tools.Tool` and overriding `observe()` and `decide()`.
|
- Implement new tools by subclassing `tools.Tool` and overriding `observe()` and `decide()`.
|
||||||
|
|
@ -104,7 +111,7 @@ All variables can be placed in the `.env` file (one `KEY=value` per line) or pro
|
||||||
- Placing the class elsewhere and configuring `MISTLE_TOOL` to `your_module:YourTool`.
|
- Placing the class elsewhere and configuring `MISTLE_TOOL` to `your_module:YourTool`.
|
||||||
- `observe(output)` receives the latest server text; `decide()` returns the next command string or `None` to stay idle.
|
- `observe(output)` receives the latest server text; `decide()` returns the next command string or `None` to stay idle.
|
||||||
- Commands issued by the tool are throttled to one per second so manual commands can still interleave smoothly.
|
- Commands issued by the tool are throttled to one per second so manual commands can still interleave smoothly.
|
||||||
- `ExploreTool` showcases a richer workflow: it sends `schau`, identifies German nouns, inspects each with `untersuche`, and prints `[Tool]` progress updates like `Explored 3/7 — untersuche Tisch`.
|
- `ExploreTool` supports two modes: room scan mode (`schau` + noun discovery) and targeted mode (`explore <target1>,<target2>` or `explore:<target1>,<target2>`) which inspects only explicit targets.
|
||||||
- `MovementTool` parses room descriptions/exits and issues a single direction command, preferring unvisited exits and randomising choices to avoid oscillation. Trigger it via `#execute move` (or set `MISTLE_TOOL=movement` for continuous roaming).
|
- `MovementTool` parses room descriptions/exits and issues a single direction command, preferring unvisited exits and randomising choices to avoid oscillation. Trigger it via `#execute move` (or set `MISTLE_TOOL=movement` for continuous roaming).
|
||||||
- `CommunicationTool` auto-replies to every direct tell with a canned greeting, while `IntelligentCommunicationTool` routes each tell through `litellm` (default model `mistral/mistral-small-2407`) to craft a contextual answer via the configured LLM.
|
- `CommunicationTool` auto-replies to every direct tell with a canned greeting, while `IntelligentCommunicationTool` routes each tell through `litellm` (default model `mistral/mistral-small-2407`) to craft a contextual answer via the configured LLM.
|
||||||
- To keep specific helpers always-on (for example, the intelligent communication tool), set `MISTLE_SIDELOAD_TOOL=intelligent`. Multiple specs can be separated with commas; each runs in its own listener thread in parallel with the primary tool or interactive session.
|
- To keep specific helpers always-on (for example, the intelligent communication tool), set `MISTLE_SIDELOAD_TOOL=intelligent`. Multiple specs can be separated with commas; each runs in its own listener thread in parallel with the primary tool or interactive session.
|
||||||
|
|
@ -112,7 +119,7 @@ All variables can be placed in the `.env` file (one `KEY=value` per line) or pro
|
||||||
## On-Demand Tools
|
## On-Demand Tools
|
||||||
|
|
||||||
- When `MISTLE_TOOL_MODE` is **off**, you can trigger an ephemeral tool at any time with `#execute <tool_spec>`.
|
- 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 look`, `#execute explore`, `#execute explore Tisch,Truhe`, `#execute explore:Tisch,Truhe`, `#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.
|
- 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
|
## Danger Zone
|
||||||
|
|
|
||||||
11
SYSTEM.md
11
SYSTEM.md
|
|
@ -9,10 +9,17 @@ Your goal is to level up as much as possible and explore the world.
|
||||||
|
|
||||||
## Tool Usage
|
## Tool Usage
|
||||||
Use tools only when appropriate. Think how you can solve problems without
|
Use tools only when appropriate. Think how you can solve problems without
|
||||||
using a tool. Do not use the "exploration" tool multiple times in a row.
|
using a tool. Only use the "explore" tool as a last resort to gather information
|
||||||
|
as it requires a lot of time to run.
|
||||||
|
When using "explore", prefer targeted calls such as `explore Tisch,Truhe`
|
||||||
|
if you only need specific details. Use plain `explore` only when full-room
|
||||||
|
inspection is actually needed.
|
||||||
|
|
||||||
## Mud Commands
|
## Mud Commands
|
||||||
The following MUD commands may be helpful to you
|
The following MUD commands may be helpful to you
|
||||||
schau - get a description of the current environment
|
schau - get a description of the current environment
|
||||||
info - examine your own stats
|
info - examine your own stats
|
||||||
hilfe - get additional help in general and about specific commands
|
hilfe - get additional help in general and about specific commands
|
||||||
|
untersuche - examine a single object or detail in the current room or your inventory
|
||||||
|
inv - show your current inventory
|
||||||
|
toete - attacks a NPC
|
||||||
|
|
|
||||||
29
app.py
29
app.py
|
|
@ -6,6 +6,7 @@ from typing import Optional
|
||||||
|
|
||||||
from agent_runtime import run_agent
|
from agent_runtime import run_agent
|
||||||
from agents import Agent, build_agent
|
from agents import Agent, build_agent
|
||||||
|
from goal_loader import load_goal_instructions
|
||||||
from mud_env import load_env_file, read_connection_settings, read_tool_settings
|
from mud_env import load_env_file, read_connection_settings, read_tool_settings
|
||||||
from mud_session import (
|
from mud_session import (
|
||||||
SessionState,
|
SessionState,
|
||||||
|
|
@ -148,7 +149,10 @@ def main() -> int:
|
||||||
print(f"[Agent] Failed to configure '{spec}': {exc}", file=sys.stderr)
|
print(f"[Agent] Failed to configure '{spec}': {exc}", file=sys.stderr)
|
||||||
return
|
return
|
||||||
|
|
||||||
_prime_agent(temp_agent, state.snapshot_output())
|
_start_agent_thread(temp_agent, label=spec)
|
||||||
|
|
||||||
|
def _start_agent_thread(agent: Agent, *, label: str) -> None:
|
||||||
|
_prime_agent(agent, state.snapshot_output())
|
||||||
|
|
||||||
def run_tool_instance(tool: Tool) -> bool:
|
def run_tool_instance(tool: Tool) -> bool:
|
||||||
run_tool_loop(
|
run_tool_loop(
|
||||||
|
|
@ -160,12 +164,12 @@ def main() -> int:
|
||||||
auto_stop=True,
|
auto_stop=True,
|
||||||
auto_stop_idle=AUTO_STOP_IDLE_SECONDS,
|
auto_stop_idle=AUTO_STOP_IDLE_SECONDS,
|
||||||
)
|
)
|
||||||
_prime_agent(temp_agent, state.snapshot_output())
|
_prime_agent(agent, state.snapshot_output())
|
||||||
return True
|
return True
|
||||||
|
|
||||||
thread = Thread(
|
thread = Thread(
|
||||||
target=run_agent,
|
target=run_agent,
|
||||||
args=(temp_agent,),
|
args=(agent,),
|
||||||
kwargs={
|
kwargs={
|
||||||
"build_tool": build_tool,
|
"build_tool": build_tool,
|
||||||
"run_tool": run_tool_instance,
|
"run_tool": run_tool_instance,
|
||||||
|
|
@ -175,9 +179,26 @@ def main() -> int:
|
||||||
daemon=True,
|
daemon=True,
|
||||||
)
|
)
|
||||||
agent_threads.append(thread)
|
agent_threads.append(thread)
|
||||||
print(f"[Agent] Executing {spec!r}")
|
print(f"[Agent] Executing {label!r}")
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
if tool_settings.autoplay:
|
||||||
|
try:
|
||||||
|
autoplay_agent = build_agent("intelligent", allowed_tools=TOOL_DESCRIPTIONS)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(f"[Autoplay] Failed to configure intelligent agent: {exc}", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
goal = load_goal_instructions()
|
||||||
|
if goal:
|
||||||
|
setattr(autoplay_agent, "instruction", goal)
|
||||||
|
print("[Autoplay] Loaded instructions from GOAL.md")
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"[Autoplay] GOAL.md missing or empty; starting without extra instructions",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
_start_agent_thread(autoplay_agent, label="intelligent (autoplay)")
|
||||||
|
|
||||||
interrupted = False
|
interrupted = False
|
||||||
try:
|
try:
|
||||||
interactive_session(
|
interactive_session(
|
||||||
|
|
|
||||||
13
goal_loader.py
Normal file
13
goal_loader.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def load_goal_instructions(path: str = "GOAL.md") -> str:
|
||||||
|
goal_path = Path(path)
|
||||||
|
if not goal_path.exists():
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return goal_path.read_text(encoding="utf-8").strip()
|
||||||
|
except OSError: # pragma: no cover - environment specific
|
||||||
|
return ""
|
||||||
|
|
@ -83,7 +83,7 @@ class IntelligentAgent(Agent):
|
||||||
"content": (
|
"content": (
|
||||||
"Respond with JSON only. Schema: {\n"
|
"Respond with JSON only. Schema: {\n"
|
||||||
" \"type\": \"tool\" or \"command\",\n"
|
" \"type\": \"tool\" or \"command\",\n"
|
||||||
" \"value\": string (tool name or raw command),\n"
|
" \"value\": string (tool name or raw command; for targeted explore use 'explore <target1>,<target2>'),\n"
|
||||||
" \"notes\": optional string explanation\n}"""
|
" \"notes\": optional string explanation\n}"""
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
@ -142,17 +142,25 @@ class IntelligentAgent(Agent):
|
||||||
}
|
}
|
||||||
|
|
||||||
if action_type == "tool":
|
if action_type == "tool":
|
||||||
lower = value.lower()
|
base_name, args = self._split_tool_value(value)
|
||||||
|
lower = base_name.lower()
|
||||||
if allowed_map and lower not in allowed_map:
|
if allowed_map and lower not in allowed_map:
|
||||||
print(
|
print(
|
||||||
f"[Agent] Tool '{value}' not in allowed list {list(self.allowed_tools)}",
|
f"[Agent] Tool '{value}' not in allowed list {list(self.allowed_tools)}",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
canonical, _ = allowed_map.get(lower, (value, ""))
|
canonical, _ = allowed_map.get(lower, (base_name, ""))
|
||||||
success = invoke_tool(canonical)
|
if args and canonical.lower() != "explore":
|
||||||
print(f"[Agent] Executed tool: {canonical} (success={success})")
|
print(
|
||||||
self.history.append({"role": "assistant", "content": f"TOOL {canonical}"})
|
f"[Agent] Tool '{canonical}' does not support arguments: {args!r}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
tool_spec = canonical if not args else f"{canonical} {args}"
|
||||||
|
success = invoke_tool(tool_spec)
|
||||||
|
print(f"[Agent] Executed tool: {tool_spec} (success={success})")
|
||||||
|
self.history.append({"role": "assistant", "content": f"TOOL {tool_spec}"})
|
||||||
self._trim_history()
|
self._trim_history()
|
||||||
if not success:
|
if not success:
|
||||||
return
|
return
|
||||||
|
|
@ -197,7 +205,7 @@ class IntelligentAgent(Agent):
|
||||||
"content": (
|
"content": (
|
||||||
"Respond with JSON only. Schema: {\n"
|
"Respond with JSON only. Schema: {\n"
|
||||||
" \"type\": \"tool\" or \"command\" or \"end\",\n"
|
" \"type\": \"tool\" or \"command\" or \"end\",\n"
|
||||||
" \"value\": string,\n"
|
" \"value\": string (for targeted explore use 'explore <target1>,<target2>'),\n"
|
||||||
" \"notes\": optional string\n}"""
|
" \"notes\": optional string\n}"""
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
@ -233,3 +241,15 @@ class IntelligentAgent(Agent):
|
||||||
if start == -1 or end == -1 or end < start:
|
if start == -1 or end == -1 or end < start:
|
||||||
raise json.JSONDecodeError("No JSON object found", text, 0)
|
raise json.JSONDecodeError("No JSON object found", text, 0)
|
||||||
return text[start : end + 1]
|
return text[start : end + 1]
|
||||||
|
|
||||||
|
def _split_tool_value(self, value: str) -> tuple[str, str]:
|
||||||
|
raw = value.strip()
|
||||||
|
if not raw:
|
||||||
|
return "", ""
|
||||||
|
if " " in raw:
|
||||||
|
base, args = raw.split(" ", 1)
|
||||||
|
return base.strip(), args.strip()
|
||||||
|
if ":" in raw:
|
||||||
|
base, args = raw.split(":", 1)
|
||||||
|
return base.strip(), args.strip()
|
||||||
|
return raw, ""
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class ToolSettings:
|
||||||
tool_mode: bool
|
tool_mode: bool
|
||||||
tool_spec: str
|
tool_spec: str
|
||||||
sideload_specs: list[str]
|
sideload_specs: list[str]
|
||||||
|
autoplay: bool
|
||||||
|
|
||||||
|
|
||||||
def load_env_file(path: str = ".env") -> None:
|
def load_env_file(path: str = ".env") -> None:
|
||||||
|
|
@ -83,8 +84,10 @@ def read_tool_settings() -> ToolSettings:
|
||||||
tool_mode = parse_bool(os.environ.get("MISTLE_TOOL_MODE", ""))
|
tool_mode = parse_bool(os.environ.get("MISTLE_TOOL_MODE", ""))
|
||||||
tool_spec = os.environ.get("MISTLE_TOOL", "")
|
tool_spec = os.environ.get("MISTLE_TOOL", "")
|
||||||
sideload_specs = parse_csv(os.environ.get("MISTLE_SIDELOAD_TOOL", ""))
|
sideload_specs = parse_csv(os.environ.get("MISTLE_SIDELOAD_TOOL", ""))
|
||||||
|
autoplay = parse_bool(os.environ.get("MISTLE_AUTOPLAY", ""))
|
||||||
return ToolSettings(
|
return ToolSettings(
|
||||||
tool_mode=tool_mode,
|
tool_mode=tool_mode,
|
||||||
tool_spec=tool_spec,
|
tool_spec=tool_spec,
|
||||||
sideload_specs=sideload_specs,
|
sideload_specs=sideload_specs,
|
||||||
|
autoplay=autoplay,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
64
mud_tools.py
64
mud_tools.py
|
|
@ -36,7 +36,10 @@ TOOL_REGISTRY = {
|
||||||
"module": "tools",
|
"module": "tools",
|
||||||
"class": "ExploreTool",
|
"class": "ExploreTool",
|
||||||
"kwargs": {},
|
"kwargs": {},
|
||||||
"description": "Sends 'schau' once, then 'untersuche <noun>' for each noun found in the room description.",
|
"description": (
|
||||||
|
"Sends 'schau' once, then 'untersuche <noun>' for each noun found in the "
|
||||||
|
"room description. Supports targeted mode: 'explore <target1>,<target2>'."
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"communication": {
|
"communication": {
|
||||||
"module": "tools",
|
"module": "tools",
|
||||||
|
|
@ -70,13 +73,14 @@ TOOL_DESCRIPTIONS = {
|
||||||
def build_tool(spec: str) -> Tool:
|
def build_tool(spec: str) -> Tool:
|
||||||
"""Instantiate a tool based on configuration."""
|
"""Instantiate a tool based on configuration."""
|
||||||
normalized = spec.strip() or "look"
|
normalized = spec.strip() or "look"
|
||||||
key = normalized.lower()
|
key, args = _parse_builtin_spec(normalized)
|
||||||
|
|
||||||
if key in TOOL_REGISTRY:
|
if key in TOOL_REGISTRY:
|
||||||
meta = TOOL_REGISTRY[key]
|
meta = TOOL_REGISTRY[key]
|
||||||
module_name = meta["module"]
|
module_name = meta["module"]
|
||||||
class_name = meta["class"]
|
class_name = meta["class"]
|
||||||
kwargs = meta.get("kwargs", {})
|
kwargs = dict(meta.get("kwargs", {}))
|
||||||
|
kwargs = _apply_builtin_args(key, args, kwargs)
|
||||||
try:
|
try:
|
||||||
module = import_module(module_name)
|
module = import_module(module_name)
|
||||||
tool_cls = getattr(module, class_name)
|
tool_cls = getattr(module, class_name)
|
||||||
|
|
@ -99,6 +103,60 @@ def build_tool(spec: str) -> Tool:
|
||||||
raise RuntimeError(f"Unknown tool spec '{spec}'.")
|
raise RuntimeError(f"Unknown tool spec '{spec}'.")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_builtin_spec(spec: str) -> tuple[str, Optional[str]]:
|
||||||
|
if ":" in spec:
|
||||||
|
prefix, suffix = spec.split(":", 1)
|
||||||
|
key = prefix.lower()
|
||||||
|
if key in {"explore"}:
|
||||||
|
return key, suffix.strip()
|
||||||
|
|
||||||
|
parts = spec.split(maxsplit=1)
|
||||||
|
key = parts[0].lower()
|
||||||
|
if key not in TOOL_REGISTRY:
|
||||||
|
return spec.lower(), None
|
||||||
|
args = parts[1].strip() if len(parts) > 1 else None
|
||||||
|
return key, args
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_builtin_args(
|
||||||
|
key: str, args: Optional[str], kwargs: dict
|
||||||
|
) -> dict:
|
||||||
|
if not args:
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
if key in {"explore"}:
|
||||||
|
targets = _parse_targets(args)
|
||||||
|
if not targets:
|
||||||
|
raise RuntimeError("Explore targets cannot be empty")
|
||||||
|
kwargs["targets"] = tuple(targets)
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Tool '{key}' does not accept arguments. Received extra input: {args!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_targets(raw: str) -> list[str]:
|
||||||
|
value = raw.strip()
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if "," in value:
|
||||||
|
parts = [segment.strip() for segment in value.split(",") if segment.strip()]
|
||||||
|
else:
|
||||||
|
parts = [value]
|
||||||
|
|
||||||
|
deduped: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for part in parts:
|
||||||
|
lowered = part.lower()
|
||||||
|
if lowered in seen:
|
||||||
|
continue
|
||||||
|
seen.add(lowered)
|
||||||
|
deduped.append(part)
|
||||||
|
return deduped
|
||||||
|
|
||||||
|
|
||||||
def _instantiate_tool(
|
def _instantiate_tool(
|
||||||
tool_cls: Type[Tool], tool_spec: str, kwargs: Optional[dict] = None
|
tool_cls: Type[Tool], tool_spec: str, kwargs: Optional[dict] = None
|
||||||
) -> Tool:
|
) -> Tool:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from textual.widgets import Footer, Header, Input, Label, Log, Static
|
||||||
|
|
||||||
from agent_runtime import run_agent
|
from agent_runtime import run_agent
|
||||||
from agents import Agent, build_agent
|
from agents import Agent, build_agent
|
||||||
|
from goal_loader import load_goal_instructions
|
||||||
from mud_env import load_env_file, read_connection_settings, read_tool_settings
|
from mud_env import load_env_file, read_connection_settings, read_tool_settings
|
||||||
from mud_session import SessionState, graceful_shutdown, login, run_tool_loop
|
from mud_session import SessionState, graceful_shutdown, login, run_tool_loop
|
||||||
from mud_tools import TOOL_DESCRIPTIONS, build_tool
|
from mud_tools import TOOL_DESCRIPTIONS, build_tool
|
||||||
|
|
@ -196,6 +197,9 @@ class MudUI(App):
|
||||||
sideload_seen.add(lowered)
|
sideload_seen.add(lowered)
|
||||||
self._launch_persistent_tool(spec)
|
self._launch_persistent_tool(spec)
|
||||||
|
|
||||||
|
if tool_settings.autoplay:
|
||||||
|
self._start_autoplay_agent()
|
||||||
|
|
||||||
self._set_status(f"Connected to {connection.host}:{connection.port}")
|
self._set_status(f"Connected to {connection.host}:{connection.port}")
|
||||||
self.input.focus()
|
self.input.focus()
|
||||||
self.log_mud(f"Connected to {connection.host}:{connection.port}")
|
self.log_mud(f"Connected to {connection.host}:{connection.port}")
|
||||||
|
|
@ -326,7 +330,28 @@ class MudUI(App):
|
||||||
self.log_brain(f"[Agent] Failed to configure '{spec}': {exc}")
|
self.log_brain(f"[Agent] Failed to configure '{spec}': {exc}")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log_brain(f"[Agent] Executing {spec!r}")
|
self._start_agent_instance(agent, label=spec)
|
||||||
|
|
||||||
|
def _start_autoplay_agent(self) -> None:
|
||||||
|
try:
|
||||||
|
autoplay_agent = build_agent("intelligent", allowed_tools=TOOL_DESCRIPTIONS)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
self.log_brain(f"[Autoplay] Failed to configure intelligent agent: {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
goal = load_goal_instructions()
|
||||||
|
if goal:
|
||||||
|
setattr(autoplay_agent, "instruction", goal)
|
||||||
|
self.log_brain("[Autoplay] Loaded instructions from GOAL.md")
|
||||||
|
else:
|
||||||
|
self.log_brain(
|
||||||
|
"[Autoplay] GOAL.md missing or empty; starting without extra instructions"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._start_agent_instance(autoplay_agent, label="intelligent (autoplay)")
|
||||||
|
|
||||||
|
def _start_agent_instance(self, agent: Agent, *, label: str) -> None:
|
||||||
|
self.log_brain(f"[Agent] Executing {label!r}")
|
||||||
self._prime_agent(agent)
|
self._prime_agent(agent)
|
||||||
|
|
||||||
def run_tool_instance(tool: Tool) -> bool:
|
def run_tool_instance(tool: Tool) -> bool:
|
||||||
|
|
@ -355,7 +380,8 @@ class MudUI(App):
|
||||||
stop_event=self._stop_event,
|
stop_event=self._stop_event,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._start_worker(run, name=f"agent-{spec}")
|
worker_name = f"agent-{label.replace(' ', '-').lower()}"
|
||||||
|
self._start_worker(run, name=worker_name)
|
||||||
|
|
||||||
def _prime_agent(self, agent: Agent) -> None:
|
def _prime_agent(self, agent: Agent) -> None:
|
||||||
last_output = self.state.snapshot_output()
|
last_output = self.state.snapshot_output()
|
||||||
|
|
|
||||||
42
tools.py
42
tools.py
|
|
@ -43,10 +43,11 @@ class LookTool(Tool):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ExploreTool(Tool):
|
class ExploreTool(Tool):
|
||||||
"""Tool that inspects every noun it discovers in the room description."""
|
"""Tool that inspects room nouns, or only explicit targets when provided."""
|
||||||
|
|
||||||
look_command: str = "schau"
|
look_command: str = "schau"
|
||||||
inspect_command: str = "untersuche"
|
inspect_command: str = "untersuche"
|
||||||
|
targets: Tuple[str, ...] = ()
|
||||||
nouns_pattern: Pattern[str] = field(
|
nouns_pattern: Pattern[str] = field(
|
||||||
default_factory=lambda: re.compile(r"\b[A-ZÄÖÜ][A-Za-zÄÖÜäöüß-]*\b")
|
default_factory=lambda: re.compile(r"\b[A-ZÄÖÜ][A-Za-zÄÖÜäöüß-]*\b")
|
||||||
)
|
)
|
||||||
|
|
@ -56,10 +57,31 @@ class ExploreTool(Tool):
|
||||||
seen_targets: Set[str] = field(default_factory=set, init=False)
|
seen_targets: Set[str] = field(default_factory=set, init=False)
|
||||||
inspected_targets: Set[str] = field(default_factory=set, init=False)
|
inspected_targets: Set[str] = field(default_factory=set, init=False)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
normalized: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for raw_target in self.targets:
|
||||||
|
target = raw_target.strip()
|
||||||
|
key = target.lower()
|
||||||
|
if not target or key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
normalized.append(target)
|
||||||
|
self.targets = tuple(normalized)
|
||||||
|
for target in self.targets:
|
||||||
|
self.seen_targets.add(target.lower())
|
||||||
|
self.pending_targets.append(target)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def targeted_mode(self) -> bool:
|
||||||
|
return bool(self.targets)
|
||||||
|
|
||||||
def observe(self, output: str) -> None:
|
def observe(self, output: str) -> None:
|
||||||
if not output:
|
if not output:
|
||||||
return
|
return
|
||||||
self.last_output = output
|
self.last_output = output
|
||||||
|
if self.targeted_mode:
|
||||||
|
return
|
||||||
if not self.look_sent:
|
if not self.look_sent:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -72,6 +94,24 @@ class ExploreTool(Tool):
|
||||||
self.pending_targets.append(target)
|
self.pending_targets.append(target)
|
||||||
|
|
||||||
def decide(self) -> Optional[str]:
|
def decide(self) -> Optional[str]:
|
||||||
|
if self.targeted_mode:
|
||||||
|
if not self.look_sent:
|
||||||
|
self.look_sent = True
|
||||||
|
print(
|
||||||
|
f"[Tool] Targeted explore mode, inspecting {len(self.targets)} object(s)"
|
||||||
|
)
|
||||||
|
if not self.pending_targets:
|
||||||
|
return None
|
||||||
|
target = self.pending_targets.popleft()
|
||||||
|
key = target.lower()
|
||||||
|
self.inspected_targets.add(key)
|
||||||
|
progress = (
|
||||||
|
f"[Tool] Targeted {len(self.inspected_targets)}/{len(self.targets)} "
|
||||||
|
f"- untersuche {target}"
|
||||||
|
)
|
||||||
|
print(progress)
|
||||||
|
return f"{self.inspect_command} {target}"
|
||||||
|
|
||||||
if not self.look_sent:
|
if not self.look_sent:
|
||||||
self.look_sent = True
|
self.look_sent = True
|
||||||
print("[Tool] Exploring room, sending 'schau'")
|
print("[Tool] Exploring room, sending 'schau'")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue