AI Support Ticket Triage
Build a FastAPI service that uses AI to triage support tickets.
Introduction
30 Second Summary
Support teams rarely receive neat requests in a predictable order. The same queue can jump from a forgotten password to a production outage within seconds.
In this project, you will build a local support ticket triage service with FastAPI that turns ticket text into a typed routing decision. You will measure a brittle keyword baseline against four labeled tickets before a structured OpenAI API workflow improves the result.
What You'll Build
Picture an interactive API page turning a messy support request into a typed routing decision you can measure against labeled test cases.
By the end of this project, you'll have:
- An interactive triage demo that turns pasted ticket text into a typed routing decision with its destination team, escalation status, next action, request ID, and latency.
- A repeatable four-ticket evaluation that shows the keyword baseline passing 1/4 cases before the AI endpoint reaches 4/4.
- A production-shaped API that returns safe upstream errors while logging request IDs and latency for successful AI calls.
- Secret Mission: Write a customer rollout and engineering handoff brief with acceptance criteria, staged adoption, fallback behavior, risks, owners, and an executable handoff checklist.
Are there any prerequisites?
The project assumes familiarity with backend frameworks and HTTP APIs. Setup includes verifying Python 3.10 or newer plus creating an OpenAI API account if needed.
Before We Start
Before the hands-on work begins, lock in the purpose of your support ticket triage service. You are building it to route tickets consistently while exposing failure modes before customer rollout.
Set Up the Python and OpenAI Workspace
Your triage service will eventually make routing decisions that a customer can measure. Those decisions need consistent package versions every time you run the service.
An isolated virtual environment keeps the project's Python packages separate from the rest of your Mac. A session-only OpenAI API key prevents the credential from entering a file that Git could track.
In this step, get ready to:
- Verify Python 3.10 or newer inside an isolated project workspace.
- Pin the five project dependencies inside requirements.txt.
- Load your API key only for the current terminal session.
Create the project workspace
The project folder gives every later command a predictable location. The virtual environment keeps its installed packages isolated.
- Press Cmd+Space to open macOS search.
- Type Terminal and press Enter to open it.
- Create the ai-support-ticket-triage folder on your Desktop by running these commands:
cd ~/Desktop
mkdir ai-support-ticket-triage
cd ai-support-ticket-triage
What do these commands do?
- The first cd moves your terminal to the Desktop.
- The mkdir command creates the project folder.
- The final cd moves your terminal into that folder.
Your terminal prompt now ends inside ai-support-ticket-triage. This gives every project file a single home.
- Check your installed Python version by running this command:
python3 --version
What does this check prove?
The project packages require Python 3.10 or newer. This check confirms your interpreter can install every pinned dependency.
✔️ I see version 3.10 or higher
Your Python version meets the project's requirement. That removes the first source of dependency problems.
ⓧ I see an older version
Apple's system Python can be older than the version this project requires. Install a current Python release before creating the virtual environment.
- Open Python's macOS installation guide.
- Follow the guide to install Python 3.10 or newer.
- Close Terminal after the installation finishes.
- Reopen Terminal through macOS search.
- Return to the version check above.
ⓧ Command not found
Your shell cannot currently find a Python installation. Install Python before continuing.
- Open Python's macOS installation guide.
- Follow the guide to install Python 3.10 or newer.
- Close Terminal after the installation finishes.
- Reopen Terminal through macOS search.
- Return to the version check above.
- Create the virtual environment inside ai-support-ticket-triage by running these commands:
python3 -m venv .venv
source .venv/bin/activate
How does the virtual environment work?
- The first command creates .venv inside the project folder.
- The second command activates that environment for the current terminal session.
- An activated environment directs python and pip to the isolated project installation.
You should see (.venv) near the start of your terminal prompt. That label confirms the isolated environment is active.
Don't see the virtual environment label?
Confirm your terminal is inside ai-support-ticket-triage. Check that the environment folder is named .venv.
Run the activation command again if you opened a new terminal session.
Help me activate the virtual environment for this Python project.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open VS Code.
- Click File in the top menu bar.
- Click Open Folder.
- Select the ai-support-ticket-triage folder on your Desktop.
- Click Open.
- Approve the workspace if VS Code asks whether you trust it.
The Explorer sidebar now shows ai-support-ticket-triage as the open folder. Your editor points at the same location as the activated terminal.
Pin and install the dependencies
A dependency pin locks a package to one tested version. This project pins FastAPI for the API layer plus the supporting command-line server and validation packages.
- Select the New File icon in the VS Code Explorer sidebar.
- Enter requirements.txt as the file name.
- Add the five dependency pins by pasting this content:
fastapi==0.143.3
fastapi-cli==0.0.32
openai==3.33.0
pydantic==2.14.1
uvicorn==0.54.0
What does each package provide?
- FastAPI provides typed routes plus interactive API documentation.
- FastAPI CLI supplies the development server command used in the next step.
- The OpenAI Python SDK connects the service to the model provider.
- Pydantic validates ticket data against typed schemas.
- Uvicorn serves the FastAPI application locally.
- Save requirements.txt.
- Switch back to the Terminal window from earlier.
- Install the pinned dependencies into the active virtual environment by running this command:
python -m pip install -r requirements.txt
What does the installation command do?
The command reads every pin in requirements.txt. It installs those exact versions inside the active .venv.
The installation output should finish without a dependency error. Your global Python installation remains unchanged.
Having trouble installing the packages?
Confirm your terminal prompt still includes (.venv). Confirm requirements.txt is saved inside ai-support-ticket-triage.
Help me diagnose the dependency installation failure.
The virtual environment contains generated files that do not belong in source control. A .gitignore file tells Git to leave those local files untracked.
- Switch back to VS Code.
- Select the New File icon in the Explorer sidebar.
- Enter .gitignore as the file name.
- Add the local file exclusions by pasting this content:
.venv/
__pycache__/
.DS_Store
What stays out of Git?
- The .venv/ entry excludes installed project packages.
- The __pycache__/ entry excludes generated Python cache files.
- The .DS_Store entry excludes macOS folder metadata.
- Save .gitignore.
- Switch back to the Terminal window from earlier.
- Inspect the installed package versions by running this command:
python -m pip list
Which versions should appear?
- Find fastapi at version 0.143.3.
- Find fastapi-cli at version 0.0.32.
- Find openai at version 3.33.0.
- Find pydantic at version 2.14.1.
- Find uvicorn at version 0.54.0.
The five pinned packages now appear in the environment. Your dependency setup is reproducible.
✔️ Awesome, I've got everything!
Both project files are saved with the required contents.
ⓧ I'd like to double check the full code
Compare each saved file with these complete references.
fastapi==0.143.3
fastapi-cli==0.0.32
openai==3.33.0
pydantic==2.14.1
uvicorn==0.54.0
.venv/
__pycache__/
.DS_Store
Load the API key and verify the workspace
The OpenAI SDK reads its credential from OPENAI_API_KEY. Exporting the key in the current terminal keeps it out of both project files.
Keep the API spend controlled
Adding credits affects billing. The included four-ticket AI evaluation is estimated to cost less than $0.05 at the verified token prices.
Keep your total OpenAI API spending within $5 while you experiment with this project.
- Open the OpenAI developer quickstart.
- Sign in to your OpenAI API account.
- Create an account if you do not have one.
- Follow the Create an API Key step in the quickstart.
- Copy the new key without sharing or displaying it.
- Add only the minimum credits required by your account.
- Switch back to the Terminal window from earlier.
- Load the key into the current session by replacing your_api_key_here in this command:
export OPENAI_API_KEY="your_api_key_here"
Why export the key this way?
The export makes OPENAI_API_KEY available to programs launched from this terminal. Closing the terminal session removes that exported value from the session.
The real key never enters requirements.txt or .gitignore. This keeps the credential outside the files Git can track.
Before you run the final check, which evidence would prove that both the dependencies and the API credential are ready?
- Verify the complete workspace by running these checks:
python -m pip list
test -n "$OPENAI_API_KEY" && echo "API key loaded"
What proves the workspace is ready?
- The first command lists the five pinned packages with their required versions.
- The second command prints API key loaded without revealing the credential.
Your terminal now shows the pinned dependency versions plus API key loaded. That's the foundation secured: the project is isolated and the credential stays outside Git.
Missing one of the final confirmations?
If a package is missing, confirm (.venv) still appears in your terminal prompt. Run the installation command again from the folder containing requirements.txt.
If the key confirmation is blank, run the export command again with your real key. Keep the key out of screenshots and project files.
Help me check my isolated environment and session-only API key.
Your workspace is reproducible and ready for local development. Next up, you will ship the first visible keyword-based triage API.
Build the Keyword Triage API
Your Python workspace is ready. You can now turn that setup into a working REST API that accepts a support ticket and returns an operational decision.
A small keyword baseline gives the customer something visible immediately. Its narrow rules also create a measurable limitation that the next step can expose.
In this step, get ready to:
- Define bounded ticket and triage contracts with Pydantic.
- Build health and keyword-based triage endpoints with FastAPI.
- Submit a password ticket through the interactive API documentation.
Define the typed API contract
A support workflow needs predictable inputs and outputs. Literal types bound each operational field to approved values while Pydantic validates the ticket text.
- Select the Explorer view in the VS Code Activity Bar.
- Click the New File... button in the Explorer view.
- Enter main.py as the file name.
- Add the imports and typed contracts by pasting this code into main.py:
from time import perf_counter
from typing import Literal
from fastapi import FastAPI
from pydantic import BaseModel, Field
MODEL = "gpt-5.4-nano-2026-03-17"
Category = Literal["account", "billing", "technical", "policy", "other"]
Priority = Literal["low", "normal", "high", "urgent"]
Team = Literal["identity", "finance", "engineering", "compliance", "general"]
class Ticket(BaseModel):
text: str = Field(min_length=10, max_length=2000)
class TriageDecision(BaseModel):
category: Category
priority: Priority
team: Team
escalate: bool
summary: str
next_action: str
class TriageResponse(TriageDecision):
request_id: str
latency_ms: float
What do these contracts do?
- The Literal types restrict categories, priorities, and teams to values that downstream systems can handle.
- The Ticket model accepts text between 10 and 2,000 characters.
- The TriageDecision model defines the operational classification returned by the baseline.
- The TriageResponse model adds tracing and timing fields to the decision.
- Save main.py.
- Confirm that main.py is listed beside requirements.txt in the Explorer sidebar.
Seeing Python warnings in the editor?
Confirm that VS Code is using the interpreter inside .venv. The imports may appear unresolved when the editor points to a different Python installation.
Check your copied type names against the code above. Help me diagnose unresolved FastAPI or Pydantic imports in VS Code.
The contracts describe the shape of the service. The next block creates the application and gives it a simple health check.
- Add the FastAPI application and health endpoint below TriageResponse in main.py by pasting this code:
app = FastAPI(title="AI Support Ticket Triage Service")
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok", "model": MODEL}
What does this code do?
- The app object represents your API and supplies its documentation title.
- The /health endpoint reports that the process is responsive.
- The health response also identifies the configured MODEL value.
- Save main.py.
- Start the development service in the activated terminal by running this command:
fastapi dev main.py
What does this command do?
The command finds the app object in main.py. It starts the local service at http://127.0.0.1:8000.
Development mode watches the file for saved changes. Press Ctrl+C when you eventually want to stop the service.
You will see the server address and the documentation address in the terminal. The process stays active while you continue editing main.py.
Development server did not start?
Confirm that the terminal prompt still shows the activated .venv environment. A terminal outside that environment may not find the installed FastAPI CLI.
Check that the application variable is spelled app in main.py. Help me troubleshoot why my FastAPI development server cannot load main.py.
Add the keyword triage logic
The baseline makes decisions through direct keyword checks. It recognizes password and refund tickets while every other ticket goes to the general queue.
- Add the naive_triage() function below the health() function in main.py by pasting this code:
def naive_triage(ticket: Ticket) -> TriageDecision:
text = ticket.text.lower()
if "password" in text:
category: Category = "account"
team: Team = "identity"
elif "refund" in text:
category = "billing"
team = "finance"
else:
category = "other"
team = "general"
priority: Priority = "urgent" if "urgent" in text else "normal"
escalate = priority == "urgent"
return TriageDecision(
category=category,
priority=priority,
team=team,
escalate=escalate,
summary="Keyword baseline decision.",
next_action=f"Route to the {team} team.",
)
How does the baseline decide?
- Lowercasing the ticket makes the keyword checks independent of capitalization.
- A password match maps the ticket to the account category and identity team.
- A refund match maps the ticket to the billing category and finance team.
- The word urgent sets urgent priority and turns escalation on.
- Save main.py.
The running development server reloads the updated module. You will see a reload message in the terminal without restarting the process.
Server stopped after the reload?
Read the final terminal lines for the file name and line number. A missing quote or mismatched indentation inside naive_triage() prevents Python from loading the module.
Compare each branch with the block above. Help me fix a syntax or indentation error in naive_triage().
The decision function still needs an API route. The route will measure execution time and return the decision through the typed response contract.
- Add the baseline endpoint below naive_triage() in main.py by pasting this code:
@app.post("/triage/baseline", response_model=TriageResponse)
def triage_baseline(ticket: Ticket) -> TriageResponse:
started = perf_counter()
decision = naive_triage(ticket)
latency_ms = round((perf_counter() - started) * 1000, 2)
return TriageResponse(
**decision.model_dump(),
request_id="baseline-local",
latency_ms=latency_ms,
)
What does the endpoint add?
- The route accepts a validated Ticket and publishes TriageResponse as its response contract.
- The perf_counter() calls measure how long the local decision takes.
- The model_dump() result carries every decision field into the response.
- The fixed baseline-local request ID identifies this result as a local baseline decision.
- Save main.py.
The development server reloads again. Its terminal remains active for the browser test.
Baseline route missing after the reload?
Confirm that the route decorator begins at the left edge of the file. If it is indented inside naive_triage(), FastAPI cannot register it as an application route.
Check that response_model references TriageResponse. Help me diagnose why /triage/baseline is missing from my FastAPI docs.
Use these tabs to confirm that your file matches the complete baseline service before testing it.
✔️ Awesome, I've got everything!
Great. Confirm that main.py is saved while the development server continues running.
ⓧ I'd like to double check the full code
from time import perf_counter
from typing import Literal
from fastapi import FastAPI
from pydantic import BaseModel, Field
MODEL = "gpt-5.4-nano-2026-03-17"
Category = Literal["account", "billing", "technical", "policy", "other"]
Priority = Literal["low", "normal", "high", "urgent"]
Team = Literal["identity", "finance", "engineering", "compliance", "general"]
class Ticket(BaseModel):
text: str = Field(min_length=10, max_length=2000)
class TriageDecision(BaseModel):
category: Category
priority: Priority
team: Team
escalate: bool
summary: str
next_action: str
class TriageResponse(TriageDecision):
request_id: str
latency_ms: float
app = FastAPI(title="AI Support Ticket Triage Service")
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok", "model": MODEL}
def naive_triage(ticket: Ticket) -> TriageDecision:
text = ticket.text.lower()
if "password" in text:
category: Category = "account"
team: Team = "identity"
elif "refund" in text:
category = "billing"
team = "finance"
else:
category = "other"
team = "general"
priority: Priority = "urgent" if "urgent" in text else "normal"
escalate = priority == "urgent"
return TriageDecision(
category=category,
priority=priority,
team=team,
escalate=escalate,
summary="Keyword baseline decision.",
next_action=f"Route to the {team} team.",
)
@app.post("/triage/baseline", response_model=TriageResponse)
def triage_baseline(ticket: Ticket) -> TriageResponse:
started = perf_counter()
decision = naive_triage(ticket)
latency_ms = round((perf_counter() - started) * 1000, 2)
return TriageResponse(
**decision.model_dump(),
request_id="baseline-local",
latency_ms=latency_ms,
)
What should the full file contain?
The file combines the bounded contracts, health route, keyword classifier, and timed baseline endpoint. Compare the order and indentation with your saved main.py file.
Test the baseline in the interactive docs
The interactive documentation turns your API contract into a browser-based test surface. You can submit a real request without building a separate frontend.
Before you execute the request, what category and team do you expect the word password to trigger?
- Navigate to http://127.0.0.1:8000/docs in your browser.
- Expand POST /triage/baseline in the interactive documentation.
- Click Try it out.
- Replace the generated text value with I cannot sign in because I forgot my password. Please help me regain access..
- Click Execute.
- Inspect the JSON in the response panel.
You will see account as the category and identity as the team. The decision also shows normal priority with false escalation.
The response includes baseline-local as the request ID and a numeric latency_ms value. That is your first working support-ticket classification from the local service.
Did the request fail?
Confirm that your ticket text has at least 10 characters. Shorter values are rejected by the Ticket contract before the baseline runs.
Return to the terminal if the documentation page cannot reach the API. The development server must remain active while the browser sends the request.
Help me diagnose an unsuccessful /triage/baseline request.
Your typed keyword baseline is live and producing visible decisions. Next, you will measure it against labeled tickets and expose where simple keyword matching falls short.
Evaluate the Baseline
Your FastAPI service now returns a typed decision for a password ticket. The next goal is to test whether that result holds across the customer's wider workflow.
One convincing demo cannot establish reliability. A labeled baseline evaluation checks the same operational fields across several cases so the keyword logic has to reveal its limitations.
In this step, get ready to:
- Define four labeled acceptance cases.
- Build a repeatable evaluation harness.
- Measure the baseline across every case.
Define the acceptance cases
Each ticket needs an expected operational decision before the evaluator runs. These fixed labels become acceptance criteria that prevent a promising demo from hiding inconsistent routing.
The JSON file is long enough to assemble in two chunks. Keep both chunks in the same file before saving it.
- Switch back to VS Code from the previous step.
- Create tickets.json in the Explorer sidebar beside main.py.
- Add the password and invoice cases by pasting this first chunk into tickets.json:
[
{
"name": "single-user password reset",
"text": "I cannot sign in because I forgot my password. Please help me regain access.",
"expected": {
"category": "account",
"priority": "normal",
"team": "identity",
"escalate": false
}
},
{
"name": "invoice explanation",
"text": "I have a question about an invoice line item. Nothing is blocked and I only need an explanation.",
"expected": {
"category": "billing",
"priority": "low",
"team": "finance",
"escalate": false
}
},
What do these cases measure?
- The password case checks the keyword path that already worked in the interactive documentation.
- The invoice case expects billing classification with low priority because no user is blocked.
- Complete tickets.json by pasting this second chunk directly below the first chunk:
{
"name": "production checkout outage",
"text": "Production checkout returns HTTP 500 for every shopper, so nobody can place an order.",
"expected": {
"category": "technical",
"priority": "urgent",
"team": "engineering",
"escalate": true
}
},
{
"name": "scheduled policy review",
"text": "Please send the data-retention policy for our scheduled compliance review. No incident is active.",
"expected": {
"category": "policy",
"priority": "low",
"team": "compliance",
"escalate": false
}
}
]
Why keep the labels fixed?
The outage case defines urgent engineering escalation for a service-wide failure. The policy case defines low-priority compliance routing for an informational request.
Keeping these expectations fixed turns the tickets into acceptance criteria. Later implementation changes must improve the service against the same test.
- Save tickets.json.
- Confirm that the Explorer sidebar lists tickets.json beside main.py.
- Confirm that the file contains four objects named single-user password reset, invoice explanation, production checkout outage, and scheduled policy review.
Seeing a JSON warning?
Check that every ticket except the last ends with a comma. Confirm that the final square bracket closes the full list.
Compare the braces around each expected object if VS Code highlights a structural problem.
Help me find the structural error in my tickets.json file.
✔️ Awesome, I've got everything!
Your four labeled tickets are saved as fixed acceptance criteria.
ⓧ I'd like to double check the full code
[
{
"name": "single-user password reset",
"text": "I cannot sign in because I forgot my password. Please help me regain access.",
"expected": {
"category": "account",
"priority": "normal",
"team": "identity",
"escalate": false
}
},
{
"name": "invoice explanation",
"text": "I have a question about an invoice line item. Nothing is blocked and I only need an explanation.",
"expected": {
"category": "billing",
"priority": "low",
"team": "finance",
"escalate": false
}
},
{
"name": "production checkout outage",
"text": "Production checkout returns HTTP 500 for every shopper, so nobody can place an order.",
"expected": {
"category": "technical",
"priority": "urgent",
"team": "engineering",
"escalate": true
}
},
{
"name": "scheduled policy review",
"text": "Please send the data-retention policy for our scheduled compliance review. No incident is active.",
"expected": {
"category": "policy",
"priority": "low",
"team": "compliance",
"escalate": false
}
}
]
Build the evaluation harness
The evaluator sends each ticket to one selected endpoint. It compares only the four fields that control the operational workflow.
You will assemble evaluate.py in four connected chunks. The completed script reports each result before returning an unsuccessful exit status when any case fails.
- Create evaluate.py in the Explorer sidebar beside tickets.json.
- Add the imports and evaluation constants by pasting this first chunk into evaluate.py:
import json
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
BASE_URL = "http://127.0.0.1:8000"
VALID_ENDPOINTS = {"/triage/baseline", "/triage"}
CHECKED_FIELDS = ("category", "priority", "team", "escalate")
def load_cases() -> list[dict]:
with open("tickets.json", encoding="utf-8") as file:
return json.load(file)
What does this setup do?
- The standard library imports provide JSON parsing plus local HTTP requests without another dependency.
- The endpoint set limits evaluation to the baseline route or the later AI route.
- The checked fields focus the score on classification, priority, routing, and escalation.
- The load_cases() function reads the fixed cases from tickets.json.
- Append the API request helper directly below load_cases() by pasting this chunk:
def call_api(endpoint: str, text: str) -> dict:
payload = json.dumps({"text": text}).encode("utf-8")
request = Request(
url=f"{BASE_URL}{endpoint}",
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
with urlopen(request, timeout=30) as response:
return json.loads(response.read().decode("utf-8"))
How does the request helper work?
- The helper converts ticket text into the same request body accepted by the service.
- The request targets the selected endpoint on the local server.
- The response body is decoded into a dictionary for comparison.
- Start the evaluator's main workflow directly below call_api() by pasting this chunk:
def main() -> int:
endpoint = sys.argv[1] if len(sys.argv) > 1 else "/triage/baseline"
if endpoint not in VALID_ENDPOINTS:
print(f"Endpoint must be one of: {', '.join(sorted(VALID_ENDPOINTS))}")
return 2
cases = load_cases()
passed = 0
for case in cases:
try:
actual = call_api(endpoint, case["text"])
except HTTPError as error:
body = error.read().decode("utf-8")
print(f"FAIL {case['name']}: HTTP {error.code} {body}")
continue
except URLError as error:
print(f"FAIL {case['name']}: could not reach API: {error.reason}")
continue
What happens in the first half?
- The baseline endpoint is selected when no command-line argument is supplied.
- Each ticket is sent to the running service one at a time.
- HTTP failures and connection failures become named case failures instead of stopping the loop.
- Complete the evaluator by pasting this final chunk directly below the last continue line:
expected = case["expected"]
mismatches = {
field: {"expected": expected[field], "actual": actual.get(field)}
for field in CHECKED_FIELDS
if actual.get(field) != expected[field]
}
if mismatches:
print(f"FAIL {case['name']}: {json.dumps(mismatches)}")
else:
passed += 1
print(f"PASS {case['name']}")
print(f"\n{passed}/{len(cases)} passed against {endpoint}")
return 0 if passed == len(cases) else 1
if __name__ == "__main__":
raise SystemExit(main())
How does the scoring work?
- The dictionary comprehension records every checked field where the actual value differs from the expected value.
- A case passes only when all four operational fields match.
- The final return value is successful only when every labeled case passes.
- Save evaluate.py.
- Confirm that the Explorer sidebar lists evaluate.py beside tickets.json.
Seeing indentation warnings?
Keep the comparison block indented inside the for case in cases: loop. Keep the final report indented inside main() but outside the loop.
Place the final module guard at the far-left margin so it can call main() when the file runs.
Help me fix the indentation in evaluate.py.
✔️ Awesome, I've got everything!
Your evaluation harness is ready to test either supported endpoint.
ⓧ I'd like to double check the full code
import json
import sys
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
BASE_URL = "http://127.0.0.1:8000"
VALID_ENDPOINTS = {"/triage/baseline", "/triage"}
CHECKED_FIELDS = ("category", "priority", "team", "escalate")
def load_cases() -> list[dict]:
with open("tickets.json", encoding="utf-8") as file:
return json.load(file)
def call_api(endpoint: str, text: str) -> dict:
payload = json.dumps({"text": text}).encode("utf-8")
request = Request(
url=f"{BASE_URL}{endpoint}",
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
with urlopen(request, timeout=30) as response:
return json.loads(response.read().decode("utf-8"))
def main() -> int:
endpoint = sys.argv[1] if len(sys.argv) > 1 else "/triage/baseline"
if endpoint not in VALID_ENDPOINTS:
print(f"Endpoint must be one of: {', '.join(sorted(VALID_ENDPOINTS))}")
return 2
cases = load_cases()
passed = 0
for case in cases:
try:
actual = call_api(endpoint, case["text"])
except HTTPError as error:
body = error.read().decode("utf-8")
print(f"FAIL {case['name']}: HTTP {error.code} {body}")
continue
except URLError as error:
print(f"FAIL {case['name']}: could not reach API: {error.reason}")
continue
expected = case["expected"]
mismatches = {
field: {"expected": expected[field], "actual": actual.get(field)}
for field in CHECKED_FIELDS
if actual.get(field) != expected[field]
}
if mismatches:
print(f"FAIL {case['name']}: {json.dumps(mismatches)}")
else:
passed += 1
print(f"PASS {case['name']}")
print(f"\n{passed}/{len(cases)} passed against {endpoint}")
return 0 if passed == len(cases) else 1
if __name__ == "__main__":
raise SystemExit(main())
Run the baseline evaluation
The running server needs its current terminal, so the evaluator uses a second terminal session. Both processes can then stay active while the tickets move through the endpoint.
- Keep the FastAPI development server running in its existing terminal.
- Select Terminal from the VS Code menu bar.
- Select New Terminal.
- Confirm that the new terminal starts in the folder containing evaluate.py.
Before you run the evaluation, how many of the four labeled cases do you think the narrow keyword rules can classify completely?
- Measure the baseline across all four tickets by running this command:
python evaluate.py
The failure is the evidence
The report shows 1/4 passed against /triage/baseline. Only single-user password reset passes.
The report names failures for invoice explanation, production checkout outage, and scheduled policy review. The nonzero exit status makes the failed acceptance test usable in automation.
This is the intended result. The baseline recognizes a narrow keyword while missing meaning expressed through invoices, service-wide impact, and policy language.
Could not reach the API?
Switch back to the first terminal from earlier. Confirm that the development server is still running at http://127.0.0.1:8000.
If the server stopped, restart the same FastAPI development command from the previous step. Run the evaluation again from the second terminal after the server is ready.
Help me diagnose why evaluate.py cannot reach my local FastAPI service.
You now have measurable proof that the keyword baseline falls short across the wider workflow. Next, you will replace that narrow matching with schema-backed AI classification while keeping these labels fixed.
Add Structured AI Triage
Your four-ticket evaluation exposed the customer risk in the FastAPI baseline. Three labeled cases still fail: invoice explanation, production checkout outage, and scheduled policy review.
This step adds schema-backed Structured Outputs through OpenAI. Pydantic constrains every decision to the operational values your service already exposes.
In this step, get ready to:
- Define semantic classification rules for the model.
- Add a schema-backed AI triage endpoint.
- Prove the AI workflow against the fixed acceptance criteria.
Define the triage contract
The model needs explicit rules for categories, priorities, teams, and escalation. These instructions turn the labeled customer expectations into one reusable classification contract.
- Switch back to main.py in VS Code.
- Find these two import lines:
from fastapi import FastAPI
from pydantic import BaseModel, Field
What are you changing?
These imports already create the API application and its typed data models. The next edit gives the service access to the installed OpenAI client.
- Replace those two lines with this updated import group:
from fastapi import FastAPI
from openai import OpenAI
from pydantic import BaseModel, Field
What does this import add?
The OpenAI class creates the SDK client used by the new route. The client reads the API key from your current terminal environment.
- Find the MODEL definition near the top of main.py.
- Add the classification contract directly below it by copying this code:
SYSTEM_INSTRUCTIONS = """
You triage support tickets into an operational workflow.
Classification and team rules:
- Login, password, authentication, or account access: category account, team identity.
- Invoice, charge, refund, or payment: category billing, team finance.
- Error, outage, broken checkout, or application failure: category technical, team engineering.
- Privacy, data retention, compliance, or policy request: category policy, team compliance.
- Anything else: category other, team general.
Priority and escalation rules:
- urgent and escalate true: an active security compromise, a production outage, or checkout unavailable to all users.
- high and escalate true: multiple users are blocked, but the service is not completely unavailable.
- normal and escalate false: a single-user issue that needs action.
- low and escalate false: an informational request with no active incident and no blocked user.
Keep summary and next_action concise. Base the decision only on the supplied ticket.
""".strip()
How does the contract guide the model?
- The classification rules map customer language to a bounded category and destination team.
- The priority rules connect operational impact to urgency and escalation.
- The final instruction keeps the generated summary grounded in the supplied ticket.
- Save main.py.
- Return to the FastAPI terminal from earlier.
Good progress. The development server reloads without a startup traceback because the AI contract is valid Python.
Did the server fail to reload?
- Confirm that OpenAI uses the same capitalization as the import shown above.
- Compare the opening and closing triple quotes around SYSTEM_INSTRUCTIONS.
Help me diagnose a startup failure after adding the OpenAI import and system instructions.
Connect the structured endpoint
The route needs one shared SDK client before it can send tickets to the pinned gpt-5.4-nano-2026-03-17 model. The current session-scoped key authenticates that client without placing a credential in your source code.
- Find the application definition in main.py.
- Locate this line:
app = FastAPI(title="AI Support Ticket Triage Service")
What does this line represent?
This line creates the existing API application. The SDK client belongs beside it because every request to the new route uses the same client configuration.
- Replace that line with these two lines:
app = FastAPI(title="AI Support Ticket Triage Service")
client = OpenAI()
How does the client authenticate?
The client reads OPENAI_API_KEY from the current terminal session. Your real credential stays outside main.py.
- Save main.py.
- Return to the FastAPI terminal from earlier.
The server reloads successfully because the client can find your session-scoped API key.
Did client creation stop the reload?
- Confirm that the terminal running FastAPI is the session where you loaded OPENAI_API_KEY.
- Confirm that client = OpenAI() appears directly below the existing application definition.
Help me diagnose why the OpenAI client cannot start inside my FastAPI service.
The new endpoint sends each ticket with the operational rules. It asks the SDK to parse the response directly into TriageDecision.
- Move to the bottom of main.py.
- Add the structured AI route below triage_baseline by copying this code:
@app.post("/triage", response_model=TriageResponse)
def triage_with_ai(ticket: Ticket) -> TriageResponse:
started = perf_counter()
response = client.responses.parse(
model=MODEL,
input=[
{"role": "system", "content": SYSTEM_INSTRUCTIONS},
{"role": "user", "content": ticket.text},
],
text_format=TriageDecision,
)
decision = response.output_parsed
if decision is None:
raise ValueError("The triage provider returned no structured decision.")
latency_ms = round((perf_counter() - started) * 1000, 2)
return TriageResponse(
**decision.model_dump(),
request_id=response._request_id,
latency_ms=latency_ms,
)
What does the structured route do?
- The input list keeps the operational contract separate from the customer ticket.
- The text_format argument binds the response to TriageDecision.
- The output_parsed property provides the validated Pydantic object.
- The returned response preserves the provider request ID and measured latency.
- Save main.py.
- Return to the interactive documentation at http://127.0.0.1:8000/docs.
- Refresh the documentation page.
You’ll see a new POST /triage endpoint beside the existing baseline route. This confirms FastAPI loaded the structured AI workflow.
Don't see the new endpoint?
- Check the FastAPI terminal for a syntax error near triage_with_ai.
- Confirm that the route starts at the left edge of main.py.
- Refresh the documentation after the development server finishes reloading.
Help me find why the new triage endpoint is missing from my FastAPI documentation.
✔️ Awesome, I've got everything!
Your structured route is in place. Your saved file should now match the reference in the other tab.
ⓧ I'd like to double check the full code
from time import perf_counter
from typing import Literal
from fastapi import FastAPI
from openai import OpenAI
from pydantic import BaseModel, Field
MODEL = "gpt-5.4-nano-2026-03-17"
SYSTEM_INSTRUCTIONS = """
You triage support tickets into an operational workflow.
Classification and team rules:
- Login, password, authentication, or account access: category account, team identity.
- Invoice, charge, refund, or payment: category billing, team finance.
- Error, outage, broken checkout, or application failure: category technical, team engineering.
- Privacy, data retention, compliance, or policy request: category policy, team compliance.
- Anything else: category other, team general.
Priority and escalation rules:
- urgent and escalate true: an active security compromise, a production outage, or checkout unavailable to all users.
- high and escalate true: multiple users are blocked, but the service is not completely unavailable.
- normal and escalate false: a single-user issue that needs action.
- low and escalate false: an informational request with no active incident and no blocked user.
Keep summary and next_action concise. Base the decision only on the supplied ticket.
""".strip()
Category = Literal["account", "billing", "technical", "policy", "other"]
Priority = Literal["low", "normal", "high", "urgent"]
Team = Literal["identity", "finance", "engineering", "compliance", "general"]
class Ticket(BaseModel):
text: str = Field(min_length=10, max_length=2000)
class TriageDecision(BaseModel):
category: Category
priority: Priority
team: Team
escalate: bool
summary: str
next_action: str
class TriageResponse(TriageDecision):
request_id: str
latency_ms: float
app = FastAPI(title="AI Support Ticket Triage Service")
client = OpenAI()
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok", "model": MODEL}
def naive_triage(ticket: Ticket) -> TriageDecision:
text = ticket.text.lower()
if "password" in text:
category: Category = "account"
team: Team = "identity"
elif "refund" in text:
category = "billing"
team = "finance"
else:
category = "other"
team = "general"
priority: Priority = "urgent" if "urgent" in text else "normal"
escalate = priority == "urgent"
return TriageDecision(
category=category,
priority=priority,
team=team,
escalate=escalate,
summary="Keyword baseline decision.",
next_action=f"Route to the {team} team.",
)
@app.post("/triage/baseline", response_model=TriageResponse)
def triage_baseline(ticket: Ticket) -> TriageResponse:
started = perf_counter()
decision = naive_triage(ticket)
latency_ms = round((perf_counter() - started) * 1000, 2)
return TriageResponse(
**decision.model_dump(),
request_id="baseline-local",
latency_ms=latency_ms,
)
@app.post("/triage", response_model=TriageResponse)
def triage_with_ai(ticket: Ticket) -> TriageResponse:
started = perf_counter()
response = client.responses.parse(
model=MODEL,
input=[
{"role": "system", "content": SYSTEM_INSTRUCTIONS},
{"role": "user", "content": ticket.text},
],
text_format=TriageDecision,
)
decision = response.output_parsed
if decision is None:
raise ValueError("The triage provider returned no structured decision.")
latency_ms = round((perf_counter() - started) * 1000, 2)
return TriageResponse(
**decision.model_dump(),
request_id=response._request_id,
latency_ms=latency_ms,
)
How should you use this reference?
Compare the imports, system instructions, client definition, and final route with your saved file. Keep tickets.json and evaluate.py unchanged.
Prove the AI result
The four labels remain fixed because they represent the customer’s initial acceptance criteria. A passing result now measures semantic classification against the same cases that exposed the baseline.
This check sends four metered API requests. The included evaluation is estimated under $0.05, which keeps it far below the project’s $5 limit.
The first case may take longer while OpenAI processes the new output schema. That delay is expected.
Before you run it, predict whether the semantic rules clear all four fixed labels:
- Evaluate the structured endpoint by running this command in a second terminal with the virtual environment activated:
python evaluate.py /triage
What does this evaluation prove?
- The endpoint argument directs every labeled case to /triage.
- The harness compares only category, priority, team, and escalation.
- The unchanged labels keep the measurement tied to the original customer requirements.
You’ll see all four ticket names marked PASS. The report ends with 4/4 passed against /triage.
Did a labeled ticket fail?
- Read the reported field mismatch for the failed ticket.
- Compare SYSTEM_INSTRUCTIONS with the full-code reference.
- Keep the expected labels in tickets.json unchanged.
Help me diagnose why my structured triage evaluation does not report four passing cases.
That’s the breakthrough. Your service now turns varied customer language into schema-valid operational decisions that satisfy the fixed evaluation.
Your AI route now passes the customer cases that defeated the baseline. Next, you’ll make provider failures safe and add the request tracing needed for production support.
Handle Failures and Add Observability
Your AI-backed route already meets the four fixed acceptance cases. A customer-facing FastAPI service still needs predictable behavior when the model provider cannot answer.
This step turns OpenAI API failures into safe HTTP responses. You will also add observability for successful decisions.
In this step, get ready to:
- Convert provider connection failures into safe API responses.
- Log request identifiers with decision details.
- Confirm malformed input is rejected before an AI call.
Convert provider failures into safe responses
Provider calls can fail because of network issues. They can also fail because the provider returns an unsuccessful status. Your API needs to translate those failures into stable responses without exposing credentials or internal details.
- In main.py, replace the import block at the top with the code below:
import logging
from time import perf_counter
from typing import Literal
import openai
from fastapi import FastAPI, HTTPException
from openai import OpenAI
from pydantic import BaseModel, Field
What do these imports add?
- The openai module exposes the SDK exception classes used by the failure handlers.
- The HTTPException class creates controlled API error responses.
- The logging module records operational details in the service terminal.
- Save main.py.
The running FastAPI terminal should complete its automatic reload without an import error.
Seeing an import error?
Compare the import names with the code block. Confirm the virtual environment from earlier is still active in the running service terminal.
Ask for help checking the imports in your updated file.
The failure handlers need a named logger before the route can record provider errors. Python's logging configuration sends those records to the running service terminal.
- In main.py, find the lines that create app and client.
- Replace those two lines with the block below:
app = FastAPI(title="AI Support Ticket Triage Service")
client = OpenAI()
logger = logging.getLogger("triage")
logging.basicConfig(level=logging.INFO)
How is logging configured?
- The triage logger groups messages from this service.
- The INFO level includes successful request records.
- The same logger captures exception details for operator diagnosis.
- Save main.py.
The service terminal should reload the application successfully. The logger is now available to the AI route.
Does the logger fail during reload?
Confirm the logger setup sits immediately after client = OpenAI(). Check that import logging remains at the top of main.py.
Ask for help diagnosing the logger setup.
Connection failures and timeouts mean the provider is temporarily unreachable. Provider status failures mean a request reached the service but did not complete successfully.
- In triage_with_ai(), find the block that assigns response from client.responses.parse.
- Replace that block with the guarded provider call below:
try:
response = client.responses.parse(
model=MODEL,
input=[
{"role": "system", "content": SYSTEM_INSTRUCTIONS},
{"role": "user", "content": ticket.text},
],
text_format=TriageDecision,
)
except (openai.APIConnectionError, openai.APITimeoutError) as exc:
logger.exception("openai_unavailable error=%s", type(exc).__name__)
raise HTTPException(
status_code=503,
detail="The triage provider is temporarily unavailable.",
) from exc
except openai.APIStatusError as exc:
logger.exception(
"openai_status_error request_id=%s status_code=%s",
exc.request_id,
exc.status_code,
)
raise HTTPException(
status_code=502,
detail="The triage provider returned an unsuccessful response.",
) from exc
How are provider failures handled?
- Connection failures and timeouts become safe HTTP 503 responses.
- Provider status failures become safe HTTP 502 responses.
- The exception logs retain diagnostic context for the operator.
- The response details avoid exposing credentials or internal exception data.
- Save main.py.
The development server should reload successfully. The route now has explicit handling for the documented OpenAI SDK failure types.
Seeing a syntax error in the route?
Confirm the except blocks align with try. Keep the provider call indented inside the try block.
Ask for help checking the exception block indentation.
A successful provider request can still lack a parsed decision. That result needs the same safe API boundary as other unsuccessful upstream responses.
- In triage_with_ai(), find the if decision is None: block.
- Replace that block with the safe response below:
decision = response.output_parsed
if decision is None:
raise HTTPException(
status_code=502,
detail="The triage provider returned no structured decision.",
)
Why handle a missing decision?
Structured output can be absent when a provider response does not produce a parsed result. The HTTP 502 response keeps downstream behavior predictable.
- Save main.py.
The service should reload without an error. Every known provider failure path now produces a controlled API response.
Does the missing-decision check fail?
Confirm decision = response.output_parsed remains after all exception handlers. Check that HTTPException is present in the FastAPI import.
Ask for help checking the missing-decision path.
Log each successful decision
Safe errors protect API consumers. Success logs give operators enough context to trace a request and understand how long it took.
- In triage_with_ai(), find the line that calculates latency_ms.
- Replace that line with the timing and success-log block below:
latency_ms = round((perf_counter() - started) * 1000, 2)
logger.info(
"triage_complete request_id=%s category=%s priority=%s escalate=%s latency_ms=%.2f",
response._request_id,
decision.category,
decision.priority,
decision.escalate,
latency_ms,
)
What does the success log capture?
- The SDK's _request_id links the local record to the upstream request.
- The category and priority show the operational result.
- The escalation value highlights decisions that require urgent attention.
- The latency value shows the complete provider-call duration in milliseconds.
- Save main.py.
The running service should reload successfully. It is ready to emit one completion record for each successful AI request.
- Switch back to the terminal you used for the AI evaluation.
This check sends four short billable requests from the fixed evaluation set. Before you run it, do you expect the new reliability code to change any classification results?
- Run the AI evaluation again with this command:
python evaluate.py /triage
What should you see?
You should see 4/4 passed against /triage. The running FastAPI terminal should contain one triage_complete record for each successful request.
Each record includes request_id, category, priority, escalate, and latency_ms.
Missing passing results or logs?
If the evaluation cannot reach the API, confirm the FastAPI process from earlier is still running. If the evaluation passes without visible logs, check the service terminal instead of the evaluation terminal.
Ask for help tracing the failed check.
Your passing evaluation now proves that reliability controls did not change the fixed acceptance criteria. Double-check the finished service before testing its validation boundary.
✔️ Awesome, I've got everything!
Great. Confirm that main.py is saved before continuing.
ⓧ I'd like to double check the full code
import logging
from time import perf_counter
from typing import Literal
import openai
from fastapi import FastAPI, HTTPException
from openai import OpenAI
from pydantic import BaseModel, Field
MODEL = "gpt-5.4-nano-2026-03-17"
SYSTEM_INSTRUCTIONS = """
You triage support tickets into an operational workflow.
Classification and team rules:
- Login, password, authentication, or account access: category account, team identity.
- Invoice, charge, refund, or payment: category billing, team finance.
- Error, outage, broken checkout, or application failure: category technical, team engineering.
- Privacy, data retention, compliance, or policy request: category policy, team compliance.
- Anything else: category other, team general.
Priority and escalation rules:
- urgent and escalate true: an active security compromise, a production outage, or checkout unavailable to all users.
- high and escalate true: multiple users are blocked, but the service is not completely unavailable.
- normal and escalate false: a single-user issue that needs action.
- low and escalate false: an informational request with no active incident and no blocked user.
Keep summary and next_action concise. Base the decision only on the supplied ticket.
""".strip()
Category = Literal["account", "billing", "technical", "policy", "other"]
Priority = Literal["low", "normal", "high", "urgent"]
Team = Literal["identity", "finance", "engineering", "compliance", "general"]
class Ticket(BaseModel):
text: str = Field(min_length=10, max_length=2000)
class TriageDecision(BaseModel):
category: Category
priority: Priority
team: Team
escalate: bool
summary: str
next_action: str
class TriageResponse(TriageDecision):
request_id: str
latency_ms: float
app = FastAPI(title="AI Support Ticket Triage Service")
client = OpenAI()
logger = logging.getLogger("triage")
logging.basicConfig(level=logging.INFO)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok", "model": MODEL}
def naive_triage(ticket: Ticket) -> TriageDecision:
text = ticket.text.lower()
if "password" in text:
category: Category = "account"
team: Team = "identity"
elif "refund" in text:
category = "billing"
team = "finance"
else:
category = "other"
team = "general"
priority: Priority = "urgent" if "urgent" in text else "normal"
escalate = priority == "urgent"
return TriageDecision(
category=category,
priority=priority,
team=team,
escalate=escalate,
summary="Keyword baseline decision.",
next_action=f"Route to the {team} team.",
)
@app.post("/triage/baseline", response_model=TriageResponse)
def triage_baseline(ticket: Ticket) -> TriageResponse:
started = perf_counter()
decision = naive_triage(ticket)
latency_ms = round((perf_counter() - started) * 1000, 2)
return TriageResponse(
**decision.model_dump(),
request_id="baseline-local",
latency_ms=latency_ms,
)
@app.post("/triage", response_model=TriageResponse)
def triage_with_ai(ticket: Ticket) -> TriageResponse:
started = perf_counter()
try:
response = client.responses.parse(
model=MODEL,
input=[
{"role": "system", "content": SYSTEM_INSTRUCTIONS},
{"role": "user", "content": ticket.text},
],
text_format=TriageDecision,
)
except (openai.APIConnectionError, openai.APITimeoutError) as exc:
logger.exception("openai_unavailable error=%s", type(exc).__name__)
raise HTTPException(
status_code=503,
detail="The triage provider is temporarily unavailable.",
) from exc
except openai.APIStatusError as exc:
logger.exception(
"openai_status_error request_id=%s status_code=%s",
exc.request_id,
exc.status_code,
)
raise HTTPException(
status_code=502,
detail="The triage provider returned an unsuccessful response.",
) from exc
decision = response.output_parsed
if decision is None:
raise HTTPException(
status_code=502,
detail="The triage provider returned no structured decision.",
)
latency_ms = round((perf_counter() - started) * 1000, 2)
logger.info(
"triage_complete request_id=%s category=%s priority=%s escalate=%s latency_ms=%.2f",
response._request_id,
decision.category,
decision.priority,
decision.escalate,
latency_ms,
)
return TriageResponse(
**decision.model_dump(),
request_id=response._request_id,
latency_ms=latency_ms,
)
Reject malformed input before the provider call
Pydantic owns the ticket contract at the API boundary. This check reveals whether invalid input reaches the provider code.
- Switch back to the interactive documentation from earlier.
- Expand POST /triage.
- Click Try it out.
- Replace the sample value for text with a value containing fewer than 10 characters.
Before you send the request, do you expect it to reach client.responses.parse?
- Click Execute.
You should see a validation response explaining that the ticket text is too short. The request is rejected at the API boundary.
- Switch back to the running FastAPI terminal.
You should see no new triage_complete log line from the malformed request. That absence proves the request never reached the AI call.
Was the malformed request accepted?
Confirm the submitted ticket contains fewer than 10 characters. Check that Ticket.text still uses Field(min_length=10, max_length=2000).
Ask for help checking the request validation boundary.
That closes the reliability loop. Your service preserves its passing AI decisions while handling provider failures and malformed input through controlled boundaries.
Secret mission
Write the Customer Rollout and Handoff Brief
Your API is technically ready for a customer conversation. You will turn it into a staged rollout plan with measurable acceptance criteria plus safety controls. You will finish with ownership details plus a handoff checklist another engineer can execute.
Clean Up Your Resources
Clean Up Your Resources
Your local FastAPI service and project files create no ongoing hosting costs. Calls to the OpenAI API remain billable whenever the key is used for more requests.
Resources you used:
- The local FastAPI development process running in your current terminal session.
- The current terminal session holding OPENAI_API_KEY.
- The disposable .venv/ environment plus the __pycache__/ cache directory.
- The local project files main.py, evaluate.py, tickets.json, requirements.txt, .gitignore, and DEPLOYMENT.md.
- The OpenAI API key used for metered requests.
Keep everything running
No action needed. Choose this if you want to keep testing the triage service or refining the customer rollout brief.
- Leave the current terminal open to keep the interactive documentation available at http://127.0.0.1:8000/docs.
- Keep the local project files for future evaluation or rollout work.
- Keep the API key outside Git.
- Review your OpenAI API usage after further experiments.
Pause - I'll come back to this later
Shut down the local process while preserving the project files for later. Closing the terminal also removes the session-scoped API key from that terminal session.
- Switch back to the FastAPI terminal from earlier.
- Press Ctrl+C to stop the development server.
- Deactivate the virtual environment by running this command:
deactivate
What Does This Command Do?
The command leaves the isolated Python environment. Your .venv/ directory and installed dependencies stay available for your return.
- Close the terminal window to end the session containing OPENAI_API_KEY.
Delete - I don't want to use this again
Removing the local artifacts is permanent. Revoking the API key prevents that key from making more billable requests.
- Switch back to the FastAPI terminal from earlier.
- Press Ctrl+C to stop the development server.
- Revoke the project API key from the OpenAI API key page.
- Remove the listed local project artifacts by running these commands from the same project folder:
rm -rf .venv/ __pycache__/
rm -f main.py evaluate.py tickets.json requirements.txt .gitignore DEPLOYMENT.md
What Do These Commands Remove?
The first command deletes the disposable virtual environment and Python cache directory.
The second command deletes the API implementation, evaluation harness, labeled tickets, dependency pins, Git exclusions, and deployment brief.
- Close the terminal window to clear the session containing OPENAI_API_KEY.
Nice Work!
Nice Work!
You made it! Your FastAPI service now turns support tickets into measured routing decisions with a customer-ready rollout path.
You've learned how to:
- Build a typed REST API in FastAPI. Use Pydantic contracts to keep ticket decisions within bounded operational values.
- Expose a keyword baseline's 1/4 result through a repeatable four-ticket evaluation. Replace the baseline with schema-backed Structured Outputs from the OpenAI API. Confirm all 4/4 acceptance cases pass.
- Add safe provider failure handling for connection issues. Preserve request IDs for traceability. Measure latency for each successful AI request.
- Secret Mission: Produce a customer rollout and engineering handoff brief with measurable acceptance criteria. Make staged adoption reversible through a manual fallback plus an urgent-escalation stop condition.
Ready to quiz yourself?