1
0
forked from Yara724/api
Files
yara724-api/src/ai/ai.service.ts
2026-05-26 16:35:13 +03:30

206 lines
6.0 KiB
TypeScript

import { createReadStream, existsSync } from "node:fs";
import { join } from "node:path";
import {
HttpException,
HttpStatus,
Injectable,
OnModuleInit,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import axios, { AxiosRequestConfig } from "axios";
import * as FormData from "form-data";
@Injectable()
export class AiService implements OnModuleInit {
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: `${this.configService.get<string>("AI_URL_V2")}/auth/login`,
data: {
username: this.configService.get<string>("AI_USERNAME"),
password: this.configService.get<string>("AI_PASSWORD"),
},
timeout: 1000, // TODO: Make this ENV
};
constructor(private readonly configService: ConfigService) {}
private get profileOptions(): AxiosRequestConfig {
return {
method: "GET",
url: `${this.configService.get<string>("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}`,
},
};
}
async onModuleInit() {
try {
const res = await this.login();
if (res?.accessToken) {
this.accessToken = res.accessToken;
await this.getApiKey();
}
} catch (error) {
// Don't prevent app startup if AI service is temporarily unavailable
// The service will attempt to re-authenticate when aiRequestImage is called
// Reset tokens so re-authentication will be attempted
this.accessToken = null;
this.apiKey = null;
}
}
private async login() {
const loginResponse = await axios.request(this.loginOptions);
return loginResponse.data;
}
private async getApiKey() {
const profileResponse = await axios.request(this.profileOptions);
this.apiKey = profileResponse.data.apiKey.key;
return this.apiKey;
}
public async aiRequestImage(file: {
path: string;
fileName?: string;
}): Promise<any> {
// Ensure authentication is set up
if (!this.accessToken || !this.apiKey) {
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) {
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(/^\.\//, ""));
// Check if file exists
if (!existsSync(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(),
};
const fs = require("fs");
const stats = fs.statSync(filePath);
const response = await axios.request({
...this.imageProcessOptions,
headers: requestHeaders,
data: form,
maxContentLength: Infinity,
maxBodyLength: Infinity,
});
// Validate response structure
if (!response.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) {
throw new HttpException(
`AI Service error: ${response.data.error}`,
HttpStatus.BAD_GATEWAY,
);
}
// Check for processed image (downloadLink)
if (!response.data.downloadLink) {
throw new HttpException(
"AI Service did not return processed image (downloadLink missing)",
HttpStatus.BAD_GATEWAY,
);
}
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";
}
// Re-throw with detailed error information
throw new HttpException(
`[${errorSource}] ${errorMessage}`,
er.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}