"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertMCPToolToLangChain = exports.createGetEvaluationAudioLangChainTool = exports.createClickhouseTextToSqlLangChainTool = exports.convertToolToLangChain = exports.appendDebugToolsAndFilters = exports.buildNoToolCallsAnswer = exports.createTokenUsageCallbackForAgent = exports.truncateMessage = exports.estimateTokens = exports.truncateToolResult = void 0;
const zod_1 = require("zod");
const messages_1 = require("@langchain/core/messages");
const tools_1 = require("@langchain/core/tools");
const logger_1 = __importDefault(require("../../../../logger"));
const clickhouseTextToSqlTool_1 = require("../tools/clickhouseTextToSqlTool");
const systemPrompt_1 = require("../utils/systemPrompt");
const pulseEvaluationService_1 = require("../../pulseEvaluationService");
const normalizeText = (value) => String(value || '')
    .toLowerCase()
    .replace(/[^a-z0-9\s._-]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
const parsePositiveInt = (v) => {
    if (typeof v === 'number' && Number.isFinite(v) && v > 0) {
        return Math.trunc(v);
    }
    if (typeof v === 'string' && /^\d+$/.test(v.trim())) {
        const n = Number.parseInt(v.trim(), 10);
        return Number.isFinite(n) && n > 0 ? n : null;
    }
    return null;
};
const asksEvaluationRunSummaryQuestion = (q) => {
    const n = normalizeText(q);
    return (/\bsummari/.test(n) ||
        (/\bevaluation\b/.test(n) && /\brun\b/.test(n)) ||
        (/\bfailures?\b/.test(n) && /\brun\b/.test(n)) ||
        (/\btest\b/.test(n) && /\brun\b/.test(n) && /\b(highlights|overview|outcome|fail|error|pass)\b/.test(n)));
};
/**
 * When SQL returns one summary row with error_count > 0, steer the agent away from
 * "all component checks passed" narratives driven by empty component breakdown columns.
 */
const buildRunSummaryNarrationHintsForSqlRows = (rows, userQuestion) => {
    if (!Array.isArray(rows) || rows.length === 0 || !asksEvaluationRunSummaryQuestion(userQuestion)) {
        return [];
    }
    const row = rows[0];
    if (!row || typeof row !== 'object' || Array.isArray(row)) {
        return [];
    }
    const r = row;
    const err = parsePositiveInt(r.error_count) ?? parsePositiveInt(r.errorCount);
    if (err === null) {
        return [];
    }
    return [
        `This row includes **error_count = ${err}** (execution/infrastructure failures; typically eval_results.failure_reason = 2, often with null grading_result). **Lead your answer with these errors**—they are real failures for the user. Do **not** say there were no failures or that all detailed checks passed **only** because component-level fields (e.g. top_failures / groupArray / ARRAY JOIN pass=false slices) are empty or unremarkable.`,
    ];
};
const levenshteinDistance = (a, b) => {
    const aa = String(a);
    const bb = String(b);
    const m = aa.length;
    const n = bb.length;
    if (m === 0) {
        return n;
    }
    if (n === 0) {
        return m;
    }
    const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
    for (let i = 0; i <= m; i += 1) {
        dp[i][0] = i;
    }
    for (let j = 0; j <= n; j += 1) {
        dp[0][j] = j;
    }
    for (let i = 1; i <= m; i += 1) {
        for (let j = 1; j <= n; j += 1) {
            const cost = aa[i - 1] === bb[j - 1] ? 0 : 1;
            dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
        }
    }
    return dp[m][n];
};
const hasFuzzyTermMatch = (text, terms, maxEditDistance = 1) => {
    const normalized = normalizeText(text);
    if (!normalized) {
        return false;
    }
    for (const term of terms) {
        const normalizedTerm = normalizeText(term);
        if (!normalizedTerm) {
            continue;
        }
        if (normalized.includes(normalizedTerm)) {
            return true;
        }
        const tokens = normalized.split(' ');
        for (const token of tokens) {
            if (token.length < 4 || normalizedTerm.length < 4) {
                continue;
            }
            if (Math.abs(token.length - normalizedTerm.length) > maxEditDistance) {
                continue;
            }
            if (levenshteinDistance(token, normalizedTerm) <= maxEditDistance) {
                return true;
            }
        }
    }
    return false;
};
const evaluateSpecializedToolApplicability = (toolName, query, historyText) => {
    if (toolName !== 'get_evaluation_audio') {
        return { allowed: true };
    }
    const mediaTerms = [
        'audio',
        'voice',
        'recording',
        'recorded',
        'call',
        'mp3',
        'wav',
        'm4a',
        'aac',
        'ogg',
        'audio url',
    ];
    const retrievalTerms = [
        'url',
        'link',
        'download',
        'listen',
        'play',
        'playback',
        'open',
        'file path',
        'path',
        'attachment',
        'share',
        'send me',
    ];
    const metricTerms = [
        'metric',
        'metrics',
        'assertion',
        'assertions',
        'plugin',
        'plugins',
        'evaluated',
        'evaluation',
        'score',
        'scores',
        'pass',
        'fail',
        'failure',
    ];
    const normalizedQuery = normalizeText(query);
    const normalizedHistory = normalizeText(historyText);
    const hasMediaInQuery = hasFuzzyTermMatch(normalizedQuery, mediaTerms, 2);
    const hasMediaInHistory = hasFuzzyTermMatch(normalizedHistory, mediaTerms, 2) ||
        /\baudiourl\b|audio_url|file\.path|gen_ai\.output\.type['"]?\s*[:=]\s*['"]?audio/i.test(historyText);
    const hasRetrievalIntent = hasFuzzyTermMatch(normalizedQuery, retrievalTerms, 2);
    const asksMetricLikeThing = hasFuzzyTermMatch(normalizedQuery, metricTerms, 1);
    if (hasMediaInQuery && hasRetrievalIntent) {
        return { allowed: true };
    }
    if (hasMediaInHistory && hasRetrievalIntent) {
        return { allowed: true };
    }
    if (asksMetricLikeThing) {
        return {
            allowed: false,
            reason: 'Not applicable: this looks like an evaluation metric/assertion/plugin query. Use ClickHouse telemetry (eval_results/grading_result).',
        };
    }
    return {
        allowed: false,
        reason: 'Not applicable: get_evaluation_audio is only for audio artifact retrieval (URL/playback/download) in an audio context.',
    };
};
/**
 * Truncate/summarize large tool results to prevent token limit errors
 * Keeps only essential fields and limits array sizes
 */
const truncateToolResult = (result) => {
    try {
        const str = typeof result === 'string' ? result : JSON.stringify(result);
        const parsed = JSON.parse(str);
        // If it's an error or summary-only, return as-is
        if (parsed?.error) {
            return str;
        }
        if (parsed?.data?.metadata?.summaryOnly) {
            return str;
        }
        const MAX_TOOL_MESSAGE_CHARS = 100000;
        /**
         * Shrink tool payloads by slicing large arrays (spans/evalResults/traces/etc.)
         * until the JSON-serialized string fits within maxChars.
         *
         * This is intentionally generic across response shapes (e.g.:
         * { traces: [...] }, { spans: [...] }, { data: { traces: [...] } }, etc.).
         */
        const shrinkToFit = (root, maxChars) => {
            if (!root || typeof root !== 'object') {
                return root;
            }
            // We only truncate the high-cardinality, high-payload fields for now.
            // Do NOT truncate top-level container lists like traces[]; instead truncate nested spans/evalResults.
            const TRUNCATABLE_KEYS = new Set(['spans', 'evalResults', 'children', 'nodes', 'edges']);
            const refs = [];
            const seen = new Set();
            const visit = (node, path) => {
                if (!node || typeof node !== 'object') {
                    return;
                }
                if (seen.has(node)) {
                    return;
                }
                seen.add(node);
                if (Array.isArray(node)) {
                    // Traverse a small prefix for nested structures
                    for (let i = 0; i < Math.min(node.length, 3); i++) {
                        visit(node[i], `${path}[${i}]`);
                    }
                    return;
                }
                for (const [k, v] of Object.entries(node)) {
                    const nextPath = path ? `${path}.${k}` : k;
                    if (Array.isArray(v)) {
                        // Only collect arrays we are willing to truncate.
                        // This prevents cases where we drop entire traces[] when a single trace has many spans.
                        if (TRUNCATABLE_KEYS.has(k)) {
                            refs.push({ parent: node, key: k, path: nextPath });
                        }
                        for (let i = 0; i < Math.min(v.length, 3); i++) {
                            visit(v[i], `${nextPath}[${i}]`);
                        }
                    }
                    else if (v && typeof v === 'object') {
                        visit(v, nextPath);
                    }
                }
            };
            const safeStringify = () => {
                try {
                    return JSON.stringify(root);
                }
                catch {
                    return null;
                }
            };
            const initial = safeStringify();
            if (initial && initial.length <= maxChars) {
                return root;
            }
            visit(root, '');
            // If evalResults are present (non-empty), we treat this as an evaluation-focused payload.
            // In that case we preserve evalResults and instead shrink/remove spans to meet size limits.
            const hasEvalResultsData = refs.some((r) => {
                if (r.key !== 'evalResults') {
                    return false;
                }
                const arr = r.parent?.[r.key];
                return Array.isArray(arr) && arr.length > 0;
            });
            const refScore = (r) => {
                const arr = r.parent?.[r.key];
                const len = Array.isArray(arr) ? arr.length : 0;
                // Prefer shrinking spans over evalResults when evalResults are present.
                if (hasEvalResultsData) {
                    return (r.key === 'spans' ? 1000000000 : 0) + len;
                }
                // Otherwise shrink spans first, then evalResults.
                return (r.key === 'spans' ? 1000000000 : 0) + len;
            };
            // Sort so we shrink big/heavy arrays first
            const ordered = refs
                .filter((r) => Array.isArray(r.parent?.[r.key]) && r.parent[r.key].length > 0)
                .sort((a, b) => refScore(b) - refScore(a));
            const markTruncated = (obj) => {
                if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
                    obj.__argus_truncated = true;
                }
            };
            // Aggressively shrink until we fit. Limits are applied in stages.
            // If evalResults are present, we drop spans quickly to preserve evalResults.
            const limits = hasEvalResultsData ? [10, 5, 2, 1, 0] : [50, 20, 10, 5, 2, 1, 0];
            for (const limit of limits) {
                for (const r of ordered) {
                    // If evalResults are present, do not truncate evalResults; instead shrink spans.
                    if (hasEvalResultsData && r.key === 'evalResults') {
                        continue;
                    }
                    const current = r.parent?.[r.key];
                    if (!Array.isArray(current)) {
                        continue;
                    }
                    if (current.length <= limit) {
                        continue;
                    }
                    r.parent[r.key] = current.slice(0, limit);
                    markTruncated(r.parent);
                    markTruncated(root);
                    const ser = safeStringify();
                    if (ser && ser.length <= maxChars) {
                        return root;
                    }
                }
            }
            return root;
        };
        let out = JSON.stringify(parsed);
        if (out.length > MAX_TOOL_MESSAGE_CHARS) {
            const shrunk = shrinkToFit(parsed, MAX_TOOL_MESSAGE_CHARS);
            out = JSON.stringify(shrunk);
        }
        // Final hard cap fallback: NEVER return broken JSON.
        // If we still can't fit, wrap a compact, parseable envelope so the model
        // does not hallucinate due to malformed/partial tool output.
        if (out.length > MAX_TOOL_MESSAGE_CHARS) {
            const previewLen = Math.max(0, MAX_TOOL_MESSAGE_CHARS - 500);
            const envelope = {
                __argus_truncated: true,
                __argus_note: 'Tool result exceeded size limit after structured truncation. Returning a compact preview; full data remains in backend.',
                preview: out.substring(0, previewLen),
            };
            out = JSON.stringify(envelope);
        }
        const approxTokens = out.length / 4;
        logger_1.default.info(`truncateToolResult output length=${out.length}, approxTokens=${approxTokens.toFixed(0)}`);
        return out;
    }
    catch {
        // If we can't parse, at least hard-cap the raw string
        const str = typeof result === 'string' ? result : JSON.stringify(result);
        const MAX_TOOL_MESSAGE_CHARS = 40000;
        let out = str;
        if (out.length > MAX_TOOL_MESSAGE_CHARS) {
            out =
                out.substring(0, MAX_TOOL_MESSAGE_CHARS) +
                    '... [truncated for model input; full data remains in backend]';
        }
        const approxTokens = out.length / 4;
        logger_1.default.info(`truncateToolResult (fallback) output length=${out.length}, approxTokens=${approxTokens.toFixed(0)}`);
        return out;
    }
};
exports.truncateToolResult = truncateToolResult;
// Estimate total token count before invoking (rough estimate: 1 token ≈ 4 characters)
// This is critical to prevent 400 errors from OpenAI
const estimateTokens = (messages) => {
    return messages.reduce((sum, msg) => {
        const content = 'content' in msg ? String(msg.content) : '';
        return sum + content.length;
    }, 0) / 4;
};
exports.estimateTokens = estimateTokens;
// Truncate individual messages if they're too large
const truncateMessage = (msg, maxChars) => {
    if ('content' in msg && typeof msg.content === 'string' && msg.content.length > maxChars) {
        const truncated = msg.content.substring(0, maxChars) + '... [truncated for token limit]';
        if (msg instanceof messages_1.AIMessage) {
            return new messages_1.AIMessage(truncated);
        }
        else if (msg instanceof messages_1.HumanMessage) {
            return new messages_1.HumanMessage(truncated);
        }
        else if (msg instanceof messages_1.SystemMessage) {
            return new messages_1.SystemMessage(truncated);
        }
    }
    return msg;
};
exports.truncateMessage = truncateMessage;
/**
 * Create a LangChain-style callback handler to capture ACTUAL token usage
 * from the underlying LLM and store it on state.metadata.tokenUsage.
 */
const createTokenUsageCallbackForAgent = (state) => {
    return {
        // This is called by LangChain when an LLM run completes
        handleLLMEnd(output) {
            try {
                const usageSource = output?.llmOutput?.tokenUsage ??
                    output?.llmOutput?.usage ??
                    output?.tokenUsage ??
                    output?.usage ??
                    null;
                if (!usageSource) {
                    return;
                }
                const tokenUsage = {
                    total: usageSource.totalTokens ??
                        usageSource.total_tokens ??
                        usageSource.total ??
                        undefined,
                    prompt: usageSource.promptTokens ??
                        usageSource.prompt_tokens ??
                        usageSource.prompt ??
                        undefined,
                    completion: usageSource.completionTokens ??
                        usageSource.completion_tokens ??
                        usageSource.completion ??
                        undefined,
                };
                // Merge into state.metadata.tokenUsage (additive, do not overwrite previous values)
                const existing = state.metadata?.tokenUsage || {};
                const merged = {
                    total: (existing.total ?? 0) +
                        (tokenUsage.total ?? 0),
                    prompt: (existing.prompt ?? 0) +
                        (tokenUsage.prompt ?? 0),
                    completion: (existing.completion ?? 0) +
                        (tokenUsage.completion ?? 0),
                };
                state.metadata = {
                    startTime: state.metadata?.startTime || new Date(),
                    endTime: state.metadata?.endTime,
                    executionPath: state.metadata?.executionPath || [],
                    apiCalls: state.metadata?.apiCalls || [],
                    error: state.metadata?.error,
                    tokenUsage: merged,
                };
                logger_1.default.info(`Argus agent actual token usage: ${JSON.stringify(tokenUsage)}`);
            }
            catch (err) {
                logger_1.default.warn(`Failed to record Argus agent token usage: ${err instanceof Error ? err.message : String(err)}`);
            }
        },
    };
};
exports.createTokenUsageCallbackForAgent = createTokenUsageCallbackForAgent;
/**
 * Handle the case where the agent did not call any tools.
 * Returns a safe, guardrailed answer for greetings / identity questions
 * and a telemetry-not-found message otherwise.
 */
const buildNoToolCallsAnswer = (state) => {
    const rawQuery = String(state.query || '').trim();
    const normalizedQuery = rawQuery.toLowerCase();
    if (String(state.domain || '').toLowerCase() === 'securityconfig') {
        return ('I need to build that draft using the security configuration tool. ' +
            'Please describe the application under test and which vulnerability plugins and attack strategies you want (or ask me to suggest categories), then I will produce the JSON draft.');
    }
    const isShortGreeting = normalizedQuery.length > 0 &&
        normalizedQuery.length <= 40 &&
        /^(hi|hello|hey|hola|sup|yo)[.!?\s]*$/i.test(normalizedQuery);
    const isWhatAreYouQuestion = normalizedQuery.length <= 120 &&
        /who (are|r) you|what (is|are) argus|what can you do|how can you help/i.test(normalizedQuery);
    if (isShortGreeting || isWhatAreYouQuestion) {
        return ('Hi, I am Argus. I help you analyze your Argus telemetry data — traces, spans, sessions, evaluations, and security findings — ' +
            'using the context you provide (like traceId, sessionId, applicationId, or time range). ' +
            'Ask me things like “Summarize this trace”, or “Which spans had the highest cost or latency?”.');
    }
    return ('I could not find any matching Argus telemetry for this request (traces, spans, sessions, evaluations, or security findings). ' +
        'Please verify that the provided identifiers or filters (such as traceId, sessionId, applicationId, or time range) correspond to data stored in Argus.');
};
exports.buildNoToolCallsAnswer = buildNoToolCallsAnswer;
/**
 * Append a debug block with tools used and filters applied to the answer,
 * and return the updated answer plus tool summary metadata.
 */
const appendDebugToolsAndFilters = (answer, apiCalls) => {
    const uniqueTools = Array.from(new Set(apiCalls.map((c) => c.tool).filter((name) => typeof name === 'string' && name)));
    // Feature flag for showing debug block (re-uses your existing condition)
    const includeToolSummary = process.env.AUTH0_CLIENT_ID === '';
    if (!includeToolSummary) {
        return { answer, apiToolsSummary: undefined, apiTools: uniqueTools };
    }
    const apiToolsSummary = uniqueTools.length > 0 ? uniqueTools.join(', ') : 'none';
    const debugLines = [
        '\n\n[QUERY EXECUTED]:',
        `  ${apiCalls.map((c) => `  - ${c.sql}`).join('\n')}\n`,
    ];
    const updatedAnswer = `${answer}\n\n${debugLines.join('\n')}`;
    return { answer: updatedAnswer, apiToolsSummary, apiTools: uniqueTools };
};
exports.appendDebugToolsAndFilters = appendDebugToolsAndFilters;
/**
 * Convert custom Tool to LangChain StructuredTool
 */
const convertToolToLangChain = (customTool, projectId, orgId, stateFilters, apiCalls) => {
    return (0, tools_1.tool)(async (input) => {
        try {
            // Merge state filters with input filters (state filters take precedence)
            const inputFilters = input &&
                typeof input.filters === 'object' &&
                input.filters !== null &&
                !Array.isArray(input.filters)
                ? input.filters
                : {};
            const safeStateFilters = stateFilters && typeof stateFilters === 'object' && !Array.isArray(stateFilters)
                ? stateFilters
                : {};
            const mergedFilters = { ...inputFilters, ...safeStateFilters };
            const normalizedStateFilters = stateFilters && typeof stateFilters === 'object' && !Array.isArray(stateFilters)
                ? stateFilters
                : undefined;
            // Extract applicationId, sessionId, traceId, and spanId from merged filters
            const applicationId = input.applicationId || mergedFilters.applicationId;
            const sessionId = input.sessionId || mergedFilters.sessionId;
            const traceId = input.traceId || mergedFilters.traceId;
            const spanId = input.spanId || mergedFilters.spanId;
            // Log extracted values for debugging
            logger_1.default.info(`Tool ${customTool.name} - Extracted from filters: applicationId=${applicationId}, sessionId=${sessionId}, traceId=${traceId}, spanId=${spanId}`);
            // Build tool input with extracted values
            const toolInput = {
                ...input,
                projectId,
                orgId,
            };
            // If values were in filters, ensure they're also at top level
            if (applicationId && !toolInput.applicationId) {
                toolInput.applicationId = applicationId;
            }
            if (sessionId && !toolInput.sessionId) {
                toolInput.sessionId = sessionId;
            }
            if (traceId && !toolInput.traceId) {
                toolInput.traceId = traceId;
            }
            if (spanId && !toolInput.spanId) {
                toolInput.spanId = spanId;
            }
            // Ensure filters object includes all these values,
            // and that stateFilters (especially traceId/sessionId/applicationId/spanId)
            // are always propagated into the underlying tool call.
            toolInput.filters = {
                ...mergedFilters,
                ...(applicationId && { applicationId }),
                ...(sessionId && { sessionId }),
                ...(traceId && { traceId }),
                ...(spanId && { spanId }),
                ...(normalizedStateFilters?.applicationId && {
                    applicationId: normalizedStateFilters.applicationId,
                }),
                ...(normalizedStateFilters?.sessionId && {
                    sessionId: normalizedStateFilters.sessionId,
                }),
                ...(normalizedStateFilters?.traceId && { traceId: normalizedStateFilters.traceId }),
                ...(normalizedStateFilters?.spanId && { spanId: normalizedStateFilters.spanId }),
            };
            logger_1.default.info(`Tool ${customTool.name} - Final toolInput filters: ${JSON.stringify(toolInput.filters)}`);
            const startedAt = new Date();
            const result = await customTool.execute(toolInput);
            if (apiCalls) {
                let configBlob = undefined;
                if (customTool.name === 'draft_security_config') {
                    try {
                        const parsed = typeof result === 'string' ? JSON.parse(result) : result;
                        // Keep metadata small: store only config + warnings/suggestions.
                        if (parsed && typeof parsed === 'object') {
                            configBlob = {
                                config: parsed.config,
                                validationWarnings: parsed.validationWarnings,
                                followUpSuggestions: parsed.followUpSuggestions,
                                targetSource: parsed.targetSource,
                                checklistComplete: parsed.checklistComplete,
                                checklistMissingKeys: parsed.checklistMissingKeys,
                            };
                        }
                    }
                    catch {
                        // ignore
                    }
                }
                apiCalls.push({
                    tool: customTool.name,
                    timestamp: startedAt,
                    success: true,
                    sql: '',
                    ...(configBlob ? { config: configBlob } : {}),
                });
            }
            // Truncate large results to prevent token limit errors
            return (0, exports.truncateToolResult)(result);
        }
        catch (error) {
            logger_1.default.error(`Tool ${customTool.name} execution error: ${error}`);
            if (apiCalls) {
                apiCalls.push({
                    tool: customTool.name,
                    timestamp: new Date(),
                    success: false,
                    sql: '',
                });
            }
            return JSON.stringify({
                error: error instanceof Error ? error.message : String(error),
            });
        }
    }, {
        name: customTool.name,
        description: customTool.description,
        schema: zod_1.z.object(Object.keys(customTool.schema.properties || {}).reduce((acc, key) => {
            const prop = customTool.schema.properties[key];
            if (prop.type === 'string') {
                acc[key] = zod_1.z.string().optional();
            }
            else if (prop.type === 'number') {
                acc[key] = zod_1.z.number().optional();
            }
            else if (prop.type === 'boolean') {
                acc[key] = zod_1.z.boolean().optional();
            }
            else if (prop.type === 'array') {
                acc[key] = zod_1.z.array(zod_1.z.string()).optional();
            }
            else if (prop.type === 'object') {
                acc[key] = zod_1.z.any().optional();
            }
            else {
                acc[key] = zod_1.z.any().optional();
            }
            return acc;
        }, {})),
    });
};
exports.convertToolToLangChain = convertToolToLangChain;
/**
 * LangChain tool wrapper around runClickhouseTextToSql.
 * This lets the agent directly generate and execute ClickHouse SQL against telemetry tables.
 */
const createClickhouseTextToSqlLangChainTool = (state, apiCalls) => {
    return (0, tools_1.tool)(async (input) => {
        const question = (input?.question && input.question.trim().length > 0)
            ? input.question
            : state.query;
        const maxRows = typeof input?.maxRows === 'number' ? input.maxRows : undefined;
        const filters = (state.filters || {});
        const contextIds = {
            traceId: filters.traceId,
            applicationId: filters.applicationId,
            evaluationId: (filters.evaluationId || filters.evalId),
            sessionId: filters.sessionId,
            spanId: filters.spanId,
            testRunId: filters.testRunId,
        };
        try {
            const result = await (0, clickhouseTextToSqlTool_1.runClickhouseTextToSql)({
                question,
                maxRows,
                contextIds,
                projectId: state.projectId,
                domain: String(state.domain ?? ''),
                includeEvaluationResults: !!filters?.includeEvaluationResults,
            });
            apiCalls.push({
                tool: 'clickhouse_text_to_sql',
                timestamp: new Date(),
                success: true,
                sql: result?.sql ?? '',
            });
            // Merge this tool's approximate token usage into state.metadata.tokenUsage
            if (result?.tokenUsage) {
                const existing = state.metadata?.tokenUsage || {};
                const merged = {
                    total: (existing.total ?? 0) + (result.tokenUsage.total ?? 0),
                    prompt: (existing.prompt ?? 0) + (result.tokenUsage.prompt ?? 0),
                    completion: (existing.completion ?? 0) + (result.tokenUsage.completion ?? 0),
                };
                state.metadata = {
                    startTime: state.metadata?.startTime || new Date(),
                    endTime: state.metadata?.endTime,
                    executionPath: state.metadata?.executionPath || [],
                    apiCalls: state.metadata?.apiCalls || [],
                    error: state.metadata?.error,
                    tokenUsage: merged,
                };
            }
            const rowCount = typeof result?.rowCount === 'number' ? result.rowCount : (result?.rows?.length ?? 0);
            const rows = Array.isArray(result?.rows) ? result.rows : [];
            const sql = typeof result?.sql === 'string' ? result.sql : '';
            const payload = {
                rowCount,
                rows,
                sql,
            };
            if (rowCount === 0) {
                payload.emptyResultNarrationHints = (0, systemPrompt_1.buildEmptySqlResultNarrationHints)(sql, question);
            }
            else {
                const runHints = buildRunSummaryNarrationHintsForSqlRows(rows, question);
                if (runHints.length > 0) {
                    payload.runSummaryNarrationHints = runHints;
                }
            }
            return JSON.stringify(payload);
        }
        catch (error) {
            logger_1.default.error(`clickhouse_text_to_sql execution error: ${error}`);
            apiCalls.push({
                tool: 'clickhouse_text_to_sql',
                timestamp: new Date(),
                success: false,
                sql: '',
            });
            return JSON.stringify({
                error: error instanceof Error ? error.message : String(error),
            });
        }
    }, {
        name: 'clickhouse_text_to_sql',
        description: 'Generate a ClickHouse SQL query from natural language using the telemetry schema and execute it. Returns JSON: { sql, rowCount, rows } (when rowCount is 0: emptyResultNarrationHints; when rowCount > 0 and the question is a run summary with execution errors: runSummaryNarrationHints—follow those hints in your reply).',
        schema: zod_1.z.object({
            question: zod_1.z.string().optional().describe('Optional override for the question; defaults to the main Argus query'),
            maxRows: zod_1.z.number().optional().describe('Optional hard cap on returned rows (defaults to 500).'),
        }),
    });
};
exports.createClickhouseTextToSqlLangChainTool = createClickhouseTextToSqlLangChainTool;
const createGetEvaluationAudioLangChainTool = (state, apiCalls) => {
    return (0, tools_1.tool)(async (input) => {
        const extractTraceIdsFromText = (text) => {
            const t = String(text || '');
            // Argus TraceId is a 32-char hex string (no dashes).
            const matches = t.match(/\b[a-f0-9]{32}\b/gi) ?? [];
            return matches.map((m) => m.toLowerCase());
        };
        const rawHistoryItems = Array.isArray(state.filters?.chatHistory)
            ? state.filters.chatHistory
            : [];
        const historyText = rawHistoryItems
            .map((m) => (typeof m?.content === 'string' ? m.content : ''))
            .join('\n');
        const applicability = evaluateSpecializedToolApplicability('get_evaluation_audio', String(state.query ?? ''), historyText);
        if (!applicability.allowed) {
            return JSON.stringify({
                success: false,
                error: applicability.reason,
            });
        }
        const filters = (state.filters || {});
        const applicationId = input?.applicationId || filters.applicationId;
        // If the user explicitly asked "Play audio for trace <id>" in THIS turn,
        // prefer that traceId over any sticky state.filters.traceId from earlier turns.
        const inferredFromUserQuery = extractTraceIdsFromText(String(state.query ?? ''));
        const requestedTraceIds = Array.isArray(input?.traceIds)
            ? input.traceIds
            : [];
        const singleTraceId = input?.traceId ||
            inferredFromUserQuery?.[0] ||
            filters.traceId;
        const allTraceIds = Array.from(new Set([
            ...(singleTraceId ? [singleTraceId] : []),
            ...requestedTraceIds,
            ...inferredFromUserQuery,
        ].filter(Boolean)));
        if (allTraceIds.length === 0) {
            return JSON.stringify({
                success: false,
                error: 'traceId or traceIds is required to fetch audio URLs',
            });
        }
        try {
            const results = await Promise.all(allTraceIds.map(async (traceId) => {
                const res = await pulseEvaluationService_1.pulseEvaluationService.getEvaluationAudio({
                    traceId,
                    applicationId: applicationId,
                });
                return {
                    traceId,
                    success: !!res?.success,
                    audioUrl: res?.success ? res?.data : null,
                    message: res?.message || null,
                };
            }));
            apiCalls.push({
                tool: 'get_evaluation_audio',
                timestamp: new Date(),
                success: true,
                sql: '',
            });
            return JSON.stringify({
                applicationId,
                count: results.length,
                results,
            });
        }
        catch (error) {
            logger_1.default.error(`get_evaluation_audio execution error: ${error}`);
            apiCalls.push({
                tool: 'get_evaluation_audio',
                timestamp: new Date(),
                success: false,
                sql: '',
            });
            return JSON.stringify({
                success: false,
                error: error instanceof Error ? error.message : String(error),
            });
        }
    }, {
        name: 'get_evaluation_audio',
        description: 'Fetch signed audio URL(s) for one or many traceIds of audio traces. Use after identifying audio traceIds from telemetry/ClickHouse results.',
        schema: zod_1.z.object({
            traceId: zod_1.z.string().optional().describe('Single traceId for audio URL fetch.'),
            traceIds: zod_1.z
                .array(zod_1.z.string())
                .optional()
                .describe('Multiple traceIds for bulk audio URL fetch in one call.'),
            applicationId: zod_1.z
                .string()
                .optional()
                .describe('Application ID. Defaults to filters.applicationId when present.'),
        }),
    });
};
exports.createGetEvaluationAudioLangChainTool = createGetEvaluationAudioLangChainTool;
/**
 * Convert MCP Tool to LangChain StructuredTool
 */
const convertMCPToolToLangChain = (mcpTool, mcpClient, projectId, orgId, stateFilters, apiCalls) => {
    return (0, tools_1.tool)(async (input) => {
        try {
            // Merge state filters with input filters (state filters take precedence)
            const inputFilters = input &&
                typeof input.filters === 'object' &&
                input.filters !== null &&
                !Array.isArray(input.filters)
                ? input.filters
                : {};
            const safeStateFilters = stateFilters && typeof stateFilters === 'object' && !Array.isArray(stateFilters)
                ? stateFilters
                : {};
            const mergedFilters = { ...inputFilters, ...safeStateFilters };
            // Extract applicationId, sessionId, traceId, and spanId from merged filters
            const applicationId = input.applicationId || mergedFilters.applicationId;
            const sessionId = input.sessionId || mergedFilters.sessionId;
            const traceId = input.traceId || mergedFilters.traceId;
            const spanId = input.spanId || mergedFilters.spanId;
            // Log extracted values for infoging
            logger_1.default.info(`MCP Tool ${mcpTool.name} - Extracted from filters: applicationId=${applicationId}, sessionId=${sessionId}, traceId=${traceId}, spanId=${spanId}`);
            // Build tool input with extracted values
            const toolInput = {
                ...input,
                projectId,
                orgId,
            };
            // If values were in filters, ensure they're also at top level
            if (applicationId && !toolInput.applicationId) {
                toolInput.applicationId = applicationId;
            }
            if (sessionId && !toolInput.sessionId) {
                toolInput.sessionId = sessionId;
            }
            if (traceId && !toolInput.traceId) {
                toolInput.traceId = traceId;
            }
            if (spanId && !toolInput.spanId) {
                toolInput.spanId = spanId;
            }
            const normalizedStateFilters = stateFilters && typeof stateFilters === 'object' && !Array.isArray(stateFilters)
                ? stateFilters
                : undefined;
            // Ensure filters object includes all these values,
            // and that stateFilters (especially traceId/sessionId/applicationId/spanId)
            // are always propagated into the underlying tool call.
            toolInput.filters = {
                ...mergedFilters,
                ...(applicationId && { applicationId }),
                ...(sessionId && { sessionId }),
                ...(traceId && { traceId }),
                ...(spanId && { spanId }),
                ...(normalizedStateFilters?.applicationId && {
                    applicationId: normalizedStateFilters.applicationId,
                }),
                ...(normalizedStateFilters?.sessionId && {
                    sessionId: normalizedStateFilters.sessionId,
                }),
                ...(normalizedStateFilters?.traceId && { traceId: normalizedStateFilters.traceId }),
                ...(normalizedStateFilters?.spanId && { spanId: normalizedStateFilters.spanId }),
            };
            logger_1.default.info(`MCP Tool ${mcpTool.name} - Final toolInput filters: ${JSON.stringify(toolInput.filters)}`);
            const startedAt = new Date();
            const result = await mcpClient.callTool(mcpTool.name, toolInput);
            if (apiCalls) {
                apiCalls.push({
                    tool: mcpTool.name,
                    timestamp: startedAt,
                    success: true,
                    sql: '',
                });
            }
            // MCPToolResult has content as string, but normalize defensively
            if (result.error) {
                return JSON.stringify({ error: result.error });
            }
            const content = typeof result.content === 'string' ? result.content : JSON.stringify(result.content ?? result);
            // Truncate large results to prevent token limit errors
            return (0, exports.truncateToolResult)(content);
        }
        catch (error) {
            logger_1.default.error(`MCP tool ${mcpTool.name} execution error: ${error}`);
            if (apiCalls) {
                apiCalls.push({
                    tool: mcpTool.name,
                    timestamp: new Date(),
                    success: false,
                    sql: '',
                });
            }
            return JSON.stringify({
                error: error instanceof Error ? error.message : String(error),
            });
        }
    }, {
        name: mcpTool.name,
        description: mcpTool.description,
        schema: zod_1.z.object(Object.keys(mcpTool.inputSchema?.properties || {}).reduce((acc, key) => {
            const prop = mcpTool.inputSchema?.properties?.[key];
            if (prop?.type === 'string') {
                acc[key] = zod_1.z.string().optional();
            }
            else if (prop?.type === 'number') {
                acc[key] = zod_1.z.number().optional();
            }
            else if (prop?.type === 'boolean') {
                acc[key] = zod_1.z.boolean().optional();
            }
            else if (prop?.type === 'array') {
                acc[key] = zod_1.z.array(zod_1.z.string()).optional();
            }
            else if (prop?.type === 'object') {
                acc[key] = zod_1.z.any().optional();
            }
            else {
                acc[key] = zod_1.z.any().optional();
            }
            return acc;
        }, {})),
    });
};
exports.convertMCPToolToLangChain = convertMCPToolToLangChain;
//# sourceMappingURL=nodeHelperFuncitons.js.map