Bolt Assistant: wake-phrase voice assistant for Bolt
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""What happens to a command the server relays back, when there's no screen
|
||||
to approve it on.
|
||||
|
||||
ASSISTANT_COMMANDS decides:
|
||||
|
||||
off nothing runs
|
||||
read (default) filectl list/read only: "what's in my Downloads?" works,
|
||||
nothing on the machine can change
|
||||
all everything, shell commands and file writes included, like
|
||||
Bolt-Pet. Anyone within earshot of the mic can ask for anything.
|
||||
|
||||
A refusal goes back to the model as the command's output, phrased so it
|
||||
tells you rather than retrying. Pet-only pseudo-commands (petctl,
|
||||
dialoguectl) are explained, never run.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
from . import config, file_ops
|
||||
|
||||
_PET = ("petctl", "bolt-pet", "dialoguectl")
|
||||
_LABEL = re.compile(r"^(?:command|cmd)\s*:\s*", re.IGNORECASE)
|
||||
|
||||
|
||||
def run_shell(command: str, timeout: int = None) -> str:
|
||||
timeout = timeout or config.COMMAND_TIMEOUT_SECONDS
|
||||
try:
|
||||
done = subprocess.run(
|
||||
command, shell=True, capture_output=True, text=True, errors="replace",
|
||||
stdin=subprocess.DEVNULL, timeout=timeout, cwd=str(Path.home()),
|
||||
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||
)
|
||||
return f"[exit {done.returncode}]\n{(done.stdout or '') + (done.stderr or '')}"[:6000]
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"[command timed out after {timeout}s]"
|
||||
except Exception as exc:
|
||||
return f"[command failed: {exc}]"
|
||||
|
||||
|
||||
def refusal(what: str, mode: str) -> str:
|
||||
return (f"[assistant] Not run: this is Bolt Assistant, a voice-only client, and it's set to "
|
||||
f"ASSISTANT_COMMANDS={mode}, which doesn't allow {what}. Tell the user in a sentence "
|
||||
"that you can't do that from here (Bolt Console can); don't retry.")
|
||||
|
||||
|
||||
class Tools:
|
||||
def __init__(self, mode: str = None, log: Callable[[str], None] = print,
|
||||
shell: Callable[[str], str] = run_shell):
|
||||
mode = mode or config.COMMANDS
|
||||
self.mode = mode if mode in ("off", "read", "all") else "read" # unknown: the safe default
|
||||
self.log = log
|
||||
self.shell = shell
|
||||
self.calls = 0
|
||||
|
||||
def __call__(self, command: str) -> str:
|
||||
self.calls += 1
|
||||
command = _LABEL.sub("", (command or "").strip(), count=1).strip().strip("`").strip()
|
||||
if not command:
|
||||
return "[assistant] empty command"
|
||||
first = command.split(None, 1)[0].lower()
|
||||
if first in _PET:
|
||||
self.log(f" (declined {first}: no pet body here)")
|
||||
return (f"[assistant] {first} isn't available: this is Bolt Assistant, voice only, "
|
||||
"with no pet body or screen.")
|
||||
try:
|
||||
action = file_ops.parse(command)
|
||||
except file_ops.FileOpError as exc:
|
||||
return f"[filectl] {exc}"
|
||||
if action is not None:
|
||||
return self._file(action)
|
||||
if self.mode != "all":
|
||||
self.log(f" (declined shell command: {command[:80]})")
|
||||
return refusal("shell commands", self.mode)
|
||||
self.log(f" $ {command[:120]}")
|
||||
return self.shell(command)
|
||||
|
||||
def _file(self, action: dict) -> str:
|
||||
kind = action["action"]
|
||||
allowed = {"off": set(), "read": {"help", "list", "read"},
|
||||
"all": {"help", "list", "read", "write", "edit"}}[self.mode]
|
||||
if kind not in allowed:
|
||||
self.log(f" (declined file {kind}: {action.get('path', '')})")
|
||||
return refusal(f"file {kind}s" if kind != "help" else "file access", self.mode)
|
||||
self.log(f" file {kind}: {action.get('path', '')}")
|
||||
try:
|
||||
return file_ops.execute(action)
|
||||
except file_ops.FileOpError as exc:
|
||||
return f"[filectl] {exc}"
|
||||
Reference in New Issue
Block a user