This commit is contained in:
2026-04-19 13:23:00 +03:00
parent e4c74025e5
commit 1346924387
25 changed files with 1639 additions and 1076 deletions
+15
View File
@@ -461,6 +461,21 @@ export class AIHealthDto {
@ApiProperty()
predictionServiceReady: boolean;
@ApiProperty({ required: false, default: true })
aiEngineReachable?: boolean;
@ApiProperty({ required: false, enum: ["closed", "open"] })
circuitState?: "closed" | "open";
@ApiProperty({ required: false, default: 0 })
consecutiveFailures?: number;
@ApiProperty({ required: false })
endpoint?: string;
@ApiProperty({ required: false, nullable: true })
detail?: string | null;
}
export * from "./smart-coupon.dto";
+80 -21
View File
@@ -19,11 +19,14 @@ import {
ValueBetDto,
AIHealthDto,
} from "./dto";
import axios, { AxiosError } from "axios";
import { Prisma } from "@prisma/client";
import { FeederService } from "../feeder/feeder.service";
import * as fs from "node:fs";
import * as path from "node:path";
import {
AiEngineClient,
AiEngineRequestError,
} from "../../common/utils/ai-engine-client";
type ConfidenceBand = "HIGH" | "MEDIUM" | "LOW";
@@ -45,6 +48,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(PredictionsService.name);
private queueEvents: QueueEvents | null = null;
private readonly aiEngineUrl: string;
private readonly aiEngineClient: AiEngineClient;
private readonly topLeagueIds = new Set<string>();
private readonly reasonTranslations: Record<string, string> = {
confidence_below_threshold: "Güven eşiğin altında",
@@ -125,6 +129,14 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
"AI_ENGINE_URL",
"http://localhost:8000",
);
this.aiEngineClient = new AiEngineClient({
baseUrl: this.aiEngineUrl,
logger: this.logger,
serviceName: PredictionsService.name,
timeoutMs: 60000,
maxRetries: 2,
retryDelayMs: 750,
});
this.topLeagueIds = this.loadTopLeagueIds();
}
@@ -149,12 +161,50 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
}
}
checkHealth(): Promise<AIHealthDto> {
return Promise.resolve({
status: "healthy",
modelLoaded: true,
predictionServiceReady: true,
});
async checkHealth(): Promise<AIHealthDto> {
const circuit = this.aiEngineClient.getSnapshot();
try {
const response = await this.aiEngineClient.get<{
status?: string;
model_loaded?: boolean;
prediction_service_ready?: boolean;
}>("/health", {
timeout: 5000,
retryCount: 0,
});
return {
status: response.data?.status || "healthy",
modelLoaded: response.data?.model_loaded ?? true,
predictionServiceReady:
response.data?.prediction_service_ready ?? true,
aiEngineReachable: true,
circuitState: circuit.state,
consecutiveFailures: circuit.consecutiveFailures,
endpoint: this.aiEngineUrl,
};
} catch (error: unknown) {
const requestError =
error instanceof AiEngineRequestError
? error
: new AiEngineRequestError("AI health check failed");
return {
status: requestError.isCircuitOpen ? "circuit_open" : "unhealthy",
modelLoaded: false,
predictionServiceReady: false,
aiEngineReachable: false,
circuitState: this.aiEngineClient.getSnapshot().state,
consecutiveFailures:
this.aiEngineClient.getSnapshot().consecutiveFailures,
endpoint: this.aiEngineUrl,
detail:
typeof requestError.detail === "string"
? requestError.detail
: requestError.message,
};
}
}
async getPredictionById(matchId: string): Promise<MatchPredictionDto | null> {
@@ -182,22 +232,21 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
// Direct HTTP mode (no Redis)
try {
const response = await axios.post(
`${this.aiEngineUrl}/v20plus/analyze/${matchId}`,
const response = await this.aiEngineClient.post<MatchPredictionDto>(
`/v20plus/analyze/${matchId}`,
{},
{ timeout: 60000 },
);
return this.enrichPredictionResponse(
response.data as MatchPredictionDto,
matchContext,
);
} catch (e: unknown) {
const error = e as AxiosError<Record<string, unknown>>;
const status = error?.response?.status;
const detail =
error?.response?.data?.detail ||
error?.response?.data ||
error?.message;
const requestError =
e instanceof AiEngineRequestError
? e
: new AiEngineRequestError("AI Engine request failed");
const status = requestError.status;
const detail = requestError.detail || requestError.message;
this.logger.error(
`Direct AI Engine call failed for ${matchId}: status=${status}, detail=${JSON.stringify(detail)}`,
);
@@ -988,14 +1037,18 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
// Direct HTTP mode
try {
const response = await axios.post(
`${this.aiEngineUrl}/smart-coupon`,
const response = await this.aiEngineClient.post(
"/smart-coupon",
{ match_ids: matchIds, strategy, ...options },
{ timeout: 60000 },
);
return response.data;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
} catch (error: unknown) {
const message =
error instanceof AiEngineRequestError
? error.message
: error instanceof Error
? error.message
: String(error);
this.logger.error(`Direct smart coupon call failed: ${message}`);
this.throwAiError(message);
}
@@ -1018,6 +1071,12 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
HttpStatus.BAD_GATEWAY,
);
}
if (message.includes("circuit breaker is open")) {
throw new HttpException(
"AI Engine is temporarily unavailable",
HttpStatus.SERVICE_UNAVAILABLE,
);
}
throw new HttpException(
"Failed to get prediction from AI Engine",
HttpStatus.SERVICE_UNAVAILABLE,