UNPKG

cyber-mysql-openai

Version:

Intelligent natural language to SQL translator with self-correction capabilities using OpenAI and MySQL

1,193 lines 54.3 kB
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CyberMySQLOpenAI = void 0;
// src/agent/cyberMySQLOpenAI.ts
const openai_1 = require("openai");
const uuid_1 = require("uuid");
const config_1 = require("../config");
const utils_1 = __importDefault(require("../utils"));
const sqlCleaner_1 = require("../utils/sqlCleaner");
const responseFormatter_1 = require("../utils/responseFormatter");
const db_1 = require("../db");
const i18n_1 = require("../utils/i18n");
const memoryCache_1 = require("../cache/memoryCache");
const queryValidator_1 = require("../utils/queryValidator");
const queryHistory_1 = require("../utils/queryHistory");
/**
 * Clase principal que proporciona la funcionalidad para traducir
 * lenguaje natural a SQL y ejecutar consultas
 */
class CyberMySQLOpenAI {
    /**
     * Constructor de la clase CyberMySQLOpenAI
     * @param config - Configuración de la librería
     */
    constructor(config = {}) {
        this.cache = null;
        // Cache de schema
        this.cachedSchema = null;
        this.schemaCachedAt = 0;
        // Validar configuración
        const errors = (0, config_1.validateConfig)(config);
        if (errors.length > 0) {
            throw new Error(`Invalid configuration: ${errors.join(", ")}`);
        }
        // Configurar el logger
        this.logger = new utils_1.default(config.logLevel || config_1.DEFAULT_LOG_LEVEL, config.logDirectory || config_1.DEFAULT_LOG_DIRECTORY, config.logEnabled !== undefined ? config.logEnabled : config_1.DEFAULT_LOG_ENABLED);
        // Inicializar i18n
        this.i18n = new i18n_1.I18n(config.language || "en");
        // Inicializar la configuración
        const openaiConfig = {
            ...config_1.DEFAULT_OPENAI_CONFIG,
            ...config.openai,
        };
        const dbConfig = {
            ...config_1.DEFAULT_DB_CONFIG,
            ...config.database,
        };
        // Inicializar componentes
        this.openai = new openai_1.OpenAI({
            apiKey: openaiConfig.apiKey,
        });
        this.openaiModel = openaiConfig.model;
        this.lightModel = openaiConfig.lightModel || "gpt-4o-mini";
        this.maxReflections = config.maxReflections || config_1.DEFAULT_MAX_REFLECTIONS;
        this.dbManager = new db_1.DBManager(dbConfig, this.logger);
        this.mode = config.mode || "direct";
        // Almacenar contexto de negocio (antes de ResponseFormatter para que buildResponseStyleInstruction funcione)
        this.schemaContext = config.context;
        this.responseFormatter = new responseFormatter_1.ResponseFormatter(openaiConfig.apiKey, openaiConfig.model, config.language || "en", this.logger, config.context?.businessDescription
            ? `\nCONTEXTO DE NEGOCIO: ${config.context.businessDescription}\n`
            : "", this.buildResponseStyleInstruction(), this.lightModel);
        // Inicializar sistema de cache
        this.cacheEnabled = config.cache?.enabled !== false; // Por defecto habilitado
        if (this.cacheEnabled) {
            this.cache = memoryCache_1.MemoryCache.getInstance(config.cache?.maxSize || 1000, config.cache?.cleanupIntervalMs || 300000);
            this.logger.info("Memory cache enabled", {
                maxSize: config.cache?.maxSize || 1000,
                cleanupInterval: config.cache?.cleanupIntervalMs || 300000,
            });
        }
        else {
            this.logger.info("Memory cache disabled");
        }
        // TTL del cache de schema (5 minutos por defecto)
        this.schemaTTL = config.schemaTTL || 300000;
        // Historial de consultas
        this.queryHistory = new queryHistory_1.QueryHistory(100);
        this.logger.info("CyberMySQLOpenAI initialized successfully", {
            model: this.openaiModel,
            maxReflections: this.maxReflections,
            language: this.i18n.getLanguage(),
            cacheEnabled: this.cacheEnabled,
            schemaTTL: this.schemaTTL,
            mode: this.mode,
            hasContext: !!config.context,
            hasCustomInstructions: !!config.context?.customInstructions?.length,
            hasExamples: !!config.context?.examples?.length,
        });
    }
    /**
     * Procesa una consulta en lenguaje natural, la traduce a SQL y la ejecuta
     * @param prompt - Consulta en lenguaje natural
     * @param options - Opciones adicionales
     * @returns Resultado de la consulta
     */
    async query(prompt, options = {}) {
        const stream = this.queryStream(prompt, options);
        let finalResult;
        for await (const chunk of stream) {
            if (chunk.type === "chunk" && options.onChunk && chunk.content) {
                options.onChunk(chunk.content);
            }
            if (chunk.type === "done" && chunk.metadata) {
                finalResult = chunk.metadata;
            }
        }
        if (!finalResult) {
            throw new Error("Query stream failed to complete");
        }
        return finalResult;
    }
    /**
     * Procesa una consulta en lenguaje natural devolviendo un generador asíncrono
     * para consumo de eventos y texto en tiempo real (streaming)
     * @param prompt - Consulta en lenguaje natural
     * @param options - Opciones adicionales
     * @returns Generador asíncrono de eventos de consulta
     */
    async *queryStream(prompt, options = {}) {
        const requestId = (0, uuid_1.v4)();
        const startTime = Date.now();
        const tokenAccumulator = {
            promptTokens: 0,
            completionTokens: 0,
            totalTokens: 0,
        };
        if (this.mode === "agentic") {
            this.logger.info("Processing natural language query in agentic mode", { prompt });
            const messages = [
                {
                    role: "system",
                    content: "You are an agentic database explorer. Your goal is to answer the user's natural language question by exploring the database tables, retrieving schemas when necessary, executing queries, and providing a natural explanation of the results. Always use tools to inspect tables and execute queries."
                },
                { role: "user", content: prompt }
            ];
            const tools = [
                {
                    type: "function",
                    function: {
                        name: "list_tables",
                        description: "List all table names in the database schema",
                        parameters: { type: "object", properties: {} }
                    }
                },
                {
                    type: "function",
                    function: {
                        name: "describe_table",
                        description: "Get detailed columns information for a specific table",
                        parameters: {
                            type: "object",
                            properties: {
                                table_name: { type: "string", description: "Table name to describe" }
                            },
                            required: ["table_name"]
                        }
                    }
                },
                {
                    type: "function",
                    function: {
                        name: "execute_sql_query",
                        description: "Execute a SELECT SQL query on the database and return results",
                        parameters: {
                            type: "object",
                            properties: {
                                sql: { type: "string", description: "SELECT query to run" }
                            },
                            required: ["sql"]
                        }
                    }
                }
            ];
            let loopCount = 0;
            let success = false;
            let sql = "";
            let results = [];
            while (loopCount < 5 && !success) {
                loopCount++;
                const response = await this.openai.chat.completions.create({
                    model: this.openaiModel,
                    messages,
                    tools,
                    tool_choice: "auto"
                });
                if (response.usage) {
                    this.accumulateTokens(tokenAccumulator, response.usage);
                }
                const choice = response.choices[0];
                if (choice.message.tool_calls && choice.message.tool_calls.length > 0) {
                    messages.push(choice.message);
                    for (const toolCall of choice.message.tool_calls) {
                        const tc = toolCall;
                        const name = tc.function.name;
                        const args = JSON.parse(tc.function.arguments);
                        if (name === "list_tables") {
                            this.logger.debug("Agent called list_tables");
                            const dbSchema = await this.getSchemaWithCache();
                            const tableNames = Object.keys(dbSchema);
                            messages.push({
                                role: "tool",
                                tool_call_id: toolCall.id,
                                name,
                                content: JSON.stringify({ tables: tableNames })
                            });
                            yield { type: "chunk", content: `\n[Agent]: Listando tablas...\n` };
                        }
                        else if (name === "describe_table") {
                            this.logger.debug("Agent called describe_table", { table: args.table_name });
                            const columns = await this.dbManager.getTableColumns(args.table_name);
                            messages.push({
                                role: "tool",
                                tool_call_id: toolCall.id,
                                name,
                                content: JSON.stringify({ table: args.table_name, columns })
                            });
                            yield { type: "chunk", content: `\n[Agent]: Describiendo tabla '${args.table_name}'...\n` };
                        }
                        else if (name === "execute_sql_query") {
                            this.logger.debug("Agent called execute_sql_query", { sql: args.sql });
                            sql = args.sql;
                            yield { type: "sql", sql };
                            try {
                                results = await this.dbManager.executeReadOnlyQuery(args.sql);
                                success = true;
                                yield { type: "results", results };
                                messages.push({
                                    role: "tool",
                                    tool_call_id: toolCall.id,
                                    name,
                                    content: JSON.stringify({ results })
                                });
                                yield { type: "chunk", content: `\n[Agent]: Ejecutando consulta SQL...\n` };
                            }
                            catch (err) {
                                messages.push({
                                    role: "tool",
                                    tool_call_id: toolCall.id,
                                    name,
                                    content: JSON.stringify({ error: err.message })
                                });
                                yield { type: "chunk", content: `\n[Agent]: Fallo al ejecutar SQL: ${err.message}\n` };
                            }
                        }
                    }
                }
                else {
                    const text = choice.message.content || "";
                    yield { type: "chunk", content: text };
                    break;
                }
            }
            let naturalResponseAccumulator = "";
            if (success) {
                const responseStream = this.responseFormatter.generateNaturalResponseStream(sql, results, options);
                for await (const chunk of responseStream) {
                    naturalResponseAccumulator += chunk;
                    yield { type: "chunk", content: chunk };
                }
            }
            else {
                const errorMsg = `No se pudo obtener resultados en modo agéntico.`;
                naturalResponseAccumulator = errorMsg;
                yield { type: "chunk", content: errorMsg };
            }
            const executionTime = Date.now() - startTime;
            if (tokenAccumulator.totalTokens > 0) {
                tokenAccumulator.estimatedCost = this.estimateTokenCost(tokenAccumulator.promptTokens, tokenAccumulator.completionTokens);
            }
            const finalResult = {
                sql,
                results,
                reflections: [],
                attempts: loopCount,
                success,
                naturalResponse: naturalResponseAccumulator,
                executionTime,
                fromCache: false,
                tokenUsage: tokenAccumulator.totalTokens > 0 ? tokenAccumulator : undefined,
            };
            yield { type: "done", metadata: finalResult };
            this.queryHistory.addRecord({
                id: requestId,
                timestamp: new Date(),
                naturalQuery: prompt,
                generatedSQL: sql,
                success,
                executionTime,
                fromCache: false,
                tokenUsage: finalResult.tokenUsage,
            });
            return finalResult;
        }
        try {
            this.logger.info("Processing natural language query in direct mode", { prompt });
            // Paso 1: Obtener el esquema de la base de datos (con cache)
            const schema = await this.getSchemaWithCache();
            const schemaHash = this.generateSchemaHash(schema);
            // Paso 2: Intentar obtener resultado del cache
            if (this.cache && this.cacheEnabled && !options.bypassCache) {
                const cachedResult = this.cache.get(prompt, this.i18n.getLanguage(), schemaHash);
                if (cachedResult) {
                    const executionTime = Date.now() - startTime;
                    this.logger.info(`🎯 Cache HIT for query: ${prompt.substring(0, 50)}...`, {
                        executionTime: `${executionTime}ms`,
                        originalExecutionTime: `${cachedResult.executionTime}ms`,
                    });
                    yield { type: "sql", sql: cachedResult.sql };
                    yield { type: "results", results: cachedResult.results };
                    yield { type: "chunk", content: cachedResult.naturalResponse };
                    const finalResult = {
                        sql: cachedResult.sql,
                        results: cachedResult.results,
                        reflections: [],
                        attempts: 0,
                        success: true,
                        naturalResponse: cachedResult.naturalResponse,
                        executionTime,
                        fromCache: true,
                    };
                    yield { type: "done", metadata: finalResult };
                    return finalResult;
                }
                this.logger.info(`💫 Cache MISS for query: ${prompt.substring(0, 50)}...`);
            }
            // Paso 3: Generar SQL a partir del lenguaje natural
            const generateResult = await this.generateSQL(prompt, schema, requestId, tokenAccumulator);
            let sql = generateResult.sql;
            const confidence = generateResult.confidence;
            yield { type: "sql", sql };
            // Paso 3.5: Validar la query generada
            const validation = (0, queryValidator_1.validateQuery)(sql, schema);
            if (validation.warnings.length > 0) {
                this.logger.warn("Advertencias de validación de query", {
                    warnings: validation.warnings,
                });
            }
            // Paso 4: Ejecutar la consulta SQL
            let results = [];
            let reflections = [];
            let attempts = 0;
            let success = false;
            try {
                results = await this.dbManager.executeReadOnlyQuery(sql);
                success = true;
                yield { type: "results", results };
            }
            catch (error) {
                yield { type: "reflection", content: `Error ejecutando SQL: ${error.message}. Iniciando autorreflexión...` };
                this.logger.warn("Error executing SQL, attempting to reflect and fix", {
                    error: error.message,
                });
                const reflectionResult = await this.reflectAndFix(prompt, sql, error.message, schema, requestId);
                sql = reflectionResult.sql;
                reflections = reflectionResult.reflections;
                attempts = reflectionResult.attempts;
                yield { type: "sql", sql };
                if (reflectionResult.success) {
                    results = reflectionResult.results;
                    success = true;
                    yield { type: "results", results };
                }
                else {
                    results = [];
                    yield { type: "results", results };
                    this.logger.error("Failed to execute query after reflection", {
                        attempts,
                    });
                }
            }
            // Paso 5: Generar respuesta en lenguaje natural
            let naturalResponseAccumulator = "";
            if (success) {
                const simpleResponse = this.responseFormatter.generateSimpleResponse(sql, results);
                if (simpleResponse) {
                    naturalResponseAccumulator = simpleResponse;
                    yield { type: "chunk", content: simpleResponse };
                }
                else {
                    const responseStream = this.responseFormatter.generateNaturalResponseStream(sql, results, { detailed: false });
                    for await (const chunk of responseStream) {
                        naturalResponseAccumulator += chunk;
                        yield { type: "chunk", content: chunk };
                    }
                }
            }
            else {
                const errorMsg = `No se pudo obtener resultados debido a un error persistente en SQL.`;
                naturalResponseAccumulator = errorMsg;
                yield { type: "chunk", content: errorMsg };
            }
            // Generar respuesta detallada si se solicita
            let detailedResponse;
            if (options.detailed && success) {
                try {
                    const detailedStream = this.responseFormatter.generateNaturalResponseStream(sql, results, { detailed: true });
                    let detailedAccumulator = "";
                    for await (const chunk of detailedStream) {
                        detailedAccumulator += chunk;
                    }
                    detailedResponse = detailedAccumulator;
                }
                catch (error) {
                    this.logger.error("Error generating detailed response", {
                        error: error.message,
                    });
                    detailedResponse = "No se pudo generar la respuesta detallada.";
                }
            }
            const executionTime = Date.now() - startTime;
            // Paso 6: Guardar en cache si fue exitoso
            if (this.cache && this.cacheEnabled && success && naturalResponseAccumulator) {
                this.cache.set(prompt, this.i18n.getLanguage(), schemaHash, sql, results, naturalResponseAccumulator, executionTime);
                this.logger.info("Result cached successfully");
            }
            // Paso 7: Devolver resultado
            if (tokenAccumulator.totalTokens > 0) {
                tokenAccumulator.estimatedCost = this.estimateTokenCost(tokenAccumulator.promptTokens, tokenAccumulator.completionTokens);
            }
            const result = {
                sql,
                results,
                reflections,
                attempts,
                success,
                confidence,
                naturalResponse: naturalResponseAccumulator,
                executionTime,
                fromCache: false,
                tokenUsage: tokenAccumulator.totalTokens > 0 ? tokenAccumulator : undefined,
            };
            if (detailedResponse) {
                result.detailedResponse = detailedResponse;
            }
            // Registrar en historial
            this.queryHistory.addRecord({
                id: requestId,
                timestamp: new Date(),
                naturalQuery: prompt,
                generatedSQL: sql,
                confidence,
                success,
                executionTime,
                fromCache: false,
                tokenUsage: result.tokenUsage,
            });
            yield { type: "done", metadata: result };
            return result;
        }
        catch (error) {
            this.logger.error("Error processing query", {
                error: error.message,
            });
            throw error;
        }
    }
    /**
     * Ejecuta una consulta SQL directamente
     * @param sql - Consulta SQL
     * @param options - Opciones adicionales
     * @returns Resultado de la consulta
     */
    async executeSQL(sql, options = {}) {
        const startTime = Date.now();
        try {
            this.logger.info("Executing SQL query directly", { sql });
            // Limpiar la consulta SQL
            const cleanedSql = (0, sqlCleaner_1.cleanSqlResponse)(sql, "direct", this.logger);
            // Ejecutar la consulta
            const results = await this.dbManager.executeReadOnlyQuery(cleanedSql);
            // Generar respuesta en lenguaje natural
            let naturalResponse = this.responseFormatter.generateSimpleResponse(cleanedSql, results);
            if (!naturalResponse) {
                naturalResponse = await this.responseFormatter.generateNaturalResponse(cleanedSql, results, { detailed: false });
            }
            // Generar respuesta detallada si se solicita
            let detailedResponse;
            if (options.detailed) {
                try {
                    detailedResponse =
                        await this.responseFormatter.generateNaturalResponse(cleanedSql, results, { detailed: true });
                }
                catch (error) {
                    this.logger.error("Error generating detailed response", {
                        error: error.message,
                    });
                    detailedResponse = "No se pudo generar la respuesta detallada.";
                }
            }
            const executionTime = Date.now() - startTime;
            // Devolver resultado
            const result = {
                sql: cleanedSql,
                results,
                success: true,
                naturalResponse,
                executionTime,
                fromCache: false,
            };
            if (detailedResponse) {
                result.detailedResponse = detailedResponse;
            }
            return result;
        }
        catch (error) {
            this.logger.error("Error executing SQL query", {
                error: error.message,
            });
            return {
                sql,
                results: [],
                success: false,
                naturalResponse: `Error ejecutando la consulta: ${error.message}`,
            };
        }
    }
    /**
     * Cierra la conexión a la base de datos
     */
    async close() {
        await this.dbManager.closePool();
        this.logger.info("CyberMySQLOpenAI connections closed");
    }
    /**
     * Cambia el idioma de las respuestas
     * @param language - Idioma a establecer ('es' | 'en')
     */
    setLanguage(language) {
        this.i18n.setLanguage(language);
        this.responseFormatter.setLanguage(language);
        this.logger.info("Language changed", { language });
    }
    /**
     * Obtiene el idioma actual
     * @returns Idioma actual
     */
    getLanguage() {
        return this.i18n.getLanguage();
    }
    /**
     * Genera SQL a partir de lenguaje natural usando OpenAI
     * Intenta usar function calling para respuestas estructuradas;
     * si el modelo no lo soporta, cae al modo texto con sqlCleaner.
     */
    async generateSQL(prompt, schema, requestId, tokenAccumulator) {
        try {
            const schemaDescription = this.buildSchemaDescription(schema);
            // Construir secciones opcionales del prompt
            const businessContext = this.schemaContext?.businessDescription
                ? `\nCONTEXTO DE NEGOCIO: ${this.schemaContext.businessDescription}\n`
                : "";
            const relationships = this.buildRelationshipsSection(schema);
            const examples = this.buildExamplesSection();
            const customInstructions = this.buildCustomInstructionsSection();
            const systemPrompt = this.i18n.getMessageWithReplace("prompts", "translateToSQL", {
                schema: schemaDescription,
                query: prompt,
                businessContext,
                relationships,
                examples,
                customInstructions,
            });
            // Intentar function calling (modo inteligente)
            try {
                const response = await this.openai.chat.completions.create({
                    model: this.openaiModel,
                    messages: [
                        { role: "system", content: systemPrompt },
                        { role: "user", content: prompt },
                    ],
                    tools: [
                        {
                            type: "function",
                            function: {
                                name: "execute_sql_query",
                                description: "Execute a SQL SELECT query against the MySQL database",
                                parameters: {
                                    type: "object",
                                    properties: {
                                        sql: {
                                            type: "string",
                                            description: "Valid MySQL SELECT query",
                                        },
                                        confidence: {
                                            type: "number",
                                            description: "Confidence score from 0 to 1 that this query correctly answers the question",
                                        },
                                        reasoning: {
                                            type: "string",
                                            description: "Brief explanation of why this query answers the question",
                                        },
                                    },
                                    required: ["sql", "confidence"],
                                },
                            },
                        },
                    ],
                    tool_choice: {
                        type: "function",
                        function: { name: "execute_sql_query" },
                    },
                });
                // Registrar uso de tokens
                if (response.usage) {
                    this.logger.logTokenUsage(requestId, "generate-sql-fc", response.usage.prompt_tokens, response.usage.completion_tokens, response.usage.total_tokens, this.openaiModel);
                    this.accumulateTokens(tokenAccumulator, response.usage);
                }
                const toolCall = response.choices[0]?.message?.tool_calls?.[0];
                if (toolCall?.function?.arguments) {
                    const args = JSON.parse(toolCall.function.arguments);
                    this.logger.debug("SQL generated via function calling", {
                        sql: args.sql,
                        confidence: args.confidence,
                        reasoning: args.reasoning,
                    });
                    return { sql: args.sql, confidence: args.confidence };
                }
                // Si no hay tool_calls, caer al contenido de mensaje
                throw new Error("No tool_calls in response");
            }
            catch (_fcError) {
                // Fallback: modo texto (compatible con modelos antiguos)
                this.logger.debug("Function calling not available, falling back to text mode", {
                    error: _fcError.message,
                });
                const response = await this.openai.chat.completions.create({
                    model: this.openaiModel,
                    messages: [
                        { role: "system", content: systemPrompt },
                        { role: "user", content: prompt },
                    ],
                });
                const sql = response.choices[0]?.message?.content?.trim() || "";
                if (response.usage) {
                    this.logger.logTokenUsage(requestId, "generate-sql-text", response.usage.prompt_tokens, response.usage.completion_tokens, response.usage.total_tokens, this.openaiModel);
                    this.accumulateTokens(tokenAccumulator, response.usage);
                }
                // Limpiar con sqlCleaner en modo texto
                const cleanedSql = (0, sqlCleaner_1.cleanSqlResponse)(sql, "generate", this.logger);
                this.logger.debug("SQL generated via text mode (fallback)", {
                    sql: cleanedSql,
                });
                return { sql: cleanedSql };
            }
        }
        catch (error) {
            this.logger.error("Error generating SQL from prompt", {
                error: error.message,
            });
            throw new Error(`Failed to generate SQL: ${error.message}`);
        }
    }
    /**
     * Construye la descripción comprimida del schema.
     * Formato: "tabla: col1* col2 col3→ref_tabla" (~20 tokens/tabla vs ~60 antes)
     * donde * = PRIMARY KEY, →ref = FK a otra tabla
     */
    buildSchemaDescription(schema) {
        const tables = Object.keys(schema);
        return tables
            .map((tableName) => {
            const tableData = schema[tableName];
            const tableContext = this.schemaContext?.tables?.[tableName];
            // Mapear FKs por columna para anotarlas inline
            const fkMap = {};
            for (const fk of tableData.foreignKeys) {
                fkMap[fk.column_name] = fk.referenced_table;
            }
            // Columnas en formato comprimido
            const cols = tableData.columns
                .map((col) => {
                let c = col.column_name;
                if (col.column_key === "PRI")
                    c += "*"; // PK
                if (fkMap[col.column_name])
                    c += `→${fkMap[col.column_name]}`; // FK
                // Anotar tipo solo si el usuario definió contexto de negocio para esta columna
                const colCtx = tableContext?.columns?.[col.column_name];
                if (colCtx)
                    c += `(${colCtx})`;
                return c;
            })
                .join(" ");
            // Descripción de tabla: nombre + descripción de negocio si existe
            const tableDesc = tableContext?.description
                ? `${tableName}(${tableContext.description})`
                : tableName;
            return `${tableDesc}: ${cols}`;
        })
            .join("\n");
    }
    /**
     * Construye la sección de relaciones FK para el prompt
     */
    buildRelationshipsSection(schema) {
        const allFKs = [];
        for (const [tableName, tableData] of Object.entries(schema)) {
            for (const fk of tableData.foreignKeys) {
                allFKs.push(`${tableName}.${fk.column_name} → ${fk.referenced_table}.${fk.referenced_column}`);
            }
        }
        if (allFKs.length === 0)
            return "";
        const lang = this.i18n.getLanguage();
        const header = lang === "es" ? "RELACIONES ENTRE TABLAS:" : "TABLE RELATIONSHIPS:";
        return `\n${header}\n${allFKs.join("\n")}\n`;
    }
    /**
     * Construye la sección de ejemplos few-shot para el prompt
     */
    buildExamplesSection() {
        if (!this.schemaContext?.examples?.length)
            return "";
        const lang = this.i18n.getLanguage();
        const header = lang === "es" ? "EJEMPLOS DE REFERENCIA:" : "REFERENCE EXAMPLES:";
        const qLabel = lang === "es" ? "Pregunta" : "Question";
        const sLabel = "SQL";
        const exampleLines = this.schemaContext.examples
            .map((ex, i) => `${i + 1}. ${qLabel}: "${ex.question}"\n   ${sLabel}: ${ex.sql}`)
            .join("\n");
        return `\n${header}\n${exampleLines}\n`;
    }
    /**
     * Intenta corregir una consulta SQL fallida mediante reflexión
     * @param prompt - Consulta original en lenguaje natural
     * @param sql - Consulta SQL que falló
     * @param errorMessage - Mensaje de error
     * @param schema - Esquema de la base de datos
     * @param requestId - ID de la solicitud para logging
     * @returns Resultado después de intentar corregir
     */
    /**
     * Extrae los nombres de tablas mencionados en una consulta SQL.
     * Se usa para filtrar el schema y no re-enviarlo completo en la reflexión.
     */
    extractTablesFromSQL(sql) {
        const matches = sql.match(/(?:FROM|JOIN|INTO|UPDATE)\s+([`"']?\w+[`"']?)/gi) || [];
        return matches.map((m) => m.replace(/(?:FROM|JOIN|INTO|UPDATE)\s+/i, "").replace(/[`"']/g, ""));
    }
    levenshtein(a, b) {
        const tmp = [];
        let i, j;
        for (i = 0; i <= a.length; i++) {
            tmp.push([i]);
        }
        for (j = 0; j <= b.length; j++) {
            tmp[0][j] = j;
        }
        for (i = 1; i <= a.length; i++) {
            for (j = 1; j <= b.length; j++) {
                tmp[i][j] = Math.min(tmp[i - 1][j] + 1, tmp[i][j - 1] + 1, tmp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
            }
        }
        return tmp[a.length][b.length];
    }
    async reflectAndFix(prompt, sql, errorMessage, schema, requestId) {
        const reflections = [];
        let attempts = 1;
        let currentSql = sql;
        let results = [];
        let success = false;
        let currentErrorMessage = errorMessage;
        // Mejora 5: filtrar schema a solo las tablas del SQL fallido
        const involvedTables = this.extractTablesFromSQL(sql);
        const reducedSchema = involvedTables.length > 0
            ? Object.fromEntries(Object.entries(schema).filter(([t]) => involvedTables.includes(t)))
            : schema; // fallback: schema completo si no se detectaron tablas
        while (attempts <= this.maxReflections && !success) {
            try {
                // Levenshtein schema hints
                let enhancedErrorMessage = currentErrorMessage;
                const tableMatch = currentErrorMessage.match(/Table\s+'[^']+\.([^']+)'\s+doesn't\s+exist/i) ||
                    currentErrorMessage.match(/Table\s+'([^']+)'\s+doesn't\s+exist/i);
                if (tableMatch) {
                    const missingTable = tableMatch[1];
                    const allTables = Object.keys(schema);
                    const suggestions = allTables
                        .map(t => ({ name: t, dist: this.levenshtein(missingTable, t) }))
                        .filter(t => t.dist <= 3)
                        .sort((x, y) => x.dist - y.dist)
                        .map(t => `'${t.name}'`);
                    if (suggestions.length > 0) {
                        enhancedErrorMessage += `\n(Hint: La tabla '${missingTable}' no existe. ¿Quisiste decir alguna de estas?: ${suggestions.join(", ")})`;
                    }
                }
                const columnMatch = currentErrorMessage.match(/Unknown\s+column\s+'([^']+)'\s+in/i);
                if (columnMatch) {
                    const missingColumn = columnMatch[1];
                    const columnsToCheck = [];
                    for (const t of involvedTables) {
                        if (schema[t]) {
                            schema[t].columns.forEach((col) => {
                                const colName = col.column_name || col.COLUMN_NAME;
                                if (colName && !columnsToCheck.includes(colName)) {
                                    columnsToCheck.push(colName);
                                }
                            });
                        }
                    }
                    const suggestions = columnsToCheck
                        .map(c => ({ name: c, dist: this.levenshtein(missingColumn, c) }))
                        .filter(c => c.dist <= 3)
                        .sort((x, y) => x.dist - y.dist)
                        .map(c => `'${c.name}'`);
                    if (suggestions.length > 0) {
                        enhancedErrorMessage += `\n(Hint: La columna '${missingColumn}' no existe en las tablas involucradas. ¿Quisiste decir alguna de estas?: ${suggestions.join(", ")})`;
                    }
                }
                // Generar reflexión sobre el error (con schema reducido)
                const reflection = await this.generateReflection(prompt, currentSql, enhancedErrorMessage, reducedSchema, requestId);
                // Ejecutar candidatos en paralelo
                const cleanedCandidates = reflection.fixedSqlCandidates.map((cand) => (0, sqlCleaner_1.cleanSqlResponse)(cand, "reflect", this.logger));
                this.logger.debug("Executing SQL candidates in parallel", { candidates: cleanedCandidates });
                const executionPromises = cleanedCandidates.map(async (candidate, index) => {
                    try {
                        const res = await this.dbManager.executeReadOnlyQuery(candidate);
                        return { index, candidate, results: res, error: undefined, success: true };
                    }
                    catch (err) {
                        return { index, candidate, results: [], error: err.message, success: false };
                    }
                });
                const executionResults = await Promise.all(executionPromises);
                const successfulRun = executionResults.find(r => r.success);
                if (successfulRun) {
                    success = true;
                    currentSql = successfulRun.candidate;
                    results = successfulRun.results;
                    reflections.push({
                        error: currentErrorMessage,
                        reasoning: reflection.reasoning,
                        fixAttempt: currentSql,
                    });
                    this.logger.info("Query fixed successfully via parallel candidate execution", {
                        attempt: attempts,
                        candidateIndex: successfulRun.index,
                    });
                }
                else {
                    reflections.push({
                        error: currentErrorMessage,
                        reasoning: reflection.reasoning,
                        fixAttempt: cleanedCandidates.join(" | "),
                    });
                    currentErrorMessage = executionResults.map(r => r.error).join(" ; ");
                    attempts++;
                    this.logger.warn("All parallel candidates failed", { attempt: attempts, errors: currentErrorMessage });
                }
            }
            catch (error) {
                attempts++;
                currentErrorMessage = error.message;
                this.logger.warn("Reflection attempt failed due to system error", {
                    attempt: attempts,
                    error: currentErrorMessage,
                });
                if (attempts > this.maxReflections) {
                    this.logger.error("Max reflection attempts reached", {
                        maxReflections: this.maxReflections,
                    });
                    break;
                }
            }
        }
        return {
            sql: currentSql,
            reflections,
            attempts,
            results,
            success,
        };
    }
    /**
     * Genera una reflexión sobre un error en una consulta SQL.
     * Usa function calling cuando está disponible, con fallback a texto.
     */
    async generateReflection(prompt, sql, errorMessage, schema, requestId) {
        try {
            // Mejora 3 + 5: schema ya viene filtrado y usamos el modelo ligero
            const schemaDescription = this.buildSchemaDescription(schema);
            const relationships = this.buildRelationshipsSection(schema);
            // Construir secciones de contexto para la reflexión
            const businessContext = this.schemaContext?.businessDescription
                ? `\nCONTEXTO DE NEGOCIO: ${this.schemaContext.businessDescription}\n`
                : "";
            const examples = this.buildExamplesSection();
            const customInstructions = this.buildCustomInstructionsSection();
            const systemPrompt = this.i18n.getMessageWithReplace("prompts", "fixSQLError", {
                error: errorMessage,
                sql: sql,
                schema: schemaDescription,
                relationships,
                businessContext,
                examples,
                customInstructions,
            });
            const userMessage = `Consulta original en lenguaje natural: ${prompt}\n\nConsulta SQL que falló:\n${sql}\n\nError recibido:\n${errorMessage}`;
            // Mejora 3: usar lightModel (gpt-4o-mini) para la reflexión
            try {
                const response = await this.openai.chat.completions.create({
                    model: this.lightModel,
                    messages: [
                        { role: "system", content: systemPrompt },
                        { role: "user", content: userMessage },
                    ],
                    tools: [
                        {
                            type: "function",
                            function: {
                                name: "fix_sql_query",
                                description: "Fix a failed SQL query by providing alternative candidate queries, ordered by likelihood of success",
                                parameters: {
                                    type: "object",
                                    properties: {
                                        fixedSqlCandidates: {
                                            type: "array",
                                            items: { type: "string" },
                                            description: "List of up to 3 candidate MySQL queries that solve the error, ordered by likelihood of success",
                                        },
                                        reasoning: {
                                            type: "string",
                                            description: "Explanation of what went wrong and how it was fixed",
                                        },
                                    },
                                    required: ["fixedSqlCandidates", "reasoning"],
                                },
                            },
                        },
                    ],
                    tool_choice: {
                        type: "function",
                        function: { name: "fix_sql_query" },
                    },
                });
                if (response.usage) {
                    this.logger.logTokenUsage(requestId, "reflect-fix-fc", response.usage.prompt_tokens, response.usage.completion_tokens, response.usage.total_tokens, this.lightModel);
                }
                const toolCall = response.choices[0]?.message?.tool_calls?.[0];
                if (toolCall?.function?.arguments) {
                    const args = JSON.parse(toolCall.function.arguments);
                    const fixedSqlCandidates = args.fixedSqlCandidates || [args.fixedSql];
                    this.logger.debug("Generated reflection via function calling", {
                        reasoning: args.reasoning,
                        candidates: fixedSqlCandidates,
                    });
                    return { reasoning: args.reasoning, fixedSqlCandidates };
                }
                throw new Error("No tool_calls in reflection response");
                // eslint-disable-next-line @typescript-eslint/no-unused-vars
            }
            catch (_fcError) {
                // Fallback: modo texto (también usa lightModel)
                this.logger.debug("Function calling not available for reflection, using text mode");
                const response = await this.openai.chat.completions.create({
                    model: this.lightModel,
                    messages: [
                        { role: "system", content: systemPrompt },
                        { role: "user", content: userMessage },
                    ],
                });
                const content = response.choices[0]?.message?.content?.trim() || "";
                if (response.usage) {
                    this.logger.logTokenUsage(requestId, "reflect-fix-text", response.usage.prompt_tokens, response.usage.completion_tokens, response.usage.total_tokens, this.lightModel);
                }
                // Extraer el razonamiento y la SQL corregida del texto
                const reasoningMatch = content.match(/RAZONAMIENTO:([\s\S]*?)SQL CORREGIDO:/i);
                const sqlMatch = content.match(/SQL CORREGIDO:([\s\S]*)/i);
                const reasoning = reasoningMatch
                    ? reasoningMatch[1].trim()
                    : "No reasoning provided";
                const fixedSql = sqlMatch ? sqlMatch[1].trim() : content;
                this.logger.debug("Generated reflection via text mode", {
                    reasoning,
                    fixedSql,
                });
                return { reasoning, fixedSqlCandidates: [fixedSql] };
            }
        }
        catch (error) {
            this.logger.error("Error generating reflection", {
                error: error.message,
            });
            throw new Error(`Failed to generate reflection: ${error.message}`);
        }
    }
    /**
     * Genera un hash del esquema de la base de datos para usar como clave de cache
     * @param schema - Esquema de la base de datos
     * @returns Hash del esquema
     */
    generateSchemaHash(schema) {
        try {
            const schemaString = JSON.stringify(schema);
            let hash = 0;
            for (let i = 0; i < schemaString.length; i++) {
                const char = schemaString.charCodeAt(i);
                hash = (hash << 5) - hash + char;
                hash = hash & hash; // Convertir a 32bit integer
            }
            return Math.abs(hash).toString(36);
        }
        catch (error) {
            this.logger.warn("Error generating schema hash, using default", {
                error: error.message,
            });
            return "default";
        }
    }
    /**
     * Obtiene estadísticas del cache
     * @returns Estadísticas del cache o null si está deshabilitado
     */
    getCacheStats() {
        if (!this.cache || !this.cacheEnabled) {
            return null;
        }
        return this.cache.getStats();
    }
    /**
     * Limpia el cache completamente
     */
    clearCache() {
        if (this.cache && this.cacheEnabled) {
            this.cache.clear();
            this.logger.info("Cache cleared successfully");
        }
    }
    /**
     * Invalida entradas del cache relacionadas con una tabla específica
     * @param tableName - Nombre de la tabla
     * @returns Número de entradas invalidadas
     */
    invalidateCacheByTable(tableName) {
        if (!this.cache || !this.cacheEnabled) {
            return 0;
        }
        const invalidated = this.cache.invalidateByTable(tableName);
        this.logger.info(`Invalidated ${invalidated} cache entries for table: ${tableName}`);
        return invalidated;
    }
    /**
     * Habilita o deshabilita el cache dinámicamente
     * @param enabled - Estado del cache
     */
    setCacheEnabled(enabled) {
        this.cacheEnabled = enabled;
        if (this.cache) {
            this.cache.setEnabled(enabled);
        }
        this.logger.info(`Cache ${enabled ? "enabled" : "disabled"}`);
    }
    /**
     * Verifica si el cache está habilitado
     * @returns Estado del cache
     */
    isCacheEnabled() {
        return this.cacheEnabled;
    }
    // ========== Cache de Schema ==========
    /**
     * Obtiene el esquema de la base de datos con cache TTL
     */
    async getSchemaWithCache() {
        const now = Date.now();
        if (this.cachedSchema && now - this.schemaCachedAt < this.schemaTTL) {
            this.logger.debug("Usando schema cacheado", {
                age: `${Math.round((now - this.schemaCachedAt) / 1000)}s`,
                ttl: `${this.schemaTTL / 1000}s`,
            });
            return this.cachedSchema;
        }
        this.logger.debug("Obteniendo schema fresco de la base de datos");
        const schema = await this.dbManager.getDatabaseSchema();
        this.cachedSchema = schema;
        this.schemaCachedAt = now;
        return schema;
    }
    /**
     * Fuerza el refresco del schema cacheado
     */
    refreshSchema() {
        this.cachedSchema = null;
        this.schemaCachedAt = 0;
        this.logger.info("Cache de schema invalidado — se refrescará en la próxima consulta");
    }
    // ========== Instrucciones Personalizadas ==========
    /**
     * Construye la sección de instrucciones personalizadas para los prompts
     */
    buildCustomInstructionsSection() {
        if (!this.schemaContext?.customInstructions?.length) {
            return "";
        }
        const instructions = this.schemaContext.customInstructions
            .map((instruction, i) => `${i + 1}. ${instruction}`)
            .join("\n");
        return `\nREGLAS PERSONALIZADAS DEL USUARIO:\n${instructions}\n`;
    }
    /**
     * Construye la instrucción de estilo de respuesta para los prompts
     */
    buildResponseStyleInstruction() {
        const style = this.schemaContext?.responseStyle;
        if (!style)
            return "";
        const styleMap = {
            concise: "Responde con respuestas breves y directas. Evita detalles innecesarios.",
            detailed: "Proporciona explicaciones completas con contexto e insights.",
            technical: "Usa lenguaje técnico e incluye detalles SQL en la respuesta.",
        };
        return `\nESTILO DE RESPUESTA: ${styleMap[style] || ""}\n`;
    }
    // ========== Seguimiento de Tokens ==========
    /**
     * Acumula el uso de tokens de una respuesta de OpenAI
     */
    accumulateTokens(accumulator, usage) {
        if (!accumulator)
            return;
        accumulator.promptTokens += usage.prompt_tokens;
        accumulator.completionTokens += usage.completion_tokens;
        accumulator.totalTokens += usage.total_tokens;
    }
    /**
     * Estima el costo en USD basado en el modelo y la cantidad de tokens.
     * Precios por 1M de tokens (actualizados a Feb 2025).
     */
    estimateTokenCost(promptTokens, completionTokens) {
        // Precios por 1M de tokens [input, output] en USD
        const pricing = {
            "gpt-4o": [2.5, 10.0],
            "gpt-4o-2024-11-20": [2.5, 10.0],
            "gpt-4o-2024-08-06": [2.5, 10.0],
            "gpt-4o-mini": [0.15, 0.6],
            "gpt-4-turbo": [10.0, 30.0],
            "gpt-4": [30.0, 60.0],
            "gpt-3.5-turbo": [0.5, 1.5],
        };
        // Buscar precio exacto o por prefijo
        let rates = pricing[this.openaiModel];
        if (!rates) {
            // Intentar match parcial (ej: "gpt-4o-mini-2024-07-18" → "gpt-4o-mini")
            const modelLower = this.openaiModel.toLowerCase();
            for (const [key, value] of Object.entries(pricing)) {
                if (modelLower.startsWith(key)) {
                    rates = value;
                    break;
                }
            }
        }
        if (!rates) {
            // Modelo desconocido — usar precio de gpt-4o-mini como fallback conservador
            rates = [0.15, 0.6];
        }
        const inputCost = (promptTokens / 1000000) * rates[0];
        const outputCost = (completionTokens / 1000000) * rates[1];
        // Redondear a 6 decimales para claridad
        return Math.round((inputCost + outputCost) * 1000000) / 1000000;
    }
    // ========== Historial de Consultas ==========
    /**
     * Obtiene el historial de ejecución de consultas
     * @param limit - Número máximo de registros a devolver
     */
    getQueryHistory(limit) {
        return this.queryHistory.getHistory(limit);
    }
    /**
     * Obtiene estadísticas sobre la ejecución de consultas
     */
    getQueryStats() {
        return this.queryHistory.getStats();
    }
    /**
     * Limpia el historial de consultas
     */
    clearQueryHistory() {
        this.queryHistory.clearHistory();
        this.logger.info("Historial de consultas limpiado");
    }
    /**
     * Exporta el historial de consultas como JSON
     */
    exportQueryHistory() {
        return this.queryHistory.exportHistory();
    }
}
exports.CyberMySQLOpenAI = CyberMySQLOpenAI;
exports.default = CyberMySQLOpenAI;
//# sourceMappingURL=cyberMySQLOpenAI.js.map