@yk1028-test/ai-chat-supporter
Version:
AI Chat Supporter - TypeScript library for intelligent chat processing with LangChain integration
396 lines • 19 kB
JavaScript
;
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