Build an AI Ticket Triage Assistant
Classify support tickets with validated Gemini AI output in Python.
Introduction
30 Second Summary
Support teams receive customer messages that range from detailed reports to a few unclear words. Turning that mix into consistent decisions gets harder as the queue grows.
In this project, you will build a command-line assistant that classifies synthetic support tickets with the Gemini Developer API. Its free-form first draft evolves into a structured output contract built with JSON Schema and validated by Pydantic.
What You'll Build
Your terminal will turn a synthetic customer message into a triage record with a category, priority, summary, and recommended action.
By the end of this project, you'll have:
- Predictable triage output that turns each synthetic ticket into four labeled fields.
- A reproducible parsing failure that shows why readable model prose can trigger a JSONDecodeError in downstream code.
- An evaluation run that checks three labeled examples and marks each result as PASS or REVIEW.
- Secret Mission: Add a human-review guardrail that catches short or ambiguous support tickets.
Are there any prerequisites?
You need a Windows computer with Python 3.10 or newer, Visual Studio Code, and a Google account that can access Google AI Studio.
Use only synthetic tickets because free-tier content may be used to improve Google's products.
Before We Start
First, lock in the purpose behind this project before you start building. You are creating a reliable support-ticket triage workflow that gives support agents consistent category, priority, summary, and recommended-action fields.
Set Up Gemini and Verify Your Connection
Your support-ticket workflow depends on the Gemini Developer API. An unverified connection would make every later result uncertain. Your Python packages also need fixed versions so the project behaves consistently.
This step creates a virtual environment in Visual Studio Code for those packages. A small smoke test then proves your API authentication works before you build the triage logic.
In this step, get ready to:
- Prepare an isolated Python project.
- Install the pinned Python dependencies.
- Prove the Gemini connection with a smoke test.
Prepare an isolated Python project
Both pinned packages require Python 3.10 or newer. You will confirm that requirement before creating the project environment.
- Check your installed Python version by running this command in PowerShell:
python --version
What does this command check?
- The python --version command prints the Python version selected by your terminal.
- The Google Gen AI SDK and Pydantic both require Python 3.10 or newer.
- Choose the tab that matches your version output.
✔️ I see version 3.10 or higher
Good start. Your Python version supports both pinned packages, so the project can use the documented SDK features.
ⓧ I see an older version
Your current Python version is below the package requirement. Install Python 3.10 or newer before creating the environment.
- Download a supported release from the official Python Windows downloads page.
- Install the downloaded Python release.
- Close Visual Studio Code after the installation completes.
- Use Windows search to reopen Visual Studio Code.
- Check the newly installed version by running:
python --version
What does this recheck prove?
The command confirms which Python installation your new terminal selects. Continue when the result shows version 3.10 or higher.
ⓧ Command not found
PowerShell cannot currently find a Python installation. Install Python before continuing with the project.
- Download Python 3.10 or newer from the official Python Windows downloads page.
- Install the downloaded Python release.
- Close Visual Studio Code after the installation completes.
- Use Windows search to reopen Visual Studio Code.
- Confirm PowerShell can now find Python by running:
python --version
What does this recheck prove?
A version number confirms that PowerShell can now find Python. Continue when the result shows version 3.10 or higher.
Your Python version is ready. The next move gives every project file a predictable home on your Desktop.
- Press the Windows key to open Windows search.
- Type Visual Studio Code.
- Press Enter to open Visual Studio Code.
- Click File in the top menu bar.
- Select Open Folder.
- Navigate to your Desktop in the folder picker.
The folder picker now shows the location where your project will live.
- Click New Folder.
- Enter ai-ticket-triage as the folder name.
- Select the ai-ticket-triage folder.
- Click Select Folder to open it in Visual Studio Code.
Asked whether you trust the folder?
Visual Studio Code can ask whether you trust a newly opened workspace. Approve the workspace because you created this empty folder yourself.
A project-specific environment keeps these packages separate from other Python projects on your computer. The .venv folder holds that isolated environment.
- Click Terminal in the Visual Studio Code menu bar.
- Select New Terminal.
- Select PowerShell from the terminal profile menu if another shell opens.
You should see a PowerShell prompt inside the ai-ticket-triage folder.
- Create the isolated environment by running:
python -m venv .venv
What does this command do?
- The venv module creates an isolated Python environment.
- The .venv argument gives the environment its folder name.
You should now see a .venv folder in the Visual Studio Code Explorer.
- Activate the new environment by running:
.\.venv\Scripts\Activate.ps1
What does activation change?
Activation points Python package commands at the project environment. This keeps the pinned dependencies inside .venv.
You should see (.venv) at the start of the PowerShell prompt. That prefix confirms the isolated environment is active.
PowerShell blocked the activation script?
PowerShell can block local activation scripts under its current execution policy. The Python documentation provides a current-user policy change for this situation.
- Allow locally created scripts for your Windows user by running:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
This policy change applies only to your Windows user account.
- Close the current Visual Studio Code terminal after the command completes.
- Open a new PowerShell terminal from the Terminal menu.
- Run the activation command above again.
I can help you diagnose the policy or activation issue without asking for credentials. The NextWork community can also help with the PowerShell output.
- Click the Python version shown in the Visual Studio Code Status Bar.
- Select the interpreter inside .venv.
Your project now uses its own Python interpreter. New terminals can target the same isolated environment.
Install the pinned dependencies
A requirements file records the exact package versions behind this project. You will pin the Google Gen AI SDK and Pydantic so the API calls and validation code stay reproducible.
- Hover over the AI-TICKET-TRIAGE heading in the Visual Studio Code Explorer.
- Click the New File icon.
- Enter requirements.txt as the file name.
- Add the pinned dependencies by pasting this file content:
google-genai==2.33.0
pydantic==2.14.1
What do these pins control?
- The google-genai==2.33.0 line selects the documented Google Gen AI SDK release.
- The pydantic==2.14.1 line selects the Pydantic release that will validate model responses later.
- Save requirements.txt by pressing Ctrl+S.
- Install the recorded dependencies into .venv by running:
python -m pip install -r requirements.txt
What does this install command do?
- The pip module reads each dependency from requirements.txt.
- The active .venv receives the downloaded packages.
The terminal should finish without a dependency error. That is the dependency layer sorted, with both packages installed at the versions this project expects.
✔️ Awesome, I've got everything!
Your requirements file is saved. Both pinned packages are installed in the active project environment.
ⓧ I'd like to double check the full code
Compare your saved requirements.txt with this complete file:
google-genai==2.33.0
pydantic==2.14.1
What should match?
The file contains exactly two dependency lines. Each package uses the pinned version shown here.
Did the dependency installation fail?
- Confirm the PowerShell prompt starts with (.venv).
- Confirm requirements.txt is saved inside the ai-ticket-triage folder.
- Compare both dependency lines with the full-file tab above.
Help me diagnose my dependency installation output. The NextWork community can also help if you share the error without sharing your API key.
Authenticate a Gemini smoke test
An API key proves that your script can use the Gemini Developer API. Credential setup can feel delicate, so this path stores the key as a Windows user variable without placing it in a project file.
Keep this test synthetic
The gemini-3.8-flash model has free input and output on the Free Tier. Free-tier content may be used to improve Google's products, so this project uses only synthetic support tickets.
- Open the Google AI Studio API key page in your browser.
- Sign in with the Google account you use for this project.
- Create a Gemini API key if no key is available.
- Copy the API key to your clipboard.
- Press the Windows key to open Windows search.
- Type Environment Variables.
- Select the result for editing your user environment variables.
The Windows environment settings now show the variables attached to your user account.
- Click New under the user variables section.
- Enter GEMINI_API_KEY in the Variable name field.
- Paste the copied key into the Variable value field.
- Save the new user variable.
- Close the environment settings window.
Your key now lives outside the project files. Keep it private because it authenticates API requests associated with your account.
- Close the current terminal with the trash icon in the Visual Studio Code terminal panel.
- Click Terminal in the Visual Studio Code menu bar.
- Select New Terminal to load the new user variable.
- Activate the project environment in the new PowerShell terminal by running:
.\.venv\Scripts\Activate.ps1
Why open a new terminal?
A new terminal loads the user environment variable you just saved. Activating .venv also restores access to the pinned project packages.
- Hover over the AI-TICKET-TRIAGE heading in the Visual Studio Code Explorer.
- Click the New File icon.
- Enter smoke_test.py as the file name.
- Create the connection test by pasting this code:
from google import genai
with genai.Client() as client:
interaction = client.interactions.create(
model="gemini-3.8-flash",
input="Explain how AI works in a few words",
)
print(interaction.output_text)
What does this smoke test do?
- The genai import gives the script access to the installed SDK.
- The genai.Client() context reads the configured credentials and closes its connection resources afterward.
- The client.interactions.create() call sends a short prompt to gemini-3.8-flash.
- The interaction.output_text value contains the model's response for printing.
- Save smoke_test.py by pressing Ctrl+S.
✔️ Awesome, I've got everything!
Your smoke test is saved. It is ready to authenticate through the user environment variable.
ⓧ I'd like to double check the full code
Compare your saved smoke_test.py with this complete file:
from google import genai
with genai.Client() as client:
interaction = client.interactions.create(
model="gemini-3.8-flash",
input="Explain how AI works in a few words",
)
print(interaction.output_text)
What should match?
The import, model ID, input text, indentation, and final print statement should match this reference exactly.
Before you run the smoke test, do you think the client can authenticate with the new user variable? Hold that prediction.
- Run the saved smoke test in the active PowerShell terminal:
python smoke_test.py
What does this command prove?
The command starts your saved Python script inside the active environment. A model response proves that the SDK found your API key and reached the Gemini Developer API.
You should see a short explanation of AI printed without an authentication error. Connection confirmed. Your project can now send prompts to Gemini from Python.
No model response?
- Confirm you opened a new terminal after saving the user environment variable.
- Confirm the environment variable name is exactly GEMINI_API_KEY.
- Confirm the new terminal prompt starts with (.venv).
Help me diagnose my Gemini smoke test without asking me to reveal the API key. The NextWork community can also help if you redact every credential before sharing terminal output.
Your environment is isolated, your dependencies are pinned, and Gemini is answering from Python. Next up, you will turn this working connection into a free-form support-ticket triage prototype.
Build a Free-Form Triage Prototype
Your smoke test proved that the Gemini Developer API can answer from this project. Now you can turn that connection into a support workflow.
A prototype built around free-form output gives you a useful triage note quickly. Its flexible format exposes the limitation that later automation must handle.
In this step, get ready to:
- Create a ticket assistant that requests a human-readable triage note.
- Run the prototype with a synthetic duplicate-charge ticket.
- Inspect which parts of the response remain under the model's control.
Create the free-form assistant
A prompt tells the model how to interpret the ticket. This first prompt requests concise prose with the four details a support agent needs.
- Select the New File icon beside the AI-TICKET-TRIAGE heading in the VS Code Explorer sidebar.
- Type ticket_triage.py into the file name field.
- Press Enter.
- Build the free-form assistant by pasting this code into ticket_triage.py:
from google import genai
MODEL_ID = "gemini-3.8-flash"
def main() -> None:
ticket = input("> ").strip()
with genai.Client() as client:
prompt = f"""Classify this synthetic support ticket for an internal help desk.
Return a concise human-readable triage note with a category, priority, summary, and recommended action.
Do not return JSON.
Support ticket:
{ticket}
"""
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
)
print(interaction.output_text)
if __name__ == "__main__":
main()
What does this code do?
- The MODEL_ID constant keeps the model choice in one place.
- The main() function captures the ticket typed into the terminal.
- The prompt requests a category, priority, summary, and recommended action. It also asks for prose instead of JSON.
- The client.interactions.create() call sends the prompt to the configured model. The returned interaction is stored in interaction.
- The interaction.output_text value contains the human-readable response that your script prints.
- Save ticket_triage.py by pressing Ctrl+S.
- Confirm that ticket_triage.py appears under AI-TICKET-TRIAGE in the Explorer sidebar.
Good progress. Your new script now contains the complete free-form triage workflow.
Cannot Find the New File?
- Check that the file is named exactly ticket_triage.py.
- Confirm that the file appears inside the ai-ticket-triage folder.
- Save the file again if its editor tab shows an unsaved-change dot.
Help me find or save ticket_triage.py in my VS Code project.
✔️ Awesome, I've got everything!
Your file is saved. The prototype is ready for its first ticket.
ⓧ I'd like to double check the full code
The complete ticket_triage.py file should match this reference.
from google import genai
MODEL_ID = "gemini-3.8-flash"
def main() -> None:
ticket = input("> ").strip()
with genai.Client() as client:
prompt = f"""Classify this synthetic support ticket for an internal help desk.
Return a concise human-readable triage note with a category, priority, summary, and recommended action.
Do not return JSON.
Support ticket:
{ticket}
"""
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
)
print(interaction.output_text)
if __name__ == "__main__":
main()
Run and inspect the triage note
A useful prototype must turn a real ticket into an understandable result. This run shows how well flexible prose serves a human reader.
Before you run it, do you expect the response to follow one fixed layout or use flexible prose?
- Switch back to the VS Code PowerShell terminal from earlier.
- Start the ticket assistant by running this command:
python ticket_triage.py
- Paste I was charged twice for my subscription and need the duplicate payment reversed into the terminal prompt.
- Press Enter.
You'll see a readable triage note containing a category, priority, summary, and recommended action.
You have a working prototype. A messy customer message now becomes a support note that a person can act on.
- Check the response for all four requested details.
- Notice how the model chooses the headings, bullets, punctuation, and wording.
The response is useful to a person. Its presentation still belongs to the model each time the script runs.
Script Not Producing a Triage Note?
- Confirm that the current terminal still uses the .venv environment.
- Confirm that the Windows user environment still contains GEMINI_API_KEY.
- Create a fresh VS Code terminal if the script reports an authentication problem.
Help me troubleshoot my ticket triage script.
Your prototype can turn a messy ticket into a useful note. Next, you will test whether that prose can safely feed downstream code.
Expose the Free-Form Parsing Failure
Your prototype turns a synthetic ticket into a useful note. That result proves Gemini can understand the triage task.
Downstream automation needs machine-readable fields. You will pass the decorated response to Python's JSON parser to test whether free-form prose can serve as an application contract.
In this step, get ready to:
- Add Python's JSON parser to the triage script.
- Pass a decorated model response into the parser.
- Run the duplicate-charge ticket to inspect the planned failure.
Add the downstream JSON consumer
Python's built-in json module converts valid JSON documents into Python data. Your script needs access to that parser before it can test the model response.
- In ticket_triage.py, find the existing import at the top of the file:
from google import genai
What Are You Looking At?
This is the current import section. It gives the script access to the Gemini client.
- Add the json import above the existing line so the import section looks like this:
import json
from google import genai
What Does This Import Do?
The json module provides json.loads(). That function attempts to deserialize a string containing a valid JSON document.
- Save ticket_triage.py by pressing Ctrl+S.
- Return to the active PowerShell terminal and run the script with this command:
python ticket_triage.py
What Does This Command Do?
This command starts the updated script inside your active virtual environment. The script pauses at its ticket prompt.
- Paste this synthetic ticket into the terminal prompt:
I was charged twice for my subscription and need the duplicate payment reversed
Why Run It Now?
This check confirms the new import leaves the existing request path intact. You should still see a readable triage note.
Good. Your script now has the JSON parser available without changing its human-readable output.
Script Stops Before the Ticket Prompt?
- Check that import json starts at the far left of the first line.
- Confirm that from google import genai remains below the blank line.
- Save ticket_triage.py before running the command again.
Help me fix the import section in ticket_triage.py.
Trigger the planned parsing failure
The readable note still belongs to the model. A downstream parser now gives you a concrete way to test whether that prose forms a dependable data boundary.
- In ticket_triage.py, locate this line below the Gemini request:
print(interaction.output_text)
What Does This Line Do?
This line sends the model's free-form response directly to the terminal. No downstream code tries to interpret its structure.
- Replace that line with these three lines:
decorated_result = f"Triage result:\n{interaction.output_text}"
print(decorated_result)
json.loads(decorated_result)
What Does This Code Do?
- decorated_result adds the fixed Triage result: prefix above the model response.
- print(decorated_result) keeps the readable note visible in the terminal.
- json.loads(decorated_result) asks Python to deserialize the entire decorated string.
- Save ticket_triage.py by pressing Ctrl+S.
✔️ Awesome, I've got everything!
Your downstream parser test is ready to run.
ⓧ I'd like to double check the full code
- Compare your ticket_triage.py file with this complete reference:
import json
from google import genai
MODEL_ID = "gemini-3.8-flash"
def main() -> None:
ticket = input("> ").strip()
with genai.Client() as client:
prompt = f"""Classify this synthetic support ticket for an internal help desk.
Return a concise human-readable triage note with a category, priority, summary, and recommended action.
Do not return JSON.
Support ticket:
{ticket}
"""
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
)
decorated_result = f"Triage result:\n{interaction.output_text}"
print(decorated_result)
json.loads(decorated_result)
if __name__ == "__main__":
main()
How Should the File Compare?
The file retains the free-form prompt and Gemini request. The new import plus the three parser-test lines are the only changes in this step.
Before you run this, do you think the decorated string forms a valid JSON document?
- Run the parser test in the active PowerShell terminal with this command:
python ticket_triage.py
What Does This Run Test?
The command starts the same free-form triage flow. This version continues into the parser after printing the response.
- Paste the same synthetic ticket into the terminal prompt:
I was charged twice for my subscription and need the duplicate payment reversed
What Should You See?
You should see Triage result: followed by the readable triage note. The traceback ends with json.decoder.JSONDecodeError.
This Failure Is Intentional
Python's parser expects the entire string to be a JSON document. The fixed prefix makes the first characters plain prose. Parsing therefore fails every time.
The note is useful presentation for a person. Downstream code needs a defined application contract.
- Read the final traceback line to locate json.decoder.JSONDecodeError.
- Trace upward to the line containing json.loads(decorated_result).
That planned failure gives you clear evidence that readable prose cannot safely drive automation. Next, you will replace this fragile boundary with validated structured output.
Enforce Structured Ticket Output
Your free-form prototype proved that Gemini can understand a support ticket. The fixed prefix also proved that decorated prose breaks when application code tries to parse it.
A JSON Schema constrains the response shape. Pydantic turns that shape into validated Python data before your program uses it.
In this step, get ready to:
- Define the four ticket fields in TicketAnalysis.
- Constrain Gemini output with a generated JSON Schema.
- Validate each model response before displaying it.
Define the ticket contract
An application contract states which fields downstream code can expect. It also limits category and priority to supported values.
- Leave import json in place for the moment.
- In ticket_triage.py, replace the existing from google import genai line with this import group:
from typing import Literal
from google import genai
from pydantic import BaseModel, Field
What do these imports provide?
- Literal restricts a field to a fixed set of string values.
- BaseModel provides the foundation for a typed Pydantic model.
- Field adds descriptions that clarify each field's purpose.
- Save ticket_triage.py by pressing Ctrl+S.
The new imports should appear without warning underlines. This confirms that the active .venv environment can find Pydantic.
Seeing an unresolved Pydantic import?
- Confirm that the Python version control in the VS Code Status Bar shows your .venv interpreter.
- Confirm that your PowerShell prompt still shows the active virtual environment.
Help me fix an unresolved Pydantic import in my active virtual environment.
- Add the ticket contract directly below MODEL_ID = "gemini-3.8-flash" by pasting this class:
class TicketAnalysis(BaseModel):
category: Literal[
"billing", "access", "bug", "feature_request", "other"
] = Field(description="The single best category for the support ticket.")
priority: Literal["low", "medium", "high", "urgent"] = Field(
description="The response priority based only on the ticket's impact and urgency."
)
summary: str = Field(description="A concise one-sentence summary of the problem.")
recommended_action: str = Field(
description="The next practical action a support agent should take."
)
What does this contract enforce?
- TicketAnalysis defines one validated record for each triage decision.
- category accepts five supported classification labels.
- priority accepts four supported urgency labels.
- summary and recommended_action must both contain strings.
- Save ticket_triage.py.
- Check that Python accepts the new contract by running this command:
python ticket_triage.py
Why run the old path now?
Python reads the new imports and class before it reaches main(). Reaching the ticket prompt confirms that the contract has valid syntax.
- Enter I was charged twice for my subscription and need the duplicate payment reversed at the prompt.
You will still see the readable triage note followed by json.decoder.JSONDecodeError. The existing main() function still sends free-form prose to json.loads().
Does the script fail before the ticket prompt?
- Check the indentation inside TicketAnalysis.
- Check every opening quote in the field descriptions for a matching closing quote.
Help me find the syntax error in my TicketAnalysis model.
Request schema-constrained output
The Gemini Developer API can constrain a final response with a schema. The prompt describes the triage task while TicketAnalysis defines the response structure.
- In ticket_triage.py, add triage_ticket() between TicketAnalysis and the existing main() function:
def triage_ticket(client, ticket: str) -> TicketAnalysis:
prompt = f"""Classify this synthetic support ticket for an internal help desk.
Base the decision only on the ticket text.
Support ticket:
{ticket}
"""
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
response_format={
"type": "text",
"mime_type": "application/json",
"schema": TicketAnalysis.model_json_schema(),
},
)
return TicketAnalysis.model_validate_json(interaction.output_text)
How does structured output work?
- prompt tells the model to classify one synthetic support ticket.
- mime_type requests an application/json response.
- TicketAnalysis.model_json_schema() generates the schema from the Pydantic model.
- TicketAnalysis.model_validate_json() validates the returned JSON before the function returns it.
- Save ticket_triage.py.
The editor should show triage_ticket() as one complete function without warning underlines. Its return type is now TicketAnalysis.
Seeing an indentation warning in triage_ticket?
- Align prompt, interaction, and the return statement one indentation level inside the function.
- Align the three response_format entries one indentation level inside their dictionary.
Help me correct the structure of my triage_ticket function.
- Add print_analysis() directly below triage_ticket() by pasting this function:
def print_analysis(analysis: TicketAnalysis) -> None:
print(f"Category: {analysis.category}")
print(f"Priority: {analysis.priority}")
print(f"Summary: {analysis.summary}")
print(f"Recommended action: {analysis.recommended_action}")
What does this function display?
print_analysis() reads attributes from a validated TicketAnalysis object. Each attribute receives a stable label for terminal output.
- Save ticket_triage.py.
You should now see print_analysis() directly above the old main() function. Its four print statements should align at the same indentation level.
Are the field names underlined?
- Compare each attribute name with its matching field in TicketAnalysis.
- Check that recommended_action uses an underscore in both locations.
Help me fix mismatched TicketAnalysis attribute names in print_analysis.
Validate and display the result
The structured request is ready. Your program now needs to route the entered ticket through triage_ticket() before displaying its validated fields.
- Replace the current main() function with this version:
def main() -> None:
with genai.Client() as client:
ticket = input("> ").strip()
if not ticket:
print("No ticket entered.")
return
analysis = triage_ticket(client, ticket)
print_analysis(analysis)
How does the new main function work?
- genai.Client() creates a client from the configured environment.
- input("> ").strip() collects the synthetic ticket and removes surrounding whitespace.
- triage_ticket() returns a validated TicketAnalysis object.
- print_analysis() displays the validated fields.
- Save ticket_triage.py.
Before you run the updated script, do you expect the fixed prefix or the four validated labels to appear?
- Start the structured assistant by running this command:
python ticket_triage.py
What does this run prove?
This command starts the updated ticket_triage.py file inside the active environment. The next response tests schema generation and typed validation together.
- Enter I was charged twice for my subscription and need the duplicate payment reversed at the prompt.
You should see four lines beginning with Category:, Priority:, Summary:, and Recommended action:. No parsing traceback should follow.
Still seeing the decorated triage result?
- Confirm that you replaced the old main() function.
- Confirm that the file tab no longer shows unsaved changes.
- Confirm that main() calls triage_ticket().
Help me find why ticket_triage.py still runs the old free-form parser.
The successful run confirms that the old parser is no longer used. The remaining json import can now be removed.
- Delete import json from the top of ticket_triage.py.
- Save ticket_triage.py.
Before the final run, what should replace the previous json.decoder.JSONDecodeError?
- Run the finished assistant with this command:
python ticket_triage.py
What does this final run prove?
This run checks the exact finished file after the obsolete import is removed. A successful response confirms that generation and validation still work together.
- Enter I was charged twice for my subscription and need the duplicate payment reversed at the prompt.
You should see the four labeled fields again. The script should finish without json.decoder.JSONDecodeError.
You have closed the fragile parsing gap. Downstream code now receives four validated fields from every successful response.
✔️ Awesome, I've got everything!
Your structured assistant is saved. The duplicate-charge ticket now produces four validated fields without a parsing traceback.
ⓧ I'd like to double check the full code
- Compare your complete ticket_triage.py file with this reference:
from typing import Literal
from google import genai
from pydantic import BaseModel, Field
MODEL_ID = "gemini-3.8-flash"
class TicketAnalysis(BaseModel):
category: Literal[
"billing", "access", "bug", "feature_request", "other"
] = Field(description="The single best category for the support ticket.")
priority: Literal["low", "medium", "high", "urgent"] = Field(
description="The response priority based only on the ticket's impact and urgency."
)
summary: str = Field(description="A concise one-sentence summary of the problem.")
recommended_action: str = Field(
description="The next practical action a support agent should take."
)
def triage_ticket(client, ticket: str) -> TicketAnalysis:
prompt = f"""Classify this synthetic support ticket for an internal help desk.
Base the decision only on the ticket text.
Support ticket:
{ticket}
"""
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
response_format={
"type": "text",
"mime_type": "application/json",
"schema": TicketAnalysis.model_json_schema(),
},
)
return TicketAnalysis.model_validate_json(interaction.output_text)
def print_analysis(analysis: TicketAnalysis) -> None:
print(f"Category: {analysis.category}")
print(f"Priority: {analysis.priority}")
print(f"Summary: {analysis.summary}")
print(f"Recommended action: {analysis.recommended_action}")
def main() -> None:
with genai.Client() as client:
ticket = input("> ").strip()
if not ticket:
print("No ticket entered.")
return
analysis = triage_ticket(client, ticket)
print_analysis(analysis)
if __name__ == "__main__":
main()
How to compare this file
This reference shows the exact finished order of the imports, model, helper functions, and main() function. Your saved file should match it exactly.
Seeing a schema or validation error?
- Confirm that response_format contains type, mime_type, and schema.
- Confirm that the schema value calls TicketAnalysis.model_json_schema().
- Confirm that the return statement calls TicketAnalysis.model_validate_json(interaction.output_text).
Help me troubleshoot my structured Gemini response and Pydantic validation.
Your assistant now produces validated ticket records that application code can trust. Next, you will test that reliability across several labeled support tickets.
Evaluate Your Triage Assistant
Your structured assistant now turns one synthetic ticket into four validated fields. One convincing result still leaves its reliability untested.
A small set of evaluation cases gives every run visible checks against known categories. You will also preserve the interactive path for testing your own synthetic ticket.
In This Step, Get Ready To:
- Add three synthetic tickets with expected categories.
- Compare each model classification with its expected category.
- Run the checks before an interactive ticket demonstration.
Add Labeled Evaluation Cases
A labeled case pairs a known input with the category you expect. These pairs turn model behavior into something your program can check.
- Return to ticket_triage.py in VS Code.
- Place your cursor on the blank line immediately above def triage_ticket(client, ticket: str) -> TicketAnalysis:.
- Add the three labeled cases by pasting this code:
TEST_CASES = [
{
"ticket": "I was charged twice for my subscription this month.",
"expected_category": "billing",
},
{
"ticket": "My password reset link expired and I cannot access my account.",
"expected_category": "access",
},
{
"ticket": "The desktop app crashes every time I export a report.",
"expected_category": "bug",
},
]
What do these cases represent?
- Each ticket value contains a synthetic customer request that is safe to send through the model.
- Each expected_category value records the category that a useful classification should return.
- The three cases cover billing problems, account access problems, and software bugs.
- Save ticket_triage.py.
- Return to the VS Code PowerShell terminal.
- Confirm the script still accepts an interactive ticket by running this command:
python ticket_triage.py
What does this check prove?
Python loads the new test data before starting the existing interactive flow. A successful run confirms that the list has valid syntax.
- Type My password reset link expired and I cannot access my account. at the > prompt.
- Press Enter.
You should see category, priority, summary, and recommended action fields. The category should describe an account access problem.
Seeing a Syntax Error After Adding the Cases?
- Check that TEST_CASES sits outside the TicketAnalysis class.
- Confirm that every opening brace has a matching closing brace.
- Confirm that the list ends with a closing bracket.
Ask for help with my test case list.
Compare Expected and Actual Categories
The evaluator sends each labeled ticket through the same validated triage path as an interactive ticket. It marks an exact category match as PASS and sends every mismatch to REVIEW.
- Place your cursor on the blank line immediately above def main() -> None:.
- Add the evaluation function by pasting this code:
def run_evaluation(client) -> None:
print("Evaluation")
print("=" * 40)
for index, case in enumerate(TEST_CASES, start=1):
analysis = triage_ticket(client, case["ticket"])
status = (
"PASS"
if analysis.category == case["expected_category"]
else "REVIEW"
)
print(
f"{index}. {status} | expected={case['expected_category']} "
f"actual={analysis.category}"
)
How Does the Evaluator Work?
- The loop passes each synthetic ticket to triage_ticket() so every response still goes through schema validation.
- The status comparison checks the validated category against the labeled category.
- A REVIEW result is useful evidence that the case needs investigation. It does not invalidate the project.
- The final output displays both categories so you can see the exact reason for each status.
- Save ticket_triage.py.
- Return to the VS Code PowerShell terminal.
- Confirm Python can load the new function by running the current script:
python ticket_triage.py
What Does This Run Confirm?
The current interactive path starts only after Python has parsed the new evaluator. Reaching the ticket prompt confirms that the function definition has valid syntax.
- Type The desktop app crashes every time I export a report. at the > prompt.
- Press Enter.
You should see four validated fields for the crashing export ticket. The category should describe a software bug.
Not Reaching the Ticket Prompt?
- Align def run_evaluation(client) -> None: with the other function definitions at the left edge of the file.
- Keep every line inside run_evaluation() indented by four spaces.
- Check that both parts of the final formatted output remain inside the same print() call.
Help me debug my evaluation function.
Preserve the Interactive Demo
The finished program needs two views of reliability. The labeled cases show repeatable checks while the interactive path shows how the assistant handles a new synthetic request.
- Select the existing main() function in ticket_triage.py.
- Replace the selected function with this updated version:
def main() -> None:
with genai.Client() as client:
run_evaluation(client)
print("\nTry your own synthetic ticket")
print("=" * 40)
ticket = input("> ").strip()
if not ticket:
print("No ticket entered.")
return
analysis = triage_ticket(client, ticket)
print()
print_analysis(analysis)
What Does the Updated Main Flow Do?
- The shared client first runs all three labeled cases through run_evaluation().
- The interactive prompt then collects one custom synthetic ticket.
- The custom result passes through triage_ticket() before print_analysis() displays its validated fields.
- The empty-input guard still ends the program cleanly when no ticket is entered.
- Save ticket_triage.py.
✔️ Awesome, I've Got Everything
Your evaluation loop and interactive demonstration are connected in one saved script.
ⓧ I'd Like to Double Check the Full Code
The complete cumulative ticket_triage.py file should match this reference.
from typing import Literal
from google import genai
from pydantic import BaseModel, Field
MODEL_ID = "gemini-3.8-flash"
class TicketAnalysis(BaseModel):
category: Literal[
"billing", "access", "bug", "feature_request", "other"
] = Field(description="The single best category for the support ticket.")
priority: Literal["low", "medium", "high", "urgent"] = Field(
description="The response priority based only on the ticket's impact and urgency."
)
summary: str = Field(description="A concise one-sentence summary of the problem.")
recommended_action: str = Field(
description="The next practical action a support agent should take."
)
TEST_CASES = [
{
"ticket": "I was charged twice for my subscription this month.",
"expected_category": "billing",
},
{
"ticket": "My password reset link expired and I cannot access my account.",
"expected_category": "access",
},
{
"ticket": "The desktop app crashes every time I export a report.",
"expected_category": "bug",
},
]
def triage_ticket(client, ticket: str) -> TicketAnalysis:
prompt = f"""Classify this synthetic support ticket for an internal help desk.
Base the decision only on the ticket text.
Support ticket:
{ticket}
"""
interaction = client.interactions.create(
model=MODEL_ID,
input=prompt,
response_format={
"type": "text",
"mime_type": "application/json",
"schema": TicketAnalysis.model_json_schema(),
},
)
return TicketAnalysis.model_validate_json(interaction.output_text)
def print_analysis(analysis: TicketAnalysis) -> None:
print(f"Category: {analysis.category}")
print(f"Priority: {analysis.priority}")
print(f"Summary: {analysis.summary}")
print(f"Recommended action: {analysis.recommended_action}")
def run_evaluation(client) -> None:
print("Evaluation")
print("=" * 40)
for index, case in enumerate(TEST_CASES, start=1):
analysis = triage_ticket(client, case["ticket"])
status = (
"PASS"
if analysis.category == case["expected_category"]
else "REVIEW"
)
print(
f"{index}. {status} | expected={case['expected_category']} "
f"actual={analysis.category}"
)
def main() -> None:
with genai.Client() as client:
run_evaluation(client)
print("\nTry your own synthetic ticket")
print("=" * 40)
ticket = input("> ").strip()
if not ticket:
print("No ticket entered.")
return
analysis = triage_ticket(client, ticket)
print()
print_analysis(analysis)
if __name__ == "__main__":
main()
How to Use This Reference
This file shows the final order of the schema, labeled cases, helper functions, evaluation loop, and interactive entry point.
Before you run the finished assistant, do you expect every labeled case to receive the same status?
- Run the finished evaluation and interactive demonstration with this command:
python ticket_triage.py
What Should You See?
You should see three numbered evaluation lines. Each line contains PASS or REVIEW beside the expected and actual categories.
The script then displays Try your own synthetic ticket followed by the > prompt. A REVIEW result identifies a case worth inspecting.
- Type one custom synthetic support request at the > prompt.
- Press Enter.
You should see a complete validated analysis with category, priority, summary, and recommended action fields.
Evaluation Not Reaching the Interactive Prompt?
- Confirm the .venv environment is still active in the VS Code PowerShell terminal.
- Confirm the terminal can still access GEMINI_API_KEY.
- Check that run_evaluation(client) remains inside the client context in main().
Help me troubleshoot my finished evaluation run.
You now have visible evidence for known cases and a validated path for new synthetic tickets. Your assistant can show where its classifications match expectations and where a person should investigate.
Secret mission
Route Ambiguous Tickets to a Human
A schema-valid classification can still come from a ticket with too little detail. Add a model uncertainty signal plus a local word-count guardrail that routes ambiguous tickets to a person.
Clean Up Your Resources
Clean Up Your Resources
This project has no ongoing local costs because the Gemini Developer API uses the Free Tier without paid billing. The options below let you keep the setup, pause your local work, or delete everything.
Keep Tickets Synthetic
Free-tier requests may be used to improve Google's products.
- Use only synthetic support tickets if you keep the API key.
Resources you used:
- The ai-ticket-triage project folder. It contains .venv plus your project files.
- A Gemini API key managed through Google AI Studio.
- The GEMINI_API_KEY Windows user variable.
Keep everything running
No action needed. Choose this if you plan to keep testing the assistant with synthetic tickets.
- Keep the ai-ticket-triage folder in its current location.
- Keep the Gemini API key private.
- Keep the GEMINI_API_KEY user variable configured.
- Use synthetic support tickets for every future free-tier request.
The project stays idle until you run one of its Python scripts again.
Pause - I'll come back to this later
End the active terminal session to pause the project while keeping its files and API access ready.
- Close the active VS Code PowerShell terminal.
The active .venv session ends when the terminal closes. No local service remains running.
Your project folder stays in place. Your free-tier API setup remains idle until another script sends a request.
Delete - I don't want to use this again
Deletion is permanent for the local folder and API key. Any code you may want later needs a separate copy.
Delete the Gemini API key
- Return to Google AI Studio.
- Open the API Keys page.
- Select the Gemini API key associated with this project.
- Choose Delete for the selected key.
- Approve the confirmation prompt.
You'll see that the key is no longer active. Requests that relied on it can no longer authenticate.
Remove the Windows user variable
- Press the Windows key to open search.
- Search for Environment Variables.
- Open the user environment settings result.
- Select GEMINI_API_KEY from the user variables list.
- Use the delete control to remove the selected variable.
You'll no longer see GEMINI_API_KEY in the user variables list.
- Confirm the environment variable changes.
New terminal sessions can no longer read the API key from that user variable.
Delete the local project folder
- Right-click ai-ticket-triage in the VS Code Explorer sidebar.
- Select the option that reveals the folder in File Explorer.
- Close Visual Studio Code.
- Use File Explorer's Up button to view the folder that contains ai-ticket-triage.
- Select ai-ticket-triage.
- Press the Delete key.
You'll see the project folder disappear from its original location.
- Open Recycle Bin from the Windows desktop.
- Select ai-ticket-triage.
- Delete the selected folder permanently.
- Approve the confirmation prompt.
The project folder is now removed from your computer. Its .venv environment and Python files are removed with it.
Nice Work!
Nice Work!
You did it! Your Python support ticket assistant now produces validated triage records through the Gemini Developer API.
You've learned how to:
- Reproduced a deliberate JSONDecodeError to prove why free-form model output cannot safely feed downstream automation.
- Built a structured-output contract with JSON Schema generated by Pydantic. Validated every model response before reading its fields.
- Created labeled evaluation cases that compare expected categories with actual results. Each run now surfaces PASS or REVIEW.
- Completed the optional Secret Mission by combining a model review flag with a local short-ticket guardrail. Ambiguous requests now display Human review: True.
Ready to quiz yourself?