SAP Datasphere + ChatGPT: Cómo construir una capa de análisis de autoservicio

Descubra cómo combinar SAP Datasphere y ChatGPT para crear una capa de análisis de autoservicio. Permita que los usuarios de negocio en Latinoamérica consulten datos SAP con lenguaje natural, reduciendo la dependencia de TI.

SAP Datasphere + ChatGPT: Cómo construir una capa de análisis de autoservicio

El problema del que nadie habla en SAP Analytics

SAP lleva más de una década prometiendo análisis de autoservicio. Desde las consultas de BW hasta Lumira y Analytics Cloud, cada generación de herramientas ha hecho la misma promesa: los usuarios de negocio podrán responder sus propias preguntas sin esperar a TI. Y cada generación se ha quedado corta por la misma razón: las herramientas aún requieren que los usuarios comprendan el modelo de datos.

Saber que sus ingresos por ventas residen en la tabla de hechos ACDOCA con un filtro de fecha de contabilización en BUDAT, el código de empresa en RBUKRS y el centro de beneficio en PRCTR no es algo que un gerente de ventas regional deba saber. Ellos quieren preguntar "¿cuáles fueron nuestros 10 principales clientes por ingresos en el primer trimestre de 2026 para la región de EMEA?" y obtener una respuesta. No un tutorial sobre cómo construir una consulta.

SAP Datasphere (el renombrado Data Warehouse Cloud) ha mejorado mucho el problema de la capa de datos. La federación, la replicación y la capa de negocio semántica le brindan una vista unificada de los datos SAP y no SAP. Pero la última milla, permitir que los usuarios no técnicos consulten esos datos de forma natural, sigue faltando en el producto estándar.

Esa es la brecha que vamos a llenar. Construiremos una interfaz de lenguaje natural sobre SAP Datasphere que utiliza GPT-4 de OpenAI (a través de Generative AI Hub de SAP AI Core, o directamente) para traducir preguntas en español simple a consultas SQL, ejecutarlas contra Datasphere y devolver resultados formateados. Todo el sistema tardó tres semanas en construirse y ha estado funcionando en producción desde noviembre de 2025.

Resumen de la arquitectura

Esto es lo que estamos construyendo:

  • SAP Datasphere — la plataforma de datos, que contiene datos replicados y federados de S/4HANA, BW/4HANA y fuentes externas.
  • Un servicio de middleware en Python — maneja la conversación, la búsqueda de metadatos y la generación de consultas.
  • OpenAI GPT-4 (o GPT-4o) — traduce el lenguaje natural a SQL.
  • Una interfaz web sencilla — interfaz tipo chat donde los usuarios escriben preguntas.
  • SAP Analytics Cloud (opcional) — para usuarios que desean visualizar los resultados como gráficos.

El truco que hace que esto realmente funcione (a diferencia de las docenas de demostraciones de "chatea con tu base de datos" que fallan con datos reales) es la capa de metadatos semánticos. No solo le lanzamos el esquema de tabla sin procesar a GPT-4 y esperamos lo mejor. Construimos un catálogo de metadatos curado que describe cada tabla y columna en lenguaje de negocio, incluye ejemplos de valores válidos, especifica relaciones de unión y proporciona plantillas de consulta para preguntas comunes.

Configuración de SAP Datasphere para acceso API

Primero, asegurémonos de que su inquilino de Datasphere sea accesible a través de la API. Necesitará:

  1. Un inquilino de SAP Datasphere con la capacidad "Consumption API" habilitada.
  2. Un usuario de base de datos con acceso de lectura a sus modelos analíticos.
  3. Los detalles de conexión ODBC/JDBC de la configuración de acceso a la base de datos de Datasphere.

En Datasphere, vaya a Gestión de Espacios > su espacio > Acceso a la Base de Datos. Cree un usuario de base de datos si aún no lo ha hecho. Anote el host, el puerto y el nombre del esquema; los necesitará para conectarse desde Python.

# Detalles de conexión desde el acceso a la base de datos de Datasphere
DSP_HOST = "your-tenant.hana.prod-eu10.hanacloud.ondemand.com"
DSP_PORT = 443
DSP_USER = "your_db_user"
DSP_PASSWORD = "your_password"  # Use variables de entorno en producción
DSP_SCHEMA = "your_space_schema"

Para la conectividad de Python, usamos la biblioteca hdbcli (cliente SAP HANA para Python):

from hdbcli import dbapi

def get_datasphere_connection():
    conn = dbapi.connect(
        address=DSP_HOST,
        port=DSP_PORT,
        user=DSP_USER,
        password=DSP_PASSWORD,
        encrypt=True,
        sslValidateCertificate=True
    )
    return conn

def execute_query(query, params=None):
    conn = get_datasphere_connection()
    cursor = conn.cursor()
    try:
        cursor.execute(query, params or [])
        columns = [desc[0] for desc in cursor.description]
        rows = cursor.fetchall()
        return columns, rows
    finally:
        cursor.close()
        conn.close()

Construcción de la capa de metadatos semánticos

Esta es la parte más importante de todo el sistema. Sin buenos metadatos, GPT-4 generará SQL sintácticamente correcto que devolverá resultados incorrectos. Y los resultados incorrectos que parecen correctos son mucho peores que un mensaje de error.

Cree un archivo de catálogo de metadatos (yo uso YAML, pero JSON también funciona):

# metadata/sales_analytics.yaml
tables:
  - name: V_SALES_ORDERS
    description: "Datos de pedidos de venta, incluyendo detalles de cabecera y partida, replicados de las tablas S/4HANA VBAK/VBAP"
    business_context: "Contiene todos los pedidos de venta. Use esto para análisis de ingresos, seguimiento de volumen de pedidos y patrones de compra de clientes."
    columns:
      - name: VBELN
        description: "Número de pedido de venta"
        business_name: "Número de Pedido"
        data_type: "NVARCHAR(10)"
        example_values: ["0000012345", "0000067890"]

      - name: ERDAT
        description: "Fecha de creación del pedido"
        business_name: "Fecha de Pedido"
        data_type: "DATE"
        notes: "Formato AAAA-MM-DD. Para análisis trimestral, use QUARTER(ERDAT)"

      - name: KUNNR
        description: "Número de cliente, clave foránea a V_CUSTOMERS"
        business_name: "Cliente"
        data_type: "NVARCHAR(10)"
        join_to: "V_CUSTOMERS.KUNNR"

      - name: NETWR
        description: "Valor neto del pedido en moneda del documento"
        business_name: "Valor del Pedido"
        data_type: "DECIMAL(15,2)"
        aggregation: "SUM para totales, AVG para valor promedio del pedido"
        notes: "Siempre empareje con WAERK (moneda) al mostrar. Para informes consolidados, use NETWR_LC (moneda local) o NETWR_GC (moneda del grupo)"

      - name: WAERK
        description: "Código de moneda del documento"
        business_name: "Moneda"
        data_type: "NVARCHAR(5)"
        example_values: ["EUR", "USD", "GBP"]

      - name: VKORG
        description: "Organización de ventas"
        business_name: "Org. de Ventas"
        data_type: "NVARCHAR(4)"
        valid_values:
          "1000": "EMEA"
          "2000": "Américas"
          "3000": "APAC"

      - name: AUART
        description: "Tipo de documento de ventas"
        business_name: "Tipo de Pedido"
        data_type: "NVARCHAR(4)"
        valid_values:
          "TA": "Pedido Estándar"
          "KE": "Reposición de Consignación"
          "RE": "Devoluciones"

      - name: NETWR_GC
        description: "Valor neto del pedido en moneda del grupo (EUR)"
        business_name: "Valor del Pedido (EUR)"
        data_type: "DECIMAL(15,2)"
        aggregation: "SUM para totales. Use esto para comparaciones interregionales"

  - name: V_CUSTOMERS
    description: "Datos maestros de clientes de KNA1/KNB1"
    business_context: "Detalles del cliente, incluyendo nombre, región, industria y datos de crédito"
    columns:
      - name: KUNNR
        description: "Número de cliente"
        business_name: "Número de Cliente"
        data_type: "NVARCHAR(10)"

      - name: NAME1
        description: "Nombre del cliente"
        business_name: "Nombre del Cliente"
        data_type: "NVARCHAR(35)"

      - name: LAND1
        description: "Código de país"
        business_name: "País"
        data_type: "NVARCHAR(3)"

      - name: BRSCH
        description: "Código de sector industrial"
        business_name: "Industria"
        data_type: "NVARCHAR(4)"
        valid_values:
          "0001": "Manufactura"
          "0002": "Minorista"
          "0003": "Servicios"
          "0004": "Tecnología"

common_queries:
  - question: "principales clientes por ingresos"
    template: |
      SELECT c.NAME1 as customer_name, SUM(s.NETWR_GC) as total_revenue
      FROM V_SALES_ORDERS s JOIN V_CUSTOMERS c ON s.KUNNR = c.KUNNR
      WHERE s.ERDAT BETWEEN '{start_date}' AND '{end_date}'
      AND s.AUART = 'TA'
      GROUP BY c.NAME1 ORDER BY total_revenue DESC LIMIT {limit}

  - question: "ingresos por región"
    template: |
      SELECT s.VKORG as sales_org, SUM(s.NETWR_GC) as revenue
      FROM V_SALES_ORDERS s
      WHERE s.ERDAT BETWEEN '{start_date}' AND '{end_date}'
      AND s.AUART NOT IN ('RE')
      GROUP BY s.VKORG ORDER BY revenue DESC

Fíjese en la cantidad de contexto que estamos proporcionando. Cada columna tiene un nombre de negocio, valores de ejemplo, mapeos de valores válidos y notas sobre cómo usarla correctamente. La sección common_queries le da a GPT-4 plantillas para preguntas frecuentes, reduciendo la posibilidad de errores.

El motor de generación de consultas

Aquí está el núcleo del sistema: el módulo que toma una pregunta en lenguaje natural, la combina con metadatos y le pide a GPT-4 que genere SQL:

import openai
import yaml
import json

class QueryGenerator:
    def __init__(self, metadata_path, openai_api_key):
        with open(metadata_path) as f:
            self.metadata = yaml.safe_load(f)
        self.client = openai.OpenAI(api_key=openai_api_key)
        self.conversation_history = []

    def _build_system_prompt(self):
        schema_description = []
        for table in self.metadata['tables']:
            cols = []
            for col in table['columns']:
                col_desc = f"  - {col['name']} ({col['business_name']}): {col['description']}"
                if 'valid_values' in col:
                    col_desc += f" Valid values: {json.dumps(col['valid_values'])}"
                if 'notes' in col:
                    col_desc += f" Note: {col['notes']}"
                cols.append(col_desc)

            schema_description.append(
                f"Table: {table['name']}\n"
                f"Description: {table['description']}\n"
                f"Business context: {table['business_context']}\n"
                f"Columns:\n" + "\n".join(cols)
            )

        common_q = ""
        if 'common_queries' in self.metadata:
            for q in self.metadata['common_queries']:
                common_q += f"\nExample - '{q['question']}':\n{q['template']}\n"

        return f"""Eres un generador de consultas SQL para SAP Datasphere (sintaxis HANA SQL).
Traduces preguntas en lenguaje natural a consultas SQL.

REGLAS IMPORTANTES:
1. Solo usa tablas y columnas del esquema a continuación. Nunca inventes columnas.
2. Usa la sintaxis HANA SQL (TOP no es compatible, usa LIMIT en su lugar).
3. Al comparar fechas, usa el formato TO_DATE('AAAA-MM-DD').
4. Siempre excluye las devoluciones (AUART = 'RE') de los cálculos de ingresos a menos que se solicite.
5. Usa NETWR_GC (moneda del grupo EUR) para comparaciones interregionales.
6. Cuando se pregunte sobre "Q1", "Q2", etc., mapea a: Q1=Ene-Mar, Q2=Abr-Jun, Q3=Jul-Sep, Q4=Oct-Dic.
7. Devuelve SOLAMENTE la consulta SQL, sin explicación.
8. Si la pregunta no puede ser respondida con las tablas disponibles, di "CANNOT_ANSWER:" seguido de una explicación.

ESQUEMA:
{chr(10).join(schema_description)}

CONSULTAS DE EJEMPLO:
{common_q}"""

    def generate_query(self, user_question):
        if not self.conversation_history:
            self.conversation_history.append({
                "role": "system",
                "content": self._build_system_prompt()
            })

        self.conversation_history.append({
            "role": "user",
            "content": user_question
        })

        response = self.client.chat.completions.create(
            model="gpt-4",
            messages=self.conversation_history,
            temperature=0.1,
            max_tokens=500
        )

        sql = response.choices[0].message.content.strip()

        self.conversation_history.append({
            "role": "assistant",
            "content": sql
        })

        return sql

Validación y seguridad de consultas

No se puede ejecutar SQL generado por IA sin validación. GPT-4 es bueno, pero ocasionalmente genera consultas que escanearían tablas enteras, producirían productos cartesianos o (en casos adversos) intentarían modificar datos.

import sqlparse

class QueryValidator:
    BLOCKED_KEYWORDS = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER',
                         'CREATE', 'TRUNCATE', 'MERGE', 'GRANT', 'REVOKE']

    MAX_RESULT_ROWS = 10000

    def __init__(self, allowed_tables):
        self.allowed_tables = {t.upper() for t in allowed_tables}

    def validate(self, sql):
        errors = []

        # Check for data modification attempts
        parsed = sqlparse.parse(sql)
        for statement in parsed:
            stmt_type = statement.get_type()
            if stmt_type and stmt_type.upper() != 'SELECT':
                errors.append(f"Solo se permiten sentencias SELECT, se obtuvo {stmt_type}")

        # Check for blocked keywords
        sql_upper = sql.upper()
        for keyword in self.BLOCKED_KEYWORDS:
            if keyword in sql_upper.split():
                errors.append(f"Palabra clave bloqueada encontrada: {keyword}")

        # Extract table names and validate
        tables_used = self._extract_tables(sql)
        unauthorized = tables_used - self.allowed_tables
        if unauthorized:
            errors.append(f"Tablas no autorizadas: {unauthorized}")

        # Ensure LIMIT clause exists
        if 'LIMIT' not in sql_upper:
            sql = sql.rstrip(';') + f' LIMIT {self.MAX_RESULT_ROWS}'

        return errors, sql

    def _extract_tables(self, sql):
        # Simple table extraction - works for common patterns
        tables = set()
        tokens = sql.upper().split()
        for i, token in enumerate(tokens):
            if token in ('FROM', 'JOIN'):
                if i + 1 < len(tokens):
                    table_name = tokens[i + 1].strip('(),')
                    if table_name and not table_name.startswith('('):
                        tables.add(table_name)
        return tables

El validador verifica tres cosas: ninguna instrucción de modificación de datos, solo tablas en la lista blanca y un límite de filas obligatorio. El usuario de la base de datos que creamos en Datasphere también tiene acceso de solo lectura, por lo que incluso si una consulta maliciosa pasa la validación, la base de datos no permitirá modificaciones.

Construyendo la capa de conversación

Un sistema de consulta única es útil, pero el análisis de autoservicio real requiere un contexto conversacional. Los usuarios quieren hacer una pregunta, ver los resultados y luego hacer un seguimiento: "ahora desglosa eso por trimestre" o "excluye el sector gubernamental".

class AnalyticsAssistant:
    def __init__(self, metadata_path, openai_key, datasphere_config):
        self.generator = QueryGenerator(metadata_path, openai_key)
        self.validator = QueryValidator(
            allowed_tables=[t['name'] for t in self.generator.metadata['tables']]
        )
        self.dsp_config = datasphere_config
        self.query_history = []

    def ask(self, question):
        # Generate SQL from natural language
        sql = self.generator.generate_query(question)

        # Check if the model said it can't answer
        if sql.startswith('CANNOT_ANSWER:'):
            return {
                'status': 'cannot_answer',
                'message': sql.replace('CANNOT_ANSWER:', '').strip(),
                'sql': None,
                'data': None
            }

        # Validate the generated SQL
        errors, validated_sql = self.validator.validate(sql)
        if errors:
            return {
                'status': 'validation_error',
                'message': f"Fallo en la validación de la consulta: {'; '.join(errors)}",
                'sql': sql,
                'data': None
            }

        # Execute against Datasphere
        try:
            columns, rows = execute_query(validated_sql)
            self.query_history.append({
                'question': question,
                'sql': validated_sql,
                'row_count': len(rows)
            })

            return {
                'status': 'success',
                'sql': validated_sql,
                'columns': columns,
                'data': rows,
                'row_count': len(rows)
            }
        except Exception as e:
            # If query fails, ask GPT-4 to fix it
            fix_response = self.generator.generate_query(
                f"La consulta anterior falló con el error: {str(e)}. "
                f"Por favor, corrige la consulta SQL para responder la pregunta original."
            )
            errors2, fixed_sql = self.validator.validate(fix_response)
            if not errors2:
                try:
                    columns, rows = execute_query(fixed_sql)
                    return {
                        'status': 'success',
                        'sql': fixed_sql,
                        'columns': columns,
                        'data': rows,
                        'row_count': len(rows),
                        'note': 'La consulta fue corregida automáticamente después del fallo inicial'
                    }
                except Exception as e2:
                    pass

            return {
                'status': 'error',
                'message': f"Fallo en la ejecución de la consulta: {str(e)}",
                'sql': validated_sql,
                'data': None
            }

Observe el bucle de autocorrección. Cuando una consulta generada falla (generalmente debido a una falta de coincidencia en el nombre de la columna o un problema de sintaxis), el sistema envía el error de vuelta a GPT-4 y le pide que corrija la consulta. En mi experiencia, esta autocorrección tiene éxito aproximadamente el 60% de las veces, lo que significa que los usuarios ven errores con menos frecuencia.

La interfaz web

El frontend es una aplicación Flask simple con una interfaz estilo chat. Nada sofisticado; el valor está en el backend, no en la interfaz de usuario:

from flask import Flask, render_template, request, jsonify, session
import uuid

app = Flask(__name__)
app.secret_key = 'your-secret-key'

assistants = {}  # session_id -> AnalyticsAssistant

@app.route('/ask', methods=['POST'])
def ask():
    session_id = session.get('id')
    if not session_id:
        session_id = str(uuid.uuid4())
        session['id'] = session_id

    if session_id not in assistants:
        assistants[session_id] = AnalyticsAssistant(
            metadata_path='metadata/sales_analytics.yaml',
            openai_key=OPENAI_API_KEY,
            datasphere_config=DSP_CONFIG
        )

    question = request.json.get('question', '')
    result = assistants[session_id].ask(question)

    # Format the response
    if result['status'] == 'success':
        # Convert to HTML table
        html_table = format_as_html_table(result['columns'], result['data'])
        return jsonify({
            'answer': html_table,
            'sql': result['sql'],
            'row_count': result['row_count'],
            'status': 'success'
        })
    else:
        return jsonify({
            'answer': result['message'],
            'sql': result.get('sql'),
            'status': result['status']
        })

Precisión y rendimiento en el mundo real

Después de ejecutar este sistema con 34 usuarios activos en los departamentos de ventas, finanzas y cadena de suministro durante cinco meses, aquí están las cifras reales:

MétricaValor
Total de preguntas realizadas4,847
Consultas exitosas (resultados correctos)3,974 (82%)
Consultas autocorrección387 (8%)
Consultas fallidas (usuario notificado)486 (10%)
Tiempo de respuesta promedio3.2 segundos
Preguntas por usuario por semana7.1
Tickets de TI anteriores para informes ad-hoc (mensual)45
Tickets de TI actuales para informes ad-hoc (mensual)8

La tasa de precisión del 82% puede parecer baja, pero el contexto importa. Antes de este sistema, esos 34 usuarios tenían que construir sus propias historias de SAC (lo que la mayoría no podía hacer) o enviar tickets de TI y esperar de 3 a 5 días hábiles para obtener una respuesta. La tasa de fallas del 10% proviene principalmente de preguntas que hacen referencia a datos que aún no están disponibles en Datasphere (datos de RRHH, por ejemplo) o preguntas que son realmente ambiguas ("¿cómo nos va?" no es una consulta que ningún sistema pueda responder).

Los tipos de preguntas que funcionan mejor

  • Consultas de agregación: "¿Cuál es el ingreso total por organización de ventas en el primer trimestre de 2026?" — 95% de precisión
  • Consultas de clasificación: "Los 20 principales clientes por número de pedidos este año" — 93% de precisión
  • Consultas de comparación: "Comparar ingresos del primer trimestre de 2025 vs primer trimestre de 2026 por región" — 88% de precisión
  • Consultas de filtrado: "Mostrar todos los pedidos superiores a USD 100K de clientes alemanes" — 91% de precisión
  • Consultas de tendencias: "Tendencia de ingresos mensuales durante los últimos 12 meses" — 85% de precisión
  • Uniones complejas: "Ingresos por industria de cliente para la región APAC" — 78% de precisión

Los tipos que tienen dificultades

  • Cálculos que implican varios pasos: "Tasa de crecimiento interanual por categoría de producto" — 62% de precisión (requiere consultas anidadas)
  • Preguntas con referencias de tiempo ambiguas: "Pedidos recientes" — 55% de precisión (¿qué tan reciente es "reciente"?)
  • Preguntas que requieren conocimientos de negocio no incluidos en los metadatos: "¿Qué clientes corren riesgo de abandono?" — 40% de precisión (el modelo no conoce su definición de abandono)

Mejora de la precisión con el tiempo

La capa de metadatos es la principal palanca para mejorar la precisión. Cada vez que una consulta falla o devuelve resultados incorrectos, analizo el fallo y añado información a los metadatos:

  1. Añadir más consultas de ejemplo. Cuando los usuarios hacen preguntas de forma consistente en un patrón con el que el sistema tiene dificultades, añado una plantilla a la sección common_queries. Esto mejoró la precisión de la comparación interanual del 62% al 84%.
  2. Ampliar los mapeos de valores válidos. Los usuarios dicen "Alemania" pero la base de datos almacena "DE". Añadir mapeos de nombre de país a código en los metadatos permite que GPT-4 realice la traducción automáticamente.
  3. Añadir anotaciones de reglas de negocio. "Ingresos" siempre debe excluir las devoluciones (AUART = 'RE') y las transferencias internas (AUART = 'ZIV'). Documentar esto en los metadatos significa que GPT-4 aplica estos filtros automáticamente.
  4. Crear definiciones de vista para patrones complejos. Cuando una pregunta requiere una consulta compleja de múltiples uniones con la que GPT-4 tiene dificultades, cree una vista de Datasphere que pre-una las tablas y añada esa vista a los metadatos como una tabla única y simple.

Análisis de costos

Hablemos de lo que cuesta ejecutar esto:

  • Costos de la API de OpenAI: Promedio de 4,800 consultas/mes x ~2,000 tokens por consulta x USD 0.03/1K tokens (GPT-4) = ~USD 288/mes. Cambiar a GPT-4o reduce esto a ~USD 48/mes con una precisión comparable para la generación de SQL.
  • SAP Datasphere: Ya pagado como parte de la licencia SAP de la organización. El costo incremental para el usuario de la API es insignificante.
  • Infraestructura: Una pequeña VM para el middleware de Python (~USD 50/mes) y alojamiento para el frontend web (~USD 20/mes).
  • Costo mensual total: ~USD 120/mes con GPT-4o, ~USD 360/mes con GPT-4.

Compare esto con el costo de 45 tickets de TI al mes para informes ad-hoc. A una tarifa de TI interna de USD 120 por ticket (incluido el tiempo del analista, el tiempo de espera y la revisión), eso es USD 5,400 al mes en mano de obra de TI. El sistema ahorra más de USD 5,000 al mes, al mismo tiempo que brinda a los usuarios respuestas instantáneas en lugar de una espera de 3 a 5 días.

Consideraciones de seguridad

Conectar un sistema de IA a sus datos SAP plantea preocupaciones de seguridad legítimas. Así es como las manejamos:

  • Los datos no van a OpenAI. Solo la pregunta y el esquema de metadatos se envían a la API. Los resultados reales de la consulta permanecen dentro de su red. GPT-4 genera SQL; nunca ve los datos.
  • Seguridad a nivel de fila. Las vistas de Datasphere utilizan privilegios analíticos para restringir los datos según el perfil de autorización del usuario que ha iniciado sesión. Un gerente de ventas en EMEA solo ve datos de EMEA, incluso si el SQL generado no filtra por región.
  • Registro de auditoría de consultas. Cada pregunta, SQL generado y tamaño del conjunto de resultados se registra con la identidad del usuario y la marca de tiempo. Esto crea un rastro de auditoría completo.
  • Sanitización de entradas. El validador de SQL evita ataques de inyección, y el usuario de la base de datos tiene permisos de solo lectura. Incluso si alguien intenta ser ingenioso, el peor resultado es una consulta fallida.

Integración con SAP Analytics Cloud

Algunos usuarios prefieren resultados visuales en lugar de tablas. Para estos usuarios, agregamos una integración opcional con SAP Analytics Cloud (SAC). Cuando una consulta devuelve datos de series temporales o categóricos, el sistema puede generar automáticamente una historia de SAC a través de la API de SAC:

def push_to_sac(columns, data, chart_type='bar'):
    # Create a dataset in SAC
    sac_client = SACClient(
        tenant_url=SAC_TENANT_URL,
        client_id=SAC_CLIENT_ID,
        client_secret=SAC_CLIENT_SECRET
    )

    # Upload results as a SAC dataset
    dataset_id = sac_client.create_dataset(
        name=f"NL_Query_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
        columns=columns,
        data=data
    )

    # Generate a simple story with the chart
    story_url = sac_client.create_story(
        dataset_id=dataset_id,
        chart_type=chart_type,
        title="Analytics Query Result"
    )

    return story_url

Esta es la parte más débil del sistema, sinceramente. La API de SAC para la creación programática de historias es limitada y los resultados parecen genéricos. La mayoría de los usuarios terminan prefiriendo las tablas HTML porque se cargan instantáneamente, mientras que la integración de SAC agrega de 8 a 10 segundos de latencia. Pero para los usuarios que necesitan incrustar resultados en presentaciones, tener la opción de SAC es valioso.

Lecciones de cinco meses en producción

Los metadatos nunca están terminados. Dedico unas 2 horas a la semana a mantener y ampliar el catálogo de metadatos. Se añaden nuevas tablas a Datasphere, cambian las reglas de negocio, los usuarios hacen preguntas sobre datos que antes no estaban mapeados. Este es un trabajo operativo continuo, no una configuración única.

Los usuarios necesitan capacitación para hacer buenas preguntas. "Muéstrame todo" no es una consulta útil. Realizamos una sesión de capacitación de 30 minutos para mostrar a los usuarios cómo hacer preguntas específicas y respondibles. La tasa de fallos disminuyó del 18% al 10% después de la capacitación.

GPT-4 es mejor en HANA SQL de lo que cabría esperar. El modelo maneja correctamente funciones específicas de HANA como ADD_DAYS, DAYSBETWEEN, ISOWEEK y QUARTER sin necesidad de indicaciones especiales. También maneja bien las jerarquías y las funciones de ventana, aunque los cálculos LAG/LEAD a veces producen errores de sintaxis que la autocorrección detecta.

El almacenamiento en caché de preguntas repetidas ahorra dinero y tiempo. Alrededor del 30% de las preguntas son variantes de la misma consulta (solo con diferentes rangos de fechas o filtros). Una capa de caché simple que reconoce la similitud semántica redujo los costos de la API en un 25% y el tiempo de respuesta en un 60% para las consultas en caché.

No subestime el valor político. Cuando un director financiero puede escribir "ingresos vs. presupuesto por centro de costos para el primer trimestre" y obtener una respuesta instantánea durante una reunión de junta, la credibilidad del equipo de datos se dispara. Varios de nuestros usuarios mencionaron específicamente esta capacidad al abogar por el presupuesto del equipo de datos durante la planificación anual. El sistema se paga solo con la credibilidad organizacional.

¿Qué sigue?

Estamos trabajando en tres mejoras:

  1. Refinamiento de consultas de varios turnos con vista previa de datos. Mostrar las primeras 5 filas de resultados y preguntar "¿es esto lo que esperabas?" antes de devolver el conjunto de datos completo. Esto detecta problemas de columna incorrecta y filtro incorrecto a tiempo.
  2. Generación automatizada de metadatos. Usar GPT-4 para leer las definiciones de tablas de Datasphere y generar el catálogo de metadatos inicial, que luego es revisado y corregido por un humano. Esto reduciría el tiempo de configuración de nuevas tablas de horas a minutos.
  3. Entrada de voz a través de Microsoft Teams. Varios usuarios pidieron la capacidad de hacer preguntas por voz en las reuniones de Teams. Estamos prototipando un bot de Teams que acepta voz, transcribe a través de Whisper, genera la consulta y devuelve los resultados como una tarjeta de Teams.

El patrón central —lenguaje natural enriquecido con metadatos a SQL— es aplicable mucho más allá de SAP. Cualquier fuente de datos estructurada con buenos metadatos puede consultarse de esta manera. Pero es en SAP donde el patrón ofrece el mayor valor porque los modelos de datos de SAP son notoriamente difíciles de navegar para los usuarios no técnicos. Cuando se elimina esa barrera, la demanda de análisis no solo aumenta, sino que cambia fundamentalmente quién en la organización puede tomar decisiones basadas en datos.