feat: Zod AI validation, AIProvenance governance, fix tests (154 green)
- Add AIProvenance + AIResponse<T> to IAIService — all 11 methods now return structured provenance (provider, model, source, fallbackUsed, validationPassed) instead of bare ItemResponse<T> - Add schemas.ts with Zod schemas for all 8 AI response types; validateAIResponse() utility returns null on failure, never throws - Rewrite OpenRouterAIService: every method validates AI JSON against its Zod schema; failed validation triggers MockAIService fallback with fallbackUsed:true — no invalid data can reach the UI - Fix MockAIService.generateFollowUpQuestions: replace broken mockParseNeed(JSON.stringify(criteria)) with direct ParsedNeedCriteria field inspection; returns max 3 prioritised FollowUpQuestion objects - Add provenance: mockProvenance() to all MockAIService responses - Improve decisionBriefPrompt: structured JSON schema example, confidence vocabulary, availability disclaimer - Improve matchExplanationPrompt: score-tier vocabulary, isFutureSignal flag forbids confirmed-availability language for future signals - Add 102 new tests: mustHaveScorer (16), softFactorEnrichment (38), aiSchemas (52) — 154 total, all passing; 0 TypeScript errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,42 @@ export interface DecisionBriefPromptInput {
|
||||
}
|
||||
|
||||
export function buildDecisionBriefPrompt(input: DecisionBriefPromptInput): { system: string; user: string } {
|
||||
const itemList = input.shortlistItems
|
||||
.map(i => `- ${i.title} (${i.city}): Score ${i.matchScore}%, ${i.areaSqm}m², CHF ${i.rentPerSqm}/m² — ${i.topReasons.join(', ')}`)
|
||||
.join('\n')
|
||||
const itemList = input.shortlistItems.length > 0
|
||||
? input.shortlistItems
|
||||
.map(i => `- ${i.title} (${i.city}): Score ${i.matchScore}%, ${i.areaSqm}m², CHF ${i.rentPerSqm}/m² — ${i.topReasons.join(', ')}`)
|
||||
.join('\n')
|
||||
: '(keine Objekte auf der Shortlist)'
|
||||
|
||||
return {
|
||||
system: `Du bist Senior Real Estate Advisor. Erstelle ein strukturiertes Entscheidungs-Briefing auf Deutsch als JSON mit: summary, sections (Zusammenfassung, Standortbewertung, Budgetanalyse, Empfohlene nächste Schritte).`,
|
||||
user: `Erstelle ein Entscheidungs-Briefing für folgende Shortlist:\n\nSuchprofil: ${input.needSummary}\n\nObjekte:\n${itemList}`,
|
||||
}
|
||||
const system = `Du bist Senior Real Estate Advisor bei Wincasa AG. Du erstellst strukturierte Entscheidungs-Briefings für Unternehmenskunden auf Deutsch.
|
||||
|
||||
WICHTIG — Konfidenzregeln:
|
||||
- Schreibe nur, was durch die Daten belegt ist. Verwende "scheint", "deutet darauf hin", "laut Datenlage" wenn du Einschätzungen machst.
|
||||
- Stelle keine Verfügbarkeit als gesichert dar, wenn sie nicht explizit bestätigt ist.
|
||||
- Unterscheide zwischen Stärken ("erfüllt vollständig") und Hinweisen ("Tendenz erkennbar").
|
||||
|
||||
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown, keine Erklärungen):
|
||||
{
|
||||
"summary": "1–2 Sätze Executive Summary",
|
||||
"sections": [
|
||||
{ "title": "Zusammenfassung", "body": "Überblick über die Shortlist und Gesamtbewertung" },
|
||||
{ "title": "Standortbewertung", "body": "Vergleich der Standorte nach Erreichbarkeit, Prestige, Eignung" },
|
||||
{ "title": "Budgetanalyse", "body": "Kostenvergleich und Budget-Effizienz der Objekte" },
|
||||
{ "title": "Empfohlene nächste Schritte", "body": "Konkrete, priorisierte Handlungsempfehlungen" }
|
||||
]
|
||||
}
|
||||
|
||||
Regeln:
|
||||
- Genau 4 Sektionen, Reihenfolge wie oben
|
||||
- summary: max 2 Sätze
|
||||
- body jeder Sektion: 2–4 Sätze, entscheidungsorientiert
|
||||
- Kein JSON in Markdown-Codeblöcken`
|
||||
|
||||
const user = `Erstelle ein Entscheidungs-Briefing für folgende Shortlist:
|
||||
|
||||
Suchprofil: ${input.needSummary}
|
||||
|
||||
Objekte:
|
||||
${itemList}`
|
||||
|
||||
return { system, user }
|
||||
}
|
||||
|
||||
@@ -5,15 +5,32 @@ export interface MatchExplanationPromptInput {
|
||||
positiveFactors: Array<{ criterion: string; explanation: string }>
|
||||
negativeFactors: Array<{ criterion: string; explanation: string }>
|
||||
needSummary: string
|
||||
isFutureSignal?: boolean
|
||||
}
|
||||
|
||||
export function buildMatchExplanationPrompt(input: MatchExplanationPromptInput): { system: string; user: string } {
|
||||
return {
|
||||
system: `Du bist ein Experte für Schweizer Gewerbeimmobilien. Erkläre Match-Ergebnisse präzise und entscheidungsorientiert auf Deutsch. Maximal 3 Sätze.`,
|
||||
user: `Erkläre warum das Objekt "${input.propertyTitle}" in ${input.propertyCity} einen Match Score von ${input.matchScore}% hat.
|
||||
const confidenceNote = input.isFutureSignal
|
||||
? '\nDieses Objekt ist ein Zukunftssignal (noch nicht verfügbar). Stelle die Verfügbarkeit NICHT als gesichert dar. Verwende Formulierungen wie "könnte verfügbar werden", "Signal deutet auf mögliche Fläche hin".'
|
||||
: ''
|
||||
|
||||
Stärken: ${input.positiveFactors.map(f => f.explanation).join(', ')}
|
||||
Schwächen: ${input.negativeFactors.map(f => f.explanation).join(', ')}
|
||||
Suchprofil: ${input.needSummary}`,
|
||||
}
|
||||
const system = `Du bist Experte für Schweizer Gewerbeimmobilien bei Wincasa AG. Du erklärst Match-Ergebnisse präzise und entscheidungsorientiert auf Deutsch.
|
||||
|
||||
Konfidenz-Vokabular:
|
||||
- Score ≥ 78: "starkes Match", "erfüllt die Kernkriterien", "klar empfehlenswert"
|
||||
- Score 52–77: "gutes Match mit Kompromissen", "weitgehend geeignet", "einzelne Einschränkungen"
|
||||
- Score < 52: "schwaches Match", "deutliche Abweichungen", "kritische Lücken"
|
||||
|
||||
Regeln:
|
||||
- Maximal 3 Sätze
|
||||
- Nenne die 1–2 stärksten Gründe für den Score
|
||||
- Erwähne die grösste Einschränkung, falls vorhanden
|
||||
- Keine allgemeinen Floskeln ("ein attraktives Objekt") — nur konkrete Fakten aus den Score-Faktoren${confidenceNote}`
|
||||
|
||||
const user = `Erkläre warum das Objekt "${input.propertyTitle}" in ${input.propertyCity} einen Match-Score von ${input.matchScore}/100 hat.
|
||||
|
||||
Stärken: ${input.positiveFactors.map(f => f.explanation).join('; ')}
|
||||
Schwächen: ${input.negativeFactors.map(f => f.explanation).join('; ')}
|
||||
Suchprofil: ${input.needSummary}`
|
||||
|
||||
return { system, user }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user