feat(fast-paths): Universeller Fast-Path-Layer -- Steuerbefehle ohne LLM-Roundtrip

Motivation: selbst mit allen bisherigen Proxy-Fixes braucht jede Nachricht
mindestens einen vollen LLM-Turn (mehrere Sekunden). Fuer "pause" oder
"naechstes Lied" unnoetig langsam -- Alexa & Co. loesen sowas ohne
generatives Modell per Intent-Klassifikation in Millisekunden.

Neues Hermes-Plugin fast-paths/, haengt am pre_gateway_dispatch-Hook
(feuert bei JEDER eingehenden Nachricht auf JEDER Plattform, noch VOR
Auth/Pairing und Agent-Dispatch -- gefunden im echten Hermes-Sourcecode,
hermes_cli/plugins.py + gateway/run.py). Matcht der normalisierte Text
gegen eine Regex, wird der Handler direkt ausgefuehrt und das LLM komplett
uebersprungen. Ein File pro Skill unter patterns/ (analog zu ARIAs eigenen
fast_patterns), erster Skill: patterns/spotify.py (pause/next/previous/
volume/current, plus "spiel Playlist X auf Geraet Y ab" per Fuzzy-Match
ohne LLM fuer den Freitext-Namen).

Sicherheit: pre_gateway_dispatch feuert VOR Hermes' eigener Autorisierung
-- der Loader prueft deshalb explizit gateway._is_user_authorized() vor
jedem Pattern-Treffer, sonst koennte ein nicht autorisierter Absender per
Fast-Path an Hermes vorbei Aktionen ausloesen. Handler koennen sich zudem
bewusst per Rueckgabe None zurueckziehen (z.B. mehrdeutiger Playlist-Name)
und faellen dann normal ans LLM zurueck statt zu raten.

Lokal verifiziert (Testharness auf aria-wohnung, Hermes' eigenen
_load_directory_module-Lademechanismus nachgebaut, gemockter SpotifyClient):
Matching, Sicherheits-Check bei nicht autorisiertem Sender, Fuzzy-Match
fuer Playlist+Geraet, und mehrere Regression-Faelle gegen False-Positives
bei laengeren/anderen Saetzen -- alle Faelle bestehen.

docker-compose.yml: neuer verschachtelter Read-Only-Mount
./fast-paths -> /opt/data/plugins/fast-paths (User-Plugin-Pfad unter
$HERMES_HOME). Muss zusaetzlich einmalig in config.yaml unter
plugins.enabled aktiviert werden (Hermes-Plugins sind Opt-in) -- siehe
README.md und fast-paths/README.md.
This commit is contained in:
ARIA
2026-07-20 16:17:29 +00:00
parent 128c11b49d
commit 9ea72886dd
8 changed files with 735 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
# fast-paths — Steuerbefehle ohne LLM-Roundtrip
## Warum
Jede Nachricht an Hermes geht normalerweise durch's volle LLM: Text →
System-Prompt bauen → Claude aufrufen (mehrere Sekunden, siehe
Root-Cause-Historie im Haupt-README) → Tool-Call parsen → Tool ausfuehren →
Antwort formulieren. Fuer einfache Steuerbefehle ("pause", "naechstes Lied",
"lauter") ist das unnoetig langsam — Alexa & Co. machen sowas mit
Intent-Klassifikation in Millisekunden, ganz ohne generatives Modell.
Dieses Plugin baut das fuer Hermes nach: ein Regex-Layer, der VOR dem LLM
sitzt. Matcht der Text, wird die Aktion direkt ausgefuehrt und Claude nie
gefragt. Matcht nichts (oder ist sich ein Handler nicht sicher), laeuft
alles normal weiter — als waere das Plugin gar nicht da.
## Architektur
```
fast-paths/
plugin.yaml <- Hermes-Plugin-Manifest (kind: standalone)
__init__.py <- Loader: sammelt patterns/*.py ein, ein Hook-Callback
patterns/
__init__.py
_base.py <- FastPattern-Dataclass, pattern()-Helper, send_reply()
spotify.py <- EIN Skill = EINE Datei
<dein-neuer-skill>.py
```
Ein Skill = eine Datei unter `patterns/`. Kein zentrales Registrieren
noetig — `__init__.py` importiert automatisch jede `*.py`-Datei in
`patterns/` (ausser Dateien mit fuehrendem `_`, das ist Infrastruktur) und
sammelt deren `PATTERNS`-Liste ein.
## Neuen Fast-Path hinzufuegen
1. Neue Datei `patterns/<skill>.py` anlegen.
2. Handler-Funktion(en) schreiben: `def handler(match, event, gateway) -> str | None`.
- `event` ist ein `MessageEvent` (siehe `gateway/platforms/base.py` im
Hermes-Sourcecode) — `event.text`, `event.source.chat_id`,
`event.source.platform`, etc.
- `gateway` ist der `GatewayRunner` — u.a. `gateway._adapter_for_source(source)`
um den Platform-Adapter zu bekommen, `gateway._is_user_authorized(source)`
fuer Auth-Checks (macht der Loader schon automatisch, siehe unten).
3. Am Ende der Datei: `PATTERNS = [pattern("name", r"^regex$", handler), ...]`
mit dem Helper aus `_base.py`.
4. Fertig — kein Eintrag irgendwo sonst noetig. Container neu starten
(`docker-compose up -d --build hermes-agent`), Loader findet die Datei
automatisch beim naechsten Plugin-Discovery.
### Rueckgabewerte des Handlers (wichtig!)
| Rueckgabe | Bedeutung |
|---|---|
| `"Text"` | Wird 1:1 an den User geschickt, LLM wird uebersprungen. |
| `""` (leer) | Aktion ausgefuehrt, aber bewusst stumm. LLM trotzdem uebersprungen. |
| `None` | Handler ist sich nicht sicher genug (z.B. mehrdeutiger Name) — faellt zurueck auf die normale LLM-Verarbeitung, GENAU wie "kein Match". **Nie raten, im Zweifel `None`.** |
| *(Exception)* | Wird geloggt, faellt ebenfalls automatisch aufs LLM zurueck. |
### Regex-Konventionen
- Immer mit `^` und `$` anchorn — sonst matcht `pause` auch mitten in
"pause die Musik und erzaehl mir was" und reisst den Rest des Satzes weg.
- Mehrere Formulierungen/Synonyme + Fuellwoerter ueber Alternativen und
optionale Gruppen abdecken (`(?:mal|bitte)?`), nicht nur eine Formulierung.
- Text wird vor dem Matchen normalisiert (lowercase, Satzzeichen am Ende
weg, Mehrfach-Leerzeichen zusammengefasst) — Regexe koennen daher
durchgehend lowercase geschrieben werden.
- Lieber ein Pattern zu eng als zu weit — ein Fast-Path, der faelschlich
matcht, kapert eine Nachricht die eigentlich ans LLM sollte. Ein Pattern,
das gar nicht matcht, kostet nur die eingesparte Geschwindigkeit (die
Nachricht geht stattdessen ganz normal ans LLM, kein Schaden).
## Sicherheit
`pre_gateway_dispatch` (der Hook, an dem dieses Plugin haengt) feuert VOR
Hermes' eigener Autorisierungspruefung. Der Loader in `__init__.py` prueft
deshalb bei JEDEM Pattern-Treffer explizit `gateway._is_user_authorized(...)`
BEVOR der Handler ueberhaupt aufgerufen wird — ein nicht autorisierter
Absender (z.B. auf einer oeffentlich erreichbaren Plattform wie Telegram)
kann also nie per Fast-Path an Hermes vorbei etwas ausloesen. Diese Pruefung
lebt zentral im Loader, nicht in den einzelnen Skill-Dateien — neue Skills
muessen sich darum nicht kuemmern.
## Bekannter Trade-off
`pre_gateway_dispatch`-Callbacks laufen **synchron** (Hermes' PluginManager
awaited sie nicht, siehe `hermes_cli/plugins.py`). Handler duerfen also
ganz normal blockierende Netzwerk-Calls machen (z.B. `httpx.request` wie
`SpotifyClient`), das blockiert den Event-Loop aber kurz (typischerweise
100-500ms pro API-Call). Fuer eine Text-Antwort an den User (das *ist*
async — `adapter.send()`) gibt's `send_reply()` in `_base.py`: schedult
einen Fire-and-forget `asyncio.Task` auf dem laufenden Event-Loop. Beides
zusammen ist immer noch um Groessenordnungen schneller als der
LLM-Roundtrip (mehrere Sekunden bis >20s, siehe Haupt-README).
## Aktivieren
Plugins sind bei Hermes standardmaessig Opt-in. Einmalig in
`hermes-data/agent-home/config.yaml`:
```yaml
plugins:
enabled:
- fast-paths
```
Danach `docker-compose up -d --build hermes-agent` (Volume-Mount siehe
`docker-compose.yml`, Kommentar beim `hermes-agent`-Service).
## Bisherige Skills
- **`patterns/spotify.py`** — pause/weiter/next/previous/lauter/leiser/
Lautstaerke setzen/aktueller Titel, plus "spiel Playlist X [auf Geraet Y]
ab" per Fuzzy-Match (kein LLM fuer den Freitext-Namen noetig, siehe
`difflib`-basiertes Matching dort — bricht bei Mehrdeutigkeit bewusst ab
und gibt an das LLM ab statt zu raten).