Skip to content

fix(gooddata-eval): stop metric-skill simulated user from dropping MAQL clauses - #1718

Open
Tomkess wants to merge 2 commits into
masterfrom
fix/metric-skill-simulated-user-prompt-fidelity
Open

fix(gooddata-eval): stop metric-skill simulated user from dropping MAQL clauses#1718
Tomkess wants to merge 2 commits into
masterfrom
fix/metric-skill-simulated-user-prompt-fidelity

Conversation

@Tomkess

@Tomkess Tomkess commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

agentic_metric_skill's simulated-user step (generate_simulated_response in
metric_skill.py) is what keeps a multi-turn metric-creation conversation
going after the agent asks a clarifying question — it prompts an LLM to answer
as the user, using the fixture's expected_output.maql as its only source of
truth.

The prompt told it to "reply briefly," with no instruction to preserve the
MAQL's structure. In practice this let it silently drop a WHERE/filter
clause, or paraphrase a label id, whenever the agent's question didn't happen
to ask about that specific part — so a well-behaved agent, faithfully
following the (already-wrong) simulated answer, still failed the eval through
no fault of its own.

Reproduced live (twice), against a real gdc-mic-ai-evaluation fixture

Question: "Create a metric for total ecommerce spend"
Expected: SELECT {metric/spend_amount_-_cutcgco} WHERE {label/ecommerce_indicator_code} = "1"

  1. Simulated reply dropped _code off ecommerce_indicator_code, anchoring
    the agent on a sibling attribute that doesn't carry that filter — cascaded
    into a follow-up the agent's own clarification-detection didn't recognize
    as needing a reply (separate, related issue, not fixed in this PR — see
    _is_asking_clarification's narrow keyword list).
  2. Simulated reply picked one of 3 metric options the agent itself offered and
    said "please proceed with that" — never mentioning the WHERE clause
    expected_output required, even though it had the full MAQL in hand.

Confirmed: prompt problem, not a model-capability problem

Ran the same agent_message/expected_maql pair through
client.chat.completions.create directly, 2×2, isolating model from prompt:

Condition Model Prompt Result
A gpt-4o-mini old drops the filter
B gpt-4o (stronger) old still drops the filter
C gpt-4o-mini (unchanged) new includes the filter
D gpt-4o new includes the filter

Then repeated A and C 5× each to rule out luck: old prompt 1/5 preserved
the exact field (and was unstable even in which base metric it picked — 2/5
picked a different one entirely); new prompt 5/5.

The fix

  • Instruct the simulating LLM to (a) ensure every clause of the expected MAQL
    is eventually satisfied even if the agent's question didn't ask about it,
    (b) quote field/label identifiers verbatim rather than paraphrase, (c)
    proactively add a filter the agent's own offered options omitted.
  • Drop "reply briefly," raise max_tokens 150→300 — brevity was part of what
    squeezed the filter clause out.
  • This brings metric_skill.py's simulated-user prompt in line with
    alert_skill.py's generate_simulated_alert_response, which already passes
    structured facts + explicit "proactively tell the agent X" instructions
    instead of one freely-paraphrased string — not a new pattern for this
    codebase, just extending an existing one to metric_skill.

What this does NOT fix (tracked separately, not in scope here)

_is_asking_clarification's narrow keyword match ("?", "could you",
"please provide", "clarif") can miss a legitimate open-ended fork/offer
from the agent and end the conversation early. Surfaced during the same
investigation, but a separate fix with its own risk profile — kept out of this
PR to keep the diff reviewable.

Test plan

  • Added test_generate_simulated_response_prompt_preserves_maql_fidelity
    asserts the sent prompt preserves clause-fidelity language and the
    raised max_tokens.
  • Full gooddata-eval suite: 272 passed. 9 pre-existing failures
    confirmed identical on clean master before this change (missing
    openai extra in this test env for test_llm_judge.py, unrelated
    failures in test_runner.py/test_summary_evaluator.py) — this change
    introduces zero new failures.
  • ruff check / ruff format --check clean on both changed files.
  • Live-reproduced the original bug against a real fixture, then confirmed
    the fix resolves it, on both the affected files' logic and via direct
    OpenAI API A/B testing (5× per condition).

Summary by CodeRabbit

  • Bug Fixes

    • Improved simulated metric responses to preserve complete query details, including filters and identifier formatting.
    • Ensured required query clauses are included in generated responses.
    • Increased the response limit to support more complete answers.
  • Tests

    • Added regression coverage verifying query fidelity and complete clause preservation.

…g MAQL clauses

agentic_metric_skill's simulated-user reply (generate_simulated_response) is
what keeps a multi-turn metric-creation conversation going after the agent
asks a clarifying question -- it prompts an LLM to answer as the user, using
the fixture's expected_output.maql as its only source of truth.

The prompt told it to "reply briefly" with no instruction to preserve the
MAQL's structure. In practice it would silently drop a WHERE/filter clause,
or paraphrase a label id, whenever the agent's question didn't happen to ask
about that part directly -- so a well-behaved agent, faithfully following
the (already-wrong) simulated answer, still failed the eval.

Reproduced live twice against a real gdc-mic-ai-evaluation fixture
("Create a metric for total ecommerce spend", expects
SELECT {metric/spend_amount_-_cutcgco} WHERE {label/ecommerce_indicator_code}
= "1"):

  1. Simulated reply dropped "_code" off ecommerce_indicator_code, anchoring
     the agent on a sibling attribute that doesn't have that filter.
  2. Simulated reply picked one of 3 metric options the agent offered and
     said "please proceed with that" -- never mentioning the WHERE clause
     that expected_output required, even though it had it in hand.

Confirmed via a 5x-repeated A/B test that this is a prompt problem, not a
model-capability one: swapping gpt-4o-mini for gpt-4o under the OLD prompt
did not fix it (still dropped the clause); the NEW prompt fixes it on the
ORIGINAL gpt-4o-mini (1/5 -> 5/5 runs preserving the exact filter).

Fix: instruct the simulating LLM to (a) ensure every clause of the expected
MAQL is eventually satisfied even if the agent's question didn't ask about
it, (b) quote field/label identifiers verbatim rather than paraphrase them,
and (c) proactively add a filter the agent's own offered options omitted.
Also drop "reply briefly" and raise max_tokens 150->300, since brevity was
part of what squeezed the filter clause out. This brings metric_skill's
simulated-user prompt in line with alert_skill's generate_simulated_alert_response,
which already passes structured facts + explicit "proactively tell the agent
X" instructions rather than one freely-paraphrased string -- not a new
pattern for this codebase.

Added a regression test asserting the sent prompt preserves clause-fidelity
language and the raised max_tokens. Full gooddata-eval suite: 272 passed
(9 pre-existing unrelated failures, confirmed identical on clean master
before this change -- missing openai extra in test env, and two unrelated
test files).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Tomkess
Tomkess requested review from hkad98, lupko and pcerny as code owners August 6, 2026 20:45
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Tomkess, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 741d711c-f530-47ee-9dc8-b26ed2593ec3

📥 Commits

Reviewing files that changed from the base of the PR and between f782d24 and 006ccb5.

📒 Files selected for processing (1)
  • packages/gooddata-eval/tests/test_agentic_metric_skill.py
📝 Walkthrough

Walkthrough

The simulated MAQL response prompt now requires complete, verbatim query preservation and allows 300 tokens. A regression test validates these requirements with a mocked optional OpenAI module.

Changes

MAQL prompt generation

Layer / File(s) Summary
Prompt contract and regression coverage
packages/gooddata-eval/src/gooddata_eval/core/agentic/metric_skill.py, packages/gooddata-eval/tests/test_agentic_metric_skill.py
The prompt preserves all MAQL clauses and identifiers, including filters, and increases max_tokens from 150 to 300. The regression test verifies the prompt requirements and optional-module integration.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: hkad98, lupko, pcerny

Poem

A rabbit checks each MAQL line,
Keeps every filter in design.
Three hundred tokens hop along,
While tests ensure the query’s strong.
No clause gets lost beneath the sun.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: preventing the simulated user from dropping MAQL clauses.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/gooddata-eval/tests/test_agentic_metric_skill.py`:
- Around line 46-56: Update the assertions in the test covering the generated
prompt to verify that the exact MAQL string from expected_output["maql"] appears
in sent_prompt, preserving the existing prompt checks while ensuring the metric
identifier, label, and filter value are all validated.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 57c71e54-018b-427f-b349-92c1767b4d22

📥 Commits

Reviewing files that changed from the base of the PR and between d1ab1ad and f782d24.

📒 Files selected for processing (2)
  • packages/gooddata-eval/src/gooddata_eval/core/agentic/metric_skill.py
  • packages/gooddata-eval/tests/test_agentic_metric_skill.py

Comment thread packages/gooddata-eval/tests/test_agentic_metric_skill.py
…er prompt

Addresses CodeRabbit review comment on #1718: the regression test only
checked for generic instruction words ("verbatim", "every clause"), not that
expected_output["maql"] itself made it into the prompt -- a regression that
stripped the metric/label reference or filter value entirely could still
pass. Assert the exact MAQL string is present.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Tomkess

Tomkess commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 006ccb5 — added assert expected_output["maql"] in sent_prompt exactly as suggested.

Tomkess added a commit that referenced this pull request Aug 6, 2026
_normalize_maql/_best_maql_match compare an agent's generated MAQL against
expected_output.maql via exact string equality after whitespace/wrapper
normalization -- but MAQL keywords (SELECT, FOR PREVIOUS, WHERE, BY, ...) are
case-insensitive at the query-engine level (confirmed against the MAQL
reference), while the comparison itself was fully case-sensitive.

Reproduced live in gdc-mic-ai-evaluation, post the #1718 fix: fixture
"Create a metric for the prior-year value of Active cards" expects
  SELECT {metric/active_card_count_-_txn_-_cutcgco}
    FOR Previous({label/process_date.year})
Agent produced, verbatim:
  SELECT {metric/active_card_count_-_txn_-_cutcgco} FOR PREVIOUS({label/process_date.year})
Byte-identical except FOR PREVIOUS vs FOR Previous -- scored as a fail.

First fix attempt considered and rejected: lowercase everything outside
{type/id} braces. That's wrong -- WHERE-clause literal values are ALSO
outside braces (e.g. WHERE {label/status} = "Active") and are real,
case-sensitive data, not keywords; blindly folding them would create a new
false-positive risk (two genuinely different filter values scored as equal).

Actual fix: per the MAQL reference, every literal value is quoted and every
identifier lives inside {..} -- both are exhaustively structural markers, so
protecting text inside either while casefolding everything else needs no
keyword list at all (which would risk being incomplete against MAQL's large
vocabulary: SELECT, BY, WHERE, HAVING, FOR PREVIOUS/NEXT/EACH, WITHOUT PF,
TOP/BOTTOM, WITHIN, RANK family, RUNSUM family, IFNULL, CASE/WHEN, 15+ math
functions, ...). Added _casefold_outside_protected(), applied as the final
step in _normalize_maql.

Tests added:
- keyword case-insensitivity on the exact reproduced case (FOR PREVIOUS vs
  FOR Previous)
- identifier case preserved ({metric/Mixed_Case_Id} untouched)
- quoted literal case preserved AND still distinguishes real differences
  (WHERE x = "Active" vs WHERE x = "active" must stay a genuine mismatch --
  this is the test that would have caught the rejected first draft)
Updated the one existing test whose expected value assumed no case
normalization ever happens (SELECT -> select).

Full gooddata-eval suite: 274 passed, 9 pre-existing unrelated failures
(missing openai extra in this test env; two unrelated test files) --
identical count to before this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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