import { Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { AiEngineClient, AiEngineRequestError, } from "../common/utils/ai-engine-client"; export interface AIPredictionResult { matchId: string; matchName: string; predictions: { betType: string; prediction: string; confidence: number; probabilities?: Record; reasoning?: string; odd?: number; valueBet?: { isValue: boolean; edge: number }; }[]; recommendedBets: string[]; homeAnalysis?: { teamName: string; formText: string; goalsAvg: number; formRating: string; squadStrength?: number; }; awayAnalysis?: { teamName: string; formText: string; goalsAvg: number; formRating: string; squadStrength?: number; }; expertComment: string; modelVersion: string; confidenceScore: number; expectedGoals?: number; } @Injectable() export class AiService { private readonly logger = new Logger(AiService.name); private readonly pythonEngineUrl: string; private readonly aiEngineClient: AiEngineClient; constructor(private readonly configService: ConfigService) { this.pythonEngineUrl = this.configService.get("AI_ENGINE_URL") || "http://127.0.0.1:8000"; this.aiEngineClient = new AiEngineClient({ baseUrl: this.pythonEngineUrl, logger: this.logger, serviceName: AiService.name, timeoutMs: 30000, maxRetries: 2, retryDelayMs: 500, }); } /** * Call the Python match analysis engine and map the result to stable frontend contract. */ async callPythonEngine( matchDetails: any, _odds: any[], _lineups: { home: any[]; away: any[] }, _substitutes: { home: any[]; away: any[] } | null, _stats: any, _eventData: any[], ): Promise { try { const matchId = String(matchDetails?.matchId || "").trim(); if (!matchId) { this.logger.warn("Skipping AI call: missing matchId"); return null; } this.logger.log( `Calling Python V28 Pro Max Engine for ${matchDetails.homeTeam} vs ${matchDetails.awayTeam}`, ); const response = await this.aiEngineClient.post( `/v20plus/analyze/${matchId}`, {}, ); if (response.data) { return this.mapPythonResponse(response.data, matchDetails); } return null; } catch (error: unknown) { const message = error instanceof AiEngineRequestError ? error.message : error instanceof Error ? error.message : "Unknown AI engine error"; this.logger.warn(`Python Engine error: ${message}`); return null; } } /** * Map Python response to our interface */ private mapPythonResponse(data: any, matchDetails: any): AIPredictionResult { const picks = Array.isArray(data?.bet_summary) ? data.bet_summary : []; const recommendedBets = picks .filter((p: any) => p?.playable) .map((p: any) => `${p.market}: ${p.pick}`); const mappedPredictions = picks.map((p: any) => ({ betType: String(p.market || ""), prediction: String(p.pick || ""), confidence: Number(p.calibrated_confidence ?? p.confidence ?? 0), probabilities: {}, reasoning: Array.isArray(p.reasons) ? p.reasons.join(" | ") : Array.isArray(p.decision_reasons) ? p.decision_reasons.join(" | ") : "", odd: typeof p.odds === "number" ? p.odds : undefined, valueBet: typeof p.edge === "number" ? { isValue: p.edge > 0, edge: p.edge, } : undefined, })); const matchInfo = data?.match_info || {}; const confidenceScore = Number( data?.main_pick?.calibrated_confidence ?? data?.main_pick?.confidence ?? 0, ); return { matchId: matchDetails.matchId || matchInfo.match_id || data.match_id, matchName: `${matchDetails.homeTeam} vs ${matchDetails.awayTeam}`, predictions: mappedPredictions, recommendedBets: data.recommended_bets && Array.isArray(data.recommended_bets) ? data.recommended_bets : recommendedBets, homeAnalysis: undefined, awayAnalysis: undefined, expertComment: data.ai_commentary || data.expert_comment || "", modelVersion: "v28-pro-max", confidenceScore: confidenceScore > 1 ? confidenceScore : confidenceScore * 100, expectedGoals: data?.score_prediction?.xg_total, }; } /** * Get mapped prediction response from the AI package. */ getPredictionForMatch(analysisData: any): any { const pyData = analysisData.liveMatchData?.pythonEnginePrediction; if (!pyData || !Array.isArray(pyData.bet_summary)) { return this.getEmptyPrediction(); } const allPredictions = pyData.bet_summary.map((p: any) => ({ betType: p.market, prediction: p.pick, confidence: p.calibrated_confidence ?? p.confidence ?? 0, probabilities: {}, reasoning: Array.isArray(p.reasons) ? p.reasons.join(" | ") : "", odd: p.odds || 0, valueBet: { is_value: typeof p.edge === "number" ? p.edge > 0 : false, edge: p.edge || 0, }, })); const firstPick = allPredictions[0]; return { predictions: allPredictions, recommendedBets: pyData.recommended_bets || [], valueBets: allPredictions.filter((p: any) => p.valueBet?.is_value), homeAnalysis: null, awayAnalysis: null, expertComment: pyData.ai_commentary || "", winnerPrediction: firstPick?.prediction || "N/A", scorePrediction: pyData.score_prediction?.ft || "-", confidenceScore: typeof firstPick?.confidence === "number" ? firstPick.confidence : 0, modelVersion: "v28-pro-max", expectedGoals: pyData.score_prediction?.xg_total || 0, keyInsights: [ `Model: v28-pro-max`, `Risk: ${pyData.risk?.level || "N/A"} (${pyData.risk?.score ?? 0})`, `Data Quality: ${pyData.data_quality?.label || "N/A"}`, `xG Beklentisi: ${ typeof pyData.score_prediction?.xg_total === "number" ? pyData.score_prediction.xg_total.toFixed(2) : "N/A" }`, ], }; } /** * Generate analysis strategy (replaces Gemini) */ getAnalysisStrategy(matchDNA: any): { analysisTactics: any[] } { const tactics: any[] = []; const odds = matchDNA?.odds || []; // MS 1 oranını bul const ms1 = odds.find( (o: any) => o.category?.toLowerCase().includes("maç sonucu") && o.selection === "1", ); // KG Var oranını bul const kgVar = odds.find( (o: any) => o.category?.toLowerCase().includes("karşılıklı gol") && o.selection?.toLowerCase() === "var", ); // Alt 2.5 oranını bul const alt25 = odds.find( (o: any) => o.category?.toLowerCase().includes("alt/üst") && o.selection?.toLowerCase() === "alt", ); // Tactic 1: Benzer MS oranları if (ms1?.odd_value) { tactics.push({ tacticName: "Benzer Maç Sonucu Oranları", description: "Ev sahibi galibiyeti için benzer oran aralığındaki maçlar", odds: [ { categoryName: "Maç Sonucu", selectionName: "1", value: parseFloat(ms1.odd_value), tolerance: 0.3, }, ], }); } // Tactic 2: Benzer KG + AU oranları if (kgVar?.odd_value && alt25?.odd_value) { tactics.push({ tacticName: "Benzer Gol Beklentisi", description: "Karşılıklı gol ve toplam gol benzerliği", odds: [ { categoryName: "Karşılıklı Gol", selectionName: "Var", value: parseFloat(kgVar.odd_value), tolerance: 0.4, }, { categoryName: "2,5 Alt/Üst", selectionName: "Alt", value: parseFloat(alt25.odd_value), tolerance: 0.3, }, ], }); } // Tactic 3: Favori analizi if (ms1?.odd_value && parseFloat(ms1.odd_value) < 1.8) { tactics.push({ tacticName: "Favori Takım Analizi", description: "Benzer şekilde favori olan ev sahibi takımların maçları", odds: [ { categoryName: "Maç Sonucu", selectionName: "1", value: parseFloat(ms1.odd_value), tolerance: 0.2, }, ], }); } return { analysisTactics: tactics }; } /** * Check Python engine health */ async checkHealth(): Promise { try { const response = await this.aiEngineClient.get<{ status?: string }>( "/health", { timeout: 5000, retryCount: 0, }, ); return response.data?.status === "healthy"; } catch { return false; } } /** * Get empty prediction fallback */ private getEmptyPrediction() { return { predictions: [], recommendedBets: [], valueBets: [], homeAnalysis: null, awayAnalysis: null, expertComment: "Analiz verisi alınamadı (Python Servis Hatası).", winnerPrediction: "N/A", scorePrediction: "-", confidenceScore: 0, modelVersion: "v28-pro-max", expectedGoals: 0, keyInsights: [], }; } }