Skip to content

Latest commit

Β 

History

90 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

The Observable Job Agent

Job Scout: a real AI job-matching agent you can see inside

Upload your CV (PDF). Get real openings ranked 0 to 100 for fit, honest gap explanations, and a tailored application pack. Then ask for all of it out loud. Every LLM and tool call traced in Opik from run one.

The human applies. The agent never submits.

Python Version LangGraph Opik Gradio Next.js Tests License

Job Scout Phase 2 architecture


The Jobvis voice console: the orb at rest, with the session restored from the checkpoint

Jobvis, the voice console added in Part 4. The orb is the state indicator, and everything beside it came from the checkpoint before a word was said.

πŸ“š The series

Every part ships as a GitHub release, so the code you clone matches the post you read.

Part Focus Blog post Code release
1 Build Build your own Job Agent - Part 1 part1.0
2 Extend, then evaluate Build your own Job Agent - Part 2 part2.0
3 Self-improve, with receipts Build your own Job Agent - Part 3 part3.0
4 (this release) Speak Build your own voice agent part4.0
# πŸ“₯ Clone the release that matches the part you are reading:
git clone --branch part4.0 https://github.com/jamwithai/observable-job-agent

πŸš€ Quick start

Prerequisites: Python 3.12 (the project pins >=3.12,<3.13) and uv. The voice console also needs Node 20+.

git clone --branch part4.0 https://github.com/jamwithai/observable-job-agent
cd observable-job-agent
uv sync --all-groups
cp .env.example .env    # add one LLM key (see below)
make test               # 230 tests, no keys or network needed
make app                # http://localhost:7860

Keys, honestly:

  • Tests, job sources (Remotive + offline cache), and the fabrication validator run with no keys at all.
  • The agent steps (profile extraction, ranking, tailoring) need one LLM key: OPENAI_API_KEY, or free via SCOUT_MODEL=groq:... (free tier) or ollama:... (local).
  • Opik tracing has its own free key: docs/opik_setup.md.
  • The voice console is optional and needs an ElevenLabs key. Everything else works without it.

The app remembers your CV and chosen locations between runs ("Start over" forgets). Jobs are always fetched fresh.

πŸŽ™οΈ Jobvis, the voice console (optional, Part 4)

Ask out loud, "find me jobs", and the search starts. Jobvis tells you it has begun, goes quiet while it runs, then breaks the silence itself to say what it found. Ask it to tailor an application and the finished pack appears on screen while it reads you the highlights.

# 1. Add to .env: an ElevenLabs key with the Agents Platform (Conversational AI)
#    read and write scopes, plus a voice id you have added to My Voices.
#    ELEVENLABS_API_KEY=sk_...
#    ELEVENLABS_VOICE_ID=...      # Voices > My Voices > ... > Copy voice ID
make jobvis-agent     # creates the agent from voice/persona.py, prints the agent id
                      # paste it back as ELEVENLABS_AGENT_ID
make web-build        # npm ci + a Next.js static export into web/out
make app              # wizard on :7860 AND the console on :8000, one process

The conversation runs in your browser over WebRTC, which is what buys real barge-in and the browser's own echo cancellation, with no audio library to build. Your key never leaves Python: the page asks for a short-lived session token and nothing more.

The grounding contract from Part 2 carries into the new modality. The browser forwards every question and never answers one, so the voice can only say what the LangGraph checkpoint returns. Watch the tool calls appear in the transcript panel as it talks.

Full chapter, including the demo script: docs/jobvis.md.

πŸ““ Interactive tutorials

✨ What's inside

  • A four-step wizard (Resume, Profile, Jobs, Tailor) with streamed progress and a per-run cost footer
  • A voice console with a Three.js orb that reacts to the real audio spectrum, live panels, and optional webcam hand control
  • You decide where to search: locations and remote are your call, not the model's guess
  • LLM-driven tool use: the model picks the search arguments; you watch it choose in the trace
  • Multi-source search: JSearch, Adzuna, Remotive, plus a committed offline cache
  • Named source failures: a source that returns nothing says why (quota exhausted, key rejected, timed out) instead of looking like a quiet day
  • Grounded tailoring: your CV becomes a typed corpus; every rewritten bullet carries a corpus_ref back to the real item
  • A deterministic fabrication validator: zero LLM calls, tunable thresholds (SCOUT_FAB_*), every report records the values it ran with
  • PDF rendering with a degradation contract: LaTeX via tectonic, falls back to .tex + Overleaf, never fails a run
  • Per-source spans: each job site is timed separately, so a slow search names the culprit instead of the total
  • A pipeline regression suite: job-scout-search-suite grades the cascade (no source over 8s, every source used contributed, the search returned something)
  • A prompt tuned against a deterministic check, not an LLM judge: fabrication 0.2768 to 0.1288 on fresh live jobs
  • The full eval stack in Opik: datasets from your own traces, experiments, LLM judges, an annotation queue, and judge-vs-human calibration

Stack: LangGraph, LangChain, Opik, Gradio, FastAPI, Next.js + Three.js, ElevenLabs Agents, Pydantic, httpx + pypdf.

πŸ—οΈ Architecture

CV extraction runs before the graph; the graph has one entry router and two doors:

extract_profile(cv) β†’ Profile ─┐
                               β–Ό
  START β†’ [route_entry] ── no selected job ──→ fetch_jobs β†’ rank_jobs β†’ [enough good matches?]
              β”‚                                    ↑                          β”‚ no
              β”‚                                    └──── reformulate_query β—„β”€β”€β”˜   β”‚ yes β†’ END
              └── selected_job_id set ──→ tailor β†’ validate_tailoring β†’ END
                  (reads profile + ranked jobs from the thread's checkpoint; nothing re-runs)
  • fetch_jobs: the model chooses the search_jobs arguments (query, country, remote)
  • rank_jobs: batches of 4 (SCOUT_RANK_BATCH), capped at SCOUT_MAX_JOBS (default 10); scored jobs are never re-scored
  • reformulate_query: broadens thin results, at most 2 loops
  • tailor + validate_tailoring: a second invocation on the same thread; the checkpoint is the handoff

The voice surface sits beside the wizard, not underneath it. Both start the same runs and read the same checkpoint, and make app runs them in one process because the bridge and the checkpoint are process-wide.

Full walkthrough: docs/architecture.md Β· Adding a job source: docs/extending_sources.md Β· The voice console: docs/jobvis.md

πŸ“‚ Project structure

observable-job-agent/
β”œβ”€β”€ src/job_scout/
β”‚   β”œβ”€β”€ app.py              # Gradio four-step wizard UI
β”‚   β”œβ”€β”€ api.py              # FastAPI: voice tokens, tool dispatch, SSE, serves the console
β”‚   β”œβ”€β”€ voice/              # persona.py (the agent as code), tools.py, bridge.py, announce.py
β”‚   β”œβ”€β”€ candidate_store.py  # the persisted candidate: profile + CV text + preferences
β”‚   β”œβ”€β”€ runner.py           # one orchestration path for UI and batch (tracing, cost, latency)
β”‚   β”œβ”€β”€ profile.py          # CV text β†’ Profile (pre-graph extraction)
β”‚   β”œβ”€β”€ corpus.py           # your CV as typed, addressable corpus items
β”‚   β”œβ”€β”€ validation.py       # deterministic fabrication validator (difflib, zero LLM)
β”‚   β”œβ”€β”€ renderer.py         # Jinja2 β†’ LaTeX β†’ PDF via tectonic (degrades to .tex)
β”‚   β”œβ”€β”€ evals/              # metrics.py: ProfileFieldAccuracy, FabricationRate, FitExplanationQuality
β”‚   β”œβ”€β”€ graph/              # graph.py (entry router + search + tailor), state, schemas, nodes/, prompts/
β”‚   β”œβ”€β”€ templates/          # cv.tex.j2, the single ATS-friendly CV template
β”‚   └── tools/              # jobs_api.py (the source cascade), cv_reader.py, research.py
β”œβ”€β”€ web/                    # the Jobvis console: Next.js + Three.js orb, WebRTC, hand tracking
β”œβ”€β”€ notebooks/              # phase1_walkthrough.ipynb, phase2_evaluation.ipynb, phase3_ollie.ipynb
β”œβ”€β”€ scripts/                # batches, dataset builders, eval suites, annotation queue, agent setup
β”œβ”€β”€ data/                   # cached_jobs.json, fixture_cvs/, fixture_linkedin/, labels/
β”œβ”€β”€ docs/                   # architecture, jobvis, opik_setup, findings, baseline + batch reports
└── tests/                  # 230 tests (LLM mocked, network mocked, Opik off)

πŸ”§ Commands

make app           # launch the wizard, and the console if it is built
make test          # run the suite
make jobvis-agent  # create or update the ElevenLabs agent from voice/persona.py
make web-build     # build the console (npm ci + Next.js static export)
make web-dev       # Next dev server on :3000 against the API on :8000
make web-assets    # vendor the MediaPipe hand-tracking assets (optional, ~43MB)
make batch         # Part 1 baseline batch (prints projected cost first)
make tailor-batch  # Part 2 tailoring batch
make eval-datasets # push ranking + tailoring datasets to Opik from traces
make evals         # eval harness (extraction/ranking/tailoring/trajectory/calibration)
make gates         # deterministic regression gate (no LLM calls)
make search-bench  # paired before/after source timing
make queue         # create the Opik annotation queue
make lint          # ruff

πŸ› οΈ Troubleshooting

  • All jobs say source: cache: no live-source keys or no network. Expected; the cache is the offline fallback.
  • "Couldn't read a profile ... api_key": add an LLM key to .env, or set SCOUT_MODEL to a free groq:/ollama: model.
  • No traces in Opik: check OPIK_ENABLED=true, OPIK_API_KEY, OPIK_WORKSPACE. See docs/opik_setup.md.
  • 502 from /api/voice/token: the ElevenLabs key is missing the Agents Platform (Conversational AI) scopes. It returns 401 on the agent endpoints and nowhere else, so everything else looks fine.
  • Jobvis sounds like nobody in particular: ELEVENLABS_VOICE_ID is empty and the name lookup found nothing. A voice is not yours until you add it under Voices > My Voices, and only then does the API list it.
  • The console is not on :8000: run make web-build first. make app serves web/out, which does not exist until you build it.

Cost: reproducing Parts 1 to 3 is a few dollars end to end with API models (the 15-case tailoring batch cost $0.37), or free with local models and free tiers. Part 4 adds ElevenLabs, whose free tier gives about 15 conversation minutes a month; conversation minutes are a separate pool from text-to-speech characters.


Clone it, add one LLM key, make app, and drop in a fixture CV.

Built with love by Jam with AI

πŸ“„ License

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

214 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages