Schema corrections: - kunden PK = ID (not Kundennummer) - objekte PK = ID (not ObjektID) - kunden↔objekte linked via objektkunde junction table (many-to-many) - Removed guessed column names, only verified PKs/FKs in SCHEMA - Added explicit JOIN patterns for the hierarchy Domain context test: 25/25 (added multi-hop Jaeger query through 4 tables) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
134 lines
5.9 KiB
Python
134 lines
5.9 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 (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
|