from __future__ import annotations import re from dataclasses import dataclass from agents.master.base import SUPPORTED_DIRECTIONS _TOKEN_RE = re.compile(r"^[a-z0-9]+(?: [a-z0-9]+)*$") _BANNED_OBJECT_TOKENS = {"a", "an", "the"} @dataclass(frozen=True) class CliCommandAst: kind: str normalized_command: str arguments: tuple[str, ...] = () @dataclass(frozen=True) class CliCommandParseResult: valid: bool normalized_command: str | None = None ast: CliCommandAst | None = None error: str | None = None def parse_cli_command(raw_command: str) -> CliCommandParseResult: normalized = normalize_cli_command(raw_command) if not normalized: return CliCommandParseResult(valid=False, error="Command must not be empty.") if normalized in {"look", "inventory", "wait"}: return _ok(normalized, normalized) if normalized in SUPPORTED_DIRECTIONS: return _ok("move", f"go {normalized}", normalized) if normalized.startswith("go "): direction = normalized[3:].strip() if direction in SUPPORTED_DIRECTIONS: return _ok("move", f"go {direction}", direction) return CliCommandParseResult(valid=False, error="Unknown direction.") if match := re.fullmatch(r"look in (?P.+)", normalized): object_text = match.group("object").strip() return _object_result("look_in", normalized, object_text) if match := re.fullmatch(r"take (?P.+) from (?P.+)", normalized): return _two_object_result("take_from", normalized, match.group("object"), match.group("source")) one_target_patterns = { "open": r"open (?P.+)", "read": r"read (?P.+)", "talk": r"talk (?P.+)", "examine": r"examine (?P.+)", } for kind, pattern in one_target_patterns.items(): if match := re.fullmatch(pattern, normalized): object_text = match.group("object").strip() return _object_result(kind, normalized, object_text) if match := re.fullmatch(r"take (?P.+)", normalized): object_text = match.group("object").strip() return _object_result("take", normalized, object_text) if match := re.fullmatch(r"unlock (?P.+) with (?P.+)", normalized): return _two_object_result("unlock", normalized, match.group("object"), match.group("tool")) if match := re.fullmatch(r"use (?P.+) on (?P.+)", normalized): return _two_object_result("use", normalized, match.group("object"), match.group("target")) if match := re.fullmatch(r"combine (?P.+) with (?P.+)", normalized): return _two_object_result("combine", normalized, match.group("object"), match.group("target")) if match := re.fullmatch(r"give (?P.+) to (?P.+)", normalized): return _two_object_result("give", normalized, match.group("object"), match.group("target")) if match := re.fullmatch(r"submit (?P[a-z0-9]+(?: [a-z0-9]+)*)", normalized): answer = match.group("answer").strip() return _ok("submit", normalized, answer) return CliCommandParseResult(valid=False, error="Command does not match the strict CLI grammar.") def normalize_cli_command(raw_command: str) -> str: return re.sub(r"\s+", " ", raw_command.strip().lower()) def _object_result(kind: str, normalized_command: str, object_text: str) -> CliCommandParseResult: object_error = _validate_object_text(object_text) if object_error is not None: return CliCommandParseResult(valid=False, error=object_error) return _ok(kind, normalized_command, object_text) def _two_object_result(kind: str, normalized_command: str, first: str, second: str) -> CliCommandParseResult: first_error = _validate_object_text(first) if first_error is not None: return CliCommandParseResult(valid=False, error=first_error) second_error = _validate_object_text(second) if second_error is not None: return CliCommandParseResult(valid=False, error=second_error) return _ok(kind, normalized_command, first.strip(), second.strip()) def _validate_object_text(value: str) -> str | None: candidate = value.strip() if not candidate: return "Command target must not be empty." if not _TOKEN_RE.fullmatch(candidate): return "Command targets must use lowercase letters, numbers, and spaces only." if any(token in _BANNED_OBJECT_TOKENS for token in candidate.split()): return "Strict CLI commands must use exact parser-safe object names without articles." return None def _ok(kind: str, normalized_command: str, *arguments: str) -> CliCommandParseResult: return CliCommandParseResult( valid=True, normalized_command=normalized_command, ast=CliCommandAst(kind=kind, normalized_command=normalized_command, arguments=arguments), )