UNPKG

@yk1028-test/ai-chat-supporter

Version:

AI Chat Supporter - TypeScript library for intelligent chat processing with LangChain integration

396 lines 19 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChatSupporterFactory = exports.ChatSupporter = void 0; const index_1 = require("../types/input/index"); const index_2 = require("../types/output/index"); const LangChainOllamaProvider_1 = require("../providers/LangChainOllamaProvider"); const base_1 = require("../personas/base"); const predefined_1 = require("../personas/predefined"); const LangChainRAGManager_1 = require("../rag/LangChainRAGManager"); const templates_1 = require("../prompts/templates"); // ChatSupporter 클래스 (LangChain 통합) class ChatSupporter { constructor(config) { this.isInitialized = false; // 기본값 설정 this.config = { provider: config.provider || 'ollama', providerConfig: { model: 'gemma3:1b', baseUrl: 'http://localhost:11434', temperature: 0.7, maxTokens: 2048, ...config.providerConfig, }, defaultPersona: config.defaultPersona || 'default', defaultLanguage: config.defaultLanguage || 'korean', enableLogging: config.enableLogging !== undefined ? config.enableLogging : true, maxChatRetries: config.maxChatRetries || 5, defaultChatLength: config.defaultChatLength || 50, ragConfig: config.ragConfig, }; // 페르소나 등록 this.setupPersonas(); } // 페르소나 설정 setupPersonas() { // 기본 제공 페르소나들 등록 Object.values(predefined_1.predefinedPersonas).forEach(persona => { base_1.globalPersonaManager.register(persona); }); } // 초기화 async initialize() { try { this.log('ChatSupporter 초기화 시작...'); // Provider 초기화 this.provider = new LangChainOllamaProvider_1.LangChainOllamaProvider({ model: this.config.providerConfig.model, baseUrl: this.config.providerConfig.baseUrl, temperature: this.config.providerConfig.temperature, maxTokens: this.config.providerConfig.maxTokens, }); // RAG 초기화 (설정된 경우) if (this.config.ragConfig) { this.ragManager = new LangChainRAGManager_1.LangChainRAGManager(this.config.ragConfig); this.log('RAG 매니저 초기화 완료'); } this.isInitialized = true; this.log('LangChain 통합 ChatSupporter 초기화 완료'); } catch (error) { this.log(`ChatSupporter 초기화 실패: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error'); throw error; } } // 입력 데이터 처리 async process(input, options = {}) { if (!this.isInitialized) { throw new Error('ChatSupporter가 초기화되지 않았습니다. initialize()를 먼저 호출하세요.'); } // 입력 데이터 검증 if (!index_1.InputDataValidator.validate(input)) { throw new Error('유효하지 않은 입력 데이터입니다.'); } const startTime = new Date(); this.log(`처리 시작 - 타입: ${input.type}, 페르소나: ${options.persona || this.config.defaultPersona}`); try { // Chat 타입이고 outputType이 'chat'인 경우 재시도 로직 사용 if (input.type === 'chat' && options.outputType === 'chat') { return await this.processChatWithRetry(input, options, startTime); } else { // 기본 처리 로직 return await this.processDefault(input, options, startTime); } } catch (error) { this.log(`처리 실패: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error'); throw error; } } // Chat 타입 재시도 로직 (요약 기반) async processChatWithRetry(input, options, startTime) { let retryCount = 0; const maxRetries = this.config.maxChatRetries; let previousResponse = null; // 동적 길이 설정: 옵션에서 지정된 값 또는 기본값 사용 (최솟값: 30) const chatLength = options.chatLength || this.config.defaultChatLength; if (chatLength < 30) { throw new Error(`chatLength는 최솟값 30 이상이어야 합니다. 현재 값: ${chatLength}`); } while (retryCount <= maxRetries) { try { let systemPrompt; if (retryCount === 0) { // 첫 번째 시도: 일반 프롬프트 + 길이 제한 지시 const ragSources = await this.buildRAGContext(input, options); const language = options.language || this.config.defaultLanguage; const basePrompt = await templates_1.ChatPromptBuilder.buildPrompt(input.type, input, options.persona || this.config.defaultPersona, options.outputType, ragSources, language); // 길이 제한 지시 추가 const lengthInstruction = language === 'english' ? `\n\nMax ${chatLength} characters` : `\n\n최대 ${chatLength}자`; systemPrompt = basePrompt + lengthInstruction; } else { // 재시도: 이전 응답을 요약하도록 요청 const language = options.language || this.config.defaultLanguage; if (previousResponse) { const summarizationPrompt = (0, templates_1.getChatSummarizationPrompt)(previousResponse, language, chatLength); systemPrompt = await summarizationPrompt.format({ originalResponse: previousResponse }); } else { // 이전 응답이 없는 경우 (에러 상황) 기존 방식 사용 const ragSources = await this.buildRAGContext(input, options); const basePrompt = await templates_1.ChatPromptBuilder.buildPrompt(input.type, input, options.persona || this.config.defaultPersona, undefined, ragSources, language); const retryPrompt = await (0, templates_1.getChatRetryPrompt)(retryCount - 1, language, chatLength).format({}); systemPrompt = `${basePrompt}\n\n${retryPrompt}`; } } this.log(`Chat 프롬프트 생성 완료 (${retryCount === 0 ? '일반' : '요약'} 시도 ${retryCount + 1}/${maxRetries + 1}) - 타입: ${input.type}, 페르소나: ${options.persona || this.config.defaultPersona}`); // 토큰 제한 적용 const language = options.language || this.config.defaultLanguage; const maxTokens = this.calculateMaxTokens(chatLength, language); this.provider.updateConfig({ maxTokens }); this.log(`토큰 제한 적용: ${maxTokens} (길이: ${chatLength}, 언어: ${language})`); // LLM 응답 생성 const response = await this.provider.invoke(systemPrompt); const trimmedResponse = response.trim(); // max_tokens로 길이가 제한되므로 대부분 성공할 것으로 예상 // 여전히 길이가 초과하는 경우에만 재시도 (20% 여유분 허용) const lengthExceeded = trimmedResponse.length > chatLength * 1.2; if (!lengthExceeded || retryCount >= maxRetries) { // 성공하거나 최대 재시도 도달 const processingTime = Date.now() - startTime.getTime(); const status = lengthExceeded ? '최대 재시도 도달' : '성공'; this.log(`Chat 응답 ${status} (${processingTime}ms, ${retryCount === 0 ? '첫 시도' : `${retryCount}회 요약`}) - 길이: ${trimmedResponse.length}자`); return this.createOutputData(trimmedResponse, input, options, undefined, { retryCount, maxRetries, processingTime, summarized: retryCount > 0, originalLength: retryCount > 0 ? (previousResponse ? previousResponse.length : trimmedResponse.length) : undefined }); } else { // 실패: 길이 초과 - 다음 재시도를 위해 응답 저장 previousResponse = trimmedResponse; retryCount++; this.log(`Chat 응답 길이 초과 (${trimmedResponse.length}자) - 재시도 ${retryCount}/${maxRetries}`, 'warn'); if (retryCount > maxRetries) { // 최대 재시도 횟수 초과 this.log(`Chat 응답 최대 재시도 초과 - 마지막 응답 사용 (${trimmedResponse.length}자, 제한: ${chatLength}자)`, 'error'); const processingTime = Date.now() - startTime.getTime(); return this.createOutputData(trimmedResponse, input, options, undefined, { retryCount: retryCount - 1, maxRetries, processingTime, failed: true, finalLength: trimmedResponse.length, summarized: true }); } // 재시도 계속 (요약 방식) continue; } } catch (error) { this.log(`Chat 처리 중 오류 (시도 ${retryCount + 1}): ${error instanceof Error ? error.message : 'Unknown error'}`, 'error'); retryCount++; if (retryCount > maxRetries) { throw error; } } } throw new Error('Chat 처리 재시도 한계 초과'); } // 기본 처리 로직 async processDefault(input, options, startTime) { // RAG 컨텍스트 구성 const ragSources = await this.buildRAGContext(input, options); // 언어 설정 const language = options.language || this.config.defaultLanguage; // 동적 길이 설정 (chat 타입에만 사용, 최솟값: 30) const chatLength = options.chatLength || this.config.defaultChatLength; if (chatLength < 30) { throw new Error(`chatLength는 최솟값 30 이상이어야 합니다. 현재 값: ${chatLength}`); } // 프롬프트 생성 let systemPrompt = await templates_1.ChatPromptBuilder.buildPrompt(input.type, input, options.persona || this.config.defaultPersona, options.outputType, ragSources, language); // 출력 타입이 chat인 경우 길이 제한 지시 추가 if (options.outputType === 'chat') { const lengthInstruction = language === 'english' ? `\n\nMax ${chatLength} characters` : `\n\n최대 ${chatLength}자`; systemPrompt = systemPrompt + lengthInstruction; } this.log(`프롬프트 생성 완료 (${options.outputType === 'chat' ? `길이 제한 ${chatLength}자 포함` : ''}) - 타입: ${input.type}, 페르소나: ${options.persona || this.config.defaultPersona}`); // 출력 타입이 chat인 경우 토큰 제한 적용 if (options.outputType === 'chat') { const maxTokens = this.calculateMaxTokens(chatLength, language); this.provider.updateConfig({ maxTokens }); this.log(`토큰 제한 적용: ${maxTokens} (길이: ${chatLength}, 언어: ${language})`); } else { // analysis 타입은 기본 토큰 수 사용 this.provider.updateConfig({ maxTokens: this.config.providerConfig.maxTokens }); } // LLM 응답 생성 const response = await this.provider.invoke(systemPrompt); const processingTime = Date.now() - startTime.getTime(); this.log(`처리 완료 (${processingTime}ms) - 타입: ${input.type}`); return this.createOutputData(response.trim(), input, options, ragSources, { retryCount: 0, maxRetries: this.config.maxChatRetries, processingTime }); } // RAG 컨텍스트 구성 async buildRAGContext(input, options) { if (!options.useRAG || !this.ragManager) { return undefined; } try { let searchQuery; switch (input.type) { case 'chat': searchQuery = input.message; break; case 'multichat': searchQuery = input.messages.map(msg => msg.message).join(' '); break; default: searchQuery = ''; } if (searchQuery) { const contextResults = await this.ragManager.buildContext(searchQuery); return contextResults; } } catch (error) { this.log(`RAG 컨텍스트 구성 실패: ${error instanceof Error ? error.message : 'Unknown error'}`, 'warn'); } return undefined; } // OutputData 생성 (재시도 메타데이터 포함) createOutputData(response, input, options, ragSources, retryInfo) { const outputType = options.outputType || 'analysis'; const commonOptions = { persona: options.persona || this.config.defaultPersona, sources: ragSources && ragSources.length > 0 ? ragSources : undefined, processingTime: retryInfo?.processingTime, metadata: { inputType: input.type, ...(retryInfo && { retryCount: retryInfo.retryCount, maxRetries: retryInfo.maxRetries, retryUsed: retryInfo.retryCount > 0, retryFailed: retryInfo.failed || false, summarized: retryInfo.summarized || false, ...(retryInfo.finalLength && { finalLength: retryInfo.finalLength }), ...(retryInfo.originalLength && { originalLength: retryInfo.originalLength }), }) } }; if (outputType === 'chat') { return index_2.OutputDataFactory.createChatOutput(response, commonOptions); } else { // analysis 타입 return index_2.OutputDataFactory.createAnalysisOutput(response, [], // insights 배열 (빈 배열로 초기화) 1.0, // confidence retryInfo?.processingTime, commonOptions.metadata); } } // RAG 문서 추가 async addRAGDocument(filePath) { if (!this.ragManager) { throw new Error('RAG가 설정되지 않았습니다.'); } await this.ragManager.addDocument(filePath); this.log(`RAG 문서 추가됨: ${filePath}`); } // 건강 상태 확인 async healthCheck() { const isHealthy = this.isInitialized && !!this.provider; return { status: isHealthy ? 'healthy' : 'unhealthy', provider: this.config.provider, model: this.config.providerConfig.model, personas: base_1.globalPersonaManager.list().length, chatRetryEnabled: this.config.maxChatRetries > 0, maxChatRetries: this.config.maxChatRetries, defaultChatLength: this.config.defaultChatLength, langchain: true, rag: !!this.ragManager, }; } // 사용 가능한 페르소나 목록 조회 getAvailablePersonas() { return base_1.globalPersonaManager.list(); } // 로그 출력 log(message, level = 'info') { if (!this.config.enableLogging) return; const timestamp = new Date().toISOString(); const prefix = `[${timestamp}] LangChain ChatSupporter ${level.toUpperCase()}:`; switch (level) { case 'error': console.error(`${prefix} ${message}`); break; case 'warn': console.warn(`${prefix} ${message}`); break; default: console.log(`${prefix} ${message}`); } } // 글자 수를 기반으로 max_tokens 계산 calculateMaxTokens(maxLength, language) { // 언어별 토큰-글자 비율 (근사치) const tokenRatios = { korean: 1.8, // 한국어는 글자당 약 1.8 토큰 english: 0.8 // 영어는 글자당 약 0.8 토큰 }; const ratio = tokenRatios[language] || tokenRatios.korean; const calculatedTokens = Math.ceil(maxLength * ratio); // 최소/최대 토큰 수 제한 const minTokens = 10; const maxTokens = 500; return Math.max(minTokens, Math.min(maxTokens, calculatedTokens)); } } exports.ChatSupporter = ChatSupporter; // ChatSupporter 라이브러리 팩토리 클래스 class ChatSupporterFactory { // 기본 ChatSupporter 생성 및 초기화 static async createInitialized(config) { const defaultConfig = { provider: 'ollama', providerConfig: { model: 'gemma3:1b', baseUrl: 'http://localhost:11434', temperature: 0.7, }, defaultPersona: 'default', enableLogging: false, maxChatRetries: 5, defaultChatLength: 50, ...config, }; const chatSupporter = new ChatSupporter(defaultConfig); await chatSupporter.initialize(); return chatSupporter; } // RAG 포함 ChatSupporter 생성 및 초기화 static async createWithRAGInitialized(config) { const ragConfig = { maxDocuments: 5, chunkSize: 1000, ...(config?.ragConfig || {}), }; const fullConfig = { provider: 'ollama', providerConfig: { model: 'gemma3:1b', baseUrl: 'http://localhost:11434', temperature: 0.7, }, defaultPersona: 'expert', enableLogging: false, maxChatRetries: 5, defaultChatLength: 50, ragConfig, ...config, }; const chatSupporter = new ChatSupporter(fullConfig); await chatSupporter.initialize(); return chatSupporter; } } exports.ChatSupporterFactory = ChatSupporterFactory; //# sourceMappingURL=ChatSupporter.js.map