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>
132 lines
4.8 KiB
Python
132 lines
4.8 KiB
Python
#!/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()
|