Build a Jev-Ready Ticket Router

Build and test an offline Python router using simulated Jev decisions.

Introduction

30 Second Summary

Support tickets rarely arrive as tidy single-issue requests. A message about an account plus a charge can make an automatic system choose a queue before the evidence is clear.

In this project, you will build an offline ticket router around simulated Jev-shaped answers. You will apply System One principles so Python keeps the final decision policy under deterministic control.

What You'll Build

In Windows PowerShell, you will watch an ambiguous ticket hit the wrong automatic queue before the corrected policy sends it to human review.

By the end of this project, you'll have:

  • An inspectable question set built from atomic questions. Intent uses Choice. Frustration uses Score. Two Noul questions test urgency plus refund intent against one structured state.
  • A terminal decision report that exposes each route's evidence. You can see T-101 reach billing_refund_queue. You can see T-103 stop at human_review. The weighted priority score remains visible beside each route.
  • Unit tests that keep known routing cases stable as questions or thresholds evolve.
  • Secret Mission: Replace one global confidence floor with separate policy floors for each predicted intent.

Are there any prerequisites?

You only need a Windows computer because the guide covers all software setup. No TypeSafe account or API key is required.

Before We Start

This project is an offline architecture lab built with simulated Jev-shaped fixtures. Before any hands-on work begins, you will define which clear support decisions your router can automate and which uncertain cases need a person.

Set Up the Windows Workspace

The offline Jev lab keeps model behavior simulated. A verified local workspace keeps later problems focused on routing logic.

Python 3.14.8 runs every script in this project. No package installation or account is needed.

Visual Studio Code holds the project files. Windows PowerShell runs each local check.

In this step, get ready to:
  • Confirm Python 3.14.8 is available through the Windows Python launcher.
  • Confirm Visual Studio Code is installed.
  • Open an empty jev-decision-lab workspace in Visual Studio Code.
Verify Python 3.14.8

The Windows Python launcher tracks the runtimes installed on your computer. Its runtime list reveals whether the required version is ready.

  • Press the Windows key to open Windows Search.
  • Type Windows PowerShell into the search field.
  • Press Enter to open PowerShell.
  • List the installed Python runtimes by running this command:
py list

What Does This Check Show?

The launcher prints every Python runtime it can use. If the required runtime is missing, the Python Install Manager installs it through the official Windows setup path.

✔️ I see the required version

Your runtime is ready. PowerShell lists Python 3.14.8 among the installed runtimes.

  • Keep the PowerShell window open for the workspace setup.

ⓧ I see an older version

The launcher works, but the installed runtime does not meet the project requirement. The Python Install Manager can add the current stable runtime.

  • Open the official Python downloads page for Windows.
  • Download the Python Install Manager from the page.
  • Run the downloaded installer.
  • Complete the installer prompts.
  • Switch back to PowerShell from the first check.
  • Install the current stable runtime by running this command:
python

What Does This Command Do?

The first launch installs the current stable Python runtime when it is missing. Python then opens its interactive prompt.

  • Return to PowerShell by entering this at the Python prompt:
exit()

Why Exit the Prompt?

This closes the interactive Python session. PowerShell becomes available for the verification check.

  • Confirm the new runtime is available by running the launcher check again:
py list

What Should You See?

PowerShell should list Python 3.14.8 as an installed runtime. That result confirms the project can use the required version.

Still Seeing Only the Older Version?

  • Close PowerShell after the runtime installation finishes.
  • Reopen PowerShell through Windows Search.
  • Repeat the launcher check in the fresh window.
  • Help me troubleshoot the missing Python runtime.

ⓧ Command not found

Windows cannot reach the Python launcher yet. Installing the Python Install Manager supplies the launcher required by this project.

  • Open the official Python downloads page for Windows.
  • Download the Python Install Manager from the page.
  • Run the downloaded installer.
  • Complete the installer prompts.
  • Switch back to PowerShell from the first check.
  • Install the current stable runtime by running this command:
python

What Does This Command Do?

The Python Install Manager obtains the current stable runtime. Python then opens its interactive prompt.

  • Return to PowerShell by entering this at the Python prompt:
exit()

Why Exit the Prompt?

This closes the interactive Python session. PowerShell becomes available for the launcher check.

  • Confirm the launcher is available by running this command:
py list

What Should You See?

PowerShell should list Python 3.14.8 as an installed runtime. The Windows launcher is now ready for the project.

Launcher Still Unavailable?

  • Close PowerShell after the Python Install Manager finishes.
  • Reopen PowerShell through Windows Search.
  • Repeat the launcher check in the fresh window.
  • Help me restore the Windows Python launcher.
Confirm Visual Studio Code

Visual Studio Code keeps every fixture and script in one workspace. Windows Search provides a quick way to check whether the editor is installed.

  • Press the Windows key to open Windows Search.
  • Type Visual Studio Code into the search field.
  • Check whether Visual Studio Code appears in the search results.

✔️ Visual Studio Code appears

The editor is installed. The final workspace check confirms that PowerShell can reach its command.

  • Press Esc to close Windows Search.

ⓧ Visual Studio Code is missing

Microsoft recommends Windows User setup for most people. The installer also adds the editor command to new console sessions.

The editor is now installed. A fresh PowerShell session loads the new editor command.

  • Close the PowerShell window from the Python check.
  • Press the Windows key to open Windows Search.
  • Type Windows PowerShell into the search field.
  • Press Enter to open a fresh PowerShell session.

Editor Installation Did Not Finish?

  • Confirm the Windows User setup installer reached its completion screen.
  • Run the official installer again if setup stopped early.
  • Help me troubleshoot the Visual Studio Code installation.
Create the project workspace

A dedicated folder gives every later fixture and script one predictable home. PowerShell must be inside that folder before it opens the editor.

  • Switch back to the PowerShell window from the setup checks.
  • Move to your Desktop by running this command:
Set-Location -Path "$env:USERPROFILE\Desktop" -PassThru

What Does This Command Do?

PowerShell changes its current location to your Desktop. It prints the resulting path so you can confirm the move.

  • Create the jev-decision-lab folder by running these commands:
New-Item -Path . -Name "jev-decision-lab" -ItemType "Directory"
Set-Location -Path "jev-decision-lab"
Get-Location

What Do These Commands Do?

  • The first command creates the jev-decision-lab folder on your Desktop.
  • The second command makes that folder the current PowerShell location.
  • The final command prints the current location for verification.

You should see a path ending in Desktop\jev-decision-lab. This confirms PowerShell is inside the empty project folder.

Folder Setup Did Not Work?

  • Confirm the Desktop location command printed a path ending in Desktop.
  • Check that the folder name is exactly jev-decision-lab.
  • Help me create the project folder in PowerShell.

Before you check the finished setup, do you expect the Python launcher to remain available inside the new project folder?

  • Verify the installed Python runtimes from the project folder by running:
py list

What Should You See?

PowerShell should still list Python 3.14.8. This proves the launcher is available from inside the project folder.

Before the final command, which folder name do you expect Visual Studio Code to show?

  • Open the current project folder in Visual Studio Code by running:
code .

What Does This Command Do?

The editor command opens the current PowerShell folder as a Visual Studio Code workspace. The period represents the folder PowerShell is currently using.

  • Select Yes, I trust the authors if Visual Studio Code asks about workspace trust.

You should see the empty jev-decision-lab folder in the Visual Studio Code Explorer. PowerShell remains open inside the same folder.

Final Workspace Check Failed?

  • Return to the matching Python outcome tab if the required runtime is absent.
  • Reopen PowerShell if the editor command is unavailable after installation.
  • Confirm the current PowerShell path ends in jev-decision-lab before repeating the editor command.
  • Help me diagnose the final workspace check.

That setup hurdle is cleared. Python and Visual Studio Code now share one empty local workspace.

Your local workspace is ready. Next, you will add simulated ticket fixtures and expose the naive router's first mistake.

Build the Naive Router

Your Windows workspace is ready. The next proof is a Python decision that runs entirely on your machine.

A Choice answer can have one winning option even when a ticket is ambiguous. This first router trusts only that winner so you can see the risk of unsafe automation.

In this step, get ready to:
  • Define three ticket states with offline Jev-shaped answers.
  • Map each selected intent to a local queue.
  • Run the ambiguous ticket through the direct-choice router.
Create the offline ticket fixtures

An offline fixture is saved sample data that lets you exercise decision logic without contacting a live service. Each ticket contains a structured state plus simulated answers for that state.

These values are teaching data. They demonstrate documented Jev answer shapes without making claims about real model accuracy.

  • In the Visual Studio Code window from the previous step, right-click the jev-decision-lab folder in the left file sidebar.
  • Select the option for creating a file.

You will see a field for the new filename.

  • Enter fixtures.py as the filename.

You will see a blank fixtures.py file open in the editor.

  • Select the second tab below.
  • Copy the complete reference into fixtures.py.

✔️ Awesome, I've got everything!

Your fixture file contains three ticket states plus one simulated response for each ticket.

ⓧ I'd like to double check the full code

"""Simulated Jev-shaped responses for an offline architecture lab.

These values are educational fixtures, not recorded Jev evaluations and not
claims about Jev's accuracy on these tickets.
"""

TICKETS = [
    {
        "id": "T-101",
        "state": {
            "ticket": {
                "message": "I was charged twice. Please refund the duplicate today."
            }
        },
    },
    {
        "id": "T-102",
        "state": {
            "ticket": {
                "message": "The export button fails, but I can still download CSV files."
            }
        },
    },
    {
        "id": "T-103",
        "state": {
            "ticket": {
                "message": "Something is wrong with my account and a recent charge."
            }
        },
    },
]

SIMULATED_RESPONSES = {
    "T-101": {
        "answers": {
            "intent": {
                "type": "choice",
                "choice": "billing",
                "probabilities": {
                    "billing": 0.92,
                    "technical": 0.05,
                    "account": 0.03,
                },
                "confidence": 0.88,
            },
            "frustration": {
                "type": "score",
                "score": 2.0,
                "legend": {
                    "0": "Calm and factual",
                    "1": "Frustrated but civil",
                    "2": "Very frustrated or threatening to leave",
                },
                "probabilities": {"0": 0.02, "1": 0.18, "2": 0.80},
                "confidence": 0.67,
            },
            "is_urgent": {"type": "noul", "noul": 0.90},
            "refund_requested": {"type": "noul", "noul": 0.98},
        }
    },
    "T-102": {
        "answers": {
            "intent": {
                "type": "choice",
                "choice": "technical",
                "probabilities": {
                    "billing": 0.05,
                    "technical": 0.80,
                    "account": 0.15,
                },
                "confidence": 0.70,
            },
            "frustration": {
                "type": "score",
                "score": 0.0,
                "legend": {
                    "0": "Calm and factual",
                    "1": "Frustrated but civil",
                    "2": "Very frustrated or threatening to leave",
                },
                "probabilities": {"0": 0.80, "1": 0.18, "2": 0.02},
                "confidence": 0.67,
            },
            "is_urgent": {"type": "noul", "noul": 0.30},
            "refund_requested": {"type": "noul", "noul": 0.05},
        }
    },
    "T-103": {
        "answers": {
            "intent": {
                "type": "choice",
                "choice": "billing",
                "probabilities": {
                    "billing": 0.40,
                    "technical": 0.35,
                    "account": 0.25,
                },
                "confidence": 0.10,
            },
            "frustration": {
                "type": "score",
                "score": 1.0,
                "legend": {
                    "0": "Calm and factual",
                    "1": "Frustrated but civil",
                    "2": "Very frustrated or threatening to leave",
                },
                "probabilities": {"0": 0.15, "1": 0.70, "2": 0.15},
                "confidence": 0.55,
            },
            "is_urgent": {"type": "noul", "noul": 0.50},
            "refund_requested": {"type": "noul", "noul": 0.55},
        }
    },
}

How Are the Fixtures Structured?

  • The TICKETS list stores the three support ticket states.
  • Each message sits inside the ticket's state object.
  • The SIMULATED_RESPONSES dictionary stores one offline response under each ticket ID.
  • The intent answer includes a selected choice plus a probability distribution and confidence value.

You will see T-101, T-102, and T-103 inside both top-level collections.

  • Save fixtures.py with the save control in Visual Studio Code.
  • Confirm that the unsaved indicator disappears from the editor tab.

Fixture File Looks Incomplete?

  • Confirm that the first line begins with the documentation string shown in the reference.
  • Confirm that the last line closes the SIMULATED_RESPONSES dictionary.
  • Compare the punctuation around each ticket ID with the full reference.
  • Help me check my offline fixture file.
Build the direct-choice router

The first routing policy reads only the selected intent category. A local mapping turns that category into a queue name through deterministic code.

  • Right-click the jev-decision-lab folder in the left file sidebar.
  • Select the option for creating a file.

You will see another field for a new filename.

  • Enter my-script.py as the filename.

You will see a blank my-script.py file open beside your fixtures.

  • Add the direct-choice routing policy to my-script.py by pasting the code below:
from fixtures import SIMULATED_RESPONSES, TICKETS

ROUTES = {
    "billing": "billing_queue",
    "technical": "technical_queue",
    "account": "account_queue",
}


def naive_destination(response):
    intent = response["answers"]["intent"]["choice"]
    return ROUTES[intent]


def main():
    ambiguous_response = SIMULATED_RESPONSES["T-103"]
    print(f"Naive route for T-103: {naive_destination(ambiguous_response)}")


if __name__ == "__main__":
    main()

What Does This Code Do?

  • The import makes TICKETS plus SIMULATED_RESPONSES available to the script.
  • The ROUTES dictionary keeps every queue name in Python code.
  • The naive_destination() function reads the selected intent choice.
  • The main() function sends the simulated T-103 response through that policy.
  • Save my-script.py with the save control in Visual Studio Code.
  • Confirm that the unsaved indicator disappears from the editor tab.

Router File Not Saving?

  • Confirm that my-script.py appears inside the jev-decision-lab folder.
  • Confirm that the filename includes the .py extension.
  • Help me check my router file.

✔️ Awesome, I've got everything!

Your direct-choice router is saved and ready for its first run.

ⓧ I'd like to double check the full code

from fixtures import SIMULATED_RESPONSES, TICKETS

ROUTES = {
    "billing": "billing_queue",
    "technical": "technical_queue",
    "account": "account_queue",
}


def naive_destination(response):
    intent = response["answers"]["intent"]["choice"]
    return ROUTES[intent]


def main():
    ambiguous_response = SIMULATED_RESPONSES["T-103"]
    print(f"Naive route for T-103: {naive_destination(ambiguous_response)}")


if __name__ == "__main__":
    main()

This reference contains the complete direct-choice router for this step.

Run the ambiguous ticket

Before you run the script, picture the queue that the selected intent is likely to choose for T-103.

  • Switch back to the PowerShell window from the previous step.
  • Run the naive router by entering the command below:
python my-script.py

What Should You See?

You will see Naive route for T-103: billing_queue in PowerShell.

The simulated intent chooses billing. The route map therefore returns billing_queue.

The ticket mentions an account problem plus a recent charge. Its simulated intent confidence is only 0.10.

The router ignores that uncertainty. This unsafe automatic route is the intended shortfall.

Script Not Printing the Route?

  • Confirm that PowerShell is still inside the jev-decision-lab folder.
  • Confirm that fixtures.py plus my-script.py appear together in Visual Studio Code.
  • Compare both files with their full-code references to catch a missing quote or bracket.
  • Help me debug my offline ticket router.

You have made the router's weak spot visible. Next, you will make its policy stop when the intent evidence is too uncertain.

Stop Guessing When Confidence Is Low

Your naive router exposed the risk of trusting a winning Jev Choice answer on its own. The ambiguous T-103 fixture reached billing_queue even though its intent confidence is 0.10.

Confidence-Gated Routing uses confidence as a second decision axis. Your code can send uncertain cases to human_review before it selects an automatic queue.

In this step, get ready to:
  • Centralize the existing routing policy in router.py.
  • Add a confidence gate that sends uncertain intent answers to human review.
  • Compare the naive decision with the safe decision for every fixture.
Create the routing module

A dedicated routing module gives the decision policy one inspectable home. The runner can focus on pairing tickets with responses.

  • In the file sidebar of the Visual Studio Code window from earlier, click the new-file icon.
  • Enter router.py in the filename field.
  • Press Enter to create the file.
  • Place the existing route map and naive function in router.py by pasting this code:
ROUTES = {
    "billing": "billing_queue",
    "technical": "technical_queue",
    "account": "account_queue",
}


def naive_destination(response):
    intent = response["answers"]["intent"]["choice"]
    return ROUTES[intent]

What Does This Code Preserve?

  • ROUTES keeps every automatic destination under the application's control.
  • naive_destination() preserves the earlier Choice-only policy for comparison.
  • The naive policy still selects a queue without checking uncertainty.
  • Save router.py.
  • Confirm that router.py appears beside fixtures.py in the file sidebar.

File Missing from the Sidebar?

  • Check that you created router.py inside the open jev-decision-lab folder.
  • Help me find the new routing file.

The complete intent answer contains the selected option plus its confidence. The new function uses both values before choosing a destination.

  • Add the confidence-gated function below naive_destination() by pasting this code:
def route_ticket(ticket, response, confidence_floor=0.50):
    intent = response["answers"]["intent"]

    if intent["confidence"] < confidence_floor:
        destination = "human_review"
    else:
        destination = ROUTES[intent["choice"]]

    return {
        "ticket_id": ticket["id"],
        "destination": destination,
        "intent_confidence": intent["confidence"],
    }

How Does the Confidence Gate Work?

  • intent holds the complete Choice answer for the ticket.
  • confidence_floor=0.50 defines the minimum confidence required by this project policy.
  • The low-confidence branch selects human_review.
  • The return value exposes the selected destination for inspection.
  • Save router.py.
  • Confirm that route_ticket() appears below naive_destination().

Confidence Gate Looking Incomplete?

  • Check that the low-confidence branch assigns human_review to destination.
  • Check that the automatic branch reads intent["choice"] from the complete intent answer.
  • Help me check my confidence-gated function.
Update the script to compare decisions

Both policies need the same fixture data for a fair comparison. The runner will keep the naive failure visible before printing the safe decision for each ticket.

  • Select my-script.py in the file sidebar.
  • Replace its contents with this code:
from fixtures import SIMULATED_RESPONSES, TICKETS
from router import naive_destination, route_ticket


def main():
    ambiguous_response = SIMULATED_RESPONSES["T-103"]
    print(f"Naive route for T-103: {naive_destination(ambiguous_response)}")

    print("Safe decisions:")
    for ticket in TICKETS:
        decision = route_ticket(ticket, SIMULATED_RESPONSES[ticket["id"]])
        print(
            f"{decision['ticket_id']} -> {decision['destination']} | "
            f"intent confidence={decision['intent_confidence']:.2f}"
        )


if __name__ == "__main__":
    main()

What Does the Updated Runner Do?

  • The second import brings both routing functions into the runner.
  • The first print keeps the designed naive failure visible.
  • The loop pairs each ticket with its corresponding simulated response.
  • Each safe result displays the destination beside the intent confidence.
  • Save my-script.py.
  • Confirm that my-script.py imports both functions from router.py.

Import Line Not Matching?

  • Check that the import names match naive_destination and route_ticket exactly.
  • Confirm that router.py is saved beside my-script.py.
  • Help me fix the routing imports.
Run the confidence check

The same three fixtures now pass through both policies. This comparison shows whether the confidence gate preserves clear routes while stopping the ambiguous case.

  • Predict whether the confidence gate changes all three routes or one route.
  • Return to the PowerShell window from earlier.
  • Run both policies against the offline fixtures with this command:
python my-script.py

What Should I See?

  • The naive result remains Naive route for T-103: billing_queue.
  • The first safe result is T-101 -> billing_queue | intent confidence=0.88.
  • The second safe result is T-102 -> technical_queue | intent confidence=0.70.
  • The ambiguous result changes to T-103 -> human_review | intent confidence=0.10.

You have closed the unsafe automation gap. The selected intent now controls a route only when its confidence clears the policy floor.

Routes Not Matching?

  • Confirm that router.py is saved in the same jev-decision-lab folder as my-script.py.
  • Check that the low-confidence condition compares intent["confidence"] with confidence_floor.
  • Check that the loop passes the matching response into route_ticket().
  • Help me debug the confidence-gated routes.

✔️ Awesome, I've got everything!

Great. Double-check that both updated files are saved.

ⓧ I'd like to double check the full code

  • Compare your saved router.py with this complete reference:
ROUTES = {
    "billing": "billing_queue",
    "technical": "technical_queue",
    "account": "account_queue",
}


def naive_destination(response):
    intent = response["answers"]["intent"]["choice"]
    return ROUTES[intent]


def route_ticket(ticket, response, confidence_floor=0.50):
    intent = response["answers"]["intent"]

    if intent["confidence"] < confidence_floor:
        destination = "human_review"
    else:
        destination = ROUTES[intent["choice"]]

    return {
        "ticket_id": ticket["id"],
        "destination": destination,
        "intent_confidence": intent["confidence"],
    }
  • Compare your saved my-script.py with this complete reference:
from fixtures import SIMULATED_RESPONSES, TICKETS
from router import naive_destination, route_ticket


def main():
    ambiguous_response = SIMULATED_RESPONSES["T-103"]
    print(f"Naive route for T-103: {naive_destination(ambiguous_response)}")

    print("Safe decisions:")
    for ticket in TICKETS:
        decision = route_ticket(ticket, SIMULATED_RESPONSES[ticket["id"]])
        print(
            f"{decision['ticket_id']} -> {decision['destination']} | "
            f"intent confidence={decision['intent_confidence']:.2f}"
        )


if __name__ == "__main__":
    main()

Your router now treats uncertainty as an input to application policy. Next, you will combine intent with frustration, urgency, and refund signals.

Compose Atomic Decisions in Code

Your confidence gate now keeps the ambiguous ticket out of an automatic queue. The router still bases each decision on one broad judgment.

A useful Jev workflow breaks that decision into atomic questions over the same structured state. Each answer stays focused on one judgment.

This step prepares one Choice judgment. It also prepares one Score judgment plus two Noul judgments. Python keeps every weight and routing action in deterministic code.

In this step, get ready to:
  • Define a Jev-style request with four atomic questions.
  • Add composite priority scoring in deterministic Python code.
  • Display the prepared primitive types plus the final routing decisions.
Define the atomic question set

The request prepares every judgment the router could need against one ticket state. This creates an inspectable question set before any route is selected.

  • In Visual Studio Code, return to router.py.
  • Replace the opening ROUTES block with this request setup:
QUESTIONS = {
    "intent": {"type": "choice", "instructions": "Which team should handle `ticket.message`?", "criteria": {"billing": "Charges, invoices, payments, or refunds", "technical": "Broken features, bugs, or integrations", "account": "Login, profile, permissions, or account access"}},
    "frustration": {"type": "score", "instructions": "How frustrated does the customer appear in `ticket.message`?", "criteria": ["Calm and factual", "Frustrated but civil", "Very frustrated or threatening to leave"]},
    "is_urgent": {"type": "noul", "instructions": "Does `ticket.message` express urgency or time pressure?"},
    "refund_requested": {"type": "noul", "instructions": "Does `ticket.message` explicitly request a refund?"},
}

ROUTES = {"billing": "billing_queue", "technical": "technical_queue", "account": "account_queue"}


def build_request(state):
    return {"state": state, "questions": QUESTIONS}

What does this request setup do?

  • The intent question uses Choice to select a team.
  • The frustration question uses Score to measure a separate priority signal.
  • The two Noul questions express yes probabilities for urgency and refund intent.
  • The build_request() function packages the ticket state with every question.

Why prepare every question together?

This follows Speculative Fan-Out. The request contains the questions the workflow may need.

Python decides which answers matter after the structured results arrive. The application keeps control of every route.

  • Save router.py.

Before you run the router, do you expect these question definitions to change the existing destinations?

  • Return to PowerShell and check the current behavior by running:
python my-script.py

What should you see?

You should see the same naive and confidence-gated routes from the previous step. That confirms the new definitions have not disturbed the working safety gate.

Seeing a Python syntax error?

Check that every question entry ends with a comma. Confirm that the closing braces around QUESTIONS remain paired.

If the error points near build_request(), confirm that the function begins at the left edge of router.py.

Help me inspect this syntax error.

The request builder now exists in router.py. The command-line demo needs to call it so you can inspect the prepared primitive types.

  • In my-script.py, replace everything from the imports through the ambiguous_response assignment with this code:
from fixtures import SIMULATED_RESPONSES, TICKETS
from router import build_request, naive_destination, route_ticket


def main():
    sample_request = build_request(TICKETS[0]["state"])
    primitive_types = ", ".join(question["type"] for question in sample_request["questions"].values())
    print(f"Prepared one Jev-style request with: {primitive_types}")
    ambiguous_response = SIMULATED_RESPONSES["T-103"]

What does this display code do?

  • The sample_request variable applies the full question set to the first ticket state.
  • The primitive_types expression reads each question type in insertion order.
  • The first printed line makes the prepared Choice, Score, and Noul questions visible.
  • Save my-script.py.

Before you run the script, which four primitive names do you expect the first line to list?

  • Display the prepared question types by running:
python my-script.py

What should you see now?

The first line should read Prepared one Jev-style request with: choice, score, noul, noul. The existing route lines should still follow it.

Good progress. Your four independent judgments are now visible from one structured request.

Missing the prepared request line?

Confirm that build_request appears in the import from router. Check that the three new lines sit inside main().

Help me trace the missing output.

Combine the answers with Python policy

Composite Scoring keeps frustration and urgency as separate signals before applying explicit weights. The router can now calculate priority without hiding policy inside a broad judgment.

The same function keeps confidence gating ahead of every automated route. Refund behavior stays inside the billing branch where it belongs.

  • In router.py, replace the existing route_ticket() function with this scoring function plus the completed route function:
def priority_score(answers):
    normalized_frustration = answers["frustration"]["score"] / 2
    urgency = answers["is_urgent"]["noul"]
    return round(0.60 * normalized_frustration + 0.40 * urgency, 2)


def route_ticket(ticket, response, confidence_floor=0.50):
    answers = response["answers"]
    intent = answers["intent"]
    score = priority_score(answers)

    if intent["confidence"] < confidence_floor:
        destination = "human_review"
    else:
        destination = ROUTES[intent["choice"]]
        if intent["choice"] == "billing" and answers["refund_requested"]["noul"] >= 0.70:
            destination = "billing_refund_queue"

    return {"ticket_id": ticket["id"], "destination": destination, "priority": "high" if score >= 0.65 else "normal", "priority_score": score, "intent_confidence": intent["confidence"]}

How does the policy work?

  • The priority_score() function normalizes the frustration score to a zero-to-one range.
  • Python weights normalized frustration at 0.60.
  • Python weights urgency at 0.40.
  • The route becomes high priority when the combined score reaches 0.65.
  • A confident billing answer reaches billing_refund_queue when its refund Noul reaches 0.70.
  • Save router.py.

Before you run the script, which clear ticket should move from the general billing queue to the refund queue?

  • Check the completed routing policy by running:
python my-script.py

What changed?

You should see T-101 move to billing_refund_queue. Its confident billing intent allows code to inspect the refund signal.

You should still see T-102 in technical_queue. You should still see T-103 in human_review.

Seeing a missing answer error?

Confirm that priority_score() reads frustration and is_urgent from the answers dictionary. Check those spellings against fixtures.py.

Help me trace the missing fixture key.

Display and verify every decision

The policy now calculates a priority label plus its numeric score. The current terminal line only displays destination and intent confidence.

  • In my-script.py, find the multiline print() block inside the for ticket in TICKETS loop.
  • Replace that block with this complete decision line:
        print(f"{decision['ticket_id']} -> {decision['destination']} | priority={decision['priority']} | score={decision['priority_score']:.2f} | intent confidence={decision['intent_confidence']:.2f}")

Why show every field?

The line exposes the destination plus every policy value returned by route_ticket(). You can now inspect the route and the priority calculation in one place.

  • Save my-script.py.

Before the final run, predict which ticket has high priority. Also predict whether the ambiguous ticket remains under human control.

  • Verify the complete offline workflow by running:
python my-script.py

What should the final run show?

  • You should see Prepared one Jev-style request with: choice, score, noul, noul first.
  • You should see Naive route for T-103: billing_queue preserve the designed failure.
  • You should see T-101 reach billing_refund_queue with high priority.
  • You should see T-102 reach technical_queue with normal priority.
  • You should see T-103 stop at human_review with normal priority.

Final output missing priority values?

Confirm that the replacement print() line remains inside the loop. Check that priority and priority_score match the keys returned by route_ticket().

Help me compare the decision output fields.

That is the complete decision layer working. Four narrow judgments now feed transparent Python policy without making an API call.

✔️ Awesome, I've got everything!

Your offline request shape and deterministic routing policy are complete. The final command confirms every expected decision.

ⓧ I'd like to double check the full code

Compare your three files with these complete versions.

fixtures.py.

"""Simulated Jev-shaped responses for an offline architecture lab.

These values are educational fixtures, not recorded Jev evaluations and not
claims about Jev's accuracy on these tickets.
"""

TICKETS = [
    {
        "id": "T-101",
        "state": {"ticket": {"message": "I was charged twice. Please refund the duplicate today."}},
    },
    {
        "id": "T-102",
        "state": {"ticket": {"message": "The export button fails, but I can still download CSV files."}},
    },
    {
        "id": "T-103",
        "state": {"ticket": {"message": "Something is wrong with my account and a recent charge."}},
    },
]

SIMULATED_RESPONSES = {
    "T-101": {"answers": {"intent": {"type": "choice", "choice": "billing", "probabilities": {"billing": 0.92, "technical": 0.05, "account": 0.03}, "confidence": 0.88}, "frustration": {"type": "score", "score": 2.0, "legend": {"0": "Calm and factual", "1": "Frustrated but civil", "2": "Very frustrated or threatening to leave"}, "probabilities": {"0": 0.02, "1": 0.18, "2": 0.80}, "confidence": 0.67}, "is_urgent": {"type": "noul", "noul": 0.90}, "refund_requested": {"type": "noul", "noul": 0.98}}},
    "T-102": {"answers": {"intent": {"type": "choice", "choice": "technical", "probabilities": {"billing": 0.05, "technical": 0.80, "account": 0.15}, "confidence": 0.70}, "frustration": {"type": "score", "score": 0.0, "legend": {"0": "Calm and factual", "1": "Frustrated but civil", "2": "Very frustrated or threatening to leave"}, "probabilities": {"0": 0.80, "1": 0.18, "2": 0.02}, "confidence": 0.67}, "is_urgent": {"type": "noul", "noul": 0.30}, "refund_requested": {"type": "noul", "noul": 0.05}}},
    "T-103": {"answers": {"intent": {"type": "choice", "choice": "billing", "probabilities": {"billing": 0.40, "technical": 0.35, "account": 0.25}, "confidence": 0.10}, "frustration": {"type": "score", "score": 1.0, "legend": {"0": "Calm and factual", "1": "Frustrated but civil", "2": "Very frustrated or threatening to leave"}, "probabilities": {"0": 0.15, "1": 0.70, "2": 0.15}, "confidence": 0.55}, "is_urgent": {"type": "noul", "noul": 0.50}, "refund_requested": {"type": "noul", "noul": 0.55}}},
}

What belongs in this file?

This file keeps the three ticket states beside their simulated Jev-shaped answers. The fixtures remain explicitly educational and offline.

router.py.

QUESTIONS = {
    "intent": {"type": "choice", "instructions": "Which team should handle `ticket.message`?", "criteria": {"billing": "Charges, invoices, payments, or refunds", "technical": "Broken features, bugs, or integrations", "account": "Login, profile, permissions, or account access"}},
    "frustration": {"type": "score", "instructions": "How frustrated does the customer appear in `ticket.message`?", "criteria": ["Calm and factual", "Frustrated but civil", "Very frustrated or threatening to leave"]},
    "is_urgent": {"type": "noul", "instructions": "Does `ticket.message` express urgency or time pressure?"},
    "refund_requested": {"type": "noul", "instructions": "Does `ticket.message` explicitly request a refund?"},
}

ROUTES = {"billing": "billing_queue", "technical": "technical_queue", "account": "account_queue"}


def build_request(state):
    return {"state": state, "questions": QUESTIONS}


def naive_destination(response):
    intent = response["answers"]["intent"]["choice"]
    return ROUTES[intent]


def priority_score(answers):
    normalized_frustration = answers["frustration"]["score"] / 2
    urgency = answers["is_urgent"]["noul"]
    return round(0.60 * normalized_frustration + 0.40 * urgency, 2)


def route_ticket(ticket, response, confidence_floor=0.50):
    answers = response["answers"]
    intent = answers["intent"]
    score = priority_score(answers)

    if intent["confidence"] < confidence_floor:
        destination = "human_review"
    else:
        destination = ROUTES[intent["choice"]]
        if intent["choice"] == "billing" and answers["refund_requested"]["noul"] >= 0.70:
            destination = "billing_refund_queue"

    return {"ticket_id": ticket["id"], "destination": destination, "priority": "high" if score >= 0.65 else "normal", "priority_score": score, "intent_confidence": intent["confidence"]}

What belongs in this policy file?

This file owns the atomic question set and request builder. It also owns every route and scoring threshold.

my-script.py.

from fixtures import SIMULATED_RESPONSES, TICKETS
from router import build_request, naive_destination, route_ticket


def main():
    sample_request = build_request(TICKETS[0]["state"])
    primitive_types = ", ".join(question["type"] for question in sample_request["questions"].values())
    print(f"Prepared one Jev-style request with: {primitive_types}")
    ambiguous_response = SIMULATED_RESPONSES["T-103"]
    print(f"Naive route for T-103: {naive_destination(ambiguous_response)}")
    print("Safe decisions:")
    for ticket in TICKETS:
        decision = route_ticket(ticket, SIMULATED_RESPONSES[ticket["id"]])
        print(f"{decision['ticket_id']} -> {decision['destination']} | priority={decision['priority']} | score={decision['priority_score']:.2f} | intent confidence={decision['intent_confidence']:.2f}")


if __name__ == "__main__":
    main()

What belongs in this runner?

This file prepares one sample request and preserves the naive failure. It then prints every confidence-gated route plus its priority result.

Your router now exposes each judgment plus every deterministic policy decision. Next, you will lock this behavior in with automated tests.

Lock In the Decision Policy with Tests

Your offline router now turns simulated Jev answers into clear routing decisions. The remaining risk is that a future policy change could quietly break a case that works today.

Python's built-in unit testing tools let you preserve known outcomes as repeatable checks. These tests protect the ambiguity gate, the refund path, and the question structure.

In this step, get ready to:
  • Create regression tests for the routing policy.
  • Run the unit test suite and inspect each named result.
  • Confirm that the tested behavior still appears in the command-line demo.
Create tests for the routing policy

Each test provides a known ticket fixture to the router. An assertion checks whether the resulting decision still matches the policy you designed.

  • Select the jev-decision-lab folder in Visual Studio Code's file sidebar.
  • Use the new-file button in the file sidebar.
  • Enter test_router.py as the file name.
  • Press Enter to create the file.
  • Add the first policy test by pasting this code into test_router.py:
import unittest

from fixtures import SIMULATED_RESPONSES, TICKETS
from router import QUESTIONS, build_request, route_ticket


class RoutingTests(unittest.TestCase):
    def test_ambiguous_ticket_goes_to_human_review(self):
        decision = route_ticket(TICKETS[2], SIMULATED_RESPONSES["T-103"])
        self.assertEqual(decision["destination"], "human_review")

What does this test protect?

  • The imports give the test access to the existing fixtures and routing functions.
  • The RoutingTests class groups checks for the decision policy.
  • The first test passes T-103 through route_ticket().
  • The assertion protects human_review as the safe result for that ambiguous ticket.
  • Save test_router.py.
  • Return to PowerShell from earlier.
  • Run the first regression test with this command:
python -m unittest -v

What does this command do?

The unittest module discovers test methods in test_router.py. The -v option displays each test name separately.

You should see one passing test followed by OK. Your ambiguity gate now has an automated safety check.

Test not discovered?

  • Confirm that PowerShell is still using the jev-decision-lab folder.
  • Check that the file is named test_router.py.
  • Check that the method name begins with test_.
  • Help me troubleshoot why Python is not discovering my unit test.

The ambiguity test protects the safest branch. The next test protects the clear billing case that should continue through automation.

  • Place your cursor on a blank line below the first test method inside RoutingTests.
  • Add the refund routing test by pasting this code:
    def test_clear_billing_refund_uses_refund_queue(self):
        decision = route_ticket(TICKETS[0], SIMULATED_RESPONSES["T-101"])
        self.assertEqual(decision["destination"], "billing_refund_queue")
        self.assertEqual(decision["priority"], "high")

What does this test protect?

  • This test passes the clear billing fixture T-101 through the same routing function.
  • The first assertion protects billing_refund_queue as the destination.
  • The second assertion protects high as the priority.
  • Save test_router.py.
  • Run both policy tests with this command:
python -m unittest -v

What does this run prove?

The test runner discovers both methods because each name starts with test_. A passing result proves the human-review path and the clear refund path still behave as specified.

You should see two passing tests followed by OK. Both sides of the confidence gate are now protected.

Seeing a failed assertion?

  • Compare the fixture references with TICKETS[0] and SIMULATED_RESPONSES["T-101"].
  • Confirm that both new lines remain indented inside test_clear_billing_refund_uses_refund_queue().
  • Help me diagnose a failed billing refund routing test.

Routing outcomes are only part of the contract. The final test protects the atomic question set that supplies the router's structured inputs.

  • Place your cursor on a blank line below the refund routing test inside RoutingTests.
  • Finish test_router.py by pasting this code:
    def test_request_contains_all_three_primitives(self):
        request = build_request(TICKETS[0]["state"])
        primitive_types = {question["type"] for question in request["questions"].values()}
        self.assertEqual(primitive_types, {"choice", "score", "noul"})
        self.assertEqual(request["questions"], QUESTIONS)


if __name__ == "__main__":
    unittest.main()

What does this test protect?

  • The test builds a request from the structured state in T-101.
  • The set comprehension collects the distinct primitive types from all four questions.
  • The first assertion requires choice, score, and noul to remain present.
  • The second assertion requires the request's questions to match QUESTIONS exactly.
  • The final guard lets the file start the test runner when executed directly.
  • Save the completed test_router.py file.

✔️ Awesome, I've got everything!

Good. Your three policy tests are saved in test_router.py.

ⓧ I'd like to double check the full code

The completed test_router.py file should match this reference.

import unittest

from fixtures import SIMULATED_RESPONSES, TICKETS
from router import QUESTIONS, build_request, route_ticket


class RoutingTests(unittest.TestCase):
    def test_ambiguous_ticket_goes_to_human_review(self):
        decision = route_ticket(TICKETS[2], SIMULATED_RESPONSES["T-103"])
        self.assertEqual(decision["destination"], "human_review")

    def test_clear_billing_refund_uses_refund_queue(self):
        decision = route_ticket(TICKETS[0], SIMULATED_RESPONSES["T-101"])
        self.assertEqual(decision["destination"], "billing_refund_queue")
        self.assertEqual(decision["priority"], "high")

    def test_request_contains_all_three_primitives(self):
        request = build_request(TICKETS[0]["state"])
        primitive_types = {question["type"] for question in request["questions"].values()}
        self.assertEqual(primitive_types, {"choice", "score", "noul"})
        self.assertEqual(request["questions"], QUESTIONS)


if __name__ == "__main__":
    unittest.main()

How to use this reference

This reference combines all three test chunks in their final order. The blank lines and indentation keep every method inside RoutingTests.

Run the complete test suite

Test discovery runs every method whose name begins with test_. Verbose output lets you connect each passing result to one protected behavior.

Before you run the suite, predict whether all three policy checks pass.

  • Run the complete suite from PowerShell with this command:
python -m unittest -v

What does the complete suite check?

  • The ambiguity test checks that T-103 reaches human_review.
  • The refund test checks that T-101 reaches billing_refund_queue with high priority.
  • The primitive test checks that build_request() includes Choice, Score, and Noul questions.

You should see all three named tests report passing results. The final status should be OK.

That is the policy locked in. Your known clear and ambiguous cases now have repeatable protection.

Suite not passing?

  • Confirm that test_router.py is saved inside jev-decision-lab.
  • Compare each method's indentation with the full-file reference.
  • Confirm that fixtures.py and router.py remain unchanged from the previous step.
  • Help me troubleshoot my three failing or undiscovered unit tests.
Confirm the end-to-end behavior

The tests verify focused policy contracts. One final demo run confirms that those contracts still work together across every fixture.

Before you run the demo again, predict whether adding tests changed any routing decision.

  • Run the completed ticket router from PowerShell with this command:
python my-script.py

What should you see?

  • You'll see the prepared request list choice, score, noul, noul.
  • You'll see the naive route send T-103 to billing_queue.
  • You'll see the safe route send T-101 to billing_refund_queue with high priority.
  • You'll see T-102 reach technical_queue.
  • You'll see T-103 stop at human_review.

Demo behavior changed?

  • Confirm that the tests only added test_router.py.
  • Restore fixtures.py, router.py, or my-script.py from the previous step if one was edited accidentally.
  • Help me compare my passing unit tests with unexpected output from my ticket router.

Strong finish. Your offline decision layer now proves its ambiguity handling, refund routing, and atomic question coverage every time the suite runs.

Secret mission

Make Confidence Thresholds Risk-Sensitive

A single confidence floor treats every routing mistake as equally costly. Extend the policy with intent-specific floors so account-access decisions require more certainty before automation.

Clean Up Your Resources

Clean Up Your Resources

Your project lives locally inside jev-decision-lab. It makes no API calls, so there are no ongoing costs or cloud resources to manage.

Your options are to keep the folder ready, close your local tools for now, or delete the folder entirely.

Resources you used:

  • The local jev-decision-lab folder. It contains fixtures.py, router.py, my-script.py, and test_router.py.

Keep everything running

No action is needed. This option keeps the offline router ready for more confidence-policy experiments.

  • Leave the jev-decision-lab folder in its current location.
  • Keep the four project files unchanged so the tested router remains ready to run.

Pause - I'll come back to this later

This option frees the memory used by your editor and shell while preserving every project file.

  • Close Visual Studio Code.
  • Close PowerShell.
  • Keep the jev-decision-lab folder in its current location for your next session.

Delete - I don't want to use this again

Deleting this folder permanently removes the offline router plus its tests. The commands below target only the local jev-decision-lab folder.

  • Close every Visual Studio Code window.
  • Switch back to PowerShell.
  • Delete the jev-decision-lab folder from its parent directory by running these commands:
Set-Location ..
Remove-Item -Recurse -Force .\jev-decision-lab

What Do These Commands Do?

The first command moves PowerShell one level above the project. The second command permanently removes the jev-decision-lab folder plus its contents.

  • Confirm that the project folder was removed by running this command:
Test-Path .\jev-decision-lab

What Should You See?

PowerShell should return a false result. This confirms that the project folder no longer exists in that location.

Is the Folder Still There?

If PowerShell reports that files are still in use, close any remaining editor windows. Run the deletion commands again.

Help me remove the local project folder safely.

Nice Work!

Nice Work!

You did it! You built an offline Jev-ready ticket router that converts structured judgments into tested routing decisions.

You've learned how to:

  • Built a runnable offline decision layer around one inspectable request using structured state. The workflow uses Choice for intent. It uses Score for frustration. Two Noul questions capture urgency plus refund intent.
  • Exposed the unsafe result of routing on a winning option alone. Rebuilt the policy with Intent Routing plus Confidence-Gated Routing. Added Composite Scoring plus Speculative Fan-Out under ordinary Python control flow.
  • Proved that T-103 reaches human_review while T-101 reaches billing_refund_queue with high priority. Locked the core policy into three passing unit tests covering ambiguity plus refund routing plus primitive coverage.
  • Completed the optional Secret Mission by replacing one global confidence floor with risk-sensitive thresholds. The account policy sends a 0.70-confidence access decision to human_review under its 0.80 floor. A fourth passing test proves the stricter rule.

Ready to quiz yourself?