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
+248
View File
@@ -0,0 +1,248 @@
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import axios from 'axios';
import { GeminiService } from '../../gemini/gemini.service';
export type PredictionRiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'EXTREME';
export type PredictionDataQuality = 'HIGH' | 'MEDIUM' | 'LOW';
export type BetGrade = 'A' | 'B' | 'C' | 'PASS';
export interface PredictionPickRow {
market: string;
pick: string;
probability: number;
confidence: number;
odds: number;
raw_confidence: number;
calibrated_confidence: number;
min_required_confidence: number;
edge: number;
play_score: number;
playable: boolean;
bet_grade: BetGrade;
stake_units: number;
decision_reasons: string[];
}
export interface PredictionBetSummaryRow {
market: string;
pick: string;
raw_confidence: number;
calibrated_confidence: number;
bet_grade: BetGrade;
playable: boolean;
stake_units: number;
play_score: number;
reasons: string[];
}
export interface SingleMatchPredictionPackage {
model_version: string;
match_info: {
match_id: string;
match_name: string;
home_team: string;
away_team: string;
league: string;
match_date_ms: number;
};
data_quality: {
label: PredictionDataQuality;
score: number;
flags: string[];
home_lineup_count: number;
away_lineup_count: number;
};
risk: {
level: PredictionRiskLevel;
score: number;
is_surprise_risk: boolean;
surprise_type: string | null;
warnings: string[];
};
engine_breakdown: {
team: number;
player: number;
odds: number;
referee: number;
};
main_pick: PredictionPickRow | null;
value_pick: PredictionPickRow | null;
bet_advice: {
playable: boolean;
suggested_stake_units: number;
reason: string;
};
bet_summary: PredictionBetSummaryRow[];
supporting_picks: PredictionPickRow[];
aggressive_pick: {
market: string;
pick: string;
probability: number;
confidence: number;
odds: number | null;
} | null;
scenario_top5: Array<{
score: string;
prob: number;
[key: string]: unknown;
}>;
score_prediction: {
ft: string;
ht: string;
xg_home: number;
xg_away: number;
xg_total: number;
};
market_board: Record<string, unknown>;
reasoning_factors: string[];
ai_commentary?: string | null;
}
export interface SmartCouponResult {
strategy: string;
generated_at: string;
match_count: number;
bets: Array<{
match_id: string;
match_name: string;
market: string;
pick: string;
probability: number;
confidence: number;
odds: number;
risk_level: PredictionRiskLevel;
data_quality: PredictionDataQuality;
}>;
total_odds: number;
expected_win_rate: number;
rejected_matches: Array<{
match_id: string;
reason: string;
threshold?: number;
}>;
}
@Injectable()
export class SmartCouponService {
private readonly logger = new Logger(SmartCouponService.name);
private readonly aiEngineUrl: string;
constructor(private readonly geminiService: GeminiService) {
this.aiEngineUrl = process.env.AI_ENGINE_URL || 'http://ai-engine:8000';
}
async analyzeMatch(matchId: string): Promise<SingleMatchPredictionPackage> {
let prediction: SingleMatchPredictionPackage;
try {
const response = await axios.post<SingleMatchPredictionPackage>(
`${this.aiEngineUrl}/v20plus/analyze/${matchId}`,
);
prediction = response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const detail = error.response?.data?.detail || error.message;
throw new HttpException(
`AI analyze failed: ${detail}`,
error.response?.status || HttpStatus.SERVICE_UNAVAILABLE,
);
}
throw new HttpException(
'AI analyze failed',
HttpStatus.SERVICE_UNAVAILABLE,
);
}
// Generate AI commentary (non-blocking — fail-safe)
prediction.ai_commentary = await this.generateMatchCommentary(prediction);
return prediction;
}
private async generateMatchCommentary(
prediction: SingleMatchPredictionPackage,
): Promise<string | null> {
if (!this.geminiService.isAvailable()) {
return null;
}
try {
const result = await this.geminiService.generateText(
JSON.stringify(prediction, null, 2),
{
model: 'gemini-2.0-flash',
temperature: 0.7,
maxTokens: 600,
systemPrompt: MATCH_COMMENTARY_SYSTEM_PROMPT,
},
);
return result.text || null;
} catch (error) {
this.logger.warn('AI commentary generation failed, skipping', error);
return null;
}
}
async generateDailyBankoCoupon(
matchIds: string[],
): Promise<SmartCouponResult | null> {
if (matchIds.length === 0) {
return null;
}
return this.getSmartCoupon(matchIds, 'SAFE', {
maxMatches: 2,
minConfidence: 78,
});
}
async getSmartCoupon(
matchIds: string[],
strategy:
| 'SAFE'
| 'BALANCED'
| 'AGGRESSIVE'
| 'VALUE'
| 'MIRACLE' = 'BALANCED',
options: { maxMatches?: number; minConfidence?: number } = {},
): Promise<SmartCouponResult> {
try {
const response = await axios.post<SmartCouponResult>(
`${this.aiEngineUrl}/v20plus/coupon`,
{
match_ids: matchIds,
strategy,
max_matches: options.maxMatches,
min_confidence: options.minConfidence,
},
);
return response.data;
} catch (error) {
this.logger.error('Failed to generate smart coupon', error);
if (axios.isAxiosError(error)) {
const detail = error.response?.data?.detail || error.message;
throw new HttpException(
`Coupon generation failed: ${detail}`,
error.response?.status || HttpStatus.SERVICE_UNAVAILABLE,
);
}
throw new HttpException(
'Coupon generation failed',
HttpStatus.SERVICE_UNAVAILABLE,
);
}
}
}
const MATCH_COMMENTARY_SYSTEM_PROMPT = `Sen uzman bir futbol bahis analistisin. Sana verilen model çıktısını analiz edip kısa, net ve aksiyon odaklı Türkçe bir yorum yaz.
Kurallar:
- Max 3-4 kısa paragraf, gereksiz uzatma
- Playable olan marketleri ve nedenlerini açıkla
- Edge pozitif olan marketleri vurgula (bahisçiden daha iyi biliyoruz)
- Tüm edge'ler negatifse "trap maç" olarak uyar
- xG ve skor senaryolarına göre strateji öner
- Bahis grade'lerini açıkla: A = güvenli, B = iyi, PASS = oynama
- Data quality ve risk seviyesini yorumla (kadro onaylı mı, probable XI mi)
- "Ben olsam..." formatında kişisel tavsiye ver
- Emoji kullan: ⚽ ✅ ⚠️ 🎯 ❌ 💰
- Markdown formatı KULLANMA, düz metin yaz
- Bahis terminolojisi kullan: edge, value, implied odds, xG`;