Files

11 KiB

id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags
id title status type domain scope priority depends blocks related created updated tags
0080 tables playground — LLM "Ask AI" + TQL/SQL emit (fase 11) completado feature
multi-app media
2026-05-12 2026-05-17

Contexto

Fase 11 del roadmap del tables playground. Dos capacidades que se construyen juntas porque comparten infra (prompt schema, runtime adapter, tests round-trip):

  1. LLM "Ask AI" — usuario o agente pregunta en lenguaje natural, modelo devuelve un nuevo TQL (o SQL DuckDB si esta linkado).
  2. TQL → SQL (DuckDB) emitter — permite a agentes escribir SQL contra el mismo modelo de datos. Ejecutable si la app linkó DuckDB; si no, solo emite el string.

Diseño one-way: TQL → SQL si, SQL → TQL no. Razon documentada en investigacion Metabase MBQL ↔ SQL: la traduccion inversa es lossy (CTEs, window fns, set ops, lateral, correlated subqueries no caben en MBQL/TQL). Patron canonico Malloy/Cube/LookML/Metabase = compile-down one-way.

Cambios

1. UI "Ask AI"

  • Boton "Ask AI" en toolbar (al lado de "+ Viz").
  • Modal:
    • InputText multiline para la pregunta.
    • Toggle output mode: TQL (default) | SQL (DuckDB) (visible solo si app fue compilada con FN_TQL_DUCKDB=1).
    • Boton "Send" + spinner.
    • Diff side-by-side: actual vs propuesto (texto highlight).
    • Botones "Apply" / "Reject" / "Edit before apply".

2. Backend LLM

  • Provider: Anthropic Claude. API key via pass anthropic/api-key.
  • Endpoint: https://api.anthropic.com/v1/messages. Model: claude-sonnet-4-6. Override env FN_LLM_MODEL.
  • Cliente HTTP: cURL via popen (sin deps nuevas).
  • Prompt template incluye:
    • Esquema TQL (de docs/TQL.md).
    • Si SQL mode: dialecto DuckDB + funciones DuckDB relevantes (date_trunc, regexp_replace, etc.).
    • Cols disponibles del stage 0 (name, type) + cols joinables.
    • Grammar Lua subset (ver §4) cuando aplique.
    • Funciones Lua disponibles (de lua_engine).
    • TQL actual.
    • Pregunta del user.
  • Response: extraer lua (TQL) o sql block del markdown, strip prose.

3. TQL → SQL DuckDB emitter

Nuevo modulo tql_to_sql.{h,cpp} (pure). Funciones:

struct SqlEmit {
    std::string sql;            // SELECT ... statement
    std::vector<std::string> params;  // bound values (?-placeholders)
    std::vector<std::string> warnings;
    std::string error;          // si emit fallo (subset out of bounds)
};

// Pure: emite SQL DuckDB equivalente a la pipeline State (stages 0..active).
// `tables` provee el schema de cada TableInput (no los cells — el caller
// decide como hidratar las tablas en DuckDB).
SqlEmit emit_sql(const State& state, const std::vector<TableInput>& tables,
                  int up_to_stage = -1 /* default = active_stage */);

Mapeo MBQL-style:

  • Stage 0 = CTE base t0 con SELECT cols + derived FROM main_t [LEFT/INNER/RIGHT/FULL JOIN joinables ON ...].
  • Stage N = CTE tN con SELECT breakouts, aggregations FROM tN-1 [WHERE filters] [GROUP BY breakouts] [ORDER BY sorts].
  • Final query SELECT * FROM t<active>.

Stage emit detalle:

  • filter Op::Eq col = "v"WHERE col = ? con params.push_back(v) (DuckDB acepta $1/?).
  • breakout "ts:month"date_trunc('month', ts) AS "ts:month". Granularity sufijo → DuckDB date_trunc.
  • aggregation countCOUNT(*) AS count.
  • aggregation p95(col)quantile_cont(col, 0.95) AS p95_col.
  • aggregation distinct colCOUNT(DISTINCT col) AS distinct_col.
  • sort {desc, col}ORDER BY col DESC.
  • Joins: 4 strategies mapean directo a LEFT/INNER/RIGHT/FULL JOIN ... ON l.k = r.k.
  • Derived cols: transpiladas via Lua subset (§4). Si formula fuera de subset → SqlEmit.error = "lua formula 'X' out of subset: <razon>".

Salida es string SQL valido DuckDB. No ejecuta — eso es responsabilidad del adapter opcional (§5).

4. Lua subset transpilable a SQL — GRAMATICA

Documentar en docs/TQL.md seccion nueva "SQL transpile subset".

Reglas duras: Lua sigue siendo potente y sin limites en runtime general. El subset solo aplica si el caller pide tql_to_sql::emit_sql(). Fuera del subset → error claro en tiempo de emit, NO en tiempo de eval. El playground sigue ejecutando Lua arbitrario sin restriccion.

Subset permitido (transpila a SQL):

Lua SQL DuckDB
Literales: numero, string "x", bool true/false, nil 1.5, 'x', TRUE/FALSE, NULL
Col ref: [colname] colname (identifier quoted si necesario)
Aritmetica: + - * / % - (unary) mismas
Comparacion: == ~= < <= > >= = <> < <= > >=
Logica: and or not AND OR NOT
String concat: .. ||
Ternary: if A then B else C end CASE WHEN A THEN B ELSE C END
Ternary inline: (A and B) or C (pattern comun Lua) CASE WHEN A THEN B ELSE C END
math.floor/ceil/abs/round/sqrt/sin/cos/log floor/ceiling/abs/round/sqrt/sin/cos/ln
math.min(a,b)/max(a,b) least(a,b)/greatest(a,b)
string.upper/lower/len(s) upper(s)/lower(s)/length(s)
string.sub(s, i, j) substring(s, i, j-i+1)
tostring(x)/tonumber(x) CAST(x AS VARCHAR)/CAST(x AS DOUBLE)
Paréntesis y precedencia mismas

Fuera de subset (error compile-time):

  • Closures: function() ... end
  • Loops: for/while/repeat
  • Locals: local x = ...
  • Tables: {...}, t[k], t.field, table.*
  • Multi-return / vararg
  • string.gsub/find/match/format (mapeo manual posible v2)
  • IO: io.*, os.*, print
  • Coroutines, metatables, debug
  • Recursion, multi-statement bodies

Error message ejemplo:

SQL transpile error en derived col 'fullname':
  formula = "[first] .. ' ' .. table.concat(parts, ',')"
  causa: 'table.concat' no esta en SQL transpile subset
  ver docs/TQL.md#sql-transpile-subset
  workaround: usar TQL puro (sin SQL emit) o reescribir formula con `..`

Helper: tql_to_sql::is_transpilable(formula, error_out) pure fn que valida una formula sin emitir.

5. DuckDB adapter (opcional)

Build flag FN_TQL_DUCKDB=1 en cpp/CMakeLists.txt opta-in. Vendor DuckDB header-only o lib (depende de tamaño). Default OFF — playground sigue compilando sin DuckDB.

API adapter:

namespace tql_duckdb {
struct Result {
    StageOutput out;        // materializado como TableInput compatible
    std::string error;
    double duration_ms = 0;
};
// Hidrata `tables` como views temp + ejecuta sql + materializa resultado.
Result execute(const std::string& sql,
                const std::vector<std::string>& params,
                const std::vector<TableInput>& tables);
}

Apps que lo usen (registry_dashboard, sqlite_api): linkean DuckDB + invocan adapter cuando user/agent pide SQL output. Playground por defecto NO linka — Ask AI solo ofrece SQL mode si #ifdef FN_TQL_DUCKDB.

6. Validacion + safety

  • Antes de aplicar TQL del LLM: tql::apply dry-run. Si fail, mostrar error + "Ask AI again with this error".
  • Antes de ejecutar SQL del LLM: parsing DuckDB en sandbox read-only (DuckDB connection sin INSERT/UPDATE/DELETE/DROP, attach read-only).
  • Lua sandbox ya cubre side effects en formulas TQL.

7. Streaming

  • Stream tokens via SSE (stream=true Anthropic).
  • Texto en vivo en modal.
  • Cuando termina, parse lua/sql block final.

8. Persistencia conversacion

  • UiState guarda lista de turns (pregunta + output propuesto + apply result + engine usado TQL/SQL).
  • Siguiente "Ask AI" turn incluye history previa.
  • Boton "Reset chat".
  • NO persistido en TQL (UI state efimero).

9. Coste / rate limit

  • Mostrar tokens estimados antes de enviar (rough char count / 4).
  • Cap input a 8000 tokens.
  • Error handling: 429 / 5xx → mensaje + reintentar.

Tests

Pure (sin red, sin DuckDB linkado)

  • Lua subset validator: is_transpilable true para casos subset, false con error claro para fuera de subset (closures, loops, table.*, string.gsub, etc.).
  • TQL → SQL emit golden tests (~20 casos):
    • stage 0 simple filter + sort → SELECT ... WHERE ... ORDER BY ...
    • stage 1 group + count → CTE chain con GROUP BY
    • granularity sufijo :monthdate_trunc('month', ts)
    • join 4 strategies con multi-key
    • derived cols subset → CASE/expressions
    • derived cols fuera subset → SqlEmit.error no vacio + warning
    • aggregation p25/p50/p75/p99 → quantile_cont(col, p)
    • empty pipeline → SELECT * FROM t0
  • TQL parseo: prompt build incluye schema + TQL + pregunta en formato esperado (mockear HTTP).
  • Response parse: extrae lua/sql block correctamente.

Round-trip (requiere DuckDB linkado)

Solo corren si FN_TQL_DUCKDB=1:

  • TQL → emit SQL → ejecutar DuckDB → resultado coincide bit-a-bit con compute_stage pure sobre los mismos cells.
  • Casos: filter, group+agg, join inner, multi-stage chain, breakout granularity month/week, derived col [a] + [b] * 2.

LLM (red real, opt-in)

  • Test integration con ANTHROPIC_API_KEY real (make test-llm): pregunta simple → recibe TQL valido → apply OK.
  • Mock test (CI): cURL stub responde con JSON predefinido → parser extrae bloque OK.

No-objetivos

  • SQL → TQL: no se implementa. Documentado en doc + en mensajes de error del Ask AI ("no soportamos SQL como input, use TQL").
  • Multi-provider (OpenAI, local): fase futura. Anthropic hardcoded v1.
  • Generacion de viz desde LLM mas alla de display token: la viz la elige TQL existente.
  • Lua subset extension (string.gsub, regex, table.*): postpone v2 si demanda real.
  • DuckDB write ops: solo SELECT/CTE. Apps que quieran INSERT/UPDATE lo hacen fuera del playground.

Flujo agente (resumen)

Agente -> "muestrame top 10 langs por total size"
LLM (TQL default) -> emite TQL { stages = {...} }
tql::apply -> State + dry-run OK
User clickea Apply -> compute_stage en memoria

Agente -> "lo mismo pero como SQL"
[Si FN_TQL_DUCKDB=1 y app linkó adapter]
LLM (SQL mode toggled) -> emite SELECT ... DuckDB
duckdb::execute(sql, params, tables) -> resultado materializado
[Si NO linkado] -> error "SQL mode requiere DuckDB. Compila con FN_TQL_DUCKDB=1"

Riesgos

  • Subset Lua restrictivo en SQL emit: usuarios usan Lua arbitrario en playground → al pedir SQL falla. Mitigacion: error message claro + sugerencia workaround.
  • DuckDB tamaño: lib ~10MB. Solo se paga si app opta-in con build flag.
  • Dialect drift DuckDB: funciones SQL pueden cambiar entre versiones. Pinear DuckDB version en CMake.
  • LLM hallucinations: TQL invalido → dry-run rechaza con error. Loop "Ask AI again with this error" recupera.
  • API key leak: pass integration mantiene fuera del repo. Build flag NUNCA imprime key.
  • Coste tokens: prompt grande (schema + grammar + TQL). Cap input + warning visual.