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:
@@ -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).
|
||||
Reference in New Issue
Block a user