use oenAi

This commit is contained in:
morteza-mortezai
2025-11-29 19:57:25 +03:30
parent 8987cf3777
commit 10f925d6b2
6 changed files with 286 additions and 449 deletions
+1 -1
View File
@@ -31,7 +31,7 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.691.0", "@aws-sdk/client-s3": "^3.691.0",
"@faker-js/faker": "^7.6.0", "@faker-js/faker": "^7.6.0",
"@google/generative-ai": "^0.21.0", "openai": "^4.0.0",
"@nestjs/mapped-types": "^2.0.6", "@nestjs/mapped-types": "^2.0.6",
"ansi-colors": "^4.1.3", "ansi-colors": "^4.1.3",
"axios": "^1.7.7", "axios": "^1.7.7",
+192 -328
View File
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -102,14 +102,11 @@ class ChatbotController extends BaseController {
res.end(); res.end();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res.write( res.write(`data: ${JSON.stringify({ type: "error", data: "خطا در تولید پاسخ هوشمند. لطفاً دوباره تلاش کنید ⚠️" })}\n\n`);
`data: ${JSON.stringify({ type: "error", data: "خطا در تولید پاسخ هوشمند. لطفاً دوباره تلاش کنید ⚠️" })}\n\n`,
);
res.end(); res.end();
} }
} }
@ApiOperation("Close a chat session") @ApiOperation("Close a chat session")
@ApiResponse("Chat session closed successfully") @ApiResponse("Chat session closed successfully")
@ApiParam("sessionId", "Session ID", true) @ApiParam("sessionId", "Session ID", true)
@@ -1,5 +1,5 @@
export const CHATBOT_CONSTANTS = { export const CHATBOT_CONSTANTS = {
DEFAULT_MODEL: "gemini-2.0-flash", DEFAULT_MODEL: "gpt-4o",
DEFAULT_TEMPERATURE: 0.7, DEFAULT_TEMPERATURE: 0.7,
DEFAULT_MAX_TOKENS: 2000, DEFAULT_MAX_TOKENS: 2000,
MAX_CONVERSATION_HISTORY: 20, MAX_CONVERSATION_HISTORY: 20,
@@ -26,7 +26,7 @@ export class ChatbotService {
@inject(IOCTYPES.ChatbotChatMessageRepository) private chatMessageRepo: ChatMessageRepository, @inject(IOCTYPES.ChatbotChatMessageRepository) private chatMessageRepo: ChatMessageRepository,
) { ) {
this.logger = new Logger("ChatbotService"); this.logger = new Logger("ChatbotService");
this.logger.info("Using Google Gemini as LLM provider"); this.logger.info("Using OpenAI as LLM provider");
} }
async createChatSession(userId: string, createDto: CreateChatSessionDto) { async createChatSession(userId: string, createDto: CreateChatSessionDto) {
@@ -104,7 +104,7 @@ export class ChatbotService {
// Update session last message time // Update session last message time
await this.chatSessionRepo.updateLastMessageTime(sendDto.sessionId); await this.chatSessionRepo.updateLastMessageTime(sendDto.sessionId);
// Generate bot response asynchronously using original Gemini service // Generate bot response asynchronously using OpenAI service
this.generateBotResponse(sendDto.sessionId, userMessage._id.toString(), userId); this.generateBotResponse(sendDto.sessionId, userMessage._id.toString(), userId);
return this.mapMessageToDto(userMessage); return this.mapMessageToDto(userMessage);
@@ -289,5 +289,4 @@ export class ChatbotService {
tokensUsed: messageObj.tokensUsed, tokensUsed: messageObj.tokensUsed,
}; };
} }
} }
+87 -110
View File
@@ -1,5 +1,5 @@
import { GoogleGenerativeAI, HarmBlockThreshold, HarmCategory } from "@google/generative-ai";
import { inject, injectable } from "inversify"; import { inject, injectable } from "inversify";
import OpenAI from "openai";
import { DataContextService } from "./data-context.service"; import { DataContextService } from "./data-context.service";
import { Logger } from "../../../core/logging/logger"; import { Logger } from "../../../core/logging/logger";
@@ -10,24 +10,26 @@ import { IChatContext, IChatbotResponse, ILLMConfig } from "../interfaces/chatbo
@injectable() @injectable()
export class LLMService { export class LLMService {
private readonly logger: Logger; private readonly logger: Logger;
private genAI: GoogleGenerativeAI; private openai: OpenAI;
private config: ILLMConfig; private config: ILLMConfig;
constructor(@inject(IOCTYPES.ChatbotDataContextService) private dataContextService: DataContextService) { constructor(@inject(IOCTYPES.ChatbotDataContextService) private dataContextService: DataContextService) {
this.logger = new Logger("LLMService"); this.logger = new Logger("LLMService");
this.config = { this.config = {
model: process.env.GEMINI_MODEL || CHATBOT_CONSTANTS.DEFAULT_MODEL, model: process.env.OPENAI_BASE_URL || CHATBOT_CONSTANTS.DEFAULT_MODEL,
temperature: Number(process.env.GEMINI_TEMPERATURE || CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE), temperature: Number(process.env.OPENAI_TEMPERATURE || CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE),
maxTokens: Number(process.env.GEMINI_MAX_TOKENS || CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS), maxTokens: Number(process.env.OPENAI_MAX_TOKENS || CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS),
topP: Number(process.env.GEMINI_TOP_P || "0.95"), topP: Number(process.env.OPENAI_TOP_P || "0.95"),
apiKey: process.env.GEMINI_API_KEY || "", apiKey: process.env.OPENAI_API_KEY || "",
}; };
if (!this.config.apiKey) { if (!this.config.apiKey) {
throw new Error("GEMINI_API_KEY is required"); throw new Error("OPENAI_API_KEY is required");
} }
this.genAI = new GoogleGenerativeAI(this.config.apiKey); this.openai = new OpenAI({
apiKey: this.config.apiKey,
});
} }
async generateResponse(message: string, context: IChatContext): Promise<IChatbotResponse> { async generateResponse(message: string, context: IChatContext): Promise<IChatbotResponse> {
@@ -38,71 +40,52 @@ export class LLMService {
// Build system instruction with context // Build system instruction with context
const systemInstruction = this.buildSystemInstruction(relevantData); const systemInstruction = this.buildSystemInstruction(relevantData);
// Get the generative model with system instruction
const model = this.genAI.getGenerativeModel({
model: this.config.model,
systemInstruction,
generationConfig: {
temperature: this.config.temperature,
maxOutputTokens: this.config.maxTokens,
topP: this.config.topP,
},
safetySettings: [
{
category: HarmCategory.HARM_CATEGORY_HARASSMENT,
threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
},
{
category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
},
{
category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
},
{
category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
},
],
});
// Build conversation history // Build conversation history
const conversationHistory = this.buildConversationHistory(context); const conversationHistory = this.buildConversationHistory(context);
// Generate response using conversation history or direct message // Build messages array for OpenAI
let result; const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
if (conversationHistory.length > 0) { {
// Use chat session for multi-turn conversation role: "system",
const chat = model.startChat({ content: systemInstruction,
history: conversationHistory, },
...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,
}); });
result = await chat.sendMessage(message);
} else {
// Single message generation
result = await model.generateContent(message);
}
const response = await result.response; const botMessage =
const botMessage = response.text() || "متأسفم، نمی‌توانم پاسخی تولید کنم. لطفاً سوال خود را دوباره مطرح کنید. 🤖"; completion.choices[0]?.message?.content ||
"متأسفم، نمی‌توانم پاسخی تولید کنم. لطفاً سوال خود را دوباره مطرح کنید. 🤖";
// Calculate token usage (approximate) // Calculate token usage from OpenAI response
const tokensUsed = this.estimateTokens(systemInstruction) + this.estimateTokens(message) + this.estimateTokens(botMessage); const tokensUsed = (completion.usage?.total_tokens || 0) + this.estimateTokens(systemInstruction);
return { return {
message: botMessage, message: botMessage,
confidence: this.calculateConfidence(response), confidence: this.calculateConfidence(completion),
sources: relevantData.sources, sources: relevantData.sources,
tokensUsed, tokensUsed,
context: { context: {
model: this.config.model, model: this.config.model,
relevantDataFound: relevantData.data.length > 0, relevantDataFound: relevantData.data.length > 0,
finishReason: response.candidates?.[0]?.finishReason || "unknown", finishReason: completion.choices[0]?.finish_reason || "unknown",
safetyRatings: response.candidates?.[0]?.safetyRatings || [], usage: completion.usage,
}, },
}; };
} catch (error) { } catch (error) {
this.logger.error("Failed to generate Gemini response", error); this.logger.error("Failed to generate OpenAI response", error);
throw new Error("Failed to generate response from AI service"); throw new Error("Failed to generate response from AI service");
} }
} }
@@ -115,33 +98,35 @@ export class LLMService {
// Build system instruction with context // Build system instruction with context
const systemInstruction = this.buildSystemInstruction(relevantData); const systemInstruction = this.buildSystemInstruction(relevantData);
// Get the generative model
const model = this.genAI.getGenerativeModel({
model: this.config.model,
systemInstruction,
generationConfig: {
temperature: this.config.temperature,
maxOutputTokens: this.config.maxTokens,
topP: this.config.topP,
},
});
// Build conversation history // Build conversation history
const conversationHistory = this.buildConversationHistory(context); const conversationHistory = this.buildConversationHistory(context);
let streamResult; // Build messages array for OpenAI
if (conversationHistory.length > 0) { const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
const chat = model.startChat({ {
history: conversationHistory, role: "system",
}); content: systemInstruction,
streamResult = await chat.sendMessageStream(message); },
} else { ...conversationHistory,
streamResult = await model.generateContentStream(message); {
} role: "user",
content: message,
},
];
return this.createAsyncIterableFromStream(streamResult); // 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) { } catch (error) {
this.logger.error("Failed to generate streaming Gemini response", error); this.logger.error("Failed to generate streaming OpenAI response", error);
throw new Error("Failed to generate streaming response from AI service"); throw new Error("Failed to generate streaming response from AI service");
} }
} }
@@ -159,65 +144,57 @@ export class LLMService {
return instruction; return instruction;
} }
private buildConversationHistory(context: IChatContext): Array<{ role: string; parts: Array<{ text: string }> }> { private buildConversationHistory(context: IChatContext): OpenAI.Chat.Completions.ChatCompletionMessageParam[] {
if (!context.conversationHistory || context.conversationHistory.length === 0) { if (!context.conversationHistory || context.conversationHistory.length === 0) {
return []; return [];
} }
const history = []; const history: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [];
const recentHistory = context.conversationHistory const recentHistory = context.conversationHistory
.slice(-CHATBOT_CONSTANTS.MAX_CONVERSATION_HISTORY) .slice(-CHATBOT_CONSTANTS.MAX_CONVERSATION_HISTORY)
.filter((msg) => msg.type !== "system"); .filter((msg) => msg.type !== "system");
for (const msg of recentHistory) { for (const msg of recentHistory) {
history.push({ history.push({
role: msg.type === "user" ? "user" : "model", role: msg.type === "user" ? "user" : "assistant",
parts: [{ text: msg.content }], content: msg.content,
}); });
} }
return history; return history;
} }
private async *createAsyncIterableFromStream(streamResult: any): AsyncIterable<string> { private async *createAsyncIterableFromStream(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): AsyncIterable<string> {
try { try {
// For Google Generative AI, the streamResult itself is the async iterable for await (const chunk of stream) {
for await (const chunk of streamResult.stream) { const content = chunk.choices[0]?.delta?.content;
const chunkText = chunk.text(); if (content) {
if (chunkText) { yield content;
yield chunkText;
} }
} }
} catch (error) { } catch (error) {
this.logger.error("Error in streaming response", error); this.logger.error("Error in streaming response", error);
// If the above doesn't work, try alternative approach for older SDK versions
try {
const response = await streamResult.response;
const text = response.text();
if (text) {
yield text;
}
} catch (fallbackError) {
this.logger.error("Fallback streaming approach also failed", fallbackError);
throw new Error("Failed to process streaming response"); throw new Error("Failed to process streaming response");
} }
} }
}
private calculateConfidence(response: any): number { private calculateConfidence(completion: OpenAI.Chat.Completions.ChatCompletion): number {
const text = response.text() || ""; const text = completion.choices[0]?.message?.content || "";
// Check safety ratings - lower confidence if blocked // Check finish reason - lower confidence if stopped early
const safetyRatings = response.candidates?.[0]?.safetyRatings || []; const finishReason = completion.choices[0]?.finish_reason;
const hasHighRiskRatings = safetyRatings.some((rating: any) => rating.probability === "HIGH" || rating.probability === "MEDIUM"); if (finishReason === "length" || finishReason === "content_filter") {
if (hasHighRiskRatings) {
return 0.4; return 0.4;
} }
// Check for uncertainty indicators // Check for uncertainty indicators
if (text.includes("I don't know") || text.includes("I'm not sure") || text.includes("uncertain")) { if (
text.includes("I don't know") ||
text.includes("I'm not sure") ||
text.includes("uncertain") ||
text.includes("نمی‌دانم") ||
text.includes("مطمئن نیستم")
) {
return 0.3; return 0.3;
} }
@@ -227,7 +204,7 @@ export class LLMService {
} }
// Check if response uses provided data // Check if response uses provided data
if (text.includes("based on") || text.includes("according to")) { if (text.includes("based on") || text.includes("according to") || text.includes("بر اساس") || text.includes("طبق")) {
return 0.95; return 0.95;
} }
@@ -236,7 +213,7 @@ export class LLMService {
private estimateTokens(text: string): number { private estimateTokens(text: string): number {
// Rough estimation: 1 token ≈ 4 characters for English text // Rough estimation: 1 token ≈ 4 characters for English text
// Gemini uses similar tokenization to other models // OpenAI uses similar tokenization to other models
return Math.ceil(text.length / 4); return Math.ceil(text.length / 4);
} }
} }