forked from Yara724/api
274 lines
9.9 KiB
TypeScript
274 lines
9.9 KiB
TypeScript
import { createReadStream, existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import {
|
|
HttpException,
|
|
HttpStatus,
|
|
Injectable,
|
|
Logger,
|
|
OnModuleInit,
|
|
} from "@nestjs/common";
|
|
import axios, { AxiosRequestConfig } from "axios";
|
|
import * as FormData from "form-data";
|
|
|
|
@Injectable()
|
|
export class AiService implements OnModuleInit {
|
|
private readonly logger = new Logger(AiService.name);
|
|
private apiKey: string;
|
|
private accessToken: string = null;
|
|
|
|
// These configurations are for authentication and getting the API key.
|
|
private readonly loginOptions: AxiosRequestConfig = {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
url: `${process.env.AI_URL_V2}/auth/login`,
|
|
data: {
|
|
username: process.env.AI_USERNAME,
|
|
password: process.env.AI_PASSWORD,
|
|
},
|
|
timeout: 30000, // 30 second timeout
|
|
};
|
|
|
|
private get profileOptions(): AxiosRequestConfig {
|
|
return {
|
|
method: "GET",
|
|
url: `${process.env.AI_URL_V2}/auth/profile`,
|
|
headers: {
|
|
Authorization: `Bearer ${this.accessToken}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
// This getter dynamically creates the base options for the image processing request.
|
|
private get imageProcessOptions(): AxiosRequestConfig {
|
|
return {
|
|
method: "POST",
|
|
url: `${process.env.AI_URL_V2}/services/car-damage/detector?version=ai-v7`,
|
|
headers: {
|
|
Authorization: `Bearer ${this.accessToken}`,
|
|
"gateway-api-key": `${this.apiKey}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
constructor() {}
|
|
|
|
async onModuleInit() {
|
|
try {
|
|
const res = await this.login();
|
|
if (res?.accessToken) {
|
|
this.logger.verbose("AI Service Authenticated Successfully.");
|
|
this.accessToken = res.accessToken;
|
|
await this.getApiKey();
|
|
this.logger.log("AI Service initialized and ready.");
|
|
} else {
|
|
this.logger.warn(
|
|
"AI Service Unavailable: Login did not return an access token. Will retry on first request.",
|
|
);
|
|
}
|
|
} catch (error) {
|
|
// Don't prevent app startup if AI service is temporarily unavailable
|
|
// The service will attempt to re-authenticate when aiRequestImage is called
|
|
this.logger.warn(
|
|
"AI Service Unavailable: Failed during initial login. Will retry on first request.",
|
|
);
|
|
this.logger.warn(`Error: ${error.message}`);
|
|
// Reset tokens so re-authentication will be attempted
|
|
this.accessToken = null;
|
|
this.apiKey = null;
|
|
}
|
|
}
|
|
|
|
private async login() {
|
|
try {
|
|
const loginResponse = await axios.request(this.loginOptions);
|
|
return loginResponse.data;
|
|
} catch (err) {
|
|
const errorMessage = err.response?.data?.message || err.message || "Unknown error";
|
|
const statusCode = err.response?.status || 500;
|
|
this.logger.error(`AI login failed: ${errorMessage} (Status: ${statusCode})`);
|
|
if (err.response?.data) {
|
|
this.logger.error(`AI login error details: ${JSON.stringify(err.response.data, null, 2)}`);
|
|
}
|
|
throw new HttpException(
|
|
`Could not authenticate with AI service: ${errorMessage}`,
|
|
statusCode >= 400 && statusCode < 500 ? statusCode : HttpStatus.UNAUTHORIZED,
|
|
);
|
|
}
|
|
}
|
|
|
|
private async getApiKey() {
|
|
try {
|
|
const profileResponse = await axios.request(this.profileOptions);
|
|
this.apiKey = profileResponse.data.apiKey.key;
|
|
this.logger.log("Successfully retrieved AI gateway API key.");
|
|
return this.apiKey;
|
|
} catch (err) {
|
|
this.logger.error("Failed to retrieve AI API key:", err.message);
|
|
throw new HttpException(
|
|
"Could not get API key from AI service",
|
|
HttpStatus.FAILED_DEPENDENCY,
|
|
);
|
|
}
|
|
}
|
|
|
|
public async aiRequestImage(file: { path: string; fileName?: string }): Promise<any> {
|
|
// Ensure authentication is set up
|
|
if (!this.accessToken || !this.apiKey) {
|
|
this.logger.warn("AI service not authenticated, attempting to re-authenticate...");
|
|
try {
|
|
const res = await this.login();
|
|
if (res?.accessToken) {
|
|
this.accessToken = res.accessToken;
|
|
await this.getApiKey();
|
|
} else {
|
|
throw new HttpException(
|
|
"AI Service authentication failed",
|
|
HttpStatus.UNAUTHORIZED,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
this.logger.error("Failed to re-authenticate AI service:", error.message);
|
|
throw new HttpException(
|
|
"AI Service authentication failed",
|
|
HttpStatus.UNAUTHORIZED,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Resolve relative paths to absolute paths
|
|
const filePath = file.path.startsWith("/")
|
|
? file.path
|
|
: join(process.cwd(), file.path.replace(/^\.\//, ""));
|
|
|
|
this.logger.log(`Processing AI image request for: ${filePath}`);
|
|
|
|
// Check if file exists
|
|
if (!existsSync(filePath)) {
|
|
this.logger.error(`File not found at path: ${filePath}`);
|
|
throw new HttpException(
|
|
`File not found: ${file.path}`,
|
|
HttpStatus.NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
const form = new FormData();
|
|
const fileStream = createReadStream(filePath);
|
|
|
|
// Append file with filename if available
|
|
if (file.fileName) {
|
|
form.append("images", fileStream, file.fileName);
|
|
} else {
|
|
// Extract filename from path if not provided
|
|
const pathParts = filePath.split("/");
|
|
const extractedFileName = pathParts[pathParts.length - 1];
|
|
form.append("images", fileStream, extractedFileName);
|
|
}
|
|
|
|
try {
|
|
const requestHeaders = {
|
|
...this.imageProcessOptions.headers,
|
|
...form.getHeaders(),
|
|
};
|
|
|
|
this.logger.log(`[STEP 1/4] Sending request to AI service: ${this.imageProcessOptions.url}`);
|
|
this.logger.log(`[STEP 1/4] File: ${filePath}, Filename: ${file.fileName || 'extracted from path'}`);
|
|
this.logger.log(`[STEP 1/4] FormData Content-Type: ${form.getHeaders()['content-type']}`);
|
|
this.logger.log(`[STEP 1/4] Authorization header present: ${!!requestHeaders.Authorization}`);
|
|
this.logger.log(`[STEP 1/4] Gateway API key present: ${!!this.apiKey}`);
|
|
this.logger.log(`[STEP 1/4] Request method: POST`);
|
|
this.logger.log(`[STEP 1/4] FormData field name: "images"`);
|
|
|
|
// Get file stats for debugging
|
|
const fs = require('fs');
|
|
const stats = fs.statSync(filePath);
|
|
this.logger.log(`[STEP 1/4] File size: ${stats.size} bytes`);
|
|
this.logger.log(`[STEP 1/4] File exists: ${existsSync(filePath)}`);
|
|
|
|
const response = await axios.request({
|
|
...this.imageProcessOptions,
|
|
headers: requestHeaders,
|
|
data: form,
|
|
maxContentLength: Infinity,
|
|
maxBodyLength: Infinity,
|
|
});
|
|
|
|
this.logger.log(`[STEP 2/4] Successfully received response from AI service (Status: ${response.status})`);
|
|
|
|
// Validate response structure
|
|
if (!response.data) {
|
|
this.logger.error(`[ERROR] AI response is empty or missing data`);
|
|
throw new HttpException(
|
|
"AI Service returned empty response",
|
|
HttpStatus.BAD_GATEWAY,
|
|
);
|
|
}
|
|
|
|
// Check for error in response first (AI service returns 201 with error in body)
|
|
if (response.data.error) {
|
|
this.logger.error(`[ERROR] AI service returned an error in response body`);
|
|
this.logger.error(`[ERROR] Error message: ${response.data.error}`);
|
|
this.logger.error(`[ERROR] Full response: ${JSON.stringify(response.data, null, 2)}`);
|
|
throw new HttpException(
|
|
`AI Service error: ${response.data.error}`,
|
|
HttpStatus.BAD_GATEWAY,
|
|
);
|
|
}
|
|
|
|
// Check for processed image (downloadLink)
|
|
if (!response.data.downloadLink) {
|
|
this.logger.error(`[ERROR] AI response missing processed image (downloadLink)`);
|
|
this.logger.error(`[ERROR] Response structure: ${JSON.stringify(Object.keys(response.data))}`);
|
|
this.logger.error(`[ERROR] Full response: ${JSON.stringify(response.data, null, 2)}`);
|
|
throw new HttpException(
|
|
"AI Service did not return processed image (downloadLink missing)",
|
|
HttpStatus.BAD_GATEWAY,
|
|
);
|
|
}
|
|
|
|
// Check for reports
|
|
if (!response.data.reports) {
|
|
this.logger.warn(`[WARNING] AI response missing reports object, but downloadLink exists`);
|
|
this.logger.warn(`[WARNING] Response keys: ${JSON.stringify(Object.keys(response.data))}`);
|
|
}
|
|
|
|
this.logger.log(`[STEP 3/4] Validated AI response - downloadLink: ${response.data.downloadLink ? 'present' : 'missing'}, reports: ${response.data.reports ? 'present' : 'missing'}`);
|
|
|
|
return response.data;
|
|
} catch (er) {
|
|
// Determine error source
|
|
let errorSource = "UNKNOWN";
|
|
let errorMessage = er.message;
|
|
let errorDetails = "No error details available";
|
|
|
|
if (er.response) {
|
|
errorSource = "AI_SERVICE_RESPONSE";
|
|
errorMessage = er.response?.data?.message || er.message || `HTTP ${er.response.status}`;
|
|
errorDetails = er.response?.data
|
|
? JSON.stringify(er.response.data, null, 2)
|
|
: `Status: ${er.response.status}, StatusText: ${er.response.statusText}`;
|
|
} else if (er.request) {
|
|
errorSource = "NETWORK_ERROR";
|
|
errorMessage = "Network error - AI service did not respond";
|
|
errorDetails = "Request was made but no response received";
|
|
} else {
|
|
errorSource = "REQUEST_SETUP_ERROR";
|
|
errorMessage = er.message || "Error setting up request";
|
|
}
|
|
|
|
this.logger.error(`[ERROR] AI request failed - Source: ${errorSource}`);
|
|
this.logger.error(`[ERROR] File path: ${filePath}`);
|
|
this.logger.error(`[ERROR] Error message: ${errorMessage}`);
|
|
this.logger.error(`[ERROR] Error details: ${errorDetails}`);
|
|
if (er.stack) {
|
|
this.logger.error(`[ERROR] Stack trace: ${er.stack}`);
|
|
}
|
|
|
|
// Re-throw with detailed error information
|
|
throw new HttpException(
|
|
`[${errorSource}] ${errorMessage}`,
|
|
er.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
}
|