Turn agent feedback and rephrased questions into an evaluation dataset¶
Mine thumbs-down feedback and user rephrases from agent traces, confirm each with one LLM call, and write a deduplicated eval dataset.
| Stage | Evaluate, Operate |
| Topics | AI Observability, Cortex Agents |
| Status | GA |
| Time to complete | 40min |
| Edition | Enterprise |
| Regions | Any region with Cortex Agents and AI_COMPLETE, AI_SIMILARITY, and AI_EMBED for the chosen models |
| Privileges | ACCOUNTADMIN for 01_setup.sql and 02_grant_unredacted.sql; everything else runs as a COOKBOOK_* role. A programmatic access token for the optional feedback notebook |
| Related skills | agent-studio (bundled with Cortex Code), cortex-ai-function-studio (bundled with Cortex Code) |
| Last verified | 2026-10-04 |
| Authors | Parker Erickson |
| Code | View all files or download agent-feedback-to-evals.zip, which includes this page as README.md. Teardown: sql/99_teardown.sql |
This recipe is a reference implementation: review the code and adapt it to your environment before production use.
Real users tell you where an agent falls short in two ways: they click thumbs down, or they ask the same thing again in different words. This recipe reads both signals from the agent’s observability events, confirms each candidate with a single LLM call, drafts a self-contained test question with ground-truth criteria, drops near-duplicates, and writes EVAL_DATASET in the INPUT_QUERY / GROUND_TRUTH shape that agent evaluations runs against.
When to use this¶
- You have an agent in use and want evaluation questions that reflect where it actually fails, not only questions you thought of in advance.
- You want the mining to be a rerunnable SQL job with every stage inspectable.
When not to use this:
- You need to score answers: that is
EXECUTE_AI_EVALUATION; see agent evaluations. - You need latency or per-request debugging: see diagnose-agent-requests.
- You cannot grant a role access to conversation text. Without
READ UNREDACTED AI OBSERVABILITY EVENTS TABLE, questions and feedback text read as NULL and there is nothing to mine.
How it works¶
- Two signals. Explicit: a
CORTEX_AGENT_FEEDBACKevent withpositive = false, linked to the turn it rates. Implicit: a follow-up turn in a thread whose question re-asks the previous one. A turn’sparent_message_idequals the previous turn’smessage_id, so pairs join exactly. - Similarity is a pre-filter, not the decision.
AI_SIMILARITYruns once per pair and is stored. A real rephrase (“How is Tofu doing?” then “I meant total Tofu revenue by region…”) scores about 0.59 and a genuine follow-up (“What was total revenue in the North region?” then “And how does that compare to the South?”) about 0.42, so no single threshold separates them. The floor only limits how many pairs reach the LLM. - One LLM call per candidate. A single
AI_COMPLETEwith a JSON schema returns whether the pair is dissatisfaction, a self-containedeval_question, anintent,ground_truth_outputcriteria, andexpected_tools. Rows the model answers badly stay inEVAL_CANDIDATESasinvalid_llm_outputinstead of disappearing. That depends onAI_SQL_ERROR_HANDLING_USE_FAIL_ON_ERROR = FALSE: withTRUE, one failed call aborts the wholeCREATE TABLE AS, so05_mine_signals.sqlsets it toFALSEfor its own session. - Dedupe by meaning.
AI_EMBEDembeds each kept question once; a candidate whose cosine similarity to a higher-priority one is at or above 0.92 is dropped. - Drafts, not truth. Ground truth is written as criteria (“must filter to Tofu, cover July 2025 to June 2026, one figure per region”), and every row carries
review_status: draftfor a human to check before the dataset is used.
Prerequisites¶
| Requirement | Notes |
|---|---|
Cortex Agents, AI_COMPLETE, AI_SIMILARITY, AI_EMBED | Models: claude-sonnet-4-6 and snowflake-arctic-embed-l-v2.0, set at the top of 05_mine_signals.sql |
SNOWFLAKE.CORTEX_USER | Granted to the recipe role by 01_setup.sql |
READ UNREDACTED AI OBSERVABILITY EVENTS TABLE | Account-level privilege, granted to the recipe role only by 02_grant_unredacted.sql and removed when teardown drops the role |
| Programmatic access token (optional) | Only for notebooks/01_submit_feedback.ipynb, exported as SNOWFLAKE_PAT. Without it, the recipe mines rephrases only. You create the token yourself for your own user. A TYPE = PERSON user can generate a token without a network policy but cannot sign in with it unless the user, or the whole account, is subject to a network policy that allows the address the notebook runs from (or an authentication policy relaxes the requirement) |
| Python | snowflake-snowpark-python and requests for the notebook |
Quickstart¶
sql/01_setup.sqlasACCOUNTADMIN: role (granted to you), warehouse, database.sql/02_grant_unredacted.sqlasACCOUNTADMIN: lets the recipe role read conversation text.sql/03_agent.sql: 800 synthetic Northwind orders, a semantic view, andORDERS_ASSISTANT.sql/04_simulate_traffic.sql: replays 8 scripted conversations (13 turns) throughDATA_AGENT_RUN, one thread per conversation, logging every call inRUN_LOG.- Optional:
notebooks/01_submit_feedback.ipynbsends the thumbs up or down inFEEDBACK_SCRIPTthrough the Feedback REST API. sql/05_mine_signals.sql: buildsAGENT_TURNS,FEEDBACK_EVENTS,TURN_PAIRS,EVAL_CANDIDATES,CANDIDATE_EMBEDDINGS, andEVAL_DATASET, then prints a funnel.- Review
EVAL_CANDIDATES, then run the dataset with agent evaluations. sql/99_teardown.sqlwhen done.
Walkthrough¶
Traffic with threads, in SQL¶
The third argument creates a thread when the body has none. The response metadata carries the thread id and assistant message id for the next turn. Each call is in its own exception block that writes the error to RUN_LOG and skips the rest of that conversation.
Explicit feedback¶
The Feedback REST API body has orig_request_id, positive, feedback_message, categories, and thread_id. The notebook finds each turn’s request id in the trace (the trace’s message_id equals the assistant message id returned by DATA_AGENT_RUN), posts the feedback, and records every HTTP status in FEEDBACK_LOG.
On the read side, the documented filter is RECORD:name = 'CORTEX_AGENT_FEEDBACK'; the documentation does not specify where inside the event the fields live. 05_mine_signals.sql therefore matches them by name across both VALUE and RECORD_ATTRIBUTES, links by request id, and falls back to the latest earlier turn in the same thread. The funnel reports feedback it could not link.
Rephrase pairs¶
One call per candidate¶
The conversation goes in as a JSON document and the instructions tell the model to treat it as data. The output is stored, never executed.
Using the dataset¶
EVAL_DATASET has the two columns an evaluation config maps:
GROUND_TRUTH contains ground_truth_output (read by answer_correctness) and expected_tools, plus intent, source, source_request_id, and review_status for your own filtering. Staging the config and running EXECUTE_AI_EVALUATION is covered in agent evaluations.
Verify¶
The funnel at the end of 05_mine_signals.sql, for the scripted traffic without the notebook, looks like:
| Stage | Expected |
|---|---|
| agent turns / turns with question text | 13 or more, equal counts (unequal means the unredacted grant is missing) |
| follow-up pairs | 5 (conversations 3, 4, 5, 6, 8), more if you rerun 04 |
| pairs above similarity floor | most of them; “Break that down by channel” usually falls below 0.40 |
| confirmed dissatisfaction | the Tofu and Online-share rephrases; the North vs South follow-up is judged not dissatisfaction |
| invalid LLM output | 0 |
| eval dataset rows | one per distinct confirmed question after dedupe |
If you ran the notebook, FEEDBACK_LOG has three rows with their HTTP status, feedback events counts what arrived, and each linked thumbs-down becomes an explicit_negative candidate. Check feedback not linked to a turn: it should be 0.
Gotchas¶
| Symptom | Cause | Fix |
|---|---|---|
turns with question text is 0 | The role lacks READ UNREDACTED AI OBSERVABILITY EVENTS TABLE, so text reads as NULL | Run 02_grant_unredacted.sql |
| A rephrase you expected is missing | Its similarity fell below the floor | Lower similarity_floor; the LLM step still filters follow-ups |
| Borderline pairs judged differently between runs | “Best month?” then a fully specified version of the same question can read as either a rephrase or a clarification | Keep humans in the loop through review_status; tune the instructions if your users’ patterns differ |
| Duplicate questions after rerunning 04 | Each rerun adds new traffic with the same questions | Dedupe removes near-identical ones; truncate old traffic by filtering AGENT_TURNS on started_at if needed |
Notebook raises Set SNOWFLAKE_PAT | No token in the environment | Export a programmatic access token for a user that can use the recipe role |
Every row in FEEDBACK_LOG shows an authentication error, although the token is current | The token’s user is not subject to a network policy, so Snowflake rejects sign-in with a programmatic access token | Activate a network policy for that user (or the account) that allows the notebook’s address, or an authentication policy with PAT_POLICY = (NETWORK_POLICY_EVALUATION = ENFORCED_NOT_REQUIRED); then rerun the submit cell |
Notebook raises feedback submission(s) failed | HTTP error from the Feedback API (auth, role, or network) | Read RESPONSE_BODY in FEEDBACK_LOG |
feedback not linked to a turn above 0 | The event carried neither a request id nor a thread id that matches a turn | Submit feedback with orig_request_id (the notebook does) |
| Draft ground truth contains figures | The model can copy numbers from the agent’s own answer, which may be the wrong answer being tested | The prompt tells it not to; still treat every draft as unverified and have a reviewer check or add exact values from the data |
| Some traces missing | Observability telemetry is best-effort; a span can be dropped or arrive late | Rerun 05 later; treat the dataset as a sample, not a census |
Teardown¶
Run sql/99_teardown.sql as ACCOUNTADMIN. It drops the role (which removes the unredacted grant), the database (agent, semantic view, data, and every mined table), and the warehouse. Events already written stay in SNOWFLAKE.LOCAL.AI_OBSERVABILITY_EVENTS, which can’t be modified.
Adapt this recipe¶
Keep these, so a customized version keeps the recipe’s security model:
- Least privilege:
ACCOUNTADMINonly for the scripts listed under Privileges; everything else runs as the recipe’s own role. - No grants to
PUBLIC. - Caller’s rights: procedures run as
EXECUTE AS CALLER, so each user’s own grants apply. - Credentials: create keys, tokens, and secrets yourself, in a separate terminal, and read them from environment variables. Never put them in a script or give them to an agent.
- Dynamic SQL, if you add any, uses bind variables or an allowlist of identifiers, never text concatenated from user or model input.
Change these to fit your use case:
- Object names. Rename them in every script, including teardown, then check that teardown still removes everything.
- Roles, personas, and users: map them to your own, each granted no more than the recipe grants.
- Data: point the scripts at your tables instead of the sample data, then run Verify again.
- Models and warehouse sizes.
To have a coding agent adapt it:
Run it with a coding agent¶
Related skills¶
agent-studio(bundled with Cortex Code): building and running agent evaluations and theagent-observability-reportfor aggregate monitoring. Use it with the dataset this recipe writes.cortex-ai-function-studio(bundled): choosing, testing, and estimating the cost of the AI functions used here (AI_COMPLETE,AI_SIMILARITY,AI_EMBED).