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 { 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 { 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 { 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; } }