Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ AGENT_STRONG_API_KEY=
AGENT_REASONING_MODEL=
AGENT_REASONING_BASE_URL=
AGENT_REASONING_API_KEY=
# HN job-lead scraping uses an LLM classifier when OpenAI-compatible credentials
# are configured, then falls back to keyword matching if the model is unavailable.
JOB_LEAD_CLASSIFIER_ENABLED=true
JOB_LEAD_CLASSIFIER_MODEL=
JOB_LEAD_CLASSIFIER_TIMEOUT_SECONDS=8.0
# Optional deterministic agent tool integrations.
# GitHub Issues are the canonical code-task backend.
GITHUB_API_TOKEN=
Expand Down
27 changes: 24 additions & 3 deletions apps/admin_dashboard/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,16 @@ type JobLead = {
apply_url?: string
tags?: string[]
confidence?: number
contractor_classification?: {
is_contractor_friendly?: boolean
posting_type?: string
tags?: string[]
confidence?: number
confidence_label?: string
rationale?: string
method?: string
}
review_summary?: string
reviewed_by_discord_user_id?: string
reviewed_at?: string
discord_guild_id?: string
Expand Down Expand Up @@ -5580,6 +5590,16 @@ function defaultJobPostTagNames(lead: JobLead, channel?: JobPostChannel) {
return bestTag ? [bestTag.name] : []
}

function jobLeadClassificationLabel(lead: JobLead) {
const method = lead.contractor_classification?.method
const confidenceLabel = lead.contractor_classification?.confidence_label
if (method && confidenceLabel) {
const methodLabel = method === "llm" ? "LLM" : "Keyword fallback"
return `${methodLabel}: ${confidenceLabel} fit`
}
return lead.review_summary || "Classification unavailable"
}

function JobLeadListItem({
lead,
loading,
Expand Down Expand Up @@ -5643,9 +5663,7 @@ function JobLeadListItem({
<strong className="text-base">{lead.title || "Untitled lead"}</strong>
<Badge variant={jobLeadStatusTone(lead.status)}>{titleCase(lead.status)}</Badge>
{lead.remote ? <Badge variant="queued">Remote</Badge> : null}
{lead.confidence !== undefined ? (
<Badge variant="neutral">{Math.round(Number(lead.confidence || 0) * 100)}%</Badge>
) : null}
<Badge variant="neutral">{jobLeadClassificationLabel(lead)}</Badge>
</div>
<div className="mt-2 flex flex-wrap gap-1.5">
{lead.organization ? <Badge variant="neutral">{lead.organization}</Badge> : null}
Expand All @@ -5662,6 +5680,9 @@ function JobLeadListItem({
<p className="mt-3 max-h-20 overflow-hidden text-sm text-muted-foreground">
{lead.body_normalized || "No lead text captured."}
</p>
{lead.review_summary ? (
<p className="mt-2 text-sm font-semibold text-foreground">{lead.review_summary}</p>
) : null}
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted-foreground">
<span>Posted {formatDate(lead.source_posted_at) || "unknown"}</span>
<span>Captured {formatDate(lead.created_at) || "unknown"}</span>
Expand Down
7 changes: 5 additions & 2 deletions apps/api/src/five08/backend/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from five08.clients.espo import EspoAPIError, EspoClient
from five08.logging import configure_observability
from five08.job_leads import (
job_lead_display_payload,
JobLeadStatus,
list_job_leads,
review_job_lead,
Expand Down Expand Up @@ -4373,7 +4374,9 @@ async def dashboard_job_leads_handler(
status=normalized_status,
limit=limit,
)
return JSONResponse(jsonable_encoder(leads))
return JSONResponse(
jsonable_encoder([job_lead_display_payload(lead) for lead in leads])
)


async def dashboard_review_job_lead_handler(
Expand Down Expand Up @@ -4435,7 +4438,7 @@ async def dashboard_review_job_lead_handler(
"status": payload.status,
},
)
return JSONResponse(jsonable_encoder(lead))
return JSONResponse(jsonable_encoder(job_lead_display_payload(lead)))


async def dashboard_post_job_lead_handler(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"index.html": {
"file": "assets/index-CV7QH8Ei.js",
"file": "assets/index-Ce7YIjfO.js",
"name": "index",
"src": "index.html",
"isEntry": true,
Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/api/src/five08/backend/static/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>508 Operations Dashboard</title>
<script type="module" crossorigin src="/dashboard/assets/index-CV7QH8Ei.js"></script>
<script type="module" crossorigin src="/dashboard/assets/index-Ce7YIjfO.js"></script>
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-2UypYUrJ.css">
</head>
<body>
Expand Down
48 changes: 38 additions & 10 deletions apps/discord_bot/src/five08/discord_bot/cogs/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
unregister_job_post_channel,
)
from five08.job_leads import (
format_job_lead_review_summary,
get_job_lead,
JobLead,
JobLeadStatus,
Expand Down Expand Up @@ -3454,12 +3455,43 @@ def _truncate_job_lead_text(value: str, limit: int) -> str:

@classmethod
def _format_job_lead_review_line(cls, index: int, lead: JobLead) -> str:
tags = ", ".join(lead.tags[:5]) if lead.tags else "untagged"
title = cls._truncate_job_lead_text(lead.title, 120)
return (
f"{index}. `{lead.id[:8]}` **{title}** "
f"({lead.confidence:.0%}; {tags})\n{lead.source_url}"
)
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}"
Comment on lines +3459 to +3460

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 👍 / 👎.


@classmethod
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
header = "Pending job leads:"
lines: list[str] = []
shown = 0
for index, lead in enumerate(leads, start=1):
line = cls._format_job_lead_review_line(index, lead)
candidate_lines = [*lines, line]
omitted = len(leads) - len(candidate_lines)
footer = f"\n\nShowing {len(candidate_lines)} of {len(leads)} leads."
if omitted > 0:
footer += " Use a lower limit to inspect omitted leads."
candidate = f"{header}\n\n" + "\n\n".join(candidate_lines) + footer
if len(candidate) > limit:
break
lines = candidate_lines
shown = len(candidate_lines)

if not lines:
first = cls._truncate_job_lead_text(
cls._format_job_lead_review_line(1, leads[0]),
max(0, limit - len(header) - 32),
)
return f"{header}\n\n{first}"

footer = f"\n\nShowing {shown} of {len(leads)} leads."
if shown < len(leads):
footer += " Use a lower limit to inspect omitted leads."
return f"{header}\n\n" + "\n\n".join(lines) + footer

@classmethod
def _format_job_lead_thread_content(cls, lead: JobLead) -> str:
Expand Down Expand Up @@ -4014,12 +4046,8 @@ async def list_sourced_job_leads(
)
return

lines = [
self._format_job_lead_review_line(index, lead)
for index, lead in enumerate(leads, start=1)
]
await interaction.followup.send(
"Pending job leads:\n\n" + "\n\n".join(lines),
self._format_job_lead_review_message(leads),
ephemeral=True,
)

Expand Down
3 changes: 3 additions & 0 deletions apps/worker/src/five08/worker/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ class WorkerSettings(SharedSettings):
agent_reasoning_api_key: str | None = None
agent_reasoning_base_url: str | None = None
agent_reasoning_model: str | None = None
job_lead_classifier_enabled: bool = True
job_lead_classifier_model: str | None = None
job_lead_classifier_timeout_seconds: float = Field(default=8.0, gt=0)
resume_ai_api_key: str | None = None
resume_ai_base_url: str | None = None
resume_ai_model: str = "gpt-4.1-mini"
Expand Down
3 changes: 3 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ intake-completed field unset, and matches resume filenames with
- `RESUME_AI_API_KEY`
- `RESUME_AI_BASE_URL`
- `RESUME_AI_MODEL`
- `JOB_LEAD_CLASSIFIER_ENABLED`
- `JOB_LEAD_CLASSIFIER_MODEL`
- `JOB_LEAD_CLASSIFIER_TIMEOUT_SECONDS`

Resume/profile LLM calls retry matching direct providers after Bifrost request
failures when direct provider credentials are configured.
Expand Down
Loading