Fast-Path-Plugin raus, offizielle quick_commands rein (funktioniert auch in der TUI)

Root Cause fuers "geht trotzdem ans LLM": unser fast-paths-Plugin war auf
pre_gateway_dispatch registriert -- der Hook feuert laut Sourcecode nur fuer
die Gateway-Plattformen (Telegram/Discord/...), nicht fuer die TUI
(tui_gateway/server.py), die komplett am Gateway-Dispatcher vorbei direkt in
den Agent-Loop laeuft. Ein Patch an tui_gateway/server.py waere noetig
gewesen, aber Concurrency-kritischer Kern-Code -- zu riskant.

Hermes hat dafuer schon einen eigenen Mechanismus: quick_commands (type:
exec) bypassen den Agent-Loop nachweislich auf JEDER Plattform inkl. TUI,
kein LLM-Call, keine Tokens, offiziell dokumentiert. Trade-off: fester
Slash-Befehl statt Freitext (/next statt "naechstes Lied") -- gleiches
Prinzip wie Alexas Intent-Slots, ohne Slot-Fuellung.

scripts/spotify_quick.py buendelt next/previous/pause/play/volume_up/
volume_down/current in einem Skript (ein Argument pro Aktion), nutzt
denselben SpotifyClient wie der echte Tool-Dispatch. docker-compose.yml
mountet scripts/ jetzt read-only nach /opt/data/scripts statt fast-paths/
nach /opt/data/plugins/fast-paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ARIA
2026-07-20 16:48:55 +00:00
co-authored by Claude Sonnet 5
parent 9ea72886dd
commit 6a989d55e4
9 changed files with 199 additions and 723 deletions
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""Zero-LLM Spotify-Transportsteuerung fuer Hermes' offizielle `quick_commands`.
Hintergrund: Wir hatten zuerst ein eigenes Fast-Path-Plugin gebaut (Regex vor
dem LLM abfangen), das ist wieder raus -- der Hook (`pre_gateway_dispatch`),
auf den es registriert war, feuert nur fuer die Gateway-Plattformen
(Telegram/Discord/...), NICHT fuer die TUI (siehe `gateway/run.py` vs.
`tui_gateway/server.py` im echten Hermes-Sourcecode). Ein eigener Patch an
`tui_gateway/server.py` waere noetig gewesen -- riskant, weil die Funktion
dort ~700 Zeilen Concurrency-kritischen Code enthaelt (History-Lock,
Busy-Queue, Session-State).
Hermes hat dafuer aber bereits einen EIGENEN, offiziellen Mechanismus:
`quick_commands` (type: exec) in config.yaml. Laut Sourcecode
(cli.py:process_command, tui_gateway/server.py, gateway/run.py) bypassen die
NACHWEISLICH den Agent-Loop komplett -- kein LLM-Call, keine Tokens -- und
funktionieren auf ALLEN Plattformen (CLI/TUI, Telegram, Discord, Slack,
WhatsApp, Signal, Email, Home Assistant), offiziell dokumentiert unter
website/docs/user-guide/configuration.md#quick-commands.
Der Haken (ehrlich, nicht schoengeredet): quick_commands reichen KEINE
Argumente durch -- jeder Befehl ist ein fester Slash-Befehl (z.B. "/next"),
kein Freitext wie ARIAs Regex-`fast_patterns`. Fuer feste Steuerbefehle
(naechstes Lied, Pause, lauter/leiser) reicht das aber exakt so gut wie
Alexas Intent-Slots -- nur ohne Slot-Fuellung. Freitext-Faelle ("spiel
Playlist X auf Geraet Y ab") bleiben bewusst beim LLM, weil das ohne Modell
so oder so nicht geht (siehe README).
Ein Skript, ein Argument pro Aktion -- vermeidet zehn Mini-Skripte. Nutzt
denselben `SpotifyClient` (plugins.spotify.client) wie der echte
Tool-Dispatch: gleiche OAuth-Session, kein doppelter Token-Code.
Aufruf (aus quick_commands heraus, IM Container):
python3 /opt/data/scripts/spotify_quick.py next
python3 /opt/data/scripts/spotify_quick.py previous
python3 /opt/data/scripts/spotify_quick.py pause
python3 /opt/data/scripts/spotify_quick.py play
python3 /opt/data/scripts/spotify_quick.py volume_up
python3 /opt/data/scripts/spotify_quick.py volume_down
python3 /opt/data/scripts/spotify_quick.py current
"""
from __future__ import annotations
import sys
try:
from plugins.spotify.client import (
SpotifyAPIError,
SpotifyAuthRequiredError,
SpotifyClient,
SpotifyError,
)
except ImportError as exc: # pragma: no cover
sys.exit(
"Konnte plugins.spotify.client nicht importieren -- Skript muss "
f"INNERHALB des hermes-agent-Containers laufen. Fehler: {exc}"
)
VOLUME_STEP = 10
def _current_volume(client: "SpotifyClient") -> int:
state = client.get_playback_state() or {}
device = state.get("device") or {}
vol = device.get("volume_percent")
return int(vol) if isinstance(vol, (int, float)) else 50
def run(action: str) -> str:
client = SpotifyClient()
if action == "next":
client.skip_next()
return "⏭ naechstes Lied"
if action == "previous":
client.skip_previous()
return "⏮ vorheriges Lied"
if action == "pause":
client.pause_playback()
return "⏸ pausiert"
if action == "play":
client.start_playback()
return "▶ weiter"
if action == "volume_up":
new_vol = min(100, _current_volume(client) + VOLUME_STEP)
client.set_volume(volume_percent=new_vol)
return f"🔊 Lautstaerke {new_vol}%"
if action == "volume_down":
new_vol = max(0, _current_volume(client) - VOLUME_STEP)
client.set_volume(volume_percent=new_vol)
return f"🔉 Lautstaerke {new_vol}%"
if action == "current":
state = client.get_currently_playing() or {}
item = state.get("item") or {}
name = item.get("name")
artists = ", ".join(a.get("name", "") for a in (item.get("artists") or []))
if not name:
return "Gerade laeuft nichts."
return f"Läuft: {name} {artists}" if artists else f"Läuft: {name}"
return (
f"Unbekannte Aktion '{action}'. Erlaubt: next, previous, pause, play, "
"volume_up, volume_down, current"
)
def main() -> None:
if len(sys.argv) < 2:
sys.exit("Usage: spotify_quick.py <next|previous|pause|play|volume_up|volume_down|current>")
action = sys.argv[1].strip().lower()
try:
print(run(action))
except SpotifyAuthRequiredError as exc:
sys.exit(f"Spotify nicht eingeloggt: {exc}")
except SpotifyAPIError as exc:
# Echten error.reason zitieren statt zu raten (z.B. NO_ACTIVE_DEVICE,
# ALREADY_PAUSED, PREMIUM_REQUIRED) -- gleiche Regel wie ueberall sonst.
sys.exit(f"Spotify-Fehler (status={exc.status_code}): {exc}")
except SpotifyError as exc:
sys.exit(f"Spotify-Fehler: {exc}")
if __name__ == "__main__":
main()