trading-system-docs/notes/trading/system-docs/modul-17-hermes-agent.md

220 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Modul-17: Hermes-Agent (autonomer Research-/Analyse-Worker unter Paperclip)
**Status: FREIGEGEBEN (21.08.2026)** · Container `Modul-17-Hermes-Agent` · Image `trading-modules-modul17-hermes-agent:latest`
> ⚠️ **Keine Namenskollision** mit fremden Containern. Hermes läuft im `trading-modules`-Netz
> als `Modul-17-Hermes-Agent`. Fremdsysteme (`paperclip-dn8l-paperclip-1` u.ä., Coolify) nie anfassen.
## Zweck & Rolle
Hermes ist die **autonome Research-/Analyse-Ausführungsebene unter Paperclip (M16)**. Er nimmt
von Paperclip delegierte Tasks (Research, längere Analysen, Strategie-Review, Analytics-Auswertung,
Backtest-/Optimization-Auswertung, Systemdiagnose READ-ONLY) entgegen und führt sie **ausschließlich
über die strikte Tool-Whitelist** aus. Das Ergebnis geht als `agent_run`/`result_refs` an Paperclip
zurück.
**Kritische Grenze — Hermes ist NICHT Teil des kritischen Trading-Pfads und darf NICHT:**
- direkt auf Modul-09 Execution zugreifen, Orders erzeugen
- Risk-/Portfolio-Regeln (M07/M08) überschreiben
- M15-Control verändern (PAUSE/HALT/RESUME)
- Strategien automatisch produktiv aktivieren
- Optimierungsparameter automatisch deployen
- Broker-Credentials erhalten
**Paperclip bleibt Orchestrator.** Hermes arbeitet NUR über die erlaubten Tools/APIs. Unbekannte
oder unerlaubte Tools → **REJECTED**.
## Architektur / Datenfluss
```
Paperclip (M16) ── Task ──▶ Modul-17-Hermes-Agent (Port 55017, NUR intern/expose)
├── app/core/tool_whitelist.py strikte Tool-Whitelist (Herzstück)
├── app/core/executor.py Hermes-Executor (Tool-Ausführung + Audit + Idempotenz)
├── app/storage/storage.py Persistenz hermes_task/run/tool_call/result
├── app/clients/clients.py Read-Clients + 2 begrenzte Write-Clients
└── app/api/main.py REST-API (/health, /tasks, /runs, /tools)
├── READ (tolerant): Modul-03 Market Data, M04 Regime, M05 Strategy,
│ M06 Ranking, M10 Journal, M11 Analytics, M15 Monitoring (GET)
└── WRITE (begrenzt): Modul-12 POST /backtest, Modul-13 POST /optimization,
Proposal/Research-Ergebnis speichern
```
**API (Docker-intern, Port 55017):**
- `GET /health` — Liveness (`{"status":"ok","service":"hermes","version":"0.1.0"}`)
- `GET /health/ready` — Readiness (DB-Schema vorhanden) → 200 `{"status":"ready"}`
- `POST /tasks` — Task einreichen (`task_type` + `payload.tools[]`); Whitelist-Prüfung
- `GET /tasks` / `GET /tasks/{id}` — Tasks + Status/result_refs
- `GET /runs/{id}` — einzelner Run mit result_refs
- `GET /tools` — Tool-Whitelist auflisten
Kein RabbitMQ: synchroner request/response. Port 55017 **nur intern** (`expose:`), kein öffentliches Mapping.
## Tool-Whitelist (strikt, `app/core/tool_whitelist.py`)
Jeder Tool-Call wird gegen die Whitelist geprüft. Unbekannte/unerlaubte Tools → REJECTED.
| Tool (namespace.method) | Zielmodul | Richtung | Status |
|-------------------------|-----------|----------|--------|
| `market_data.prices` | Modul-03 | READ | erlaubt |
| `regime.analyze` | Modul-04 | READ | erlaubt |
| `strategy.list` | Modul-05 | READ | erlaubt |
| `ranking.current` | Modul-06 | READ | erlaubt |
| `journal.entries` | Modul-10 | READ | erlaubt |
| `analytics.portfolio` | Modul-11 | READ | erlaubt |
| `monitoring.status` | Modul-15 | READ | erlaubt (NUR GET) |
| `backtest.start` | Modul-12 | WRITE | erlaubt (CONTROLLED) |
| `optimization.start` | Modul-13 | WRITE | erlaubt (CONTROLLED) |
| `execution.place_order` | Modul-09 | WRITE | **REJECTED** |
| `risk.set_limits` | Modul-07 | WRITE | **REJECTED** |
| `portfolio.rebalance` | Modul-08 | WRITE | **REJECTED** |
| `monitoring.pause/halt/resume` | Modul-15 | Control | **REJECTED** |
| alles andere | — | — | **REJECTED** |
**M09/M07/M08 sind strukturell NICHT in den Clients implementiert** (`clients.py`) — ein Verstoß
ist damit zur Laufzeit unmöglich, zusätzlich zur Whitelist-Barriere. M15-Control-Writes fehlen ebenfalls.
## Persistenz / Audit (Migration `migrations/001_hermes.sql`)
Vier Tabellen in Modul-01-PostgreSQL (PostgreSQL-01), NICHT im kritischen Trading-Pfad:
- `hermes_task` — Aufgaben (id, task_type, agent_role, status, payload, result_refs, error_code)
- `hermes_run` — Ausführungen (run_id, task_id, status, result, result_refs, idempotent)
- `hermes_tool_call`**jeder Tool-Call nachvollziehbar**: task_id, tool, target_module, input_hash,
status, timestamps, error_code
- `hermes_result` — gespeicherte Ergebnisse/Reports
Jeder Tool-Call wird in `hermes_tool_call` auditier. Keine Secrets/Prompts mit Credentials in Logs.
## Sicherheit
- **Keine öffentlichen Ports**: 55017 nur `expose` (Docker-intern), kein Host-Mapping (verifiziert: `docker port` leer).
- **Internes Docker-Netzwerk** `trading-modules`.
- **Kein Docker-Socket, kein Root-/Shell-Zugriff auf andere Container.**
- **Secrets nur ENV/Secret** (`PG_PASSWORD` aus Compose `environment`); keine Secrets in Logs/API/DB.
- **Keine Broker-Credentials** (kein Client, kein ENV dafür).
- **Hermes-Ausfall beeinflusst M0316 NICHT** (eigene DB-Tabellen, keine Kopplung an Trading-Core).
- **M15-Ausfall → Monitoring-/Systemdiagnose-Task FAILED** (FAIL-CLOSED, nicht schönreden).
## Read-Clients (tolerant)
Einzelne Quellen-Fehler (404/5xx/Timeout) werden als Teil-Ergebnis `{"_error": code}` gekapselt,
der Gesamt-Task bleibt SUCCEEDED. Komplett unreachable (`UNREACHABLE`/`TIMEOUT`) → `None`.
**Ausnahme — Monitoring (FAIL-CLOSED):** Der `monitoring.status`-Call ruft M15 direkt auf. Ist M15
nicht erreichbar → Task **FAILED** (kein "ok" bei ausgefallenem M15). Gleiches gilt für M12/M13:
Ausfall → zugehöriger Backtest-/Optimization-Task **FAILED** (`UNREACHABLE`), Hermes bleibt healthy,
Trading-Core unbeeinflusst.
## Idempotenz
Jeder `task_id` erzeugt genau **einen** `hermes_run` (SUCCEEDED). Wiederholte Einreichung derselben
Task-Payload → je Task genau 1 Run, kein Doppel-Run. `input_hash` in `hermes_tool_call` identifiziert
identische Aufrufe.
## Tests & E2E-Verifikation (VPS, 21.08.2026)
- **Unit-Tests** `tests/test_hermes.py`: 13/13 grün (Whitelist-Gates, Executor, Idempotenz, REJECTED).
- **T1 Research-Task** → M03 → **SUCCEEDED**, result_refs mit `tool_output` + `report`.
- **T2 Analytics-Task** → M11 → **SUCCEEDED**.
- **T3 Backtest-Task** → M12 `POST /backtest` (BT_PULL, stock, 450 Candles) → **SUCCEEDED**.
- **T4 Optimization-Task** → M13 `POST /optimization` (BT_PULL) → **SUCCEEDED**.
- **T5 Monitoring READ-ONLY** → M15 `GET /status`**SUCCEEDED**.
- **T6 Execution-Tool** (`execution.place_order`) → **REJECTED** (`TOOL_NOT_ALLOWED`).
- **T7 M15-Control-Tool** (`monitoring.pause`) → **REJECTED**.
- **T8 Risk/Portfolio-Write** (`risk.set_limits`) → **REJECTED**.
- **T9 ungültiges Tool** (`bogus.tool`) → **REJECTED**.
- **T10 Ausfall**: M15 down → Monitoring FAILED; M12 down → Backtest FAILED; M13 down → Optimization
FAILED (jeweils `UNREACHABLE`). Erholung → SUCCEEDED. Hermes bleibt healthy.
- **T11 Idempotenz**: je Task genau 1 Run.
- **T12 Restart/Reconnect**: nach `docker restart` ready, Daten persistent.
- **T13 keine Secrets**: `docker logs` frei von password/secret/token/api_key.
- **T14 Port nur intern**: `ss -ltn` kein Host-Listening auf 55017.
- **T15 Hermes-Ausfall**: bei gestopptem Hermes sind M03/04/05/11/12/13/15/16 alle healthy
(`/health` ok) → Trading-Core unbeeinflusst.
**E2E-Gesamt: 15/15 grün.**
## Bugs / Fixes (modul-17, dokumentiert)
1. **Methodenname `_last_succeeded_run` vs `_run_last_succeeded_run`**: Executor referenzierte
`_last_succeeded_run`, Methode hieß anders → benannt in `_last_succeeded_run` (Idempotenz-Check).
2. **Test-Pfad**: `sys.path.insert` musste auf `../app` zeigen (nicht Modul-Root) für `app.*`-Importe.
3. **M12 422 "Unzureichende Daten"**: EURUSD/demo lieferte nur 1 Candlestick (< 220 nötig). Fix im
E2E: Symbol **`BT_PULL`** (asset_class `stock`, 450 Candles) für Backtest/Optimization.
4. **M13 erfordert Pflichtfelder `start_date`+`end_date`** (anders als M12): Optimization-Payload
musste beide Datumsfelder enthalten, sonst HTTP 422.
5. **E2E-Skript**: BusyBox wget im Alpine-Worker kennt kein `--post-file` JSON per `docker cp`
+ `--post-data="$(cat …)"`. Funktionsname `mkpayload`→`mk_put` (aufrufender Code nutzte `mk_put`).
6. **SSH-Quoting**: verschachtelte `$(…)` mit doppelten Anführungszeichen im Inline-Python kappen
das Kommando Inline-Echo entfernt, reiner grep-Check.
## Compose / Betrieb
- Netz `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Port 55017 nur intern (`expose`),
`restart: unless-stopped`. Kein öffentliches Port-Mapping.
- **Kein Teil des kritischen Trading-Pfads:** kein M09-, kein M07/M08-, kein M15-Control-Zugriff.
- Paperclip (M16) bleibt Orchestrator; Hermes ist seine Worker-Ausführungsebene.
---
## LLM-Schicht (KI-/Analyse-Erweiterung, M16 additiv `llm=true`)
Hermes kann Tasks zusätzlich über eine **LLM-Schicht** (Tool-Calling-Loop) ausführen. Die LLM-Schicht
ist **additiv** (M16 setzt `llm=true` im Task-Payload); ohne Flag bleibt das deterministische M17-Verhalten
unverändert. Die Tool-Whitelist und Task-Gates bleiben **deterministisch außerhalb des LLM** das LLM
führt nie selbst Tools aus, sondern liefert `requested_tools[]`, die durch die bestehende Whitelist geprüft werden.
### 2-Stufen-Modellkonzept (final, 21.08.2026)
| Stufe | Modell | Provider | Verwendung |
|-------|--------|----------|------------|
| **STANDARD** | `deepseek-v4-flash:cloud` | `ollama_cloud` | Normale Research-/Analyse-Tasks, voller Tool-Calling-Loop, Structured JSON, vollständiger Audit |
| **LOKAL** | `llama3.1:8b` | `ollama_local` | NUR expliziter Privacy-/Offline-Modus (`llm_mode=local`), **Analyse-only** (kein Tool-Loop), CPU-/Timeout-Schutz |
**Regeln (hart):**
- **KEIN automatischer CloudLocal-Fallback.** Cloud-Ausfall Task **FAILED** (`LLM_UNAVAILABLE`),
kein minutenlanger Wechsel auf das lokale Modell.
- **LOKAL nur explizit** über `llm_mode=local` im Task-Payload (`extra.llm_mode`). Nicht als Standard verwenden.
- **LOKAL = Analyse-only**: genau EIN Analyse-Call, keine Tool-Anfragen. `llama3.1:8b` auf CPU versteht
den Tool-Calling-Loop nicht zuverlässig (LLM_MAX_ROUNDS) und ist langsam (~60-90s/Call).
- **FAIL-CLOSED**: LLM down/Timeout/invalid JSON/Schema/Max-Runden Task FAILED, kein Trading-Core-Effekt.
- **Safety unverändert**: kein `execution.place_order`, kein `risk.set_limits`, kein `portfolio.rebalance`,
kein M15 pause/halt/resume, Proposal maximal DRAFT/PROPOSED.
### LLM-Audit (`hermes_llm_run`, Migration `002_hermes_llm.sql`)
Fünfte Audit-Tabelle. Je LLM-Run: provider, model, config_version, input_context_hash, requested_tools,
executed_tools, rejected_tools, tokens_in/out, latency_ms, structured_result, status, error_code.
**Vollständig AUCH im FAILED-Pfad** (Timeout/Invalid-JSON/Max-Runden persistieren Metriken). Keine
vollständigen sensiblen Prompts, keine Secrets.
### LLM-ENV (Compose M17-Block)
- `LLM_PROVIDER=ollama_cloud` (Standard = Cloud)
- `LLM_MODEL_CLOUD=deepseek-v4-flash:cloud`
- `LLM_MODEL_LOCAL=llama3.1:8b`
- `LLM_BASE_URL=http://10.0.4.2:11434` (Ollama-Netz)
- `LLM_TIMEOUT_SECONDS=300`
### LLM-E2E-Verifikation (final, 21.08.2026)
- **Cloud-Standard** (`llm=true`, standard): **SUCCEEDED** (deepseek-v4-flash:cloud, 4 requested/2 executed, Audit vollständig).
- **Local explizit** (`llm=true`, `llm_mode=local`): **SUCCEEDED** (llama3.1:8b, Analyse-only, Audit vollständig).
- **Cloud-down** (isoliert, ungültige Base-URL): **FAILED** (`LLM_UNAVAILABLE`), **kein Auto-Fallback** (Provider bleibt Cloud).
- **llm=false**: **SUCCEEDED** (deterministisches M17, kein LLM-Run).
- **Audit vollständig**: requested/executed/rejected/tokens/latency/context_hash persistiert, auch im FAILED-Pfad.
- **M03M15 unbeeinflusst**: alle Module healthy während der LLM-Tests.
- **Unit-Tests** `tests/test_llm.py`: **20/20 grün** (inkl. 4 REJECTED-Gate-Tests: place_order, monitoring_pause, risk_set_limits, unknown_tool).
### LLM-Bugs / Fixes
1. **Audit-Metriken leer (Cloud-E2E)**: `finish_llm_run` schrieb nur status/result/error um
requested/executed/rejected_tools, tokens_in/out, latency_ms, structured_result, provider/model/config_version erweitert.
2. **FAILED-Pfad verlor Audit-Metriken**: Max-Runden-Exception trug keine Metriken Exception trägt jetzt
Audit-Dict, `execute()` persistiert sie auch bei LLM_MAX_ROUNDS/Timeout/Invalid-JSON.
3. **NOT NULL-Constraint**: `requested_tools` etc. `NOT NULL DEFAULT '[]'` `finish_llm_run` koerziert `None`→`[]`.
4. **Structured Output**: Cloud-Modell ignoriert Schema ohne `format`-Constraint `format`-Constraint im Payload.
5. **ToolCall-Format**: ToolCall-Modell erwartet `name`/`args` (nicht `tool`/`params`) Skript/Executor angepasst.
6. **Lokales Modell ungeeignet für Tool-Loop**: `llama3.1:8b` auf CPU LLM_MAX_ROUNDS (fragt endlos Tools an).
Fix: lokaler Modus = **Analyse-only** (kein Tool-Loop), wie vom User empfohlen.
## Geändert
```
Geändert von: Rain Ocampo
Datum: 21.08.2026
Grund: LLM-Schicht (2-Stufen-Konzept) ergaenzt: STANDARD=deepseek-v4-flash:cloud (voller Tool-Loop),
LOKAL=llama3.1:8b (nur explizit llm_mode=local, Analyse-only), KEIN Auto-Fallback, Cloud-down->FAILED
(LLM_UNAVAILABLE), llm=false deterministisch, Audit hermes_llm_run vollstaendig (auch FAILED-Pfad),
Unit 20/20, E2E 5/5 Szenarien gruen, M03-M15 unbeeinflusst. FREIGEGEBEN.
Geändert von: Rain Ocampo
Datum: 21.08.2026
Grund: Modul-17-Hermes-Agent implementiert + E2E verifiziert (Research/Analytics/Backtest/Optimization/Monitoring-Success,
Safety REJECTED 6/7/8/9, M12/M13/M15-Ausfall -> FAILED (FAIL-CLOSED), Idempotenz, Restart, keine Secrets, Port intern,
Hermes-Ausfall beeinflusst M03-16 nicht). Tool-Whitelist strikt, 4 Audit-Tabellen (hermes_task/run/tool_call/result),
6 API-Endpunkte. Unit 13/13, E2E 15/15 gruen. FREIGEGEBEN. Keine weiteren Module begonnen.
```