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>
This commit is contained in:
Nico 2026-03-29 19:34:01 +02:00
parent faeb9d3254
commit 067cbccea6
3 changed files with 124 additions and 56 deletions

View File

@ -96,17 +96,27 @@ IstGesperrt, Selbstableser (bool)
=== geraete (56726 rows) — meters/devices ===
PK: ID (int)
NutzeinheitID (FK → nutzeinheit.ID)
ArtikelID (FK → geraetekatalog), GeraeteTypID (FK)
Fabriknummer, Funkkennung (longtext) — serial numbers
Einbaudatum, Ausbaudatum, GeeichtBis (datetime)
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), Ablesung, Verbrauch, Faktor (double)
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

View File

@ -76,30 +76,45 @@ Write a concise, natural response. 1-3 sentences.
def __init__(self, send_hud, process_manager=None):
super().__init__(send_hud)
MAX_RETRIES = 3
async def execute(self, job: str, language: str = "de") -> ThoughtResult:
"""Execute a self-contained job. Returns ThoughtResult.
Expert knows the schema — plan once, execute, respond."""
"""Execute a self-contained job with retry on SQL errors.
Expert knows the schema — plan, execute, retry if needed, respond."""
await self.hud("thinking", detail=f"planning: {job[:80]}")
# Step 1: Plan tool sequence (expert knows schema, no DESCRIBE needed)
errors_so_far = []
tool_sequence = []
response_hint = ""
for attempt in range(1, self.MAX_RETRIES + 1):
# Plan (or re-plan with error context)
plan_prompt = f"Job: {job}"
if errors_so_far:
plan_prompt += "\n\nPREVIOUS ATTEMPTS FAILED:\n"
for err in errors_so_far:
plan_prompt += f"- Query: {err['query']}\n Error: {err['error']}\n"
plan_prompt += "\nFix the query using ONLY columns from the schema. Do NOT repeat the same mistake."
plan_messages = [
{"role": "system", "content": self.PLAN_SYSTEM.format(
domain=self.DOMAIN_SYSTEM, schema=self.SCHEMA,
database=self.default_database)},
{"role": "user", "content": f"Job: {job}"},
{"role": "user", "content": plan_prompt},
]
plan_raw = await llm_call(self.model, plan_messages)
tool_sequence, response_hint = self._parse_plan(plan_raw)
await self.hud("planned", tools=len(tool_sequence),
hint=response_hint[:80], attempt=attempt)
await self.hud("planned", tools=len(tool_sequence), hint=response_hint[:80])
# Step 2: Execute remaining tools
# Execute tools
actions = []
state_updates = {}
display_items = []
machine_ops = []
tool_used = ""
tool_output = ""
had_error = False
for step in tool_sequence:
tool = step.get("tool", "")
@ -127,18 +142,34 @@ Write a concise, natural response. 1-3 sentences.
database = args.get("database", self.default_database)
try:
result = await asyncio.to_thread(run_db_query, query, database)
if result.startswith("Error:"):
errors_so_far.append({"query": query, "error": result})
had_error = True
await self.hud("tool_result", tool="query_db",
output=f"ERROR (attempt {attempt}): {result[:150]}")
break # stop executing, retry with new plan
tool_used = "query_db"
tool_output = result
await self.hud("tool_result", tool="query_db", output=result[:200])
except Exception as e:
tool_used = "query_db"
tool_output = f"Error: {e}"
await self.hud("tool_result", tool="query_db", output=str(e)[:200])
errors_so_far.append({"query": query, "error": str(e)})
had_error = True
await self.hud("tool_result", tool="query_db",
output=f"ERROR (attempt {attempt}): {e}")
break
# Step 3: Generate response
if not had_error:
break # success — stop retrying
log.info(f"[expert] attempt {attempt} failed, {len(errors_so_far)} errors")
# Generate response (with whatever we have — success or final error)
results_text = ""
if tool_output:
results_text = f"Tool result:\n{tool_output[:500]}"
elif errors_so_far:
results_text = f"All {len(errors_so_far)} query attempts failed:\n"
for err in errors_so_far[-2:]:
results_text += f" {err['error'][:100]}\n"
resp_messages = [
{"role": "system", "content": self.RESPONSE_SYSTEM.format(

View File

@ -0,0 +1,27 @@
# Expert Recovery
Tests that the expert recovers from SQL errors by retrying with corrected queries,
not by reporting the error and stopping.
## Setup
- clear history
## Steps
### 1. Expert recovers from column error silently
- send: zeig mir alle Geraete von Objekt 4 mit Bezeichnung und Einbaudatum
- expect_trace: has tool_call
- expect_response: not contains "Unknown column" or "1054" or "error" or "Error"
- expect_response: length > 20
### 2. Multi-table query with potential errors
- send: zeig mir alle Nutzer und ihre Geraete fuer Kunde 2
- expect_trace: has tool_call
- expect_response: not contains "Unknown column" or "1054" or "error" or "Error"
- expect_response: length > 20
### 3. Expert does not give up on first failure
- send: zeig mir Verbrauchswerte fuer Geraet 50 im letzten Monat
- expect_trace: has tool_call
- expect_response: not contains "I need assistance" or "developer" or "schema issue"
- expect_response: length > 10