feat(analytics): add Python NeighborhoodScore service + NestJS HTTP proxy (TEC-2756)

- libs/ai-services: new POST /neighborhood/score router computing weighted
  6-axis livability score from per-category POI counts; algorithm versioned
  for future iteration (sigmoid curves, percentile thresholds).
- apps/api: HttpNeighborhoodScoreService proxies to Python first, falls back
  to PrismaNeighborhoodScoreService when AI service unavailable. Mirrors the
  HttpAVMService pattern. Existing GET /analytics/neighborhoods/:district/score
  endpoint and CQRS handler now flow through the proxy.
- AnalyticsModule binds Http variant by default, retains Prisma variant as
  injectable fallback.
- Tests: 5 pytest cases for Python heuristic, 4 vitest cases for HTTP proxy
  fallback behaviour.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-18 15:07:02 +07:00
parent 329a821b4a
commit 2c1e3771e9
9 changed files with 551 additions and 93 deletions

View File

@@ -91,12 +91,42 @@ export interface AiModerationResponse {
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<string, number>;
algorithm_version: string;
}
export const AI_SERVICE_CLIENT = Symbol('AI_SERVICE_CLIENT');
export interface IAiServiceClient {
predict(req: AiPredictRequest): Promise<AiPredictResponse>;
predictIndustrial(req: AiIndustrialPredictRequest): Promise<AiIndustrialPredictResponse>;
moderate(req: AiModerationRequest): Promise<AiModerationResponse>;
scoreNeighborhood(req: AiNeighborhoodScoreRequest): Promise<AiNeighborhoodScoreResponse>;
isAvailable(): Promise<boolean>;
}
@@ -124,6 +154,12 @@ export class AiServiceClient implements IAiServiceClient {
return this.post<AiModerationResponse>('/moderation/check', req);
}
async scoreNeighborhood(
req: AiNeighborhoodScoreRequest,
): Promise<AiNeighborhoodScoreResponse> {
return this.post<AiNeighborhoodScoreResponse>('/neighborhood/score', req);
}
async isAvailable(): Promise<boolean> {
try {
const response = await fetch(`${this.baseUrl}/health`, {