"""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) === auftraege (2960 rows) — billing work orders === PK: ID (int) AuftragNummer, Bezeichnung (longtext) ErstellDatum, Abgeschlossen (datetime) ZugeordneteAbrechnungsinformationID (FK → abrechnungsinformationen.ID) ErstellMitarbeiterID (FK), AuftragsTyp (int), Status (int) Anmerkung, ObererText, UntererText (longtext) === auftragspositionen (5094 rows) — line items per work order === PK: ID (int) AuftragID (FK → auftraege.ID) ArtikelID (FK → artikel.ID) SollMenge, IstMenge (int) ZugeordneterGeraeteArtikelID (FK), ZugeordneteVertragPositionID (FK) === artikelposition (70164 rows) — billing line items with prices === PK: ID (int) ZugewiesenerArtikelID (FK → artikel.ID) ZugewieseneAbrechnungID (FK → abrechnungsinformationen.ID) RechnungID (FK → rechnung.ID) MengeVorgabe, Menge (decimal), NettoVorgabe, Netto (decimal), MWST (decimal) Rechnungsart (int), VorschussBerechnung (bool), ARechnung (bool) VerstecktInNebenkostenID (FK), ZugeordneteVertragPositionID (FK) === artikel (1078 rows) — service/product catalog === PK: ID (int) Artikelnummer, Bezeichnung (longtext) Netto (decimal), MWST (decimal) BerechnungsZiel (int), UmlageIn (int) ZugeordnetePreislisteID (FK) IstStandard, ARechnung, AppZusatz, IstEigenKostenpos (bool) === rechnung (7356 rows) — invoices === PK: ID (int) Rechnungsnummer (longtext), Rechnungsart (int) BezahltAm (datetime), BezahlterBetrag (decimal) Druckdatum, Erstelldatum, Exportdatum (datetime) AbrechnungsinformationID (FK → abrechnungsinformationen.ID) AbschlagSummeSonder, AbschlagSummeStandard (decimal) Bankeinzug (bool) === abrechnungsinformationen (4261 rows) — billing periods/settings === PK: ID (int) Von, Bis (datetime) — billing period AbrechnungHeizung, AbrechnungWarmwasser, AbrechnungNebenkosten, AbrechnungKaltwasser (bool) Tarifabrechnung, BHKW, HeizsaldoInNebenkosten, AbrechnungLegionellen, AbrechnungRauchmelder (bool) === nebenkosten (42209 rows) — ancillary cost items === PK: ID (int) Von, Bis (datetime) Bezeichnung (longtext), Mwst (decimal), Brutto (decimal) EinheitDerKostenart (longtext), Umlage (int), UmlageZiel (int) ZugeordnetesObjektID (FK → objekte.ID) NurEigentuemer, NurNutzer (bool) === vorauszahlungen (83932 rows) — advance payments per tenant === PK: ID (int) ZugeordneterNutzerID (FK → nutzer.ID) BetragNebenkosten, BetragHeizkosten, BetragWarmwasser (decimal) Von, Bis (datetime), IstNetto (bool) === heizbetriebskosten (22557 rows) — heating operation costs === PK: ID (int) Von, Bis (datetime), Bezeichnung (longtext) Mwst (decimal), Brutto (decimal), Art (int) ZugeordnetesObjektID (FK → objekte.ID) ZugeordneteVerbrauchsgruppeID (FK) === brennstofflieferungen (6477 rows) — fuel deliveries === PK: ID (int) GeliefertAm (datetime), Menge (decimal), Betrag (decimal) Mwst (decimal), Heizwert (decimal) Anfangsstand, Endstand (decimal) ZugeordneterEnergieVerwerterID (FK), BrennstoffMediumID (FK) ZugeordneteAbrechnungsinformationID (FK → abrechnungsinformationen.ID) === vertragpositionen (4395 rows) — contract line items === PK: ID (int) LaufzeitVon, LaufzeitBis (datetime) Menge (decimal), Gesamtpreis (decimal), PreisProEinheit (decimal), Mwst (decimal) ArtikelID (FK → artikel.ID), VertragNummer (longtext) Art (int), Umlage (int) 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 Auftrag → Positionen: JOIN auftragspositionen ap ON ap.AuftragID = a.ID Auftrag → Abrechnung: JOIN abrechnungsinformationen ai ON ai.ID = a.ZugeordneteAbrechnungsinformationID Artikelpos → Artikel: JOIN artikel art ON art.ID = ap.ZugewiesenerArtikelID Artikelpos → Rechnung: JOIN rechnung r ON r.ID = ap.RechnungID Artikelpos → Abrechnung: JOIN abrechnungsinformationen ai ON ai.ID = ap.ZugewieseneAbrechnungID Nebenkosten → Objekt: JOIN objekte o ON o.ID = nk.ZugeordnetesObjektID Vorauszahlung → Nutzer: JOIN nutzer nu ON nu.ID = vz.ZugeordneterNutzerID RULES: - For tables listed above: use ONLY the listed column names. Never guess. - For tables NOT listed above: use SELECT * with LIMIT to discover columns. - If a query fails, the retry system will show you the error. Fix the column name and try again. - Always LIMIT large queries (max 50 rows). - Use LEFT JOIN when results might be empty."""