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 { EntityManager } from "@mikro-orm/core"; import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; import { MemoryVectorStore } from "langchain/vectorstores/memory"; import { Company } from "../../companies/entities/company.entity"; import { Industry } from "../../industries/entities/industry.entity"; import { CHATBOT_CONSTANTS } from "../constants/chatbot.constants"; import { IChatContext, IChatbotResponse } from "../interfaces/chatbot.interface"; @Injectable() export class LangChainService implements OnModuleInit { private readonly logger = new Logger(LangChainService.name); private embeddings: GoogleGenerativeAIEmbeddings; private llm: ChatGoogleGenerativeAI; private vectorStore: MemoryVectorStore; private textSplitter: RecursiveCharacterTextSplitter; private isInitialized = false; constructor( private em: EntityManager, private configService: ConfigService, ) { // Initialize Google Gemini components this.embeddings = new GoogleGenerativeAIEmbeddings({ apiKey: this.configService.getOrThrow("GEMINI_API_KEY"), modelName: "embedding-001", // Google's embedding model }); this.llm = new ChatGoogleGenerativeAI({ apiKey: this.configService.getOrThrow("GEMINI_API_KEY"), 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", ".", "!", "?", "؟", "!", ".", " ", ""], }); } async onModuleInit() { await this.initializeVectorStore(); } private async initializeVectorStore() { try { this.logger.log(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.LOADING_DATA); // Load company and industry data for training const documents = await this.loadCompanyAndIndustryDocuments(); 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.log( 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.log(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 loadCompanyAndIndustryDocuments(): Promise { const documents: Document[] = []; const em = this.em.fork(); try { // Load companies data with their products and services const companies = await em.find(Company, { isActive: true, deletedAt: null }, { populate: ["industry", "business", "products", "services"] }); companies.forEach((company) => { // Company basic information using template const companyContent = this.replaceTemplate(CHATBOT_CONSTANTS.COMPANY_DATA_TEMPLATES.COMPANY_INFO, { name: company.name, ceo: company.chiefExecutiveOfficer, email: company.email, phone: company.phone, registrationNumber: company.identificationNumber, establishmentDate: new Date(company.dateOfEstablishment).toLocaleDateString("fa-IR"), address: company.address, website: company.websiteUrl || CHATBOT_CONSTANTS.DEFAULT_VALUES.NO_WEBSITE, description: company.description, industry: company.industry?.title || CHATBOT_CONSTANTS.DEFAULT_VALUES.UNSPECIFIED, status: company.status, business: company.business?.name || CHATBOT_CONSTANTS.DEFAULT_VALUES.UNSPECIFIED, }); documents.push( new Document({ pageContent: companyContent, metadata: { type: "company", id: company.id, name: company.name, industry: company.industry?.title, businessId: company.business?.id, status: company.status, }, }), ); // Company products using template if (company.products && company.products.getItems().length > 0) { company.products.getItems().forEach((product) => { const productContent = this.replaceTemplate(CHATBOT_CONSTANTS.COMPANY_DATA_TEMPLATES.COMPANY_PRODUCT, { companyName: company.name, productTitle: product.title, industry: company.industry?.title || CHATBOT_CONSTANTS.DEFAULT_VALUES.UNSPECIFIED, companyDescription: company.description, companyAddress: company.address, companyPhone: company.phone, companyEmail: company.email, }); documents.push( new Document({ pageContent: productContent, metadata: { type: "product", id: product.id, title: product.title, companyId: company.id, companyName: company.name, industry: company.industry?.title, }, }), ); }); } // Company services using template if (company.services && company.services.getItems().length > 0) { company.services.getItems().forEach((service) => { const serviceContent = this.replaceTemplate(CHATBOT_CONSTANTS.COMPANY_DATA_TEMPLATES.COMPANY_SERVICE, { companyName: company.name, serviceTitle: service.title, industry: company.industry?.title || CHATBOT_CONSTANTS.DEFAULT_VALUES.UNSPECIFIED, companyDescription: company.description, companyAddress: company.address, companyPhone: company.phone, companyEmail: company.email, }); documents.push( new Document({ pageContent: serviceContent, metadata: { type: "service", id: service.id, title: service.title, companyId: company.id, companyName: company.name, industry: company.industry?.title, }, }), ); }); } }); // Load industries data using template const industries = await em.find(Industry, { isActive: true, deletedAt: null }, { populate: ["companies", "business"] }); industries.forEach((industry) => { const companiesInIndustry = industry.companies?.getItems().filter((c) => c.isActive && !c.deletedAt) || []; const industryContent = this.replaceTemplate(CHATBOT_CONSTANTS.COMPANY_DATA_TEMPLATES.INDUSTRY_INFO, { title: industry.title, status: industry.isActive ? CHATBOT_CONSTANTS.DEFAULT_VALUES.ACTIVE : CHATBOT_CONSTANTS.DEFAULT_VALUES.INACTIVE, companiesCount: companiesInIndustry.length.toString(), business: industry.business?.name || CHATBOT_CONSTANTS.DEFAULT_VALUES.UNSPECIFIED, companiesList: companiesInIndustry.map((company) => `- ${company.name}`).join("\n"), }); documents.push( new Document({ pageContent: industryContent, metadata: { type: "industry", id: industry.id, title: industry.title, isActive: industry.isActive, companiesCount: companiesInIndustry.length, businessId: industry.business?.id, }, }), ); }); // 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.log(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.log(CHATBOT_CONSTANTS.LANGCHAIN_MESSAGES.REFRESHING_VECTOR_STORE); await this.initializeVectorStore(); } private estimateTokens(text: string): number { // Rough estimation: 1 token ≈ 4 characters for most languages return Math.ceil(text.length / 4); } /** * Replace placeholders in template with actual values */ private replaceTemplate(template: string, values: Record): string { let result = template; Object.entries(values).forEach(([key, value]) => { result = result.replace(new RegExp(`{${key}}`, "g"), value); }); return result; } get initialized(): boolean { return this.isInitialized; } }