UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

405 lines (351 loc) 11.7 kB
import { RAGEngine } from "../engine"; import { ConversationContextManager, ConversationConfig, } from "./context-manager"; import { PromptManager, PromptContext } from "./prompt-manager"; import { ChatMessage, SearchResult, ConversationContext } from "../types"; export interface ChatbotConfig { ragEngine: RAGEngine; llmConfig: { apiKey: string; modelName: string; temperature?: number; maxTokens?: number; baseURL?: string; }; conversationConfig?: Partial<ConversationConfig>; defaultPromptTemplate?: string; retrievalConfig?: { topK: number; minScore: number; searchMethod: "hybrid" | "vector" | "keyword"; }; languageDetection?: boolean; } export interface ChatResponse { message: ChatMessage; sources: SearchResult[]; conversationId: string; metadata: { retrievalTime: number; generationTime: number; totalTokens?: number; model: string; template: string; }; } export interface ChatRequest { message: string; conversationId?: string; systemPrompt?: string; promptTemplate?: string; retrievalOptions?: { topK?: number; minScore?: number; searchMethod?: "hybrid" | "vector" | "keyword"; }; userContext?: Record<string, any>; } export class RAGChatbot { private ragEngine: RAGEngine; private contextManager: ConversationContextManager; private promptManager: PromptManager; private config: ChatbotConfig; constructor(config: ChatbotConfig) { this.config = config; this.ragEngine = config.ragEngine; this.contextManager = new ConversationContextManager( config.conversationConfig ); this.promptManager = new PromptManager(); } async chat(request: ChatRequest): Promise<ChatResponse> { const startTime = Date.now(); // Generate conversation ID if not provided const conversationId = request.conversationId || this.generateConversationId(); // Add user message to conversation const userMessage = this.contextManager.addMessage(conversationId, { role: "user", content: request.message, }); try { // Retrieve relevant documents const retrievalStart = Date.now(); const sources = await this.retrieveDocuments(request); const retrievalTime = Date.now() - retrievalStart; // Generate response const generationStart = Date.now(); const assistantMessage = await this.generateResponse( conversationId, request, sources ); const generationTime = Date.now() - generationStart; // Add assistant message to conversation const responseMessage = this.contextManager.addMessage(conversationId, { role: "assistant", content: assistantMessage.content, metadata: { sources, model: this.config.llmConfig.modelName, tokenCount: assistantMessage.metadata?.tokenCount, }, }); return { message: responseMessage, sources, conversationId, metadata: { retrievalTime, generationTime, totalTokens: assistantMessage.metadata?.tokenCount, model: this.config.llmConfig.modelName, template: request.promptTemplate || this.config.defaultPromptTemplate || "rag-default-en", }, }; } catch (error) { // Add error message to conversation const errorMessage = this.contextManager.addMessage(conversationId, { role: "assistant", content: `죄송합니다. 응답을 생성하는 중에 오류가 발생했습니다: ${error}`, metadata: { error: String(error), }, }); throw new Error(`Chat failed: ${error}`); } } private async retrieveDocuments( request: ChatRequest ): Promise<SearchResult[]> { const retrievalOptions = { topK: 5, minScore: 0.1, searchMethod: "hybrid" as const, ...this.config.retrievalConfig, ...request.retrievalOptions, }; return await this.ragEngine.searchWithOptions(request.message, { method: retrievalOptions.searchMethod, k: retrievalOptions.topK, minScore: retrievalOptions.minScore, }); } private async generateResponse( conversationId: string, request: ChatRequest, sources: SearchResult[] ): Promise<ChatMessage> { // Get conversation history (exclude current user message) const conversationHistory = this.contextManager.getConversationHistory(conversationId); const previousMessages = conversationHistory.slice(0, -1); // Remove the just-added user message // Detect language if enabled let language = "en"; if (this.config.languageDetection) { language = this.detectLanguage(request.message); } // Determine prompt template const templateId = request.promptTemplate || this.config.defaultPromptTemplate || `rag-default-${language}`; // Build prompt context const promptContext: PromptContext = { query: request.message, retrievedDocuments: sources, conversationHistory: previousMessages, userContext: request.userContext, language, }; // Generate prompt const prompt = this.promptManager.buildPrompt(templateId, promptContext); // Call LLM (mock implementation for now) const response = await this.callLLM(prompt, request.systemPrompt); return { id: this.generateMessageId(), role: "assistant", content: response.content, timestamp: new Date(), metadata: { sources, model: this.config.llmConfig.modelName, tokenCount: response.tokenCount, }, }; } // Mock LLM call - in production, this would integrate with actual LLM providers private async callLLM( prompt: string, systemPrompt?: string ): Promise<{ content: string; tokenCount: number; }> { // This is a mock implementation // In production, integrate with OpenAI, Anthropic, or other LLM providers const mockResponse = `Based on the provided documents, I can help answer your question. This is a mock response that would be replaced with actual LLM integration using the generated prompt. Prompt used: ${prompt.substring(0, 200)}... System prompt: ${systemPrompt || "None provided"}`; return { content: mockResponse, tokenCount: Math.ceil(mockResponse.length / 4), }; } private detectLanguage(text: string): string { // Simple language detection - in production, use proper language detection library const koreanPattern = /[ㄱ-ㅎ가-힣]/; return koreanPattern.test(text) ? "ko" : "en"; } // Conversation management methods getConversation(conversationId: string): ConversationContext | null { return this.contextManager.getConversation(conversationId); } clearConversation(conversationId: string): boolean { return this.contextManager.clearConversation(conversationId); } getConversationHistory( conversationId: string, lastN?: number ): ChatMessage[] { return this.contextManager.getConversationHistory(conversationId, lastN); } // Stream chat for real-time responses async *streamChat(request: ChatRequest): AsyncGenerator<{ type: "retrieval" | "generation" | "complete"; data: any; }> { const conversationId = request.conversationId || this.generateConversationId(); // Add user message const userMessage = this.contextManager.addMessage(conversationId, { role: "user", content: request.message, }); yield { type: "retrieval", data: { status: "starting" } }; // Retrieve documents const sources = await this.retrieveDocuments(request); yield { type: "retrieval", data: { sources, status: "complete" } }; yield { type: "generation", data: { status: "starting" } }; // For streaming, we'd implement actual streaming LLM calls here const response = await this.generateResponse( conversationId, request, sources ); yield { type: "generation", data: { message: response, status: "complete" }, }; yield { type: "complete", data: { conversationId, sources } }; } // Utility methods private generateConversationId(): string { return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } private generateMessageId(): string { return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // Configuration methods updateConfig(updates: Partial<ChatbotConfig>): void { this.config = { ...this.config, ...updates }; } addPromptTemplate(template: { id: string; name: string; template: string; variables: string[]; description?: string; language?: string; }): void { this.promptManager.addTemplate(template); } // Analytics and monitoring getStats(): { conversations: number; totalMessages: number; ragEngineStats: any; promptTemplates: number; } { const contextStats = this.contextManager.getManagerStats(); const ragStats = this.ragEngine.getSearchStats(); const promptStats = this.promptManager.getTemplateStats(); return { conversations: contextStats.totalConversations, totalMessages: contextStats.totalMessages, ragEngineStats: ragStats, promptTemplates: promptStats.totalTemplates, }; } // Advanced features async summarizeConversation(conversationId: string): Promise<string> { const history = this.getConversationHistory(conversationId); if (history.length === 0) { return "No conversation to summarize."; } // Use the summarization prompt template const documents = history.map((msg) => ({ chunk: { id: msg.id, content: `${msg.role}: ${msg.content}`, metadata: { documentId: conversationId, chunkIndex: 0, startOffset: 0, endOffset: msg.content.length, tokens: Math.ceil(msg.content.length / 4), source: "conversation", }, }, score: 1.0, document: { id: conversationId, content: msg.content, metadata: { title: "Conversation History", createdAt: msg.timestamp, updatedAt: msg.timestamp, fileType: "conversation", fileSize: msg.content.length, }, source: "conversation", }, })); const prompt = this.promptManager.buildPrompt("summarize-documents", { query: "Summarize this conversation", retrievedDocuments: documents, conversationHistory: [], }); const response = await this.callLLM(prompt); return response.content; } async suggestFollowUpQuestions(conversationId: string): Promise<string[]> { const lastMessage = this.getConversationHistory(conversationId, 1)[0]; if (!lastMessage || lastMessage.role !== "assistant") { return []; } const sources = lastMessage.metadata?.sources || []; if (sources.length === 0) { return []; } const prompt = this.promptManager.buildPrompt("generate-questions", { query: "Generate follow-up questions", retrievedDocuments: sources, conversationHistory: [], }); const response = await this.callLLM(prompt); // Parse questions from response (simple implementation) const questions = response.content .split("\n") .filter((line) => line.trim().startsWith("-") || line.match(/^\d+\./)) .map((line) => line.replace(/^[-\d.]\s*/, "").trim()) .filter((q) => q.length > 0); return questions; } }