cyber-mysql-openai
Version:
Intelligent natural language to SQL translator with self-correction capabilities using OpenAI and MySQL
489 lines (486 loc) • 20.6 kB
JavaScript
"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");
/**
* 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;
// 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.maxReflections = config.maxReflections || config_1.DEFAULT_MAX_REFLECTIONS;
this.dbManager = new db_1.DBManager(dbConfig, this.logger);
this.responseFormatter = new responseFormatter_1.ResponseFormatter(openaiConfig.apiKey, openaiConfig.model, config.language || 'en', this.logger);
// 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');
}
this.logger.info('CyberMySQLOpenAI initialized successfully', {
model: this.openaiModel,
maxReflections: this.maxReflections,
language: this.i18n.getLanguage(),
cacheEnabled: this.cacheEnabled
});
}
/**
* 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 requestId = (0, uuid_1.v4)();
const startTime = Date.now();
try {
this.logger.info('Processing natural language query', { prompt });
// Paso 1: Obtener el esquema de la base de datos
const schema = await this.dbManager.getDatabaseSchema();
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`
});
return {
sql: cachedResult.sql,
results: cachedResult.results,
reflections: [],
attempts: 0,
success: true,
naturalResponse: cachedResult.naturalResponse,
executionTime,
fromCache: true
};
}
this.logger.info(`💫 Cache MISS for query: ${prompt.substring(0, 50)}...`);
}
// Paso 3: Generar SQL a partir del lenguaje natural
let sql = await this.generateSQL(prompt, schema, requestId);
// Limpiar la consulta SQL generada
sql = (0, sqlCleaner_1.cleanSqlResponse)(sql, 'generate', this.logger);
// 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;
}
catch (error) {
// Si falla, intentar reflexionar y corregir
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;
if (reflectionResult.success) {
results = reflectionResult.results;
success = true;
}
else {
results = [];
this.logger.error('Failed to execute query after reflection', { attempts });
}
}
// Paso 5: Generar respuesta en lenguaje natural
let naturalResponse = this.responseFormatter.generateSimpleResponse(sql, results);
if (!naturalResponse) {
naturalResponse = await this.responseFormatter.generateNaturalResponse(sql, results, { detailed: false });
}
// Generar respuesta detallada si se solicita
let detailedResponse;
if (options.detailed) {
try {
detailedResponse = await this.responseFormatter.generateNaturalResponse(sql, 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;
// Paso 6: Guardar en cache si fue exitoso
if (this.cache && this.cacheEnabled && success && naturalResponse) {
this.cache.set(prompt, this.i18n.getLanguage(), schemaHash, sql, results, naturalResponse, executionTime);
this.logger.info('💾 Result cached successfully');
}
// Paso 7: Devolver resultado
const result = {
sql,
results,
reflections,
attempts,
success,
naturalResponse,
executionTime,
fromCache: false
};
if (detailedResponse) {
result.detailedResponse = detailedResponse;
}
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
* @param prompt - Consulta en lenguaje natural
* @param schema - Esquema de la base de datos
* @param requestId - ID de la solicitud para logging
* @returns Consulta SQL generada
*/
async generateSQL(prompt, schema, requestId) {
try {
const tables = Object.keys(schema);
const schemaDescription = tables.map(tableName => {
const columns = schema[tableName];
const columnDescriptions = columns.map((col) => `${col.column_name} (${col.data_type}${col.column_key === 'PRI' ? ', PRIMARY KEY' : ''})`)
.join(', ');
return `Tabla ${tableName}: ${columnDescriptions}`;
}).join('\n\n');
// Usar el prompt localizado
const systemPrompt = this.i18n.getMessageWithReplace('prompts', 'translateToSQL', {
schema: schemaDescription,
query: prompt
});
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() || '';
// Registrar uso de tokens
if (response.usage) {
this.logger.logTokenUsage(requestId, 'generate-sql', response.usage.prompt_tokens, response.usage.completion_tokens, response.usage.total_tokens, this.openaiModel);
}
this.logger.debug('SQL generated from prompt', { sql });
return sql;
}
catch (error) {
this.logger.error('Error generating SQL from prompt', { error: error.message });
throw new Error(`Failed to generate SQL: ${error.message}`);
}
}
/**
* 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
*/
async reflectAndFix(prompt, sql, errorMessage, schema, requestId) {
const reflections = [];
let attempts = 1;
let currentSql = sql;
let results = [];
let success = false;
while (attempts <= this.maxReflections && !success) {
try {
// Generar reflexión sobre el error
const reflection = await this.generateReflection(prompt, currentSql, errorMessage, schema, requestId);
// Limpiar la SQL corregida
const correctedSql = (0, sqlCleaner_1.cleanSqlResponse)(reflection.fixedSql, 'reflect', this.logger);
reflections.push({
error: errorMessage,
reasoning: reflection.reasoning,
fixAttempt: correctedSql
});
// Intentar ejecutar la consulta corregida
results = await this.dbManager.executeReadOnlyQuery(correctedSql);
success = true;
currentSql = correctedSql;
this.logger.info('Query fixed successfully on attempt', { attempt: attempts });
}
catch (error) {
attempts++;
errorMessage = error.message;
this.logger.warn('Reflection attempt failed', {
attempt: attempts,
error: errorMessage
});
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
* @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 Reflexión y SQL corregido
*/
async generateReflection(prompt, sql, errorMessage, schema, requestId) {
try {
const tables = Object.keys(schema);
const schemaDescription = tables.map(tableName => {
const columns = schema[tableName];
const columnDescriptions = columns.map((col) => `${col.column_name} (${col.data_type}${col.column_key === 'PRI' ? ', PRIMARY KEY' : ''})`)
.join(', ');
return `Tabla ${tableName}: ${columnDescriptions}`;
}).join('\n\n');
// Usar el prompt localizado para corregir errores SQL
const systemPrompt = this.i18n.getMessageWithReplace('prompts', 'fixSQLError', {
error: errorMessage,
sql: sql,
schema: schemaDescription
});
const response = await this.openai.chat.completions.create({
model: this.openaiModel,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: `
Consulta original en lenguaje natural: ${prompt}
Consulta SQL que falló:
${sql}
Error recibido:
${errorMessage}
Por favor, analiza el error y corrige la consulta SQL.
` }
]
});
const content = response.choices[0]?.message?.content?.trim() || '';
// Registrar uso de tokens
if (response.usage) {
this.logger.logTokenUsage(requestId, 'reflect-fix', response.usage.prompt_tokens, response.usage.completion_tokens, response.usage.total_tokens, this.openaiModel);
}
// Extraer el razonamiento y la SQL corregida
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 se proporcionó razonamiento';
const fixedSql = sqlMatch ? sqlMatch[1].trim() : content;
this.logger.debug('Generated reflection', { reasoning, fixedSql });
return {
reasoning,
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;
}
}
exports.CyberMySQLOpenAI = CyberMySQLOpenAI;
exports.default = CyberMySQLOpenAI;
//# sourceMappingURL=cyberMySQLOpenAI.js.map