272 lines
9.9 KiB
TypeScript
272 lines
9.9 KiB
TypeScript
import { Document } from "@langchain/core/documents";
|
|
import { StringOutputParser } from "@langchain/core/output_parsers";
|
|
import { PromptTemplate } from "@langchain/core/prompts";
|
|
import { RunnableSequence } from "@langchain/core/runnables";
|
|
import { ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings } from "@langchain/google-genai";
|
|
import { injectable } from "inversify";
|
|
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
|
|
import { MemoryVectorStore } from "langchain/vectorstores/memory";
|
|
|
|
import { CHATBOT_CONSTANTS } from "../constants/chatbot.constants";
|
|
import { IChatContext, IChatbotResponse } from "../interfaces/chatbot.interface";
|
|
import { Logger } from "../../../core/logging/logger";
|
|
|
|
@injectable()
|
|
export class LangChainService {
|
|
private readonly logger: Logger;
|
|
private embeddings: GoogleGenerativeAIEmbeddings;
|
|
private llm: ChatGoogleGenerativeAI;
|
|
private vectorStore: MemoryVectorStore;
|
|
private textSplitter: RecursiveCharacterTextSplitter;
|
|
private isInitialized = false;
|
|
|
|
constructor() {
|
|
this.logger = new Logger("LangChainService");
|
|
const apiKey = process.env.GEMINI_API_KEY;
|
|
if (!apiKey) {
|
|
throw new Error("GEMINI_API_KEY is required");
|
|
}
|
|
|
|
// Initialize Google Gemini components
|
|
this.embeddings = new GoogleGenerativeAIEmbeddings({
|
|
apiKey,
|
|
modelName: "embedding-001", // Google's embedding model
|
|
});
|
|
|
|
this.llm = new ChatGoogleGenerativeAI({
|
|
apiKey,
|
|
model: CHATBOT_CONSTANTS.DEFAULT_MODEL,
|
|
temperature: CHATBOT_CONSTANTS.DEFAULT_TEMPERATURE,
|
|
maxOutputTokens: CHATBOT_CONSTANTS.DEFAULT_MAX_TOKENS,
|
|
});
|
|
|
|
this.textSplitter = new RecursiveCharacterTextSplitter({
|
|
chunkSize: 1000,
|
|
chunkOverlap: 200,
|
|
separators: ["\n\n", "\n", ".", "!", "?", "؟", "!", ".", " ", ""],
|
|
});
|
|
|
|
// Initialize vector store asynchronously
|
|
this.initializeVectorStore().catch((error) => {
|
|
this.logger.error("Failed to initialize vector store", error);
|
|
});
|
|
}
|
|
|
|
private async initializeVectorStore() {
|
|
try {
|
|
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.LOADING_DATA);
|
|
|
|
// Load documents for training
|
|
// TODO: Load actual data from your database models (Product, Category, etc.)
|
|
const documents = await this.loadDocuments();
|
|
|
|
if (documents.length === 0) {
|
|
this.logger.warn(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.NO_DATA_FOUND);
|
|
this.vectorStore = new MemoryVectorStore(this.embeddings);
|
|
this.isInitialized = true;
|
|
return;
|
|
}
|
|
|
|
// Split documents into chunks
|
|
const splitDocs = await this.textSplitter.splitDocuments(documents);
|
|
|
|
this.logger.info(
|
|
CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.DOCUMENTS_SPLIT.replace("{totalDocs}", documents.length.toString()).replace(
|
|
"{chunks}",
|
|
splitDocs.length.toString(),
|
|
),
|
|
);
|
|
|
|
// Create vector store from documents
|
|
this.vectorStore = await MemoryVectorStore.fromDocuments(splitDocs, this.embeddings);
|
|
|
|
this.isInitialized = true;
|
|
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.VECTOR_STORE_READY);
|
|
} catch (error) {
|
|
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.VECTOR_STORE_ERROR, error);
|
|
// Fallback to empty vector store
|
|
this.vectorStore = new MemoryVectorStore(this.embeddings);
|
|
this.isInitialized = true;
|
|
}
|
|
}
|
|
|
|
private async loadDocuments(): Promise<Document[]> {
|
|
const documents: Document[] = [];
|
|
|
|
try {
|
|
// TODO: Load actual data from your database
|
|
// Example:
|
|
// const products = await ProductModel.find({ ... }).limit(100);
|
|
// products.forEach((product) => {
|
|
// documents.push(
|
|
// new Document({
|
|
// pageContent: `Product: ${product.title_fa} - Description: ${product.description}`,
|
|
// metadata: { type: "product", id: product._id.toString() },
|
|
// }),
|
|
// );
|
|
// });
|
|
|
|
// Add comprehensive Farsi guidance documents from constants
|
|
const companyGuidanceDocuments = [
|
|
new Document({
|
|
pageContent: CHATBOT_CONSTANTS.COMPANY_GUIDANCE_DOCUMENTS.COMPANY_GUIDE,
|
|
metadata: {
|
|
type: "company_guide",
|
|
category: "guidance",
|
|
language: "فارسی",
|
|
},
|
|
}),
|
|
new Document({
|
|
pageContent: CHATBOT_CONSTANTS.COMPANY_GUIDANCE_DOCUMENTS.COMPANY_FAQ,
|
|
metadata: {
|
|
type: "company_faq",
|
|
category: "support",
|
|
language: "فارسی",
|
|
},
|
|
}),
|
|
new Document({
|
|
pageContent: CHATBOT_CONSTANTS.COMPANY_GUIDANCE_DOCUMENTS.SEARCH_GUIDE,
|
|
metadata: {
|
|
type: "search_guide",
|
|
category: "tutorial",
|
|
language: "فارسی",
|
|
},
|
|
}),
|
|
new Document({
|
|
pageContent: CHATBOT_CONSTANTS.COMPANY_GUIDANCE_DOCUMENTS.INDUSTRY_GUIDE,
|
|
metadata: {
|
|
type: "industry_guide",
|
|
category: "education",
|
|
language: "فارسی",
|
|
},
|
|
}),
|
|
];
|
|
|
|
documents.push(...companyGuidanceDocuments);
|
|
|
|
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.DOCUMENTS_LOADED.replace("{count}", documents.length.toString()));
|
|
return documents;
|
|
} catch (error) {
|
|
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.LOADING_ERROR, error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async generateResponse(message: string, context: IChatContext): Promise<IChatbotResponse> {
|
|
if (!this.isInitialized) {
|
|
throw new Error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.SERVICE_NOT_INITIALIZED);
|
|
}
|
|
|
|
try {
|
|
// Retrieve relevant documents based on the user's query
|
|
const relevantDocs = await this.vectorStore.similaritySearch(message, 5);
|
|
|
|
// Build context from retrieved documents
|
|
const retrievedContext = relevantDocs.map((doc, index) => `${index + 1}. ${doc.pageContent}`).join("\n\n");
|
|
|
|
// Build conversation history
|
|
const conversationHistory =
|
|
context.conversationHistory
|
|
?.slice(-CHATBOT_CONSTANTS.MAX_CONVERSATION_HISTORY / 4)
|
|
?.map((msg) => {
|
|
const roleLabel = msg.type === "user" ? "کاربر" : msg.type === "bot" ? "ربات" : "سیستم";
|
|
return `${roleLabel}: ${msg.content}`;
|
|
})
|
|
?.join("\n") || "";
|
|
|
|
// Create the prompt template using constants
|
|
const promptTemplate = PromptTemplate.fromTemplate(CHATBOT_CONSTANTS.COMPANY_GUIDANCE_SYSTEM_PROMPT);
|
|
|
|
// Create the runnable sequence
|
|
const chain = RunnableSequence.from([promptTemplate, this.llm, new StringOutputParser()]);
|
|
|
|
// Execute the chain
|
|
const response = await chain.invoke({
|
|
context: retrievedContext,
|
|
conversation_history: conversationHistory,
|
|
question: message,
|
|
});
|
|
|
|
// Calculate approximate token usage
|
|
const tokensUsed = this.estimateTokens(retrievedContext) + this.estimateTokens(message) + this.estimateTokens(response);
|
|
|
|
return {
|
|
message: response,
|
|
confidence: 0.9, // Higher confidence for company guidance
|
|
sources: relevantDocs.map((doc) => {
|
|
const type = doc.metadata.type || "شرکت";
|
|
return CHATBOT_CONSTANTS.SOURCE_TYPE_LABELS[type as keyof typeof CHATBOT_CONSTANTS.SOURCE_TYPE_LABELS] || type;
|
|
}),
|
|
tokensUsed,
|
|
context: {
|
|
model: `${CHATBOT_CONSTANTS.DEFAULT_MODEL}-company-guide`,
|
|
relevantDataFound: relevantDocs.length > 0,
|
|
documentsRetrieved: relevantDocs.length,
|
|
vectorStoreInitialized: this.isInitialized,
|
|
language: "فارسی",
|
|
dataTypes: relevantDocs.map((doc) => doc.metadata.type).filter((type, index, arr) => arr.indexOf(type) === index),
|
|
focus: "company_and_industry_guidance",
|
|
},
|
|
};
|
|
} catch (error) {
|
|
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.RESPONSE_ERROR, error);
|
|
throw new Error(CHATBOT_CONSTANTS.ERROR_MESSAGES.LLM_SERVICE_ERROR);
|
|
}
|
|
}
|
|
|
|
async generateStreamResponse(message: string, context: IChatContext) {
|
|
if (!this.isInitialized) {
|
|
throw new Error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.SERVICE_NOT_INITIALIZED);
|
|
}
|
|
|
|
try {
|
|
// Retrieve relevant documents
|
|
const relevantDocs = await this.vectorStore.similaritySearch(message, 5);
|
|
const retrievedContext = relevantDocs.map((doc, index) => `${index + 1}. ${doc.pageContent}`).join("\n\n");
|
|
|
|
// Build conversation history
|
|
const conversationHistory =
|
|
context.conversationHistory
|
|
?.slice(-CHATBOT_CONSTANTS.MAX_CONVERSATION_HISTORY / 4)
|
|
?.map((msg) => {
|
|
const roleLabel = msg.type === "user" ? "کاربر" : msg.type === "bot" ? "ربات" : "سیستم";
|
|
return `${roleLabel}: ${msg.content}`;
|
|
})
|
|
?.join("\n") || "";
|
|
|
|
// Create prompt template using constants
|
|
const promptTemplate = PromptTemplate.fromTemplate(CHATBOT_CONSTANTS.COMPANY_GUIDANCE_SYSTEM_PROMPT);
|
|
|
|
// Create streaming chain
|
|
const chain = RunnableSequence.from([promptTemplate, this.llm, new StringOutputParser()]);
|
|
|
|
// Return async iterable for streaming
|
|
const stream = await chain.stream({
|
|
context: retrievedContext,
|
|
conversation_history: conversationHistory,
|
|
question: message,
|
|
});
|
|
|
|
return stream;
|
|
} catch (error) {
|
|
this.logger.error(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.STREAM_RESPONSE_ERROR, error);
|
|
throw new Error(CHATBOT_CONSTANTS.ERROR_MESSAGES.LLM_SERVICE_ERROR);
|
|
}
|
|
}
|
|
|
|
async refreshVectorStore(): Promise<void> {
|
|
this.logger.info(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.REFRESHING_VECTOR_STORE);
|
|
this.isInitialized = false;
|
|
await this.initializeVectorStore();
|
|
}
|
|
|
|
private estimateTokens(text: string): number {
|
|
// Rough estimation: 1 token ≈ 4 characters for most languages
|
|
return Math.ceil(text.length / 4);
|
|
}
|
|
|
|
get initialized(): boolean {
|
|
return this.isInitialized;
|
|
}
|
|
}
|