23 KiB
Job Agent Project Plan
Objective
Build a local AI-assisted job discovery system that:
- understands your CV and work experience from a PDF,
- uses an additional markdown profile as structured context for more precise matching,
- understands the kinds of jobs you want,
- searches and extracts job listings from major French job sites,
- ranks listings against your profile,
- explains why each listing is or is not a good fit,
- lets you discuss results through OpenCode using your connected LLM providers such as OpenAI.
The system is not a generic job recommender. It is optimized for a narrow Target Role Profile:
- junior Data Engineer roles, around 2-3 years of experience,
- strong Python,
- good SQL, Terraform, GCP, and BigQuery,
- France-based CDI listings only,
- French and English listings both allowed, with a slight preference for French,
- no ranking preference between remote, hybrid, and onsite.
v1 Scope
The first version should optimize for job discovery and ranking, not application automation.
Included in v1:
- PDF CV ingestion and structured profile extraction
- Markdown profile ingestion as a first-class structured input
- One saved default Search Preferences profile plus temporary chat overrides
- Read-only scraping / extraction from one public job board first
- Normalized storage of logical Job Listings and Listing Snapshots
- Matching and ranking pipeline with deterministic-first scoring
- Structured explanations with fit score, strengths, blockers, and summary
- Dismissed Listing memory so rejected listings stay hidden by default
- CLI-oriented workflow that fits OpenCode usage
- OpenCode discussion over stored results, with refresh as an explicit action
Explicitly out of scope for v1:
- Automatic application submission
- Cover letter generation as a core flow
- Full browser UI product
- CRM-grade candidate tracking system
- Support for every job site from day one
- Login-required scraping flows
- Salary-based ranking or salary filtering
Resolved Product Decisions
These decisions were resolved during planning and should be treated as the default product behavior for v1.
Search Target
- Primary matches: Data Engineer, Analytics Engineer, BI Engineer with strong SQL/Python pipelines, Junior Data Platform Engineer
- Stretch Opportunities: ML Engineer, Data Scientist, Backend Engineer, Senior Platform Engineer
- Seniority-mismatched roles should not rank in the main list; they belong in Stretch Opportunities
Geography and Contract Rules
- Only France-based listings are in scope
- France-based means the listing explicitly targets France or a French city or region
- Company nationality alone is not enough
- Only CDI listings belong in the main ranking
- CDD, freelance, internship, and alternance listings are excluded in v1
- Remote, hybrid, and onsite are treated equally if the listing is in France
Language and Freshness
- French and English listings are both in scope
- French gets a small ranking preference
- Listings from the last 14 days are preferred
- Listings from 15-30 days are penalized
- Listings older than 30 days are hidden unless the source shows a recent refresh
Crawl and Refresh Rules
- v1 starts with a single source: Apec
- Only public listings are in scope in v1
- Refresh is explicit, not automatic during chat
- Refresh means: crawl current enabled sources using saved Search Preferences, then normalize, deduplicate, and re-rank automatically
- Chat-level Search Preference overrides can re-rank stored results immediately without triggering a new crawl
- Stored results remain discussable, but the system should warn if the latest crawl is older than 3 days
Profile Merge Rules
- PDF CV is the source of truth for raw career history unless explicitly corrected
- Markdown profile is the source of truth for curated interpretation of strengths and goals
- Search Preferences from chat override current-session ranking priorities only
- The normalized Candidate Profile must be human-inspectable and editable in YAML or JSON
Data Retention and Explanation Rules
- Store logical Job Listings separately from Listing Snapshots
- Keep raw HTML/content snapshots plus extraction metadata by default
- Keep screenshots only for failures or debug mode
- Dismissed Listings stay hidden by default but remain recoverable on request
- Explanations must show both strengths and blockers
- Deterministic ranking remains primary; the LLM may slightly adjust and explain, but should not dominate ordering
Primary User Flow
- You provide a PDF CV and a markdown profile.
- The system extracts and merges both into a structured candidate profile.
- You save default Search Preferences and optionally add temporary chat overrides.
- You run an explicit refresh.
- The system crawls Apec public listings using broad source-side filters for France and CDI.
- The system stores raw artifacts, normalizes data, deduplicates into logical Job Listings and Listing Snapshots, and ranks results.
- The system presents a ranked main list plus a separate Stretch Opportunities section.
- An LLM adds bounded explanation and analysis on top of deterministic ranking.
- You can ask follow-up questions in OpenCode, for example:
- "Show me the best Data Engineer jobs from the latest crawl."
- "Why was this Apec listing ranked low?"
- "For this session, bias toward Analytics Engineer roles and re-rank."
- "Show dismissed listings from the last refresh."
Target Job Sources
v1 target:
- Apec
Post-v1 targets:
- Welcome to the Jungle
- Indeed France
Recommended rollout order:
- Apec
- Welcome to the Jungle
- Indeed France
Reasoning:
- Start with one adapter and a stable data model.
- Some sites are more hostile to scraping or have more dynamic rendering.
- A phased rollout reduces debugging complexity and anti-bot risk.
Product Requirements
Core Requirements
- The system must extract structured profile data from a PDF CV.
- The system must ingest a markdown profile containing curated context that may be missing or ambiguous in the CV.
- The system must let you define Search Preferences beyond what is written in the CV.
- The system must support one saved default Search Preferences profile and temporary chat overrides.
- The system must query public France-based job listings, starting with Apec only in v1.
- The system must support Playwright-based extraction for JavaScript-heavy websites.
- The system must store raw artifacts, normalized Job Listings, and Listing Snapshots locally.
- The system must deduplicate reposted or updated pages into one logical Job Listing when confidence is high.
- The system must rank job listings with transparent reasoning.
- The system must keep deterministic ranking primary and use the LLM only as a bounded secondary layer.
- The system must expose results in a format that is easy to inspect from OpenCode.
- The system must support repeated refresh runs without flooding results with duplicate listings.
- The system must support Dismissed Listings that remain hidden by default but recoverable on request.
- The system must support re-ranking stored results after chat preference overrides without a new crawl.
Quality Requirements
- Explainability: every score should have a short rationale.
- Traceability: keep source URL, scrape timestamp, and extraction metadata.
- Resilience: one failing site adapter must not break the full refresh.
- Local-first privacy: CV and job data stay local by default.
- Configurability: role filters and ranking criteria must be editable.
- Transparency: the structured Candidate Profile must be inspectable and editable.
- Freshness awareness: the system must warn when discussion relies on crawl data older than 3 days.
Suggested Architecture
Use a modular Python application with clear boundaries.
Components
- Profile ingestion
- Preference management
- Site adapter for Apec
- Crawl artifact and snapshot storage
- Normalization and deduplication pipeline
- Matching and ranking engine
- LLM reasoning layer
- CLI commands for refresh, inspect, re-rank, dismiss, and explain
High-Level Flow
ingest-cvparses the PDF and produces a raw career-history contribution.ingest-profilereads a markdown profile and merges curated intent and clarifications into the candidate profile.set-preferencesstores the saved default Search Preferences.refreshcrawls Apec using broad source-side filters for France and CDI.- The crawl persists raw HTML/content and extraction metadata as Listing Snapshots.
- The pipeline normalizes and deduplicates snapshots into logical Job Listings.
rankcomputes deterministic scores first, then bounded LLM explanation and small adjustments.- OpenCode reads stored results, applies temporary preference overrides, and re-ranks without crawling again when requested.
Recommended Technical Stack
Core Language
- Python 3.13
Reasoning:
- The repo already starts in Python.
- Python is a good fit for scraping orchestration, PDF parsing, structured data pipelines, and LLM integration.
Browser Automation / Extraction
playwrightfor Python- Playwright MCP for development-time debugging and manual inspection
Reasoning:
- Many job boards render content dynamically.
- Python Playwright should be the runtime dependency for the product itself.
- MCP is best treated as a development and inspection tool, not the only runtime path.
Data Modeling and Validation
pydantic
Use for:
- candidate profile schema,
- search preferences schema,
- normalized job listing schema,
- scoring explanation schema.
Storage
- Start with SQLite
- Consider Postgres only if the project grows into a multi-user system
Store:
- candidate profile snapshots,
- preferences,
- raw scraped payloads,
- normalized job listings,
- score history,
- run logs.
CLI and Developer Experience
typerfor CLI commandsrichfor readable terminal output
LLM Integration
- OpenAI-compatible provider access through your OpenCode-connected providers
- Keep the LLM layer behind a small internal interface so providers can be swapped
Suggested uses for the LLM:
- reconciling CV text with markdown profile context,
- CV structuring when deterministic parsing is insufficient
- extracting nuanced skills and experience signals
- explaining ranking outcomes
- answering chat questions over stored job data
Avoid using the LLM for:
- the entire ranking logic,
- basic filtering,
- source-of-truth data storage.
PDF and Text Extraction
Candidates:
pypdfpymupdf- OCR only if needed later
Recommendation:
- Start with
pymupdforpypdffor text extraction. - Add OCR later only if the CV is image-based.
HTML Parsing and Helpers
beautifulsoup4as a fallback parser for static fragmentshttpxfor non-browser HTTP calls where a full browser is unnecessary
Proposed Local Data Model
CandidateProfile
- name
- summary
- location
- languages
- profile_notes
- target_roles
- seniority
- skills
- industries
- preferred_contract_types
- years_of_experience
- experience_entries
- education_entries
- exclusion_rules
Profile Sources
cv_pdfprofile_md- merge strategy metadata
Recommendation:
- Treat the markdown profile as the higher-trust source for intent, preferences, and clarifications.
- Treat the PDF CV as the source for factual career history unless explicitly corrected in the markdown profile.
- Persist the merged result as a human-editable normalized profile in YAML or JSON.
JobPreferences
- desired_titles
- required_keywords
- excluded_keywords
- target_locations
- remote_policy
- company_types
- board_filters
- language_preference
- include_stretch_opportunities
Notes:
- Salary is ignored in v1.
- Saved JobPreferences and temporary session overrides should be stored separately.
JobListing
- source
- source_job_id
- url
- title
- company
- location
- remote_type
- contract_type
- salary_text
- description_text
- tags
- published_at
- refreshed_at
- scraped_at
- raw_payload_path
- freshness_status
- dismissal_state
ListingSnapshot
- job_listing_id
- source
- source_job_id
- url
- captured_at
- html_snapshot_path
- extraction_metadata
- screenshot_path
DismissedListing
- listing_id
- dismissed_at
- reason
- hidden_by_default
JobScore
- listing_id
- deterministic_score
- llm_adjustment
- final_score
- positives
- concerns
- explanation
- stretch_reason
Matching Strategy
Use a hybrid ranking approach.
Deterministic Layer First
Compute a base score from explicit signals such as:
- title-family match
- skill overlap
- related-signal overlap
- seniority match
- location match
- contract type match
- language requirements
- freshness
- hard exclusions
Benefits:
- predictable behavior
- easier debugging
- lower cost than pure LLM ranking
Suggested deterministic rules for v1:
- Treat CDI and France-based scope as hard filters
- Treat non-target role families as heavy penalties or Stretch Opportunities
- Treat seniority mismatch as a strong penalty that usually pushes a listing into Stretch Opportunities
- Prefer explicit Data Engineer / Analytics Engineer / BI pipeline signals over generic Python backend signals
- Use a curated related-signal map for BigQuery, GCP, Dataflow, Airflow, dbt, warehousing, ETL/ELT, and SQL pipelines
- Penalize missing fields only when they block a critical decision, such as contract type or country
- Ignore salary entirely in v1
- Apply freshness weighting before the LLM layer
LLM Layer Second
Use the LLM to:
- interpret fuzzy fit,
- detect transferable experience,
- summarize tradeoffs,
- explain why a role is still interesting even if some keywords are missing.
Keep the LLM bounded:
- pass structured profile + structured listing,
- ask for JSON output,
- cap influence over final score.
- require positives and blockers in every explanation.
Scraping Strategy
Start with one adapter only: ApecAdapter.
Adapter Responsibilities
- navigate search results
- handle pagination
- capture job detail pages
- extract structured fields
- respect rate limits and retries
- emit raw + normalized records
Recommended Adapter Pattern
BaseJobBoardAdapterApecAdapterWelcomeToTheJungleAdapterIndeedAdapter
Each adapter should define:
- search URL generation
- page navigation flow
- field extraction selectors
- fallback extraction logic
- deduplication key strategy
Important Constraints
- Review site terms of service before scraping.
- Expect anti-bot measures, dynamic DOM changes, and inconsistent markup.
- Prefer low-volume, respectful scraping with caching.
- Treat login-required flows as a later phase unless strictly necessary.
v1 Crawl Policy
- Use broad source-side filters for France and CDI when the source supports them
- Avoid over-filtering by title at crawl time; keep recall reasonably broad and rank afterward
- Crawl public listings only
- Persist raw HTML/content snapshots and extraction metadata for every Listing Snapshot
- Persist screenshots only for failures or debug mode
OpenCode Integration Plan
The simplest useful approach is not to build a separate chat agent first.
Instead:
- keep the job system as a local Python app,
- expose CLI commands that produce structured JSON and readable markdown,
- use OpenCode as the conversational layer on top.
OpenCode should behave as a tool-using assistant over local data and CLI commands, not as a separate long-running orchestration agent.
Example commands:
python -m job_research ingest-cv data/cv.pdfpython -m job_research ingest-profile data/profile.mdpython -m job_research set-preferences config/preferences.yamlpython -m job_research refresh --source apecpython -m job_research rank --top 50python -m job_research rerank --session-overrides overrides.yamlpython -m job_research explain --job-id <id>python -m job_research dismiss --job-id <id>python -m job_research show-dismissedpython -m job_research export --format markdown
This gives OpenCode something concrete to call and inspect while still letting you discuss opportunities in natural language.
Suggested Folder Structure
docs/
job-agent-project-plan.md
src/
job_research/
cli.py
config.py
models.py
storage.py
freshness.py
profile/
ingest.py
extract.py
boards/
base.py
apec.py
welcome_to_the_jungle.py
indeed.py
listings/
normalize.py
dedupe.py
ranking/
rules.py
llm.py
reporting/
explain.py
export.py
data/
raw/
snapshots/
normalized/
runs/
profiles/
exports/
preferences/
tests/
Executable Roadmap
Phase 1: Foundations and Local State
Goal:
-
establish the package shape, persistence model, and command surface for a single-user local workflow
-
Create Python package structure
-
Add configuration and CLI skeleton
-
Define Pydantic models
-
Set up SQLite storage
-
Add logging and run tracking
Deliverables:
- package scaffold under
src/job_research/ - initial SQLite schema for profiles, preferences, listings, snapshots, scores, and dismissals
- CLI entrypoints for
ingest-cv,ingest-profile,set-preferences,refresh,rank,explain, anddismiss
Exit criteria:
- CLI runs locally
- models and storage are stable enough for feature work
- a refresh run can create a tracked run record even before scraping is implemented
Phase 2: Profile Ingestion
Goal:
-
create a trustworthy merged Candidate Profile from PDF, markdown, and manual correction
-
Parse PDF CV
-
Parse markdown profile
-
Extract structured profile fields
-
Merge CV facts with markdown clarifications
-
Add manual correction path via JSON or markdown
-
Save profile snapshot locally
Deliverables:
- PDF text extraction module
- markdown profile parser
- normalized Candidate Profile serializer in YAML or JSON
- merge policy implementation reflecting the resolved authority rules
Exit criteria:
- You can ingest a real CV plus markdown profile and inspect a usable merged profile
- you can manually correct the normalized profile and re-use it for ranking
Phase 3: First Job Source Adapter
Goal:
-
build one reliable end-to-end public crawler on Apec
-
Implement one source adapter, preferably Apec
-
Support search, listing extraction, and normalization
-
Store raw and normalized results
Deliverables:
- Playwright-based Apec adapter
- broad source-side filters for France and CDI when possible
- raw HTML/content snapshot persistence
- extraction metadata and failure logging
Exit criteria:
- One command fetches real listings and stores them locally
- repeated refresh runs do not create uncontrolled duplicates
Phase 4: Normalization, Deduplication, and Freshness
Goal:
-
turn raw crawl output into stable logical Job Listings that remain useful across repeated refreshes
-
normalize source payloads into the shared JobListing schema
-
deduplicate reposted or updated pages into one logical listing when confidence is high
-
classify freshness from published and refreshed dates
-
preserve Listing Snapshots under the logical listing
Deliverables:
- normalization pipeline
- deduplication heuristics for company/title/location/time-window matching
- freshness classification module
Exit criteria:
- the same opportunity can appear across multiple refreshes without flooding the result set
- each logical listing can be inspected alongside its snapshots
Phase 5: Ranking Engine
Goal:
-
produce trustworthy main results and Stretch Opportunities from deterministic rules first
-
Implement deterministic scoring
-
Add top-match reporting
-
Add explainability output
Deliverables:
- title-family and seniority scoring rules
- curated related-signal map for data stack technologies
- hard filters for CDI and France-based scope
- separate Stretch Opportunities classification
Exit criteria:
- The system can rank jobs against your profile with understandable reasons
- a generic backend Python role is outranked by a real data-platform role with weaker Python emphasis
Phase 6: LLM-Assisted Explanations and Re-ranking
Goal:
-
add bounded LLM reasoning without giving up deterministic control
-
Add provider abstraction
-
Add structured LLM prompts for fit analysis
-
Generate concise explanations and follow-up insights
Deliverables:
- provider interface compatible with OpenCode-connected providers
- JSON-shaped prompt/response contract
rerankflow for temporary Search Preferences overrides over stored listings
Exit criteria:
- Explanations are better than raw score tables and remain grounded in structured data
- chat overrides can re-rank stored results without a new crawl
Phase 7: Dismissal, Export, and OpenCode Workflow Hardening
Goal:
-
make the tool comfortable for repeated day-to-day use inside OpenCode
-
Add markdown / JSON exports optimized for chat workflows
-
Add commands for summaries, comparisons, and filters
-
Add Dismissed Listing behavior
-
Add stale-data warning behavior for discussion over old crawls
Deliverables:
- dismissed listing persistence and recovery flow
- markdown export shaped for OpenCode review
- stale crawl warning logic for data older than 3 days
Exit criteria:
- OpenCode becomes the comfortable day-to-day interface for analyzing opportunities
- dismissed listings no longer repeatedly waste attention
Phase 8: Multi-Source Expansion
Goal:
-
expand source coverage after the full loop is proven on Apec
-
Add Welcome to the Jungle adapter
-
Add Indeed adapter
-
Improve deduplication across boards
Deliverables:
- source-specific adapters for additional boards
- cross-source deduplication heuristics
Exit criteria:
- A full refresh can aggregate from multiple supported sources
Key Risks
Legal / Platform Risk
- Some sites may restrict scraping or aggressively block automation.
- Mitigation: start small, inspect ToS, use throttling, and favor stable flows.
Extraction Fragility
- DOM selectors may break often.
- Mitigation: isolate selectors per adapter and keep raw payloads for debugging.
LLM Hallucination
- The model may overstate job fit.
- Mitigation: deterministic scoring stays primary; LLM output is constrained and explainable.
CV Parsing Quality
- PDF extraction may be messy.
- Mitigation: keep a manual review/edit path for the structured profile.
Immediate Build Order
Build the first thin vertical slice in this order:
- create the package, storage, and CLI surface,
- ingest the PDF CV and markdown profile,
- persist a corrected normalized Candidate Profile,
- implement Apec crawl and artifact storage,
- normalize, deduplicate, and classify freshness,
- rank into main results and Stretch Opportunities,
- add structured explanations,
- expose rerank, dismiss, and export flows for OpenCode.
Recommended Next Step
Start with Phase 1 and Phase 2 only:
- scaffold the Python package and local SQLite state,
- implement PDF CV + markdown profile ingestion,
- persist a normalized editable Candidate Profile,
- stop there and validate the profile before touching scraping.
That is the safest first checkpoint because a bad Candidate Profile will poison every later ranking decision.