Files
craftvia/docs/craftvia/lanes/lotse-chat.md
T

18 KiB
Raw Blame History

Lane L16 – Lotse-Chat für Monteure (lane/lotse-chat)

Stand: 2026-09-15 · Basis 6b8cdf5 (feature/craftvia-mvp, L1–L14 integriert) · Spec §15, §27, §31 · Brandbook §4.3, §9, §11.4, §12 · ARCHITEKTUR §4.5, §4.8

Ziel: In der mobilen Web-App schreibt oder spricht der Monteur wie in einem Chat („Auftrag fertig, Speicher installiert, 2 Std., 1 Filter verbaut“). Der Lotse ordnet den Auftrag zu, fragt fehlende Angaben nach und schlägt konkrete Aktionen vor. Er führt nichts selbst aus – jede Änderung braucht den Tipp des Monteurs auf „Bestätigen“.

1. Umfang / erfüllte Auftragspunkte

Auftrag Umsetzung
Datenmodell LotseConversation (Mandant, Nutzer, optionaler Auftragskontext, lastMessageAt), LotseMessage (Rolle user/assistant/tool, Text, content JSON mit Chips, Karten-IDs, Sprungzielen, Modell-Fassung), LotseActionProposal (Art, Nutzlast, HMAC-payloadHash, Status proposed/confirmed/discarded/expired/failed, expiresAt = +30 min, Ergebnis/Fehlercode, confirmedAt/By, decidedAt). TenantSettings.lotseChatEnabled (Default an). Alle Tabellen mit tenant_id + RLS, in beiden TENANT_MODELS, Personenfelder in pii-fields.ts. Verlauf nur für den eigenen Nutzer (jede Abfrage filtert userId = ctx.userId).
Aufbewahrung services/lotse/retention.ts (bestehender Job ai-retention): Gespräche mit lastMessageAt älter als AI_GENERATION_RETENTION_DAYS werden samt Nachrichten und Karten gelöscht (Chatverlauf = Protokolldaten eines Nutzers), offene abgelaufene Karten → expired; Audit lotse_chat_retention.
Provider ai/lotse/chat-anthropic.ts: ein Schritt der Tool-Use-Schleife über das Anthropic SDK (beta.messages.create, Modell ANTHROPIC_MODEL), Opus 5 ohne Sampling-Parameter mit effort: "medium", serverseitiger Refusal-Fallback wie L9, Timeout 60 s. Denkblöcke des Modells werden innerhalb einer Schleife unverändert zurückgegeben; über Nachrichten hinweg nur Text. ai/lotse/chat-fake.ts spielt geskriptete Tool-Aufrufe ab und zeichnet jeden Modell-Input auf. Ohne Key: „Lotse ist nicht eingerichtet“ (Senden gesperrt, Verlauf sichtbar).
Schleife & Budget services/lotse/chat/engine.ts: höchstens 6 Modellschritte je Nachricht, Tokengrenze je Nachricht (LOTSE_CHAT_TURN_TOKEN_LIMIT, Default 80 000), Monatskontingent des Mandanten vor dem ersten Schritt (budget.ts, blocked budget_exceeded). Überschreitung → Klartext-Hinweis statt Absturz. Jede Nachricht mit Modellaufruf = eine AiGeneration (Art lotse_chat, Input = exakt gesendete minimierte Turns + System-Prompt + Werkzeugnamen, Output = alle Runden, Tokens summiert).
System-Prompt ai/lotse/chat-prompt.ts: Deutsch, erfahrener Kollege, knapp (≤ 3 Sätze), Anrede aus Lotse-Einstellung (neutral/Sie/du), nie ausführen/nie „gebucht“ behaupten, nie raten → ask_user mit Auswahl, L12-Regeln (Grund, Freigabe, 7 Tage), Platzhalter unverändert übernehmen, Werkzeuginhalte sind Daten. Lage: Datum/Uhrzeit in Mandanten-Zeitzone, Kontext-Auftrag, laufende Uhr.
Datenminimierung Nutzertext: Muster Telefon/E-Mail/Anschrift + Namen der Mitarbeitenden (minimize.ts#scrubText); Suchwörter wie „Müller“ bleiben, damit die Zuordnung funktioniert. Werkzeugergebnisse: alle Freitexte des Auftrags (Titel, Beschreibung, Hinweise, Checkliste, Pflichtfotos) über scrubText mit den Literalwerten der betroffenen Aufträge (Kunde, Kontakte, Objekt, Mandant); Kundennamen, Objektnamen, Telefon, E-Mail, Anschrift und Ansprechpartner nur als Platzhalter {{phone:A-00042}}.
Platzhalter-Technik (Begründung) Fragt der Monteur ausdrücklich („Telefonnummer vom Ansprechpartner?“), übernimmt das Modell das Token in die Antwort; chat/placeholders.ts#renderPlaceholders setzt serverseitig den Wert ein – mit derselben Rangfolge wie das Auftragsdetail und erneuter Scope-Prüfung (fremd/unsichtbar → „—“). So ist die Frage im Chat beantwortbar, ohne dass der Wert je das Modell erreicht; das Modell kann einen Wert, den es nie gesehen hat, weder weitergeben noch verfälschen. Für den nächsten Turn wird die Token-Fassung (content.modelText) gesendet.
Lese-Werkzeuge list_my_orders (heute + bis 7 Tage, laufende), get_order_details (Status, Zeitfenster, Beschreibung, Hinweise, Objekt-Hinweise inkl. Zugang, Checkliste, Plan-/Zusatzmaterial, Pflichtfotos, eigene Uhr, verfügbare Platzhalter), get_running_clock, check_completeness (L9 completeness.ts, Sprungziele), search_material (Planpositionen + bisher verwendete Bezeichnungen im Scope), ask_user (Rückfrage mit Chips, beendet die Schleife). Alles mit Monteur-ctx im Scope von visibility.ts.
Vorschlags-Werkzeuge transition_work_order (annehmen, Anfahrt, Arbeit starten, Pause, fortsetzen, technisch abschließen; Abschluss prüft Blocker vorab → keine Karte, Sprungziele), book_time (work/travel/return_travel/material_procurement, Dauer bis jetzt oder von–bis, Datum; Grund Pflicht; 7-Tage-/Zukunft-/16-h-/Überschneidungsprüfung vorab), record_material (Planposition per Name, Status aus Planmenge, Abweichung/Zusatzmaterial mit Grund; validateMaterialUsage von L4), add_note (Art + Text), suggest_report_fields (nur mit offenem Bericht). Fehlende Pflichtwerte → missing_values, keine Karte.
Auftragszuordnung chat/orders.ts#resolveOrder: Suchbegriff (Nummer auch als Ziffern, Kunde, Objekt, Ort, Titel) > Kontext-Auftrag > Auftrag der laufenden Uhr > einziger heutiger Auftrag; mehrdeutig → order_ambiguous + Auswahl-Chips (Beschriftung mit Kunde nur für den Monteur, Wert „Auftrag A-00042“).
Bestätigen chat/confirm.ts: Eigentümer (sonst not_found), Status proposed, Ablauf (→ expired), HMAC-Hash (→ failed tampered + Audit denied), Bearbeiten nur für freigegebene Felder (Auftrag/IDs fix, Hash neu). Ein atomares Row-Update beansprucht die Karte (Doppeltipp/parallel → idempotent), danach Ausführung über die bestehenden Services mit Monteur-ctx: transitionWorkOrder, startSession/pauseSession/resumeSession/endSession (inkl. L12-Auto-Wechsel, auf der Karte angekündigt), addManualTimeEntry (eigene Zeit → pending, Freigabepflicht), upsertMaterialUsage, createNote, Berichtsvorschlag in content.lotse (L9-Mechanismus: im Bericht übernehmen/verwerfen, Prüfbestätigung beim Absenden). Service-Fehler → Karte failed mit Klartext-Code + Lotse-Hinweis mit Sprungzielen (Pflichtfoto, Checkliste, Unterschrift, Meine Zeiten, Material, Bericht). Audit lotse_action_proposal before/after mit source: "lotse_chat". „Alle bestätigen“: feste Reihenfolge Zeit → Material → Notiz → Bericht → Status, Stopp beim ersten Fehler mit Hinweis „N Karten noch offen“.
Keine äußere Transaktion (Entscheidung) Die Services öffnen eigene Transaktionen und senden Benachrichtigungen (Mail). Eine umschließende Transaktion hätte die DB-Transaktion über den Mailversand offen gehalten (5-s-Timeout, im Gate beobachtet). Deshalb: Karte atomar beanspruchen, Abschluss-Blocker vor jeder Änderung prüfen, dann Services einzeln.
UI mobil /m/lotse (Verlauf, Eingabe, Senden, „Neuer Chat“, Mikrofon-Taste: Aufnahme ≤ 2 min → POST /api/v1/lotse/transcribe → Transkript im Eingabefeld editierbar; ohne Transkriptionsanbieter deaktiviert mit Hinweis). Aktionskarten mit klaren Werten (z. B. „Arbeitszeit · Auftrag A-00042 · Di., 15.09. · 13:05–15:05 Uhr · 2:00 Std. · Grund … · Zählt nach Freigabe“), Bestätigen/Bearbeiten/Verwerfen, Status als Text + Icon, „Gültig bis“. Chips nur an der letzten Antwort, Sprungziele als Liste. Offline: Hinweis, Eingabe bleibt (auch localStorage), nichts wird gesendet/ausgeführt. Einstieg: Bottom-Navigation „Lotse“ (nur wenn nutzbar) und „Lotse fragen“ im Auftragsdetail (Kontext). Touch ≥ 48 px, Farben nur Tokens, Lotse-Mark in Signalorange, Signalorange sonst nur an Primärknöpfen/offenen Karten.
Einstellungen /settings/lotse: Schalter „Lotse-Chat für Monteure“ (wirkt nur bei eingeschaltetem Lotse), Datenfluss um Chat und Spracheingabe ergänzt, KI-Protokoll zeigt Art „Lotse-Chat“ (Transkriptionen der Spracheingabe: Art transcription, entityType lotse_chat, ohne Inhalt).

2. Routen / Screens

Route Rolle Inhalt
/m/lotse Monteur, Teamleiter (lotse:use + field:execute) Chat ohne Auftragsbezug; ausgeschaltet → Hinweisseite
/m/lotse?order=<id> dito Chat mit Auftragskontext; fremder/unsichtbarer Auftrag → 404
/m/orders/[id] dito Button „Lotse fragen“ unter der Vollständigkeits-Karte
Bottom-Navigation dito Eintrag „Lotse“ (6 Einträge, nur wenn Chat nutzbar)
/settings/lotse Mandantenadministrator Schalter + Datenfluss
POST /api/v1/lotse/transcribe dito multipart file → { text } (Audio wird nicht gespeichert), OpenAPI ergänzt

Server Actions (actions/lotse/chat.ts, moduleGuard("lotse") + lotse:use, field:execute): senden, bestätigen (optional mit Änderungen), verwerfen, alle bestätigen, neuer Chat – jeweils mit frischer Chat-Ansicht als Ergebnis.

3. Dateien

Neu (Ownership L16 / Lotse-Pfade)

  • prisma/migrations/20260916200000_lotse_chat/migration.sql
  • src/lib/lotse/chat.ts (client-safe: Kartenarten, Zod-Schemas, Ansichtstypen)
  • src/server/ai/lotse/{chat-types,chat-prompt,chat-fake,chat-anthropic}.ts
  • src/server/services/lotse/chat/{access,orders,placeholders,proposals,tools,engine,conversations,confirm,transcribe}.ts
  • src/server/actions/lotse/{chat,_chat-state}.ts, src/app/api/v1/lotse/transcribe/route.ts
  • src/app/(field)/m/(core)/lotse/{layout,page}.tsx
  • src/components/lotse/chat/{lotse-chat,proposal-card,mic-button,ask-button}.tsx
  • scripts/test-lotse-chat-engine.ts, scripts/test-lotse-chat-confirm.ts, scripts/test-lotse-chat-live.ts, scripts/lib/lotse-chat-fixture.ts

Geändert in Lotse-Pfaden (erlaubt laut Auftrag)

  • src/server/services/lotse/{settings,retention}.ts, src/server/actions/lotse-settings.ts, src/app/(app)/settings/lotse/page.tsx, messages/{de,en}/lotse.json

Fremdeingriffe (je klein, markiert „L16“)

  • prisma/schema.prisma (3 Modelle, 2 Enums, 1 Spalte) · src/server/db.ts + src/server/backup/topology.ts (TENANT_MODELS) · src/server/dsgvo/pii-fields.ts (3 Felder)
  • src/components/field/bottom-nav.tsx (Eintrag + Prop lotse), src/app/(field)/m/layout.tsx (Flag lotseChat), messages/{de,en}/field.json (nav.lotse)
  • src/app/(field)/m/(core)/orders/[id]/page.tsx (Import + 1 Zeile Button)
  • src/lib/api/openapi.ts (Pfad /lotse/transcribe – test-betrieb-api verlangt jede Route im Dokument)
  • src/components/audit-trail.tsx (Entity-Labels)
  • scripts/test-e2e-tenant-isolation.ts (je eine Zeile für die 3 neuen Tenant-Modelle – der Test verlangt vollständige Abdeckung aller TENANT_MODELS)
  • scripts/smoke-auth.ts (Monteur-/Admin-Prüfungen)

Keine Änderung an rbac.ts (bestehende Rechte lotse:use + field:execute + field:record_own_time/report:write genügen), keine neuen npm-Abhängigkeiten, keine Stubs.

4. Schema / Migration

20260916200000_lotse_chat (additiv): Enums LotseMessageRole, LotseProposalStatus; Tabellen lotse_conversations, lotse_messages, lotse_action_proposals (Kaskade von Gespräch zu Nachrichten/Karten), tenant_settings.lotse_chat_enabled BOOLEAN DEFAULT true; SELECT enable_tenant_rls(...) für alle drei Tabellen. Begründung: Chatverlauf und bestätigungspflichtige Vorschläge brauchen persistente, nutzerbezogene Datensätze mit Ablauf, Ergebnis und Audit-Bezug – weder AiGeneration (Protokoll, wird pseudonymisiert) noch Report-content passen. workOrderId ohne Fremdschlüssel (weiche Referenz, Auftrag kann soft-gelöscht werden; Sichtbarkeit wird bei jedem Lesen neu geprüft). Deploy: prisma migrate deploy (keine Rechte-Synchronisation nötig).

5. Tests

Skript Prüfungen Ergebnis
test-lotse-chat-engine.ts 75: System-Prompt, Suchwörter; Zuordnung Kontext > laufende Uhr, Suchbegriff Kunde/Objekt/Nummer, Mehrdeutigkeit → Chips (Beschriftung mit Kunde, Wert ohne Namen), ask_user beendet Schleife; vier Karten aus einer Nachricht ohne jede Änderung an Zeiten/Material/Notizen/Status; fehlende Pflichtwerte (Grund, Dauer, Zusatzmaterial-/Mindermengen-Grund) → keine Karte; Überschneidung mit laufender Uhr, Pflichtfoto-Blocker mit Sprungziel, kein offener Bericht → keine Karte; Datenminimierung: Modell-Input ohne Telefon/E-Mail/Anschrift/PLZ/Kunden-, Kontakt-, Firmen-, Mitarbeiternamen/Mandantenkontakt, Platzhalter-Token vorhanden, fachliche Hinweise erhalten, Wert serverseitig eingesetzt, AiGeneration minimiert; Scope (Monteur ohne Zuweisung → not_found / Auftrag nicht gefunden), fremder Verlauf im selben Mandanten und aus Mandant B → not_found, B findet A-Aufträge nicht, ohne lotse:use → forbidden; Chat-Schalter aus / Modul aus → blocked, kein Modellaufruf, Audit; ohne Anbieter → not_configured; Monatskontingent → budget_exceeded; Rundengrenze; Tokengrenze; Anbieterfehler → Hinweis; Protokoll zeigt lotse_chat; Aufbewahrung löscht Verläufe nur des Mandanten grün
test-lotse-chat-confirm.ts 52: Annehmen/Arbeit starten über Services (Statuswechsel, Session, Akteur), Doppeltipp idempotent, Audit mit Quelle; Zeitnachtrag pending mit Grund, Bearbeiten (nur erlaubte Felder, Hash aktualisiert, Audit edited), Überschneidung → failed overlap; Material, Notiz (paralleler Doppeltipp → genau eine Notiz); Abschluss mit fehlendem Pflichtfoto → failed blocked, Auftrag + Session unverändert, Sprungziel; erneutes Bestätigen → already_decided; Ablauf nach 30 min; fremder Monteur/Admin/Mandant B → not_found; manipulierte Nutzlast → tampered; Verwerfen (idempotent, danach nicht bestätigbar); Monteur ohne Zuweisung → Service verweigert; „Alle bestätigen“ Reihenfolge + Stopp + zweiter Lauf; Berichtsvorschlag in content.lotse und Übernahme per L9; Chat aus / Modul aus → blocked grün
test-lotse-chat-live.ts optional gegen Claude, nur mit ANTHROPIC_API_KEY übersprungen (kein Key)

Angepasst: test-e2e-tenant-isolation.ts (Fixture um die 3 Tenant-Modelle ergänzt) – grün.

Gate: npm run gate grün – prisma generate, tsc, lint (0 Fehler, 3 bestehende Warnungen außerhalb L16), build inkl. Modul-Guard-Check (37 Action-Dateien), 79/79 Testskripte. Im ersten Lauf schlugen test-e2e-tenant-isolation (fehlende Fixture-Zeilen, behoben) und test-einsatz-field fehl (Transaktions-Timeout 5 s beim Mailversand innerhalb einer L12-Session-Transaktion unter Last paralleler Lanes; einzeln grün, im zweiten Gate grün – bestehendes Verhalten, siehe L12-Bericht §5.5).

RLS-Lauf RLS_ENFORCED=true (Lane-DB craftvia_lotsechat, Rolle craftvia_app): test-lotse-chat-engine, test-lotse-chat-confirm, test-e2e-tenant-isolation, test-tenant-isolation, test-rls-enforcement – 5/5 grün.

HTTP-Smoke (npx next start -p 3116, scripts/smoke-auth.ts, Demo-Seed): Monteur 37/37 grün (neu: /m/lotse mit „Lotse-Chat“, „Neuer Chat“, „Sprechen“; /m/lotse?order=<DEMO-07> mit Auftragsnummer; Auftragsdetail mit „Lotse fragen“; /m/lotse?order=<fremder Auftrag DEMO-08> → 404), Admin 12/12 grün (/settings/lotse mit „Lotse-Chat für Monteure“, Protokoll). Server danach beendet.

Visuelle Browserprüfung nicht durchgeführt (Anmeldung hätte Passwort- oder Token-Eingabe durch den Agenten erfordert).

6. Stubs & Abhängigkeiten

Keine Stubs. Genutzt: L2 transitionWorkOrder/computeCompletionBlockers/workOrderScope, L4 requireFieldOrder/createNote/upsertMaterialUsage/validateMaterialUsage/sniffMime, L12 startSession/pauseSession/resumeSession/endSession/getMyActiveSession/addManualTimeEntry/earliestStart, L5 requireVisibleReport/contentOf, L9 checkCompleteness/scrubText/lotseVoice/isLotseEnabled/Transkriptionsanbieter, L10b assertTokenBudget/requireApiContext/Aufbewahrungsjob.

7. Bekannte Lücken / Hinweise an den Architekten

  1. Live-Verhalten mit Claude ungeprüft (kein Key): Tool-Definitionen, Prompt und Denkblock-Rückgabe sind typgeprüft und gegen den Fake getestet; scripts/test-lotse-chat-live.ts läuft, sobald ein Key gesetzt ist. Prompt-Feinschliff (Tonalität, Rückfragen) nach ersten echten Gesprächen empfohlen.
  2. Kein /api/v1 für Senden/Bestätigen – die mobile UI nutzt Server Actions (wie L9); nur die Transkription ist eine API-Route (Upload > Server-Action-Limit). Für native Clients ggf. nachziehen.
  3. Doppeltipp während der Ausführung: Der zweite Tipp erhält sofort confirmed (idempotent), auch wenn die Ausführung danach noch scheitert; die Karte zeigt nach dem Neuladen failed.
  4. Abschluss nicht vollständig atomar: Blocker werden vorab geprüft; scheitert der Statuswechsel danach aus anderem Grund (z. B. parallele Änderung), bleibt die eigene Uhr beendet – wie beim bestehenden Zwei-Schritt-Flow der Primäraktion.
  5. Audit der Fachentitäten ohne Quelle: Die Services schreiben ihre Audits unverändert; die Verbindung zur Chat-Karte steht im Audit lotse_action_proposal (Ergebnis-IDs, source: "lotse_chat"). Ein source-Feld im Fundament-Audit wäre sauberer.
  6. Gespeicherte Antworten enthalten eingesetzte Werte (z. B. Telefonnummer), sichtbar nur für den Nutzer, gelöscht mit der Aufbewahrung. Das Modell sieht sie nie.
  7. Namen im Freitext des Monteurs werden (außer Mitarbeitenden) nicht ersetzt, damit die Suche „Objekt Müller“ funktioniert; Telefon/E-Mail/Anschrift schon.
  8. Zeit „2 Std.“ über Mitternacht → Rückfrage nach der Uhrzeit (kein Raten des Vortags).
  9. Verlauf ans Modell: letzte 20 Nachrichten als Text + Kartenstatus; Werkzeugergebnisse früherer Nachrichten werden nicht erneut gesendet (Kosten, Datenminimierung).
  10. Bottom-Navigation mit 6 Einträgen (~62 px je Eintrag bei 375 px) – bei weiteren Einträgen Profil/Sync zusammenlegen.
  11. Offline gibt es bewusst keine Warteschlange für Chat-Nachrichten (Eingabe bleibt lokal erhalten).