Skip to content

fix(orchestrator): repair failed artifact sub-queries and stop inventing figures - #90

Merged
alberthflores95 merged 2 commits into
mainfrom
fix/artifact-subquery-repair
Oct 23, 2026
Merged

alberthflores95 merged 2 commits into
mainfrom
fix/artifact-subquery-repair

Conversation

@alberthflores95

@alberthflores95 alberthflores95 commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The analytical sub-queries behind /v1/artifact are written by an LLM that sees only the main SQL and a one-line dialect note: no semantic layer, no server version. In the first GONAC artifact on ClickHouse 22.1, two of three failed and came back empty:

  • SUM("facturado") AS facturado, SUM("facturado") / nullIf(SUM("objetivo"), 0): the alias shadows the column, so the second SUM(facturado) is a sum of a sum (ILLEGAL_AGGREGATION).
  • SUM(SUM(x)) OVER (...) / nullIf(SUM(SUM(y)) OVER (...), 0): a window result cannot be used inside another expression of the same SELECT, not even through its alias (UNKNOWN_IDENTIFIER).

Nothing retried them. Separately, when no dataset has rows, the document prompt asked for "realistic benchmarks" and "indicative estimates", so the document printed invented figures that read like real ones.

Change

  • One repair per failed sub-query. The LLM gets the failed SQL and the database error and returns a corrected statement, which runs once more. A statement that fails twice keeps its error. Repairs run on the request thread because the token tracker is a ContextVar the pool threads do not inherit. The user's sql_override is never rewritten.
  • _ERROR_HINTS explains the errors whose text does not name the fix.
  • The ClickHouse dialect note carries both rules, worded so the SQL they ask for is valid on any version.
  • A failed main SQL is an error, and gets the same repair. run_query_pipeline reports a failure in answer with rows=None instead of raising, so the artifact used to show an empty main dataset with no error and derive the sub-queries from the failed statement. Seen live on the PoC: the chat's own SQL carried SUM(facturado) AS facturado next to SUM(facturado) / SUM(objetivo). If the repair fails too, the main dataset carries the error and no sub-query is built on it. A query that matched nothing is not a failure.
  • No data means no figures. The document states the data could not be retrieved instead of estimating.

Verification

  • tests/unit/test_artifact_sub_sql_repair.py: 15 tests. The first 10 fail against main, and 3 of the 4 main-SQL tests fail against the first commit (the fourth guards the no-rows case). Together with test_artifact_env.py: 22 passed. Ruff clean.
  • Measured with the real model (Anthropic Sonnet 5 on the GONAC PoC server, throwaway container), each repaired statement executed on the production database:
    • Without the hint: the window case failed again, because the model moved the aggregation into a subquery and kept the ratio in the same SELECT.
    • With the hint: 6 of 6 ran (3 per case), with figures matching a hand-written query: cumulative invoiced 3,739.7 M, cumulative progress 96.6 %.

Out of scope

The main chat SQL generator (ask-sql-generation/.../prompts/clickhouse.py) lacks the same two rules and does not retry on execution errors either (BACKLOG group P). Changing it affects every question and needs a benchmark re-run.

🤖 Generated with Claude Code

alberthflores95 and others added 2 commits October 22, 2026 17:15
…ing figures

The analytical sub-queries behind /v1/artifact are written by an LLM that sees
neither the semantic layer nor the server version. On ClickHouse 22.1 two of
three failed in the first GONAC artifact: an alias named after its column
(SUM(facturado) AS facturado) turned a later SUM(facturado) into a sum of a
sum, and a ratio of two window functions in one SELECT is an unknown
identifier. Nothing retried them.

- Each failed sub-query gets one repair attempt with its SQL and the database
  error, run on the request thread so the token tracker counts it. Where the
  error text does not name its fix, a hint explains it: without the hint the
  window case failed again; with it, 6 of 6 repairs ran on the real server.
  The user's sql_override is never rewritten.
- The ClickHouse dialect note carries both rules.
- With no data at all, the document said to use "realistic benchmarks" and
  "indicative estimates", so it printed invented figures. It now says the
  data could not be retrieved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…epair it

run_query_pipeline reports a failure in `answer` with rows=None instead of
raising, and create_artifact only caught exceptions. When the chat's own SQL
failed on the database, the main dataset came back empty with no error and the
sub-queries were derived from the statement that had just failed. Seen on the
GONAC PoC: the main SQL carried SUM(facturado) AS facturado next to
SUM(facturado) / SUM(objetivo) and ClickHouse 22.1 rejected it.

The failure is now read from the pipeline result. A failed main SQL gets the
same single repair a sub-query gets; if it cannot be repaired, the main
dataset carries the error and no sub-query is built on it. A query that ran
and matched nothing is not a failure. The user's sql_override is still never
rewritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alberthflores95
alberthflores95 merged commit d814877 into main Oct 23, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant