173 lines
5.5 KiB
Python
173 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from importlib import import_module
|
|
from typing import Optional, Type
|
|
|
|
from tools import Tool
|
|
|
|
|
|
DEFAULT_LLM_MODEL = "mistral/mistral-small"
|
|
LLM_MODEL_ENV = "MISTLE_LLM_MODEL"
|
|
|
|
|
|
TOOL_REGISTRY = {
|
|
"look": {
|
|
"module": "tools",
|
|
"class": "LookTool",
|
|
"kwargs": {},
|
|
"description": "Sends the 'schau' look command to refresh the room description.",
|
|
},
|
|
"simple": {
|
|
"module": "tools",
|
|
"class": "LookTool",
|
|
"kwargs": {},
|
|
"description": "Alias of 'look'. 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. Supports targeted mode: 'explore <target1>,<target2>'."
|
|
),
|
|
},
|
|
"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": {},
|
|
"description": "Uses an LLM to craft a polite reply to private tells.",
|
|
},
|
|
"intelligentcommunication": {
|
|
"module": "intelligent_tool",
|
|
"class": "IntelligentCommunicationTool",
|
|
"kwargs": {},
|
|
"description": "Alias of 'intelligent'. Uses an LLM to craft a polite reply to private tells.",
|
|
},
|
|
}
|
|
|
|
TOOL_DESCRIPTIONS = {
|
|
name: meta["description"] for name, meta in TOOL_REGISTRY.items()
|
|
}
|
|
|
|
|
|
def build_tool(spec: str) -> Tool:
|
|
"""Instantiate a tool based on configuration."""
|
|
normalized = spec.strip() or "look"
|
|
key, args = _parse_builtin_spec(normalized)
|
|
|
|
if key in TOOL_REGISTRY:
|
|
meta = TOOL_REGISTRY[key]
|
|
module_name = meta["module"]
|
|
class_name = meta["class"]
|
|
kwargs = dict(meta.get("kwargs", {}))
|
|
kwargs = _apply_builtin_args(key, args, kwargs)
|
|
if key in {"intelligent", "intelligentcommunication"}:
|
|
default_model = kwargs.get("model", DEFAULT_LLM_MODEL)
|
|
env_model = os.environ.get(LLM_MODEL_ENV, "").strip()
|
|
kwargs["model"] = env_model or default_model
|
|
try:
|
|
module = import_module(module_name)
|
|
tool_cls = getattr(module, class_name)
|
|
except AttributeError as exc: # pragma: no cover - optional dependency
|
|
raise RuntimeError(f"{class_name} is not available in tools module") from exc
|
|
tool = _instantiate_tool(tool_cls, normalized, kwargs)
|
|
model_name = kwargs.get("model") if kwargs else None
|
|
if model_name:
|
|
print(f"[Tool] Using LLM model: {model_name}")
|
|
return tool
|
|
|
|
if ":" in normalized:
|
|
module_name, class_name = normalized.split(":", 1)
|
|
if not module_name or not class_name:
|
|
raise RuntimeError("MISTLE_TOOL must be in 'module:ClassName' format")
|
|
module = import_module(module_name)
|
|
tool_cls = getattr(module, class_name)
|
|
return _instantiate_tool(tool_cls, normalized)
|
|
|
|
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(
|
|
tool_cls: Type[Tool], tool_spec: str, kwargs: Optional[dict] = None
|
|
) -> Tool:
|
|
if not issubclass(tool_cls, Tool):
|
|
raise RuntimeError(f"{tool_spec} is not a Tool subclass")
|
|
try:
|
|
kwargs = kwargs or {}
|
|
return tool_cls(**kwargs)
|
|
except TypeError as exc:
|
|
raise RuntimeError(f"Failed to instantiate {tool_spec}: {exc}") from exc
|