fb029cf0bc
- IAIService.recommendPreMarketRent: recommended price + range, verdict (UNDERPRICED/FAIR/AMBITIOUS), drivers, rationale, confidence
- MockAIService: deterministic recommendation from locationIntelligence — regional comp median, vacancy (supply), demand strength + days-on-market, rent trend (forward for pre-market)
- BackendAIService: LLM prompt with market context + mock fallback
- usePreMarketRentRecommendation hook; PreMarketPriceAdvisor component shows the recommendation per released unit with verdict ("zu günstig" when underpriced) + adjustable expected price + "Empfehlung übernehmen"
- Replaces the simple indexed suggestion with a market-driven AI recommendation that flags underpricing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
251 lines
8.7 KiB
TypeScript
251 lines
8.7 KiB
TypeScript
import type { CreateNeedInput } from '../../domain/need'
|
|
import type { ParseNeedResult, ParsedNeedCriteria, FollowUpQuestion } from '../../domain/needBuilder'
|
|
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
|
|
|
|
// ── AI Provenance ─────────────────────────────────────────────────────────────
|
|
// Attached to every AI response so the UI can always trace where data came from.
|
|
|
|
export interface AIProvenance {
|
|
/** Which AI provider produced this response */
|
|
provider: 'openrouter' | 'mock' | 'backend'
|
|
/** Exact model ID (e.g. 'anthropic/claude-3-5-haiku') or 'mock' */
|
|
model: string
|
|
/** ISO-8601 timestamp of generation */
|
|
generatedAt: string
|
|
/** Prompt version string used to generate this response */
|
|
promptVersion: string
|
|
/** Zod schema version used for validation */
|
|
schemaVersion: string
|
|
/** Whether the response is AI-only, mock-only, or a hybrid merge */
|
|
source: 'ai' | 'mock' | 'hybrid'
|
|
/** True when the original AI call failed and mock was substituted */
|
|
fallbackUsed: boolean
|
|
/** Human-readable reason why a fallback occurred — undefined when no fallback */
|
|
fallbackReason?: string
|
|
/** True when the AI response passed Zod schema validation */
|
|
validationPassed: boolean
|
|
/** Unique request ID — correlates AIResponse with AITrace.id */
|
|
traceId: string
|
|
/** Wall-clock latency for this call in milliseconds */
|
|
latencyMs?: number
|
|
}
|
|
|
|
/**
|
|
* All AI service methods return AIResponse<T> instead of ItemResponse<T>.
|
|
* `data` is backward-compatible — existing call sites using `result.data.xxx`
|
|
* continue to work unchanged.
|
|
*/
|
|
export type AIResponse<T> = {
|
|
data: T
|
|
provenance: AIProvenance
|
|
}
|
|
|
|
// ── Helper ────────────────────────────────────────────────────────────────────
|
|
|
|
export function mockProvenance(overrides?: Partial<AIProvenance>): AIProvenance {
|
|
return {
|
|
provider: 'mock',
|
|
model: 'mock',
|
|
generatedAt: new Date().toISOString(),
|
|
promptVersion: 'mock',
|
|
schemaVersion: 'mock',
|
|
source: 'mock',
|
|
fallbackUsed: false,
|
|
validationPassed: true,
|
|
traceId: crypto.randomUUID(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
// ── Response Types ────────────────────────────────────────────────────────────
|
|
|
|
export interface DecisionBrief {
|
|
id: string
|
|
shortlistId: string
|
|
summary: string
|
|
sections: { title: string; body: string }[]
|
|
generatedAt: string
|
|
isDraft: true
|
|
}
|
|
|
|
export interface ComparisonSummary {
|
|
strongestOption: { matchId: string; label: string; reason: string }
|
|
bestValue: { matchId: string; label: string; reason: string } | null
|
|
highestConfidence: { matchId: string; label: string; confidenceLevel: number }
|
|
biggestTradeoffs: string[]
|
|
missingDataWarnings: string[]
|
|
recommendedNextStep: string
|
|
overallAssessment: string
|
|
perPropertyAssessment: Array<{
|
|
matchId: string
|
|
label: string
|
|
strengths: string[]
|
|
weaknesses: string[]
|
|
bestFor: string
|
|
keyRisk: string | null
|
|
}>
|
|
recommendation: string
|
|
}
|
|
|
|
export interface OfferEmailPayload {
|
|
needTitle: string
|
|
properties: string[]
|
|
matchScores: number[]
|
|
}
|
|
|
|
export interface ParsedListingData {
|
|
assetType?: string
|
|
areaSqm?: number
|
|
rentPerSqm?: number
|
|
city?: string
|
|
softLevels?: Record<string, string>
|
|
parking?: number
|
|
fitOut?: string
|
|
}
|
|
|
|
export interface MatchExplanation {
|
|
headline: string
|
|
summary: string
|
|
keyReasons: string[]
|
|
}
|
|
|
|
export interface TradeOffSummary {
|
|
headline: string
|
|
items: Array<{ concern: string; severity: 'LOW' | 'MEDIUM' | 'HIGH'; mitigation?: string }>
|
|
overallRisk: 'LOW' | 'MEDIUM' | 'HIGH'
|
|
}
|
|
|
|
export interface DataQualitySummary {
|
|
overallAssessment: string
|
|
missingCriticalFields: string[]
|
|
recommendation: string
|
|
confidence: number
|
|
}
|
|
|
|
export interface MarketSignalClassification {
|
|
signalType: 'VACANCY' | 'CONSTRUCTION' | 'RESTRUCTURING' | 'EXPANSION' | 'RELOCATION' | 'UNKNOWN'
|
|
probability: number
|
|
timeHorizonMonths: number | null
|
|
areaSqmEstimate: number | null
|
|
credibility: 'LOW' | 'MEDIUM' | 'HIGH'
|
|
reasoning: string
|
|
}
|
|
|
|
// ── Input Types ───────────────────────────────────────────────────────────────
|
|
|
|
export interface MatchExplanationInput {
|
|
propertyTitle: string
|
|
propertyCity: string
|
|
matchScore: number
|
|
positiveFactors: Array<{ criterion: string; explanation: string }>
|
|
negativeFactors: Array<{ criterion: string; explanation: string }>
|
|
needSummary: string
|
|
}
|
|
|
|
export interface TradeOffInput {
|
|
criterion: string
|
|
concern: string
|
|
severity: 'LOW' | 'MEDIUM' | 'HIGH'
|
|
mitigation?: string
|
|
}
|
|
|
|
export interface DataQualityInput {
|
|
score: number
|
|
freshness: string
|
|
missingCriticalFields: string[]
|
|
missingOptionalFields: string[]
|
|
warnings: string[]
|
|
}
|
|
|
|
// ── Fit-out advice ────────────────────────────────────────────────────────────
|
|
|
|
export interface FitOutAdviceInput {
|
|
fitOut: string
|
|
areaSqm: number
|
|
mabPerSqm: number
|
|
requiredFitOut?: string
|
|
tenantBudgetPerSqm?: number
|
|
monthlyRentPerSqm: number
|
|
}
|
|
|
|
export interface FitOutAdvice {
|
|
recommendation: 'MIETERAUSBAU' | 'BKZ' | 'MAB_AMORTISATION'
|
|
headline: string
|
|
explanation: string
|
|
negotiationTip: string
|
|
estimatedNetInvestment: string
|
|
}
|
|
|
|
// ── Pre-market rent recommendation ─────────────────────────────────────────────
|
|
|
|
export interface PreMarketRentInput {
|
|
city: string
|
|
assetType: string
|
|
areaSqm: number
|
|
currentRentPerSqm: number
|
|
availableFrom?: string // ISO — Pre-Market liegt in der Zukunft
|
|
}
|
|
|
|
export interface PreMarketRentRecommendation {
|
|
recommendedPerSqm: number
|
|
rangeMinPerSqm: number
|
|
rangeMaxPerSqm: number
|
|
verdict: 'UNDERPRICED' | 'FAIR' | 'AMBITIOUS' // Bewertung des heutigen Preises
|
|
deltaVsCurrentPct: number // Empfehlung vs. heutiger Preis
|
|
drivers: string[] // Vergleichsmiete, Angebot, Nachfrage, Trend …
|
|
rationale: string
|
|
confidence: 'LOW' | 'MEDIUM' | 'HIGH'
|
|
}
|
|
|
|
// ── Legacy types (kept for backward compatibility) ────────────────────────────
|
|
|
|
export interface CriteriaExtractionResult {
|
|
extractedCriteria: Partial<CreateNeedInput>
|
|
confidence: number
|
|
missingFields: string[]
|
|
assumptions: string[]
|
|
followUpQuestions: string[]
|
|
}
|
|
|
|
export interface AIServiceProvider {
|
|
extractCriteria(naturalLanguageInput: string): Promise<CriteriaExtractionResult>
|
|
generateFollowUp(partialNeed: Partial<CreateNeedInput>): Promise<string[]>
|
|
}
|
|
|
|
// ── Service Interface ─────────────────────────────────────────────────────────
|
|
|
|
export interface IAIService {
|
|
// Need parsing (F008)
|
|
parseNeed(input: string): Promise<AIResponse<ParseNeedResult>>
|
|
generateFollowUpQuestions(criteria: ParsedNeedCriteria): Promise<AIResponse<FollowUpQuestion[]>>
|
|
|
|
// Match explainability (F004)
|
|
generateMatchExplanation(input: MatchExplanationInput): Promise<AIResponse<MatchExplanation>>
|
|
summarizeTradeOffs(tradeoffs: TradeOffInput[]): Promise<AIResponse<TradeOffSummary>>
|
|
|
|
// Compare (F014)
|
|
summarizeComparison(items: UnifiedMatchResult[]): Promise<AIResponse<ComparisonSummary>>
|
|
|
|
// Shortlist decision brief
|
|
generateDecisionBrief(shortlistId: string): Promise<AIResponse<DecisionBrief>>
|
|
|
|
// Data quality
|
|
generateDataQualitySummary(propertyId: string, quality: DataQualityInput): Promise<AIResponse<DataQualitySummary>>
|
|
|
|
// Market signal classification (OPERATIONS)
|
|
classifyMarketSignal(signalText: string): Promise<AIResponse<MarketSignalClassification>>
|
|
|
|
// Offer email (supply side)
|
|
generateOfferEmail(payload: OfferEmailPayload): Promise<AIResponse<{ subject: string; body: string }>>
|
|
|
|
// Fit-out investment advice (demand side)
|
|
generateFitOutAdvice(input: FitOutAdviceInput): Promise<AIResponse<FitOutAdvice>>
|
|
|
|
// Pre-market rent recommendation (supply side) — based on regional comparables, supply & demand
|
|
recommendPreMarketRent(input: PreMarketRentInput): Promise<AIResponse<PreMarketRentRecommendation>>
|
|
|
|
// Legacy methods
|
|
extractCriteria(input: string): Promise<AIResponse<CriteriaExtractionResult>>
|
|
generateFollowUp(partialNeed: Partial<CreateNeedInput>): Promise<AIResponse<string[]>>
|
|
}
|