import { Injectable } from '@nestjs/common'; import { type LoggerService } from '@modules/shared'; export interface AiPredictRequest { area: number; district: string; city: string; property_type: string; bedrooms?: number; bathrooms?: number; floors?: number; frontage?: number; road_width?: number; year_built?: number | null; has_legal_paper?: boolean; } export interface AiPredictResponse { estimated_price_vnd: number; confidence: number; price_per_m2: number; price_range_low: number; price_range_high: number; } export interface AiIndustrialPredictRequest { province: string; region: string; park_occupancy_rate: number; park_area_ha: number; park_age_years: number; distance_to_port_km: number; distance_to_airport_km: number; distance_to_highway_km: number; property_type: string; area_m2: number; ceiling_height_m?: number; floor_load_ton_m2?: number; power_capacity_kva?: number; building_coverage?: number; loading_docks?: number; zoning?: string; industry_demand_index?: number; fdi_province_musd?: number; labor_cost_province_vnd?: number; logistics_connectivity_score?: number; } export interface AiIndustrialComparable { park_name: string; province: string; property_type: string; area_m2: number; rent_usd_m2: number; similarity_score: number; } export interface AiIndustrialFeatureImportance { feature: string; importance: number; } export interface AiIndustrialPredictResponse { estimated_rent_usd_m2: number; confidence: number; rent_range_low_usd_m2: number; rent_range_high_usd_m2: number; annual_rent_usd_m2: number; total_monthly_rent_usd: number; comparables: AiIndustrialComparable[]; drivers: AiIndustrialFeatureImportance[]; model_version: string; } export interface AiModerationRequest { text: string; context?: string; } export interface AiModerationFlag { category: string; severity: string; matched_text: string; reason: string; } export interface AiModerationResponse { is_flagged: boolean; score: number; flags: AiModerationFlag[]; cleaned_text: string | null; } export interface AiNeighborhoodPOICounts { education: number; healthcare: number; transport: number; shopping: number; greenery: number; safety: number; } export interface AiNeighborhoodScoreRequest { district: string; city: string; poi_counts: AiNeighborhoodPOICounts; } export interface AiNeighborhoodScoreResponse { district: string; city: string; education_score: number; healthcare_score: number; transport_score: number; shopping_score: number; greenery_score: number; safety_score: number; total_score: number; poi_counts: Record; algorithm_version: string; } export const AI_SERVICE_CLIENT = Symbol('AI_SERVICE_CLIENT'); export interface IAiServiceClient { predict(req: AiPredictRequest): Promise; predictIndustrial(req: AiIndustrialPredictRequest): Promise; moderate(req: AiModerationRequest): Promise; scoreNeighborhood(req: AiNeighborhoodScoreRequest): Promise; isAvailable(): Promise; } @Injectable() export class AiServiceClient implements IAiServiceClient { private readonly baseUrl: string; private readonly apiKey: string; private readonly timeoutMs: number; constructor(private readonly logger: LoggerService) { this.baseUrl = process.env['AI_SERVICE_URL'] ?? 'http://localhost:8000'; this.apiKey = process.env['AI_SERVICE_API_KEY'] ?? ''; this.timeoutMs = Number(process.env['AI_SERVICE_TIMEOUT_MS']) || 5000; } async predict(req: AiPredictRequest): Promise { return this.post('/avm/predict', req); } async predictIndustrial(req: AiIndustrialPredictRequest): Promise { return this.post('/avm/industrial/predict', req); } async moderate(req: AiModerationRequest): Promise { return this.post('/moderation/check', req); } async scoreNeighborhood( req: AiNeighborhoodScoreRequest, ): Promise { return this.post('/neighborhood/score', req); } async isAvailable(): Promise { try { const response = await fetch(`${this.baseUrl}/health`, { method: 'GET', signal: AbortSignal.timeout(2000), }); return response.ok; } catch { return false; } } private async post(path: string, body: unknown): Promise { const url = `${this.baseUrl}${path}`; const headers: Record = { 'Content-Type': 'application/json', }; if (this.apiKey) { headers['X-API-Key'] = this.apiKey; } const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body), signal: AbortSignal.timeout(this.timeoutMs), }); if (!response.ok) { const text = await response.text().catch(() => ''); throw new Error(`AI service ${path} returned ${response.status}: ${text}`); } return response.json() as Promise; } }