agent-runtime/agent/nodes/eras_expert.py
Nico 2d649fa448 v0.15.3: Domain context, iterative plan-execute, FK mappings, ES6 node inspector
Eras Expert domain context:
- Full Heizkostenabrechnung business model (Kunde>Objekte>Nutzeinheiten>Geraete)
- Known PK/FK mappings: kunden.Kundennummer, objekte.KundenID, etc.
- Correct JOIN example in SCHEMA prompt
- PA knows domain hierarchy for better job formulation

Iterative plan-execute in ExpertNode:
- DESCRIBE queries execute first, results injected into re-plan
- Re-plan uses actual column names from DESCRIBE
- Eliminates "Unknown column" errors on first query

Frontend:
- Node inspector: per-node cards with model, tokens, progress, last event
- Graph switcher buttons in top bar
- Clear button in top bar
- Nodes panel 300px wide
- WS reconnect on 1006 (deploy) without showing login
- Model info emitted on context HUD events

Domain context test: 21/21 (hierarchy, JOINs, FK, PA job quality)
Default graph: v4-eras

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

117 lines
5.2 KiB
Python

"""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: Kunde → Objekte → Nutzeinheiten → Geraete → Verbraeuche
→ Nutzer
Kunde → Abrechnungen
Kunde → Auftraege
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 PRIMARY KEYS AND FOREIGN KEYS:
- kunden: PK = Kundennummer (int), name columns: Name1, Name2, Name3
- objekte: PK = ObjektID, FK = KundenID → kunden.Kundennummer
- nutzeinheit: FK = ObjektID → objekte.ObjektID
- geraete: FK = NutzeinheitID → nutzeinheit.NutzeinheitID (verify with DESCRIBE)
IMPORTANT: Always DESCRIBE tables you haven't seen before to verify column names.
Use the FK mappings above for JOINs. Do NOT guess — use exact column names.
Example for "how many Objekte per Kunde":
[
{{"tool": "query_db", "args": {{"query": "SELECT k.Kundennummer, k.Name1, COUNT(o.ObjektID) as AnzahlObjekte FROM kunden k LEFT JOIN objekte o ON o.KundenID = k.Kundennummer GROUP BY k.Kundennummer, 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