A working prototype for the ADM AI Challenge: one pool of internal-comms posts, routed and re-ranked per person by a model that learns from what they click. The point isn't a prettier intranet — it's AI as a personalization + routing layer on top of comms you already send, with its reasoning shown on screen so the room can argue about it.
backend/ FastAPI service + SQLite event log + the model
frontend/ single-file React app wired to the backend
1. Backend
cd backend
pip install -r requirements.txt
uvicorn main:app --reload --port 80002. Frontend — just open frontend/index.html in a browser.
(If your browser blocks file:// requests, serve it: python -m http.server 5500
from the frontend/ folder, then visit http://localhost:5500.)
The page talks to the API at http://localhost:8000. Change API at the top of
the <script> in index.html if you run the backend elsewhere.
- Switch personas (top-right). The same 15 posts re-rank instantly — a program analyst sees grants and governance up top; the data/AI analyst sees the AI pilots. That's cold-start from a role prior, no clicks yet.
- Interact. Read a couple of cards, hit an action button, thumbs-down something irrelevant, dismiss the cafeteria post. Watch the "What the model knows" panel fill in and the For you feed reorder.
- Point at the override. The network outage and mandatory training stay pinned in Pushed to you the whole time — the model never gets to hide those.
- Retrain. After a dozen interactions, hit Retrain on the event log to flip from the live content model to a trained logistic model.
The most important design decision is not which model — it's the value score
in recommender.py. A raw click is a bad training target because it rewards
clickbait. Instead every event collapses to one graded label: an impression scrolled
past is a weak negative, an open is mild, a full read is strong, a completed action
is stronger, an explicit "useful" is the ceiling, a dismiss is negative. Both models
learn on that.
- Tier A — content-based profile (default, always on). Each item is a TF-IDF vector over its category + tags + text. A user's profile is a time-decayed, value-weighted sum of the items they've engaged with (Rocchio relevance feedback). Score = cosine similarity. Updates instantly per click, explainable (we can name the tags that drove each card), handles cold start via role priors.
- Tier B — trained engagement model (opt-in). A logistic regression over item features + role, trained on the whole event log to predict P(engage). The "real trained model" story. Needs data: with only a handful of events it underfits — which is itself the honest lesson about when each tier wins.
| method | path | purpose |
|---|---|---|
| GET | /personas |
role options (cold-start priors) |
| POST | /user |
set a user's role |
| GET | /feed?user_id=&model= |
ranked feed, split into push / recommended / discovery |
| POST | /event |
log an interaction (the model's training data) |
| GET | /profile/{user_id} |
the learned interest profile |
| POST | /retrain |
train the Tier-B model on the event log |
| POST | /reset/{user_id} |
clear a user's history |
- Mandatory override. Personalization governs the discretionary feed only. Urgent and mandatory items bypass the model entirely and always reach the user.
- Exploration. A Discover slot injects an off-profile item so the feed can't collapse into a filter bubble — a real governance risk for internal comms.
- No clickbait. The value score deliberately rewards reading and acting over raw clicks.
- Transparency. The right-hand panel shows the model's current interest profile and every card shows why it surfaced. This is what makes an AI-decides-what-you-see system defensible — and it's the conversation the challenge is meant to spark.
All content is illustrative and HC/PHAC-flavoured. None of it is real.