/**
 * Triple 추출 서비스
 * LLM을 사용하여 Episodic Memory의 observation에서 (subject, predicate, object) 형태의 지식 그래프 트리플을 추출합니다.
 *
 * AriGraph 파이프라인의 핵심 컴포넌트로, Episodic Memory에서 Semantic Memory로의 자동 학습을 지원합니다.
 * RelationExtractor는 규칙/LLM 하이브리드 관계 후보를 담당; 본 서비스는 트리플 텍스트 추출·정규화·지속화 경로를 담당한다.
 *
 * 비용 최적화 전략:
 * - Rate limit (토큰 버킷 알고리즘)
 * - 캐싱 (TTL 기반)
 * - 비용 모니터링
 * - 에러 처리 및 폴백
 */
import OpenAI from 'openai';
import { tripleExtractionLogger } from '../../../../infrastructure/logging/triple-extraction-logger.js';
import { mementoConfig } from '../../../../shared/config/index.js';
import { getRetryOptions } from '../../../../shared/config/retry-options-loader.js';
import { LLMClientInitializer } from '../../../../shared/services/llm-client-initializer.js';
import { logger } from '../../../../shared/utils/logger.js';
import { PromptTemplateLoader } from '../../../../shared/utils/prompt-template-loader.js';
import { TripleCacheService } from '../../../../shared/utils/triple-cache.js';
import { EntityLinker } from './entity-linker.js';
import { PredicateCanonicalizer } from './predicate-canonicalizer.js';
import { classifyTripleExtractionErrorType, shouldRetryTripleLlmError, } from './triple-extraction-errors.js';
import { createTripleLlmUnavailableResponse, invokeTripleProviderRawOutput, logTripleExtractionClientInitResult, resolveTripleParseOrFailure, TRIPLE_EXTRACTION_LLM_UNAVAILABLE_MESSAGE, } from './triple-extraction-llm-pipeline.js';
import { TokenBucketRateLimiter } from './triple-extraction-rate-limiter.js';
import { createTripleExtractionFailureResult, normalizeTripleExtractionResult, trackTripleExtractionSteps, } from './triple-extraction-result-helpers.js';
import { TripleExtractionStatisticsService } from './triple-extraction-statistics.js';
import { TripleNormalizer } from './triple-normalizer.js';
import { TripleParser } from './triple-parser.js';
import { RelationRetryManager } from '../relation-retry-manager.js';
/**
 * Triple 추출 서비스
 */
export class TripleExtractionService {
    openaiClient = null;
    geminiClient = null;
    preferredProvider = null;
    initializedProviders = [];
    cache;
    rateLimiter;
    costMetrics;
    canonicalizer;
    entityLinker;
    parser;
    normalizer;
    statistics;
    retryManager;
    initializationPromise = null;
    DEFAULT_TEMPERATURE = 0.3;
    DEFAULT_MAX_TOKENS = 2000;
    CACHE_SIZE = 100;
    CACHE_TTL_MS = 6 * 60 * 60 * 1000;
    RATE_LIMITER_CAPACITY = 1;
    RATE_LIMITER_REFILL_RATE = 1;
    SUCCESS_SAMPLING_RATE = 0.1;
    constructor(retryManager) {
        this.cache = new TripleCacheService(this.CACHE_SIZE, this.CACHE_TTL_MS);
        this.rateLimiter = new TokenBucketRateLimiter(this.RATE_LIMITER_CAPACITY, this.RATE_LIMITER_REFILL_RATE);
        this.costMetrics = {
            totalCalls: 0,
            totalTokens: 0,
            totalCost: 0,
            lastReset: Date.now(),
        };
        this.canonicalizer = new PredicateCanonicalizer();
        this.entityLinker = new EntityLinker();
        this.parser = new TripleParser();
        this.normalizer = new TripleNormalizer(this.canonicalizer, this.entityLinker);
        this.statistics = new TripleExtractionStatisticsService();
        const retryOptions = getRetryOptions();
        this.retryManager = retryManager ?? new RelationRetryManager({
            maxAttempts: retryOptions.external_api.maxAttempts,
            baseDelay: retryOptions.external_api.baseDelay,
        });
        this.initializationPromise = this.initializeClients();
    }
    getLlmCallDeps() {
        return {
            retryManager: this.retryManager,
            shouldRetry: shouldRetryTripleLlmError,
            onTokenUsage: (provider, promptTokens, completionTokens) => this.updateCostMetrics(provider, promptTokens, completionTokens),
            defaultTemperature: this.DEFAULT_TEMPERATURE,
            defaultMaxTokens: this.DEFAULT_MAX_TOKENS,
        };
    }
    async initializeClients() {
        try {
            const initializer = new LLMClientInitializer();
            const result = await initializer.initialize();
            this.openaiClient = result.openaiClient;
            this.geminiClient = result.geminiClient;
            this.preferredProvider = result.preferredProvider;
            this.initializedProviders = result.initializedProviders;
            logTripleExtractionClientInitResult(result);
        }
        catch (error) {
            logger.error('TripleExtractionService: LLM 클라이언트 초기화 실패', {
                error: error instanceof Error ? error.message : String(error),
            });
            this.openaiClient = null;
            this.geminiClient = null;
            this.preferredProvider = null;
            this.initializedProviders = [];
        }
    }
    async ensureInitialized() {
        if (this.initializationPromise) {
            try {
                await this.initializationPromise;
            }
            catch (error) {
                logger.error('TripleExtractionService: 초기화 대기 실패', {
                    error: error instanceof Error ? error.message : String(error),
                });
            }
            finally {
                this.initializationPromise = null;
            }
        }
    }
    determineProvider(requestedProvider) {
        if (requestedProvider === 'auto') {
            if (this.openaiClient)
                return 'openai';
            if (this.geminiClient)
                return 'gemini';
            return null;
        }
        if (requestedProvider === 'openai' && this.openaiClient) {
            return 'openai';
        }
        if (requestedProvider === 'gemini' && this.geminiClient) {
            return 'gemini';
        }
        if (requestedProvider === 'ollama') {
            if (this.initializedProviders.includes('ollama')) {
                return 'ollama';
            }
        }
        if (requestedProvider === 'openai') {
            if (this.geminiClient)
                return 'gemini';
        }
        else if (requestedProvider === 'gemini') {
            if (this.openaiClient)
                return 'openai';
        }
        else if (requestedProvider === 'ollama') {
            if (this.openaiClient)
                return 'openai';
            if (this.geminiClient)
                return 'gemini';
        }
        return null;
    }
    isAvailable() {
        return this.preferredProvider !== null;
    }
    enqueueExtractionLog(result, memoryId, observation, rawLLMOutput) {
        this.logExtractionResult(result, memoryId, observation, rawLLMOutput).catch((err) => {
            logger.error('TripleExtractionService: 로깅 실패', { error: err });
        });
    }
    async extractTriples(observation, options = {}, memoryId) {
        await this.ensureInitialized();
        const normalizedObservation = observation?.trim() ?? '';
        if (!normalizedObservation) {
            const result = createTripleExtractionFailureResult('no_triple', 'Observation이 비어있습니다.');
            const normalizedResult = this.normalizeExtractionResult(result);
            this.enqueueExtractionLog(normalizedResult, memoryId, normalizedObservation, 'Observation이 비어있습니다.');
            return normalizedResult;
        }
        const extractionStartTime = Date.now();
        const cached = this.cache.get(normalizedObservation);
        if (cached) {
            const normalizedCached = this.normalizeExtractionResult(cached);
            logger.debug('TripleExtractionService: 캐시 히트', {
                contentLength: normalizedObservation.length,
                tripleCount: normalizedCached.triples.length,
            });
            const extractionTime = Date.now() - extractionStartTime;
            this.statistics.recordExtraction(normalizedCached, extractionTime, true, 0, 0, 0);
            return normalizedCached;
        }
        if (mementoConfig.nodeEnv === 'test' && process.env.MEMENTO_ALLOW_LLM_IN_TESTS !== 'true') {
            const errorMessage = TRIPLE_EXTRACTION_LLM_UNAVAILABLE_MESSAGE;
            const result = createTripleExtractionFailureResult('llm_unavailable', errorMessage);
            const normalizedResult = this.normalizeExtractionResult(result);
            this.enqueueExtractionLog(normalizedResult, memoryId, normalizedObservation, errorMessage);
            return normalizedResult;
        }
        try {
            const provider = options.provider || this.preferredProvider || 'auto';
            const { result, rawLLMOutput } = await this.extractWithLLM(normalizedObservation, provider, options);
            const normalizedResult = this.normalizeExtractionResult(result);
            this.cache.set(normalizedObservation, normalizedResult);
            const extractionTime = Date.now() - extractionStartTime;
            const costMetrics = this.getCostMetrics();
            const llmCalls = 1;
            const tokens = costMetrics.totalTokens;
            const cost = costMetrics.totalCost;
            this.statistics.recordExtraction(normalizedResult, extractionTime, false, llmCalls, tokens, cost);
            this.enqueueExtractionLog(normalizedResult, memoryId, normalizedObservation, rawLLMOutput);
            return normalizedResult;
        }
        catch (error) {
            const errorType = classifyTripleExtractionErrorType(error);
            const errorMessage = error instanceof Error ? error.message : 'Unknown error';
            logger.error('TripleExtractionService: Triple 추출 실패', {
                error: errorMessage,
                errorType,
                observation: normalizedObservation.substring(0, 100),
                retryable: errorType === 'network' || errorType === 'rate_limit' || errorType === 'timeout',
                immediateFailure: errorType === 'api_key',
            });
            const failureReason = 'llm_api_error';
            const result = createTripleExtractionFailureResult(failureReason, errorMessage);
            const normalizedResult = this.normalizeExtractionResult(result);
            this.enqueueExtractionLog(normalizedResult, memoryId, normalizedObservation, errorMessage);
            return normalizedResult;
        }
    }
    async extractWithLLM(observation, provider, options) {
        const actualProvider = this.determineProvider(provider);
        if (actualProvider === null) {
            logger.error('TripleExtractionService: LLM 서비스 사용 불가능', {
                requestedProvider: provider,
                preferredProvider: this.preferredProvider,
                openaiAvailable: this.openaiClient !== null,
                geminiAvailable: this.geminiClient !== null,
            });
            return createTripleLlmUnavailableResponse();
        }
        await this.rateLimiter.consume();
        const prompt = PromptTemplateLoader.loadAndRender('triple-extraction', {
            observation,
        });
        const deps = this.getLlmCallDeps();
        let rawLLMOutput;
        try {
            rawLLMOutput = await invokeTripleProviderRawOutput({
                actualProvider,
                openaiClient: this.openaiClient,
                geminiClient: this.geminiClient,
                deps,
                prompt,
                options,
            });
        }
        catch (error) {
            const errorType = classifyTripleExtractionErrorType(error);
            const errorMessage = error instanceof Error ? error.message : String(error);
            logger.error('TripleExtractionService: LLM 호출 실패', {
                error: errorMessage,
                errorType,
                provider: actualProvider,
                retryable: errorType === 'network' || errorType === 'rate_limit' || errorType === 'timeout',
            });
            throw error;
        }
        const parsed = resolveTripleParseOrFailure(this.parser, rawLLMOutput);
        if (parsed.ok === false) {
            return { result: parsed.result, rawLLMOutput: parsed.rawLLMOutput };
        }
        const triples = parsed.triples;
        const normalizedTriples = this.normalizer.normalize(triples);
        const steps = trackTripleExtractionSteps(normalizedTriples, this.canonicalizer, this.entityLinker);
        const extractionInfo = {
            steps,
        };
        return {
            result: {
                triples: normalizedTriples,
                extractionInfo,
            },
            rawLLMOutput,
        };
    }
    updateCostMetrics(provider, promptTokens, completionTokens) {
        this.costMetrics.totalCalls += 1;
        this.costMetrics.totalTokens += promptTokens + completionTokens;
        let cost = 0;
        if (provider === 'openai') {
            cost = (promptTokens / 1_000_000) * 0.15 + (completionTokens / 1_000_000) * 0.6;
        }
        else if (provider === 'gemini') {
            cost = (promptTokens / 1_000_000) * 0.075 + (completionTokens / 1_000_000) * 0.3;
        }
        this.costMetrics.totalCost += cost;
    }
    async logExtractionResult(result, memoryId, observation, rawLLMOutput) {
        const isSuccess = result.triples.length > 0;
        const isFailure = !isSuccess || result.extractionInfo.failureReason !== undefined;
        let shouldLog = false;
        if (isFailure) {
            shouldLog = true;
        }
        else if (isSuccess) {
            shouldLog = Math.random() < this.SUCCESS_SAMPLING_RATE;
        }
        if (!shouldLog) {
            return;
        }
        const resultWithRawOutput = {
            ...result,
            extractionInfo: {
                ...result.extractionInfo,
                rawLLMOutput,
            },
        };
        await tripleExtractionLogger.logExtraction(resultWithRawOutput, memoryId, observation);
    }
    normalizeExtractionResult(result) {
        return normalizeTripleExtractionResult(result, (triples) => trackTripleExtractionSteps(triples, this.canonicalizer, this.entityLinker));
    }
    getCostMetrics() {
        return { ...this.costMetrics };
    }
    getStatistics() {
        return this.statistics.getStatistics();
    }
    getFailureReasonStatistics() {
        return this.statistics.getFailureReasonStatistics();
    }
}
//# sourceMappingURL=triple-extraction-service.js.map