3ee67cb4d6
- Refactor `run_local_command` to manage subprocesses more effectively, ensuring child processes are terminated on timeout. - Introduce `_terminate` function to handle process group termination and capture output. - Implement `_command_output` to format command results with a character limit. - Add local intent recognition in `intents.py` to handle commands like "stop", "go to sleep", and "come here" without server interaction. - Normalize user input to match local intents while stripping filler words. - Update tests to cover new local intent functionality and ensure proper command handling. - Enhance speech processing to handle abbreviations and improve spoken output clarity.
113 lines
4.0 KiB
Python
113 lines
4.0 KiB
Python
"""Local intent recognition — pure string logic, no hardware or display.
|
|
|
|
The interesting tests are the negative ones: this feature's whole risk is
|
|
swallowing something that was meant for the server.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from bolt_pet import intents
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
|
|
@pytest.mark.parametrize("said, expected", [
|
|
("stop", "stop"),
|
|
("Stop!", "stop"),
|
|
("never mind", "stop"),
|
|
("be quiet", "stop"),
|
|
("go to sleep", "nap"),
|
|
("take a nap", "nap"),
|
|
("goodnight", "nap"),
|
|
("wake up", "wake"),
|
|
("come here", "come"),
|
|
("follow my cursor", "come"),
|
|
("get out of the way", "go_away"),
|
|
("hide", "go_away"),
|
|
("say that again", "repeat"),
|
|
("what did you say?", "repeat"),
|
|
("go for a walk", "wander_on"),
|
|
("stay put", "wander_off"),
|
|
("sit", "wander_off"),
|
|
("use your normal voice", "voice_reset"),
|
|
("go back to your normal voice", "voice_reset"),
|
|
("be yourself again", "voice_reset"),
|
|
])
|
|
def test_recognized_phrases(said, expected):
|
|
intent = intents.recognize(said)
|
|
assert intent is not None and intent.name == expected
|
|
|
|
|
|
@pytest.mark.parametrize("said", [
|
|
# Each of these starts with (or contains) an intent phrase, and every one is
|
|
# a real request. A substring match would eat all of them.
|
|
"stop the docker container",
|
|
"stop the deploy and tell me what broke",
|
|
"can you hide the window that's covering my terminal",
|
|
"come up with a name for this branch",
|
|
"repeat the last command but with sudo",
|
|
"what did you say the disk usage was on the server",
|
|
"move the config file to the backup directory",
|
|
"sit down and write me a haiku about kubernetes",
|
|
"what time is it",
|
|
"go to sleep mode on the server",
|
|
"",
|
|
" ",
|
|
# Pure filler leaves an empty string, which must not match anything.
|
|
"hey bolt",
|
|
"okay bolt please",
|
|
])
|
|
def test_real_requests_are_left_for_the_server(said):
|
|
assert intents.recognize(said) is None
|
|
|
|
|
|
def test_filler_is_stripped_from_both_ends():
|
|
assert intents.normalize("Hey Bolt, could you please just stop now?") == "stop"
|
|
assert intents.normalize("okay, come here buddy") == "come here"
|
|
|
|
|
|
def test_normalize_returns_empty_for_pure_filler():
|
|
assert intents.normalize("hey bolt") == ""
|
|
assert intents.normalize("...") == ""
|
|
|
|
|
|
def test_intents_carry_ui_actions_in_the_pet_actions_shape():
|
|
"""The action dicts go straight to PetWindow.apply_action, so they have to
|
|
match the vocabulary pet_actions.parse produces — no new UI cases."""
|
|
assert intents.recognize("come here").action == {"action": "move", "anchor": "cursor"}
|
|
assert intents.recognize("stay put").action == {"action": "wander", "enabled": False}
|
|
assert intents.recognize("go to sleep").action == {"action": "nap", "enabled": True}
|
|
|
|
|
|
def test_stop_says_nothing():
|
|
"""Answering "okay!" when told to be quiet defeats the purpose."""
|
|
intent = intents.recognize("be quiet")
|
|
assert intent.speak == "" and intent.action is None
|
|
|
|
|
|
def test_a_phrase_claimed_by_two_intents_fails_at_import(monkeypatch):
|
|
"""Without this guard the phrase would silently bind to whichever intent was
|
|
declared last — a table edit that looks fine and misbehaves on a mic."""
|
|
monkeypatch.setattr(intents, "_TABLE", (
|
|
(intents.Intent("stop"), ("enough",)),
|
|
(intents.Intent("nap"), ("enough",)),
|
|
))
|
|
with pytest.raises(ValueError, match="claimed by both"):
|
|
intents._build()
|
|
|
|
|
|
def test_a_phrase_of_pure_filler_fails_at_import(monkeypatch):
|
|
"""It would normalise to "" and then match any all-filler utterance."""
|
|
monkeypatch.setattr(intents, "_TABLE", ((intents.Intent("stop"), ("please bolt",)),))
|
|
with pytest.raises(ValueError, match="normalises to nothing"):
|
|
intents._build()
|
|
|
|
|
|
def test_every_table_phrase_round_trips():
|
|
for phrase, intent in intents._BY_PHRASE.items():
|
|
assert phrase, "a phrase normalised to nothing"
|
|
assert intents.recognize(phrase) is intent
|