<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Tomás Murúa - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Tomás Murúa - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/es/search-labs/author/tomas-murua</link>
    </image>
    <link>https://www.elastic.co/es/search-labs/author/tomas-murua</link>
    <atom:link href="https://www.elastic.co/es/search-labs/rss/author/tomas-murua.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[es]]></language>
    <lastBuildDate>Tue, 22 Sep 2026 11:53:55 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Crear un conector de ChatGPT con Elasticsearch para buscar incidencias de GitHub.]]></title>
    <description><![CDATA[Aprende cómo crear un conector personalizado de ChatGPT y desplegar un servidor MCP de Elasticsearch que utiliza una búsqueda híbrida para buscar incidencias internas de GitHub.]]></description>
    <content:encoded><![CDATA[<p>Recientemente, OpenAI anunció la característica de <a href="https://help.openai.com/en/articles/11487775-connectors-in-chatgpt">conectores personalizados</a> para ChatGPT en los planes Pro/Business/Enterprise y Edu. Además de los conectores listos para usar que permiten acceder a datos en Gmail, GitHub, Dropbox, etc. Es posible crear conectores personalizados utilizando servidores MCP.</p><p>Los conectores personalizados te permiten combinar tus conectores de ChatGPT existentes con otras fuentes de datos como Elasticsearch para obtener respuestas integrales.</p><p>En este artículo, crearemos un servidor <a href="https://modelcontextprotocol.io/docs/getting-started/intro">MCP</a> que conecta ChatGPT a un índice de Elasticsearch que contiene información sobre incidencias internas de GitHub y solicitudes de extracción. Esto permite responder a búsquedas en lenguaje natural mediante los datos de Elasticsearch.</p><p>Desplegaremos el servidor MCP utilizando <a href="https://gofastmcp.com/getting-started/welcome">FastMCP</a> en Google Colab con ngrok para obtener una URL pública a la que ChatGPT pueda conectarse, lo que eliminará la necesidad de una infraestructura compleja.</p><p>Para una visión general del MCP y su ecosistema, consulta <a href="https://www.elastic.co/search-labs/blog/mcp-current-state">El estado actual de MCP</a>.</p><h2>Prerrequisitos</h2><p>Antes de comenzar, necesitarás:</p><ul><li><p>Clúster de Elasticsearch (8.X o superior).</p></li><li><p>Clave API de Elasticsearch con acceso de lectura a tu índice.</p></li><li><p>Cuenta de Google (para Google Colab)</p></li><li><p>Cuenta de Ngrok (el nivel gratuito funciona)</p></li><li><p>Cuenta de ChatGPT con plan Pro/Enterprise/Business o Edu.</p></li></ul><h2>Comprensión de los requisitos del conector MCP de ChatGPT.</h2><p>Los conectores MCP de ChatGPT requieren la implementación de dos herramientas: <code>search</code> y <code>fetch</code>. Para más detalles, consulta <a href="https://platform.openai.com/docs/mcp#create-an-mcp-server">OpenAI Docs</a>.</p><h3><a href="https://platform.openai.com/docs/mcp#search-tool">Herramienta de búsqueda</a></h3><p>Devuelve una lista de resultados relevantes de tu índice de Elasticsearch según la búsqueda del usuario.</p><h4>Lo que recibe:</h4><ul><li><p>Un solo texto con la búsqueda de lenguaje natural del usuario.</p></li><li><p>Ejemplo: “Encuentra incidencias relacionadas con la migración de Elasticsearch”.</p></li></ul><h4>Lo que devuelve: </h4><ul><li><p>Un objeto con una clave <code>result</code> que contiene un arreglo de objetos de resultado. Cada resultado incluye:</p><ul><li><p><code>id</code> - Identificador único de documentos.</p></li><li><p><code>title</code> - Título de la incidencia o PR.</p></li><li><p><code>url</code> - Enlace a la incidencia o PR.</p></li></ul></li></ul><h4>En nuestra implementación:</h4>return {
    "results": [
        {
            "id": "PR-612",
            "title": "Fix memory leak in WebSocket notification service",
            "url": "https://internal-git.techcorp.com/pulls/612"
        },
        # ... more results
    ]
}<h3><a href="https://platform.openai.com/docs/mcp#fetch-tool">Herramienta de extracción</a></h3><p>Recupera el contenido completo de un documento específico.</p><h4>Lo que recibe:</h4><ul><li><p>Una sola cadena de texto con el ID del documento de Elasticsearch del resultado de la búsqueda.</p></li><li><p>Ejemplo: “Consígueme los detalles de PR-578”.</p></li></ul><h4>Lo que devuelve:</h4><ul><li><p>Un objeto de documento completo con:</p><ul><li><p><code>id</code> - Identificador único de documentos.</p></li><li><p><code>title</code> - Título de la incidencia o PR.</p></li><li><p><code>text</code> - Descripción completa del problema/PR y sus detalles</p></li><li><p><code>url</code> - Enlace a la incidencia o PR.</p></li><li><p><code>type</code> - Tipo de documento (incidencia, pull_request).</p></li><li><p><code>status</code> - Estado actual (abierto, en progreso, resuelto)</p></li><li><p><code>priority</code> - Nivel de prioridad (bajo, medio, alto, crítico)</p></li><li><p><code>assignee</code> - Persona asignada al problema/PR</p></li><li><p><code>created_date</code> - Fecha de creación.</p></li><li><p><code>resolved_date</code> - Cuando se resolvió (si procede)</p></li><li><p><code>labels</code> - Etiquetas asociadas al documento</p></li><li><p><code>related_pr</code> - ID de solicitud de extracción relacionado</p></li></ul></li></ul>return {
    "id": "PR-578",
    "title": "Security hotfix: Patch SQL injection vulnerabilities",
    "text": "Description: CRITICAL SECURITY FIX for ISSUE-1889. Patches SQL...",
    "url": "https://internal-git.techcorp.com/pulls/578",
    "type": "pull_request",
    "status": "closed",
    "priority": "critical",
    "assignee": "sarah_dev",
    "created_date": "2025-09-19",
    "resolved_date": "2025-09-19",
    "labels": "security, hotfix, sql",
    "related_pr": null
}<p><strong>Nota</strong>: Este ejemplo usa una estructura plana donde todos los campos están en el nivel raíz. Los requisitos de OpenAI son flexibles y también admiten objetos de metadatos anidados.</p><h2>Sets de datos de incidencias y PR de GitHub</h2><p>Para este tutorial, vamos a usar un set de datos interno de GitHub que contenga incidencias y solicitudes de extracción. Esto representa un escenario en el que deseas buscar datos internos privados a través de ChatGPT.</p><p>Los sets de datos se pueden encontrar <a href="https://gist.github.com/TomasMurua/4e7bbdf7a7ebbdffaa663c43578d934a">aquí</a>. Y actualizaremos el índice de los datos mediante la <a href="https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk">API de bulk</a>.</p><p>Este sets de datos incluye:</p><ul><li><p>Problemas con descripciones, estado, prioridad y responsables.</p></li><li><p>Solicitudes de extracción con cambios de código, revisiones e información de despliegue.</p></li><li><p>Relaciones entre incidencias y PR (p. ej., PR-578 soluciona ISSUE-1889).</p></li><li><p>Etiquetas, fechas y otros metadatos</p></li></ul><h3>Mappings de índices</h3><p>El índice utiliza los siguientes <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">mappings</a> para brindar soporte a la búsqueda híbrida con <a href="https://www.elastic.co/docs/explore-analyze/machine-learning/nlp/ml-nlp-elser">ELSER</a>. El campo <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text">text_semantic</a> se utiliza para la búsqueda semántica, mientras que los demás campos permiten la búsqueda por palabras clave.</p>{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "title": {
        "type": "text"
      },
      "text": {
        "type": "text"
      },
      "text_semantic": {
        "type": "semantic_text",
        "inference_id": ".elser-2-elasticsearch"
      },
      "url": {
        "type": "keyword"
      },
      "type": {
        "type": "keyword"
      },
      "status": {
        "type": "keyword"
      },
      "priority": {
        "type": "keyword"
      },
      "assignee": {
        "type": "keyword"
      },
      "created_date": {
        "type": "date",
        "format": "iso8601"
      },
      "resolved_date": {
        "type": "date",
        "format": "iso8601"
      },
      "labels": {
        "type": "keyword"
      },
      "related_pr": {
        "type": "keyword"
      }
    }
  }
}<h2>Construye el servidor MCP</h2><p>Nuestro servidor MCP implementa dos herramientas que siguen las especificaciones de OpenAI, y utilizan búsquedas híbridas para combinar coincidencia semántica y textual para obtener mejores resultados.</p><h3>Herramienta de búsqueda</h3><p>Usa la búsqueda híbrida con <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/reciprocal-rank-fusion">RRF</a> (Fusión de Rango Recíproco), combinando la búsqueda semántica con la coincidencia de texto:</p>@mcp.tool()
    async def search(query: str) -&gt; Dict[str, List[Dict[str, Any]]]:
        """
        Search for internal issues and PRs using hybrid search (semantic + text with RRF).
        Returns list with id, title, and url per OpenAI spec.
        """
        if not query or not query.strip():
            return {"results": []}

        logger.info(f"Searching for: '{query}'")

        try:
            # Hybrid search with RRF (Reciprocal Rank Fusion)
            response = es_client.search(
                index=ELASTICSEARCH_INDEX,
                size=10,
                source=["id", "title", "url", "type", "priority"],
                retriever={
                    "rrf": {
                        "retrievers": [
                            {
                                # Semantic search with ELSER
                                "standard": {
                                    "query": {
                                        "semantic": {
                                            "field": "text_semantic",
                                            "query": query
                                        }
                                    }
                                }
                            },
                            {
                                # Text search (BM25) for keyword matching
                                "standard": {
                                    "query": {
                                        "multi_match": {
                                            "query": query,
                                            "fields": [
                                                "title^3",
                                                "text^2",
                                                "assignee^2",
                                                "type",
                                                "labels",
                                                "priority"
                                            ],
                                            "type": "best_fields",
                                            "fuzziness": "AUTO"
                                        }
                                    }
                                }
                            }
                        ],
                        "rank_window_size": 50,
                        "rank_constant": 60
                    }
                }
            )

            results = []
            if response and 'hits' in response:
                for hit in response['hits']['hits']:
                    source = hit['_source']
                    results.append({
                        "id": source.get('id', hit['_id']),
                        "title": source.get('title', 'Unknown'),
                        "url": source.get('url', '')
                    })

            logger.info(f"Found {len(results)} results")
            return {"results": results}

        except Exception as e:
            logger.error(f"Search error: {e}")
            raise ValueError(f"Search failed: {str(e)}")<h3>Puntos clave:</h3><ul><li><p><strong>Búsqueda híbrida con RRF:</strong> Combina búsqueda semántica (ELSER) y búsqueda por texto (BM25) para mejores resultados.</p></li><li><p><strong>Búsqueda de múltiples coincidencias:</strong> <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-multi-match-query">Busca en múltiples campos</a> con mejores ponderaciones (título^3, texto^2, responsable^2). El símbolo de intercalación (^) multiplica las puntuaciones de relevancia, y prioriza las coincidencias en los títulos sobre el contenido.</p></li><li><p><strong>Correspondencia aproximada:</strong> <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/common-options#fuzziness"><code>fuzziness: AUTO</code></a> maneja los errores tipográficos y ortográficos al permitir coincidencias aproximadas.</p></li><li><p><strong>Ajuste de parámetros de RRF:</strong></p><ul><li><p><code>rank_window_size: 50</code> - Especifica cuántos resultados principales de cada recuperador (semántico y textual) se consideran antes de combinarlos.</p></li><li><p><code>rank_constant: 60</code> - Este valor determina cuánta influencia tienen los documentos en los conjuntos de resultados individuales sobre el resultado final clasificado.</p></li></ul></li><li><p><strong>Solo devuelve los campos obligatorios:</strong> <code>id</code>, <code>title</code>, <code>url</code> según la especificación de OpenAI, y evita exponer otros campos innecesariamente.</p></li></ul><h3>Herramienta de extracción</h3><p>Recupera los detalles del documento por ID de documento, si existe:</p>@mcp.tool()
    async def fetch(id: str) -&gt; Dict[str, Any]:
        """
        Retrieve complete issue/PR details by ID.
        Returns id, title, text, url.
        """
        if not id:
            raise ValueError("ID is required")

        logger.info(f"Fetching: {id}")

        try:
            # Search by the 'id' field (not _id) since IDs are stored as a field
            response = es_client.search(
                index=ELASTICSEARCH_INDEX,
                body={
                    "query": {
                        "term": {
                            "id": id  # Search by your custom 'id' field
                        }
                    },
                    "size": 1
                }
            )

            if not response or not response['hits']['hits']:
                raise ValueError(f"Document with id '{id}' not found")

            hit = response['hits']['hits'][0]
            source = hit['_source']

            result = {
                "id": source.get('id', id),
                "title": source.get('title', 'Unknown'),
                "text": source.get('text', ''),
                "url": source.get('url', ''),
                "type": source.get('type', ''),
                "status": source.get('status', ''),
                "priority": source.get('priority', ''),
                "assignee": source.get('assignee', ''),
                "created_date": source.get('created_date', ''),
                "resolved_date": source.get('resolved_date', ''),
                "labels": source.get('labels', ''),
                "related_pr": source.get('related_pr', '')
            }

            logger.info(f"Fetched: {result['title']}")
            return result

        except Exception as e:
            logger.error(f"Fetch error: {e}")
            raise ValueError(f"Failed to fetch '{id}': {str(e)}")<h3>Puntos clave:</h3><ul><li><p><strong>Búsqueda por campo de ID de documento:</strong> usa la búsqueda de término en el campo personalizado <code>id</code>.</p></li><li><p><strong>Devuelve el documento completo:</strong> incluye el campo completo <code>text</code> con todo el contenido.</p></li><li><p><strong>Estructura plana:</strong> todos los campos en el nivel raíz, coincidiendo con la estructura de documentos de Elasticsearch.</p></li></ul><h2>Desplegar en Google Colab</h2><p>Usaremos Google Colab para ejecutar nuestro servidor MCP y ngrok para exponerlo públicamente de forma tal que ChatGPT pueda conectarse.</p><h3>Paso 1: Abre el cuaderno de Google Colab.</h3><p>Accede a nuestro cuaderno preconfigurado <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/elasticsearch-chatgpt-connector">Elasticsearch MCP para ChatGPT</a>.</p><h3>Paso 2: Configura tus credenciales</h3><p>Necesitarás tres datos:</p><ul><li><p><strong>URL de Elasticsearch:</strong> Tu <a href="https://www.elastic.co/docs/deploy-manage/deploy/cloud-enterprise/connect-elasticsearch">URL del cluster de Elasticsearch</a>.</p></li><li><p><strong>Clave API de Elasticsearch:</strong> <a href="https://www.elastic.co/docs/deploy-manage/api-keys/elasticsearch-api-keys">Clave API</a> con acceso de lectura a tu índice.</p></li><li><p><strong>Token de autenticación ngrok:</strong> Token gratis de <a href="https://ngrok.com/">ngrok</a>. Usaremos ngrok para exponer la URL de MCP a internet y así ChatGPT pueda conectarse.</p></li></ul><h4>Obtener tu token ngrok</h4><ol><li><p>Regístrate para una cuenta gratis en <a href="https://ngrok.com/">ngrok</a></p></li><li><p>Ve a tu <a href="https://dashboard.ngrok.com/">dashboard de ngrok</a></p></li><li><p>Copia tu token de autenticación</p></li></ol><h4>Agregar secretos a Google Colab</h4><p>En el cuaderno de Google Colab:</p><ol><li><p>Haz clic en el <strong>icono de llave </strong>en la barra lateral izquierda para abrir <strong>Secretos</strong>.</p></li><li><p>Añade estos tres secretos:</p></li></ol>ELASTICSEARCH_URL=https://your-cluster.elastic.com:443
ELASTICSEARCH_API_KEY=your-api-key
NGROK_TOKEN=your-ngrok-token<p>3.   Habilitar el acceso al cuaderno para cada secreto</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5acae97b386277f8/6a17f08f5ea30f74c964b6c2/d5dd6ac19fe816a562c6351fdb0f11369da0e877-609x321.jpg" alt="Agregar secretos a Google Collab" /><h3>Paso 3: Ejecutar el cuaderno</h3><ol><li><p>Haz clic en <strong>Tiempo de ejecución</strong> y, a continuación, en <strong>Ejecutar todo</strong> para ejecutar todas las celdas.</p></li><li><p>Espera que el servidor se inicie (aproximadamente 30 segundos).</p></li><li><p>Busque la salida que muestre su URL pública de ngrok</p></li></ol><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdd11aacf2deab67c/6a17f091e8fbce81f13a1a41/f185100e8869624bc9e1c7b2b4eb32785e2d89e7-1189x283.png" alt="" /><p>4. La salida mostrará algo como:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8891d917fdbaaf48/6a17f092abe0f208c7dfeaf6/e02e625e91ed9136454e4401b184575fb03a336e-1052x465.jpg" alt="La salida de ejecutar un cuaderno en Google Collab" /><h2>Conéctate a ChatGPT.</h2><p>Ahora conectaremos el servidor MCP a tu cuenta de ChatGPT.</p><ol><li><p>Abre ChatGPT y ve a <strong>Configuración</strong>.</p></li><li><p>Navega a <strong>Conectores</strong>.Si estás usando una cuenta Pro, debes activar el <a href="https://platform.openai.com/docs/guides/developer-mode">modo de desarrollador</a> en los conectores.</p></li></ol><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt95efdcb2c39307e7/6a17f094abe0f24d8edfeafa/32c02192912fc0e7e5a52e9399077ba7ae3b4901-739x715.png" alt="Conectando el servidor MPC a una cuenta de ChatGPT" /><p><em>Si estás usando la versión Enterprise o Business de ChatGPT, debes publicar el conector en tu lugar de trabajo.</em></p><p>3. Haz clic en <strong>Crear.</strong></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd4c8fc8dd6033918/6a17f095631730de19585b7b/15c53e5ccc381108a9dc0052cca05bf0fc97679a-755x683.png" alt="Agregar un conector a ChatGPT" /><p><em><strong>Nota</strong></em><em>: En los espacios de trabajo Business, Enterprise y Edu, solo los propietarios, administradores y usuarios que tengan habilitada la correspondiente configuración (para Enterprise/Edu) pueden agregar conectores personalizados. Los usuarios con un rol de miembro común no tienen la capacidad de agregar conectores personalizados por su cuenta.</em></p><p><em>Una vez que un propietario o usuario administrador agrega un conector y lo habilita, estará disponible para que lo usen todos los miembros del espacio de trabajo.</em></p><p>4. Introduce la información requerida y la URL de tu ngrok que termina en <code>/sse/</code>. Recuerda la “/” después de “sse”. No funcionará si no la agregas:</p><ul><li><p><strong>Nombre:</strong> Elasticsearch MCP</p></li><li><p><strong>Descripción: </strong>MCP personalizado para buscar y extraer información interna de GitHub.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd716ad0beeeb1d35/6a17f09714d90c11cc79b6d7/162a85705cc8ac48a3f2f665551d513e0719f93d-479x684.png" alt="Crear un conector MCP de Elastic " /><p>5. Presione <strong>Crear</strong> para guardar el MCP personalizado.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt857794237d7d3b5a/6a17f0983e03d729b74f2d54/97eb5fb0a32b86bfadfb35561f698616f217c049-913x629.png" alt="Guardar el conector MCP personalizado haciendo clic en crear" /><p>La conexión es instantánea si tu servidor está en funcionamiento. No se necesita ninguna otra autenticación, ya que la clave API de Elasticsearch está configurada en tu servidor.</p><h2>Prueba el servidor MCP</h2><p>Antes de hacer preguntas, necesitas seleccionar qué conector ChatGPT debe usar.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd1602c48878dc7a9/6a17f09a6df731cca90a0fff/77a6fc1eb263a0eb16aac64f2ecaca5f4ac12ec2-966x568.gif" alt="Seleccionar qué conector debe usar ChatGPT" /><h3>Indicación 1: Buscar incidencias</h3><p>Pregunta: “<strong>Encuentre incidencias relacionadas con la migración de Elasticsearch” </strong>y confirma la llamada a la herramienta de acciones.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt6c204ceacf897f61/6a17f09c9da390fb1de4657d/cfd781acbff8cd7c8095bbe29224f8b26d581f77-650x375.png" alt="Pide a ChatGPT “Encuentra incidencias relacionadas con la migración de Elasticsearch” y confirma la llamada a la herramienta de acciones." /><p>ChatGPT llamará a la herramienta <code>search</code> con tu búsqueda. Puedes ver que está buscando herramientas disponibles y preparándose para llamar a la herramienta Elasticsearch y confirma con el usuario antes de tomar cualquier acción en relación con la herramienta.</p><h4>Solicitud de llamada a la herramienta:</h4>{
  "query": "Elasticsearch migration issues"
}<h4>Respuesta de la herramienta:</h4>{
  "results": [
    {
      "id": "PR-598",
      "title": "Elasticsearch 8.x migration - Application code changes",
      "url": "https://internal-git.techcorp.com/pulls/598"
    },
    {
      "id": "ISSUE-1712",
      "title": "Migrate from Elasticsearch 7.x to 8.x",
      "url": "https://internal-git.techcorp.com/issues/1712"
    },
    {
      "id": "RFC-045",
      "title": "Design Proposal: Microservices Migration Architecture",
      "url": "https://internal-git.techcorp.com/rfcs/045"
    }
    // ... 7 more results
  ]
}<p>ChatGPT procesa los resultados y los presenta en un formato natural y conocido.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1b4378e7d26b4ad0/6a17f09ddbb4ff4de1fb57bf/9d5b6cff85c7e54ccc2584b8ae96d45495fae8c1-923x1352.png" alt="Cómo procesa ChatGPT los resultados de la solicitud de llamada a la herramienta y la respuesta de la herramienta" /><h3>Entre bastidores</h3><h4>Indicación: “Busca incidencias relacionadas con la migración de Elasticsearch”</h4><p>1. Llamadas de ChatGPT <code>search(“Elasticsearch migration”)</code></p><p>2. Elasticsearch realiza una búsqueda híbrida</p><ul><li><p>La <strong>búsqueda semántica</strong> entiende conceptos como “actualización” y “<em>compatibilidad de versiones”.</em></p></li><li><p>La <strong>búsqueda de texto</strong> encuentra coincidencias exactas de "<em>Elasticsearch</em>" y "migración".</p></li><li><p><strong>RRF</strong> combina y clasifica los resultados de ambos enfoques</p></li></ul><p>3. Devuelve los 10 mejores eventos que coinciden con <code>id</code>, <code>title</code>, <code>url</code></p><p>4. ChatGPT identifica “<em>ISSUE-1712: migrar de Elasticsearch 7.x a 8.x</em>” como el resultado más relevante.</p><h3>Indicación 2: Obtén los detalles completos</h3><p>Pregunta: <em><strong>“Obtén detalles de ISSUE-1889”</strong></em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8d1a53db8bfe8326/6a17f09f445de966104d021a/5c0db5245535ce67a36056e61e135bddc97ce496-934x629.png" alt="ChatGPT reconoce que quieres información detallada sobre un problema específico y llama a la herramienta de búsqueda y confirma con el usuario antes de hacer cualquier acción con la herramienta." /><p>ChatGPT reconoce que deseas información detallada sobre una incidencia específica, llama a la herramienta <code>fetch</code> y confirma con el usuario antes de tomar cualquier acción con la herramienta.</p><h4>Solicitud de llamada a la herramienta:</h4>{
  "id": "ISSUE-1889"
}<h4>Respuesta de la herramienta:</h4>{
  "id": "ISSUE-1889",
  "title": "SQL injection vulnerability in search endpoint",
  "text": "Description: Security audit identified SQL injection vulnerability in /api/v1/search endpoint. User input from query parameter is not properly sanitized before being used in raw SQL query. Severity: HIGH - Immediate action required Affected Code: - File: services/search/query_builder.py - Line: 145-152 - Issue: String concatenation used instead of parameterized queries Investigation: - @security_team_alice: Confirmed exploitable with UNION-based injection - @sarah_dev: Checking all other endpoints for similar patterns - @john_backend: Found 3 more instances in legacy codebase Remediation: - Rewrite using SQLAlchemy ORM or parameterized queries - Add input validation and sanitization - Implement WAF rules as additional layer - Security regression tests Comments: - @tech_lead_mike: Stop all other work, this is P0 - @sarah_dev: PR-578 ready with fixes for all 4 vulnerable endpoints - @alex_devops: Deployed hotfix to production 2025-09-19 at 14:30 UTC - @security_team_alice: Verified fix, conducting full pentest next week Resolution: All vulnerable endpoints patched. Added pre-commit hooks to catch raw SQL queries. Security training scheduled for team.",
  "url": "https://internal-git.techcorp.com/issues/1889",
  "type": "issue",
  "status": "closed",
  "priority": "critical",
  "assignee": "sarah_dev",
  "created_date": "2025-09-18",
  "resolved_date": "2025-09-19",
  "labels": "security, vulnerability, bug, sql",
  "related_pr": "PR-578"
}<p>ChatGPT sintetiza la información y la presenta de manera clara.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt560958fa3bd212d0/6a17f0a0faa91355ba93c974/410f19f213e94fc4e3c47eeef6e04b69e0c86159-602x462.png" alt="Cómo ChatGPT sintetiza la información y la presenta " /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcccf35a584e8373b/6a17f0a2505ac3471cad8c2e/54d8ffa117628a1e3afc317c3ab75d4f7731d7ab-767x1600.png" alt="Cómo ChatGPT presenta la información" /><h3>Entre bastidores</h3><h4>Indicación: «Obtén los detalles de ISSUE-1889»</h4><ol><li><p>Llamadas de ChatGPT <code>fetch(“ISSUE-1889”)</code></p></li><li><p>Elasticsearch recupera el documento completo</p></li><li><p>Devuelve un documento completo con todos los campos a nivel raíz.</p></li><li><p>ChatGPT sintetiza la información y responde con citas adecuadas.</p></li></ol><h2>Conclusión</h2><p>En este artículo, creamos un servidor MCP personalizado que conecta ChatGPT a Elasticsearch con herramientas MCP dedicadas de <strong>búsqueda</strong> y <strong>extracción</strong>, lo cual permite realizar búsquedas en lenguaje natural sobre datos privados.</p><p>Este patrón MCP funciona para cualquier índice, documentación, productos, logs o cualquier otro dato de Elasticsearch que quieras buscar mediante lenguaje natural.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/chatgpt-connector-mcp-server-github-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/chatgpt-connector-mcp-server-github-elasticsearch</guid>
    <category><![CDATA[AI agéntica]]></category>
    <category><![CDATA[Búsqueda híbrida]]></category>
    <dc:creator><![CDATA[Tomás Murúa]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd1602c48878dc7a9/6a17f09a6df731cca90a0fff/77a6fc1eb263a0eb16aac64f2ecaca5f4ac12ec2-966x568.gif" length="0" type="image/gif"/>
    <pubDate>Mon, 01 Dec 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Construir un agente de IA para RRHH con Elastic Agent Builder y GPT-OSS]]></title>
    <description><![CDATA[Descubre cómo crear un agente de IA que pueda responder consultas en lenguaje natural sobre los datos de RRHH de tus empleados usando Elastic Agent Builder y GPT-OSS.]]></description>
    <content:encoded><![CDATA[<h2>Introducción</h2><p>Este artículo te mostrará cómo construir un agente de IA para RRHH usando <a href="https://openai.com/index/introducing-gpt-oss/">GPT-OSS</a> y Elastic Agent Builder. El agente puede responder a tus preguntas sin enviar datos a OpenAI, Anthropic ni ningún servicio externo.</p><p>Usaremos LM Studio para servir GPT-OSS localmente y lo conectaremos a Elastic Agent Builder.</p><p>Al final de este artículo, tendrás un agente de IA personalizado que podrá responder preguntas en lenguaje natural sobre los datos de tus empleados manteniendo el control total sobre tu información y modelo.</p><h2>Prerrequisitos</h2><p>Para este artículo, necesitas:</p><ul><li><p><a href="https://www.elastic.co/cloud">Elastic Cloud</a> alojaba la versión 9.2, despliegue serverless o <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart">local</a></p></li><li><p>Máquina con 32GB de RAM recomendada (mínimo 16GB para GPT-OSS 20B)</p></li><li><p><a href="https://lmstudio.ai/">LM Studio</a> instalado</p></li><li><p><a href="https://www.docker.com/products/docker-desktop/">Escritorio Docker</a> Instalado</p></li></ul><h2>¿Por qué usar GPT-OSS?</h2><p>Con un LLM local tienes el control para desplegarlo en tu propia infraestructura y ajustarlo para adaptarlo a tus propias necesidades. Todo esto manteniendo el control sobre los datos que compartes con el modelo y, por supuesto, no tienes que pagar una tasa de licencia a un proveedor externo.</p><p>OpenAI <a href="https://openai.com/index/introducing-gpt-oss/">lanzó GPT-OSS</a> el 5 de agosto de 2025, como parte de su compromiso con el ecosistema del modelo abierto.</p><p>El modelo de parámetros 20B ofrece:</p><ul><li><p><strong>Capacidades de uso de herramientas</strong></p></li><li><p><strong>Inferencia eficiente</strong></p></li><li><p><strong>Compatible con SDK OpenAI</strong></p></li><li><p><strong>Compatible con flujos de trabajo agentes</strong></p></li></ul><p>Comparación de referencias:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt58fab956edb40412/6a170cfcb0367da43a72bd80/29160e3345352088e8213297630882f252b00c47-1600x680.png" alt="" /><h2>Arquitectura de soluciones</h2><p>La arquitectura funciona completamente en tu máquina local. Elastic (que funciona en Docker) se comunica directamente con tu LLM local a través de LM Studio, y Elastic Agent Builder emplea esta conexión para crear agentes de IA personalizados que pueden consultar los datos de tus empleados.</p><p>Para más detalles, consulte esta <a href="https://www.elastic.co/docs/solutions/observability/connect-to-own-local-llm">documentación</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt80db5bb0a797f51b/6a170cfd0e2e492f2c41a16f/a4a886750ff25fa8bb7aefc7448161e52cf73ed3-1600x896.png" alt="" /><h2>Construir un agente de IA para RRHH: Pasos</h2><p>Dividiremos la implementación en 5 pasos:</p><ol><li><p>Configurar LM Studio con un modelo local</p></li><li><p>Despliega Elastic Local con Docker</p></li><li><p>Crea el conector OpenAI en Elastic</p></li><li><p>Sube los datos de los empleados a Elasticsearch</p></li><li><p>Construye y prueba tu agente de IA</p></li></ol><h2>Paso 1: Configurar LM Studio con GPT-OSS 20B</h2><p>LM Studio es una aplicación fácil de usar que te permite ejecutar grandes modelos de lenguaje localmente en tu computadora. Proporciona un servidor API compatible con OpenAI, lo que facilita su integración con herramientas como Elastic sin un proceso de configuración complejo. Para más detalles, consulta la <a href="https://lmstudio.ai/docs/app">documentación de LM Studio</a>.</p><p>Primero, descarga e instala LM Studio desde el sitio web oficial. Una vez instalado, abre la aplicación.</p><h3>En la interfaz de LM Studio:</h3><ol><li><p>Ve a la pestaña de búsqueda y busca "GPT-OSS"</p></li><li><p>Selecciona el <code>openai/gpt-oss-20b</code> de OpenAI</p></li><li><p>Haz clic en descargar</p></li></ol><p>El tamaño de este modelo debería ser aproximadamente <strong>12,10GB</strong>. La descarga puede tardar unos minutos, dependiendo de tu conexión a Internet.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2dc341a6625e34b7/6a170cff839dfa2eb4dcff44/5d01bc4dcb377b5259fc6b521fe2425a31b90ca4-1312x872.png" alt="" /><h4>Una vez descargado el modelo:</h4><ol><li><p>Ve a la pestaña del servidor local</p></li><li><p>Selecciona el openai/gpt-oss-20b</p></li><li><p>Usa el puerto predeterminado 1234</p></li><li><p>En el panel derecho, ve a <strong>Cargar </strong>y establece la longitud de contexto en <strong>40K</strong> o más</p></li></ol><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3704ca1b28465cc4/6a170d00d7c022ed8fde64ef/e546033f916381647b876815b2c1f1ae2a08365f-326x337.png" alt="" /><p>5. Haz clic en iniciar servidor</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7b9170a4945ff857/6a170d0266c4f9ffadf8c0a6/28ee78a3caa84d14e04db3d42f30acbe4d4d005a-1312x872.png" alt="" /><p>Deberías ver esto si el servidor está funcionando.</p>[LM STUDIO SERVER] Success! HTTP server listening on port 1234
[LM STUDIO SERVER] Supported endpoints:
[LM STUDIO SERVER] -&gt;	GET  http://localhost:1234/v1/models
[LM STUDIO SERVER] -&gt;	POST http://localhost:1234/v1/responses
[LM STUDIO SERVER] -&gt;	POST http://localhost:1234/v1/chat/completions
[LM STUDIO SERVER] -&gt;	POST http://localhost:1234/v1/completions
[LM STUDIO SERVER] -&gt;	POST http://localhost:1234/v1/embeddings
Server started.<h2>Paso 2: Desplegar Elastic Local con Docker</h2><p>Ahora configuraremos Elasticsearch y Kibana localmente usando Docker. Elastic proporciona un script conveniente que gestiona todo el proceso de configuración. Para más detalles, consulte la <a href="https://www.elastic.co/docs/deploy-manage/deploy/self-managed/local-development-installation-quickstart">documentación oficial</a>.</p><h3>Ejecuta el script de inicio local</h3><p>Ejecuta el siguiente comando en tu terminal:</p>curl -fsSL https://elastic.co/start-local | sh<p>Este guion será:</p><ul><li><p>Descargar y configurar Elasticsearch y Kibana</p></li><li><p>Inicia ambos servicios usando Docker Compose</p></li><li><p>Activa automáticamente una licencia de prueba Platinum de 30 días</p></li></ul><h3>Producción esperada</h3><p>Solo espera el siguiente mensaje y almacena la contraseña y la clave API que se muestra; los necesitarás para acceder a Kibana:</p>🎉 Congrats, Elasticsearch and Kibana are installed and running in Docker!
🌐 Open your browser at http://localhost:5601
   Username: elastic
   Password: KSUlOMNr
🔌 Elasticsearch API endpoint: http://localhost:9200
🔑 API key: cnJGX0pwb0JhOG00cmNJVklUNXg6cnNJdXZWMnM4bncwMllpQlFlUTlWdw==
Learn more at https://github.com/elastic/start-local<h3>Acceso a Kibana</h3><p>Abre tu navegador y navega a:</p>http://localhost:5601<p>Inicia sesión usando las credenciales obtenidas en la salida del terminal.</p><h3>Habilitar Agent Builder</h3><p>Una vez iniciado sesión en Kibana, navega a <strong>Management </strong>&gt;<strong> AI </strong>&gt;<strong> Agent Builder </strong>y activa el Agent Builder.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0a934bd99fa6a0ce/6a170d046234e019c3db1a5a/92e104cb846c20d875865ded8a3d37f5c7daae9b-1491x1528.png" alt="" /><h2>Paso 3: Crea el conector OpenAI en Elastic</h2><p>Ahora configuraremos Elastic para que use tu LLM local.</p><h3>Conectores de acceso</h3><ol><li><p>En Kibana</p></li><li><p>Ve a Configuración &gt; <strong>Gestión del Proyecto</strong> </p></li><li><p>En <strong>Alertas e Información</strong>, selecciona <strong>Conectores</strong></p></li><li><p>Haz clic en Crear conector</p></li></ol><h3>Configurar el conector</h3><p>Selecciona <strong>OpenAI</strong> de la lista de conectores. LM Studio emplea el SDK OpenAI, lo que lo hace compatible.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt762023c39781eb78/6a170d06a29299a59ed01087/5ac87042e086c7a2bd47a8039e646ec831f0dcc6-923x974.png" alt="" /><p>Diligencia el espacio con estos valores:</p><ul><li><p><strong>Nombre del conector: </strong>LM Studio - GPT-OSS 20B</p></li><li><p><strong>Selecciona un proveedor de OpenAI: </strong>Otros (Servicio compatible con OpenAI)</p></li><li><p><strong>URL: </strong><code>http://host.docker.internal:1234/v1/chat/completions</code></p></li><li><p><strong>Modelo por defecto: </strong>openai/gpt-oss-20b</p></li><li><p><strong>API Key:</strong> testkey-123 (cualquier texto funciona, porque LM Studio Server no requiere autenticación.)</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt980e595f80e2be2e/6a170d086f7f0468a19148cc/2084ac32fcf1fb810c8b54ecab1c85a1e3e8905b-672x1302.png" alt="" /><p>Para terminar la configuración, haz clic en <strong>Almacenar y probar</strong>.</p><p><strong>Importante:</strong> Activa la opción "<strong>Habilitar la llamada a funciones nativas</strong>"; esto es necesario para que el Constructor de Agentes funcione correctamente. Si no activas esto, te aparecerá un error de <strong><code>No tool calls found in the response</code></strong> .</p><h3>Prueba la conexión</h3><p>El elástico debería probar automáticamente la conexión. Si todo está configurado correctamente, verás un mensaje de éxito como este:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4d2e815dd558f881/6a170d090e2e49076541a177/f567d767f1969c4730c1daa92f651789dc3742ac-1042x812.png" alt="" /><p>Respuesta:</p>{
  "status": "ok",
  "data": {
    "id": "chatcmpl-flj9h0hy4wcx4bfson00an",
    "object": "chat.completion",
    "created": 1761189456,
    "model": "openai/gpt-oss-20b",
    "choices": [
      {
        "index": 0,
        "message": {
          "role": "assistant",
          "content": "Hello! 👋 How can I assist you today?",
          "reasoning": "Just greet.",
          "tool_calls": []
        },
        "logprobs": null,
        "finish_reason": "stop"
      }
    ],
    "usage": {
      "prompt_tokens": 69,
      "completion_tokens": 23,
      "total_tokens": 92
    },
    "stats": {},
    "system_fingerprint": "openai/gpt-oss-20b"
  },
  "actionId": "ee1c3aaf-bad0-4ada-8149-118f52dad757"
}<h2>Paso 4: Subir los datos de los empleados a Elasticsearch</h2><p>Ahora subiremos el <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/gpt-oss-with-elasticsearch/hr-employees-bulk.json">conjunto de datos de empleados de RRHH</a> para demostrar cómo el agente trabaja con datos sensibles. Generé un conjunto de datos ficticio con esta estructura.</p><h3>Estructura del conjunto de datos</h3>{
  "employee_id": "0f4dce68-2a09-4cb1-b2af-6bcb4821539b",
  "full_name": "Daffi Stiebler",
  "email": "lscutchings0@huffingtonpost.com",
  "date_of_birth": "1975-06-20T15:39:36Z",
  "hire_date": "2025-07-28T00:10:45Z",
  "job_title": "Physical Therapy Assistant",
  "department": "HR",
  "salary": "108455",
  "performance_rating": "Needs Improvement",
  "years_of_experience": 2,
  "skills": "Java",
  "education_level": "Master's Degree",
  "manager": "Carl MacGibbon",
  "emergency_contact": "Leigha Scutchings",
  "home_address": "5571 6th Park"
}<h3>Crea el índice con mapeos</h3><p>Primero, crea el índice con mapeos adecuados. Ten en cuenta que estamos usando <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/semantic-text">semantic_text</a> campos para algunos campos clave; Esto permite capacidades de búsqueda semántica para nuestro índice.</p>​​PUT hr-employees
{
  "mappings": {
    "properties": {
      "@timestamp": {
        "type": "date"
      },
      "employee_id": {
        "type": "keyword"
      },
      "full_name": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "email": {
        "type": "keyword"
      },
      "date_of_birth": {
        "type": "date",
        "format": "iso8601"
      },
      "hire_date": {
        "type": "date",
        "format": "iso8601"
      },
      "job_title": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "department": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "salary": {
        "type": "double"
      },
      "performance_rating": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "years_of_experience": {
        "type": "long"
      },
      "skills": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "education_level": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "manager": {
        "type": "text",
        "copy_to": "employee_semantic"
      },
      "emergency_contact": {
        "type": "keyword"
      },
      "home_address": {
        "type": "keyword"
      },
      "employee_semantic": {
        "type": "semantic_text"
      }
    }
  }
}<h3>Índice con API Bulk</h3><p>Copia y pega el <a href="https://github.com/elastic/elasticsearch-labs/blob/main/supporting-blog-content/gpt-oss-with-elasticsearch/hr-employees-bulk.json">conjunto de datos</a> en tus Dev Tools en Kibana y ejecutalo:</p>POST hr-employees/_bulk
{"index": {}}
{"employee_id": "57728b91-e5d7-4fa8-954a-2384040d3886", "full_name": "Filide Gane", "email": "vhallahan1@booking.com", "job_title": "Business Systems Development Analyst", "department": "Marketing", "salary": "$52330.27", "performance_rating": "Meets Expectations", "years_of_experience": 12, "skills": "Java", "education_level": "Bachelor's Degree", "date_of_birth": "2000-02-07T16:49:32Z", "hire_date": "2023-11-07T13:03:16Z", "manager": "Freedman Kings", "emergency_contact": "Vilhelmina Hallahan", "home_address": "75 Dennis Junction"}
{"index": {}}
{"employee_id": "...", ...}<h3>Verifica los datos</h3><p>Realiza una consulta para verificar:</p>GET hr-employees/_search<h2>Paso 5: Construye y prueba tu agente de IA</h2><p>Con todo configurado, es hora de crear un agente de IA personalizado usando Elastic Agent Builder. Para más detalles, consulte la <a href="https://www.elastic.co/docs/solutions/search/agent-builder/get-started">documentación de Elastic</a>.</p><h3>Agregar el conector</h3><p>Antes de poder crear nuestro nuevo agente, tenemos que configurar nuestro constructor de agentes para usar nuestro conector personalizado llamado <code>LM Studio - GPT-OSS 20B</code> porque el predeterminado es el <a href="https://www.elastic.co/docs/reference/kibana/connectors-kibana/elastic-managed-llm">Elastic Managed LLM</a>. Para eso, necesitamos ir a <strong>Configuración</strong> &gt; <strong>Gestión de Proyectos</strong> &gt; <strong>Configuración</strong> de GenAI; ahora seleccionamos la que creamos y hacemos clic <strong>en</strong> Almacenar.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc42f079c5e756057/6a170d0acf4f2501d9b2d1c7/11e830c3e2fb4c298b020c928fa5422f3397ba08-1600x1152.png" alt="" /><h3>Constructor de Agentes de Acceso</h3><ol><li><p>Ir a <strong>Agentes</strong></p></li><li><p>Haz clic <strong>en Crear un nuevo agente</strong></p></li></ol><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb8e734817c5a7c6a/6a170d0ca929cf867cae0a34/c1e60541563650163f972ac9088dc1ed1de759a7-1600x1054.png" alt="" /><h3>Configurar el agente</h3><p>Para crear un nuevo agente, los campos requeridos son el <strong>ID del agente</strong>, <strong>el nombre de visualización</strong> y <strong>las instrucciones de visualización</strong>.</p><p>Pero hay más opciones de personalización, como las Instrucciones Personalizadas que guían cómo se comportará tu agente e interactuará con tus herramientas, similar a una indicación del sistema, pero para nuestro agente personalizado. Las etiquetas ayudan a organizar tus agentes, el color del avatar y el símbolo del avatar.</p><p>Los que elegí para nuestro agente basándome en el conjunto de datos son:

<strong>ID del agente:</strong> <code>hr_assistant</code></p><p><strong>Instrucciones personalizadas:</strong></p>You are an HR Analytics Assistant that helps answer questions about employee data.
When responding to queries:
- Provide clear, concise answers
- Include relevant employee details (name, department, salary, skills)
- Format monetary values with currency symbols
- Be professional and maintain data confidentiality<p>
Etiquetas: <code>Human Resources</code> y <code>GPT-OSS</code></p><p>Nombre de visualización: <code>HR Analytics Assistant</code></p><p>Descripción de la exposición:</p>A specialized AI assistant for Human Resources that helps analyze employee data, compensation, performance metrics, and talent management. Ask questions about employees, departments, salaries, or performance analytics.<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt23fb011e5b4f4d49/6a170d0e7d8d67f47a70e77f/f94bb2bf08497e5e756ca76b30a3a51f42927756-1424x1217.png" alt="" /><p>Con todos los datos ahí, podemos hacer clic en <strong>Almacenar</strong> a nuestro nuevo agente.</p><h3>Prueba al agente</h3><p>Ahora puedes hacer preguntas en lenguaje natural sobre los datos de tus empleados, y GPT-OSS 20B entenderá la intención y generará una respuesta adecuada.</p><h4>Pronto:</h4>Which employee is the one with the highest salary in the hr-employees index?<h4>Respuesta:</h4><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc0c52faacf63b583/6a170d0f0e2e497bfd41a17b/94ad19f80b96304028a59f60beca51dfc9aecc8a-899x631.png" alt="" /><p>El proceso de Agente fue:</p><p>1. Entiende tu pregunta usando el conector GPT-OSS</p><p>2. Generar la consulta Elasticsearch adecuada (usando las herramientas integradas o <a href="https://www.elastic.co/docs/reference/query-languages/esql">ES| personalizadoQL</a>)</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte32a8a7e6363c7f2/6a170d115091680077e1bb44/6f2961d0d1b97475f6dda300acee84da540938e6-844x466.png" alt="" /><p>3. Recuperar los registros de empleados coincidentes</p><p>4. Presentar resultados en lenguaje natural con un formato adecuado</p><p>A diferencia de la búsqueda léxica tradicional, el agente impulsado por GPT-OSS entiende la intención y el contexto, facilitando encontrar información sin conocer los nombres exactos de los campos o la sintaxis de la consulta. Para más detalles sobre el proceso de pensamiento del agente, consulta este <a href="https://www.elastic.co/search-labs/blog/ai-agent-builder-experiments-performance">artículo</a>.</p><h2>Conclusión</h2><p>En este artículo, creamos un agente de IA personalizado usando el Agent Builder de Elastic para conectarse al modelo OpenAI GPT-OSS que se ejecuta localmente. Al desplegar tanto Elastic como el LLM en tu máquina local, esta arquitectura te permite aprovechar las capacidades de IA generativa manteniendo el control total sobre tus datos, todo ello sin enviar información a servicios externos.</p><p>Usamos GPT-OSS 20B como experimento, pero <a href="https://www.elastic.co/docs/solutions/search/agent-builder/models#recommended-models">aquí</a> se hacen referencia a los modelos oficialmente recomendados para Elastic Agent Builder. Si necesitas capacidades de razonamiento más avanzadas, también está la <a href="https://huggingface.co/openai/gpt-oss-120b">variante de parámetros 120B</a> que rinde mejor para escenarios complejos, aunque requiere una máquina de especificaciones más altas para ejecutar localmente. Para más detalles, consulte la <a href="https://openai.com/open-models/">documentación oficial de OpenAI</a>.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/build-an-ai-agent-hr-elastic-agent-builder-gpt-oss</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/build-an-ai-agent-hr-elastic-agent-builder-gpt-oss</guid>
    <category><![CDATA[AI agéntica]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Tomás Murúa]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt664f490053e46e6b/6a170d13b0367d2d7e72bd84/05d2d0513fff67d975f9223d75108aa9f50646bc-1600x914.png" length="0" type="image/png"/>
    <pubDate>Wed, 26 Nov 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Paneles impulsados por IA: de una visión a Kibana]]></title>
    <description><![CDATA[Genera un panel de control usando un LLM para procesar una imagen y convertirla en un Panel de Kibana.
]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/kibana/kibana-lens">Kibana Lens</a> hace que los paneles de control sean muy sencillos, pero cuando necesitas decenas de paneles, los clics se acumulan. ¿Y si pudieras hacer un boceto de un panel de control, hacer capturas de pantalla y dejar que un LLM termine todo el proceso por ti?</p><p>En este artículo, haremos que eso suceda. Crearemos una aplicación que tome una imagen de un panel de control, analice nuestros mapeos y luego genere un panel sin que tengamos que tocar Kibana en absoluto.</p><p><strong>Pasos</strong>:</p><ol><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#background-&amp;-application-workflow">Antecedentes y flujo de trabajo de la aplicación</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#prepare-data">Preparar datos</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#llm-configuration">Configuración de LLM</a></p></li><li><p><a href="https://www.elastic.co/search-labs/blog/ai-powered-dashboards#application-functions">Funciones de aplicación</a></p></li></ol><h2>Antecedentes y flujo de trabajo de la aplicación</h2><p>La primera idea que se me ocurrió fue dejar que el LLM generara todo el formato NDJSON <a href="https://www.elastic.co/docs/explore-analyze/find-and-organize/saved-objects">de los objetos almacenados</a> de Kibana y luego los importara a Kibana.</p><p>Probamos algunos modelos:</p><ul><li><p>Gemini 2.5 pro</p></li><li><p>GPT o3 / o4-mini-alto / 4,1</p></li><li><p>Claude 4 soneto</p></li><li><p>Grok 3</p></li><li><p>Deepseek (Deepthink R1)</p></li></ul><p>Y para los prompts, empezamos tan sencillos como:</p>You are an Elasticsearch Saved-Object generator (Kibana 9.0).
INPUTS
=====
1. PNG screenshot of a 4-panel dashboard (attached).
2. Index mapping (below) – trimmed down to only the fields present in the screenshot.
3. Example NDJSON of *one* metric visualization (below) for reference.

TASK
====
Return **only** a valid NDJSON array that recreates the dashboard exactly:
* 2 metric panels (Visits, Unique Visitors)
* 1 pie chart (Most used OS)
* 1 vertical bar chart (State Geo Dest)
* Use index pattern `kibana_sample_data_logs`.
* Preserve roughly the same layout (2×2 grid).
* Use `panelIndex` values 1-4 and random `id` strings.
* Kibana version: 9.0<p>A pesar de repasar <a href="https://www.elastic.co/search-labs/blog/function-calling-with-elastic#:~:text=Few%2Dshot%20prompting%20involves%20providing%20examples%20of%20the%20types%20of%20queries%20you%20want%20it%20to%20return%2C%20which%20helps%20in%20increasing%20consistency.">algunos ejemplos de planos</a> y explicaciones detalladas sobre cómo construir cada visualización, no tuvimos suerte. Si te interesa esta experimentación, puedes encontrar <a href="https://gist.github.com/TomasMurua/a78dc283e115624731beffc98984b70b">detalles aquí</a>.</p><p>El resultado de este enfoque fue ver estos mensajes al intentar subir a Kibana los archivos producidos por el LLM:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9ea005966a783057/6a1707d266c4f90e4ef8bf88/2b599443b5613c9f0fc3235581614add5b4b3900-891x98.png" alt="" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5e5632d6d95b998c/6a1707d3a6c2b9441de79661/d87ccfc033bc00ee8188c5cae18043fbca22784c-741x233.png" alt="" /><p>Esto significa que el JSON generado es inválido o está mal formateado. Los problemas más comunes eran que el LLM producía NDJSON incompleto, alucinaban parámetros o devolvían JSON normal en lugar de NDJSON, por mucho que intentáramos hacer cumplir lo contrario.</p><p>Inspirados por <a href="https://www.elastic.co/search-labs/blog/llm-functions-elasticsearch-intelligent-query">este artículo</a> —donde <a href="https://www.elastic.co/docs/solutions/search/search-templates">las plantillas de búsqueda</a> funcionaban mejor que el estilo libre de un LLM— decidimos dar plantillas al LLM en lugar de pedir generar el archivo NDJSON completo y luego, en código, usar los parámetros dados por el LLM para crear las visualizaciones adecuadas. Este enfoque no decepcionó, y es previsible y ampliable, ya que ahora el código hace el trabajo duro y no el LLM.</p><p>El flujo de trabajo de la aplicación será el siguiente:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9f7738a4c7ddd0cd/6a1707d52b835f0a25f4b166/52c587cf0cf3517fdd4ee7ab95581dd4f2bce030-725x668.png" alt="" /><p></p><p><em>Omitiremos algo de código para simplificar, pero puedes encontrar el código funcional de la aplicación completa en </em><a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/from-image-idea-to-kibana-dashboard-using-ai.ipynb"><em><strong>este</strong></em></a><em> cuaderno.</em></p><h2>Prerrequisitos</h2><p>Antes de empezar a desarrollar, necesitarás lo siguiente:</p><ol><li><p>Python 3.8 o superior</p></li><li><p>Un entorno <a href="https://docs.python.org/3/library/venv.html">Venv</a> Python</p></li><li><p>Una instancia de Elasticsearch en ejecución, junto con su endpoint y clave API</p></li><li><p>Una clave de API de OpenAI almacenada bajo el nombre de la variable de entorno OPENAI_API_KEY:</p></li></ol>export OPENAI_API_KEY="your-openai-api-key"<h2>Preparar datos</h2><p>Para los datos, lo mantendremos sencillo y usaremos registros sitio web de muestra de Elastic. Puedes aprender cómo importar esos datos a tu <a href="https://www.elastic.co/docs/manage-data/ingest/sample-data#add-sample-data-sets">clúster aquí</a>.</p><p>Cada documento incluye detalles sobre el anfitrión que emitió las solicitudes a la aplicación, junto con información sobre la propia solicitud y su estado de respuesta. A continuación se muestra un documento de ejemplo:</p>{
    "agent": "Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24",
    "bytes": 8509,
    "clientip": "70.133.115.149",
    "extension": "css",
    "geo": {
        "srcdest": "US:IT",
        "src": "US",
        "dest": "IT",
        "coordinates": {
            "lat": 38.05134111,
            "lon": -103.5106908
        }
    },
    "host": "cdn.elastic-elastic-elastic.org",
    "index": "kibana_sample_data_logs",
    "ip": "70.133.115.149",
    "machine": {
        "ram": 5368709120,
        "os": "osx"
    },
    "memory": null,
    "message": "70.133.115.149 - - [2018-08-30T23:35:31.492Z] \"GET /styles/semantic-ui.css HTTP/1.1\" 200 8509 \"-\" \"Mozilla/5.0 (X11; Linux i686) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.50 Safari/534.24\"",
    "phpmemory": null,
    "referer": "http://twitter.com/error/john-phillips",
    "request": "/styles/semantic-ui.css",
    "response": 200,
    "tags": [
        "success",
        "info"
    ],
    "@timestamp": "2025-07-03T23:35:31.492Z",
    "url": "https://cdn.elastic-elastic-elastic.org/styles/semantic-ui.css",
    "utc_time": "2025-07-03T23:35:31.492Z",
    "event": {
        "dataset": "sample_web_logs"
    },
    "bytes_gauge": 8509,
    "bytes_counter": 51201128
}<p>Ahora, vamos a tomar los mapeos del índice que acabamos de cargar, <code>kibana_sample_data_logs</code>:</p>INDEX_NAME = "kibana_sample_data_logs"

es_client = Elasticsearch(
    [os.getenv("ELASTICSEARCH_URL")],
    api_key=os.getenv("ELASTICSEARCH_API_KEY"),
)

result = es_client.indices.get_mapping(index=INDEX_NAME)
index_mappings = result[list(result.keys())[0]]["mappings"]["properties"]<p>Vamos a pasar los mapeos junto con la imagen que cargaremos más adelante.</p><h2>Configuración de LLM</h2><p>Configuremos el LLM para que use <a href="https://python.langchain.com/docs/concepts/structured_outputs/">una salida estructurada</a> para introducir una imagen y recibir un JSON con la información que necesitamos pasar a nuestra función para producir los objetos JSON.</p><p>Instalamos las dependencias:</p>pip install elasticsearch pydantic langchain langchain-openai -q<p>Elasticsearch nos ayudará a recuperar los <a href="https://www.elastic.co/docs/manage-data/data-store/mapping">mapeos de índice</a>. Pydantic nos permite definir esquemas en Python para luego pedir al LLM que lo siga, y <a href="https://www.elastic.co/search-labs/integrations/langchain">LangChain</a> es el framework que facilita la llamada a LLMs y herramientas de IA.</p><p>Crearemos un esquema Pydantic para definir la salida que queremos del LLM. Lo que necesitamos saber de la imagen es el tipo de gráfico, campo, título de visualización y título del panel de control:</p>class Visualization(BaseModel):
    title: str = Field(description="The dashboard title")
    type: List[Literal["pie", "bar", "metric"]]
    field: str = Field(
        description="The field that this visualization use based on the provided mappings"
    )


class Dashboard(BaseModel):
    title: str = Field(description="The dashboard title")
    visualizations: List[Visualization]<p>Para la entrada de imagen enviaremos un panel de control que acabo de dibujar:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7870f6421986d11d/6a1707d78b73cb3408189fa3/36441d7b5dc1f3ff2ac2a30710208d57ad41c716-1600x898.jpg" alt="" /><p>Ahora declaramos la llamada al modelo LLM y la carga de imagen. Esta función recibirá los mapeos del índice de Elasticsearch y una imagen del panel de control que queremos generar.</p><p>Con <code>with_structured_output</code> podemos usar nuestro esquema de <code>Dashboard</code> Pydantic como objeto de respuesta que producirá el LLM. Con <a href="https://docs.pydantic.dev/latest/">Pydantic</a>, podemos definir modelos de datos con validación, lo que garantiza que la salida del LLM coincida con la estructura esperada.</p><p>Para convertir la imagen a base64 y enviarla como entrada, puedes usar un <a href="https://www.base64-image.de/">convertidor online</a> o hacerlo <a href="https://www.geeksforgeeks.org/python-convert-image-to-string-and-vice-versa/">en código</a>.</p>prompt = f"""
    You are an expert in analyzing Kibana dashboards from images for the version 9.0.0 of Kibana.

    You will be given a dashboard image and an Elasticsearch index mapping.

    Below are the index mappings for the index that the dashboard is based on.
    Use this to help you understand the data and the fields that are available.

    Index Mappings:
    {index_mappings}

    Only include the fields that are relevant for each visualization, based on what is visible in the image.
    """

message = [
    {
        "role": "user",
        "content": [
            {"type": "text", "text": prompt},
            {
                "type": "image",
                "source_type": "base64",
                "data": image_base64,
                "mime_type": "image/png",
            },
        ],
    }
]


try:
    llm = init_chat_model("gpt-4.1-mini")
    llm = llm.with_structured_output(Dashboard)
    dashboard_values = llm.invoke(message)

    print("Dashboard values generated by the LLM successfully")
    print(dashboard_values)
except Exception as e:
    print(f"Failed to analyze image and match fields: {str(e)}")<p>El LLM ya tiene contexto sobre los paneles Kibana, así que no necesitamos explicar todo en el prompt, solo algunos detalles para cerciorarnos de que no olvide que está funcionando con Elasticsearch y Kibana.</p><p>Vamos a desglosar el prompt:</p><p>Sección</p><p>Razón</p><p>Eres un experto en analizar paneles de Kibana a partir de imágenes para la versión 9.0.0 de Kibana.</p><p>Al reforzar esto es Elasticsearch, y la versión de Elasticsearch reducimos la probabilidad de que el LLM alucine parámetros antiguos o inválidos.</p><p>Se te dará una imagen del panel de control y un mapeo de índice de Elasticsearch.</p><p>Explicamos que la imagen trata sobre paneles para evitar interpretaciones erróneas por parte del LLM.</p><p>A continuación se muestran los mapeos de índices del índice en el que se basa el panel de control. Emplea esto para ayudarte a entender los datos y los campos disponibles. Mapeos de índice: {index_mappings}</p><p>Es crucial proporcionar los mapeos para que el LLM pueda seleccionar campos válidos dinámicamente. De lo contrario, podríamos codificar los mapeos aquí, lo cual es demasiado rígido, o confiar en la imagen que contiene los nombres de campo correctos, lo cual no es fiable.</p><p>Incluye solo los campos relevantes para cada visualización, basándote en lo que sea visible en la imagen.</p><p>Tuvimos que agregar este refuerzo porque a veces intenta agregar campos que no son relevantes para la imagen.</p><p>Esto devolverá un objeto con un serial de visualizaciones para mostrar:</p>"Dashboard values generated by the LLM successfully
title=""Client, Extension, OS, and Response Keyword Analysis""visualizations="[
   "Visualization(title=""Count of Client IP",
   "type="[
      "metric"
   ],
   "field=""clientip"")",
   "Visualization(title=""Extension Keyword Distribution",
   "type="[
      "pie"
   ],
   "field=""extension.keyword"")",
   "Visualization(title=""Most Used OS",
   "type="[
      "bar"
   ],
   "field=""machine.os.keyword"")",
   "Visualization(title=""Response Keyword Distribution",
   "type="[
      "bar"
   ],
   "field=""response.keyword"")"
]<h2>Procesamiento de la respuesta de los LLM</h2><p>Creamosun panel de panel de muestra 2x2 y luego lo exportamos en JSON usando <a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-get-dashboards-dashboard">la API Get a dashboard</a>, y después almacenamos los paneles como plantillas de visualización (pastel, barra, métrica) donde podemos reemplazar algunos parámetros para crear nuevas visualizaciones con diferentes campos según la pregunta.</p><p>Puedes ver los archivos JSON <a href="https://github.com/Delacrobix/elasticsearch-labs/tree/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/supporting-blog-content/from-image-idea-to-kibana-dashboard-using-ai/templates"><strong>de plantilla aquí</strong></a>. Fíjate en cómo cambiamos los valores de los objetos que queremos reemplazar más adelante por {<code>variable_name</code>}
</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc55d69d84a08e668/6a1707d8a2929903acd00fb8/ec7e1ac0cd8b470df13e60940162b56778acb386-315x234.png" alt="" /><p>Con la información que nos proporcionó el LLM, podemos decidir qué plantilla usar y qué valores reemplazar.</p><p><code>fill_template_with_analysis</code> recibirá los parámetros de un único panel, incluyendo la plantilla JSON de la visualización, un título, un campo y las coordenadas de la visualización en la cuadrícula.</p><p>Luego, reemplazará los valores de la plantilla y devolverá la visualización JSON final.</p>def fill_template_with_analysis(
    template: Dict[str, Any],
    visualization: Visualization,
    grid_data: Dict[str, Any],
):
    template_str = json.dumps(template)
    replacements = {
	 "{visualization_id}": str(uuid.uuid4()),
        "{title}": visualization.title,
        "{x}": grid_data["x"],
        "{y}": grid_data["y"],
    }

    if visualization.field:
        replacements["{field}"] = visualization.field

    for placeholder, value in replacements.items():
        template_str = template_str.replace(placeholder, str(value))

    return json.loads(template_str)<p>Para simplificar, tendremos coordenadas estáticas que asignaremos a los paneles que el LLM decida crear y produciremos un panel de cuadrícula 2x2 como en la imagen anterior.</p># Filling templates fields
panels = []    
grid_data = [
    {"x": 0, "y": 0},
    {"x": 12, "y": 0},
    {"x": 0, "y": 12},
    {"x": 12, "y": 12},
]


i = 0

for vis in dashboard_values.visualizations:
    for vis_type in vis.type:
        template = templates.get(vis_type, templates.get("bar", {}))
        filled_panel = fill_template_with_analysis(template, vis, grid_data[i])
        panels.append(filled_panel)
        i += 1<p>Dependiendo del tipo de visualización decidido por el LLM, elegiremos una plantilla de archivo JSON y reemplazaremos la información relevante usando <code>fill_template_with_analysis</code> , luego agregaremos el nuevo panel a un array que usaremos más adelante para crear el panel de control.</p><p>Cuando el panel esté listo, usaremos la <a href="https://www.elastic.co/docs/api/doc/kibana/operation/operation-post-dashboards-dashboard-id">API Crear un panel</a> para enviar el nuevo archivo JSON a Kibana y generar el panel:
</p>try:
    dashboard_id = str(uuid.uuid4())

    # post request to create the dashboard endpoint
    url = f"{os.getenv('KIBANA_URL')}/api/dashboards/dashboard/{dashboard_id}"

    dashboard_config = {
        "attributes": {
            "title": dashboard_values.title,
            "description": "Generated by AI",
            "timeRestore": True,
            "panels": panels,  # Visualizations with the values generated by the LLM
            "timeFrom": "now-7d/d",
            "timeTo": "now",
        },
    }

    headers = {
        "Content-Type": "application/json",
        "kbn-xsrf": "true",
        "Authorization": f"ApiKey {os.getenv('ELASTICSEARCH_API_KEY')}",
    }

    requests.post(
        url,
        headers=headers,
        json=dashboard_config,
    )

    # Url to the generated dashboard
    dashboard_url = f"{os.getenv('KIBANA_URL')}/app/dashboards#/view/{dashboard_id}"

    print("Dashboard URL: ", dashboard_url)
    print("Dashboard ID: ", dashboard_id)

except Exception as e:
    print(f"Failed to create dashboard: {str(e)}")<p>Para ejecutar el script y generar el panel de control, ejecuta el siguiente comando en la consola:</p>python &lt;file_name&gt;.py<p>El resultado final será el siguiente:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5ceffed004153a4f/6a1707d9a929cf9147ae0901/e909afbf0e47d9a6e0f7bd07dfb2efcfa5cf06ac-921x715.png" alt="" /><h2>Conclusión</h2><p>Los LLMs muestran sus fuertes capacidades visuales al hacer texto a código o convertir imágenes en código. La API de los paneles también permite convertir archivos JSON en paneles, y con un LLM y algo de código, podemos convertir imágenes en un panel Kibana.</p><p>El siguiente paso es mejorar la flexibilidad de los gráficos del salpicadero empleando diferentes configuraciones de cuadra, tamaños y posiciones de tablero. Además, ofrecer soporte para visualizaciones y tipos de visualización más complejos sería una adición útil a esta aplicación.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/ai-powered-dashboards</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/ai-powered-dashboards</guid>
    <category><![CDATA[Kibana]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[Jeffrey Rengifo,Tomás Murúa]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt41727cbee6155a68/6a1707dbb0367dd2fd72bc86/eb60ceb2fbc3941745b21ae3357cbb6ea8fab18c-1443x811.png" length="0" type="image/png"/>
    <pubDate>Wed, 16 Jul 2025 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>