Saltearse al contenido

API AI Query — modelos Ossie

La API AI Query para Ossie es la contraparte en YAML semántico de la API AI Query para cubos OLAP. La misma disciplina: los agentes obtienen un schema autodescriptivo, rellenan una petición JSON contra él, el servidor valida cada nombre, ejecuta, y devuelve registros tipados. Las mismas barandillas: los agentes nunca escriben SQL directamente.

Donde la API AI OLAP trabaja contra cubos Mondrian, esta trabaja contra modelos declarados en YAML de Open Semantic Interchange / Apache Ossie. Todo lo que hace el workbench — filtros, ordenaciones, sobrescrituras de agregación, pivote crosstab, vista de gráfico — está disponible programáticamente a través de esta API, más una capa /ask de lenguaje natural y analíticas por consulta (detección de anomalías, forecasting).

Orientación rápida

Tres endpoints cubren ~90% del uso de los agentes:

EndpointPropósito
GET /rest/saiku/api/ai/ossie/modelsLista cada modelo Ossie que el llamador puede consultar.
GET /rest/saiku/api/ai/ossie/schema/{connection}/{model}Schema autodescriptivo — datasets, campos, métricas, relaciones, JSON Schema del cuerpo de la petición, cuerpos de ejemplo listos para usar.
POST /rest/saiku/api/ai/ossie/queryEjecuta una petición tipada de estado de estanterías. Respuesta en formato de registros por defecto; matrix en ?format=matrix.

Endpoints de cola larga:

EndpointPropósito
POST /rest/saiku/api/ai/ossie/query/previewCompila a SQL sin ejecutar. Misma forma VALIDATION_ERROR que /query.
GET /rest/saiku/api/ai/ossie/values/searchBúsqueda por subcadena de valores distintos en un campo.
POST /rest/saiku/api/ai/ossie/query/execute-asyncEnvía para ejecución en segundo plano.
GET /rest/saiku/api/ai/ossie/query/status/{queryId}Consulta el estado.
GET /rest/saiku/api/ai/ossie/query/result/{queryId}Obtiene un resultado async completado.
DELETE /rest/saiku/api/ai/ossie/query/{queryId}Cancela una consulta en vuelo.
POST /rest/saiku/api/ai/ossie/row-detailEl análogo de drillthrough de Ossie — re-ejecuta las estanterías como filas crudas.
POST /rest/saiku/api/ai/ossie/anomalyEjecuta la consulta y luego marca anomalías a lo largo de un eje temporal.
POST /rest/saiku/api/ai/ossie/forecastProyecta puntos futuros usando ETS / ARIMA / Prophet.
POST /rest/saiku/api/ai/ossie/askPregunta en lenguaje natural. Necesita una clave de LLM.
GET /rest/saiku/api/ai/ossie/ask/healthSi la capa de ask está configurada en esta instancia.

Todos los endpoints requieren una sesión autenticada; los endpoints POST requieren el par cookie/cabecera CSRF. Mismo modelo de auth que la API AI OLAP.

Paso 1 — listar los modelos

GET /rest/saiku/api/ai/ossie/models
[
{
"connectionName": "unknown_TPCDS",
"modelName": "TPCDS",
"description": "TPC-DS retail — sales, customers, products, stores.",
"factDataset": "store_sales",
"datasetCount": 5,
"metricCount": 5
}
]

El par connectionName + modelName es el identificador usado en todo lo demás.

Paso 2 — obtener el schema

GET /rest/saiku/api/ai/ossie/schema/unknown_TPCDS/TPCDS

Añada ?refresh=true para saltarse la caché de valores de muestra (el TTL por defecto es 5 minutos; sobrescriba mediante SAIKU_AI_OSSIE_SAMPLES_TTL_MINUTES).

La respuesta es densa por diseño — esto es lo que hace la API autodescriptiva:

{
"modelId": "unknown_TPCDS/TPCDS",
"connectionName": "unknown_TPCDS",
"modelName": "TPCDS",
"factDataset": "store_sales",
"datasets": {
"item": {
"name": "item",
"source": "ITEM",
"primaryKey": ["I_ITEM_SK"],
"fields": {
"i_brand": {
"name": "i_brand",
"label": "Brand",
"type": "VARCHAR",
"cardinality": "low",
"sampleValues": ["AudioLine", "BookHouse", "CasualCo", "DeskPro"]
},
"i_category": {
"name": "i_category",
"label": "Category",
"type": "VARCHAR",
"cardinality": "low",
"sampleValues": ["Apparel", "Books", "Electronics", "Furniture"]
}
}
}
},
"metrics": {
"total_sales": {
"name": "total_sales",
"expression": "SUM(\"store_sales\".\"SS_QUANTITY\" * \"store_sales\".\"SS_SALES_PRICE\")",
"aggregationKind": "sum",
"supportedOverrides": ["SUM", "AVG", "MIN", "MAX", "COUNT"]
},
"transaction_count": {
"name": "transaction_count",
"expression": "COUNT(*)",
"aggregationKind": "count",
"supportedOverrides": ["COUNT"]
}
},
"relationships": [
{
"name": "store_sales_to_item",
"from": "store_sales",
"to": "item",
"fromColumns": ["SS_ITEM_SK"],
"toColumns": ["I_ITEM_SK"]
}
],
"requestSchema": { /* JSON Schema for the POST /query body */ },
"examples": {
"simpleGroupBy": {
"description": "Total sales grouped by item brand",
"body": {
"model": "TPCDS",
"rows": [{"dataset": "item", "field": "i_brand"}],
"values": [{"metric": "total_sales"}]
}
}
}
}

Facilidades notables:

  • label en cada campo es el nombre legible por humanos de la especificación Ossie — expuesto en todas partes donde se renderizan columnas. NETREVENUE aparece como “Net Revenue”.
  • sampleValues le da al agente valores reales con los que filtrar. Nada de alucinar “US” cuando el valor real es “United States”.
  • Pistas de cardinality (low / medium / medium-high / high) — con estimatedDistinct cuando el warehouse soporta APPROX_COUNT_DISTINCT. Los agentes pueden decidir si “filtrar por esta columna” es realista.
  • supportedOverrides — el conjunto de sobrescrituras de agregación que el traductor realmente reescribirá. Las métricas COUNT(*) solo aceptan COUNT (expuesto por la suite de fuzz).
  • examples — cuerpos de petición para copiar y pegar para las formas comunes (simpleGroupBy, crosstab, topN).

Paso 3 — ejecutar una consulta

POST /rest/saiku/api/ai/ossie/query
Content-Type: application/json

Petición:

{
"connection": "unknown_TPCDS",
"model": "TPCDS",
"rows": [{"dataset": "customer", "field": "c_state"}],
"values": [{"metric": "total_sales"}],
"sorts": [{"metric": "total_sales", "direction": "DESC"}],
"limit": 5
}

Respuesta (formato de registros, el por defecto):

{
"queryId": "ossie-ai-a3f81",
"runtime": 210,
"columns": [
{"key": "customer.c_state", "label": "State", "type": "dimension"},
{"key": "total_sales", "label": "total_sales", "type": "metric", "aggregationKind": "sum"}
],
"records": [
{"customer.c_state": "CA", "total_sales": {"value": 1835.0, "formatted": "1835.00"}},
{"customer.c_state": "NY", "total_sales": {"value": 1281.8, "formatted": "1281.80"}}
],
"meta": {
"rowCount": 2,
"truncated": false
}
}

Añada ?format=matrix para una salida indexada por posición cellSetHeaders + cellSetBody — la forma que el endpoint de registros OLAP devuelve para consumidores aguas abajo que ya la manejan.

Operadores de filtro

EQ, NEQ, LT, LTE, GT, GTE, IN, BETWEEN, IS_NULL, IS_NOT_NULL. Los operadores de valor único usan value; IN / BETWEEN usan values. Un IN vacío sintetiza el predicado trivialmente falso (devuelve cero filas sin un error de parseo).

{
"filters": [
{"dataset": "customer", "field": "c_state", "op": "IN",
"values": ["CA", "NY", "TX"]},
{"dataset": "store_sales", "field": "SS_SALES_PRICE", "op": "GT",
"value": "50"}
]
}

Sobrescrituras de agregación al vuelo

Cambie la agregación externa declarada de una métrica mediante values[i].aggregation. El servidor valida contra los supportedOverrides de la métrica:

// Bad: SUM on a COUNT(*) metric
{"metric": "transaction_count", "aggregation": "SUM"}
// 400 Response
{
"error": "VALIDATION_ERROR",
"field": "values[0].aggregation",
"message": "aggregation 'SUM' not supported for metric 'transaction_count'",
"available": ["COUNT"]
}

Supresión por k-anonimato

Configure en el servidor mediante SAIKU_AI_KANONYMITY_K (por defecto 5) y SAIKU_AI_KANONYMITY_MASK (por defecto null). Cuando está activado, las filas cuya métrica de forma de recuento cae por debajo del umbral tienen sus celdas de métrica enmascaradas, y un bloque de nivel superior meta.suppressed registra el recuento:

{
"records": [
{"customer.c_state": "MA", "transaction_count": {"formatted": "null"}}
],
"meta": {
"rowCount": 4,
"suppressed": {"count": 4, "reason": "k-anonymity threshold k=5"}
}
}

Aplica a la salida de registros + matrix, y a los endpoints derivados /ai/anomaly y /ai/forecast — las filas suprimidas se enmascaran antes de que el puntuador de anomalías o el forecaster las vean, así que un valor de cohorte pequeño no puede filtrarse de vuelta a través de una anotación.

Redacción de PII

Los campos marcados como pii: true en el YAML de Ossie se eliminan por completo de la vista del schema. Los llamadores no pueden referenciarlos; una consulta que lo hace devuelve VALIDATION_ERROR nombrando el campo como “unknown”.

Paso 4 — cómo la API enseña al agente

Cada nombre incorrecto vuelve con una lista de candidatos. El siguiente intento del agente elige uno:

// Request with a typo
{"rows": [{"dataset": "geographi", "field": "region"}], "values": [{"metric": "net_revenue"}]}
// 400
{
"error": "VALIDATION_ERROR",
"field": "rows[0].dataset",
"message": "unknown dataset 'geographi'",
"available": ["fact_pharma", "geography", "payer", "product"]
}

La misma forma cubre campos desconocidos, métricas desconocidas, operadores de filtro no soportados, BETWEEN con menos de dos valores, referencias de ordenación que nombran tanto metric como field, limit ≤ 0, rows/columns/values vacíos, y timeAxis en /anomaly + /forecast que no aparece en la consulta.

Paso 5 — preview

POST /rest/saiku/api/ai/ossie/query/preview

Mismo cuerpo que /query. Respuesta:

{
"queryId": "ossie-ai-preview-9fe75acf",
"status": "PREVIEW",
"generatedSql": "SELECT \"customer\".\"C_STATE\" AS \"customer.c_state\", SUM(\"store_sales\".\"SS_QUANTITY\" * \"store_sales\".\"SS_SALES_PRICE\") AS \"total_sales\" FROM \"store_sales\", \"customer\" GROUP BY \"customer\".\"C_STATE\""
}

Usa el mismo traductor que ejecuta el ejecutor — ve 1:1 lo que /query despacharía.

Paso 6 — búsqueda de valores

GET /rest/saiku/api/ai/ossie/values/search?connection=unknown_TPCDS&dataset=customer&field=C_STATE&q=CA
{
"matches": ["CA"]
}

Ejecuta SELECT DISTINCT ... WHERE UPPER(CAST(... AS VARCHAR)) LIKE '%...%' LIMIT n. Omita q para los primeros N valores distintos.

Paso 7 — detalle de fila (drillthrough)

POST /rest/saiku/api/ai/ossie/row-detail?maxrows=5

Mismo cuerpo que /query. El servidor re-ejecuta las estanterías con values=[] para que el ejecutor emita filas crudas en lugar de un agregado. La respuesta es en formato de registros con meta.truncated: true cuando se alcanza el límite de filas (por defecto 100, máx 10 000).

Paso 8 — async

Para consultas que espera que tarden más de unos pocos segundos:

  1. EnviarPOST /query/execute-async. Mismo cuerpo que /query. Respuesta 202: {"queryId": "...", "status": "PENDING"}.

  2. ConsultarGET /query/status/{queryId}. Transiciones de estado: PENDINGRUNNINGDONE | FAILED | CANCELLED.

  3. ObtenerGET /query/result/{queryId}. 202 con {queryId, status} mientras se ejecuta, 200 con la respuesta completa de registros (o ?format=matrix) en DONE.

  4. CancelarDELETE /query/{queryId}.

Paso 9 — analíticas

Detección de anomalías

POST /rest/saiku/api/ai/ossie/anomaly
{
"query": {
"connection": "unknown_TPCDS", "model": "TPCDS",
"rows": [{"dataset": "date_dim", "field": "d_month"}],
"values": [{"metric": "total_sales"}]
},
"timeAxis": "date_dim.d_month",
"method": "zscore",
"threshold": 1.5
}

Detectores: zscore (z-score clásico, sigmas desde la media), mad (desviación absoluta mediana, robusta frente a outliers), stl (descomposición estacional-tendencia; recurre a zscore con datos no estacionales).

La respuesta es la forma de registros con una celda de métrica anotada con anomalía donde el detector marcó un punto:

{
"records": [
{
"date_dim.d_month": "December",
"total_sales": {
"value": 4235.75,
"formatted": "4235.75",
"anomaly": {"score": 1.85, "expected": 2900.12, "direction": "high"}
}
}
],
"anomaly": {"method": "zscore", "threshold": 1.5, "anomalyCount": 1}
}

Forecast

POST /rest/saiku/api/ai/ossie/forecast
{
"query": { /* ... */ },
"timeAxis": "date_dim.d_month",
"method": "ets",
"horizon": 3,
"interval": 0.95
}

Forecasters: ets, arima, prophet. Los registros históricos quedan intactos; las proyecciones aterrizan bajo un bloque de nivel superior forecast indexado por métrica:

{
"records": [ /* historical rows */ ],
"forecast": {
"total_sales": {
"method": "ets",
"horizon": 3,
"confidence": 0.95,
"points": [
{"index": 4, "value": 573.41, "lower": 417.73, "upper": 729.08},
{"index": 5, "value": 598.97, "lower": 378.81, "upper": 819.14}
]
}
}
}

Paso 10 — pregunta en lenguaje natural

GET /rest/saiku/api/ai/ossie/ask/health
{"configured": true, "provider": "anthropic (claude-sonnet-4-6)"}

Active en el servidor:

  • saiku.ai.ask.provider = anthropic | openai
  • env ANTHROPIC_API_KEY (Anthropic) o OPENAI_API_KEY (OpenAI)
  • opcional saiku.ai.ask.model — sobrescritura del id del modelo
  • opcional saiku.ai.ask.endpoint — URL base personalizada para proxies compatibles con OpenAI (vLLM, Ollama, Together)

Luego:

POST /rest/saiku/api/ai/ossie/ask
{
"connection": "unknown_TPCDS",
"model": "TPCDS",
"question": "What's total revenue per state for the CA and NY brands?",
"history": [
{"role": "user", "content": "show me sales by product"},
{"role": "assistant", "content": "here's revenue by brand..."}
]
}

history es opcional — cada turno se pasa al LLM para que pueda resolver seguimientos como “what about by state?”. Respuesta:

{
"question": "...",
"connection": "unknown_TPCDS",
"model": "TPCDS",
"queryUsed": { /* the OssieAiQueryRequest the LLM produced */ },
"response": { /* the full records-format execution result */ }
}

El LLM es forzado a salida estructurada mediante tool_use (Anthropic) / tool_choice: function (OpenAI) cuyo schema refleja OssieAiQueryRequest. Las preguntas fuera de tema vuelven como un sobre OFF_TOPIC expuesto como 400 con la razón adjunta.

Integración MCP

Las cinco herramientas Ossie se exponen mediante el endpoint MCP junto a las seis herramientas OLAP:

Herramienta MCPEquivalente REST
list_ossie_modelsGET /ai/ossie/models
describe_ossie_modelGET /ai/ossie/schema/{c}/{m}
search_field_valuesGET /ai/ossie/values/search
run_ossie_queryPOST /ai/ossie/query
preview_ossie_queryPOST /ai/ossie/query/preview

Claude Desktop, Cursor, Cline — cualquier cosa que hable MCP — ve las once herramientas una vez autenticado.

Un bucle de agente típico

  1. list_ossie_models o GET /models — elija un modelo.
  2. describe_ossie_model o GET /schema/{c}/{m} — lea datasets, campos, métricas, ejemplos.
  3. Si el usuario nombra un valor que el schema no muestreó — search_field_values o GET /values/search — confirme la ortografía.
  4. Construya un cuerpo de consulta a partir de examples.simpleGroupBy (u otro ejemplo) como plantilla, sustituyendo las dimensiones y métricas del usuario.
  5. run_ossie_query o POST /query — si la respuesta es un VALIDATION_ERROR, elija de available y reintente.
  6. Ante una petición de gráfico → repita con format: "matrix".
  7. Ante “explain the trend” → /anomaly o /forecast.
  8. Ante “show me the underlying rows” → /row-detail.

O para flujos de lenguaje natural: sáltese 4–7 y use /ask.

De dónde vienen los modelos

  • Escriba su propio YAML OSI — declare datasets, métricas, relaciones. Apunte Saiku a él mediante un archivo de datasource .sds.
  • Apunte a un proyecto dbt existentela guía de conexión dbt recorre el conversor de ~200 líneas que entregamos, que lee YAML de MetricFlow y emite YAML conforme a OSI.
  • Exporte un schema Mondrian — el CLI saiku ossie-export en el launcher de Saiku convierte un schema XML Mondrian a YAML OSI.

Ver también