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:
| Endpoint | Propósito |
|---|---|
GET /rest/saiku/api/ai/ossie/models | Lista 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/query | Ejecuta una petición tipada de estado de estanterías. Respuesta en formato de registros por defecto; matrix en ?format=matrix. |
Endpoints de cola larga:
| Endpoint | Propósito |
|---|---|
POST /rest/saiku/api/ai/ossie/query/preview | Compila a SQL sin ejecutar. Misma forma VALIDATION_ERROR que /query. |
GET /rest/saiku/api/ai/ossie/values/search | Búsqueda por subcadena de valores distintos en un campo. |
POST /rest/saiku/api/ai/ossie/query/execute-async | Enví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-detail | El análogo de drillthrough de Ossie — re-ejecuta las estanterías como filas crudas. |
POST /rest/saiku/api/ai/ossie/anomaly | Ejecuta la consulta y luego marca anomalías a lo largo de un eje temporal. |
POST /rest/saiku/api/ai/ossie/forecast | Proyecta puntos futuros usando ETS / ARIMA / Prophet. |
POST /rest/saiku/api/ai/ossie/ask | Pregunta en lenguaje natural. Necesita una clave de LLM. |
GET /rest/saiku/api/ai/ossie/ask/health | Si 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/TPCDSAñ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:
labelen cada campo es el nombre legible por humanos de la especificación Ossie — expuesto en todas partes donde se renderizan columnas.NETREVENUEaparece como “Net Revenue”.sampleValuesle 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) — conestimatedDistinctcuando el warehouse soportaAPPROX_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étricasCOUNT(*)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/queryContent-Type: application/jsonPetició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/previewMismo 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=5Mismo 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:
-
Enviar —
POST /query/execute-async. Mismo cuerpo que/query. Respuesta 202:{"queryId": "...", "status": "PENDING"}. -
Consultar —
GET /query/status/{queryId}. Transiciones de estado:PENDING→RUNNING→DONE|FAILED|CANCELLED. -
Obtener —
GET /query/result/{queryId}. 202 con{queryId, status}mientras se ejecuta, 200 con la respuesta completa de registros (o?format=matrix) en DONE. -
Cancelar —
DELETE /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) oOPENAI_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 MCP | Equivalente REST |
|---|---|
list_ossie_models | GET /ai/ossie/models |
describe_ossie_model | GET /ai/ossie/schema/{c}/{m} |
search_field_values | GET /ai/ossie/values/search |
run_ossie_query | POST /ai/ossie/query |
preview_ossie_query | POST /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
list_ossie_modelsoGET /models— elija un modelo.describe_ossie_modeloGET /schema/{c}/{m}— lea datasets, campos, métricas, ejemplos.- Si el usuario nombra un valor que el schema no muestreó —
search_field_valuesoGET /values/search— confirme la ortografía. - Construya un cuerpo de consulta a partir de
examples.simpleGroupBy(u otro ejemplo) como plantilla, sustituyendo las dimensiones y métricas del usuario. run_ossie_queryoPOST /query— si la respuesta es unVALIDATION_ERROR, elija deavailabley reintente.- Ante una petición de gráfico → repita con
format: "matrix". - Ante “explain the trend” →
/anomalyo/forecast. - 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 existente — la 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-exporten el launcher de Saiku convierte un schema XML Mondrian a YAML OSI.
Ver también
- Ossie / OSI en Apache — la especificación + YAMLs de ejemplo
- Servidor MCP — el envoltorio de herramientas para agentes LLM
- Conexión dbt — apunte Saiku a su proyecto dbt existente
- API AI Query OLAP — la contraparte de cubos Mondrian de esta API