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.
118 lines
5.4 KiB
Markdown
118 lines
5.4 KiB
Markdown
# 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).
|