¡Bienvenido/a a esta guía completa! Aquí aprenderás, paso a paso, a construir tu propio asistente inteligente capaz de entender tus preguntas en español y buscar respuestas directamente en tu base de datos PostgreSQL. Lo mejor de todo: ¡correrá 100% en tu máquina local! [cite: 1522, 1523]
El objetivo es crear un agente conversacional local que pueda conectarse a una base de datos PostgreSQL y responder preguntas en lenguaje natural (español) sobre los datos almacenados[cite: 1522]. Imagina preguntarle a tu computadora "¿Cuánto se percibió en abril?" o "Muéstrame los clientes de la zona norte" y obtener respuestas directas y precisas de tus datos[cite: 1530, 1561].
Para lograrlo, utilizaremos modelos de lenguaje grandes (LLM) ejecutados localmente a través de Ollama, y herramientas de la librería LangChain en Python para convertir esas preguntas en consultas SQL que nuestra base de datos pueda entender[cite: 1523].
Construiremos un LLM local que dialoga con tu base de datos PostgreSQL.
La solución que construiremos tendrá una arquitectura donde un LLM puede usar herramientas externas mediante un protocolo conceptualmente similar a MCP (Model Context Protocol), pero todo corriendo localmente[cite: 1525].
Componentes principales que usaremos:
Model Context Protocol (MCP) es un protocolo abierto estandarizado que permite conectar modelos de lenguaje con herramientas y fuentes de datos externas de forma segura[cite: 1527]. Básicamente, MCP define cómo un cliente (por ejemplo, una interfaz de chat con un LLM) puede invocar herramientas definidas en un servidor MCP[cite: 1528]. En nuestro proyecto, el servidor MCP en Node.js expondrá herramientas para inspeccionar el esquema de la base de datos y ejecutar consultas SQL de solo lectura[cite: 1529]. Esto permitirá que el modelo, ante una pregunta como "¿Cuánto se percibió en abril?", pueda, mediante MCP, invocar la herramienta adecuada para obtener la respuesta de la base de datos[cite: 1530, 1531].
Antes de empezar, asegúrate de tener instaladas y configuradas las siguientes herramientas en tu sistema local[cite: 1532, 1533, 1534, 1535, 1536].
Asegúrate de tener Python (3.10 o superior) y Node.js (18 o superior) instalados en tu sistema. Puedes descargarlos de sus sitios oficiales:
Ollama es la plataforma que nos permitirá ejecutar LLMs localmente[cite: 1536]. Ollama es un framework de código abierto que facilita descargar y ejecutar modelos de lenguaje en local, ofreciendo incluso una interfaz API local[cite: 1538].
ollama
funciona (ollama help
debería mostrar la ayuda)[cite: 1541].Cuando Ollama se ejecuta, levanta un servicio local (por defecto en el puerto 11434)[cite: 1542]. La primera vez, probablemente descargará un modelo por defecto (por ejemplo, Llama 2 7B)[cite: 1543]. Puedes hacerlo manualmente ejecutando:
ollama run llama2
Este comando descargará el modelo "llama2" (Llama 2 de 7 mil millones de parámetros, ~3.8GB) si no lo tienes, y luego iniciará un chat interactivo con él[cite: 1544]. Ten en cuenta que descargar modelos grandes puede tardar.
Una vez que tengas al menos un modelo descargado, asegúrate de que el servidor local de Ollama esté activo. Por defecto, Ollama inicia un servidor HTTP en http://localhost:11434
que expone una API de inferencia[cite: 1547, 1548]. Puedes probar que funciona haciendo una petición curl
desde otra terminal, por ejemplo:
curl http://localhost:11434/api/generate -d '{
"model": "llama2",
"prompt": "¿Por qué el cielo es azul?"
}'
Si todo está correcto, Ollama recibirá la solicitud, utilizará el modelo indicado (aquí "llama2") para generar una respuesta, y te devolverá un JSON con la respuesta[cite: 1549, 1550].
Necesitamos una base de datos PostgreSQL con la cual nuestro agente pueda interactuar[cite: 1554]. Si ya tienes una base de datos local con datos, puedes usarla[cite: 1555]. De lo contrario, puedes crear una pequeña base de ejemplo[cite: 1556].
Para este tutorial, imaginemos una base de datos sencilla llamada "finanzas" que contiene una tabla de ingresos mensuales[cite: 1557].
id
(entero, llave primaria, autoincremental)fecha
(date, la fecha del ingreso)monto
(numeric, el monto de dinero ingresado en esa fecha)concepto
(texto, opcionalmente una descripción del ingreso) [cite: 1558, 1559]CREATE TABLE ingresos (
id SERIAL PRIMARY KEY,
fecha DATE,
monto NUMERIC(10,2),
concepto TEXT
);
INSERT INTO ingresos (fecha, monto, concepto) VALUES
('2025-03-10', 10000.00, 'Venta ABC'),
('2025-03-15', 8500.50, 'Venta XYZ'),
('2025-04-01', 12000.00, 'Servicio 123'),
('2025-04-20', 15000.00, 'Proyecto Acme'),
('2025-04-30', 5000.00, 'Venta DEF');
Con estos datos, una pregunta como "¿Cuánto se percibió en abril?" debería llevar al agente a sumar los montos de abril 2025 (12000.00 + 15000.00 + 5000.00 = 32000.00)[cite: 1561, 1562].
Asegúrate de poder conectarte a esta base de datos desde Node.js y Python[cite: 1565]. Es altamente recomendable crear un usuario PostgreSQL con privilegios de solo lectura sobre esta base de datos para mayor seguridad[cite: 1566, 1567].
Este servidor actuará como puente entre el LLM y PostgreSQL, exponiendo herramientas para obtener información de la base de datos[cite: 1568, 1569]. Para simplificar, implementaremos un servicio HTTP REST local[cite: 1570, 1571].
Crea una carpeta para el servidor (ej: mcp-server
), inicializa un proyecto Node.js y instala las dependencias:
mkdir mcp-server
cd mcp-server
npm init -y
npm install pg express
Usaremos el paquete pg
para PostgreSQL y express
para crear los endpoints HTTP[cite: 1573].
index.js
)Crea un archivo index.js
en la carpeta mcp-server
con el siguiente contenido[cite: 1574]:
// index.js
const express = require('express');
const { Client } = require('pg');
const app = express();
app.use(express.json());
// Configura la conexión a PostgreSQL
const client = new Client({
host: 'localhost',
port: 5432,
user: 'tu_usuario_postgres', // Reemplaza con tu usuario
password: 'tu_password_postgres', // Reemplaza con tu contraseña
database: 'finanzas' // Nombre de tu base de datos
});
client.connect().catch(err => {
console.error('Error conectando a PostgreSQL:', err);
process.exit(1);
});
// Endpoint 1: Obtener esquema (tablas y columnas)
app.get('/schema', async (req, res) => {
try {
const result = await client.query(
`SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position;`
);
const schemaInfo = {};
result.rows.forEach(row => {
const { table_name, column_name, data_type } = row;
if (!schemaInfo[table_name]) {
schemaInfo[table_name] = [];
}
schemaInfo[table_name].push(\`\${column_name} (\${data_type})\`);
});
res.json(schemaInfo);
} catch (err) {
console.error('Error al obtener esquema:', err);
res.status(500).send('Error interno al obtener el esquema');
}
});
// Endpoint 2: Ejecutar una consulta SQL (solo lectura)
app.post('/query', async (req, res) => {
const sql = req.body.query;
if (typeof sql !== 'string') {
return res.status(400).send('Falta la consulta SQL en el cuerpo de la petición');
}
// Seguridad: prevenir consultas no permitidas
const lowerSql = sql.trim().toLowerCase();
if (!lowerSql.startsWith('select')) {
return res.status(403).send('Solo se permiten consultas SELECT en este servicio.');
}
try {
const result = await client.query(sql);
const rows = result.rows;
// Limitamos filas devueltas para evitar respuestas enormes
if (rows.length > 100) { // [cite: 1660]
return res.json(rows.slice(0, 100)); // máx 100 filas
}
res.json(rows);
} catch (err) {
console.error('Error al ejecutar consulta SQL:', err);
res.status(500).send(\`Error en la consulta: \${err.message}\`);
}
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(\`Servidor MCP escuchando en http://localhost:\${PORT}\`);
});
Recuerda reemplazar 'tu_usuario_postgres'
y 'tu_password_postgres'
con tus credenciales reales de PostgreSQL[cite: 1575].
Ejecuta el servidor con node index.js
en la carpeta mcp-server
[cite: 1583]. Deberías ver "Servidor MCP escuchando en http://localhost:3000".
http://localhost:3000/schema
en tu navegador o con curl
. Debería devolver un JSON con las tablas y columnas, por ejemplo:
{
"ingresos": [
"id (integer)",
"fecha (date)",
"monto (numeric)",
"concepto (text)"
]
}
[cite: 1584]
curl
para hacer un POST a http://localhost:3000/query
:
curl -X POST http://localhost:3000/query \
-H "Content-Type: application/json" \
-d '{ "query": "SELECT SUM(monto) as total_abril FROM ingresos WHERE fecha >= '\''2025-04-01'\'' AND fecha < '\''2025-05-01'\''" }'
[{"total_abril":"32000.00"}]
[cite: 1585].
Este servidor expone dos operaciones: /schema
para conocer la estructura de la base de datos y /query
para ejecutar consultas SELECT[cite: 1586].
Ahora construiremos el agente en Python que usará el LLM local (vía Ollama) y nuestro servidor MCP para responder preguntas sobre la base de datos[cite: 1593].
El flujo será: Usuario pregunta -> Agente con LLM determina si usar DB -> Formula SQL -> Invoca herramienta (servidor MCP) -> LLM genera respuesta final[cite: 1594, 1595, 1596, 1597].
En tu entorno Python (se recomienda un virtualenv), instala:
pip install langchain langchain-ollama requests
langchain
es la biblioteca principal, langchain-ollama
integra con Ollama, y requests
para llamadas HTTP al servidor MCP[cite: 1601, 1602, 1603].
Crea un archivo Python (ej: agente_sql.py
) con el siguiente contenido:
from langchain_ollama import OllamaLLM
import requests
from langchain.agents import Tool, initialize_agent, AgentType
# 1. Configurar el LLM local en LangChain
llm = OllamaLLM(
model="llama2", # Nombre del modelo cargado en Ollama
base_url="http://localhost:11434", # Endpoint local de Ollama [cite: 1606]
# Opcional: ajustar parámetros
# temperature=0.1
)
# 2. Definir herramientas para el agente
# Herramienta: obtener esquema de la base de datos
def obtener_esquema(_=None) -> str:
"""Devuelve el esquema (tablas y columnas) de la base de datos en formato texto."""
try:
res = requests.get("http://localhost:3000/schema", timeout=5)
if res.status_code == 200:
schema = res.json()
esquema_texto = ""
for tabla, columnas in schema.items():
esquema_texto += f"Tabla {tabla}:\\n"
for col in columnas:
esquema_texto += f"- {col}\\n"
return esquema_texto.strip()
else:
return f"(Error obteniendo esquema: {res.status_code} {res.text})"
except Exception as e:
return f"(No se pudo obtener el esquema: {e})"
# Herramienta: ejecutar consulta SQL
def ejecutar_sql(consulta: str) -> str:
"""Ejecuta una consulta SQL SELECT en la base de datos y devuelve los resultados en texto."""
try:
payload = {"query": consulta}
# Asegurarse que la consulta sea SELECT
if not consulta.strip().lower().startswith("select"):
return "(Error: Solo se permiten consultas SELECT)"
res = requests.post("http://localhost:3000/query", json=payload, timeout=10)
if res.status_code == 200:
data = res.json()
if not data:
return "(La consulta no devolvió filas)"
# Formateo simple de resultados
if isinstance(data, list) and len(data) > 0 and isinstance(data[0], dict):
if len(data[0]) == 1: # Un solo valor/agregación
key = list(data[0].keys())[0]
return f"{key}: {data[0][key]}"
# Formato tabular simple
headers = list(data[0].keys())
header_line = " | ".join(headers)
rows_lines = [header_line, "---" * len(header_line)]
for row_dict in data:
row_values = [str(row_dict.get(h, '')) for h in headers]
rows_lines.append(" | ".join(row_values))
return "\\n".join(rows_lines)
return str(data) # Fallback
else:
return f"(Error SQL: {res.status_code} {res.text})"
except Exception as e:
return f"(Error de conexión al ejecutar SQL: {e})"
herramientas = [
Tool(
name="obtener_esquema_bd",
func=obtener_esquema,
description="Útil para obtener el esquema de las tablas y columnas disponibles en la base de datos PostgreSQL."
),
Tool(
name="ejecutar_consulta_sql",
func=ejecutar_sql,
description="Útil para ejecutar una consulta SQL SELECT sobre la base de datos PostgreSQL y obtener el resultado. El input debe ser una consulta SQL SELECT válida."
)
]
# 3. Crear el agente conversacional
# Usaremos ZERO_SHOT_REACT_DESCRIPTION que funciona bien para decidir herramientas
agente = initialize_agent(
tools=herramientas,
llm=llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True, # Para ver los pasos del agente
handle_parsing_errors=True # Para manejar errores de parseo del LLM
)
# 4. Probar el agente
if __name__ == "__main__":
print("Agente SQL local listo. Escribe 'salir' para terminar.")
while True:
pregunta_usuario = input("Pregunta: ")
if pregunta_usuario.lower() == 'salir':
break
if pregunta_usuario:
try:
respuesta_agente = agente.run(pregunta_usuario)
print("Respuesta del Agente:", respuesta_agente)
except Exception as e:
print(f"Error al procesar la pregunta: {e}")
Este script configura el LLM[cite: 1604, 1606], define las herramientas `obtener_esquema` y `ejecutar_sql` que se comunican con el servidor Node.js[cite: 1609, 1610, 1611, 1615, 1616], y crea un agente que puede usar estas herramientas[cite: 1620, 1622].
Asegúrate de que tu servidor MCP (Node.js) esté corriendo en una terminal, y que el servidor de Ollama también esté activo.
En otra terminal (con tu entorno Python activado), ejecuta el script del agente:
python agente_sql.py
Ahora puedes hacerle preguntas en español. Por ejemplo:
¿Cuánto se percibió en abril?
[cite: 1626]Lista las tablas de la base de datos.
¿Cuáles son las columnas de la tabla ingresos?
Si verbose=True
está activado, verás el "razonamiento" del LLM: cómo decide qué herramienta usar, qué consulta SQL formula, y cómo interpreta la respuesta para dartela en lenguaje natural[cite: 1623, 1627, 1628, 1629, 1630, 1631, 1632, 1633, 1634, 1635, 1636, 1637, 1638, 1639, 1640].
Al permitir que un LLM genere y ejecute consultas SQL, la seguridad es primordial[cite: 1654].
SELECT
[cite: 1586, 1657]. Se podrían añadir más filtros (lista blanca de palabras clave, etc.)[cite: 1658].verbose=True
en el agente, puedes monitorear las consultas generadas[cite: 1667, 1668]. Ajusta los prompts o descripciones de herramientas si es necesario[cite: 1669].Aunque no lo implementaremos en esta guía, la arquitectura está preparada para una futura integración con APIs como la de WhatsApp[cite: 1526, 1676]. Esto implicaría:
Esto permitiría a los usuarios interactuar con la base de datos a través de WhatsApp[cite: 1695, 1696, 1697].
¡Felicidades! Has construido un agente conversacional SQL completamente local[cite: 1701]. Hemos integrado:
Ahora puedes hacer preguntas en español a tu base de datos y obtener respuestas coherentes, todo en tu entorno local, priorizando la privacidad y el control[cite: 1709, 1710].
Siguientes pasos posibles:
Esperamos que esta guía te haya brindado una comprensión clara y práctica. ¡Feliz desarrollo! [cite: 1716, 1717, 1718]