chatbot : init
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user