Ship an AI Incident Router

Build, test, containerize, and automate an AI incident-routing API.

Introduction

30 Second Summary

When a system breaks, the first report is often a rushed sentence with no clear owner. Sending it to the wrong team costs time when every minute matters.

In this project, you will build an AI incident router that classifies short reports as database, network, or deployment issues. You'll replace brittle keyword rules with text classification before packaging the tested service with Docker and automating its checks with GitHub Actions.

What You'll Build

You'll paste a replica-lag report into interactive API documentation and see the containerized router return database.

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

  • A before-and-after routing demo where the keyword version returns network for replica lag before the fitted model returns database.
  • A reusable Docker image that serves the same prediction from a container on your Windows device.
  • A public GitHub repository with a continuous integration quality gate that runs the API tests and builds the Docker image on every push or pull request.
  • Secret Mission: Add a model-aware readiness probe that runs a sentinel prediction before reporting the service as ready.

Are there any prerequisites?

You need a Windows device with at least 8 GB of RAM. Step 1 checks compatibility before guiding you through every required tool.

Before We Start

This step is your moment to commit to the AI incident router you will build. A clear reason keeps the automated pipeline connected to your goal.

Set Up the Windows Toolchain

Your incident router needs a dependable local foundation before application failures can teach you anything useful. A supported runtime keeps tool problems separate from the routing behavior you investigate later.

In this step, you prepare Windows PowerShell, Docker Desktop, Python, Visual Studio Code, and Git. You also create an isolated project environment with every dependency pinned to a tested version.

In this step, get ready to:
  • Confirm Windows compatibility before installing Docker Desktop with its WSL 2 backend.
  • Install Python 3.14.9, Visual Studio Code, Git for Windows, and prepare a GitHub account.
  • Create the ai-incident-router project with an activated .venv environment and pinned dependencies.
Check compatibility and install Docker Desktop

Docker Desktop runs Linux containers through WSL 2 on this Windows setup. Its engine requires a supported Windows release, at least 8 GB of RAM, and hardware virtualization.

  • Press the Windows key to open Windows search.
  • Type Settings into the search box.
  • Press Enter to open Settings.
  • Select System from the sidebar.
  • Select About.
  • Confirm that Version and OS build show either Windows 10 22H2 build 19045 or Windows 11 23H2 build 22631 or later.
  • Confirm that Installed RAM shows at least 8 GB.
  • Confirm that System type shows a 64-bit operating system.

What Do These Checks Prove?

The release check confirms that Docker Desktop supports your Windows installation. The memory check confirms that the machine meets the container engine requirement.

The 64-bit system type matches the processor architecture used by this project. The next check confirms that Windows can access the processor virtualization features.

  • Right-click Start on the Windows taskbar.
  • Select Task Manager.
  • Select Performance.
  • Select CPU.
  • Confirm that Virtualization shows Enabled.

Does Your Device Miss a Requirement?

A device below the listed Windows or memory requirements cannot use this Docker Desktop path. Update Windows or move to a compatible Windows device before continuing.

If virtualization is disabled, follow the official Windows virtualization guide for your device manufacturer.

Ask for targeted help with checking your Docker Desktop compatibility.

WSL supplies the Linux environment used by the recommended Docker backend. Its version check reveals whether Windows is ready or needs an update.

  • Press the Windows key to open Windows search.
  • Type PowerShell into the search box.
  • Press Enter to open Windows PowerShell.
  • Check the installed WSL release by running this command:
wsl --version

What Does This Command Do?

The command prints version details for WSL and its Linux kernel components. Docker Desktop requires WSL 2.1.5 or later for this backend.

✔️ I see version 2.1.5 or higher

Your WSL release supports Docker Desktop. Keep this PowerShell window available for the remaining checks.

ⓧ I see an older version

Your installed WSL release needs an update before Docker Desktop can use it reliably.

  • Press the Windows key to open Windows search.
  • Type PowerShell into the search box.
  • Select Run as administrator from the search results.
  • Approve the Windows permission request.
  • Update WSL by running this command:
wsl --update

What Does This Command Do?

The command downloads the current WSL components. Windows may request a restart after the update.

  • Restart Windows if the update requests it.
  • Return to PowerShell after Windows restarts.
  • Confirm the updated WSL release by running this command:
wsl --version

What Should You See?

The output should report WSL 2.1.5 or higher. That result clears the WSL version requirement.

ⓧ Command not found

Windows cannot find WSL yet. Installing it enables the Windows components required by the Docker backend.

  • Press the Windows key to open Windows search.
  • Type PowerShell into the search box.
  • Select Run as administrator from the search results.
  • Approve the Windows permission request.
  • Install WSL by running this command:
wsl --install

What Does This Command Do?

The command enables WSL and installs its required Windows components. Windows may request a restart before WSL becomes available.

  • Restart Windows when the installation requests it.
  • Return to PowerShell after Windows restarts.
  • Confirm the installed WSL release by running this command:
wsl --version

What Should You See?

The output should include WSL version details at 2.1.5 or higher. Windows is now ready for the Docker Desktop installer.

The per-user Docker Desktop installation uses WSL 2 as the default backend for most Windows users. This setup keeps the Linux container engine inside your Windows account.

  • Visit the official Docker Desktop installation page.
  • Download Docker Desktop for Windows for the x86_64 architecture.
  • Run the downloaded Docker Desktop installer.
  • Choose the per-user installation path.
  • Keep the recommended WSL 2 backend selected.
  • Complete the installation wizard.

Why Use the WSL 2 Backend?

WSL 2 provides the Linux environment needed by the containers in this project. Docker Desktop manages that environment while you work through familiar Windows tools.

This path lets the same service run locally in Windows and build later on a clean Linux runner.

The first launch presents a subscription agreement that can feel like a billing prompt. Personal and educational use stays free for this project.

  • Press the Windows key to open Windows search.
  • Type Docker Desktop into the search box.
  • Press Enter to start Docker Desktop.
  • Review the subscription agreement shown on first launch.
  • Accept the agreement to continue.
  • Keep Docker Desktop open until its engine reports that it is running.

Before you run the check, do you think the Docker engine is ready to answer PowerShell?

  • Verify the Docker engine by running this command in PowerShell:
docker version

What Does This Command Prove?

The command asks both the Docker command-line client and the local engine for their version details. A working setup prints separate Client and Server sections.

You should see details under both Client and Server. That result confirms that Docker Desktop is installed and its engine is reachable.

Are the Server Details Missing?

Confirm that Docker Desktop is still open. Wait until its engine reports that it is running before retrying the command.

Recheck that WSL reports 2.1.5 or higher. Confirm that Task Manager still shows virtualization as enabled.

Ask for help with diagnosing the Docker Desktop engine.

Install the remaining Windows tools

Python 3.14.9 matches the dependency versions used throughout the project. Visual Studio Code provides the editor while Git and GitHub provide the source-control path used by continuous integration later.

  • Return to the PowerShell window from the Docker check.
  • Check the Python runtime currently selected by Windows by running this command:
python -VV

What Does This Command Do?

Passing the version flag twice prints Python's extended version and build information. This check reveals whether Windows already selects Python 3.14.9.

✔️ I see version 3.14.9

Python 3.14.9 is already available through the python command. Continue to the Visual Studio Code installation below.

ⓧ I see an older version

Windows currently selects a different Python runtime. Installing the pinned release gives this project the tested interpreter it needs.

  • Visit the official Python downloads page for Windows.
  • Download the 64-bit Windows installer for Python 3.14.9.
  • Run the downloaded Python installer.
  • Enable the installer option that adds Python to your PATH.
  • Complete the default installation.
  • Close the current PowerShell window.
  • Open a fresh PowerShell window through Windows search.
  • Verify the selected runtime by running this command:
python -VV

What Should You See?

The extended version output should identify Python 3.14.9. This confirms that the pinned runtime has priority in the fresh terminal.

ⓧ Command not found

Windows cannot find Python yet. The official installer supplies the required runtime and command.

  • Visit the official Python downloads page for Windows.
  • Download the 64-bit Windows installer for Python 3.14.9.
  • Run the downloaded Python installer.
  • Enable the installer option that adds Python to your PATH.
  • Complete the default installation.
  • Close the current PowerShell window.
  • Open a fresh PowerShell window through Windows search.
  • Verify the installed runtime by running this command:
python -VV

What Should You See?

PowerShell should report Python 3.14.9 with its build details. The python command is now available for the project.

Python 3.14.9 is ready. The next installers add the editor command and the source-control command used later.

The installer should report a completed Visual Studio Code installation. User Setup also adds the code command to PATH for fresh PowerShell sessions.

  • Visit the official Git for Windows installation page.
  • Download the current x64 Git for Windows installer.
  • Run the downloaded Git installer.
  • Keep the recommended selections on each configuration page.
  • Complete the Git installation.

What Will the Git Installer Ask?

The installer presents several configuration pages for its editor, terminal, branch, and PATH behavior. The recommended selections provide the git command required by this project.

The completion screen confirms that Git for Windows is installed. A fresh PowerShell session picks up the new PATH entry.

A verified email address lets your GitHub account create the public repository used later. Choose the path that matches your current account state.

✔️ I already have a GitHub account

  • Sign in to GitHub in your browser.
  • Confirm that your account has a verified email address.

Your GitHub account is ready for the public repository created later in the project.

ⓧ I need a GitHub account

  • Visit the official GitHub signup page.
  • Follow the signup prompts to create a free personal account.
  • Open the verification message sent to your email address.
  • Complete the email verification from that message.

Your verified GitHub account can now create the public repository used by GitHub Actions later.

Did an Installer Fail?

Confirm that you downloaded the Windows installer from the official page for the affected tool. Close older PowerShell windows after an installation changes PATH.

Ask for help with diagnosing the Windows tool installation.

Create and verify the project environment

A virtual environment keeps this project's Python packages separate from global packages. The pinned requirements.txt file makes that isolated environment reproducible.

  • Close any PowerShell windows that were open before the Visual Studio Code and Git installations.
  • Open a fresh PowerShell window through Windows search.
  • Move PowerShell to your Desktop by running this command:
Set-Location -Path "$HOME\Desktop"

What Does This Command Do?

The command moves the PowerShell session to the Desktop folder inside your Windows home directory. The project folder you create next now has a predictable location.

  • Create the ai-incident-router folder by running these commands:
New-Item -Path '.\ai-incident-router' -ItemType Directory
Set-Location -Path '.\ai-incident-router'

What Do These Commands Do?

The first command creates the project folder on your Desktop. The second command moves PowerShell into that folder.

PowerShell should print details for the new directory. The prompt path should now end with Desktop\ai-incident-router.

Was the Project Folder Not Created?

Confirm that PowerShell is positioned on your Desktop. Check whether an ai-incident-router folder already exists there before retrying.

Ask for help with creating the project folder in PowerShell.

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

What Does This Command Do?

The code command starts Visual Studio Code. The dot opens the folder represented by the current PowerShell location.

Visual Studio Code should open with ai-incident-router shown in the Explorer sidebar. PowerShell remains positioned inside the same folder.

Is the Code Command Unavailable?

Close PowerShell before opening a fresh session. Confirm that you installed the recommended Visual Studio Code User Setup before retrying.

Ask for help with restoring the Visual Studio Code command.

The dependency file records every library version used by the finished service. Exact pins keep the API, classifier, and test behavior consistent.

  • Select the Explorer icon in the Visual Studio Code sidebar.
  • Select the New File button in the Explorer toolbar.
  • Enter requirements.txt as the file name.
  • Add the pinned dependencies by pasting this content into requirements.txt:
fastapi[standard]==0.161.1
pydantic==2.14.1
scikit-learn==1.9.1
httpx==0.28.1
pytest==9.1.1

What Do These Dependencies Provide?

  • FastAPI provides the web API and interactive documentation.
  • Pydantic defines typed request and response models.
  • Scikit-learn provides the text-classification pipeline.
  • HTTPX supplies the request layer used by the API test client.
  • Pytest discovers and runs the automated API checks.
  • Save requirements.txt.
  • Confirm that requirements.txt appears under ai-incident-router in the Explorer sidebar.

Does the Dependency File Look Different?

Compare every package name and version with the snippet above. Keep one dependency on each line.

Ask for help with checking the requirements file.

The app folder holds the service code in the next step. An empty app\__init__.py file marks that folder as a Python package.

  • Switch back to the PowerShell window positioned inside ai-incident-router.
  • Create the application folder with its empty package file by running these commands:
New-Item -Path '.\app' -ItemType Directory
New-Item -Path '.\app\__init__.py' -ItemType File

What Do These Commands Do?

The first command creates the app directory inside the project. The second command creates an empty app\__init__.py file.

  • Return to the Visual Studio Code Explorer sidebar.
  • Expand the app folder.
  • Confirm that __init__.py appears inside the folder.
  • Confirm that __init__.py contains no text.

Is the App Folder Missing?

Confirm that the PowerShell prompt path ends with ai-incident-router. Retry the creation commands from that location.

Ask for help with creating the Python package files.

✔️ Awesome, I've got everything!

Your project contains the pinned requirements.txt file. The app\__init__.py file is present and empty.

ⓧ I'd like to double check the full code

Compare requirements.txt with this exact file.

fastapi[standard]==0.161.1
pydantic==2.14.1
scikit-learn==1.9.1
httpx==0.28.1
pytest==9.1.1

What Should Match?

Each package and version should match exactly. The file ends after the pytest pin.

The app\__init__.py file should be completely empty. Any text inside it should be removed before continuing.

The project files now have a stable local home. The virtual environment keeps the dependencies beside those files without changing the global Python installation.

  • Switch back to PowerShell.
  • Create .venv by running this command:
python -m venv .venv

What Does This Command Do?

Python's environment module creates an isolated interpreter inside .venv. Packages installed through that interpreter remain local to this project.

  • Return to the Visual Studio Code Explorer sidebar.
  • Confirm that the .venv folder appears under ai-incident-router.

Was the Virtual Environment Not Created?

Confirm that the PowerShell prompt ends with ai-incident-router. Rerun the Python version check if PowerShell cannot find the interpreter.

Ask for help with creating the virtual environment.

  • Activate the project environment by running this command:
.\.venv\Scripts\Activate.ps1

What Does This Command Do?

The activation script places the environment's Python executable first for the current PowerShell session. Later Python and pip commands now target .venv.

✔️ I see the activated environment

The PowerShell prompt begins with (.venv). This prefix confirms that the isolated environment is active.

ⓧ PowerShell blocked the script

PowerShell can block local activation scripts under a restrictive execution policy. The documented current-user policy permits locally created scripts while retaining checks for downloaded scripts.

  • Apply the documented policy for your Windows account by running this command:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

What Does This Command Do?

The command changes the execution policy for your Windows user. It does not change the policy for other accounts on the device.

  • Confirm the policy change if PowerShell requests approval.
  • Retry the activation by running this command:
.\.venv\Scripts\Activate.ps1

What Should You See?

The prompt should now begin with (.venv). That prefix confirms that the retry activated the environment.

Is Activation Still Blocked?

Confirm that the activation path starts from the ai-incident-router folder. Check that .venv\Scripts\Activate.ps1 exists in the Explorer sidebar.

Ask for help with activating the PowerShell environment.

The first dependency installation downloads the web framework and scientific Python packages. Keep the terminal open while the package output continues to move.

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

What Does This Command Do?

The command runs pip through the active environment's Python interpreter. Pip reads every exact package version from requirements.txt.

The installation output should finish without a dependency error. The API, classifier, and testing packages are now available inside .venv.

Did the Dependency Installation Fail?

Confirm that the PowerShell prompt starts with (.venv). Check your internet connection before retrying the installation.

Compare requirements.txt with the full-code tab if pip reports a package or version problem.

Ask for help with diagnosing the dependency installation.

Before you run the final check, which tools do you expect PowerShell to identify successfully?

  • Verify the complete Windows toolchain by running these commands:
python -VV
git --version
docker version
code .

What Should You See?

  • The Python output identifies release 3.14.9 with its build details.
  • The Git output prints its installed version.
  • The Docker output contains both Client and Server sections.
  • Visual Studio Code opens the ai-incident-router folder.

That is the full local foundation in place. Your editor, runtime, source control, container engine, project files, and isolated dependencies are ready.

Your Windows toolchain is ready. Next, you will build a working API whose keyword logic fails on a realistic database incident.

Build a Failing Keyword Router

Your Windows toolchain is ready. The next goal is to turn the empty project into a working FastAPI service you can question through a browser.

Keyword routing looks convincing when a report contains an obvious label. Real incidents often describe symptoms, so this version deliberately tests how far literal matching can go.

In this step, get ready to:
  • Launch a FastAPI development server from app/main.py.
  • Define a typed API contract for incident predictions.
  • Expose the keyword rule's blind spot with a realistic incident report.
Create the FastAPI application shell

A FastAPI application begins with an app object. Its metadata gives the generated documentation a recognizable name.

  • In the Visual Studio Code Explorer sidebar from earlier, select the app folder.
  • Select New File in the Explorer toolbar.

A filename field appears inside the app folder.

  • Enter main.py.
  • Press Enter.

You should see main.py open in a new editor tab.

  • Paste this application shell into app/main.py:
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="AI Incident Router", version="1.0.0")

What Does This Code Do?

  • The FastAPI import provides the application constructor.
  • The BaseModel import supports the typed data models added next.
  • The app object stores the service metadata used by the generated documentation.
  • Save app/main.py.

The development server monitors saved changes. It keeps this PowerShell terminal occupied while running.

  • Start the development server from the activated terminal by running this command:
fastapi dev app/main.py

What Does This Command Do?

The command starts FastAPI in development mode from app/main.py. Development mode reloads saved changes automatically.

The process serves your interactive documentation at http://127.0.0.1:8000/docs.

  • Enter http://127.0.0.1:8000/docs in your browser's address bar.

You should see the interactive documentation page displaying AI Incident Router. The page confirms that your application shell is running.

Documentation Page Not Loading?

Confirm that the activated environment marker is still visible in PowerShell.

Check that the terminal is still running from the ai-incident-router folder.

Ask for targeted help with diagnosing the FastAPI development server.

Define the request and response models

Pydantic models give each request and response a declared shape. FastAPI uses that contract to validate data before the router handles it.

  • Place your cursor below the app = FastAPI(title="AI Incident Router", version="1.0.0") line in the app/main.py tab.
  • Define both data models with this code:
class Incident(BaseModel):
    text: str


class Prediction(BaseModel):
    category: str

What Do These Models Do?

  • Incident requires each incoming request to contain a text string.
  • Prediction requires each response to contain a category string.
  • FastAPI uses both models to produce a documented contract for callers.
  • Save app/main.py.

You've now given the API a typed contract. PowerShell should show a completed reload with no startup error.

Server Reload Failed?

Check that both class definitions align at the left edge of app/main.py.

Confirm that text: str sits inside Incident.

Ask for help with fixing the model definitions.

Test the keyword router's limits

The endpoint checks literal database words before checking deployment words. Every unmatched report reaches the network fallback.

  • Place your cursor below the Prediction class in app/main.py.
  • Add the temporary routing endpoint with this code:
@app.post("/predict", response_model=Prediction)
def predict_incident(incident: Incident) -> Prediction:
    text = incident.text.lower()
    if "database" in text or "sql" in text or "postgres" in text:
        category = "database"
    elif "deployment" in text or "deploy" in text or "release" in text:
        category = "deployment"
    else:
        category = "network"
    return Prediction(category=category)

What Does This Code Do?

  • lower() makes the keyword checks ignore letter case.
  • The first branch recognizes three literal database terms.
  • The second branch recognizes three literal deployment terms.
  • The final branch assigns every unmatched report to network.
  • Prediction returns the selected category through the declared response contract.
  • Save app/main.py.

PowerShell should report another completed reload. The running server now includes the prediction route.

  • Return to the interactive documentation from earlier.
  • Refresh the browser page.

You should see a POST /predict endpoint card. This proves the running application loaded your new route.

Prediction Route Missing?

Confirm that the route decorator begins at the left edge of app/main.py.

Check PowerShell for a syntax error from the latest reload.

Ask for help with finding the missing prediction route.

✔️ Awesome, I've got everything!

Great. Double-check that app/main.py is saved before testing the route.

ⓧ I'd like to double check the full code

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="AI Incident Router", version="1.0.0")


class Incident(BaseModel):
    text: str


class Prediction(BaseModel):
    category: str


@app.post("/predict", response_model=Prediction)
def predict_incident(incident: Incident) -> Prediction:
    text = incident.text.lower()
    if "database" in text or "sql" in text or "postgres" in text:
        category = "database"
    elif "deployment" in text or "deploy" in text or "release" in text:
        category = "deployment"
    else:
        category = "network"
    return Prediction(category=category)
  • Expand POST /predict in the interactive documentation.
  • Select Try it out.
  • Replace the request body's text value with database.
  • Select Execute.

The response body should show database as the category. The explicit keyword reaches the first branch.

  • Replace the request body's text value with deployment.
  • Select Execute.

The response body should show deployment as the category. The explicit keyword reaches the second branch.

What Did These Checks Prove?

Each report contains a literal word that its branch checks. These examples prove the endpoint works when the incident names its category directly.

Before you send the final report, do you think literal matching can route a symptom that names no category?

  • Replace the request body's text value with replica lag is increasing.
  • Select Execute.

You'll see {"category":"network"} in the response body. That wrong category is the intended shortfall because replica lag belongs with database incidents.

You found the weakness on purpose: your live API can route obvious labels while symptom-based language slips through. Next, you'll replace the literal checks with a fitted text-classification pipeline.

Train the Machine Learning Router

You made the weakness visible: your running FastAPI service routes the replica-lag incident to the network fallback. That recorded failure gives the machine learning version a clear target.

A machine learning pipeline for text classification can learn weighted language patterns from labelled examples.

Scikit-learn combines TF-IDF with Complement Naive Bayes. The fitted model can recognize replica lag as a database issue.

In this step, get ready to:
  • Add 15 balanced incident examples with their category labels.
  • Fit a TF-IDF and Complement Naive Bayes pipeline.
  • Send incident predictions through the fitted model.
Add balanced training examples

A supervised classifier learns from text paired with category labels. Five examples per category keep this teaching corpus balanced.

  • Switch back to app/main.py in Visual Studio Code.
  • Select the two existing import lines at the top of the file.
  • Replace them with this complete import group:
from fastapi import FastAPI
from pydantic import BaseModel
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import ComplementNB
from sklearn.pipeline import Pipeline

What Do These Imports Provide?

  • TfidfVectorizer converts raw incident text into weighted numeric features.
  • ComplementNB learns which category best matches those features.
  • Pipeline connects the text transformer to the classifier.
  • Save app/main.py.
  • Check the server terminal for the automatic reload.

The development server should finish reloading successfully. The interactive documentation remains available at http://127.0.0.1:8000/docs.

The Server Cannot Import Scikit-Learn?

Confirm that the terminal still uses the activated .venv. Check that each import matches the snippet exactly.

Ask for targeted help with diagnosing the scikit-learn imports.

  • Place the cursor below the import group in app/main.py.
  • Add the incident training corpus by pasting this block before app = FastAPI(title="AI Incident Router", version="1.0.0"):
TRAINING_TEXTS = [
    "replica lag is increasing",
    "database connection pool is exhausted",
    "sql query latency is high",
    "postgres writes are failing",
    "database storage is full",
    "dns resolution is failing",
    "packet loss between services",
    "network request timed out",
    "load balancer cannot reach upstream",
    "gateway connection was refused",
    "deployment failed during release",
    "container image could not start",
    "rollback required after deploy",
    "new release caused errors",
    "build artifact is missing",
]

How Is the Corpus Structured?

TRAINING_TEXTS contains five database incidents. It also contains five network incidents.

The final five examples describe deployment incidents. The replica-lag sentence gives the classifier the pattern that defeated the keyword router.

  • Save app/main.py.
  • Check the server terminal for another automatic reload.

The server should reload with all 15 training sentences in memory. Its successful reload confirms that the corpus has valid Python syntax.

The Training Corpus Stops the Reload?

Check every incident sentence for matching quotation marks. Confirm that each list item ends with a comma.

Ask for help with checking the training corpus syntax.

  • Place the cursor below the closing bracket of TRAINING_TEXTS.
  • Pair every incident with its category by adding this label list:
TRAINING_LABELS = [
    "database",
    "database",
    "database",
    "database",
    "database",
    "network",
    "network",
    "network",
    "network",
    "network",
    "deployment",
    "deployment",
    "deployment",
    "deployment",
    "deployment",
]

How Do the Labels Teach Categories?

Each position in TRAINING_LABELS describes the incident at the same position in TRAINING_TEXTS. The first incident therefore receives the database label.

Each category appears five times. This balance gives every team category equal representation in the training corpus.

  • Save app/main.py.
  • Check the server terminal for the completed reload.

The development server should return to its running state. The two lists now contain 15 entries each.

The Labels Cause a Syntax Problem?

Confirm that every category is inside quotation marks. Count five database labels before the five network labels.

Ask for targeted help with checking the training labels.

Fit the classification pipeline

The training lists hold the examples. A fitted pipeline turns those examples into a reusable prediction process when the module loads.

  • Place the cursor below the closing bracket of TRAINING_LABELS.
  • Build the classifier before the FastAPI application is created by adding this block:
model = Pipeline(
    [
        ("vect", TfidfVectorizer()),
        ("clf", ComplementNB()),
    ]
)
model.fit(TRAINING_TEXTS, TRAINING_LABELS)

How Does the Pipeline Learn?

  • vect names the step that calculates TF-IDF features from each incident.
  • clf names the Complement Naive Bayes classification step.
  • model.fit() learns from the paired training texts and labels once during startup.
  • model keeps the fitted transformer and classifier together for inference.
  • Save app/main.py.
  • Watch the server terminal while the module reloads.

The development server should finish reloading successfully. That successful startup proves the model fitted from all 15 labelled examples.

The Model Does Not Fit?

Confirm that TRAINING_TEXTS appears before TRAINING_LABELS. Check that both lists contain the same number of entries.

Ask for help with diagnosing the pipeline fit.

Route incidents through the model

The endpoint still contains the temporary keyword branches. Replacing that function body keeps the existing API contract while changing the source of its decision.

  • Select the existing predict_incident() function in app/main.py.
  • Include the @app.post("/predict", response_model=Prediction) decorator in the selection.
  • Replace the selection with this model-backed endpoint:
@app.post("/predict", response_model=Prediction)
def predict_incident(incident: Incident) -> Prediction:
    category = str(model.predict([incident.text])[0])
    return Prediction(category=category)

How Does Inference Reach the API?

model.predict([incident.text]) sends one incident through the fitted vectorizer and classifier. The first result holds the predicted category.

str() converts that result into the value expected by the Pydantic response model. Prediction preserves the endpoint's typed response contract.

  • Save app/main.py.
  • Check the server terminal for a successful reload.

Before you submit the recorded incident again, which category do you expect the fitted model to return?

  • Return to the browser tab showing http://127.0.0.1:8000/docs.
  • Refresh the interactive documentation.
  • Expand POST /predict.
  • Enable request editing.
  • Replace the request body with {"text":"replica lag is increasing"}.
  • Submit the request.

You should see {"category":"database"}. That is the turning point: the same incident that reached the network fallback now reaches the database category.

Still Seeing the Network Category?

Confirm that predict_incident() calls model.predict([incident.text]). Remove the old keyword branches if any remain inside the function.

Save app/main.py before refreshing the documentation. Check that the server terminal completed its reload.

Ask for targeted help with debugging the incorrect prediction.

✔️ Awesome, I've got everything!

Your saved app/main.py now contains the balanced corpus, fitted pipeline, and model-backed prediction endpoint.

ⓧ I'd like to double check the full code

from fastapi import FastAPI
from pydantic import BaseModel
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import ComplementNB
from sklearn.pipeline import Pipeline

TRAINING_TEXTS = [
    "replica lag is increasing",
    "database connection pool is exhausted",
    "sql query latency is high",
    "postgres writes are failing",
    "database storage is full",
    "dns resolution is failing",
    "packet loss between services",
    "network request timed out",
    "load balancer cannot reach upstream",
    "gateway connection was refused",
    "deployment failed during release",
    "container image could not start",
    "rollback required after deploy",
    "new release caused errors",
    "build artifact is missing",
]

TRAINING_LABELS = [
    "database",
    "database",
    "database",
    "database",
    "database",
    "network",
    "network",
    "network",
    "network",
    "network",
    "deployment",
    "deployment",
    "deployment",
    "deployment",
    "deployment",
]

model = Pipeline(
    [
        ("vect", TfidfVectorizer()),
        ("clf", ComplementNB()),
    ]
)
model.fit(TRAINING_TEXTS, TRAINING_LABELS)

app = FastAPI(title="AI Incident Router", version="1.0.0")


class Incident(BaseModel):
    text: str


class Prediction(BaseModel):
    category: str


@app.post("/predict", response_model=Prediction)
def predict_incident(incident: Incident) -> Prediction:
    category = str(model.predict([incident.text])[0])
    return Prediction(category=category)

Your router now learns from incident examples and returns the corrected database category. Next, you will protect that behavior with automated tests before running the service from a reproducible container.

Test and Containerize the Service

The fitted model now routes replica lag correctly through your FastAPI development server. A model tied to one terminal is difficult to ship with confidence.

This step adds automated tests that lock in the expected predictions. Docker then packages the service inside a reproducible container.

In this step, get ready to:
  • Protect the database and deployment predictions with automated API tests.
  • Control the Docker build context with ignore rules.
  • Run the service from a reproducible Docker image.
Add deterministic API tests

FastAPI TestClient sends requests directly to the existing application. pytest turns the expected status codes and categories into repeatable checks.

  • Use the Explorer sidebar's new-folder control beside ai-incident-router to create tests.
  • Use the new-file control beside tests to create test_main.py.

The Explorer sidebar now shows tests/test_main.py inside ai-incident-router.

  • Add the TestClient setup and replica-lag check to tests/test_main.py by pasting this code:
from fastapi.testclient import TestClient

from app.main import app

client = TestClient(app)


def test_replica_lag_routes_to_database():
    response = client.post(
        "/predict",
        json={"text": "replica lag is increasing"},
    )

    assert response.status_code == 200
    assert response.json() == {"category": "database"}

What Does This Test Check?

  • TestClient wraps the existing app so the test can call its API without starting another server.
  • client.post() sends the replica-lag incident to /predict.
  • The first assertion requires a successful HTTP status.
  • The second assertion locks in the expected database category.
  • Save tests/test_main.py.

Before you run the test, do you expect the fitted pipeline to preserve the corrected replica-lag result?

  • Run the first API test from the activated PowerShell terminal with this command:
pytest

What Does This Command Do?

The test runner discovers the function whose name starts with test_. It fails the run if either assertion does not match the API response.

PowerShell confirms that one test passed. Your corrected replica-lag behavior now has an automated safety check.

The First Test Did Not Pass?

  • Confirm that test_main.py sits inside the tests folder.
  • Confirm that the PowerShell prompt is inside ai-incident-router.
  • Compare the request path and response category with the code above.

Ask for targeted help with diagnosing the failing replica-lag test.

The first test protects the incident that exposed the keyword router. A second category check proves that the fitted model still handles release failures.

  • Leave two blank lines at the end of tests/test_main.py.
  • Append the deployment prediction test by pasting this code:
def test_failed_release_routes_to_deployment():
    response = client.post(
        "/predict",
        json={"text": "deployment failed during release"},
    )

    assert response.status_code == 200
    assert response.json() == {"category": "deployment"}

What Does the Second Test Protect?

  • The request uses a release failure from the embedded deployment examples.
  • The assertions require a successful response with the deployment category.
  • Together the tests guard two distinct routing paths.
  • Save tests/test_main.py.

Before you rerun the suite, do you expect both protected categories to pass?

  • Run the complete local test suite with this command:
pytest

What Does the Complete Run Prove?

The test runner executes both request scenarios against the same fitted application. A passing run confirms that the database and deployment contracts remain stable.

PowerShell reports 2 passed. Good work. Your router now has repeatable evidence for its two protected predictions.

Only One Test Passed?

  • Confirm that both function names start with test_.
  • Check the indentation inside the second function.
  • Compare the deployment request text with the code block above.

Ask for help with finding the failing API assertion.

✔️ Awesome, I've got everything!

Both API tests are saved in tests/test_main.py.

ⓧ I'd like to double check the full code

from fastapi.testclient import TestClient

from app.main import app

client = TestClient(app)


def test_replica_lag_routes_to_database():
    response = client.post(
        "/predict",
        json={"text": "replica lag is increasing"},
    )

    assert response.status_code == 200
    assert response.json() == {"category": "database"}


def test_failed_release_routes_to_deployment():
    response = client.post(
        "/predict",
        json={"text": "deployment failed during release"},
    )

    assert response.status_code == 200
    assert response.json() == {"category": "deployment"}
Define the Docker image

A Docker build context contains the files available during an image build. Ignore rules keep local environments and test caches outside that context.

Why Containerize This Service?

A Docker image bundles the application with its Python runtime and pinned dependencies. The same recipe can rebuild the service on another compatible machine.

Installing dependencies before copying the application also lets Docker reuse the dependency layer when only the app code changes.

  • Use the Explorer sidebar's new-file control beside ai-incident-router to create .gitignore.

The Explorer sidebar now lists .gitignore beside requirements.txt.

  • Add the local Python exclusions to .gitignore by pasting this code:
.venv/
__pycache__/
.pytest_cache/
*.pyc

What Does This File Exclude?

  • The .venv/ entry keeps the machine-specific virtual environment out of future commits.
  • The cache entries exclude generated Python and pytest files.
  • The *.pyc pattern excludes compiled Python files.
  • Save .gitignore.
  • Confirm that the Explorer sidebar still lists .gitignore inside ai-incident-router.

The Ignore File Is Missing?

Confirm that the filename starts with a dot. Check that the editor did not add a text-file extension.

Ask for help with creating the project ignore file.

✔️ Awesome, I've got everything!

Your local Python environment and caches are excluded by .gitignore.

ⓧ I'd like to double check the full code

.venv/
__pycache__/
.pytest_cache/
*.pyc
  • Use the Explorer sidebar's new-file control beside ai-incident-router to create .dockerignore.

The Explorer sidebar now shows .dockerignore with the other project files.

  • Define the Docker build exclusions in .dockerignore by pasting this code:
.venv
.git
.github
__pycache__
.pytest_cache
tests

What Stays Outside the Image?

  • The local virtual environment stays outside the build context.
  • Source-control metadata and workflow files stay outside the build context.
  • Tests and generated caches stay outside the runtime image.
  • Save .dockerignore.
  • Confirm that the Explorer sidebar lists .dockerignore inside ai-incident-router.

The Docker Ignore File Looks Wrong?

Check that each exclusion occupies its own line. Confirm that the file is named .dockerignore without an added extension.

Ask for help with checking the Docker build exclusions.

✔️ Awesome, I've got everything!

Your Docker build context now excludes local and development-only files.

ⓧ I'd like to double check the full code

.venv
.git
.github
__pycache__
.pytest_cache
tests
  • Use the Explorer sidebar's new-file control beside ai-incident-router to create Dockerfile.

The Explorer sidebar now lists Dockerfile at the same level as requirements.txt.

  • Define the reproducible image in Dockerfile by pasting this code:
FROM python:3.14.9-slim

WORKDIR /code

COPY requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir -r /code/requirements.txt

COPY app /code/app

CMD ["fastapi", "run", "app/main.py", "--port", "8000"]

How Does the Image Recipe Work?

  • The base image supplies Python 3.14.9 in a slim runtime.
  • WORKDIR gives the container a predictable application directory.
  • The requirements copy and package installation form a reusable dependency layer.
  • The application copy adds the existing app package after the dependencies.
  • CMD starts FastAPI on container port 8000.
  • Save Dockerfile.
  • Confirm that the Explorer sidebar lists Dockerfile inside ai-incident-router.

The Dockerfile Is Not Recognized?

Confirm that the filename uses a capital D. Check that the editor did not add an extension.

Ask for help with checking the image recipe.

✔️ Awesome, I've got everything!

Your Dockerfile now defines the Python runtime and FastAPI startup command.

ⓧ I'd like to double check the full code

FROM python:3.14.9-slim

WORKDIR /code

COPY requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir -r /code/requirements.txt

COPY app /code/app

CMD ["fastapi", "run", "app/main.py", "--port", "8000"]
Build and run the container

The local development server currently owns port 8000. Stopping it frees that port for the containerized service.

  • Switch back to the PowerShell terminal running the FastAPI development server.
  • Press Ctrl+C to stop the development server.

The PowerShell prompt returns inside ai-incident-router. Port 8000 is now available for the container.

The first image build downloads the Python base image. A few quiet minutes during this build are normal.

  • Build the ai-incident-router image by running this command:
docker build -t ai-incident-router .

What Does This Build Command Do?

Docker uses the current ai-incident-router folder as its build context. The tag names the completed image ai-incident-router.

The Dockerfile installs the pinned requirements before copying the application package. Future app-only changes can reuse the dependency layer.

PowerShell completes the build with an image tagged ai-incident-router.

The Image Build Failed?

  • Confirm that Docker Desktop is still running.
  • Confirm that PowerShell is inside ai-incident-router.
  • Compare Dockerfile with the full-file reference above.

Ask for targeted help with diagnosing the Docker build.

  • Start the image as the named incident-router container by running this command:
docker run -d --name incident-router -p 127.0.0.1:8000:8000 ai-incident-router

What Does This Run Command Do?

  • The -d flag runs the service in the background.
  • The --name value gives the container the reusable name incident-router.
  • The port mapping binds local address 127.0.0.1:8000 to container port 8000.
  • The final value selects the ai-incident-router image.

PowerShell prints a container identifier. The incident-router container is now serving the API in the background.

The Container Did Not Start?

  • Confirm that the local development server remains stopped.
  • Confirm that the image build completed with the ai-incident-router tag.
  • Check that no earlier container already uses the name incident-router.

Ask for help with diagnosing the container startup.

  • Switch back to the browser tab showing the interactive documentation from earlier.
  • Refresh http://127.0.0.1:8000/docs so it connects to the containerized service.

Before you submit the request, do you expect the container to preserve the fitted model's database prediction?

  • Submit this request body to POST /predict using the same interactive controls as earlier:
{"text":"replica lag is increasing"}

What Does This Request Prove?

The request reaches the FastAPI application running inside the container. Its result confirms that the image contains the fitted text-classification service.

The response body is {"category":"database"}. That is the hard part complete. Your tested model now produces the same corrected prediction from a reproducible Docker image.

The Documentation Page Did Not Load?

  • Confirm that you refreshed the address after starting the container.
  • Confirm that the address uses port 8000.
  • Confirm that the local development server did not restart in another terminal.

Ask for help with checking the containerized API.

Your service now passes local tests and runs from a reproducible container image. Next, GitHub Actions will repeat both quality checks on a clean hosted runner.

Ship Continuous Integration

Your AI incident router now passes two local tests. The same service also runs from a Docker image on your Windows machine.

Local success can drift. A forgotten test or broken image can reach the repository when checks depend on memory.

Continuous integration reruns those checks for every proposed change. GitHub Actions will run the tests on a clean hosted runner. It will also build the Docker image from scratch.

In this step, get ready to:
  • Define a CI workflow for pushes and pull requests.
  • Publish the committed project to a public GitHub repository.
  • Confirm the hosted runner passes every quality gate.
Create the CI workflow

A workflow records the checks that every clean runner must perform. This one installs the pinned environment before testing the API and building its image.

  • Create .github/workflows/ci.yml inside ai-incident-router through the VS Code Explorer sidebar with the following content:
name: CI

on:
  push:
  pull_request:

permissions:
  contents: read

jobs:
  test-and-build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7

      - name: Set up Python
        uses: actions/setup-python@v7
        with:
          python-version: "3.14.9"

      - name: Install dependencies
        run: python -m pip install -r requirements.txt

      - name: Run tests
        run: pytest

      - name: Build Docker image
        run: docker build -t ai-incident-router .

What Does This Workflow Do?

  • The push trigger runs the workflow after repository updates.
  • The pull_request trigger checks proposed changes before they merge.
  • The contents permission gives the workflow read-only repository access.
  • The test-and-build job uses ubuntu-latest as its clean hosted environment.
  • The setup steps check out the repository and install Python 3.14.9.
  • The final steps install the pinned dependencies. They run the test suite. They build the ai-incident-router image.
  • Save .github/workflows/ci.yml.
  • Confirm that workflows/ci.yml appears beneath .github in the Explorer sidebar.

Workflow File in the Wrong Place?

Check that the complete path is .github/workflows/ci.yml inside ai-incident-router.

Confirm that the filename ends in .yml.

Ask for help with checking the workflow path and YAML indentation.

✔️ Awesome, I've got everything!

Your saved workflow is ready to travel with the first commit.

ⓧ I'd like to double check the full code

name: CI

on:
  push:
  pull_request:

permissions:
  contents: read

jobs:
  test-and-build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7

      - name: Set up Python
        uses: actions/setup-python@v7
        with:
          python-version: "3.14.9"

      - name: Install dependencies
        run: python -m pip install -r requirements.txt

      - name: Run tests
        run: pytest

      - name: Build Docker image
        run: docker build -t ai-incident-router .
Publish the repository

A Git repository records the project as a versioned snapshot. A public GitHub repository gives GitHub Actions access to that snapshot.

  • Visit GitHub in your browser.
  • Sign in with the account from setup if prompted.
  • Select New repository from the upper-right menu.
  • Enter ai-incident-router as the repository name.
  • Select the public visibility option.
  • Leave every repository initialization option cleared.
  • Select Create repository.

Your empty public repository is ready to receive the project. Its blank state prevents generated files from conflicting with your local commit.

  • Return to the PowerShell terminal in VS Code.
  • Create the local repository and record the complete project by running these commands:
git init -b main
git add .
git commit -m "Build AI incident router"

What Do These Commands Do?

  • The first command initializes the repository on the main branch.
  • The second command stages every project file that is not excluded by .gitignore.
  • The third command records the staged project as one snapshot named Build AI incident router.
  • Confirm that PowerShell prints a successful commit summary containing Build AI incident router.

Commit Did Not Complete?

Check the PowerShell prompt path. It should end with ai-incident-router.

If Git requests an author identity, follow its instructions before retrying the commit.

Ask for help with diagnosing the failed first commit.

The first push may ask you to authenticate with GitHub. Complete the browser sign-in if that prompt appears.

  • Copy the HTTPS URL shown on the empty repository page.
  • Replace REMOTE-URL in the code below with the copied repository URL.
  • Connect the local repository to GitHub and publish main by running these commands:
git remote add origin REMOTE-URL
git push -u origin main

What Do These Commands Do?

The first command names your GitHub repository origin. The second command publishes main and records its upstream branch.

  • Return to the ai-incident-router repository page in GitHub.
  • Refresh the browser page.

You should see the project files from your first commit. The list should include app, tests, Dockerfile, and .github.

Push Did Not Reach GitHub?

Confirm that you replaced REMOTE-URL with the URL copied from your repository.

Complete any GitHub authentication request before retrying the push.

Ask for help with diagnosing the GitHub push.

Inspect the hosted quality gates

The push starts the CI workflow on a clean hosted runner. Its logs reveal each command and result behind the final status.

The hosted run may stay in progress while the clean runner installs every pinned dependency from scratch.

Before you check the run, do you expect the clean runner to reproduce both local successes?

  • Select the Actions tab in the repository navigation.
  • Select the CI workflow.
  • Open the most recent workflow run.

The run should finish with a green check. That status confirms every workflow step completed successfully.

  • Expand the Run tests step in the workflow log.

You should see pytest report 2 passed.

  • Expand the Build Docker image step in the workflow log.

You should see the Docker image build complete successfully. The clean runner has reproduced the same image build that worked on your Windows machine.

Workflow Did Not Turn Green?

Open the first failed step because later failures can be consequences of that earlier problem.

Compare a failed test step with your saved requirements.txt and tests/test_main.py. Compare a failed image step with your saved Dockerfile.

Ask for help with interpreting the failed workflow log.

That green run is a major shipping milestone. Your API tests now pass in a clean hosted environment. Your Docker image also builds outside your local machine.

Secret mission

Add a Model-Aware Readiness Probe

Add a readiness endpoint that runs a sentinel incident through the fitted model. Prove the result locally before the existing GitHub Actions workflow validates the extension.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep the service ready, stop the container for later, or remove the resources. The main path costs $0 because its public GitHub repository uses standard GitHub-hosted runners.

Cost warning

Docker Desktop is free for personal use. It is also free for education or non-commercial open source work.

Small businesses qualify when they have fewer than 250 employees plus less than $10 million in annual revenue.

Larger-enterprise commercial use requires a paid subscription.

Resources you used:

  • The local ai-incident-router folder containing the application code plus its tests.
  • The repository-local .venv environment.
  • A local Docker image named ai-incident-router.
  • A local Docker container named incident-router.
  • The public GitHub repository named ai-incident-router.
  • The GitHub Actions workflow stored in the public repository.

Keep everything running

No action needed. Choose this if you want the incident router ready for more development or demonstrations.

  • Keep the incident-router container running at 127.0.0.1:8000.
  • Keep the ai-incident-router image for reproducible local runs.
  • Keep the local ai-incident-router folder for future changes.
  • Keep the public repository so its remote code remains available.
  • Keep the workflow so future pushes continue to run the tests plus the image build.

Your API remains available through the running container. GitHub Actions only runs when its configured repository events occur.

Pause - I'll come back to this later

Stop the running container to free its active memory. The container plus its image remain available for your return.

  • Return to the PowerShell terminal from earlier.
  • Stop the running container by running this command:
docker stop incident-router

What does this command do?

The command stops the running incident-router container. Its configuration remains available for another run.

  • Confirm the stopped state for incident-router in Docker Desktop.
  • Keep the ai-incident-router image unchanged.
  • Keep the local ai-incident-router folder unchanged.
  • Keep the public repository unchanged.

You should see incident-router listed as stopped. The API at 127.0.0.1:8000 no longer responds while the container is stopped.

Delete - I don't want to use this again

Remove the local Docker resources before deleting the project files. This clears the container plus the reusable image from your Windows device.

  • Return to the PowerShell terminal from earlier.
  • Stop the container plus remove both Docker resources by running these commands:
docker stop incident-router
docker rm incident-router
docker rmi ai-incident-router

What do these commands remove?

  • The docker stop command stops the running container.
  • The docker rm command removes the stopped container.
  • The docker rmi command removes the local image.
  • Confirm incident-router no longer appears in the Docker Desktop container list.
  • Confirm ai-incident-router no longer appears in the Docker Desktop image list.

That clears the local Docker resources. Your project files plus remote repository remain available until you remove them below.

Having trouble removing a Docker resource?

A removal command can report a missing resource when that resource was already deleted. Confirm the container plus image lists before repeating the cleanup.

Help me diagnose my Docker cleanup commands.

  • Deactivate the current .venv environment by running this command:
deactivate

What does this command do?

The command exits the active .venv shell environment. The environment files remain inside the project folder until you delete that folder.

  • Confirm .venv no longer appears at the start of the PowerShell prompt.
  • Close Visual Studio Code to release the project files.
  • Return to the parent directory containing ai-incident-router in File Explorer.
  • Delete the ai-incident-router folder.
  • Confirm the folder no longer appears in File Explorer.

Deleting the project folder also removes its .venv environment plus every local project file.

Deleting the public repository is irreversible. Keep it if you still need the remote code or its CI history.

  • Return to the public ai-incident-router repository on GitHub from earlier.
  • Open the repository settings page.
  • Scroll to the repository removal section at the bottom.
  • Start the permanent repository deletion process.
  • Enter ai-incident-router when GitHub asks for confirmation.
  • Complete the final deletion confirmation.
  • Confirm the public repository URL no longer displays the project.

The repository deletion also removes its GitHub Actions workflow. Its run history disappears with the repository.

Nice Work!

Nice Work!

You did it! Your AI incident router now classifies operational reports through a tested service with automated delivery checks.

You've learned how to:

  • Build a typed FastAPI service that routes incident reports through a fitted TF-IDF and Complement Naive Bayes text-classification pipeline.
  • Protect the service with automated API tests in pytest before reproducing its predictions from a Docker container.
  • Ship a continuous integration workflow with GitHub Actions that tests the API and builds its image on every push or pull request.
  • Secret Mission: Extend the service with a model-aware readiness probe that verifies a sentinel prediction through GET /health.

Ready to quiz yourself? Let's see what you remember.