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:
@@ -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,
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user