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:
@@ -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 (0–1). 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: 0–1, 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}`,
|
||||
}
|
||||
}
|
||||
@@ -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}`,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user