A customer says, “Thanks for the update. I appreciate your efforts, but my account is still locked.” A sentiment model may reasonably interpret that as neutral or courteous. But a support operations dashboard needs to surface a more consequential fact: the issue remains unresolved.
This distinction motivated a small open-source project I built with Snowflake Cortex AI, SQL, and Streamlit in Snowflake. Instead of reducing a support conversation to positive or negative sentiment, the implementation asks a different question: What evidence does the customer's message provide about the status of the issue?
The repository includes reproducible SQL, synthetic case data, stored Cortex outputs, evaluation queries, a Streamlit dashboard, and a short presentation video. You can inspect the observed results without needing a Snowflake account.
Project: Snowflake Cortex Customer Intelligence on GitHub
Archived project: Zenodo DOI 10.5281/zenodo.24098765
90-second presentation: Watch the implementation walkthrough
Why sentiment doesn't answer the resolution question
Customer support language is often polite, indirect, or incomplete. Consider four hypothetical messages:
| Customer message | What we can infer |
|---|---|
| “I tested the fix and can now sign in.” | The customer confirms resolution. |
| “I tried again, but sign-in still fails.” | The problem persists. |
| “Your ticket says resolved, but I still cannot sign in.” | The customer disputes a claimed resolution. |
| “Can you send me the sign-in documentation?” | There isn't enough evidence of resolution status. |
A sentiment score does not reliably distinguish these operational situations. In particular, an unresolved issue is not necessarily a disputed resolution. A dispute requires a prior assertion that the problem was fixed or closed and a customer response contradicting it.
I therefore separated the demonstration into three concepts: sentiment polarity, expressed dissatisfaction, and resolution evidence. The 200-case experiment discussed below evaluates only the third concept. It does not establish accuracy for dissatisfaction or sentiment classification.
A Snowflake-native architecture
View the complete Snowflake Cortex architecture diagram on GitHub
The implementation has five parts:
- Input tables: Store synthetic customer messages and their authored reference labels.
-
Cortex inference: Classify the message into a resolution-evidence state with
AI_CLASSIFY. -
Versioned results: Persist raw model responses as
VARIANTwith a run version and timestamp. - Evaluation view: Compare extracted model labels with the constructed references.
- Streamlit in Snowflake: Read stored results and let users inspect cohort metrics, the confusion matrix, and individual cases.
An important design choice is separating inference from presentation. Streamlit doesn't invoke the AI function on each refresh. Instead, it reads a saved comparison view. That makes the application easier to audit and helps prevent repeated inference charges when someone changes a filter.
The broader repository also includes a bounded enrichment pipeline that demonstrates AI_SENTIMENT, AI_EXTRACT, and issue-category AI_CLASSIFY. The resolution experiment is a separate, focused evaluation.
Step 1: Define resolution evidence explicitly
I used four labels with operational meanings:
| Label | Definition |
|---|---|
confirmed_resolved |
The customer explicitly verifies the desired outcome or a working fix. |
reported_unresolved |
The customer says an issue is ongoing or an attempted fix failed, without contradicting a prior claim of completion. |
disputed |
The customer explicitly challenges a previous statement or ticket status that the problem was resolved. |
unknown |
The message offers insufficient current resolution evidence. |
These definitions matter as much as the model call. For example, “I tried your suggestion and it failed” is not automatically a disputed case. It may be only reported_unresolved unless the earlier suggestion was explicitly declared to have solved the problem.
Step 2: Run four-class classification with Cortex AI_CLASSIFY
The experiment stores cases in RESOLUTION_V3_CASES and writes model outputs to RESOLUTION_V3_RESULTS. The central classification call uses label descriptions and task-level instructions:
SELECT
c.CASE_ID,
'resolution-v3.0' AS VERSION,
CURRENT_TIMESTAMP() AS RUN_AT,
AI_CLASSIFY(
c.TRANSCRIPT,
[
{'label':'confirmed_resolved',
'description':'The customer explicitly confirms they tested a fix or received the desired outcome and the problem is resolved.'},
{'label':'reported_unresolved',
'description':'The customer reports the problem is ongoing or an attempted fix failed, without explicitly contradicting a previous declaration that it was resolved.'},
{'label':'disputed',
'description':'The customer explicitly challenges another actor''s statement or ticket status asserting the issue is resolved, completed, or fixed.'},
{'label':'unknown',
'description':'There is insufficient evidence about current resolution status: a procedural question, future promise, documentation request, or polite closing.'}
],
{
'task_description':
'Classify resolution evidence only, not sentiment or courtesy. '
|| 'Choose disputed only for explicit contradiction of a prior '
|| 'completion claim. For future plans or questions without '
|| 'current issue evidence, choose unknown. Return one label.',
'output_mode':'single'
}
) AS RESOLUTION_RAW
FROM RESOLUTION_V3_CASES c;
Illustrative excerpt adapted from the project's executable script. Use the complete versioned SQL file to rerun the experiment; the excerpt above is not a substitute for its schema, idempotency and persistence logic.
The runnable script avoids reclassifying case/version pairs that already have a successful result. This is valuable when rerunning a notebook or working in a time-limited trial environment.
Full SQL: sql/09_resolution_v3_200_benchmark.sql
Step 3: Persist predictions and compare evidence
Cortex responses are stored in a VARIANT column instead of immediately discarded after extracting a label. A versioned comparison view then exposes both the raw response and a normalized prediction:
SELECT
c.CASE_ID,
c.GOLD_RESOLUTION,
r.RESOLUTION_RAW,
r.RESOLUTION_RAW:labels[0]::VARCHAR AS CORTEX_RESOLUTION,
IFF(
r.RESOLUTION_RAW:labels[0] IS NULL,
NULL,
IFF(
r.RESOLUTION_RAW:labels[0]::VARCHAR = c.GOLD_RESOLUTION,
1, 0
)
) AS EXACT_MATCH
FROM RESOLUTION_V3_CASES c
LEFT JOIN RESOLUTION_V3_RESULTS r
ON c.CASE_ID = r.CASE_ID
AND r.VERSION = 'resolution-v3.0';
Simplified excerpt; the committed comparison view also selects the latest run per case and includes product, domain, cohort, and timestamp.
Preserving the raw responses helps with debugging, schema changes, audits of unexpected labels, and more nuanced evaluation than a single aggregate percentage.
Step 4: Show the observed outcomes in Streamlit
The Streamlit application uses an active Snowflake session to read CORTEX_CI_DEMO.APP.V_RESOLUTION_V3_COMPARISON:
import streamlit as st
from snowflake.snowpark.context import get_active_session
@st.cache_data(ttl=300)
def load_predictions():
session = get_active_session()
return session.sql("""
SELECT CASE_ID, PRODUCT, DOMAIN, COHORT,
TRANSCRIPT, GOLD_RESOLUTION,
CORTEX_RESOLUTION, EXACT_MATCH,
TO_VARCHAR(RESOLUTION_RAW) AS RESOLUTION_RAW_TEXT
FROM CORTEX_CI_DEMO.APP.V_RESOLUTION_V3_COMPARISON
WHERE CASE_ID LIKE 'R3-%'
ORDER BY CASE_ID
""").to_pandas()
This is a read-only data retrieval path—no Cortex inference runs when a dashboard user reloads the page.
Actual Snowsight screenshot of the 200-case dashboard. The agreement metric reflects controlled synthetic examples, not field accuracy.
The dashboard exposes per-class counts, a confusion matrix, filters by reference label and domain, and an individual-case explorer that displays the message, expected label, Cortex prediction, and stored raw response.
Actual case-level explorer from the working application.
Streamlit source: streamlit_resolution_v3.py
What did the experiments show?
I ran three small exercises during the November 2026 Snowflake trial:
| Exercise | Observed outcome | Interpretation |
|---|---|---|
| 8-case enrichment smoke test | 8/8 enriched; 6/8 sentiment-label and 5/8 issue-label matches | Validated the initial end-to-end pipeline, not general model quality. |
| 24-case resolution v2 | 20/24 exact matches (83.33%) | Exposed confusion around unresolved versus disputed resolution. |
| 200-case resolution v3 | 200/200 exact matches (100%) | Consistent behavior on an intentionally structured synthetic set. |
The 24-case experiment was more informative than the perfect-looking final percentage. Of the four mismatches, three ongoing-issue messages were classified as disputes, while a future-oriented request was interpreted as unresolved. That pointed to a semantic boundary that needed more explicit treatment: a failed fix is not always a challenge to a claimed resolution.
After revising the label instructions, I ran a 200-case synthetic demonstration. The dataset was balanced across four reference categories: 50 examples per category. Every model prediction matched its constructed reference label.
But the important caveat is that the 200 examples were generated from 20 sentence templates across 10 product/feature contexts. They are not 200 independent, naturally occurring support conversations; labels were authored as part of dataset creation. Both the instructions and datasets changed between v2 and v3, so the difference in observed agreement cannot be attributed solely to better instructions.
That means 200/200 is not evidence of 100% real-world accuracy. It is a controlled implementation and taxonomy demonstration. A rigorous next benchmark would require diverse, independently annotated, multi-turn messages, including ambiguous closures, partial fixes, indirect complaints and contrastive examples that differ by only one important clause.
The project preserves the full observed output for inspection: 200-row CSV and evaluation notes.
How to reproduce the implementation
With compatible Snowflake Cortex access and a suitable warehouse:
- Review the repository's setup instructions, role privileges, and potential Cortex inference charges.
- Create the expected
CORTEX_CI_DEMOdatabase,APPschema, andCORTEX_CI_WHwarehouse, or adjust those names in the script. - Run the complete
09_resolution_v3_200_benchmark.sqlin Snowsight. It seeds synthetic examples, performs bounded classification, stores outputs, and createsV_RESOLUTION_V3_COMPARISON. - Create a Streamlit in Snowflake application using
streamlit_resolution_v3.pyand grant the app role read access to the view. - Inspect class counts, the confusion matrix, and individual messages. Export results or read the checked-in CSV to audit the outputs offline.
If you don't have a Snowflake account, you can still inspect the CSV, architecture, code and screenshots on GitHub. The repository also documents the security and responsible-use considerations required before exposing customer data to a model-driven process.
What I would change for a production system
A working prototype is not a finished support decision engine. Before using this architecture on real conversations, I would add transcript authorization and PII protection, role-based access, cost controls, retention limits, drift monitoring, independently labeled evaluation sets, and human review for consequential actions. I'd also test longer multi-turn dialogue histories and conflicts between an agent's claimed resolution and a customer's later observations.
More fundamentally, I'd resist turning unknown into a forced prediction. If a customer asks for documentation or says they will test later, there may simply be no evidence that a fix succeeded or failed. Keeping that uncertainty visible is an operational feature rather than a defect.
The takeaway
Sentiment tells us something about tone. Resolution evidence tells us something about whether the customer actually got an outcome. Those signals overlap, but they are not interchangeable.
This project shows one way to make that distinction concrete with Snowflake-native SQL, Cortex classification, persisted model responses, and a read-only Streamlit dashboard. The most useful next step isn't a bigger template dataset; it's a harder and more independent evaluation of the boundary between ongoing issues, challenged resolutions and insufficient evidence.
If you're building customer-service analytics with Snowflake Cortex, I'd welcome feedback on the taxonomy, evaluation design, and edge cases worth testing next.
Explore the open-source implementation: GitHub repository
Persistent archive: 10.5281/zenodo.24098765
Implementation presentation: 90-second video
This is an independent educational project using synthetic conversations. No employer or real-customer transcripts were used. Reported runs were observed by the project author and have not been independently replicated in a second Snowflake environment.


Top comments (0)