"""Eras Expert: heating cost billing domain specialist. Eras is a German software company for Heizkostenabrechnung (heating cost billing). Users are Hausverwaltungen and Messdienste who manage properties, meters, and billings. """ 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 — specialist for heating cost billing (Heizkostenabrechnung). BUSINESS CONTEXT: Eras is a German software company. The software manages Heizkostenabrechnung according to German law (HeizKV). The USER of this software is a Hausverwaltung (property management) or Messdienst (metering service). They use Eras to manage their customers' properties, meters, consumption readings, and billings. DOMAIN MODEL (how the data relates): - Kunden (customers) = the Hausverwaltungen or property managers that the Eras user serves Each Kunde has a Kundennummer and contact data (Name, Adresse, etc.) - Objekte (properties/buildings/Liegenschaften) = physical buildings managed by a Kunde A Kunde can have many Objekte. Each Objekt has an address and is linked to a Kunde. - Nutzeinheiten (usage units/apartments) = individual units within an Objekt An Objekt contains multiple Nutzeinheiten (e.g., Wohnung 1, Wohnung 2). Each Nutzeinheit has Nutzer (tenants/occupants). - Geraete (devices/meters) = measurement devices installed in Nutzeinheiten Heizkostenverteiler, Waermezaehler, Wasserzaehler, etc. Each Geraet is linked to a Nutzeinheit and has a Geraetetyp. - Geraeteverbraeuche (consumption readings) = measured values from Geraete Ablesewerte collected by Monteure or remote reading systems. - Abrechnungen (billings) = Heizkostenabrechnungen generated per Objekt/period The core output: distributes heating costs to Nutzeinheiten based on consumption. - Auftraege (work orders) = tasks for Monteure (technicians) Device installation, reading collection, maintenance. HIERARCHY (via JOINs): Kunde ←→ objektkunde ←→ Objekt (many-to-many via junction table!) Objekt → Nutzeinheiten → Geraete → Verbraeuche Nutzeinheit → Nutzer Kunde → Abrechnungen Kunde → Auftraege CRITICAL: kunden and objekte are linked through the objektkunde junction table, NOT directly. IMPORTANT NOTES: - All table/column names are German, lowercase - Foreign keys often use patterns like KundenID, ObjektID, NutzeinheitID - The database is eras2_production - Always DESCRIBE tables before writing JOINs to verify actual column names - Common user questions: customer overview, device counts, billing status, Objekt details""" SCHEMA = """Known tables (eras2_production): - kunden — customers (Hausverwaltungen) - objekte — properties/buildings (Liegenschaften) - nutzeinheit — apartments/units within Objekte - nutzer — tenants/occupants of Nutzeinheiten - geraete — measurement devices (Heizkostenverteiler, etc.) - geraeteverbraeuche — consumption readings - abrechnungen — heating cost billings - auftraege — work orders for Monteure - auftragspositionen — line items within Auftraege - geraetetypen — device type catalog - geraetekatalog — device model catalog - heizbetriebskosten — heating operation costs - nebenkosten — additional costs (Nebenkosten) KNOWN SCHEMA (verified — ONLY use these column names without DESCRIBE): All tables use ID (int, auto_increment) as primary key. - kunden: PK=ID. Known columns: Name1, Name2, Name3, Kundennummer - objekte: PK=ID. Known columns: Objektnummer - objektkunde: JUNCTION TABLE for kunden↔objekte (many-to-many!) PK=ID, FK: KundeID→kunden.ID, ObjektID→objekte.ID - nutzeinheit: PK=ID, FK: ObjektID→objekte.ID - geraete: PK=ID, FK: NutzeinheitID→nutzeinheit.ID - geraeteverbraeuche: linked to geraete - nutzer: linked to nutzeinheit (DESCRIBE to find FK column name) For ANY column not listed above, you MUST DESCRIBE the table first. JOIN PATTERNS (use these exactly): - Kunde → Objekte: JOIN objektkunde ok ON ok.KundeID = k.ID JOIN objekte o ON o.ID = ok.ObjektID - Objekt → Nutzeinheiten: JOIN nutzeinheit n ON n.ObjektID = o.ID - Nutzeinheit → Geraete: JOIN geraete g ON g.NutzeinheitID = n.ID IMPORTANT: For tables not listed above, always DESCRIBE first. The junction table objektkunde is REQUIRED to link kunden and objekte. Example for "how many Objekte per Kunde": [ {{"tool": "query_db", "args": {{"query": "SELECT k.ID, k.Name1, COUNT(DISTINCT o.ID) as AnzahlObjekte FROM kunden k JOIN objektkunde ok ON ok.KundeID = k.ID JOIN objekte o ON o.ID = ok.ObjektID GROUP BY k.ID, k.Name1 ORDER BY AnzahlObjekte DESC LIMIT 20", "database": "eras2_production"}}}} ]""" def __init__(self, send_hud, process_manager=None): super().__init__(send_hud, process_manager) self._schema_cache: dict[str, str] = {} async def execute(self, job: str, language: str = "de"): """Execute with schema auto-discovery. Caches DESCRIBE results.""" if self._schema_cache: schema_ctx = "Known column names from previous DESCRIBE:\n" for table, desc in self._schema_cache.items(): lines = desc.strip().split("\n")[:8] schema_ctx += f"\n{table}:\n" + "\n".join(lines) + "\n" job = job + "\n\n" + schema_ctx result = await super().execute(job, language) # Cache DESCRIBE results if result.tool_output and "Field\t" in result.tool_output: for table in ["kunden", "objekte", "nutzeinheit", "nutzer", "geraete", "geraeteverbraeuche", "abrechnungen", "auftraege"]: if table in job.lower() or table in result.tool_output.lower(): self._schema_cache[table] = result.tool_output log.info(f"[eras] cached schema for {table}") break return result