Voice-Bot Substanz-Iteration — Knowledge-Base + Verfuegbarkeit + Confirm-Loop

Overview

Sub-Plan zum Master-Plan 2026-05-13-001. Detailliert die Master-Units 2 (Knowledge-Base + Tool-Splitting), 3 (Verfuegbarkeits-Check) und 4 (Confirm-Loop) in 6 implementierbare Sub-Units. Zerlegt die im Master schon entschiedenen Architektur-Pfaeder so dass ein Implementer (Marvin + Claude in ce:work) ohne Re-Planning direkt loslegen kann.

Stand vor diesem Plan (2026-05-13 Abend, nach Live-Test):

  • Master-Units 1 (Telegram-Push) + 5 (Eskalation) implementiert + Live-getestet
  • Live-Fix: Multi-Lang (DE/TR/AR/KU/RU/PL/IT/FR) Code-Switching + end_call-Tool
  • Marvin’s Test-Feedback: „Das ist schon ziemlich gut” — Bot redet sauber, eskaliert, verabschiedet sich, ist mehrsprachig. Halluziniert aber bei Speisekarten-Fragen offensichtlich („Karte wechselt regelmaessig” — frei erfunden, siehe _index.md §Aylem-Test-Outputs).

Was sich aendert: Bot bekommt eine echte Knowledge-Base (menu.md, hours.md, policies.md, faq.md) und 4 neue Tools (search_knowledge + 3 Convenience-Tools get_menu/get_hours/get_allergens). Verfuegbarkeits-Check verhindert Buchungen ausserhalb Oeffnungszeit. Confirmation-Loop verhindert dass STT-Verhoerer ungeprueft in die DB wandern.

Was sich nicht aendert: Stack, Tools 1/5 (Push/Eskalation), Multi-Lang-Verhalten, end_call, DSGVO-Stand. Master-Plan-Decisions bleiben — dieser Sub-Plan operationalisiert nur was dort schon entschieden ist.

Problem Frame

Aus Live-Test 2026-05-13 — drei konkrete UX-Defekte:

  1. Halluzinations-Problem (Menu/Preise/Allergene): Bot antwortet auf „Was kostet ein Doener-Teller?” oder „Habt ihr was Veganes?” aus dem LLM-Pretraining + System-Prompt-Improvisation, nicht aus echten Restaurant-Daten. Glaubwuerdigkeitskiller in Sales-Demo.
  2. Blind-Buchungs-Problem (Verfuegbarkeit): Bot ruft book_reservation ohne zu pruefen ob das Restaurant zur gewuenschten Zeit ueberhaupt offen ist. Anrufer kann „Tisch am Montag” buchen obwohl Doener-Restaurant typisch Montag-Ruhetag hat.
  3. STT-Vertrauens-Problem (Confirm-Loop): Bot uebernimmt STT-Output 1:1 in book_reservation-Args. Bei Verhoerer „50 statt 15 Personen” landet das ungeprueft in der DB. Master-Plan-Unit 4 hat das spezifiziert — hier wird es operationalisiert.

Quelle dieses Sub-Plans: Master-Plan-Units 2/3/4 (schon im document-review-pass gehaertet) plus zwei Live-Test-Iterationen mit gezielten Test-Calls.

Requirements Trace

  • R1. Bot beantwortet Menu-/Preis-Fragen ausschliesslich aus knowledge/aylem/menu.md, niemals aus Pre-Training. Verifizierbar via Conversation-Log: jeder Menu-Antwort geht ein Tool-Call get_menu oder search_knowledge voraus.
  • R2. Bot beantwortet Oeffnungszeiten-Fragen aus knowledge/aylem/hours.md. Versteht relative Anfragen („Habt ihr morgen offen?” → Datum aus Date-Context + Tag-of-week-Lookup).
  • R3. Bot beantwortet Allergen-Fragen aus menu.md (Allergene pro Gericht) oder faq.md. Bei „ich habe Nussallergie” → konkrete Antwort welche Gerichte safe sind, kein generisches „rufen Sie an”.
  • R4. Bot ruft check_availability(date, time, guests) vor jedem book_reservation auf. Bei nicht-verfuegbar antwortet er hilfreich („Montags haben wir leider Ruhetag — Dienstag oder Donnerstag ist frei”) statt zu buchen.
  • R5. Bot wiederholt vor jedem book_reservation und take_order die gesammelten Slots zur Bestaetigung und ruft das Tool nur nach explizitem „ja/passt/stimmt” auf. Bei Schweigen: nicht auto-buchen, sondern nachfragen oder eskalieren.
  • R6. Bei take_order mit mehr als 5 Items chunked Bot die Bestaetigung in 2 Bloecke („Erst die Hauptgerichte: …, passt das? Und an Beilagen/Getraenken: …, passt das auch?”).
  • R7. System-Prompt erzwingt Tool-Order: bei KB-Fragen ZUERST Knowledge-Tool, bei Reservierung ZUERST check_availability, vor Buchung ZUERST CONFIRM-State. Reihenfolge wird in der Eval-Dimension „Tool-Correctness” (Master-Plan Unit 7) gemessen.

Scope Boundaries

In Scope:

  • 4 neue Markdown-Files unter intern/projekte/telefon-assistent-aws/tools-mcp/knowledge/aylem/ (menu, hours, policies, faq)
  • 5 neue MCP-Tools in tools-mcp/server.py: search_knowledge, get_menu, get_hours, get_allergens, check_availability (plus 1 Refactor: get_services delegiert intern an get_menu)
  • System-Prompt-Erweiterung in livekit-agent/agent.py (Tool-Discipline + Confirm-Loop-Block + Multi-Intent + LMIV-Klartext-Pflicht)
  • Knowledge-Loader-Modul _knowledge.py als stdlib-only Helper inkl. LMIV-Code-zu-Klartext-Mapping
  • Tool-Liste nach Sub-Plan: 9 MCP-Tools (4 existing: book_reservation, take_order, escalate_to_human, get_services; 5 neu wie oben). end_call lebt LOKAL im livekit-agent (kein MCP-Tool), nicht mitgezaehlt.

Out of Scope (explizit; alles im Master-Plan oder Folge-Plan abgedeckt):

  • Echte API-Anbindung an OrderStork / TastyIgniter / GloriaFood (Master-Plan-Open-Loop)
  • Multi-Tenant (Master-Unit 11 — eigener Sub-Plan oder eigene Iteration)
  • DSGVO-Pre-Flight + Loesch-CLI + AVV (Master-Unit 12 — eigener Sub-Plan)
  • Conversation-Logging in JSONL (Master-Unit 6)
  • Claude-as-Judge-Eval (Master-Unit 7)
  • System-Prompt-Restruktur in 7 Sektionen (Master-Unit 8 — die Confirm-Loop-Aenderung hier ist eine Vorstufe)
  • STT-Confidence-Fallback / Senior-Pattern (Master-Sub-Unit 4b — Folge-Iteration)
  • Synthetic Test-Personas (Master-Plan-Out-of-Scope)
  • Demo-Pack (Master-Unit 13)

Context & Research

Relevant Code and Patterns

  • server.py — heutiger MCP-Server. Pattern fuer neue Tools: @mcp.tool() async def name(...) -> str mit DB-Insert + Telegram-Push + Logging. Bei diesem Sub-Plan: Knowledge-Tools haben keinen DB-Insert, aber Logging + Push (nur bei Confirm-Trigger-Eskalationen).
  • _telegram.py — Vorbild fuer den neuen _knowledge.py-Helper (stdlib-only, swallowing errors, sauberes Logging).
  • agent.pySYSTEM_PROMPT_TEMPLATE ist der Erweiterungspunkt. Aktuell hat es 3 Blocks (Persona, Mehrsprachigkeit, Eskalation) — der neue Block „Tool-Discipline + Confirm-Loop” wird vor „Mehrsprachigkeit” eingefuegt damit Tool-Order vor Sprach-Switch geklaert ist.
  • voice-bot-de-pattern.md — Anti-IVR-Phrasen-Liste, Spoken-German-Block. Wird in den Tool-Discipline-Block gegen Banned-Phrases verlinkt.

Institutional Learnings

  • Knowledge-RAG ist Greenfield im Vault — keine bestehende Implementation zum Wiederverwenden. Erste Implementation. Wenn das Pattern gut funktioniert: kandidat fuer voice-bot-de-pattern.md Erweiterung.
  • Telegram-Push-Pattern (Phase 1, Unit 1) hat Cross-Repo-Bindungs-Problem gezeigt — agentic_common.telegram war Lambda-Layer, nicht pip-installable. Sub-Plan zieht Konsequenz: _knowledge.py ebenfalls stdlib-only, kein Import aus anderen Repos.
  • DB-Schema-Freeze ist seit Master-Plan-Operational-Note aktiv — Sub-Plan aendert keine Tabellen, nur Reads aus Knowledge-Files.

External References

Bereits im Master-Plan zitiert, hier nur die fuer diese Iteration relevanten:

  • GetStream Restaurant RAG Pattern — Vorbild fuer Markdown-Files unter knowledge/, Tool delegiert an Retrieval. Wir nehmen das Pattern, aber ohne Turbopuffer (Substring + DE-Synonyme reichen fuer <10 kB).
  • Kwindla Advice on Voice Agents §Tool-Splitting — bestaetigt unseren Master-Plan-Entscheidung: spezifische Tools (get_menu, get_hours) statt eines generischen Search-Tools sind robuster.
  • LMIV (EU Lebensmittel-Informations-Verordnung) — Pflichtangaben Allergene. Beeinflusst Inhalt von menu.md (alle 14 EU-Allergene pro Gericht annotieren).

Key Technical Decisions

  • Knowledge-Files unter tools-mcp/knowledge/aylem/, nicht im Vault unter intern/kunden/aylem/. Begruendung: Knowledge-Files werden vom MCP-Subprocess gelesen — Pfad-Auflosung relativ zum MCP-Verzeichnis. Vault-Konvention waere semantisch sauberer, aber praktisch unguenstig. Plus: tools-mcp/ ist .gitignored (Vault-Pattern fuer Code-Subprojekte) — Knowledge-Files leben mit dem Code zusammen.
  • hours.md-Format: YAML-Frontmatter mit weekly_hours-Dict + optional holidays-Liste. Begruendung: strukturierte Daten = strukturierter Parser = robuste Verfuegbarkeit-Logic. Markdown-Tabellen waeren menschen-lesbarer aber Parser-anfaelliger. Beispiel-Struktur siehe High-Level Technical Design.
  • menu.md-Format: Markdown mit Headings pro Kategorie + Frontmatter mit Tenant-Meta. Begruendung: jede Kategorie (Hauptgerichte / Beilagen / Getraenke / Vegetarisch) ist eine Section, Heading-basierter Chunker zerlegt automatisch. Allergene pro Gericht inline als „[Allergene: A, B, F]“-Notation.
  • Section-Retrieval: Substring + DE-Synonym-Liste, kein BM25. Begruendung: Master-Plan-Decision. Mit 4 Files je 1–3 kB ist Substring schnell und ausreichend. Synonym-Liste wird beim Schreiben iterativ aufgebaut (Test-Queries → Misses identifizieren → Synonyme ergaenzen).
  • check_availability returnt strukturiertes Dict {"available": bool, "reason": str, "alternative_suggestion": str} (als JSON-String fuer MCP-Kompatibilitaet). Begruendung: Bot soll selbst die Anrufer-Antwort formulieren (mehrsprachig!), nicht das Tool eine fertige Phrase mitliefern. Tool gibt Rohdaten + Begruendung, Bot baut die Antwort.
  • Confirmation-Loop als System-Prompt-Pattern, nicht als Tool. Begruendung: Master-Plan-Decision. CONFIRM-State ist Konversations-Mechanik, kein Backend-Vorgang. Kein neues Tool noetig — nur Prompt-Block.
  • Fail-Behavior bei Parser-/Read-Fehlern (differenziert). Begruendung: nicht alle „Fehler” sind gleich, Pauschal-fail-open ist gefaehrlich (siehe security + adversarial Review). Differenzierung:
    • hours.md komplett unlesbar oder nicht vorhanden (Setup-Fehler, kein Buchungs-Risk): {"available": true, "reason": "Verfuegbarkeit konnte nicht geprueft werden, Team meldet sich"} + log.exception + Telegram-Push mit urgent=True an Restaurant. Selbst dann: kein book_reservation ohne explizite User-Confirm.
    • hours.md parsbar aber requested weekday closed oder Holiday-Treffer: harter {"available": false, ...} — KEIN fail-open.
    • Datum oder Time-String unparsbar: {"available": false, "reason": "Datum konnte ich nicht verstehen"} — Bot fragt nach.
    • realistic-mock-Daten + Holiday-Liste unvollstaendig: bekanntes Risiko (Mock hat nicht alle Feiertage). Mitigation: wenn inhalt_status: realistic-mock, Bot-System-Prompt zwingt vor book_reservation zusaetzlich „Bitte bestaetigen Sie nochmal das Datum”-Confirm-Pass (siehe Unit 5 Approach).
  • LMIV-Allergen-Codes zentral in _knowledge.py zu Klartext aufloesen. Begruendung: User-Feedback aus design-Review — „Allergene A, G” am Telefon ist unverstaendlich. _knowledge.py haelt das LMIV-Code-Mapping (A=Gluten, G=Milch, ...), alle Tools (search_knowledge, get_menu, get_allergens) returnen IMMER Klartext (enthaelt Gluten und Milch). Codes erscheinen NIE im Tool-Output und damit NIE in Voice-TTS.
  • Knowledge-Files-Trust-Modell: operator-Level. Begruendung: Knowledge-Files werden nicht von Anrufern editiert. Trust-Boundary verlaeuft zwischen Anrufer-Input (untrusted) und Knowledge-File-Inhalt (trusted). Bei Multi-Tenant-Erweiterung (Master-Unit 11) muss diese Annahme neu gepruef werden — dort kann der Kunde sein eigenes Knowledge editieren, dann ist Trust-Level untrusted operator. In diesem Sub-Plan: Single-Tenant, Marvin editiert allein, Risiko niedrig.
  • visibility: public-kunde als Pflicht-Frontmatter-Feld auf allen Knowledge-Files. Begruendung: Vault-CLAUDE.md-Rule 7 (Internes nie in Kunden-Export). Knowledge-Files gehen ueber Voice-Output an Anrufer = Kunden-Export. Loader-Code verweigert das Laden eines Files ohne visibility: public-kunde (fail-closed).
  • get_services wird Wrapper um get_menu. Begruendung: Backward-Compatibility mit bestehendem System-Prompt (Master-Plan-Unit-2-Approach hat das auch so). Wenn das Tool aufgerufen wird, returnt es Menu-Inhalt — fuer alte Tool-Aufrufe transparent.

Open Questions

Resolved During Planning

  • Format hours.md / menu.md → YAML-Frontmatter + Markdown-Sections (siehe Decisions).
  • Section-Retrieval-Algorithmus → Substring + Synonyme, kein BM25 (Master-Plan-Decision, hier bestaetigt).
  • Wo Knowledge-Files liegentools-mcp/knowledge/<tenant>/ (siehe Decisions).
  • check_availability Return-Format → JSON-String mit Dict-Inhalt (siehe Decisions).

Deferred to Implementation

  • Konkrete Aylem-Speisekarten-Items + Preise. Marvin hat „keine Aylem-Telefonate”-Annahme. Quellen fuer Inhalt: (a) aylem.orderstork.com Public-Menu, (b) typische DACH-Doener-Restaurant-Karten als Default. Inhalt wird beim Schreiben des Files realistisch gefuellt — Marvin kann selbst nach-editieren. Implementer-Disclaimer im Frontmatter: inhalt_status: realistic-mock damit klar ist dass die Karte nicht von Aylem freigegeben ist.
  • Deutsche Synonym-Liste fuer search_knowledge. Initialer Startsatz (knapp, iterativ erweiterbar): oeffnungszeit ↔ öffnungszeit ↔ öffnungszeiten ↔ geoeffnet, karte ↔ speisekarte ↔ menü, preis ↔ kostet ↔ kosten ↔ EUR ↔ Euro, allergie ↔ allergen, vegan ↔ vegetarisch ↔ fleischlos. Plus ae/oe/ue ↔ ä/ö/ü als generische Regel. EUR/Euro im Preis-Synonym-Set ist zwingend, weil menu.md die Codes „12.50 EUR” enthaelt und der Substring-Score sonst kein Match auf „was kostet” findet (siehe adversarial F2). Weitere Synonyme erst nach realen Test-Queries — Test-Iteration-Aufwand fuer iterative Erweiterung: ~10 s pro Patch (Worker-Restart noetig, da Liste Code-Konstante).
  • Konkrete Aylem-Oeffnungszeiten. Default-Annahme: Montag Ruhetag, Di–Do 11:30–22:30, Fr 11:30–23:00, Sa 12:00–23:00, So 12:00–22:00. Bei realer Aylem-Pilotierung (Folge-Plan) korrigieren.
  • Wo wird die Synonym-Liste verwaltet — Code oder Daten-File? Default: als Konstante in _knowledge.py (Code). Wenn sich zeigt dass pro Tenant unterschiedlich noetig: nach knowledge/<tenant>/_synonyms.yaml migrieren (Master-Unit-11-Multi-Tenant-Iteration). Konsequenz fuer Sub-Plan-Iteration: Jeder Synonym-Edit kostet Worker-Restart (~5–10 s). Plant 10 s pro Patch ein wenn Sie 20+ Synonym-Adjustments brauchen.
  • Section-Chunker: ## vs ### Hierarchie. Default: Chunker splittet primaer auf ## (Top-Level-Kategorien). ###-Sub-Headings bleiben innerhalb der ##-Section sichtbar. Fuer get_allergens faehrt ein zusaetzlicher Sub-Chunker auf ### der nur innerhalb der ##-Section sucht. Score auf section_text = heading + "\n" + body (Heading + Body konkateniert, Heading nicht extra gewichtet — einfacher, vorhersagbarer). Konkrete Implementation in Unit 2.
  • Score-Floor fuer search_knowledge: wenn Top-1-Score == 0 (keine Substring/Synonym-Treffer), returnt das Tool den Default-String "Zu deiner Frage habe ich nichts Spezifisches gefunden. Bitte praezisier oder ich leite an unser Team weiter." statt random Top-3-Sections. Verhindert dass der LLM unfundiert Material reininterpretiert. Konkrete Schwelle wird beim Schreiben des Scoring-Codes festgelegt — vermutlich Top-1-Match-Count >= 1.

High-Level Technical Design

Das illustriert die intendierte Architektur und ist directional guidance fuer Review, nicht Implementation-Spec.

Tool-Call-Sequence: „Was kostet ein Doener-Teller?”

sequenceDiagram
    participant U as Anrufer
    participant B as Bot (Haiku 4.5 EU)
    participant M as tools-mcp Subprocess
    participant K as knowledge/aylem/

    U->>B: "Was kostet ein Doener-Teller?"
    Note over B: System-Prompt-Regel:<br/>Menu-Frage -> get_menu zuerst
    B->>M: get_menu(category="doener-teller")
    M->>K: load menu.md, chunk by heading
    K-->>M: section "Doener-Teller"
    M-->>B: "Doener-Teller mit Lamm: 12.50 EUR, scharf optional. Allergene: A, G."
    B->>U: "Der Doener-Teller mit Lamm kostet zwoelf-fünfzig."

hours.md-Format (Beispiel-Sketch)

---
tenant: aylem
inhalt_status: realistic-mock
visibility: public-kunde
last_review: 2026-05-13
weekly_hours:
  monday: closed
  tuesday: ["11:30-15:00", "17:00-22:30"]
  wednesday: ["11:30-22:30"]
  thursday: ["11:30-22:30"]
  friday: ["11:30-23:00"]
  saturday: ["12:00-23:00"]
  sunday: ["12:00-22:00"]
# holidays-Feld out-of-scope dieser Iteration — siehe Open Questions / Risks.
# notes_de-Feld entfernt — Reservierungs-Policy gehoert nach policies.md.
---
 
# Oeffnungszeiten Aylem Eat & Meet
 
## Regelbetrieb
Montags Ruhetag. Dienstag bis Donnerstag von 11:30 bis 22:30 Uhr,
mit Mittagspause Dienstag 15:00-17:00.
...

Strukturierte Daten im Frontmatter (Parser-Input fuer check_availability), Mensch-lesbare Beschreibung im Body (Output fuer get_hours).

---
tenant: aylem
inhalt_status: realistic-mock
visibility: public-kunde
currency: EUR
last_review: 2026-05-13
---
 
## Doener-Teller
 
### Doener-Teller mit Lamm
Preis: 12.50 EUR · scharf optional · Allergene: Gluten (A), Milch (G)
 
### Doener-Teller vegetarisch (Halloumi)
Preis: 11.50 EUR · Allergene: Gluten (A), Milch (G)
 
## Beilagen & Vorspeisen
...
 
## Getraenke
...
 
## Allergen-Codes (nur tatsaechlich verwendete, LMIV-konform)
A=Gluten, C=Eier, F=Soja, G=Milch/Laktose, M=Senf, N=Sesam

Das Wort „Preis” im Body ist bewusst — der Substring-Score braucht das Token, weil Anrufer „was kostet” sagt und der Synonym kostet ↔ Preis ↔ EUR matched. Allergene werden im File auch schon als Klartext-Format (Gluten (A)) hinterlegt damit der Tool-Output ohne weitere Aufloesung verstaendlich ist.

Confirmation-Loop-State (Pseudo-Struktur im System-Prompt)

[CONFIRM-State fuer book_reservation]:
  Pflicht-Slots: {date, time, guests, name, phone}
  Optional-Slots (kein Confirm noetig): {notes}

  Wenn alle Pflicht-Slots gesammelt:
    1. "Lassen Sie mich kurz wiederholen: <slots in 1 Satz>. Passt das so?"
    2. Auf "ja" / "passt" / "stimmt" / "genau" / "richtig" warten
    3. Auf Korrektur: betroffenen Slot updaten, dann zurueck zu Schritt 1
    4. Bei Schweigen: einmal nachfragen ("Sind Sie noch dran? Soll ich die
       Reservierung jetzt anlegen?")
    5. Bei weiterem Schweigen / unklarer Antwort: escalate_to_human(
       reason="Confirm-Stille", summary=<slots>, urgency="normal")

[CONFIRM-State fuer take_order]:
  Pflicht-Slots: {items, customer_name, phone}
  Optional-Slots: {address, pickup_time}

  Bei >5 items: chunk in 2 Bloecke
    Block 1: Hauptgerichte
    Block 2: Beilagen + Getraenke
  Pro Block: wiederholen + Bestaetigung abwarten

  Sonst wie book_reservation-CONFIRM.

Implementation Units

flowchart TB
    U1[Unit 1: Aylem Knowledge-Files anlegen]
    U2[Unit 2: _knowledge.py Loader + search_knowledge Tool]
    U3[Unit 3: get_menu/get_hours/get_allergens Convenience-Tools]
    U4[Unit 4: check_availability Tool + hours.md Parser]
    U5[Unit 5: System-Prompt Tool-Discipline + Confirm-Loop]
    U6[Unit 6: End-to-End Smoke-Test]

    U1 --> U2
    U1 --> U4
    U2 --> U3
    U3 --> U5
    U4 --> U5
    U5 --> U6
  • Unit 1: Aylem Knowledge-Files anlegen (2026-05-13: 4 Files unter tools-mcp/knowledge/aylem/ mit YAML-Frontmatter inhalt_status: realistic-mock + visibility: public-kunde. YAML-Parsing verifiziert.)

    Goal: Vier Markdown-Files (menu.md, hours.md, policies.md, faq.md) unter tools-mcp/knowledge/aylem/ mit realistic-mock-Inhalt — genug Substanz dass spaetere Tools sinnvolle Outputs liefern, klar als „nicht von Aylem freigegeben” gekennzeichnet.

    Requirements: R1, R2, R3 (Daten-Substrat fuer alle drei)

    Dependencies: keine

    Files:

    • Create: intern/projekte/telefon-assistent-aws/tools-mcp/knowledge/aylem/menu.md
    • Create: intern/projekte/telefon-assistent-aws/tools-mcp/knowledge/aylem/hours.md
    • Create: intern/projekte/telefon-assistent-aws/tools-mcp/knowledge/aylem/policies.md
    • Create: intern/projekte/telefon-assistent-aws/tools-mcp/knowledge/aylem/faq.md
    • (knowledge/README.md gestrichen — premature documentation; Multi-Tenant-Pattern landet stattdessen in voice-bot-de-pattern.md wenn die Iteration validiert ist.)

    Approach:

    • Jedes File mit YAML-Frontmatter (Felder: tenant, inhalt_status: realistic-mock, currency bei menu, last_review). inhalt_status macht deutlich dass Marvin den Inhalt nicht von Aylem verifiziert hat.
    • menu.md: 4–6 Kategorien (Doener-Teller, Beilagen, Getraenke, Vegetarisch, Desserts), pro Kategorie 3–5 Items, Preis in EUR + auch das Wort EUR ausgeschrieben (damit Substring-Score auf „kostet” via Synonym kostet↔EUR matched), Allergene als LMIV-Code (nur die in der Karte tatsaechlich vorkommenden Codes, typisch A=Gluten, C=Eier, F=Soja, G=Milch, M=Senf, N=Sesam). Allergen-Legende am Ende der Datei (Section ## Allergen-Codes (LMIV-EU-konform)).
    • hours.md: YAML weekly_hours mit Mo–So. Body als Mensch-lesbarer Text fuer get_hours-Output. holidays-Feld ist out-of-scope dieser Iteration (kein Maintenance-Mechanismus, kein klarer Owner) — wird erst spaeter ergaenzt wenn Aylem-Live-Pilot startet. Folge: check_availability prueft nur Wochentag + Oeffnungsfenster + Personenzahl, nicht Feiertage.
    • policies.md: Reservierungs-Policy (min 2 Personen, Stornierung), Lieferzone (Hamm + 5 km), Mindestbestellwert (20 EUR), Zahlungsarten.
    • faq.md: Top-10 FAQ — Parken, WLAN, Kinderstuhl, Hund-erlaubt, Geburtstagsfeiern (— letzteres triggert Eskalation, gut fuer Test), vegane Optionen, glutenfrei, halal.
    • Inhalts-Quelle: aylem.orderstork.com Public-Page als Anhaltspunkt, generische Doener-Restaurant-Defaults wo nicht verfuegbar. Implementer-Notiz im File-Header: „Inhalte sind realistische Defaults, nicht von Aylem-Inhaber freigegeben. Vor Live-Pilot mit echtem Inhaber-Input ersetzen.”

    Patterns to follow:

    Test scenarios:

    • Happy path: alle 4 Files existieren, YAML-Frontmatter parsbar, Body nicht leer
    • Happy path: menu.md hat mindestens 12 Gerichte mit Preisen und Allergen-Codes
    • Happy path: hours.md YAML-Block hat alle 7 Wochentage definiert (closed oder Liste)
    • Edge case: jede mit inhalt_status: realistic-mock markiert
    • Integration: PyYAML kann hours.md ohne Exception laden

    Verification: python -c "import yaml; data = yaml.safe_load(open('knowledge/aylem/hours.md').read().split('---')[1]); print(data['weekly_hours'])" zeigt strukturierte Wochentag-Daten. Bei manuellem Lesen ergibt jeder File ein konsistentes Bild eines plausiblen Doener-Restaurants.

  • Unit 2: _knowledge.py Loader + search_knowledge Tool (2026-05-13: _knowledge.py mit load_tenant_files, chunk_by_heading, score_section, search, parse_hours; search_knowledge Tool in server.py; pyyaml + pytest installiert; 16 Tests in tests/test_knowledge.py gruen.)

    Goal: Markdown-Heading-Chunker + Substring-Retrieval + search_knowledge MCP-Tool. Fuer generische KB-Fragen die nicht in get_menu/get_hours/get_allergens fallen.

    Requirements: R1, R2, R3 (Retrieval-Layer fuer alle drei)

    Dependencies: Unit 1

    Files:

    • Create: intern/projekte/telefon-assistent-aws/tools-mcp/_knowledge.py
    • Modify: intern/projekte/telefon-assistent-aws/tools-mcp/server.py (search_knowledge-Tool + Import _knowledge)
    • Modify: intern/projekte/telefon-assistent-aws/tools-mcp/requirements.txt (pyyaml>=6.0 + pytest>=8.0 ergaenzen — beides nicht im aktuellen venv installiert)
    • Test: intern/projekte/telefon-assistent-aws/tools-mcp/tests/test_knowledge.py (pytest, neu — bisher gibt es kein tests/-Setup; Unit 2 baut das auf)
    • Create: intern/projekte/telefon-assistent-aws/tools-mcp/tests/__init__.py (leer, damit pytest discoverable)
    • Modify: intern/projekte/telefon-assistent-aws/tools-mcp/README.md (Tools-Tabelle ergaenzen)

    Approach:

    • _knowledge.py enthaelt:
      • load_tenant_files(tenant: str) -> dict[str, str] — laedt alle 4 Markdown-Files, returnt {filename: content}. Fail-loud wenn Tenant-Ordner fehlt.
      • chunk_by_heading(content: str) -> list[(heading, body)] — splittet an ##/### Headings, gibt Sections zurueck.
      • score_section(query: str, section_text: str) -> float — Substring-Match-Count + Synonym-Expansion. Synonyme als Modul-Konstante DE_SYNONYMS: dict[str, list[str]].
      • search(query: str, tenant: str, top_k: int = 3, max_output_chars: int = 1500) -> str — orchestriert: load → chunk → score → top_k → joinen mit Source-Tag. Schneidet hart bei 1500 Zeichen.
    • search_knowledge MCP-Tool: thin wrapper um _knowledge.search(). Query auf 200 Zeichen begrenzen, sonst returnt „Frage ist zu lang, bitte praezisieren”.
    • System-Prompt-Update fuer dieses Tool kommt in Unit 5.
    • Path-Resolution: knowledge/<tenant>/ via Path(__file__).resolve().parent / "knowledge" / tenant (file-relativ, niemals cwd-relativ). Override per ENV TOOLS_KNOWLEDGE_DIR — Pfad-Whitelist-Pruefung: ENV-Wert MUSS unterhalb von Path(__file__).resolve().parent liegen, sonst fail-closed mit ValueError beim Modul-Import (Path-Traversal-Schutz). tenant-String aus TOOLS_TENANT-ENV: nur [a-z0-9-_] erlaubt, kein /, kein .. — Substring-Pruefung via Regex beim Load.
    • Tool-Argument-Validierung pro Tool (Input-Sanitization, gegen Path-Traversal + ReDoS + DoS):
      • search_knowledge(query): max 200 Zeichen, sonst Hinweis-String returnen.
      • get_menu(category): optional, falls vorhanden max 60 Zeichen, nur [a-zA-Z0-9-_äöüß ], kein Pfad-Separator.
      • get_allergens(item): required-non-empty (sonst „bitte Gericht nennen”), max 100 Zeichen, gleicher Whitelist-Regex wie get_menu.
      • get_hours(day): optional, falls vorhanden Whitelist {None, "monday"-"sunday", ISO-Datum-Regex \d{4}-\d{2}-\d{2}}.
      • check_availability(date, time, guests): date Whitelist ISO, time \d{2}:\d{2}, guests 1..50 (bei >12 returnt false mit „grosse Gruppe”, siehe Unit 4).
      • File-Read-Cap: max 256 KB pro Knowledge-File, sonst log.warning + Skip dieser Datei.
    • LMIV-Code-Mapping als Konstante: LMIV_CODES = {"A": "Gluten", "G": "Milch", ...}. Initiale Liste nur die Codes die in der realen Doener-Karte vorkommen (A, C, F, G, M, N — kein Krebstier/Lupin/Sulfit-Code wenn nicht im menu.md vorhanden). Tool-Output ersetzt Codes IMMER mit Klartext bevor Returnen.

    Patterns to follow:

    • _telegram.py — stdlib-only, kein Cross-Repo-Import, error-swallowing-Pattern, Logger-Setup.
    • server.py — Tool-Decorator-Pattern, async def, log.info-Format.

    Test scenarios:

    • Happy path: search("doener teller preis") → liefert Menu-Section mit „Doener-Teller mit Lamm: 12.50 EUR”
    • Happy path: search("oeffnungszeit montag") → liefert hours.md-Section mit Ruhetag-Hinweis
    • Edge case: Synonyme — search("öffnungszeiten") matched gegen „oeffnungszeit” via Synonym-Map
    • Edge case: query > 200 Zeichen → returnt Praezisierungs-Hinweis, keine Suche
    • Edge case: leere Knowledge-Base (Tenant-Ordner existiert aber Files leer) → returnt „keine Wissensbasis verfuegbar”
    • Edge case: Tenant-Ordner fehlt → returnt sauber „keine Wissensbasis verfuegbar”, loggt warning, kein Crash
    • Edge case: query mit Sonderzeichen / Umlauten / Anfuehrungszeichen → kein Crash, sauberes Matching
    • Error path: YAML-Parse-Fehler in hours.md → search returnt nur die anderen Files, loggt warning
    • Integration: zwei aufeinanderfolgende search()-Aufrufe in derselben Session liefern konsistente Top-3

    Verification: pytest tests/test_knowledge.py ist gruen. Manueller Direkt-Tool-Call via python -c "from server import search_knowledge; import asyncio; print(asyncio.run(search_knowledge('doener preis')))" liefert sinnvollen Output mit File-Source-Tag.

  • Unit 3: Convenience-Tools get_menu, get_hours, get_allergens (2026-05-13: alle 3 Tools live + get_services refactored zu wrapper. Smoke-Tests gruen: get_hours() liefert heute+morgen formatiert, get_hours(“monday”) liefert “Ruhetag”, get_allergens(empty) returnt freundlichen Fallback.)

    Goal: Drei spezifische Tools die fuer den LLM klarer benannt sind als generisches search_knowledge — folgt der Master-Plan-Decision „Tool-Splitting statt Discipline-Erziehung”.

    Requirements: R1, R2, R3, R7

    Dependencies: Unit 2

    Files:

    • Modify: intern/projekte/telefon-assistent-aws/tools-mcp/server.py (drei neue Tools + get_services Refactor)
    • Test: intern/projekte/telefon-assistent-aws/tools-mcp/tests/test_knowledge.py (Tests fuer die Convenience-Wrapper ergaenzen)

    Approach:

    • get_menu(category: Optional[str] = None) -> str: delegiert an _knowledge.search(query=f"menu speisekarte {category or ''}", ...). Bei category=None: returnt Top-Kategorien-Uebersicht. Bei category="doener-teller": returnt nur diese Section.
    • get_hours(day: Optional[str] = None) -> str: bei day=None heute + naechster Tag. Bei day="monday" Wochentag-spezifisch. Bei day="2026-05-17" ISO-Datum-spezifisch. Nutzt Date-Context-Logic aus agent.py build_date_context als Inspiration fuer Relative-Datum-Aufloesung.
    • get_allergens(item: str) -> str: searched gezielt in menu.md nach dem Gericht + LMIV-Code-Mapping. Returnt z.B. „Doener-Teller mit Lamm enthaelt: Gluten (A), Milch (G). Kein Schweinefleisch.”
    • get_services (bestehend) wird thin wrapper um get_menu(category=None) — Backward-Compat fuer alte System-Prompts.

    Patterns to follow:

    • Bestehende Tool-Signaturen in server.py.
    • Optional[str]-Default-Pattern wie in book_reservation.notes.

    Test scenarios:

    • Happy path: get_menu() → returnt Top-Kategorien-Liste
    • Happy path: get_menu("doener-teller") → returnt die Section mit allen Doener-Variationen
    • Happy path: get_hours() → returnt heute + morgen, mit [empfehlung]-Block bei Ruhetag
    • Happy path: get_hours("monday") → returnt „Montag: Ruhetag”
    • Happy path: get_allergens("doener-teller mit lamm") → returnt „Gluten, Milch” + Klarstellung „kein Schwein”
    • Edge case: get_menu("nonsense-category") → returnt „Kategorie nicht gefunden, hier die Top-Kategorien-Uebersicht” als sinnvoller Fallback
    • Edge case: get_hours("31. Februar") → returnt „Datum nicht erkannt, bitte praezisieren”
    • Edge case: get_allergens("") → returnt „bitte Gericht nennen” statt Crash
    • Integration: alter Tool-Call get_services() returnt weiterhin sinnvoll (Menu-Top-Kategorien)
    • Integration: Voice-Bot fragt zu Anfang get_hours() und kriegt heute/morgen ohne zusaetzliche Args

    Verification: Smoke-Test in Browser: „Was kostet ein Doener?” → Log zeigt get_menu Aufruf, Bot-Antwort enthaelt echten Preis aus menu.md. „Habt ihr morgen offen?” → Log zeigt get_hours Aufruf, Bot kennt Wochentag.

  • Unit 4: check_availability Tool + hours.md Parser (2026-05-13: Tool returnt JSON-String mit available/reason/alternative_suggestion. Mo=Ruhetag/Di-Mittagspause/Vergangenheit/>12 Personen/Datumsfehler alle korrekt. Fail-open mit urgent Telegram-Push bei hours.md-Read-Fehler. 10 Tests in tests/test_availability.py gruen.)

    Goal: Vor jedem book_reservation prueft Bot ob Datum/Zeit in Oeffnungs-Fenster liegt und Personenzahl plausibel ist. Bei Nicht-Verfuegbarkeit liefert Tool eine Begruendung + Alternative-Vorschlag, Bot formuliert die Anrufer-Antwort daraus.

    Requirements: R4

    Dependencies: Unit 1 (hours.md muss existieren), Unit 2 (Loader-Helper im _knowledge.py wiederverwenden)

    Files:

    • Modify: intern/projekte/telefon-assistent-aws/tools-mcp/_knowledge.py (Funktion parse_hours(tenant) -> dict)
    • Modify: intern/projekte/telefon-assistent-aws/tools-mcp/server.py (neues check_availability-Tool)
    • Test: intern/projekte/telefon-assistent-aws/tools-mcp/tests/test_availability.py

    Approach:

    • parse_hours(tenant) in _knowledge.py: laedt knowledge/<tenant>/hours.md, extrahiert YAML-Frontmatter via yaml.safe_load(). Returnt Dict {"weekly_hours": {"monday": "closed", "tuesday": [(time(11,30), time(15,0)), (time(17,0), time(22,30))], ...}, "holidays": [...]}. Wandelt String-Intervals direkt in datetime.time-Tuples um.
    • check_availability(date: str, time: str, guests: int) MCP-Tool:
      1. Datum parsen (datetime.fromisoformat(date)), bei Parse-Fehler → {"available": false, "reason": "Datum konnte ich nicht verstehen"}.
      2. In-Vergangenheit? → {"available": false, "reason": "Datum liegt in der Vergangenheit"}.
      3. Personenzahl 1..12 plausibel? Sonst → {"available": false, "reason": "Bei Gruppen ueber 12 bitte direkt im Restaurant anrufen"}.
      4. Holiday? (hours["holidays"] durchsuchen) → {"available": false, "reason": <holiday_note>}.
      5. Wochentag-Slot pruefen: weekly_hours[weekday] ist "closed"{"available": false, "reason": "<weekday> Ruhetag", "alternative_suggestion": <naechster offener Tag>}.
      6. Wochentag-Slot ist Liste von Intervals: liegt time in einem? → {"available": true, "reason": ""}. Sonst → {"available": false, "reason": "Zur gewuenschten Uhrzeit haben wir leider zu", "alternative_suggestion": <nearest open slot>}.
      7. Bei jedem Parse-/Read-Fehler: fail-open auf {"available": true, "reason": "Verfuegbarkeit konnte nicht geprueft werden, Team meldet sich zur Bestaetigung"} plus log.exception. Anrufer soll nie wegen unserem Code-Problem abgewiesen werden.
    • Returnt JSON-serialisiertes Dict als String (MCP-Tool-Kompatibilitaet).
    • System-Prompt-Update fuer dieses Tool kommt in Unit 5.

    Patterns to follow:

    • Date-Context-Logic aus agent.py build_date_context (Wochentag-Names auf Deutsch, ISO-Format).
    • Tool-Error-Handling aus server.py book_reservation (try/except sqlite3.Error → fallback-string).

    Test scenarios:

    • Happy path: Mi 2026-05-21 um 19:00, 4 Personen → {"available": true, "reason": ""}
    • Edge case: Mo (Ruhetag laut hours.md) → {"available": false, "reason": "Montags Ruhetag", "alternative_suggestion": "Dienstag ab 11:30"}
    • Edge case: Di 16:00 (Mittagspause 15:00-17:00) → {"available": false, "reason": "Mittagspause", "alternative_suggestion": "ab 17:00"}
    • Edge case: Datum in der Vergangenheit (heute-1) → {"available": false, "reason": "Datum liegt in der Vergangenheit"}
    • Edge case: 50 Personen → {"available": false, "reason": "Bei Gruppen ueber 12 bitte direkt anrufen"}
    • Edge case: ISO-Datum mit Holiday-Treffer (2026-12-25) → {"available": false, "reason": "1. Weihnachtsfeiertag, geschlossen"}
    • Edge case: malformed Datum-String („nicht-isoformat”) → {"available": false, "reason": "Datum konnte ich nicht verstehen"}
    • Edge case: malformed Time-String → analog
    • Error path: hours.md fehlt → fail-open auf available=true mit reason „Verfuegbarkeit nicht pruefbar”
    • Error path: YAML-Parse-Fehler in hours.md → analog fail-open
    • Integration: Voice-Bot-Call „Tisch am Montag 19 Uhr” → check_availability returnt Mo-Ruhetag, Bot schlaegt Di vor → User akzeptiert Di → check_availability(Di) returnt available

    Verification: Direkt-Tool-Aufruf mit allen Edge-Case-Inputs liefert konsistente Strukturen. Voice-Test gegen Mo + 50 Personen + Mittagspause produziert hilfreiche statt blinde Antworten.

  • Unit 5: System-Prompt Tool-Discipline + Confirmation-Loop (2026-05-13: TOOL-DISCIPLINE-Block ersetzt die alten “Karte wechselt regelmaessig”-Mock-Anweisungen. Multi-Intent Question-First-Pattern + Korrektur-Limit + Multi-Lang Bestaetigungs-Worte (TR/AR/RU/PL/IT/FR) + Realistic-Mock-Disclaimer-Zusatz + Chunking >5 Items. Worker neu gestartet, neue MCP-Tools sichtbar.)

    Goal: System-Prompt erzwingt Reihenfolge: KB-Frage → Knowledge-Tool, Reservierung → check_availability → CONFIRM-State → book_reservation. CONFIRM-State wiederholt Slots, wartet auf „ja”, nutzt Chunking bei >5 Items, eskaliert bei Schweigen.

    Requirements: R5, R6, R7

    Dependencies: Units 2, 3, 4 (alle Tools muessen existieren bevor der Prompt sie referenziert)

    Files:

    • Modify: intern/projekte/telefon-assistent-aws/livekit-agent/agent.py (SYSTEM_PROMPT_TEMPLATE-Block „Tool-Discipline + Confirm-Loop” neu)

    Approach:

    • Neuer Prompt-Block direkt nach „Wie du Anrufer behandelst” und vor „MEHRSPRACHIGKEIT”:
      TOOL-DISCIPLINE — Welches Tool wann:
      - Speisekarte / Preis / Gericht / „was habt ihr": ZUERST get_menu() oder get_menu(category="<kategorie-name>") aufrufen, dann antworten
      - Oeffnungszeiten / „habt ihr offen" / Wochentag: ZUERST get_hours() oder get_hours(day="<wochentag>"|"YYYY-MM-DD") aufrufen, dann antworten
      - Allergene / „enthaelt X" / vegan / glutenfrei: ZUERST get_allergens(item="<gericht>") aufrufen, dann antworten
      - Sonstige Wissens-Fragen (Parkplatz, WLAN, Hund, Catering...): ZUERST search_knowledge(query="...") aufrufen
      - Reservierung: ZUERST check_availability(date, time, guests) aufrufen. Bei available=false:
        dem Anrufer die reason erklaeren NATUERLICH (nicht IVR-haft) und alternative_suggestion vorschlagen.
        Beispiel-Phrasen statt IVR:
          IVR-schlecht: „Montags haben wir Ruhetag. Eine Alternative waere Dienstag um 11:30."
          natuerlich:  „Ach Mist, Montag ist bei uns Ruhetag. Ginge Dienstag auch? Da sind wir ab halb zwoelf da."
        Bei available=true: weiter zum CONFIRM-State.
      - Bestellung: keinen Verfuegbarkeits-Check noetig (Restaurants nehmen waehrend Oeffnungszeit Bestellungen an —
        Confirm-Loop reicht).
      
      Niemals Menu/Preis/Allergen/Oeffnungszeit aus dem Kopf beantworten. Wenn ein KB-Tool nichts liefert oder
      einen „nicht gefunden"-String returnt: ehrlich „das weiss ich nicht, mein Team meldet sich gleich" und
      escalate_to_human aufrufen.
      
      KB-Tool-OUTPUT-LANGE: Tools koennen bis zu 1.5 kB Text zurueckliefern. Du LIEST DAS NIE WORTWOERTLICH VOR.
      Du fasst in 1-2 Saetzen zusammen und bietest an „mehr Details bei Bedarf". Beispiel:
        Tool returnt: <8 vegetarische Gerichte, alle mit Preis + Allergenen>
        Bot sagt:    „Vegetarisch haben wir einiges, am beliebtesten ist der Falafel-Teller und die
                     Halloumi-Pfanne. Soll ich Ihnen zu einem davon mehr sagen?"
      
      LMIV-Allergen-CODES: niemals Codes wie „A" oder „G" am Telefon vorlesen — IMMER Klartext.
      Beispiel: nicht „Allergene A, G" sondern „enthaelt Gluten und Milch". Das _knowledge-Modul liefert dir
      bereits aufgeloeste Klartext-Form — bleib bei der.
      
      CONFIRM-State vor book_reservation und take_order:
      Sobald alle Pflicht-Slots gesammelt sind, MUSST du die Slots zurueckspielen und auf explizite
      Bestaetigung warten. NIEMALS direkt das Tool aufrufen.
      
      Fuer book_reservation Pflicht-Slots (5): {date, time, guests, customer_name, phone}. Optional (no-confirm): notes.
      Fuer take_order Pflicht-Slots (3): {items, customer_name, phone}. Optional (no-confirm): address, pickup_time.
      
      HINWEIS: Slot-Name ist `customer_name` fuer beide Tools (frueher hiess es bei book_reservation `name` —
      wird einheitlich auf `customer_name` umgestellt in Unit 1 server.py-Refactor).
      
      Confirm-Phrasen variieren (nie zweimal denselben Satz):
      - „Okay, also: 4 Personen am Samstag um 19 Uhr, auf Mueller, 0170 12345. Passt das so?"
      - „Lassen Sie mich kurz zusammenfassen — 4 Leute, Sa 19:00, Mueller, 0170 12345. Stimmt das?"
      - „Ich hab notiert: 4 Personen, Samstag 19 Uhr, Mueller, 0170 12345. Richtig?"
      
      Auf „ja" / „passt" / „stimmt" / „genau" / „richtig" → Tool aufrufen.
      
      Mehrsprachige Bestaetigungs-Worte (gelten genauso als „ja"):
      - Tuerkisch: „tamam", „evet", „olur", „oldu"
      - Arabisch: „naam", „tamam", „aiwa"
      - Russisch: „da", „horosho", „davai"
      - Polnisch: „tak", „ok", „dobrze"
      - Italienisch: „si", „va bene", „perfetto"
      - Franzoesisch: „oui", „d'accord", „parfait"
      Wiederhole den Confirm IMMER in der Sprache des Anrufers.
      
      Auf Korrektur (z.B. „nein, 5 Personen") → Slot updaten, dann erneut bestaetigen.
      KORREKTUR-LIMIT: Wenn der Anrufer im selben Vorgang 3x Korrekturen macht, statt 4. Confirm-Loop
      direkt eskalieren: escalate_to_human(reason="Slot-Verwirrung", summary=<slots>, urgency="normal").
      
      MULTI-INTENT mid-CONFIRM: Wenn der Anrufer mid-CONFIRM eine Frage einwirft (z.B. „warte, was kostet
      der Doener-Teller?" waehrend gerade Reservierungs-Slots wiederholt werden):
        1. Slots in deinem Kopf BEHALTEN — nicht vergessen
        2. Die Frage mit dem richtigen KB-Tool beantworten (kurz)
        3. Anschluss-Satz: „Okay — und zurueck zur Reservierung: <slot-wiederholung>. Passt das so?"
      Dieser Question-First-mit-Slot-Persistence-Pfad ist Pflicht.
      
      Auf Schweigen (Bot wartet >5 s nach Confirm-Frage) → einmal nachfragen: „Sind Sie noch dran? Soll ich
      die Reservierung jetzt anlegen?". Bei weiterem Schweigen (>10 s) → escalate_to_human(
      reason="Confirm-Stille", summary=<slots>, urgency="normal"). NIE auto-buchen.
      
      REALISTIC-MOCK-CONTEXT (gilt fuer Aylem-Demo-Tenant): hours.md und menu.md sind nicht von Aylem
      verifiziert. Bei Reservierungs-Confirm im Realistic-Mock-Modus ergaenze immer einmal:
      „Ich melde das gleich ans Team weiter zur Bestaetigung — die rufen Sie zurueck unter <phone>."
      Macht klar dass die Reservierung noch nicht final-zugesagt ist.
      
      CHUNKING bei take_order mit MEHR als 5 verschiedenen Items:
      - Block 1: Hauptgerichte zurueckspielen, bestaetigen lassen
      - Block 2: Beilagen + Getraenke zurueckspielen, bestaetigen lassen
      - Dann take_order aufrufen
      - Genau 5 Items oder weniger: kein Chunking (eine Confirm-Wiederholung reicht).
      - Beispiel: „Erstmal die Hauptgerichte — 2x Doener mit scharf, 1x Falafel ohne Zwiebel. Passt das?"
        User „ja". „Und an Beilagen — 3x Ayran, 1x Salat. Passt das auch?" User „ja". → take_order.
      
      WICHTIG: Confirm-Loop und alle obigen Regeln gelten genauso bei Mehrsprachigkeit (TR/AR/RU/PL/...).
      Wiederhole in derselben Sprache. Eskalations-Trigger gelten ebenfalls.
      
    • System-Prompt-Laenge bleibt unter 2.5 kB damit Cache greift. Wenn knapp: Anti-IVR-Phrasen-Block kuerzen, Verweis auf voice-bot-de-pattern.md.

    Patterns to follow:

    • Bestehender System-Prompt-Aufbau in agent.py.
    • Pflicht-Vier-Bloecke aus voice-bot-de-pattern.md.
    • „Tool-Pflicht ohne Tool kein Confirm” aus dem bestehenden Block fuer book_reservation (Z. 155).

    Test scenarios (alle als Voice-End-to-End, kein Unit-Test moeglich):

    • Happy path: Reservierungs-Flow mit allen 5 Slots, User bestaetigt mit „ja passt” → check_availability + Confirm-Wiederholung + book_reservation in Log
    • Happy path: Menu-Frage „Was kostet ein Doener-Teller?” → get_menu im Log, Bot antwortet mit Preis aus menu.md (nicht halluziniert)
    • Happy path: Oeffnungszeit-Frage „Habt ihr morgen offen?” → get_hours im Log, korrekte Antwort
    • Edge case: STT-Verhoerer (50 statt 15 Personen) → Bot wiederholt „50”, User korrigiert „nein, 15”, Bot updated, wiederholt erneut, dann book mit 15
    • Edge case: User schweigt nach Confirm-Frage → Bot fragt einmal nach, dann eskaliert (escalate_to_human in Log statt book_reservation)
    • Edge case: Tisch am Montag → check_availability returnt Mo-Ruhetag, Bot schlaegt Di vor, User akzeptiert Di, neuer check_availability(Di), book_reservation(Di)
    • Edge case: Bestellung mit 8 Items → Bot chunked in 2 Bloecke, je einzeln bestaetigen lassen
    • Edge case: User wechselt mid-CONFIRM zu TR („tamam”) → Bot bestaetigt auf TR + book_reservation
    • Integration: Allergen-Frage „Habt ihr was ohne Gluten?” → get_allergens / search_knowledge im Log, Bot antwortet konkret welche Gerichte glutenfrei sind

    Verification: 10+ Voice-Test-Calls in versch. Variationen — keine Halluzination bei KB-Fragen, keine Buchung ohne Confirm, kein Tool-Call ohne Wiederholung im Transkript.

  • Unit 6: End-to-End Smoke-Test (manuelle Test-Sequenz)

    Goal: Definierter Test-Pfad der die Units 1–5 zusammen validiert. Wird einmal manuell durchgespielt nach Implementierung, Logs werden archiviert als „Reference-Case-Demo-Baseline” fuer den Master-Plan Unit 13 (Demo-Pack).

    Requirements: alle (R1–R7)

    Dependencies: Units 1–5 vollstaendig

    Files:

    • Create: intern/projekte/telefon-assistent-aws/demo-pack/2026-05-XX-substanz-iter-walkthrough.md (Datum bei Durchfuehrung einsetzen)

    Approach:

    • Voller Browser-Call-Pfad gegen /cartesia, folgendes Skript abspielen:
      1. Greeting → Bot grueßt mit KI-Disclaimer
      2. Frage zur Karte „Was habt ihr fuer Doener-Teller?” → erwartet get_menu("doener-teller") Log + konkrete Antwort
      3. Allergen-Frage „Hab ich Gluten-Allergie — geht das?” → erwartet get_allergens Log + konkrete glutenfreie Option
      4. Oeffnungs-Frage „Habt ihr Montag offen?” → erwartet get_hours("monday") Log + „Mo Ruhetag” Antwort
      5. Reservierungs-Versuch Mo „Reservieren Sie mir bitte einen Tisch fuer Montag 19 Uhr” → erwartet check_availability Log + Bot schlaegt Alternative
      6. Reservierung Di „Dann Dienstag, 4 Personen, 19 Uhr, Mueller, 0170 12345” → erwartet check_availability ok + CONFIRM-Wiederholung
      7. Confirm-Korrektur Bot wiederholt mit „50” statt „5” Personen → User korrigiert → Bot updated und wiederholt nochmal → User „ja” → erwartet book_reservation Log + Telegram-Push
      8. Bestellung 8 Items „Ich moechte zur Mitnahme bestellen — [8 Items]” → erwartet Chunking in 2 Bloecke + zwei einzelne Bestaetigungen + take_order Log
      9. Sonderwunsch „Macht ihr Catering fuer 80 Personen?” → erwartet escalate_to_human(reason="Sonderwunsch") Log + Telegram-Push
      10. Verabschiedung „Danke, Tschuess” → Bot verabschiedet + end_call + Session-Disconnect
    • Walkthrough-Datei dokumentiert pro Schritt: User-Ausgabe, Bot-Antwort, Tool-Calls aus Log, Telegram-Push-Screenshot (Hash-anonymisiert).

    Patterns to follow:

    • Bestehende Aylem-Test-Outputs-Tabelle in _index.md §Phase 0.

    Test expectation: none — manueller Walkthrough, kein Code-Test. Die Validierung ist die Walkthrough-Datei selbst.

    Verification: Walkthrough-File existiert mit allen 10 Schritten dokumentiert. Keine Halluzination, keine Blind-Buchung, keine Geister-Reservierung. Pro Schritt klare Tool-Call-Spuren im Log.

System-Wide Impact

  • Interaction graph: livekit-agent/agent.py (System-Prompt) ↔ tools-mcp/server.py (5 neue Tools) ↔ tools-mcp/_knowledge.py (Helper) ↔ tools-mcp/knowledge/aylem/*.md (Daten). Plus: bestehende Tools (book_reservation, take_order, escalate_to_human, end_call, get_services) bleiben funktional, get_services wird thin wrapper um get_menu.
  • Error propagation: Knowledge-File-Read-Errors → fail-open auf „weiss ich nicht, Team meldet sich” + log.warning. YAML-Parse-Errors in hours.mdcheck_availability fail-open auf available=true (Anrufer-Schutz). Tool-Call-Errors duerfen Tool-Discipline-Prompt nicht durchbrechen — Bot soll dann eskalieren statt halluzinieren.
  • State lifecycle risks: Confirmation-Loop ist Prompt-State, kein DB-State — bei Worker-Restart mitten in einem Call ist der State weg. Anrufer-Connection bricht in dem Fall auch ab (LiveKit-WebRTC), daher kein Daten-Verlust-Risk. Knowledge-Files werden bei jedem Tool-Call frisch gelesen (kein Cache) — Aenderungen sind sofort wirksam, kein Worker-Reload noetig.
  • API surface parity: get_services bleibt erhalten (als Wrapper an get_menu) — Backward-Compat fuer bestehenden System-Prompt-Pfad und externe Claude-Code/Desktop-Nutzer die den MCP eingebunden haben. Tool-Liste waechst von 4 (book_reservation, take_order, escalate_to_human, get_services) auf 9 (zusaetzlich search_knowledge, get_menu, get_hours, get_allergens, check_availability). MCP-Tool-Liste ist pro AgentSession gecached — Aenderungen an server.py werden NICHT automatisch erkannt; nach jedem server.py-Edit MUSS der Worker manuell neu gestartet werden (Ctrl-C im dev-Terminal + erneut python agent.py dev). Knowledge-Markdown-Edits sind ohne Restart sofort wirksam (Tool liest pro Call frisch).
  • Integration coverage: Voice-End-to-End-Tests (Unit 6) sind nicht durch unit-Tests ersetzbar — Tool-Discipline-Prompt-Befolgung, Confirm-Loop-Verhalten und STT-Korrektur-Flows brauchen den echten Voice-Loop.
  • Unchanged invariants: SQLite-Schema (reservations, orders, escalations) bleibt unveraendert. ID-Format (RES/ORD/ESC-XXXXXX) bleibt. Bedrock-Modell, LiveKit-Region, Cartesia-Voice bleiben. ADR keine-eigene-plattform weiter eingehalten — alle neuen Tools im MCP.

Risks & Dependencies

RiskLikelihoodImpactMitigation
Knowledge-File-Inhalt inhaltlich falsch (Marvin hat keine Aylem-Inhaber-Verifikation)HochHoch (LMIV-Werbeaussage-Risiko bei Allergenen)inhalt_status: realistic-mock-Marker im Frontmatter PLUS System-Prompt-Pflicht-Erweiterung (Unit 5): Bot ergaenzt jede Reservierungs-Confirm um „Team meldet sich zur Bestaetigung” — verschiebt die finale Zusage zum Inhaber. Bei Live-Demo gegen Sales-Lead: Reference-Case-Onepager (Master-Unit 13) macht den Demo-Status explizit. Eskalation: bei Aylem-Live-Pilot-Folge-Plan zwingend echter Inhaber-Input vorab.
Substring-Score liefert irrelevante SectionsMittelMittelSynonym-Liste iterativ erweitern beim Testen. Top-3-Hard-Cap auf 1.5 kB verhindert Token-Aufblaehung selbst bei mehrfachen Treffern.
LLM ignoriert Tool-Discipline-Prompt und antwortet aus Pre-TrainingMittelHochTool-Splitting (klar benannte spezifische Tools) reduziert das schon strukturell. Eval (Master-Unit 7) misst „Hallucination” als Dimension. Iteratives Prompt-Tightening wenn nach 20 Test-Calls Hallucinations sichtbar sind.
check_availability schlaegt fail-open bei realistic-mock zu generoes — bucht obwohl Restaurant zuMittel (bei realistic-mock)Hoch (Anrufer steht vor zugesperrtem Laden)Differenzierter Fail-Pfad (siehe Decisions): fail-open NUR bei Setup-Fehler (hours.md fehlt komplett), nicht bei Daten-Luecken. Plus: System-Prompt zwingt Realistic-Mock-Disclaimer „Team meldet sich zur Bestaetigung” — verschiebt finale Zusage zum Inhaber. Telegram-Push bei fail-open mit urgent=True.
search_knowledge ohne Score-Floor liefert random Top-3 bei No-Match-Query („was habt ihr da so?“)MittelMittelScore-Floor (siehe Decisions): bei Top-1-Score=0 returnt das Tool Praezisierungs-Hinweis statt random Sections. Verhindert Hallucinations-Surrogate.
Confirm-Loop bei Multi-Intent (KB-Frage mid-CONFIRM) loescht gesammelte SlotsMittelMittelSystem-Prompt-Block „MULTI-INTENT mid-CONFIRM” (Unit 5): Question-First-mit-Slot-Persistence-Pfad ist Pflicht. Test-Szenario in Unit 6 ergaenzen.
LLM liest LMIV-Allergen-Codes statt Klartext am Telefon vorMittelMittel_knowledge.py Code-zu-Klartext-Mapping zentralisiert; Tools liefern bereits Klartext im Output; System-Prompt-Verbot Codes vorzulesen. menu.md schreibt Allergene schon in Klartext-Format Gluten (A).
Tool-Liste-Cache pro AgentSession — neue Tools sichtbar erst nach Worker-RestartHoch (waehrend Implementation)NiedrigOperational-Notes klargestellt (siehe oben). Implementer-Hinweis: nach jedem server.py-Edit Ctrl-C + neu starten.
Confirmation-Loop greift bei Multi-Lang nicht (TR/AR-Anrufer wird ohne Confirm gebucht)MittelMittelSystem-Prompt-Block explizit: „Auch bei Mehrsprachigkeit gilt der Confirm-Loop”. Test-Persona „TR-Reservierung” in Unit 6 Smoke-Test.
Knowledge-File-Inhalt aendert sich live waehrend Call (z.B. menu.md edit)NiedrigNiedrigTool liest pro Call frisch. Im schlechtesten Fall: Bot zitiert die alte und die neue Version in zwei aufeinanderfolgenden Saetzen — fuer Reference-Case-Demos akzeptabel.
LMIV-Allergen-Codes in menu.md inkonsistent / falsch zugeordnetMittelNiedrig (Reference-Case) / Hoch (Live-Pilot)Realistic-Mock-Disclaimer. Echter Live-Pilot braucht ohnehin LMIV-Audit (Folge-Plan).
Worker-Hot-Reload via python agent.py dev greift nicht bei _knowledge.py-AenderungNiedrigNiedriglivekit-agents dev-mode hat File-Watcher — testen waehrend Implementation. Wenn nicht: manuell Worker neustarten (~5 s).
search_knowledge-Output bricht UTF-8-Encoding bei SonderzeichenNiedrigNiedrigPython-Default-UTF-8 fuer File-IO. Telegram-Format hat Escape-Helper. Test-Szenario: Umlaut-haltige Section.

Documentation Plan

  • Update Master-Plan: Units 2/3/4 markieren als „siehe Sub-Plan 2026-05-13-002” oder direkt abhaken wenn Sub-Plan komplett ist.
  • Update _index.md Open-Loops-Tabelle: Sub-Plan-Eintrag „Substanz-Iteration geplant 2026-05-13” plus separate Eintraege fuer Knowledge-Base / Verfuegbarkeit / Confirm-Loop wenn implementiert.
  • Update README.md: Tools-Tabelle waechst auf 9 Tools (book, take, escalate, services, search_knowledge, get_menu, get_hours, get_allergens, check_availability) + neues Setup-Step „knowledge-Folder anlegen”.
  • Erweitern voice-bot-de-pattern.md: neuer Section „Knowledge-Base-Pattern fuer Service-Vertikalen” mit _meta.md-Format + Substring+Synonyme-Approach als Wiederverwendungs-Vorlage.

Operational / Rollout Notes

  • Rollout: Lineare Sequenz Unit 1 → 2 → 3 → 4 → 5 → 6. Unit 1 ist parallelisierbar mit Unit 2-Beginn (Mock-Files schreiben + Loader-Code starten ohne Wartezeit), aber Unit 2 braucht Unit 1-Files fertig fuer den ersten Smoke-Test. Unit 5 braucht Tools 2+3+4 alle live damit System-Prompt nicht auf nicht-existierende Tools verweist.
  • Reload-Strategie (verifiziert gegen livekit-agents 1.5.8): python agent.py dev hat einen File-Watcher der ausschliesslich agent.py und Plugin-Module ueberwacht — NICHT tools-mcp/. Konkret:
    • agent.py-Edit → automatischer Hot-Reload, neuer Worker-Process. OK.
    • server.py- oder _knowledge.py-Edit → kein automatischer Reload. Plus: das MCP-Subprocess wird zwar bei jedem neuen Job neu gespawned, aber die MCP-Tool-Liste ist pro AgentSession gecached (livekit-agents MCPToolset.setup() faehrt einmal beim Session-Start). Wenn neue Tools dazukommen, sieht eine laufende Session sie nicht. Konsequenz: Nach jedem server.py-/_knowledge.py-Edit Ctrl-C + erneut python agent.py dev (~5 s).
    • Knowledge-Markdown-Edit (menu.md, hours.md, policies.md, faq.md) → sofort wirksam beim naechsten Tool-Call, kein Reload. Tool laedt pro Call frisch (kein Cache).
  • Knowledge-File-Updates ohne Restart: Tool liest pro Call frisch. Aenderungen in menu.md sind sofort wirksam, kein Reload noetig.
  • DB-Schema-Freeze bleibt: keine neuen Tabellen, keine ADD COLUMN. Master-Plan-Operational-Note-konform.
  • Eval-Hook: Unit 6 produziert eine Walkthrough-Datei die direkt fuer Master-Plan Unit 13 (Demo-Pack) wiederverwendbar ist. Plan-Verkettung: Substanz-Iter → Demo-Pack → Reference-Case-Asset-Done.

Sources & References