chatbot : init

This commit is contained in:
morteza-mortezai
2025-11-29 11:46:23 +03:30
parent d0dbde0fef
commit 3ebae01605
20 changed files with 2976 additions and 0 deletions
@@ -0,0 +1,237 @@
import { GoogleGenerativeAI, HarmBlockThreshold, HarmCategory } from "@google/generative-ai";
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { DataContextService } from "./data-context.service";
import { CHATBOT_CONSTANTS } from "../constants/chatbot.constants";
import { IChatContext, IChatbotResponse, ILLMConfig } from "../interfaces/chatbot.interface";
@Injectable()
export class LLMService {
private readonly logger = new Logger(LLMService.name);
private genAI: GoogleGenerativeAI;
private config: ILLMConfig;
constructor(
private configService: ConfigService,
private dataContextService: DataContextService,
) {
this.config = {
model: this.configService.get("GEMINI_MODEL", CHATBOT_CONSTANTS.DEFAULT_MODEL),
temperature: Number(this.configService.get("GEMINI_TEMPERATURE", CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE)),
maxTokens: Number(this.configService.get("GEMINI_MAX_TOKENS", CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS)),
topP: Number(this.configService.get("GEMINI_TOP_P", 0.95)),
apiKey: this.configService.getOrThrow("GEMINI_API_KEY"),
};
this.genAI = new GoogleGenerativeAI(this.config.apiKey);
}
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);
// 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);
}
const response = await result.response;
const botMessage = response.text() || "متأسفم، نمی‌توانم پاسخی تولید کنم. لطفاً سوال خود را دوباره مطرح کنید. 🤖";
// Calculate token usage (approximate)
const tokensUsed = this.estimateTokens(systemInstruction) + this.estimateTokens(message) + this.estimateTokens(botMessage);
return {
message: botMessage,
confidence: this.calculateConfidence(response),
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 || [],
},
};
} catch (error) {
this.logger.error("Failed to generate Gemini 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);
// 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);
}
return this.createAsyncIterableFromStream(streamResult);
} catch (error) {
this.logger.error("Failed to generate streaming Gemini 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): Array<{ role: string; parts: Array<{ text: string }> }> {
if (!context.conversationHistory || context.conversationHistory.length === 0) {
return [];
}
const history = [];
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 }],
});
}
return history;
}
private async *createAsyncIterableFromStream(streamResult: any): 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;
}
}
} 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() || "";
// 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) {
return 0.4;
}
// Check for uncertainty indicators
if (text.includes("I don't know") || text.includes("I'm not sure") || text.includes("uncertain")) {
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")) {
return 0.95;
}
return 0.8;
}
private estimateTokens(text: string): number {
// Rough estimation: 1 token ≈ 4 characters for English text
// Gemini uses similar tokenization to other models
return Math.ceil(text.length / 4);
}
}