use oenAi
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
Generated
+192
-328
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
result = await chat.sendMessage(message);
|
||||
} else {
|
||||
// Single message generation
|
||||
result = await model.generateContent(message);
|
||||
}
|
||||
// Build messages array for OpenAI
|
||||
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: systemInstruction,
|
||||
},
|
||||
...conversationHistory,
|
||||
{
|
||||
role: "user",
|
||||
content: message,
|
||||
},
|
||||
];
|
||||
|
||||
const response = await result.response;
|
||||
const botMessage = response.text() || "متأسفم، نمیتوانم پاسخی تولید کنم. لطفاً سوال خود را دوباره مطرح کنید. 🤖";
|
||||
// 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,
|
||||
});
|
||||
|
||||
// Calculate token usage (approximate)
|
||||
const tokensUsed = this.estimateTokens(systemInstruction) + this.estimateTokens(message) + this.estimateTokens(botMessage);
|
||||
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(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");
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user