Harden command parsing (review fix)
interpret() now extracts the FIRST balanced [...] array and tolerates code fences / trailing prose, instead of a greedy [.*] that could swallow trailing bracketed text and fail to parse. Falls back gracefully to a spoken apology. Added regression tests for trailing brackets, fenced objects, and garbage. 44 tests passing; edge cases (angle 0, offset 0, negative moves, unknown stock) verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
892a376669
commit
7b5c58902c
|
|
@ -73,17 +73,47 @@ User said: "{utterance}"
|
|||
"""
|
||||
|
||||
|
||||
def _extract_calls(raw: str) -> list[dict] | None:
|
||||
"""Pull a JSON array of calls out of a model response, tolerating code
|
||||
fences and trailing prose. Tries the whole string, then the FIRST balanced
|
||||
[...] (not greedy-to-last-bracket, which would swallow trailing text)."""
|
||||
raw = raw.strip()
|
||||
if raw.startswith("```"):
|
||||
raw = re.sub(r"^```[a-zA-Z]*\n?", "", raw)
|
||||
raw = re.sub(r"\n?```$", "", raw).strip()
|
||||
|
||||
candidates = [raw]
|
||||
start = raw.find("[")
|
||||
if start != -1:
|
||||
depth = 0
|
||||
for i in range(start, len(raw)):
|
||||
if raw[i] == "[":
|
||||
depth += 1
|
||||
elif raw[i] == "]":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
candidates.append(raw[start:i + 1])
|
||||
break
|
||||
|
||||
for candidate in candidates:
|
||||
try:
|
||||
value = json.loads(candidate)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
if isinstance(value, dict):
|
||||
return [value]
|
||||
return None
|
||||
|
||||
|
||||
def interpret(utterance: str, schemas: str) -> list[dict]:
|
||||
prompt = SYSTEM.format(schemas=schemas, scene=scene_summary(), utterance=utterance)
|
||||
raw = _run(REASON_PROVIDER.split(), stdin=prompt)
|
||||
match = re.search(r"\[.*\]", raw, re.DOTALL) # tolerate stray text/fences
|
||||
if not match:
|
||||
return [{"tool": "say", "args": {"text": "Sorry, I didn't catch a command."}}]
|
||||
try:
|
||||
calls = json.loads(match.group(0))
|
||||
except json.JSONDecodeError:
|
||||
return [{"tool": "say", "args": {"text": "Sorry, I couldn't parse that."}}]
|
||||
return calls if isinstance(calls, list) else [calls]
|
||||
calls = _extract_calls(raw)
|
||||
if calls is None:
|
||||
return [{"tool": "say", "args": {"text": "Sorry, I couldn't parse that command."}}]
|
||||
return calls
|
||||
|
||||
|
||||
def dispatch(calls: list[dict], verbose: bool = True) -> list[str]:
|
||||
|
|
|
|||
|
|
@ -83,3 +83,18 @@ def test_interpret_handles_garbage(monkeypatch):
|
|||
monkeypatch.setattr(driver, "_run", lambda cmd, stdin="": "I'm not sure what you mean")
|
||||
calls = driver.interpret("blah", schemas="[]")
|
||||
assert calls[0]["tool"] == "say"
|
||||
|
||||
|
||||
def test_extract_calls_ignores_trailing_brackets():
|
||||
"""A greedy [.*] would swallow the trailing '[note]' and fail to parse."""
|
||||
raw = '[{"tool": "wood-undo", "args": {}}]\n\nLet me know [if that helps].'
|
||||
assert driver._extract_calls(raw) == [{"tool": "wood-undo", "args": {}}]
|
||||
|
||||
|
||||
def test_extract_calls_strips_fences_and_handles_object():
|
||||
assert driver._extract_calls('```json\n{"tool": "wood-clear", "args": {}}\n```') == \
|
||||
[{"tool": "wood-clear", "args": {}}]
|
||||
|
||||
|
||||
def test_extract_calls_returns_none_on_garbage():
|
||||
assert driver._extract_calls("no json here") is None
|
||||
|
|
|
|||
Loading…
Reference in New Issue