Remove separate job profile files — filter uses search config + profile.json

Search config already defines what each track is looking for (keywords,
exclude_keywords, salary_min, remote). Profile.json defines who the
candidate is. No need for a third file duplicating both.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 09:26:47 -08:00
parent ac05c54c06
commit 7e1bce924e
2 changed files with 31 additions and 34 deletions

View File

@@ -31,7 +31,7 @@ process.stdout.write = (chunk, ...args) => { logStream.write(chunk); return orig
process.stderr.write = (chunk, ...args) => { logStream.write(chunk); return origStderrWrite(chunk, ...args); };
import { getJobsByStatus, updateJobStatus, loadConfig, loadQueue, saveQueue, dedupeAfterFilter, initQueue } from './lib/queue.mjs';
import { loadProfile, submitBatches, checkBatch, downloadResults } from './lib/filter.mjs';
import { submitBatches, checkBatch, downloadResults } from './lib/filter.mjs';
import { sendTelegram, formatFilterSummary } from './lib/notify.mjs';
import { DEFAULT_FILTER_MODEL, DEFAULT_FILTER_MIN_SCORE } from './lib/constants.mjs';
@@ -234,31 +234,28 @@ async function submit(settings, searchConfig, candidateProfile) {
return;
}
// Build job profiles map by track
const profilePaths = settings.filter?.job_profiles || {};
const jobProfilesByTrack = {};
for (const [track, path] of Object.entries(profilePaths)) {
const profile = await loadProfile(path);
if (profile) jobProfilesByTrack[track] = profile;
else console.warn(`⚠️ Could not load job profile for track "${track}" at ${path}`);
// Build search tracks map from search config
const searchesByTrack = {};
for (const search of searchConfig.searches) {
searchesByTrack[search.track] = search;
}
// Filter out jobs with no profile (will pass through unscored)
const filterable = jobs.filter(j => jobProfilesByTrack[j.track || 'ae']);
const noProfile = jobs.length - filterable.length;
// Filter out jobs with no matching search track
const filterable = jobs.filter(j => searchesByTrack[j.track || 'ae']);
const noTrack = jobs.length - filterable.length;
if (noProfile > 0) console.warn(`⚠️ ${noProfile} jobs skipped — no profile for their track`);
if (noTrack > 0) console.warn(`⚠️ ${noTrack} jobs skipped — no search track configured`);
if (filterable.length === 0) {
console.log('Nothing filterable — no job profiles configured for any track.');
console.log('Nothing filterable — no matching search tracks.');
return;
}
const model = settings.filter?.model || DEFAULT_FILTER_MODEL;
const submittedAt = new Date().toISOString();
console.log(`🚀 Submitting batches — ${filterable.length} jobs across ${Object.keys(jobProfilesByTrack).length} tracks, model: ${model}`);
console.log(`🚀 Submitting batches — ${filterable.length} jobs across ${Object.keys(searchesByTrack).length} tracks, model: ${model}`);
const submitted = await submitBatches(filterable, jobProfilesByTrack, candidateProfile, model, apiKey);
const submitted = await submitBatches(filterable, searchesByTrack, candidateProfile, model, apiKey);
writeState({
batches: submitted,

View File

@@ -7,34 +7,34 @@
import {
ANTHROPIC_BATCH_API_URL, FILTER_DESC_MAX_CHARS, FILTER_BATCH_MAX_TOKENS
} from './constants.mjs';
import { loadJSON } from './storage.mjs';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
export async function loadProfile(profilePath) {
if (!profilePath) return null;
try { return await loadJSON(profilePath, null); } catch { return null; }
}
function buildSystemPrompt(searchTrack, candidateProfile) {
const trackInfo = {
keywords: searchTrack.keywords,
exclude_keywords: searchTrack.exclude_keywords,
salary_min: searchTrack.salary_min,
remote: searchTrack.filters?.remote,
};
function buildSystemPrompt(jobProfile, candidateProfile) {
return `You are a job relevance scorer. Score each job listing 0-10 based on how well it matches the candidate profile below.
return `You are a job relevance scorer. Score each job listing 0-10 based on how well it matches the candidate and search criteria below.
## Candidate Profile
## Candidate
${JSON.stringify(candidateProfile, null, 2)}
## Target Job Profile
${JSON.stringify(jobProfile, null, 2)}
## Search Criteria (${searchTrack.name})
${JSON.stringify(trackInfo)}
## Instructions
- Use the candidate profile and target job profile above as your only criteria
- Score based on title fit, industry fit, experience match, salary range, location/remote requirements, and any exclude_keywords
- Score based on title fit, industry fit, experience match, salary range, location/remote, and exclude_keywords
- 10 = perfect match, 0 = completely irrelevant
- If salary is unknown, do not penalize
- If a posting is from a staffing agency but the role itself matches, score the role — not the agency
Return ONLY a JSON object: {"score": <0-10>, "reason": "<one concise line>"}`;
Return ONLY: {"score": <0-10>, "reason": "<one concise line>"}`;
}
function sanitize(str) {
@@ -66,27 +66,27 @@ function apiHeaders(apiKey) {
}
/**
* Submit one batch per track (one per job profile/search description).
* Submit one batch per track.
* Each batch uses the system prompt for that track only — maximizes prompt caching.
* Returns array of { track, batchId, idMap, jobCount }
*/
export async function submitBatches(jobs, jobProfilesByTrack, candidateProfile, model, apiKey) {
export async function submitBatches(jobs, searchesByTrack, candidateProfile, model, apiKey) {
// Group jobs by track
const byTrack = {};
for (const job of jobs) {
const track = job.track || 'ae';
if (!jobProfilesByTrack[track]) continue; // no profile → skip
if (!searchesByTrack[track]) continue;
if (!byTrack[track]) byTrack[track] = [];
byTrack[track].push(job);
}
if (Object.keys(byTrack).length === 0) throw new Error('No jobs to submit — check job profiles are configured');
if (Object.keys(byTrack).length === 0) throw new Error('No jobs to submit — check search config has tracks matching queued jobs');
const submitted = [];
for (const [track, trackJobs] of Object.entries(byTrack)) {
const jobProfile = jobProfilesByTrack[track];
const systemPrompt = buildSystemPrompt(jobProfile, candidateProfile);
const searchTrack = searchesByTrack[track];
const systemPrompt = buildSystemPrompt(searchTrack, candidateProfile);
const idMap = {};
const requests = [];