agent-runtime/agent/nodes/eras_expert.py
Nico 067cbccea6 v0.15.8: Expert retry loop, fixed geraete schema, action routing, stable nodes
Expert retry loop (max 3 attempts):
- On SQL error, re-plans with error context injected
- "PREVIOUS ATTEMPTS FAILED" section tells LLM what went wrong
- Breaks out of tool sequence on error, retries full plan
- Only reports failure after exhausting retries
- Recovery test: 13/13

Schema fixes:
- geraete: Geraetenummer, Bezeichnung, Beschreibung (were Fabriknummer, Funkkennung)
- geraeteverbraeuche: all columns verified
- nutzer: all columns verified

Action routing:
- Button clicks route through PA→Expert in v4 (was missing has_pa check)
- WS handler catches exceptions, sends error HUD instead of crashing

Nodes panel:
- Fixed pipeline order, no re-sorting
- Normalized names (pa_v1→pa, expert_eras→eras)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:34:01 +02:00

136 lines
5.5 KiB
Python

"""Eras Expert: Heizkostenabrechnung domain specialist.
The expert knows the full database schema. No DESCRIBE at runtime.
All queries use verified column names and JOIN patterns.
"""
import asyncio
import logging
from .expert_base import ExpertNode
from ..db import run_db_query
log = logging.getLogger("runtime")
class ErasExpertNode(ExpertNode):
name = "eras_expert"
default_database = "eras2_production"
DOMAIN_SYSTEM = """You are the Eras domain expert for Heizkostenabrechnung (German heating cost billing).
BUSINESS CONTEXT:
Eras is software for Hausverwaltungen and Messdienste who manage properties, meters, and billings.
The USER of this agent is an Eras customer exploring their data. They think in domain terms
(Kunden, Objekte, Wohnungen, Zaehler) — NOT in SQL. Never expose SQL or table names to the user.
DOMAIN MODEL:
- Kunden = property managers (Hausverwaltungen). 693 in the system.
- Objekte = buildings/Liegenschaften managed by Kunden. 780 total. Linked via objektkunde (m:n).
- Nutzeinheiten = apartments/units inside Objekte. 4578 total.
- Nutzer = tenants/occupants of Nutzeinheiten. 8206 total.
- Geraete = measurement devices (Heizkostenverteiler, Zaehler). 56726 total.
- Verbraeuche = consumption readings from Geraete. 1.3M readings.
- Adressen = postal addresses, linked via objektadressen/kundenadressen.
RESPOND IN DOMAIN LANGUAGE:
- Say "Kunde Jaeger hat 3 Objekte" not "SELECT COUNT..."
- Say "12 Wohnungen mit 45 Geraeten" not "nutzeinheit rows"
- Present data as summaries, not raw tables"""
SCHEMA = """COMPLETE DATABASE SCHEMA (eras2_production) — use these exact column names:
=== kunden (693 rows) ===
PK: ID (int)
Name1, Name2, Name3 (longtext) — customer name parts
Kundennummer (longtext) — customer number
AnredeID (FK), BriefanredeID (FK), ZugeordneterKomplettdruckID (FK)
Anmerkung, Fremdnummer, Ansprechpartner (longtext)
Steuernummer, UmsatzsteuerID (longtext)
HatHistorie, IstWebkunde, IstNettoKunde, BrennstoffkostenNachFIFO, BelegePerEmail (bool)
MietpreisAnpassungProzent (decimal)
=== objektkunde (911 rows) — JUNCTION: kunden ↔ objekte (many-to-many) ===
PK: ID (int)
KundeID (FK → kunden.ID)
ObjektID (FK → objekte.ID)
ZeitraumVon, ZeitraumBis (datetime)
IstKunde, IstEigentuemer, IstRechnungsempfaenger, IstAbrechnungsempfaenger (bool)
=== objekte (780 rows) ===
PK: ID (int)
Objektnummer (longtext) — building reference number
AbleserID, MonteurID, UVIRefObjektID, ZugeordneterKomplettdruckID (FK)
Anmerkung, AnmerkungIntern (longtext)
HatHistorie, VorauszahlungGetrennt, Selbstablesung, IstObjektFreigegeben (bool)
=== objektadressen — JUNCTION: objekte ↔ adressen ===
PK: ID, ObjektID (FK → objekte.ID), AdresseID (FK → adressen.ID), IstPrimaer (bool)
=== kundenadressen — JUNCTION: kunden ↔ adressen ===
PK: ID, KundeID (FK → kunden.ID), AdresseID (FK → adressen.ID), TypDerAdresseID (FK)
=== adressen (1762 rows) ===
PK: ID (int)
Strasse, Hausnummer, Postleitzahl, Ort, Adresszusatz, Postfach (longtext)
LandID (FK), Laengengrad, Breitengrad (double)
=== nutzeinheit (4578 rows) ===
PK: ID (int)
ObjektID (FK → objekte.ID)
NeNummerInt (longtext) — unit number
Lage, Stockwerk, Flaeche, Nutzflaeche (various)
AdresseID (FK), CustomStatusKeyID (FK)
=== kundenutzeinheit — JUNCTION: kunden ↔ nutzeinheit ===
PK: ID, KundeID (FK → kunden.ID), NutzeinheitID (FK → nutzeinheit.ID), Von, Bis (datetime)
=== nutzer (8206 rows) — tenants/occupants ===
PK: ID (int)
NutzeinheitID (FK → nutzeinheit.ID)
Name1, Name2, Name3, Name4 (longtext) — tenant name
NutzungVon, NutzungBis (datetime)
ArtDerNutzung (int), AnredeID (FK), BriefanredeID (FK)
IstGesperrt, Selbstableser (bool)
=== geraete (56726 rows) — meters/devices ===
PK: ID (int)
NutzeinheitID (FK → nutzeinheit.ID)
Geraetenummer (longtext) — device number/serial
Bezeichnung (longtext) — device name/label
Beschreibung (longtext) — description
ArtikelID (FK), NutzergruppenID (FK), Einheit (int)
Einbaudatum, Ausbaudatum, GeeichtBis, GeeichtAm, ErstInbetriebnahme, DefektAb (datetime)
FirmwareVersion, LaufendeNummer, GruppenKennung, Memo, AllgemeinesMemo (longtext)
AnsprechpartnerID, ZugeordneterRaumID, CustomStatusKeyID (FK)
Gemietet, Gewartet, KeinAndruck, IstAbzuziehendesGeraet, HatHistorie (bool)
=== geraeteverbraeuche (1.3M rows) — consumption readings ===
PK: ID (int)
GeraetID (FK → geraete.ID)
Ablesedatum (datetime) — reading date
Ablesung (double) — meter reading value
Verbrauch (double) — consumption value
Faktor (double) — factor
Aenderungsdatum (datetime)
AbleseartID (FK), Schaetzung (int), Status (int)
IstRekonstruiert (bool), Herkunft (int)
ManuellerWert (double), Rohablesung (double)
Anmerkung, Fehler, Ampullenfarbe (longtext)
JOIN PATTERNS (use exactly):
Kunde → Objekte: JOIN objektkunde ok ON ok.KundeID = k.ID JOIN objekte o ON o.ID = ok.ObjektID
Objekt → Adresse: JOIN objektadressen oa ON oa.ObjektID = o.ID JOIN adressen a ON a.ID = oa.AdresseID
Kunde → Adresse: JOIN kundenadressen ka ON ka.KundeID = k.ID JOIN adressen a ON a.ID = ka.AdresseID
Objekt → NE: JOIN nutzeinheit ne ON ne.ObjektID = o.ID
NE → Nutzer: JOIN nutzer nu ON nu.NutzeinheitID = ne.ID
NE → Geraete: JOIN geraete g ON g.NutzeinheitID = ne.ID
Geraet → Verbrauch: JOIN geraeteverbraeuche gv ON gv.GeraetID = g.ID
RULES:
- NEVER use DESCRIBE at runtime. You know the schema.
- NEVER guess column names. Use ONLY columns listed above.
- For unknown tables: return an error, do not explore.
- Always LIMIT large queries (max 50 rows).
- Use LEFT JOIN when results might be empty."""