Curso IA Conversacional con PostgreSQL - Guía Completa

Chateando con tu Base de Datos PostgreSQL Usando IA Local

⏱️ Tiempo estimado: 120+ minutos
📚 Nivel: Intermedio

¡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]

🎯 Objetivo Principal: Tu Asistente de Datos Personal

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].

Representación de un agente conversacional interactuando con una base de datos

Construiremos un LLM local que dialoga con tu base de datos PostgreSQL.

🏗️ Arquitectura General y Tecnologías

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:

  • Python 3.10+: Para construir el agente conversacional con LangChain[cite: 1532].
  • LangChain: Una librería de Python que facilita la creación de aplicaciones potenciadas por LLMs, incluyendo la conversión de lenguaje natural a SQL[cite: 1523].
  • Ollama: La plataforma que nos permitirá descargar y ejecutar LLMs (como Llama 2, Mistral, etc.) directamente en nuestra máquina[cite: 1523, 1536].
  • Node.js 18+: Lo usaremos para implementar un servidor ligero que actuará como puente (mediante un protocolo conceptualmente similar a MCP) entre el LLM y la base de datos PostgreSQL[cite: 1524, 1533].
  • PostgreSQL: Nuestra base de datos de ejemplo. Puedes usar una instancia local o una en Docker[cite: 1522, 1534].

¿Qué es MCP (Model Context Protocol)?

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].

🛠️ Preparativos y Configuración del Entorno Local

Antes de empezar, asegúrate de tener instaladas y configuradas las siguientes herramientas en tu sistema local[cite: 1532, 1533, 1534, 1535, 1536].

1. Instalación de Python y Node.js

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:

2. Instalación y Configuración de Ollama

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].

Pasos para instalar Ollama:

  1. Visita el sitio oficial de Ollama y descarga el instalador para tu sistema operativo (Windows, macOS o Linux)[cite: 1539].
  2. Sigue las instrucciones de instalación. Por ejemplo, en macOS, se descarga una app que debes mover a la carpeta Aplicaciones y luego ejecutar para que instale la interfaz de línea de comandos[cite: 1540].
  3. Una vez instalado, abre una terminal y verifica que el comando 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:

Terminal
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:

Terminal
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].

3. Configuración de la Base de Datos PostgreSQL

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].

Estructura de la tabla `ingresos`:

  • 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]

Script SQL para crear la tabla e insertar datos de ejemplo:

SQL
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].

🖥️ Implementación del Servidor MCP en Node.js

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].

1. Crear Proyecto Node.js e Instalar Dependencias

Crea una carpeta para el servidor (ej: mcp-server), inicializa un proyecto Node.js y instala las dependencias:

Terminal
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].

2. Código del Servidor MCP (index.js)

Crea un archivo index.js en la carpeta mcp-server con el siguiente contenido[cite: 1574]:

mcp-server/index.js
// 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".

Probando el servidor MCP:

  • Inspeccionar esquema: Visita 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]
  • Probar consulta SELECT: Usa curl para hacer un POST a http://localhost:3000/query:
    Terminal
    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'\''" }'
    Debería responder con [{"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].

🐍 Creación del Agente Conversacional con LangChain (Python)

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].

1. Instalación de Paquetes Python

En tu entorno Python (se recomienda un virtualenv), instala:

Terminal
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].

2. Script del Agente Python

Crea un archivo Python (ej: agente_sql.py) con el siguiente contenido:

agente_sql.py
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].

3. Ejecutar y Probar el Agente

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:

Terminal
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].

🛡️ Buenas Prácticas de Seguridad

Al permitir que un LLM genere y ejecute consultas SQL, la seguridad es primordial[cite: 1654].

  • Permisos de Base de Datos: Usa un usuario PostgreSQL de solo lectura para la conexión desde el servidor MCP[cite: 1655, 1566]. Esto previene modificaciones accidentales o maliciosas.
  • Validación en el Servidor MCP: El servidor Node.js ya valida que solo se ejecuten consultas SELECT[cite: 1586, 1657]. Se podrían añadir más filtros (lista blanca de palabras clave, etc.)[cite: 1658].
  • Limitación de Resultados: El servidor MCP limita la salida a 100 filas para evitar sobrecarga[cite: 1660, 1661].
  • Sandbox de Herramientas: MCP y LangChain ayudan a encapsular las herramientas. No expongas funcionalidades innecesarias o peligrosas[cite: 1663, 1664, 1665, 1666].
  • Supervisión: Con 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].
  • Privacidad y Datos Locales: Dado que todo corre localmente, tus datos permanecen en tu máquina[cite: 1670, 1671, 1672]. Sin embargo, evita introducir datos extremadamente sensibles directamente en los prompts del LLM[cite: 1673].

📱 (Opcional) Futura Integración con WhatsApp

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:

  1. Elegir un proveedor de API de WhatsApp (oficial o vía Twilio)[cite: 1677, 1678].
  2. Crear un webhook para recibir mensajes de WhatsApp[cite: 1681, 1682].
  3. Conectar este webhook con tu agente LangChain (ya sea directamente en Python o mediante una llamada HTTP al proceso del agente)[cite: 1684, 1685, 1688].
  4. Enviar la respuesta del agente de vuelta a WhatsApp[cite: 1690, 1691, 1692, 1693].

Esto permitiría a los usuarios interactuar con la base de datos a través de WhatsApp[cite: 1695, 1696, 1697].

🎉 Conclusiones de la Guía

¡Felicidades! Has construido un agente conversacional SQL completamente local[cite: 1701]. Hemos integrado:

  • Ollama + LLM local: Para procesamiento de lenguaje natural sin depender de la nube[cite: 1702].
  • LangChain (Python): Para orquestar el LLM y las herramientas[cite: 1703, 1704].
  • Servidor MCP (Node.js): Como capa de seguridad y puente a PostgreSQL[cite: 1705, 1706, 1707].
  • PostgreSQL: Nuestra fuente de datos[cite: 1708].

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:

  • Añadir más herramientas o capacidades al servidor MCP[cite: 1712].
  • Probar diferentes modelos LLM en Ollama, quizás algunos especializados en español o text-to-SQL[cite: 1713].
  • Implementar memoria conversacional en LangChain para diálogos más largos[cite: 1714].
  • Refinar aún más la seguridad y el monitoreo[cite: 1715].

Esperamos que esta guía te haya brindado una comprensión clara y práctica. ¡Feliz desarrollo! [cite: 1716, 1717, 1718]