Skip to content

[codex] Use LLM classification for job leads#378

Merged
michaelmwu merged 4 commits into
mainfrom
michaelmwu/fix-job-lead-tags
Jul 9, 2026
Merged

[codex] Use LLM classification for job leads#378
michaelmwu merged 4 commits into
mainfrom
michaelmwu/fix-job-lead-tags

Conversation

@michaelmwu

@michaelmwu michaelmwu commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

  • add schema-validated LLM classification for HN job leads, with keyword fallback when no OpenAI-compatible provider is available
  • store contractor classification metadata and expose shared review_summary / contractor_classification fields through the dashboard API
  • update /list-job-leads and the admin dashboard to show readable classification context instead of raw confidence percentages
  • add worker/runtime config, env docs, tests, and rebuild the bundled dashboard asset

Validation

  • uv run pytest tests/unit/test_job_lead_sources.py tests/unit/test_jobs.py tests/unit/test_backend_api.py -q
  • uv run ruff check packages/shared/src/five08/job_lead_sources.py packages/shared/src/five08/job_leads.py packages/shared/src/five08/runtime_config.py apps/api/src/five08/backend/api.py apps/discord_bot/src/five08/discord_bot/cogs/jobs.py apps/worker/src/five08/worker/config.py tests/unit/test_job_lead_sources.py tests/unit/test_jobs.py tests/unit/test_backend_api.py
  • ./scripts/pyrefly.sh
  • bun run typecheck
  • bun run lint
  • bun run test
  • bun run build

Note

Medium Risk
Classification logic gates which sourced leads look contractor-friendly; incorrect LLM or heuristic results could affect review volume, but behavior degrades to keywords and does not touch auth or payments.

Overview
Adds LLM-first contractor-fit classification for Hacker News job-lead intake via JobLeadClassifier, with keyword/heuristic fallback when no OpenAI-compatible provider is available or the model call fails. Classification metadata is stored on leads and normalized through job_lead_display_payload, exposing contractor_classification and a human-readable review_summary.

Dashboard API list and review job-lead handlers now return that display payload instead of raw lead rows. The admin dashboard types and list UI show method/confidence labels (e.g. LLM vs keyword fallback) and the review summary, replacing the previous confidence-percentage badge.

New JOB_LEAD_CLASSIFIER_* env settings document enablement, model, and timeout for the classifier.

Reviewed by Cursor Bugbot for commit 0113dd0. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added contractor-fit classification details to job leads, including a clearer review summary and confidence label.
    • Dashboard and Discord job-lead views now show the new summary format instead of raw percentage-style confidence.
    • Added configurable settings for enabling classification, choosing a model, and adjusting timeout behavior.
  • Bug Fixes

    • Improved Discord lead listings to stay within message limits and include a concise “shown vs omitted” footer.
  • Documentation

    • Updated configuration docs and example environment settings for the new options.

@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_94ccb1b9-a6ab-4ace-90c2-b10c210bb1fb)

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds contractor-friendly job lead classification via a two-stage heuristic/LLM classifier for HN "Who is hiring?" scraping, threads classifier configuration through worker/runtime settings, exposes classification metadata and review summaries through shared helpers and API payloads, and updates admin dashboard and Discord bot UIs to display the new classification/summary fields instead of raw confidence percentages.

Changes

Job lead contractor classification pipeline

Layer / File(s) Summary
Classifier configuration settings
.env.example, apps/worker/src/five08/worker/config.py, packages/shared/src/five08/runtime_config.py, docs/configuration.md
Adds JOB_LEAD_CLASSIFIER_ENABLED, JOB_LEAD_CLASSIFIER_MODEL, and JOB_LEAD_CLASSIFIER_TIMEOUT_SECONDS settings to env example, worker settings, runtime config registry, and docs.
Classification types and classifier logic
packages/shared/src/five08/job_lead_sources.py, tests/unit/test_job_lead_sources.py
Defines JobLeadClassification and JobLeadLLMClassificationResponse, implements JobLeadClassifier with heuristic fallback and LLM structured/JSON parsing, updates HN lead extraction and source wiring to use the classifier, with expanded unit tests covering heuristic/LLM/provider-failure paths.
Display payload and review summary helpers
packages/shared/src/five08/job_leads.py
Adds job_lead_classification, format_job_lead_review_summary, and job_lead_display_payload functions that normalize classification fields and build API display payloads.
API endpoints and tests
apps/api/src/five08/backend/api.py, tests/unit/test_backend_api.py
Dashboard list/review endpoints now serialize leads via job_lead_display_payload; unit tests assert contractor_classification and review_summary fields on responses.
Dashboard UI classification display
apps/admin_dashboard/src/main.tsx, apps/api/src/five08/backend/static/dashboard/*, tests/integration/test_dashboard_playwright.py
Extends JobLead type, adds classification badge helper replacing percentage display, renders review_summary text, updates built dashboard assets, and expands the Playwright fixture data.
Discord bot review formatting
apps/discord_bot/src/five08/discord_bot/cogs/jobs.py, tests/unit/test_jobs.py
Uses format_job_lead_review_summary for per-lead lines, refactors the pending-leads message builder to respect Discord character limits with a footer, and updates /list-job-leads to use the new message builder; adds corresponding unit tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scraper as scrape_job_leads
  participant Source as HackerNewsWhoIsHiringLeadSource
  participant Classifier as JobLeadClassifier
  participant LLM as LLM API
  participant API as Dashboard API
  participant UI as Admin Dashboard/Discord Bot

  Scraper->>Source: build_job_lead_source(classifier)
  Source->>Classifier: classify(comment_text)
  Classifier->>LLM: structured classification request
  LLM-->>Classifier: classification response
  Classifier-->>Source: JobLeadClassification
  Source-->>Scraper: JobLeadInput with metadata
  API->>API: job_lead_display_payload(lead)
  API-->>UI: contractor_classification + review_summary
  UI->>UI: render classification badge / review summary
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding LLM-based classification for job leads.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch michaelmwu/fix-job-lead-tags

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3d81dde852

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +482 to +486
classification = (
classifier.classify(text)
if classifier is not None
else classify_contractor_lead_heuristic(text)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep SEEKING WORK filtering deterministic

When the LLM classifier is enabled, this branch now trusts the classifier before applying the existing _SEEKING_WORK_RE guard. In HN Who-is-hiring threads, applicant comments commonly start with SEEKING WORK and also contain terms like Freelance or Contract; if the LLM or an injected classifier returns a positive classification for that text, _lead_from_hn_comment will insert an applicant advert into job_leads, whereas the previous heuristic always rejected these comments. Apply the deterministic SEEKING WORK check before calling/trusting the classifier.

Useful? React with 👍 / 👎.

Comment on lines +338 to +341
except Exception:
pass

response = client.chat.completions.create(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid doubling classifier timeouts before fallback

If the structured parse call times out or the provider is down, this broad except falls through into a second chat.completions.create request before classify() can use the keyword heuristic. During an HN scrape this happens once per top-level comment, so with the default 8s classifier timeout a dead provider can cost roughly 16s per comment and block the scrape worker for many minutes before any leads are upserted. Treat timeout/provider failures as a signal to return the heuristic fallback instead of making another LLM call.

Useful? React with 👍 / 👎.

Comment on lines +3434 to +3435
summary = cls._truncate_job_lead_text(format_job_lead_review_summary(lead), 180)
return f"{index}. `{lead.id[:8]}` **{title}**\n{summary}\n{lead.source_url}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep lead list responses under Discord's limit

With /list-job-leads limit:10, each row now includes up to a 180-character LLM/keyword summary, and the command concatenates all rows into one followup.send; a normal set of pending leads with HN URLs can exceed settings.discord_sendmsg_character_limit (2000), causing Discord to reject the response. Split or cap the total response when adding these summaries.

Useful? React with 👍 / 👎.

Comment on lines +393 to +394
fireworks_api_key=_clean(getattr(settings, "fireworks_api_key", None)),
fireworks_model=_clean(getattr(settings, "agent_fast_model", None)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use classifier model for Fireworks direct attempts

When only JOB_LEAD_CLASSIFIER_MODEL is set to a Fireworks model with FIREWORKS_API_KEY (and no OpenAI/Bifrost primary), this passes agent_fast_model as the Fireworks model instead of the classifier override. If AGENT_FAST_MODEL is unset, build_openai_compatible_provider_attempts() produces no Fireworks provider, so the enabled classifier silently never calls the LLM and all scrapes use the keyword fallback. Pass the resolved classifier model here for that direct-provider configuration.

Useful? React with 👍 / 👎.

@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_f716fe73-3398-4fd4-8492-1ebc491ea610)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/discord_bot/src/five08/discord_bot/cogs/jobs.py (1)

3437-3467: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Guard against empty leads in _format_job_lead_review_message.

If called with an empty list, leads[0] in the if not lines: fallback branch raises IndexError. Currently safe because the only caller (list_sourced_job_leads) checks if not leads first, but the method itself has no such guard, making it a latent trap for future callers/tests.

🛡️ Optional defensive guard
     def _format_job_lead_review_message(cls, leads: list[JobLead]) -> str:
         """Format a bounded Discord response for pending lead review."""
+        if not leads:
+            return "Pending job leads:\n\nNo pending job leads found."
         limit = settings.discord_sendmsg_character_limit
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/discord_bot/src/five08/discord_bot/cogs/jobs.py` around lines 3437 -
3467, The fallback path in _format_job_lead_review_message can still index
leads[0] when the input list is empty, so add a defensive empty-list guard at
the start of this method. Return an appropriate bounded empty-state message (or
otherwise avoid the fallback branch) before any access to leads[0], and keep the
existing formatting logic for non-empty inputs unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/shared/src/five08/job_lead_sources.py`:
- Around line 322-353: The structured-output branch in _classify_with_llm is
missing the deterministic sampling setting used by the JSON path. Update the
client.beta.chat.completions.parse call in _classify_with_llm to pass
temperature=0 as well, keeping both classification paths consistent and
deterministic while preserving the existing response parsing logic.

---

Nitpick comments:
In `@apps/discord_bot/src/five08/discord_bot/cogs/jobs.py`:
- Around line 3437-3467: The fallback path in _format_job_lead_review_message
can still index leads[0] when the input list is empty, so add a defensive
empty-list guard at the start of this method. Return an appropriate bounded
empty-state message (or otherwise avoid the fallback branch) before any access
to leads[0], and keep the existing formatting logic for non-empty inputs
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 116d7c7c-9d05-49da-8fca-227ba917857b

📥 Commits

Reviewing files that changed from the base of the PR and between 9edf3f8 and 6bb74cf.

📒 Files selected for processing (16)
  • .env.example
  • apps/admin_dashboard/src/main.tsx
  • apps/api/src/five08/backend/api.py
  • apps/api/src/five08/backend/static/dashboard/.vite/manifest.json
  • apps/api/src/five08/backend/static/dashboard/assets/index-DFyKzfnC.js
  • apps/api/src/five08/backend/static/dashboard/index.html
  • apps/discord_bot/src/five08/discord_bot/cogs/jobs.py
  • apps/worker/src/five08/worker/config.py
  • docs/configuration.md
  • packages/shared/src/five08/job_lead_sources.py
  • packages/shared/src/five08/job_leads.py
  • packages/shared/src/five08/runtime_config.py
  • tests/integration/test_dashboard_playwright.py
  • tests/unit/test_backend_api.py
  • tests/unit/test_job_lead_sources.py
  • tests/unit/test_jobs.py

Comment thread packages/shared/src/five08/job_lead_sources.py
@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_6a65777d-c575-44b6-8ce9-5a078be3ed81)

@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_55d47667-d25e-4cca-9feb-6ee504bb3674)

@michaelmwu
michaelmwu added this pull request to the merge queue Jul 9, 2026
Merged via the queue into main with commit ea4cbc2 Jul 9, 2026
13 checks passed
@michaelmwu
michaelmwu deleted the michaelmwu/fix-job-lead-tags branch July 9, 2026 07:37

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0113dd0f2e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +410 to +411
openai_direct_model=_clean(getattr(settings, "openai_direct_model", None))
or _clean(getattr(settings, "agent_fallback_model", None)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor classifier model for OpenAI direct providers

When the worker is configured with only OPENAI_DIRECT_API_KEY (or uses a Bifrost primary with direct OpenAI fallback) and sets JOB_LEAD_CLASSIFIER_MODEL, these lines still pass agent_fallback_model as openai_direct_model; build_openai_compatible_provider_attempts() prefers that argument over the primary classifier model, so the classifier silently calls the generic fallback model instead of the configured classifier override. Use the resolved classifier model for this direct-provider path unless OPENAI_DIRECT_MODEL is explicitly intended to override it.

Useful? React with 👍 / 👎.

Comment on lines +385 to +387
explicit_fast = _clean(getattr(settings, "agent_fast_model", None))
if explicit_fast:
return explicit_fast

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve OpenRouter model normalization

When OPENROUTER_API_KEY is configured and AGENT_FAST_MODEL is an unqualified model such as gpt-4.1-mini, this returns that raw value as the OpenRouter direct model. That bypasses build_openai_compatible_provider_attempts()'s normal _openrouter_model_from_primary() conversion to openai/gpt-4.1-mini, so the direct OpenRouter fallback is called with the wrong model id and the classifier falls back to heuristics whenever the primary provider is unavailable.

Useful? React with 👍 / 👎.

Comment on lines +375 to +377
explicit_fast = _clean(getattr(settings, "agent_fast_model", None))
if explicit_fast:
return explicit_fast

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Only pass Fireworks models to Fireworks direct

When FIREWORKS_API_KEY is set and AGENT_FAST_MODEL is an unqualified or OpenAI/OpenRouter model, this returns that model as the Fireworks direct model; build_openai_compatible_provider_attempts() then appends a fireworks-direct attempt with that invalid Fireworks model id. In configurations where the primary provider is unavailable or absent, the classifier tries Fireworks with the wrong model and falls back to heuristics instead of using a valid direct provider.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant