L16 Lotse-Chat für Monteure: Datenmodell, Provider mit Tool-Schleife, Vorschläge und Bestätigen über bestehende Services

Chatverlauf (LotseConversation/-Message) und Aktionskarten (LotseActionProposal) als
Mandantentabellen mit RLS; Schalter lotseChatEnabled. Tool-Use-Schleife mit Runden- und
Tokengrenze, Datenminimierung per Platzhalter, Auftragszuordnung im Sichtbarkeits-Scope,
Bestätigen/Verwerfen/Alle bestätigen mit Hash, Ablauf und Idempotenz, Transkriptions-API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 18:57:20 +02:00
co-authored by Claude Opus 5
parent 6b8cdf543b
commit 68f4eb32dc
26 changed files with 2363 additions and 6 deletions
+84
View File
@@ -79,6 +79,8 @@ model TenantSettings {
lotseAddressForm String? @map("lotse_address_form")
// L10b (Spec §31): monthly AI token budget (input + output) per tenant; null = env AI_MONTHLY_TOKEN_LIMIT, 0 = unlimited
aiMonthlyTokenLimit Int? @map("ai_monthly_token_limit")
// L16 Lotse-Chat für Monteure: switch per tenant (effective only while the Lotse module is on)
lotseChatEnabled Boolean @default(true) @map("lotse_chat_enabled")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -1433,3 +1435,85 @@ model BillingRecord {
@@index([tenantId, workOrderId])
@@map("billing_records")
}
// ---------- L16 Lotse-Chat für Monteure ----------
enum LotseMessageRole {
user
assistant
tool
}
enum LotseProposalStatus {
proposed
confirmed
discarded
expired
failed
}
/// Chat of ONE user with the Lotse (only visible to that user). Optional work order context
/// (chat opened from the order detail). Deleted by the Lotse retention (services/lotse/retention.ts).
model LotseConversation {
id String @id @default(cuid())
tenantId String @map("tenant_id")
userId String @map("user_id")
workOrderId String? @map("work_order_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
lastMessageAt DateTime @default(now()) @map("last_message_at")
messages LotseMessage[]
proposals LotseActionProposal[]
@@index([tenantId, userId, lastMessageAt])
@@index([tenantId, lastMessageAt])
@@map("lotse_conversations")
}
/// One chat message. `text` = what the user sees; `content` = structured parts (chips, proposal ids,
/// links, placeholder text sent to the model).
model LotseMessage {
id String @id @default(cuid())
tenantId String @map("tenant_id")
conversationId String @map("conversation_id")
role LotseMessageRole
text String
content Json?
aiGenerationId String? @map("ai_generation_id")
createdAt DateTime @default(now()) @map("created_at")
conversation LotseConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
@@index([tenantId, conversationId, createdAt])
@@map("lotse_messages")
}
/// Action card proposed by the Lotse. Nothing is executed before the owner confirms it
/// (services/lotse/chat/confirm.ts runs the existing services with the owner's context).
model LotseActionProposal {
id String @id @default(cuid())
tenantId String @map("tenant_id")
conversationId String @map("conversation_id")
messageId String? @map("message_id")
userId String @map("user_id")
workOrderId String? @map("work_order_id")
kind String
payload Json
payloadHash String @map("payload_hash")
status LotseProposalStatus @default(proposed)
expiresAt DateTime @map("expires_at")
result Json?
errorCode String? @map("error_code")
confirmedAt DateTime? @map("confirmed_at")
confirmedById String? @map("confirmed_by_id")
decidedAt DateTime? @map("decided_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
conversation LotseConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
@@index([tenantId, conversationId, createdAt])
@@index([tenantId, userId, status])
@@map("lotse_action_proposals")
}