Files
iddaai-be/src/services/ai.service.ts
T
fahricansecer 27e96da31d
Deploy Iddaai Backend / build-and-deploy (push) Successful in 29s
main
2026-05-04 18:00:40 +03:00

331 lines
9.3 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, number>;
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<AIPredictionResult | null> {
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<boolean> {
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: [],
};
}
}