chore: chat bot
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
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<Document[]> {
|
||||
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<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.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, string>): 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user