feat: OpenRouter-ready AI service — all 11 methods implemented

IAIService:
+ generateMatchExplanation, summarizeTradeOffs, generateDataQualitySummary,
  classifyMarketSignal (4 new methods covering all documented AI output types)

OpenRouterAIService:
- All 11 methods now make real API calls via withFallback() pattern
- Every fallback is explicit (console.warn/error) — no silent mock bleed-through
- Proper JSON extraction with type-safe parsers, no any casts
- parseNeed: AI JSON → ParseNeedResult mapping (no TODO stubs)
- generateFollowUpQuestions, generateMatchExplanation, summarizeTradeOffs,
  generateDataQualitySummary, classifyMarketSignal, generateOfferEmail,
  extractCriteria, generateFollowUp: fully implemented

Prompts: +followUpQuestionsPrompt, +tradeOffPrompt, +dataQualityPrompt, +marketSignalPrompt

Factory (index.ts):
- VITE_AI_PROVIDER=mock|openrouter (new, takes priority)
- VITE_USE_REAL_AI=true still supported (legacy compat)
- Missing API key → explicit console.warn + MockAIService fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-24 12:43:51 +02:00
parent 723f553939
commit c972392b78
9 changed files with 755 additions and 88 deletions
@@ -0,0 +1,28 @@
import type { DataQualityInput } from '../IAIService'
export function buildDataQualityPrompt(propertyId: string, quality: DataQualityInput): { system: string; user: string } {
const scorePercent = Math.round(quality.score * 100)
const criticalList = quality.missingCriticalFields.join(', ') || 'keine'
const optionalList = quality.missingOptionalFields.join(', ') || 'keine'
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.
Antworte als valides JSON:
{
"overallAssessment": "...",
"missingCriticalFields": ["..."],
"recommendation": "...",
"confidence": 0.0
}
Die Confidence entspricht dem übergebenen Score (01). Halte die Bewertung unter 3 Sätzen.`,
user: `Qualitätsbewertung für Objekt ${propertyId}:
- Score: ${scorePercent}%
- Freshness: ${quality.freshness}
- Fehlende Pflichtfelder: ${criticalList}
- Fehlende optionale Felder: ${optionalList}
- Warnungen: ${warningList}`,
}
}
@@ -0,0 +1,32 @@
import type { ParsedNeedCriteria } from '../../../domain/needBuilder'
export interface FollowUpQuestionsPromptInput {
criteria: ParsedNeedCriteria
missingFields: string[]
}
export function buildFollowUpQuestionsPrompt(input: FollowUpQuestionsPromptInput): { system: string; user: string } {
const knownFields = Object.entries(input.criteria)
.filter(([, v]) => v != null)
.map(([k]) => k)
.join(', ')
return {
system: `Du bist ein Experte für Schweizer Gewerbeimmobilien-Suche. Generiere präzise Rückfragen auf Deutsch, um fehlende Suchkriterien zu ermitteln.
Antworte als valides JSON-Array mit maximal 3 Einträgen, priorisiert nach Wichtigkeit:
[
{
"questionText": "...",
"targetField": "assetType|areaRange|preferredLocations|budgetRange|timing|mustHaveCriteria",
"reason": "...",
"suggestedAnswerOptions": ["...", "..."],
"importance": "required|recommended|optional"
}
]`,
user: `Bereits bekannte Kriterien: ${knownFields || 'keine'}
Fehlende Felder: ${input.missingFields.join(', ') || 'keine'}
Generiere Rückfragen für die wichtigsten fehlenden Informationen.`,
}
}
@@ -0,0 +1,29 @@
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.
Antworte als valides JSON:
{
"signalType": "VACANCY|CONSTRUCTION|RESTRUCTURING|EXPANSION|RELOCATION|UNKNOWN",
"probability": 0.0,
"timeHorizonMonths": null,
"areaSqmEstimate": null,
"credibility": "LOW|MEDIUM|HIGH",
"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)`,
user: `Klassifiziere folgendes Marktsignal:\n\n${signalText}`,
}
}
+23
View File
@@ -0,0 +1,23 @@
import type { TradeOffInput } from '../IAIService'
export function buildTradeOffPrompt(tradeoffs: TradeOffInput[], propertyTitle: string): { system: string; user: string } {
const tradeoffList = tradeoffs.length > 0
? tradeoffs
.map(t => `- ${t.criterion}: ${t.concern} (Schweregrad: ${t.severity})${t.mitigation ? ` — Massnahme: ${t.mitigation}` : ''}`)
.join('\n')
: '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.
Antworte als valides JSON:
{
"headline": "...",
"items": [{ "concern": "...", "severity": "LOW|MEDIUM|HIGH", "mitigation": "..." }],
"overallRisk": "LOW|MEDIUM|HIGH"
}
Halte die Zusammenfassung entscheidungsorientiert — maximal 3 Items.`,
user: `Fasse folgende Trade-offs für "${propertyTitle}" zusammen:\n\n${tradeoffList}`,
}
}