Build a Jev-Powered Room Router

Build a vision-powered router that confidence-gates Jev room modes.

Introduction

30 Second Summary

A room can hold clues for work, rest, or emptiness. The difficult part is deciding when those clues are strong enough to trigger an action.

In this project, you will build a Windows command-line router that turns a room photo into structured evidence before asking Jev to choose a room mode. A confidence gate decides whether Python runs the selected action or sends the result to manual review.

What You'll Build

You'll run room.jpg through one workflow that saves annotated-room.jpg before printing Jev's room-mode probabilities and one controlled route.

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

  • An annotated room image where YOLO26n makes object labels and detection confidence scores visible.
  • A structured evidence trail that prints the JSON sent to Jev while keeping the source image local.
  • A confidence-gated route that reveals every Choice probability before exactly one simulated action or manual-review outcome.
  • Secret Mission: Stress-test the router with ambiguous-room.jpg to interpret Jev's probability spread against the confidence gate.

Are there any prerequisites?

You need a Windows computer with internet access plus one room image named room.jpg.

You also need an active TypeSafe early-access account that can create an API key.

Without provisioned TypeSafe access, this live-only project stops during setup because there is no mock fallback.

Before We Start

This checkpoint locks in the live Jev integration you are about to build with no mock fallback. The goal is a confidence-checked choice of focus, relax, vacant, or review from YOLO evidence.

Set Up Live Jev Access

Your router eventually sends a live request to Jev. That request needs an active TypeSafe credential.

An isolated Python environment keeps the project's pinned packages together. This step prepares the tools before proving your account can reach a live model.

In this step, get ready to:
  • Confirm the local development tools.
  • Create a TypeSafe API key.
  • Build a project environment that reaches live Jev.
Verify Python and Visual Studio Code

Python runs the TypeSafe SDK from your computer. Visual Studio Code gives you one place to edit the project while Windows PowerShell runs it.

  • Press the Windows key to open Windows Search.
  • Type Windows PowerShell into the search field.
  • Press Enter to launch PowerShell.
  • Check the installed Python version by running this command:
python --version

What does this command do?

The command prints the Python interpreter version that PowerShell launches. This project requires Python 3.10 or newer.

✔️ I see a supported Python version

Your output shows Python 3.10 or newer. Python is ready for the TypeSafe SDK.

ⓧ I see an older Python version

  • Open the official Python Windows guide.
  • Use the guide to install a current Python release.
  • Close PowerShell after the installation finishes.
  • Launch a new PowerShell window through Windows Search.
  • Repeat the Python version check above.

ⓧ Python is not found

  • Open the official Python Windows guide.
  • Install Python through the documented Windows installation path.
  • Close PowerShell after the installation finishes.
  • Launch a new PowerShell window through Windows Search.
  • Repeat the Python version check above.
  • Check whether the Visual Studio Code command is available by running:
code --version

What does this command do?

The command prints the installed Visual Studio Code version. A successful result also confirms that PowerShell can launch the editor.

✔️ I see a Visual Studio Code version

Visual Studio Code is installed. Its command is also available from PowerShell.

ⓧ The code command is not found

  • Open the official Visual Studio Code Windows setup guide.
  • Download the Windows User setup installer.
  • Run the downloaded installer.
  • Follow the installer prompts.
  • Close PowerShell after the installation finishes.
  • Launch a new PowerShell window through Windows Search.
  • Repeat the Visual Studio Code version check above.

That clears the local-tool hurdle. PowerShell can now reach Python plus Visual Studio Code.

Create your TypeSafe API key

The TypeSafe AI Console provides the API key that authenticates your live model requests. Jev is in early access, so your account must expose key creation before you continue.

This key can authorize live requests. Treat it like a password throughout the project.

  • Open the TypeSafe API key page in your browser.
  • Sign in with your TypeSafe early-access account.
  • Use the current console controls to create an API key.
  • Copy the new key when the console presents it.
  • Store the copied key in a password manager.
  • Return to the PowerShell window from earlier.
  • Replace your-api-key-here with the copied key in the command below.
  • Set the key for the current PowerShell session by running the edited command:
$env:TYPESAFE_API_KEY = "your-api-key-here"

What does this command do?

PowerShell stores the credential under TYPESAFE_API_KEY for the current process. The TypeSafe SDK reads that environment variable when it creates a client.

The assignment leaves your project files unchanged. Closing this PowerShell session removes the value from that session.

Cannot create a key?

Stop here if the keys page does not provide controls for creating a key. This project uses live Jev access with no mock fallback.

TypeSafe is granting Jev access through early access. Your account must be provisioned before the remaining checks can succeed.

Ask for help with checking your TypeSafe early-access status.

Build the project and confirm live access

A virtual environment gives this project its own package installation. That isolation keeps its pinned dependencies separate from other Python projects.

  • Return to the PowerShell window that contains your API key.
  • Prepare the jev-room-router workspace by running these commands:
cd ~/Desktop
mkdir jev-room-router
cd jev-room-router
code .

What do these commands do?

  • The first command moves PowerShell to your Desktop.
  • The second command creates the jev-room-router folder.
  • The third command moves PowerShell into that folder.
  • The final command opens the current folder in Visual Studio Code.

You'll see Visual Studio Code open with jev-room-router as the current folder. PowerShell remains inside the same folder.

Did the project folder fail to open?

Confirm that the Visual Studio Code version check succeeded earlier. Restart PowerShell if Visual Studio Code was installed while the terminal was open.

Ask for help with opening the project folder from PowerShell.

  • Prepare the isolated Python environment by running these commands:
python -m venv .venv
.venv\Scripts\Activate.ps1

What do these commands do?

Python creates the environment inside .venv. The activation script makes its Python interpreter the current terminal's default.

Your PowerShell prompt should now begin with the virtual-environment name in parentheses. That visible prefix confirms the environment is active.

Did PowerShell block activation?

PowerShell can block local scripts through its execution policy. Follow the activation guidance in the official Python virtual-environment documentation.

Restart PowerShell after changing the policy. Repeat the activation command from this section.

Ask for help with activating the virtual environment safely.

  • Click the New File control in the Visual Studio Code Explorer.
  • Enter requirements.txt in the filename field.
  • Press Enter to create the file.

You'll see a blank requirements.txt tab in the editor.

  • Add the pinned dependencies by pasting this package list:
typesafe-sdk==0.7.1
ultralytics==8.4.162

What does this file define?

  • The typesafe-sdk package is pinned to 0.7.1 for the live Jev request.
  • The ultralytics package is pinned to 8.4.162 for local object detection.
  • Press Ctrl+S to save requirements.txt.
  • Return to the PowerShell window inside jev-room-router.

This installation can take several minutes while Python downloads the pinned packages. A quiet pause during that download is normal.

  • Install the project dependencies by running:
python -m pip install -r requirements.txt

What does this command do?

Python runs pip from the active virtual environment. The requirements file supplies the exact package versions.

You'll see package download details in PowerShell. The prompt returns when the installation finishes.

Did the dependency installation fail?

  • Confirm that the PowerShell prompt shows the active virtual environment.
  • Confirm that requirements.txt contains both lines shown above.
  • Check that your internet connection is still active.

Ask for help with installing the pinned project dependencies.

✔️ Awesome, I've got everything!

Great. Make sure requirements.txt is saved before you continue.

ⓧ I'd like to double check the full code

typesafe-sdk==0.7.1
ultralytics==8.4.162

Reference file

This reference matches the complete requirements.txt file for this step.

  • Click the New File control in the Visual Studio Code Explorer.
  • Enter check_jev.py in the filename field.
  • Press Enter to create the file.

You'll see a blank check_jev.py tab beside the requirements file.

  • Add the live model-list check by pasting this script:
from typesafe_sdk import TypeSafeClient


with TypeSafeClient() as client:
    available_models = client.models.list()

for model in available_models.models:
    print(model.name, model.release_date, model.description)

What does this code do?

  • The TypeSafeClient reads the API key from the PowerShell environment.
  • The client.models.list() request asks the live service which models your account can access.
  • The final loop prints identifying details for every returned model.
  • Press Ctrl+S to save check_jev.py.

Before you run the check, do you expect your early-access account to return the Jev alias?

  • Run the live model-list check from PowerShell with this command:
python check_jev.py

What does this check prove?

The script must authenticate with your current TypeSafe API key before it can retrieve the model list. A returned list proves the SDK reached the live service.

You should see model details that include jev-latest. That alias confirms your account can access the Jev model used by this project.

You have cleared the live-access checkpoint. Your local project can now authenticate with TypeSafe.

Missing the live Jev model?

  • Confirm that this is the same PowerShell session where you set the API key.
  • Repeat the API-key assignment if you opened a new PowerShell window.
  • Confirm that the virtual environment is still active.
  • Stop here if the model list succeeds without showing jev-latest.

Ask for help with diagnosing the live model-list check.

✔️ Awesome, I've got everything!

Great. Make sure check_jev.py is saved before you finish this step.

ⓧ I'd like to double check the full code

from typesafe_sdk import TypeSafeClient


with TypeSafeClient() as client:
    available_models = client.models.list()

for model in available_models.models:
    print(model.name, model.release_date, model.description)

Reference file

This reference matches the complete check_jev.py file for this step.

  • Scroll the API-key assignment outside the visible PowerShell area before taking your screenshot.

Your live Jev connection is ready. Next, you will turn room.jpg into structured YOLO evidence.

Extract YOLO Room Evidence

The live access check proved your project can reach the service. Your router now needs a compact view of what appears in a room photo.

YOLO26n handles object detection locally. This step turns its results into structured JSON evidence.

In this step, get ready to:
  • Run local YOLO26n detection against your room image.
  • Aggregate repeated labels into compact evidence with maximum confidence scores.
  • Save an annotated image that shows what the detector found.
Prepare the local image pipeline

The detector receives a local image path. It loads the pretrained model before running inference on that image.

  • Copy room.jpg into the jev-room-router folder using File Explorer.
  • Return to the jev-room-router folder in VS Code.
  • Confirm room.jpg appears directly inside the folder in the Explorer sidebar.
  • Create room_router.py directly inside jev-room-router using the Explorer sidebar.
  • Add the detector imports with the local prediction function by pasting this code into room_router.py:
import argparse
import json
from pathlib import Path

from ultralytics import YOLO

YOLO_MODEL = "yolo26n.pt"
ANNOTATED_IMAGE = Path("annotated-room.jpg")


def detect_room(image_path: Path) -> dict[str, object]:
    model = YOLO(YOLO_MODEL)
    result = model.predict(
        source=str(image_path),
        conf=0.25,
        verbose=False,
    )[0]

What Does This Code Do?

  • YOLO_MODEL names the pretrained weights used for detection.
  • ANNOTATED_IMAGE fixes the output path for the visual result.
  • model.predict passes the local image into the detector.
  • conf=0.25 sets the minimum detection confidence.

Ultralytics downloads yolo26n.pt automatically when the weights are missing. The prediction still runs on your computer.

  • Save room_router.py.
  • Confirm room_router.py appears beside room.jpg in the Explorer sidebar.

Seeing an Import Warning?

  • Confirm the selected Python interpreter points to .venv inside jev-room-router.
  • Return to the activated PowerShell terminal from the previous step before running the file.

Help me fix the unresolved Ultralytics import in this project.

Build compact object evidence

A detector can return several boxes for the same label. The router condenses those boxes into one entry per label.

  • In room_router.py, find the line that ends the prediction call: )[0].
  • Leave one blank line after )[0].
  • Complete detect_room() by pasting this code on the next line:
    objects: dict[str, dict[str, float | int]] = {}
    for box in result.boxes:
        class_id = int(box.cls.item())
        label = result.names[class_id]
        confidence = float(box.conf.item())
        entry = objects.setdefault(label, {"count": 0, "max_confidence": 0.0})
        entry["count"] = int(entry["count"]) + 1
        entry["max_confidence"] = max(float(entry["max_confidence"]), confidence)

    detected_objects = [
        {
            "label": label,
            "count": values["count"],
            "max_yolo_confidence": round(float(values["max_confidence"]), 3),
        }
        for label, values in sorted(objects.items())
    ]

    result.save(filename=str(ANNOTATED_IMAGE))
    return {
        "image_name": image_path.name,
        "detected_objects": detected_objects,
    }

How Does the Evidence Take Shape?

  • result.boxes.cls provides the detected class IDs.
  • result.boxes.conf provides the confidence score for each box.
  • result.names converts each class ID into a readable label.
  • objects stores one count with one maximum confidence per label.
  • detected_objects converts the grouped results into a sorted list for the final room state.

The result.save call writes the annotated image to annotated-room.jpg. The returned dictionary stays small enough for downstream decision code.

  • Save room_router.py.
  • Confirm the aggregation block remains indented inside detect_room().

Seeing an Indentation Error?

  • Confirm every pasted line begins with four spaces.
  • Keep the new block inside detect_room().
  • Keep the closing brace of the returned dictionary aligned with return.

Help me correct the indentation in detect_room().

Run the detect-only checkpoint

The command-line entry point validates the image path before detection begins. It prints the exact structured state produced from the room photo.

  • Leave two blank lines after detect_room().
  • Add the argument parser with the detector output by pasting this code below those blank lines:
def main() -> None:
    parser = argparse.ArgumentParser(
        description="Extract YOLO evidence from a room image."
    )
    parser.add_argument("image", type=Path, help="Path to a room image")
    parser.add_argument(
        "--detect-only",
        action="store_true",
        help="Run YOLO and stop before any decision request",
    )
    args = parser.parse_args()

    if not args.image.is_file():
        raise SystemExit(f"Image not found: {args.image}")

    room_state = detect_room(args.image)
    print("YOLO evidence:")
    print(json.dumps(room_state, indent=2))
    print(f"Annotated image: {ANNOTATED_IMAGE}")
    print("LIMITATION: Object detections alone do not select a room mode.")


if __name__ == "__main__":
    main()

What Does the Entry Point Control?

  • argparse requires an image path from the command line.
  • --detect-only identifies this run as the local evidence checkpoint.
  • room_state holds the compact dictionary returned by detect_room().
  • json.dumps formats the evidence so each field is easy to inspect.
  • Save room_router.py.

Before you run this, predict whether detected object labels can select one room mode without any decision logic.

  • Run the detector-only workflow in the activated PowerShell terminal by running this command:
python room_router.py room.jpg --detect-only

What Should You See?

Your terminal prints YOLO evidence: followed by room-specific JSON. It also confirms the path to annotated-room.jpg.

The final line reads LIMITATION: Object detections alone do not select a room mode. This is the intended boundary between visual evidence and a software decision.

  • Select annotated-room.jpg in the VS Code Explorer sidebar.
  • Inspect the image for bounding boxes with labels and confidence scores.

You can now compare the marked objects with the structured terminal output. The same detections appear in a compact format that code can consume.

Missing the Annotated Image?

  • Confirm the source image is named exactly room.jpg.
  • Confirm room.jpg sits directly inside jev-room-router.
  • Run the command from the activated terminal that already points to jev-room-router.

Help me troubleshoot why annotated-room.jpg was not created.

✔️ Awesome, I've got everything!

Your room_router.py now matches the complete detector-only checkpoint.

ⓧ I'd like to double check the full code

import argparse
import json
from pathlib import Path

from ultralytics import YOLO

YOLO_MODEL = "yolo26n.pt"
ANNOTATED_IMAGE = Path("annotated-room.jpg")


def detect_room(image_path: Path) -> dict[str, object]:
    model = YOLO(YOLO_MODEL)
    result = model.predict(
        source=str(image_path),
        conf=0.25,
        verbose=False,
    )[0]

    objects: dict[str, dict[str, float | int]] = {}
    for box in result.boxes:
        class_id = int(box.cls.item())
        label = result.names[class_id]
        confidence = float(box.conf.item())
        entry = objects.setdefault(label, {"count": 0, "max_confidence": 0.0})
        entry["count"] = int(entry["count"]) + 1
        entry["max_confidence"] = max(float(entry["max_confidence"]), confidence)

    detected_objects = [
        {
            "label": label,
            "count": values["count"],
            "max_yolo_confidence": round(float(values["max_confidence"]), 3),
        }
        for label, values in sorted(objects.items())
    ]

    result.save(filename=str(ANNOTATED_IMAGE))
    return {
        "image_name": image_path.name,
        "detected_objects": detected_objects,
    }


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Extract YOLO evidence from a room image."
    )
    parser.add_argument("image", type=Path, help="Path to a room image")
    parser.add_argument(
        "--detect-only",
        action="store_true",
        help="Run YOLO and stop before any decision request",
    )
    args = parser.parse_args()

    if not args.image.is_file():
        raise SystemExit(f"Image not found: {args.image}")

    room_state = detect_room(args.image)
    print("YOLO evidence:")
    print(json.dumps(room_state, indent=2))
    print(f"Annotated image: {ANNOTATED_IMAGE}")
    print("LIMITATION: Object detections alone do not select a room mode.")


if __name__ == "__main__":
    main()

How to Use This Reference

This reference shows the complete room_router.py file at the end of this step. It preserves the detector-only boundary you just observed.

You now have a local detector that turns a room photo into compact evidence. Next, you'll ask Jev to make a bounded room-mode Choice from that evidence.

Ask Jev for a Room Mode

Your local YOLO pipeline now turns a room photo into compact evidence. Its detect-only run exposed the missing semantic decision.

Jev closes that gap through a bounded Choice question. Each response includes a selected label plus a complete probability distribution.

Only the compact structured state leaves your computer. The source image stays inside the local detection pipeline.

In this step, get ready to:
  • Define the fixed room-mode options through a Jev Choice.
  • Send the compact YOLO evidence through the TypeSafe Python SDK.
  • Inspect the selected mode plus its live decision data.
Define the bounded room-mode Choice

A bounded Choice defines the only valid answers Jev can select. Each criterion describes the evidence that supports one room mode.

Why use a Jev Choice?

A hard-coded rules engine would create a brittle mapping between object labels and room modes. A Jev Choice evaluates the complete evidence against four explicit outcomes.

The TypeSafe Python SDK also handles the typed request through regular Python objects. This keeps the project focused on the decision boundary.

  • Find the block from from ultralytics import YOLO through ANNOTATED_IMAGE = Path("annotated-room.jpg") in room_router.py.
  • Replace that block with this code:
from typesafe_sdk import Choice, TypeSafeClient
from ultralytics import YOLO

JEV_MODEL = "jev-1.13.0"
YOLO_MODEL = "yolo26n.pt"
ANNOTATED_IMAGE = Path("annotated-room.jpg")

What does this code add?

  • The TypeSafe import provides the question type plus the synchronous client.
  • JEV_MODEL pins each request to jev-1.13.0.
  • The existing YOLO model plus annotated-image path stay unchanged.
  • Save room_router.py.
  • Confirm that the new SDK import loads by running the detection-only command:
python room_router.py room.jpg --detect-only

What should you see?

You should see the structured YOLO evidence followed by the existing limitation message. This confirms that the TypeSafe import loads without disrupting local detection.

Does the TypeSafe import fail?

  • Check that your activated .venv still appears at the start of the PowerShell prompt.
  • Check that typesafe-sdk==0.7.1 remains in requirements.txt.

Help me diagnose the TypeSafe SDK import in my virtual environment.

  • Find ANNOTATED_IMAGE = Path("annotated-room.jpg") in room_router.py.
  • Add the fixed room-mode question below that line by pasting this code:
ROOM_MODE_QUESTION = Choice(
    instructions=(
        "Choose the safest room mode from the YOLO object-detection summary. "
        "Treat missing detections as uncertainty rather than proof."
    ),
    criteria={
        "focus": (
            "A person is present and the detected objects suggest focused desk, "
            "study, reading, or computer work."
        ),
        "relax": (
            "A person is present and the detected objects suggest resting, "
            "conversation, or entertainment."
        ),
        "vacant": (
            "The detections provide good evidence that no person is present, "
            "so the room can enter an energy-saving mode."
        ),
        "review": (
            "The detections are empty, weak, conflicting, or insufficient to "
            "choose another mode safely."
        ),
    },
)

How is the Choice bounded?

  • The instructions ask Jev to choose the safest mode from the object summary.
  • The criteria limit valid answers to focus, relax, vacant, or review.
  • The review criterion gives weak evidence a safe destination.
  • The vacant criterion requires positive evidence before selecting an energy-saving mode.
  • Save room_router.py.
  • Confirm that Python constructs the Choice by running the local detection path again:
python room_router.py room.jpg --detect-only

What does this check prove?

You should reach the limitation message again. Reaching that output proves that Python loaded the new Choice definition successfully.

Does the Choice stop the script?

  • Check that every room-mode label remains inside the criteria mapping.
  • Compare each closing parenthesis with the code block above.

Help me find the syntax problem in my ROOM_MODE_QUESTION definition.

Send structured evidence to Jev

The detector already returns a compact dictionary containing the image name plus detected objects. The request function passes that dictionary directly as Jev state.

  • Find the end of detect_room() in room_router.py.
  • Add this request function between detect_room() and main() by pasting the code below:
def ask_jev(room_state: dict[str, object]) -> tuple[str, float, dict[str, float], str]:
    with TypeSafeClient(model=JEV_MODEL) as client:
        response = client.system_one(
            state=room_state,
            questions={"room_mode": ROOM_MODE_QUESTION},
        )

    answer = response.choices["room_mode"]
    return answer.choice, answer.confidence, answer.probabilities, response.model

What does this function do?

  • TypeSafeClient uses the versioned model stored in JEV_MODEL.
  • system_one() receives only the compact room dictionary as state.
  • The questions mapping requests one typed room_mode answer.
  • The returned tuple exposes the selected label plus its confidence.
  • The same tuple exposes every probability plus the responding model ID.
  • Save room_router.py.
  • Confirm that Python loads the request function by running the detection-only path:
python room_router.py room.jpg --detect-only

Why does detection-only work here?

Python loads ask_jev() before entering main(). Seeing the limitation message confirms that the new function has valid syntax.

Does Python stop before detection?

  • Check that ask_jev() starts after the closing brace returned by detect_room().
  • Check that answer is assigned after the TypeSafe client block closes.

Help me debug where ask_jev belongs in room_router.py.

Print the live decision

The command-line path now needs two routes. Detection-only mode preserves the visible limitation while a normal run requests the live decision.

  • Find the start of main() through args = parser.parse_args() in room_router.py.
  • Replace that block with this updated parser:
def main() -> None:
    parser = argparse.ArgumentParser(
        description="Route a room image through YOLO evidence and a live Jev Choice."
    )
    parser.add_argument("image", type=Path, help="Path to a room image")
    parser.add_argument(
        "--detect-only",
        action="store_true",
        help="Run YOLO and stop before the Jev request",
    )
    args = parser.parse_args()

What changes in the command-line path?

The parser now describes the complete image-to-decision workflow. The --detect-only flag still identifies the local-only route.

  • Save room_router.py.
  • Verify that the updated parser preserves detection-only mode by running:
python room_router.py room.jpg --detect-only

What should remain unchanged?

You should still see the structured YOLO evidence followed by the limitation message. This confirms that the flag still stops before a Jev request.

Does detection-only mode fail?

  • Check that the argument name is exactly --detect-only.
  • Check that its action remains store_true.

Help me check why the detect-only flag no longer follows the local path.

The final branch keeps the limitation visible for detection-only runs. A normal run calls Jev before printing the typed response data.

  • Find the existing limitation print at the end of main().
  • Replace that print with this decision branch:
    if args.detect_only:
        print("LIMITATION: Object detections alone do not select a room mode.")
        return

    choice, confidence, probabilities, model_name = ask_jev(room_state)
    print(f"\nJev model: {model_name}")
    print(f"Choice: {choice}")
    print(f"Confidence: {confidence:.2f}")
    print("Probabilities:")
    for option, probability in sorted(
        probabilities.items(), key=lambda item: item[1], reverse=True
    ):
        print(f"  {option}: {probability:.3f}")

How is the response displayed?

  • The early return preserves the local-only route.
  • ask_jev() supplies the selected room mode.
  • The confidence line shows Jev's confidence from zero to one.
  • The loop sorts every option by probability.
  • The model line confirms which version answered the request.
  • Save room_router.py.
  • Return to the PowerShell session that contains your activated .venv.

This next run makes one billable Jev request. You stay in control because the script sends only the compact JSON evidence.

TypeSafe bills Jev input at $0.042 per million tokens. Output tokens are free.

Before you run it, do you expect Jev to strongly favor one mode or spread probability across several modes?

  • Run the complete image-to-decision workflow with this command:
python room_router.py room.jpg

What should you see?

You should see Jev model: jev-1.13.0 after the YOLO evidence. A Choice: line shows the selected room mode.

A Confidence: line reports the live confidence value. The probability list includes every fixed option.

The script stops after displaying the decision data. That is the semantic gap closed: your local detections now produce a live bounded decision.

Does the live request fail?

  • Check that TYPESAFE_API_KEY remains set in the current PowerShell session.
  • Check that you ran the command from the jev-room-router folder.
  • Check that your TypeSafe account still has live Jev access.

Help me diagnose my live Jev request without exposing my API key.

✔️ Awesome, I've got everything!

Your saved room_router.py now sends local YOLO evidence to live Jev. It prints the complete bounded decision response.

ⓧ I'd like to double check the full code

import argparse
import json
from pathlib import Path

from typesafe_sdk import Choice, TypeSafeClient
from ultralytics import YOLO

JEV_MODEL = "jev-1.13.0"
YOLO_MODEL = "yolo26n.pt"
ANNOTATED_IMAGE = Path("annotated-room.jpg")

ROOM_MODE_QUESTION = Choice(
    instructions=(
        "Choose the safest room mode from the YOLO object-detection summary. "
        "Treat missing detections as uncertainty rather than proof."
    ),
    criteria={
        "focus": (
            "A person is present and the detected objects suggest focused desk, "
            "study, reading, or computer work."
        ),
        "relax": (
            "A person is present and the detected objects suggest resting, "
            "conversation, or entertainment."
        ),
        "vacant": (
            "The detections provide good evidence that no person is present, "
            "so the room can enter an energy-saving mode."
        ),
        "review": (
            "The detections are empty, weak, conflicting, or insufficient to "
            "choose another mode safely."
        ),
    },
)


def detect_room(image_path: Path) -> dict[str, object]:
    model = YOLO(YOLO_MODEL)
    result = model.predict(
        source=str(image_path),
        conf=0.25,
        verbose=False,
    )[0]

    objects: dict[str, dict[str, float | int]] = {}
    for box in result.boxes:
        class_id = int(box.cls.item())
        label = result.names[class_id]
        confidence = float(box.conf.item())
        entry = objects.setdefault(label, {"count": 0, "max_confidence": 0.0})
        entry["count"] = int(entry["count"]) + 1
        entry["max_confidence"] = max(float(entry["max_confidence"]), confidence)

    detected_objects = [
        {
            "label": label,
            "count": values["count"],
            "max_yolo_confidence": round(float(values["max_confidence"]), 3),
        }
        for label, values in sorted(objects.items())
    ]

    result.save(filename=str(ANNOTATED_IMAGE))
    return {
        "image_name": image_path.name,
        "detected_objects": detected_objects,
    }


def ask_jev(room_state: dict[str, object]) -> tuple[str, float, dict[str, float], str]:
    with TypeSafeClient(model=JEV_MODEL) as client:
        response = client.system_one(
            state=room_state,
            questions={"room_mode": ROOM_MODE_QUESTION},
        )

    answer = response.choices["room_mode"]
    return answer.choice, answer.confidence, answer.probabilities, response.model


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Route a room image through YOLO evidence and a live Jev Choice."
    )
    parser.add_argument("image", type=Path, help="Path to a room image")
    parser.add_argument(
        "--detect-only",
        action="store_true",
        help="Run YOLO and stop before the Jev request",
    )
    args = parser.parse_args()

    if not args.image.is_file():
        raise SystemExit(f"Image not found: {args.image}")

    room_state = detect_room(args.image)
    print("YOLO evidence:")
    print(json.dumps(room_state, indent=2))
    print(f"Annotated image: {ANNOTATED_IMAGE}")

    if args.detect_only:
        print("LIMITATION: Object detections alone do not select a room mode.")
        return

    choice, confidence, probabilities, model_name = ask_jev(room_state)
    print(f"\nJev model: {model_name}")
    print(f"Choice: {choice}")
    print(f"Confidence: {confidence:.2f}")
    print("Probabilities:")
    for option, probability in sorted(
        probabilities.items(), key=lambda item: item[1], reverse=True
    ):
        print(f"  {option}: {probability:.3f}")


if __name__ == "__main__":
    main()

How should you compare the file?

Compare each section with your saved file. Pay close attention to indentation inside main() plus the placement of ask_jev().

Your router now turns local visual evidence into a live bounded decision. Next, you will place a confidence gate between that decision and the function that runs.

Gate and Route the Decision

Your live Jev call now returns a bounded Choice with a complete probability distribution. It also returns a confidence value.

A typed answer still needs an explicit control boundary before code executes it. This step adds a confidence gate plus function dispatch so the router runs exactly one safe path.

In this step, get ready to:
  • Add an illustrative confidence threshold for automatic actions.
  • Define one visible handler for each room-mode route.
  • Dispatch exactly one handler from the live Jev result.
Set the confidence boundary

The confidence value expresses how strongly the response supports its selected Choice. A threshold turns that uncertainty into a clear boundary between automatic action and manual review.

  • In room_router.py, find the constants directly above ROOM_MODE_QUESTION.
  • Locate this block:
JEV_MODEL = "jev-1.13.0"
YOLO_MODEL = "yolo26n.pt"
ANNOTATED_IMAGE = Path("annotated-room.jpg")

Why start with these constants?

These constants already collect the fixed values that shape the workflow. The routing threshold belongs beside them because it controls the program's behavior across every live response.

  • Add ACTION_CONFIDENCE_THRESHOLD directly below ANNOTATED_IMAGE so the block becomes:
JEV_MODEL = "jev-1.13.0"
YOLO_MODEL = "yolo26n.pt"
ANNOTATED_IMAGE = Path("annotated-room.jpg")
ACTION_CONFIDENCE_THRESHOLD = 0.65

Why use 0.65?

This project uses 0.65 as an illustrative control boundary. Results below it require manual review.

A real deployment needs threshold testing against its own data and risk level. The threshold stays in ordinary Python so you can tune it without changing the Jev question.

Connect modes to visible outcomes

Each allowed mode needs a matching Python function with an observable result. A fixed dispatch dictionary keeps those permitted choices explicit.

  • In room_router.py, find the final return statement inside ask_jev().
  • Add the room-mode handlers below ask_jev() by pasting this code:
def activate_focus_mode() -> None:
    print("SIMULATED ACTION: Focus mode selected. Prepare bright task lighting and quiet notifications.")


def activate_relax_mode() -> None:
    print("SIMULATED ACTION: Relax mode selected. Prepare warm lighting and entertainment controls.")


def activate_vacant_mode() -> None:
    print("SIMULATED ACTION: Vacant mode selected. Prepare lights and room devices to switch off.")


def request_manual_review() -> None:
    print("MANUAL REVIEW: Jev did not provide enough confidence for an automatic room-mode action.")


ACTIONS = {
    "focus": activate_focus_mode,
    "relax": activate_relax_mode,
    "vacant": activate_vacant_mode,
    "review": request_manual_review,
}

What does this code do?

  • The three activation functions print the simulated outcome for focus, relax, or vacant mode.
  • The request_manual_review() function provides the safe route for uncertainty or an explicit review Choice.
  • The ACTIONS dictionary maps every permitted label to one callable function.
  • Save room_router.py.
  • Check that the new definitions load successfully in PowerShell by running:
python room_router.py room.jpg

What does this check prove?

Python loads the new functions plus the dispatch dictionary before main() runs. A complete Jev response without a traceback confirms that the new definitions are valid.

You should see the live Jev decision complete successfully. The final visible section contains the sorted room-mode probabilities.

Seeing a Python error?

  • Check that the four handlers sit between ask_jev() and main().
  • Confirm that ACTIONS is aligned with each function definition at the left edge of the file.
  • Check that each dictionary label matches one of the four room-mode criteria exactly.

Help me fix the action handlers or dispatch dictionary in room_router.py.

Dispatch exactly one route

The final connection compares the live confidence with the threshold. It resolves one dictionary key before calling one function.

  • In room_router.py, scroll to the probability loop near the end of main().
  • Find this block:
    for option, probability in sorted(
        probabilities.items(), key=lambda item: item[1], reverse=True
    ):
        print(f"  {option}: {probability:.3f}")

Where does routing begin?

This loop finishes displaying the complete probability distribution. The routing decision belongs immediately after it so the learner can inspect the evidence before seeing the executed outcome.

  • Add the routing lines directly below the loop so the section reads:
    for option, probability in sorted(
        probabilities.items(), key=lambda item: item[1], reverse=True
    ):
        print(f"  {option}: {probability:.3f}")

    routed_mode = choice if confidence >= ACTION_CONFIDENCE_THRESHOLD else "review"
    ACTIONS[routed_mode]()

How does the gate work?

  • The routed_mode value keeps Jev's selected Choice when confidence meets the threshold.
  • A lower confidence replaces the selected Choice with review.
  • The ACTIONS lookup calls exactly one function for the resolved route.
  • Save room_router.py.

Before you run it, consider whether the live confidence will clear the gate or send the result to review.

  • Run the complete image-to-action workflow with this command:
python room_router.py room.jpg

What should this run prove?

The script prints the local YOLO evidence before sending its compact state to Jev. It then prints the model ID plus the selected Choice.

The probability distribution appears before the confidence gate dispatches one handler. That order keeps the decision evidence visible beside the resulting action.

After the probability lines, you should see exactly one final line. It begins with SIMULATED ACTION: when an automatic route runs or MANUAL REVIEW: when the gate sends the result to review.

Missing the final route?

  • Confirm that ACTION_CONFIDENCE_THRESHOLD = 0.65 appears beside the other constants.
  • Check that the routed_mode assignment is indented inside main().
  • Check that ACTIONS[routed_mode]() aligns with the routed_mode assignment.

Help me debug why room_router.py prints Jev probabilities without a final routed action.

Your confidence gate now turns a probabilistic answer into one controlled software route. The router can act on a supported decision while sending uncertain evidence to manual review.

✔️ Awesome, I've got everything!

Your finished router now prints its decision evidence before executing exactly one visible route.

ⓧ I'd like to double check the full code

Your finished room_router.py should match this file.

import argparse
import json
from pathlib import Path

from typesafe_sdk import Choice, TypeSafeClient
from ultralytics import YOLO

JEV_MODEL = "jev-1.13.0"
YOLO_MODEL = "yolo26n.pt"
ANNOTATED_IMAGE = Path("annotated-room.jpg")
ACTION_CONFIDENCE_THRESHOLD = 0.65

ROOM_MODE_QUESTION = Choice(
    instructions=(
        "Choose the safest room mode from the YOLO object-detection summary. "
        "Treat missing detections as uncertainty rather than proof."
    ),
    criteria={
        "focus": (
            "A person is present and the detected objects suggest focused desk, "
            "study, reading, or computer work."
        ),
        "relax": (
            "A person is present and the detected objects suggest resting, "
            "conversation, or entertainment."
        ),
        "vacant": (
            "The detections provide good evidence that no person is present, "
            "so the room can enter an energy-saving mode."
        ),
        "review": (
            "The detections are empty, weak, conflicting, or insufficient to "
            "choose another mode safely."
        ),
    },
)


def detect_room(image_path: Path) -> dict[str, object]:
    model = YOLO(YOLO_MODEL)
    result = model.predict(
        source=str(image_path),
        conf=0.25,
        verbose=False,
    )[0]

    objects: dict[str, dict[str, float | int]] = {}
    for box in result.boxes:
        class_id = int(box.cls.item())
        label = result.names[class_id]
        confidence = float(box.conf.item())
        entry = objects.setdefault(label, {"count": 0, "max_confidence": 0.0})
        entry["count"] = int(entry["count"]) + 1
        entry["max_confidence"] = max(float(entry["max_confidence"]), confidence)

    detected_objects = [
        {
            "label": label,
            "count": values["count"],
            "max_yolo_confidence": round(float(values["max_confidence"]), 3),
        }
        for label, values in sorted(objects.items())
    ]

    result.save(filename=str(ANNOTATED_IMAGE))
    return {
        "image_name": image_path.name,
        "detected_objects": detected_objects,
    }


def ask_jev(room_state: dict[str, object]) -> tuple[str, float, dict[str, float], str]:
    with TypeSafeClient(model=JEV_MODEL) as client:
        response = client.system_one(
            state=room_state,
            questions={"room_mode": ROOM_MODE_QUESTION},
        )

    answer = response.choices["room_mode"]
    return answer.choice, answer.confidence, answer.probabilities, response.model


def activate_focus_mode() -> None:
    print("SIMULATED ACTION: Focus mode selected. Prepare bright task lighting and quiet notifications.")


def activate_relax_mode() -> None:
    print("SIMULATED ACTION: Relax mode selected. Prepare warm lighting and entertainment controls.")


def activate_vacant_mode() -> None:
    print("SIMULATED ACTION: Vacant mode selected. Prepare lights and room devices to switch off.")


def request_manual_review() -> None:
    print("MANUAL REVIEW: Jev did not provide enough confidence for an automatic room-mode action.")


ACTIONS = {
    "focus": activate_focus_mode,
    "relax": activate_relax_mode,
    "vacant": activate_vacant_mode,
    "review": request_manual_review,
}


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Route a room image through YOLO evidence and a live Jev Choice."
    )
    parser.add_argument("image", type=Path, help="Path to a room image")
    parser.add_argument(
        "--detect-only",
        action="store_true",
        help="Run YOLO and stop before the Jev request",
    )
    args = parser.parse_args()

    if not args.image.is_file():
        raise SystemExit(f"Image not found: {args.image}")

    room_state = detect_room(args.image)
    print("YOLO evidence:")
    print(json.dumps(room_state, indent=2))
    print(f"Annotated image: {ANNOTATED_IMAGE}")

    if args.detect_only:
        print("LIMITATION: Object detections alone do not select a room mode.")
        return

    choice, confidence, probabilities, model_name = ask_jev(room_state)
    print(f"\nJev model: {model_name}")
    print(f"Choice: {choice}")
    print(f"Confidence: {confidence:.2f}")
    print("Probabilities:")
    for option, probability in sorted(
        probabilities.items(), key=lambda item: item[1], reverse=True
    ):
        print(f"  {option}: {probability:.3f}")

    routed_mode = choice if confidence >= ACTION_CONFIDENCE_THRESHOLD else "review"
    ACTIONS[routed_mode]()


if __name__ == "__main__":
    main()

Secret mission

Stress-Test Jev with an Ambiguous Room

Put the finished router under pressure with a room that plausibly supports more than one mode. You will predict the leading options, inspect the live probability spread, explain the route through the confidence gate, and plan a controlled follow-up experiment.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep the local project at no ongoing cost, pause its active session, or delete its resources. Future live Jev calls cost $0.042 per million input tokens with free output tokens.

Resources you used:

  • The API key created in the TypeSafe AI Console for live Jev requests.
  • The TYPESAFE_API_KEY value held by the current PowerShell session.
  • The local jev-room-router folder. It contains .venv, requirements.txt, check_jev.py, room_router.py, room.jpg, ambiguous-room.jpg, and annotated-room.jpg.
  • The downloaded YOLO26n model weights stored as yolo26n.pt for local inference.

Keep everything running

No cleanup is required. Choose this if you want to keep testing the router with controlled room images.

  • Leave jev-room-router in its current location.
  • Keep the project key active in the TypeSafe AI Console.
  • Make future live Jev requests only when you intend to use billed input tokens.

Pause - I'll come back to this later

Close the current PowerShell session to remove the session-scoped credential from memory. Your project folder stays in place while the console key remains active.

  • Close the current PowerShell window.
  • Leave jev-room-router in its current location.

The TYPESAFE_API_KEY value disappears when the PowerShell session ends. You can set it again when you return for another live experiment.

Delete - I don't want to use this again

Deleting the project permanently removes its code, images, environment, model weights, and generated output. Your TypeSafe account remains intact.

  • Return to the TypeSafe API key page.
  • Locate the project key used for this router.
  • Remove the key with the controls currently shown in your account.
  • Confirm the project key no longer appears on the page.

The deleted project key can no longer authorize future Jev requests. The value already loaded into the current PowerShell process remains until that session ends.

  • Close the current PowerShell window.
  • Navigate to the location containing jev-room-router with your Windows file browser.
  • Select the jev-room-router folder.
  • Permanently delete the selected folder with the file browser's deletion controls.

You should no longer see jev-room-router in its previous location. This confirms that .venv, the Python packages, the model weights, the source files, the room images, and the generated annotation have been removed.

Nice Work!

Nice Work!

You did it! Your Windows room router now converts a room photo into structured evidence. It asks Jev for a bounded decision before a Python confidence gate selects one controlled route.

What you learned:

  • Built a local YOLO26n object detector that turns room.jpg into a compact JSON summary. It saves labeled visual evidence in annotated-room.jpg.
  • Called live Jev 1.13.0 with a typed Choice built from the detection summary. Inspected the selected room mode. Read every option probability. Checked the response confidence. Confirmed the responding model ID.
  • Connected the decision to an explicit 0.65 confidence threshold. Used the ACTIONS dispatch dictionary to select one function. The router executes one simulated action for a sufficiently confident result. It sends a lower-confidence result to manual review.
  • Secret Mission: Stress-tested room_router.py with ambiguous-room.jpg. Compared the predicted top two modes with the live probability spread. Explained how the 0.65 gate determined the final route. Recorded a future criterion or threshold experiment without treating one run as proof of accuracy.

Ready to quiz yourself?