Eric Le

Project

Jobtriage

A live agent that triages Swedish job ads against any profile.

problem

I wanted a project where a language model drove the interface rather than sitting behind it. Handing it the canvas, the filters, and the layout is a different problem from returning a good answer.

Job boards rank for what the platform earns from rather than for what fits the candidate. Profile-driven match is a first-class operation here rather than a filter bolted onto a search box.

system

Two postures share one agent shell: same prompt, same tools, different data path.

agent shell
deploy
JobTech taxonomy and JobSearch APIs
local
SQLite corpus, hybrid retrieval stack

The deployed posture runs against the JobTech taxonomy and JobSearch APIs. The local CLI and local dev run the hybrid retrieval stack against a SQLite corpus.

Frontend
Next.js App Router on Vercel, Vercel AI SDK
Backend
FastAPI on Cloud Run europe-west1, 1Gi memory
Retrieval
BM25 + multilingual-e5-base dense + RRF over SQLite
BYOK
Anthropic, OpenAI, Gemini, local Ollama, mock replay
Domain
Cloudflare A record fronting Vercel

Two entry paths carry the surface. Mock replay lets a recruiter drive the agent in five seconds with no key. BYOK lets a technical visitor drive it with their own. Most agent demos offer neither.

retrieval

Both paths depend on the same thing underneath: finding the right ad. That was measured rather than assumed.

Measured on a 50-query Swedish golden set against a 59-ad corpus drawn from Spotify, Klarna, Volvo Group, Volvo Cars, Ericsson, HT Engineering, Stig Ericsson Bil, Montico, and Isaksson Rekrytering. Embeddings from intfloat/multilingual-e5-base.

Three numbers repeat through this section: P@1 asks whether the top result is right, R@10 asks whether the right one is somewhere in the top ten, and p95 ms is the 95th-percentile query latency.

hybrid retrieval ablation
configuration P@1 R@10 p95 ms
filter-only 0.020 0.150 0.0
bm25-only 0.680 0.920 1.2
dense-only 0.780 0.965 7.8
hybrid 0.720 0.950 15.2

Dense alone takes P@1 by 6 points over hybrid on this corpus. Hybrid earns its place on adversarial queries where an exact keyword carries the match, such as a model name or employer jargon. An RRF score floor at JOBTRIAGE_RRF_FLOOR=0.025 suppresses low-relevance noise at the API boundary.

multilingual encoder comparison (dense)
encoder P@1 R@10 dim
MiniLM (en) 0.700 0.855 384
e5-base (ml) 0.780 0.965 768
e5-large (ml) 0.860 0.945 1024

English-only MiniLM gives up 11 points of recall@10 against e5-base on the Swedish set. e5-large lifts P@1 by another 8 points over e5-base and hands back two points of recall@10, so e5-base ships as the balanced default. The MiniLM dense numbers run slightly suppressed because the e5 prefix tokens it never trained on read as noise. The encoder choice carries the result rather than decorating it.

agent

Retrieval finds the right ad. What the agent does with it once found is a separate problem.

Spatial tool pairings are pinned in the system prompt, so the agent fires a spatial tool after every data tool and the canvas answers the question rather than illustrating it.

searchJobs
placeAds
triageBatch
groupAds
matchProfile
connectProfileToAds
compareRoles
pairAdsForCompare
deadlineWatch
placeAdsOnTimeline
trackStatus
markStatus

React Flow surfaces four canonical views: triage clusters, deadline timeline, side-by-side compare, and pinned shortlist. Each view carries custom nodes rather than the React Flow defaults.