Eric Le

Project

Jobtriage

A job-search agent over Sweden's public JobTech API that turns a profile and a question into a visual shortlist, for any profession.

problem

Sweden's job board can filter by occupation, region, and deadline. It cannot answer "which of these mention agentic systems and close before Friday", so I built something that could, against the same public API.

Job boards return a list. The question a candidate has is which roles fit them, why, which one closes first, and which two are worth putting side by side. That is a decision made against a profile, and a ranked list is not shaped for it.

try

Two paths share the same interface. A visitor without a key gets a mock replay of four recorded conversations, enough to drive the canvas. A visitor with a key from Anthropic, OpenAI, or Gemini drives the live agent against JobTech data. The key stays in the browser and is never stored on the server.

The deployed path searches the live JobTech taxonomy and JobSearch APIs, so results are current. Local development runs against a fixed corpus instead, which is what makes a retrieval change measurable rather than merely different.

canvas

The agent does not stop at a paragraph of text. Its tool calls drive a React Flow canvas, so the same set of ads becomes triage clusters, a deadline timeline, a side-by-side comparison, or a pinned shortlist.

The canvas makes the next decision visible rather than leaving a candidate to reconstruct it from a chat transcript.

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. The table is the local set. The deployed agent carries a smaller one, since corpus tools such as triageBatch and deadlineWatch have nothing to search against live data.

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

retrieval

Finding the right ad is the thing everything else rests on, so it was measured rather than assumed. The evaluation covers the local corpus, not the live JobTech path.

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, which is the opposite of the result hybrid retrieval is usually assumed to give. 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, English 0.700 0.855 384
e5-base, multilingual 0.780 0.965 768
e5-large, multilingual 0.860 0.945 1024

English-only MiniLM gives up 11 points of recall@10 against e5-base on the Swedish set, and fusing BM25 back in recovers 7 of them. That is the strongest case for keeping hybrid: it holds up when the encoder is weaker than the one it was tuned on. e5-large lifts P@1 by another 8 points over e5-base, for about 70% more memory and dense latency, and hands back two points of recall@10, so e5-base ships as the default.

system

Two postures share one agent shell: same prompt, same tools, different data path. 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 and multilingual-e5-base dense, fused by RRF over SQLite
Providers
Anthropic, OpenAI, Gemini by visitor key, a no-key mock replay, local Ollama in development
Domain
Cloudflare A record fronting Vercel

React Flow surfaces four canonical views, each carrying custom nodes rather than the React Flow defaults.

The agent is measured too, on ten scripted probes against the live path. It passed five. It picked the right tools 92% of the time and then answered incompletely, which makes the gap a prompt and answer-quality problem rather than a routing one. That is the next thing to fix.