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 { 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> { 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): AsyncIterable { 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); } }