@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
405 lines (351 loc) • 11.7 kB
text/typescript
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;
}
}