Label classifier
Classify text into categories with no training and no inference-time model call
Showcase — Label classifier
Classify text into categories with no training and no inference-time model call beyond an embedding. Each category is a short description, embedded once; an incoming message is embedded and assigned to the nearest description by cosine similarity. Adding a category is one sentence — the "retraining" is instant.
Run
bash bootstrap-secrets.sh # reads ../../../../.env, writes secrets/
docker compose up --build # default: PROVIDER=openai
Open http://localhost:3000.
To run against Gemini instead:
PROVIDER=gemini docker compose up --build
What's where
backend/ai_openai.py/backend/ai_gemini.py— the label descriptions (embedded once), and nearest-label assignment by cosine similarity.backend/main.py— identical FastAPI loader; reads PROVIDER and dispatches.frontend/app/page.tsx— message box + predicted label + all scores.
This is a fine baseline classifier. When the categories get subtle or the volume gets large, that's when a fine-tune (later in the course) earns its keep.
Stop
docker compose down
Run locally
Download the project as a ZIP and run it with Docker. Brings up a FastAPI backend + Next.js frontend on localhost:3000.
unzip label-classifier.zip
cd label-classifier
bash bootstrap-secrets.sh # one-time: pulls API keys into ./secrets
docker compose up --build # default provider: openai
# or: PROVIDER=gemini docker compose up --build
Type some input, pick a provider, and run the same code shown in Source against the live API. Sign-in required.
The same modules the Run button hits. The whole project (frontend, Dockerfile, compose) is in the ZIP under README.
backend/ai_openai.py
"""Showcase 2 (OpenAI): zero-shot classification with embeddings.
No training, no fine-tuning, no model call at inference beyond embedding. Embed
a set of label descriptions once, embed the incoming text, and assign whichever
label sits closest in cosine similarity. Add a new category by writing a
sentence — that's the whole "retraining" step.
"""
import math
from openai import OpenAI
_client = OpenAI()
_MODEL = "text-embedding-3-small"
# Label -> a short description. Richer descriptions embed to better anchors than
# bare one-word labels, so the nearest-neighbor decision is sharper.
_LABELS = {
"billing": "A question or problem about payment, charges, invoices, or refunds.",
"technical support": "Something is broken, erroring, crashing, or not working.",
"feature request": "Asking for a new capability or an improvement.",
"shipping": "About delivery, tracking, or the status of an order in transit.",
"account access": "Login trouble, passwords, two-factor, or being locked out.",
"praise": "Positive feedback or a compliment about the product or service.",
}
_names = list(_LABELS.keys())
_label_vecs: list[list[float]] | None = None
def _embed(texts: list[str]) -> list[list[float]]:
return [item.embedding for item in _client.embeddings.create(model=_MODEL, input=texts).data]
def _cosine(a: list[float], b: list[float]) -> float:
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(y * y for y in b))
return dot / (na * nb) if na and nb else 0.0
def _labels() -> list[list[float]]:
global _label_vecs
if _label_vecs is None: # embed the label descriptions once
_label_vecs = _embed(list(_LABELS.values()))
return _label_vecs
def run(text: str) -> str:
tv = _embed([text.strip()])[0]
scored = sorted(
((_cosine(tv, lv), name) for lv, name in zip(_labels(), _names)),
reverse=True,
)
best_score, best_name = scored[0]
rows = "\n".join(f" {score:.3f} {name}" for score, name in scored)
return f"Predicted category: {best_name} (cosine {best_score:.3f})\n\nAll labels:\n{rows}"
backend/ai_gemini.py
"""Showcase 2 (Gemini): zero-shot classification with embeddings.
Same nearest-label idea, Gemini's embedding model.
"""
import math
import os
from google import genai
_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
_MODEL = "gemini-embedding-001"
_LABELS = {
"billing": "A question or problem about payment, charges, invoices, or refunds.",
"technical support": "Something is broken, erroring, crashing, or not working.",
"feature request": "Asking for a new capability or an improvement.",
"shipping": "About delivery, tracking, or the status of an order in transit.",
"account access": "Login trouble, passwords, two-factor, or being locked out.",
"praise": "Positive feedback or a compliment about the product or service.",
}
_names = list(_LABELS.keys())
_label_vecs: list[list[float]] | None = None
def _embed(texts: list[str]) -> list[list[float]]:
return [e.values for e in _client.models.embed_content(model=_MODEL, contents=texts).embeddings]
def _cosine(a: list[float], b: list[float]) -> float:
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(y * y for y in b))
return dot / (na * nb) if na and nb else 0.0
def _labels() -> list[list[float]]:
global _label_vecs
if _label_vecs is None:
_label_vecs = _embed(list(_LABELS.values()))
return _label_vecs
def run(text: str) -> str:
tv = _embed([text.strip()])[0]
scored = sorted(
((_cosine(tv, lv), name) for lv, name in zip(_labels(), _names)),
reverse=True,
)
best_score, best_name = scored[0]
rows = "\n".join(f" {score:.3f} {name}" for score, name in scored)
return f"Predicted category: {best_name} (cosine {best_score:.3f})\n\nAll labels:\n{rows}"
Project files
.gitignoreREADME.es.mdREADME.mdbackend/Dockerfilebackend/ai_gemini.pybackend/ai_openai.pybackend/main.pybackend/requirements.txtbootstrap-secrets.shdocker-compose.ymlfrontend/Dockerfilefrontend/app/layout.tsxfrontend/app/page.tsxfrontend/next.config.tsfrontend/package.jsonfrontend/tsconfig.json