first (part 3: src directory)
Deploy Iddaai Backend / build-and-deploy (push) Successful in 33s

This commit is contained in:
2026-04-16 15:12:27 +03:00
parent 2f0b85a0c7
commit 182f4aae16
125 changed files with 22552 additions and 0 deletions
+319
View File
@@ -0,0 +1,319 @@
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
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;
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
) {
this.pythonEngineUrl =
this.configService.get('AI_ENGINE_URL') || 'http://127.0.0.1:8000';
}
/**
* 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 V25 Engine for ${matchDetails.homeTeam} vs ${matchDetails.awayTeam}`,
);
const response = await firstValueFrom(
this.httpService.post(
`${this.pythonEngineUrl}/v20plus/analyze/${matchId}`,
{},
{
timeout: 30000,
},
),
);
if (response.data) {
return this.mapPythonResponse(response.data, matchDetails);
}
return null;
} catch (error: any) {
this.logger.warn(`Python Engine error: ${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: data.model_version || 'v25.main',
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: pyData.model_version || 'v25.main',
expectedGoals: pyData.score_prediction?.xg_total || 0,
keyInsights: [
`Model: ${pyData.model_version || 'v25.main'}`,
`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 firstValueFrom(
this.httpService.get(`${this.pythonEngineUrl}/health`, {
timeout: 5000,
}),
);
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: 'v25.main',
expectedGoals: 0,
keyInsights: [],
};
}
}