feat(ai): observability tracing + improved prompt templates

- Add AITrace type, AITraceStore (circular buffer, localStorage in DEV,
  window.__aiTraces for DevTools), provenanceToStatus() helper
- Instrument OpenRouterAIService withFallback with latency tracking and
  trace recording across all three paths (no-key, success, error)
- Wrap all MockAIService methods with traceMock for consistent in-memory
  tracing including method name, latency, and validation status
- Improve all 6 prompt templates with ROLLE/AUFGABE/VERBOTE/BEISPIEL
  structure; marketSignalPrompt carries hard prohibition against claiming
  confirmed availability from unconfirmed signals

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 14:18:42 +02:00
parent 7934da7669
commit d4171fe9b5
9 changed files with 640 additions and 170 deletions
@@ -15,7 +15,37 @@ export function buildCompareSummaryPrompt(input: CompareSummaryPromptInput): { s
.join('\n')
return {
system: `Du bist Entscheidungsassistent für Gewerbeimmobilien-Mieter. Erstelle eine präzise Vergleichsanalyse auf Deutsch als valides JSON mit den Feldern: overallAssessment, strongestOption, recommendation.`,
system: `ROLLE: Du bist Entscheidungsassistent für Gewerbeimmobilien-Mieter in der Schweiz. Du analysierst mehrere Objekte und gibst eine klare, begründete Empfehlung.
AUFGABE: Vergleiche die übergebenen Objekte. Identifiziere das stärkste Objekt und erkläre kurz warum — basierend auf Match-Score, Kosten und den genannten Faktoren.
KONFIDENZ-REGELN:
- Klare Empfehlung: nur wenn ein Objekt in Score UND Kosten-/Risikoprofil dominiert
- Enger Unterschied (< 5 Punkte Score-Differenz oder < 10 CHF/m²): "Beide Optionen sind vergleichbar — Besichtigung empfohlen"
- overallAssessment: Vergleich in maximal 3 Sätzen
- recommendation: bevorzugtes Objekt benennen ODER erklären, warum keine eindeutige Empfehlung möglich ist
VERBOTE — NIEMALS:
- Eine Option als "definitiv besser" bezeichnen, wenn Scores oder Daten ähnlich sind
- Objektinformationen erfinden (Fläche, Preis, Lage), die nicht im Input stehen
- Mehr als 3 Sätze pro Ausgabefeld verwenden
- Stärken eines Objekts übertreiben, nur um eine Empfehlung zu konstruieren
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block, keine Erklärungen):
{
"overallAssessment": "...",
"strongestOption": "...",
"recommendation": "..."
}
BEISPIEL:
Input: Option 1 — Score 82%, CHF 45/m², Stärken: zentrale Lage, Ausbaustandard. Option 2 — Score 67%, CHF 38/m², Stärken: Kostenvorteil, Risiken: Randlage, schlechte ÖV-Anbindung.
Ausgabe:
{
"overallAssessment": "Option 1 erzielt einen deutlich höheren Match-Score (82% vs. 67%) und erfüllt die Kernkriterien besser. Option 2 bietet einen Kostenvorteil von CHF 7/m², weist jedoch Abstriche bei Lage und Anbindung auf.",
"strongestOption": "Option 1",
"recommendation": "Option 1 empfohlen — der Score-Vorsprung von 15 Punkten überwiegt den Kostenvorteil von Option 2. Bei engem Budget lohnt sich die Besichtigung beider Objekte."
}`,
user: `Vergleiche folgende Objekte und gib eine strukturierte Empfehlung:\n\n${propertyList}`,
}
}
+29 -4
View File
@@ -7,17 +7,42 @@ export function buildDataQualityPrompt(propertyId: string, quality: DataQualityI
const warningList = quality.warnings.join(', ') || 'keine'
return {
system: `Du bist Datenqualitäts-Experte für Schweizer Gewerbeimmobilien-Daten. Erstelle eine klare, handlungsorientierte Qualitätsbewertung auf Deutsch.
system: `ROLLE: Du bist Datenqualitäts-Experte für Schweizer Gewerbeimmobilien-Daten. Du bewertest Datenvollständigkeit und -aktualität und gibst handlungsorientierte Empfehlungen.
Antworte als valides JSON:
AUFGABE: Erstelle eine klare, handlungsorientierte Qualitätsbewertung basierend auf dem übergebenen Score, den fehlenden Feldern und dem Freshness-Status.
QUALITÄTSSTUFEN (basierend auf Score):
- ≥ 85%: Exzellent — alle Kernfelder vollständig und aktuell
- 7084%: Gut — kleinere Lücken, Match-Genauigkeit nicht wesentlich beeinträchtigt
- 5569%: Ausreichend — fehlende Felder können Match-Genauigkeit beeinträchtigen
- 4054%: Gering — wichtige Felder fehlen, Match-Score mit Vorsicht interpretieren
- < 40%: Kritisch — fundamentale Felder fehlen, Match-Ergebnis stark eingeschränkt
VERBOTE — NIEMALS:
- Den "confidence"-Wert anders setzen als exakt ${quality.score} (der übergebene Score)
- Objektinformationen erfinden, die nicht im Input stehen
- Fehlende Felder als vorhanden darstellen
- Mehr als 3 Sätze für "overallAssessment" verwenden
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block, keine Erklärungen):
{
"overallAssessment": "...",
"missingCriticalFields": ["..."],
"recommendation": "...",
"confidence": 0.0
"confidence": ${quality.score}
}
Die Confidence entspricht dem übergebenen Score (01). Halte die Bewertung unter 3 Sätzen.`,
PFLICHT: Der Wert "confidence" im JSON MUSS exakt ${quality.score} betragen — niemals davon abweichen.
BEISPIEL:
Input: Score 58%, Freshness STALE, fehlende Pflichtfelder: rentPerSqm, availableFrom
Ausgabe:
{
"overallAssessment": "Ausreichende Datenqualität (58%). Mietpreis und Verfügbarkeitsdatum fehlen, was die Treffergenauigkeit der Suche beeinträchtigt. Der Datensatz ist veraltet und sollte aktualisiert werden.",
"missingCriticalFields": ["rentPerSqm", "availableFrom"],
"recommendation": "Pflichtfelder ergänzen: Mietpreis (rentPerSqm) und Verfügbarkeitsdatum (availableFrom). Datensatz danach neu verifizieren.",
"confidence": 0.58
}`,
user: `Qualitätsbewertung für Objekt ${propertyId}:
- Score: ${scorePercent}%
- Freshness: ${quality.freshness}
@@ -12,9 +12,25 @@ export function buildFollowUpQuestionsPrompt(input: FollowUpQuestionsPromptInput
.join(', ')
return {
system: `Du bist ein Experte für Schweizer Gewerbeimmobilien-Suche. Generiere präzise Rückfragen auf Deutsch, um fehlende Suchkriterien zu ermitteln.
system: `ROLLE: Du bist ein erfahrener Berater für Schweizer Gewerbeimmobilien. Du formulierst präzise Rückfragen, um fehlende Suchkriterien vom Kunden zu erfragen.
Antworte als valides JSON-Array mit maximal 3 Einträgen, priorisiert nach Wichtigkeit:
AUFGABE: Generiere maximal 3 Rückfragen (priorisiert nach Wichtigkeit) für die fehlenden Felder. Formuliere jede Frage klar und direkt — kein Smalltalk, keine Wiederholung bekannter Informationen.
PRIORITÄTSREIHENFOLGE:
1. assetType (required) — ohne Nutzungstyp ist keine Suche möglich
2. areaRange (required) — ohne Fläche keine Filterung
3. preferredLocations (required) — ohne Ort keine Ergebnisse
4. budgetRange (recommended) — wichtig für Preisfilterung
5. timing (recommended) — wichtig für Verfügbarkeitsabgleich
6. mustHaveCriteria (optional) — Pflichtmerkmale (Parkplätze, Laderampe etc.)
VERBOTE — NIEMALS:
- Fragen zu bereits bekannten Kriterien stellen
- Mehr als 3 Fragen ausgeben
- Fragen erfinden, die nicht einem der 6 definierten Zielfelder entsprechen
- Doppelfragen stellen
AUSGABEFORMAT — antworte ausschliesslich als valides JSON-Array (kein Markdown-Block, keine Erklärungen):
[
{
"questionText": "...",
@@ -23,6 +39,34 @@ Antworte als valides JSON-Array mit maximal 3 Einträgen, priorisiert nach Wicht
"suggestedAnswerOptions": ["...", "..."],
"importance": "required|recommended|optional"
}
]
BEISPIEL:
Bekannte Kriterien: areaRange, preferredLocations
Fehlende Felder: assetType, budgetRange, timing
Ausgabe:
[
{
"questionText": "Welchen Nutzungstyp suchen Sie?",
"targetField": "assetType",
"reason": "Nutzungstyp ist zwingend für die Matchsuche",
"suggestedAnswerOptions": ["Büro", "Retail", "Logistik", "Produktion", "Gastro"],
"importance": "required"
},
{
"questionText": "Was ist Ihr maximales Budget pro m² und Monat (CHF)?",
"targetField": "budgetRange",
"reason": "Budget ist wichtig für die Filterung unpassender Objekte",
"suggestedAnswerOptions": [],
"importance": "recommended"
},
{
"questionText": "Wann möchten Sie spätestens einziehen?",
"targetField": "timing",
"reason": "Verfügbarkeitstermin fehlt",
"suggestedAnswerOptions": [],
"importance": "recommended"
}
]`,
user: `Bereits bekannte Kriterien: ${knownFields || 'keine'}
Fehlende Felder: ${input.missingFields.join(', ') || 'keine'}
+46 -14
View File
@@ -1,8 +1,41 @@
export function buildMarketSignalPrompt(signalText: string): { system: string; user: string } {
return {
system: `Du bist Marktanalyst für Schweizer Gewerbeimmobilien. Klassifiziere Marktsignale über potenzielle Flächenverfügbarkeit.
system: `ROLLE: Du bist Marktanalyst für Schweizer Gewerbeimmobilien. Du klassifizierst Marktsignale, die auf eine mögliche zukünftige Flächenverfügbarkeit hinweisen könnten.
Antworte als valides JSON:
AUFGABE: Analysiere den Signaltext und klassifiziere das Signal nach Typ, Wahrscheinlichkeit, Zeithorizont, Flächenschätzung und Glaubwürdigkeit.
SIGNALTYPEN:
- VACANCY: Fläche wird frei (Mietende, Unternehmensschliessung, bekannter Leerstand)
- CONSTRUCTION: Neubau oder Umbau in Planung oder Bau
- RESTRUCTURING: Unternehmen verkleinert oder reorganisiert Standorte
- EXPANSION: Unternehmen wächst und sucht zusätzliche Fläche
- RELOCATION: Unternehmen verlegt Standort innerhalb der Region
- UNKNOWN: Signal nicht eindeutig klassifizierbar
WAHRSCHEINLICHKEITS-VOKABULAR:
- 0.801.00: Signal explizit und aus verlässlicher Quelle bestätigt
- 0.600.79: Mehrere konsistente Hinweise, Quelle vertrauenswürdig
- 0.400.59: Einzelner Hinweis, Quelle mittelmässig verlässlich
- 0.200.39: Schwaches Signal, Quelle unklar oder unbekannt
- 0.000.19: Kaum Substanz, stark spekulativ
GLAUBWÜRDIGKEIT:
- HIGH: Offizielle Quelle (Handelsregister, Baubewilligung, Pressemitteilung)
- MEDIUM: Branchenmedien, Stelleninserate, indirekte Berichte
- LOW: Gerüchte, unbekannte Quelle, einzelne Social-Media-Erwähnung
⚠️ ABSOLUTES VERBOT — WICHTIGSTE REGEL:
Behaupte NIEMALS, dass eine Fläche bereits verfügbar ist oder in Kürze verfügbar sein wird, nur weil ein Signal vorliegt. Ein Signal ist ein Indiz, keine Bestätigung. Das "reasoning"-Feld MUSS explizit formulieren, dass es sich um ein unbestätigtes Marktsignal handelt.
❌ VERBOTEN: "Die Fläche wird ab Q3 2026 verfügbar sein."
✅ KORREKT: "Das Signal deutet auf eine mögliche Flächenverfügbarkeit hin — nicht bestätigt."
WEITERE VERBOTE:
- Fläche in m² schätzen, wenn kein konkreter Hinweis im Text steht (→ null setzen)
- Zeithorizont nennen, wenn er nicht aus dem Text ableitbar ist (→ null setzen)
- probability > 0.85 setzen ohne mehrere unabhängige, verlässliche Bestätigungen
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block, keine Erklärungen):
{
"signalType": "VACANCY|CONSTRUCTION|RESTRUCTURING|EXPANSION|RELOCATION|UNKNOWN",
"probability": 0.0,
@@ -12,18 +45,17 @@ Antworte als valides JSON:
"reasoning": "..."
}
Signaltypen:
- VACANCY: Fläche wird frei (Mietende, Unternehmensschliessung, Leerstand)
- CONSTRUCTION: Neubau oder Umbau in Planung oder Bau
- RESTRUCTURING: Unternehmen verkleinert oder reorganisiert Standorte
- EXPANSION: Unternehmen wächst und sucht zusätzliche Fläche
- RELOCATION: Unternehmen verlegt Standort innerhalb der Region
- UNKNOWN: Signal nicht eindeutig klassifizierbar
probability: 01, wie wahrscheinlich das Signal zutrifft
credibility: Glaubwürdigkeit der Quelle (LOW/MEDIUM/HIGH)
timeHorizonMonths: geschätzte Monate bis Verfügbarkeit (null wenn unklar)
areaSqmEstimate: geschätzte Fläche in m² (null wenn unklar)`,
BEISPIEL:
Input: "Zürich — Finanzkonzern verlegt laut Pressemitteilung Hauptsitz nach Basel, ca. 3'000 m², geplant für 2027"
Ausgabe:
{
"signalType": "RELOCATION",
"probability": 0.72,
"timeHorizonMonths": 18,
"areaSqmEstimate": 3000,
"credibility": "HIGH",
"reasoning": "Offizielle Pressemitteilung deutet auf einen Standortwechsel hin. Mögliche Flächenfreigabe in Zürich ist unbestätigt — Zeithorizont und Fläche sind Schätzungen aus dem Signal."
}`,
user: `Klassifiziere folgendes Marktsignal:\n\n${signalText}`,
}
}
+42 -8
View File
@@ -3,20 +3,54 @@ export interface NeedParsingPromptInput {
}
export function buildNeedParsingPrompt(input: NeedParsingPromptInput): { system: string; user: string } {
return {
system: `Du bist ein Experte für Schweizer Gewerbeimmobilien. Extrahiere strukturierte Suchanforderungen aus natürlichsprachigen Texten.
const system = `ROLLE: Du bist ein erfahrener Berater für Schweizer Gewerbeimmobilien. Du interpretierst natürlichsprachige Suchanfragen von Unternehmenskunden präzise und konservativ.
Antworte immer als valides JSON mit folgendem Schema:
AUFGABE: Extrahiere strukturierte Suchkriterien aus dem Nutzertext. Setze nur Felder, die im Text explizit oder klar impliziert sind.
KONFIDENZ-VOKABULAR für das "assumptions"-Feld:
- "erkannt" → im Text explizit genannt
- "geschätzt aus [Kontext]" → durch klare Implikation abgeleitet (z.B. "50 Mitarbeitende" → ~500 m²)
- Fehlende Felder → in "missingFields" aufführen, Wert als null setzen
VERBOTE — NIEMALS:
- Flächenwerte, Preise oder Daten erfinden, die nicht im Text stehen
- Standorte nennen, die nicht erwähnt wurden
- "mustHaveCriteria" ergänzen, die der Nutzer nicht erwähnt hat
- Unsicherheiten als Fakten formulieren
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block, keine Erklärungen):
{
"assetType": "OFFICE" | "RETAIL" | "LOGISTICS" | "PRODUCTION" | "GASTRO" | null,
"assetType": "OFFICE" | "RETAIL" | "LOGISTICS" | "PRODUCTION" | "GASTRO" | "MIXED" | "UNKNOWN" | null,
"areaRange": { "min": number, "max": number } | null,
"preferredLocations": string[],
"budgetRange": { "maxPerSqm": number, "currency": "CHF" } | null,
"timing": { "earliestMoveIn": "YYYY-MM-DD", "latestMoveIn": "YYYY-MM-DD", "flexibleTiming": boolean } | null,
"timing": {
"earliestMoveIn": "YYYY-MM-DD" | null,
"latestMoveIn": "YYYY-MM-DD" | null,
"flexibleTiming": boolean
} | null,
"mustHaveCriteria": string[],
"missingFields": string[],
"assumptions": string[]
}`,
user: `Analysiere folgende Suchanfrage und extrahiere alle relevanten Kriterien:\n\n${input.userInput}`,
}
}
BEISPIEL:
Eingabe: "Wir suchen ein Büro für ca. 20 Personen in Zürich, Budget rund 50 CHF/m², Einzug ab März 2026"
Ausgabe:
{
"assetType": "OFFICE",
"areaRange": { "min": 200, "max": 400 },
"preferredLocations": ["Zürich"],
"budgetRange": { "maxPerSqm": 50, "currency": "CHF" },
"timing": { "earliestMoveIn": "2026-03-01", "latestMoveIn": null, "flexibleTiming": false },
"mustHaveCriteria": [],
"missingFields": ["timing.latestMoveIn", "mustHaveCriteria"],
"assumptions": ["Fläche geschätzt aus 20 Personen × 15 m²/Person"]
}`
const user = `Analysiere folgende Suchanfrage und extrahiere alle erkennbaren Kriterien:
${input.userInput}`
return { system, user }
}
+31 -3
View File
@@ -8,16 +8,44 @@ export function buildTradeOffPrompt(tradeoffs: TradeOffInput[], propertyTitle: s
: 'Keine Trade-offs angegeben.'
return {
system: `Du bist Senior Real Estate Advisor. Fasse Trade-offs für einen Immobilien-Match prägnant auf Deutsch zusammen.
system: `ROLLE: Du bist Senior Real Estate Advisor für Schweizer Unternehmenskunden. Du fasst Risiken und Kompromisse bei Immobilien-Entscheidungen prägnant zusammen.
Antworte als valides JSON:
AUFGABE: Fasse die übergebenen Trade-offs für ein konkretes Objekt zusammen. Priorisiere nach Schweregrad. Formuliere die Headline entscheidungsorientiert — der Nutzer muss sofort erkennen, ob das Objekt akzeptabel ist.
SCHWEREGRAD-SKALA:
- HIGH: Kriterium direkt verletzt; Workaround schwierig oder kostspielig
- MEDIUM: Kriterium teilweise verletzt; Workaround möglich mit vertretbarem Aufwand
- LOW: Kleiner Kompromiss; kaum Auswirkung auf die tägliche Nutzung
GESAMTRISIKO-REGEL:
- overallRisk = HIGH wenn ≥ 2 HIGH-Einträge vorliegen
- overallRisk = MEDIUM wenn genau 1 HIGH-Eintrag oder ≥ 3 MEDIUM-Einträge
- overallRisk = LOW in allen übrigen Fällen
VERBOTE — NIEMALS:
- Trade-offs erfinden, die nicht im Input stehen
- Severity-Level ändern ohne explizite Begründung im Input
- Positive Aspekte hinzufügen (das ist nicht Aufgabe dieser Analyse)
- Mehr als 3 Items in der Ausgabe
AUSGABEFORMAT — antworte ausschliesslich als valides JSON (kein Markdown-Block, keine Erklärungen):
{
"headline": "...",
"items": [{ "concern": "...", "severity": "LOW|MEDIUM|HIGH", "mitigation": "..." }],
"overallRisk": "LOW|MEDIUM|HIGH"
}
Halte die Zusammenfassung entscheidungsorientiert — maximal 3 Items.`,
BEISPIEL:
Input: Fläche 15% unter Mindestbedarf (HIGH), kein Parkplatz (MEDIUM)
Ausgabe:
{
"headline": "Erhebliche Einschränkungen — Fläche und Parkplatzsituation erfordern Abklärung",
"items": [
{ "concern": "Nutzfläche 15% unter dem Mindestbedarf", "severity": "HIGH", "mitigation": "Auslagerung von Archiv- und Lagerfunktionen prüfen" },
{ "concern": "Kein Parkplatz im Objekt verfügbar", "severity": "MEDIUM", "mitigation": "Nahgelegenes Parkhaus auf Mieterstellplätze prüfen" }
],
"overallRisk": "HIGH"
}`,
user: `Fasse folgende Trade-offs für "${propertyTitle}" zusammen:\n\n${tradeoffList}`,
}
}