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": {
"@aws-sdk/client-s3": "^3.691.0",
"@faker-js/faker": "^7.6.0",
"@google/generative-ai": "^0.21.0",
"openai": "^4.0.0",
"@nestjs/mapped-types": "^2.0.6",
"ansi-colors": "^4.1.3",
"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();
} catch (error) {
console.error(error);
res.write(
`data: ${JSON.stringify({ type: "error", data: "خطا در تولید پاسخ هوشمند. لطفاً دوباره تلاش کنید ⚠️" })}\n\n`,
);
res.write(`data: ${JSON.stringify({ type: "error", data: "خطا در تولید پاسخ هوشمند. لطفاً دوباره تلاش کنید ⚠️" })}\n\n`);
res.end();
}
}
@ApiOperation("Close a chat session")
@ApiResponse("Chat session closed successfully")
@ApiParam("sessionId", "Session ID", true)
@@ -1,5 +1,5 @@
export const CHATBOT_CONSTANTS = {
DEFAULT_MODEL: "gemini-2.0-flash",
DEFAULT_MODEL: "gpt-4o",
DEFAULT_TEMPERATURE: 0.7,
DEFAULT_MAX_TOKENS: 2000,
MAX_CONVERSATION_HISTORY: 20,
@@ -26,7 +26,7 @@ export class ChatbotService {
@inject(IOCTYPES.ChatbotChatMessageRepository) private chatMessageRepo: ChatMessageRepository,
) {
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) {
@@ -104,7 +104,7 @@ export class ChatbotService {
// Update session last message time
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);
return this.mapMessageToDto(userMessage);
@@ -289,5 +289,4 @@ export class ChatbotService {
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 OpenAI from "openai";
import { DataContextService } from "./data-context.service";
import { Logger } from "../../../core/logging/logger";
@@ -10,24 +10,26 @@ import { IChatContext, IChatbotResponse, ILLMConfig } from "../interfaces/chatbo
@injectable()
export class LLMService {
private readonly logger: Logger;
private genAI: GoogleGenerativeAI;
private openai: OpenAI;
private config: ILLMConfig;
constructor(@inject(IOCTYPES.ChatbotDataContextService) private dataContextService: DataContextService) {
this.logger = new Logger("LLMService");
this.config = {
model: process.env.GEMINI_MODEL || CHATBOT_CONSTANTS.DEFAULT_MODEL,
temperature: Number(process.env.GEMINI_TEMPERATURE || CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE),
maxTokens: Number(process.env.GEMINI_MAX_TOKENS || CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS),
topP: Number(process.env.GEMINI_TOP_P || "0.95"),
apiKey: process.env.GEMINI_API_KEY || "",
model: process.env.OPENAI_BASE_URL || CHATBOT_CONSTANTS.DEFAULT_MODEL,
temperature: Number(process.env.OPENAI_TEMPERATURE || CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE),
maxTokens: Number(process.env.OPENAI_MAX_TOKENS || CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS),
topP: Number(process.env.OPENAI_TOP_P || "0.95"),
apiKey: process.env.OPENAI_API_KEY || "",
};
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> {
@@ -38,71 +40,52 @@ export class LLMService {
// Build system instruction with context
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
const conversationHistory = this.buildConversationHistory(context);
// Generate response using conversation history or direct message
let result;
if (conversationHistory.length > 0) {
// Use chat session for multi-turn conversation
const chat = model.startChat({
history: conversationHistory,
// 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,
});
result = await chat.sendMessage(message);
} else {
// Single message generation
result = await model.generateContent(message);
}
const response = await result.response;
const botMessage = response.text() || "متأسفم، نمی‌توانم پاسخی تولید کنم. لطفاً سوال خود را دوباره مطرح کنید. 🤖";
const botMessage =
completion.choices[0]?.message?.content ||
"متأسفم، نمی‌توانم پاسخی تولید کنم. لطفاً سوال خود را دوباره مطرح کنید. 🤖";
// Calculate token usage (approximate)
const tokensUsed = this.estimateTokens(systemInstruction) + this.estimateTokens(message) + this.estimateTokens(botMessage);
// Calculate token usage from OpenAI response
const tokensUsed = (completion.usage?.total_tokens || 0) + this.estimateTokens(systemInstruction);
return {
message: botMessage,
confidence: this.calculateConfidence(response),
confidence: this.calculateConfidence(completion),
sources: relevantData.sources,
tokensUsed,
context: {
model: this.config.model,
relevantDataFound: relevantData.data.length > 0,
finishReason: response.candidates?.[0]?.finishReason || "unknown",
safetyRatings: response.candidates?.[0]?.safetyRatings || [],
finishReason: completion.choices[0]?.finish_reason || "unknown",
usage: completion.usage,
},
};
} 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");
}
}
@@ -115,33 +98,35 @@ export class LLMService {
// Build system instruction with context
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
const conversationHistory = this.buildConversationHistory(context);
let streamResult;
if (conversationHistory.length > 0) {
const chat = model.startChat({
history: conversationHistory,
});
streamResult = await chat.sendMessageStream(message);
} else {
streamResult = await model.generateContentStream(message);
}
// Build messages array for OpenAI
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: "system",
content: systemInstruction,
},
...conversationHistory,
{
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) {
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");
}
}
@@ -159,65 +144,57 @@ export class LLMService {
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) {
return [];
}
const history = [];
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" : "model",
parts: [{ text: msg.content }],
role: msg.type === "user" ? "user" : "assistant",
content: msg.content,
});
}
return history;
}
private async *createAsyncIterableFromStream(streamResult: any): AsyncIterable<string> {
private async *createAsyncIterableFromStream(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): AsyncIterable<string> {
try {
// For Google Generative AI, the streamResult itself is the async iterable
for await (const chunk of streamResult.stream) {
const chunkText = chunk.text();
if (chunkText) {
yield chunkText;
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);
// 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");
}
}
}
private calculateConfidence(response: any): number {
const text = response.text() || "";
private calculateConfidence(completion: OpenAI.Chat.Completions.ChatCompletion): number {
const text = completion.choices[0]?.message?.content || "";
// Check safety ratings - lower confidence if blocked
const safetyRatings = response.candidates?.[0]?.safetyRatings || [];
const hasHighRiskRatings = safetyRatings.some((rating: any) => rating.probability === "HIGH" || rating.probability === "MEDIUM");
if (hasHighRiskRatings) {
// 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")) {
if (
text.includes("I don't know") ||
text.includes("I'm not sure") ||
text.includes("uncertain") ||
text.includes("نمی‌دانم") ||
text.includes("مطمئن نیستم")
) {
return 0.3;
}
@@ -227,7 +204,7 @@ export class LLMService {
}
// 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;
}
@@ -236,7 +213,7 @@ export class LLMService {
private estimateTokens(text: string): number {
// 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);
}
}