Files
shop-api/src/modules/chatbot/providers/llm.service.ts
T
morteza-mortezai 9a8515d7ab chat
2025-11-30 11:52:38 +03:30

224 lines
7.6 KiB
TypeScript

import { inject, injectable } from "inversify";
import OpenAI from "openai";
import { DataContextService } from "./data-context.service";
import { Logger } from "../../../core/logging/logger";
import { IOCTYPES } from "../../../IOC/ioc.types";
import { CHATBOT_CONSTANTS } from "../constants/chatbot.constants";
import { IChatContext, IChatbotResponse, ILLMConfig } from "../interfaces/chatbot.interface";
@injectable()
export class LLMService {
private readonly logger: Logger;
private openai: OpenAI;
private config: ILLMConfig;
constructor(@inject(IOCTYPES.ChatbotDataContextService) private dataContextService: DataContextService) {
this.logger = new Logger("LLMService");
this.config = {
model: process.env.OPENAI_MODEL || "openai/gpt-4o-mini",
temperature: Number(process.env.OPENAI_TEMPERATURE || "0.7"),
maxTokens: Number(process.env.OPENAI_MAX_TOKENS || "2000"),
topP: Number(process.env.OPENAI_TOP_P || "0.95"),
apiKey:
process.env.OPENAI_API_KEY ||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiI2OTFlMzBjOGI1NDRmYzJkY2JlMTQ2MGUiLCJ0eXBlIjoiYWlfa2V5IiwiaWF0IjoxNzYzNTg2MjQ4fQ.XRMe-TVV9FXCMdueI_xbxycBLj7KJEACXYlkxv3QZrE",
};
if (!this.config.apiKey) {
throw new Error("OPENAI_API_KEY is required");
}
const baseURL = process.env.OPENAI_BASE_URL || "https://ai.liara.ir/api/v1/691e30a204c9b93ad278578b";
this.openai = new OpenAI({
apiKey: this.config.apiKey,
baseURL: baseURL,
});
}
async generateResponse(message: string, context: IChatContext): Promise<IChatbotResponse> {
try {
// Get relevant data from database
const relevantData = await this.dataContextService.getRelevantContext(message, context);
// Build system instruction with context
const systemInstruction = this.buildSystemInstruction(relevantData);
// Build conversation history
const conversationHistory = this.buildConversationHistory(context);
// Build messages array for OpenAI
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: "system",
content: systemInstruction,
},
...conversationHistory,
{
role: "user",
content: message,
},
];
// Generate response using OpenAI
const completion = await this.openai.chat.completions.create({
model: this.config.model,
messages,
temperature: this.config.temperature,
max_tokens: this.config.maxTokens,
top_p: this.config.topP,
});
const botMessage =
completion.choices[0]?.message?.content || "متأسفم، نمی‌توانم پاسخی تولید کنم. لطفاً سوال خود را دوباره مطرح کنید. 🤖";
// Calculate token usage from OpenAI response
const tokensUsed = (completion.usage?.total_tokens || 0) + this.estimateTokens(systemInstruction);
return {
message: botMessage,
confidence: this.calculateConfidence(completion),
sources: relevantData.sources,
tokensUsed,
context: {
model: this.config.model,
relevantDataFound: relevantData.data.length > 0,
finishReason: completion.choices[0]?.finish_reason || "unknown",
usage: completion.usage,
},
};
} catch (error) {
this.logger.error("Failed to generate OpenAI response", error);
throw new Error("Failed to generate response from AI service");
}
}
async generateStreamResponse(message: string, context: IChatContext): Promise<AsyncIterable<string>> {
try {
// Get relevant data from database
const relevantData = await this.dataContextService.getRelevantContext(message, context);
// Build system instruction with context
const systemInstruction = this.buildSystemInstruction(relevantData);
// Build conversation history
const conversationHistory = this.buildConversationHistory(context);
// Build messages array for OpenAI
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: "system",
content: systemInstruction,
},
...conversationHistory,
{
role: "user",
content: message,
},
];
// Generate streaming response using OpenAI
const stream = await this.openai.chat.completions.create({
model: this.config.model,
messages,
temperature: this.config.temperature,
max_tokens: this.config.maxTokens,
top_p: this.config.topP,
stream: true,
});
return this.createAsyncIterableFromStream(stream);
} catch (error) {
this.logger.error("Failed to generate streaming OpenAI response", error);
throw new Error("Failed to generate streaming response from AI service");
}
}
private buildSystemInstruction(relevantData: { data: string[]; sources: string[] }): string {
let instruction = CHATBOT_CONSTANTS.SYSTEM_PROMPT;
// Add relevant data context
if (relevantData.data.length > 0) {
instruction += "\n\nRelevant information from our database:\n";
instruction += relevantData.data.map((data, index) => `${index + 1}. ${data}`).join("\n");
instruction += "\n\nUse this information to provide accurate and specific answers.";
}
return instruction;
}
private buildConversationHistory(context: IChatContext): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {
if (!context.conversationHistory || context.conversationHistory.length === 0) {
return [];
}
const history: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [];
const recentHistory = context.conversationHistory
.slice(-CHATBOT_CONSTANTS.MAX_CONVERSATION_HISTORY)
.filter((msg) => msg.type !== "system");
for (const msg of recentHistory) {
history.push({
role: msg.type === "user" ? "user" : "assistant",
content: msg.content,
});
}
return history;
}
private async *createAsyncIterableFromStream(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): AsyncIterable<string> {
try {
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
yield content;
}
}
} catch (error) {
this.logger.error("Error in streaming response", error);
throw new Error("Failed to process streaming response");
}
}
private calculateConfidence(completion: OpenAI.Chat.Completions.ChatCompletion): number {
const text = completion.choices[0]?.message?.content || "";
// Check finish reason - lower confidence if stopped early
const finishReason = completion.choices[0]?.finish_reason;
if (finishReason === "length" || finishReason === "content_filter") {
return 0.4;
}
// Check for uncertainty indicators
if (
text.includes("I don't know") ||
text.includes("I'm not sure") ||
text.includes("uncertain") ||
text.includes("نمی‌دانم") ||
text.includes("مطمئن نیستم")
) {
return 0.3;
}
// Check response length and quality
if (text.length < 50) {
return 0.6;
}
// Check if response uses provided data
if (text.includes("based on") || text.includes("according to") || text.includes("بر اساس") || text.includes("طبق")) {
return 0.95;
}
return 0.8;
}
private estimateTokens(text: string): number {
// Rough estimation: 1 token ≈ 4 characters for English text
// OpenAI uses similar tokenization to other models
return Math.ceil(text.length / 4);
}
}