224 lines
7.6 KiB
TypeScript
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);
|
|
}
|
|
}
|