443 lines
21 KiB
Python
443 lines
21 KiB
Python
# Procyon — resume tailoring + fit scoring + cover email drafting.
|
||
# LLM-first, deterministic keyword fallback. Honest-only: never fabricates.
|
||
# Holds a resume LIBRARY (tech / hydro / driving) and combines the right
|
||
# resumes per job ("the grower who also builds" differentiator).
|
||
|
||
import os
|
||
|
||
import db
|
||
import llm
|
||
|
||
RESUME_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resumes')
|
||
|
||
# library: track -> ordered list of resume files (first = primary)
|
||
TRACKS = {
|
||
'tech': ['tech-costco.txt', 'tech-polished.txt'],
|
||
'hydro': ['hydro-clean.txt'],
|
||
'driving': ['driving.txt'],
|
||
}
|
||
|
||
HYDRO_KW = ['hydroponic', 'horticulture', 'grower', 'cultivat', 'greenhouse', 'agricultur',
|
||
'nursery', 'botan', 'plant', 'ipm', 'irrigation', 'nutrient', 'cannabis', 'crop']
|
||
DRIVE_KW = ['delivery', 'driver', 'logistics', 'warehouse', 'forklift', 'route', 'fleet',
|
||
'dispatch', 'courier', 'cdl', 'field operations', 'shipping', 'transport']
|
||
TECH_KW = ['software', 'engineer', 'developer', 'devops', 'sysadmin', 'system admin',
|
||
'infrastructure', 'cloud', 'python', 'linux', 'it ', 'network', 'ai',
|
||
'automation', 'full-stack', 'full stack', 'backend', 'frontend', 'sre',
|
||
'platform', 'data ', 'security', 'mcp', 'llm']
|
||
|
||
|
||
def _load(name):
|
||
p = os.path.join(RESUME_DIR, name)
|
||
if os.path.exists(p):
|
||
with open(p, 'r', encoding='utf-8', errors='replace') as f:
|
||
return f.read()
|
||
return ''
|
||
|
||
|
||
def list_library():
|
||
out = []
|
||
for track, files in TRACKS.items():
|
||
for fn in files:
|
||
p = os.path.join(RESUME_DIR, fn)
|
||
out.append({'track': track, 'file': fn, 'exists': os.path.exists(p),
|
||
'chars': os.path.getsize(p) if os.path.exists(p) else 0})
|
||
return out
|
||
|
||
|
||
def classify_track(job):
|
||
"""Return the primary track for a job based on keyword density."""
|
||
text = ((job.get('title') or '') + ' ' + (job.get('description') or '')).lower()
|
||
hydro = sum(1 for k in HYDRO_KW if k in text)
|
||
drive = sum(1 for k in DRIVE_KW if k in text)
|
||
tech = sum(1 for k in TECH_KW if k in text)
|
||
# default tech when ambiguous (his primary target)
|
||
if hydro > drive and hydro >= tech and hydro > 0:
|
||
return 'hydro'
|
||
if drive > hydro and drive >= tech and drive > 0:
|
||
return 'driving'
|
||
return 'tech'
|
||
|
||
|
||
def _tech_differentiator():
|
||
"""Compact 'also a builder' block extracted from the tech resume."""
|
||
tech = _load('tech-costco.txt')
|
||
profile = ''
|
||
if '## Technical Profile' in tech:
|
||
profile = tech.split('## Technical Profile', 1)[1].split('## Core Engineering', 1)[0]
|
||
return ('\n\n## Technical Differentiator\n\n'
|
||
f"Beyond cultivation, I'm a self-taught full-stack developer and infrastructure "
|
||
f"engineer who builds the software and sensor hardware that modern growing "
|
||
f"operations run on (see hydro.thetempleofdoom.com).\n{profile.strip()}")
|
||
|
||
|
||
def build_master(job):
|
||
"""Select + combine the right resumes. Returns (tracks_used, combined_text)."""
|
||
track = classify_track(job)
|
||
tracks = [track]
|
||
if track == 'hydro':
|
||
text = _load('hydro-clean.txt') + _tech_differentiator()
|
||
tracks.append('tech')
|
||
elif track == 'driving':
|
||
text = _load('driving.txt')
|
||
# if the role has a meaningful tech/ops slant, fold in the tech profile
|
||
jt = ((job.get('title') or '') + ' ' + (job.get('description') or '')).lower()
|
||
if sum(1 for k in TECH_KW if k in jt) >= 2:
|
||
text += _tech_differentiator()
|
||
tracks.append('tech')
|
||
else:
|
||
text = _load('tech-costco.txt')
|
||
if not text.strip():
|
||
text = _load('tech-polished.txt')
|
||
# single-resume override (Settings) takes precedence
|
||
override = db.get_setting('master_resume', '')
|
||
if override and os.path.exists(override):
|
||
with open(override, 'r', encoding='utf-8', errors='replace') as f:
|
||
return ['custom'], f.read()
|
||
# always append the canonical work history so tailoring has the full verified record
|
||
wh = _load('work-history.txt')
|
||
if wh:
|
||
text += '\n\n## WORK HISTORY (verified facts)\n' + wh
|
||
return tracks, text
|
||
|
||
|
||
def score_job(job):
|
||
"""Return (score 0..1, opinion string)."""
|
||
desc = job.get('description') or ''
|
||
title = job.get('title') or ''
|
||
company = job.get('company') or ''
|
||
track = classify_track(job)
|
||
prompt = (
|
||
f"Score how well this job fits a self-taught full-stack developer and self-hosted "
|
||
f"infrastructure engineer (Python, Flask, Linux, Proxmox, Docker, networking, "
|
||
f"automation, LLM/agent tooling). Candidate track: {track}. Return JSON: "
|
||
f"{{\"score\": <0.0 to 1.0>, \"opinion\": \"<one sentence why>\", "
|
||
f"\"top_skills\": [\"..\"]}}\n\n"
|
||
f"Job title: {title}\nCompany: {company}\n\n{desc[:3000]}"
|
||
)
|
||
data = llm.llm_json(prompt)
|
||
if data and isinstance(data, dict) and 'score' in data:
|
||
try:
|
||
score = float(data['score'])
|
||
except (TypeError, ValueError):
|
||
score = 0.5
|
||
return round(max(0.0, min(1.0, score)), 3), str(data.get('opinion', ''))
|
||
score, opinion = llm.keyword_score(desc)
|
||
return score, opinion
|
||
|
||
|
||
def tailor_resume(job):
|
||
"""Tailor the combined master resume for a specific job. Returns tailored text."""
|
||
tracks, master = build_master(job)
|
||
title = job.get('title') or ''
|
||
company = job.get('company') or ''
|
||
desc = (job.get('description') or '')[:3000]
|
||
prompt = (
|
||
f"Rewrite this resume to target the role '{title}' at {company}. "
|
||
"Keep every fact identical — same employers, titles, dates, skills, credentials. "
|
||
"Only reorder sections to surface the most relevant experience first and rephrase bullet "
|
||
"points to echo the job description's own terminology WITHOUT inventing anything. "
|
||
"Do not add skills the candidate doesn't have. Do NOT add a 'Targeting' header or any "
|
||
"meta-commentary — it should read like a normal resume.\n\n"
|
||
f"JOB DESCRIPTION:\n{desc}\n\nMASTER RESUME (combined tracks: {', '.join(tracks)}):\n{master}"
|
||
)
|
||
tailored = llm.llm_chat(prompt)
|
||
if tailored:
|
||
return tailored
|
||
# keyword fallback: keep master intact (no fabricated targeting header)
|
||
return master
|
||
|
||
|
||
def draft_email(job, tailored_resume, research=''):
|
||
"""Draft a tailored outreach email. No fabrication; human, specific, natural voice."""
|
||
title = job.get('title') or 'the role'
|
||
company = job.get('company') or 'your team'
|
||
from_name = db.get_setting('from_name', 'Indiana Holmes')
|
||
phone = db.get_setting('resume_phone', '')
|
||
email = db.get_setting('resume_email', '')
|
||
website = db.get_setting('website', 'https://thetempleofdoom.com').strip()
|
||
|
||
research_hint = ''
|
||
if research:
|
||
research_hint = (
|
||
f"\n\nCOMPANY CONTEXT (weave ONE natural, specific reference into the body — show "
|
||
f"you actually looked at what they do; do NOT lead with it or recite it like a "
|
||
f"fact sheet):\n{research[:1200]}\n"
|
||
)
|
||
|
||
prompt = (
|
||
f"You are {from_name}, a self-taught full-stack developer and infrastructure engineer "
|
||
f"based in Seattle, WA. You are applying to {company} for the '{title}' role as an "
|
||
f"EXTERNAL candidate — you have NEVER worked at {company}, and you do NOT currently "
|
||
f"hold this role or any role there. Your only real experience is what is in the "
|
||
f"TAILORED RESUME below.\n\n"
|
||
f"Write a short, warm, plain-spoken cold-application email for that role. It should read "
|
||
f"like a real person wrote it quickly and confidently — not a template and not "
|
||
f"AI-sounding. Rules:\n"
|
||
"- 2-3 short paragraphs, varied sentence length, concrete and specific.\n"
|
||
"- NO buzzwords and NO cliche phrases: 'I am writing to', 'I hope this email finds you "
|
||
"well', 'I would welcome', 'I am excited to', 'passionate about', 'I believe', "
|
||
"'leverage', 'delve', 'synergy'. Avoid em-dashes and semicolons.\n"
|
||
"- Open naturally with a specific reason this role caught your eye, then name the TWO "
|
||
"strongest RELEVANT qualifications from the resume in plain concrete terms, then a "
|
||
"short direct close.\n"
|
||
"- If a COMPANY CONTEXT block is present, reference ONE specific fact about the company "
|
||
"as something you've noticed about THEM (e.g. 'I've been following FreedomPay's work in "
|
||
"payments'), NOT as your own employment. NEVER claim or imply you currently work at "
|
||
f"{company}, have ever worked at {company}, or currently hold the '{title}' role.\n"
|
||
"- Do NOT invent any employer, job title, date, skill, or metric that is not in the "
|
||
"resume. If the resume does not support a claim, do not make it.\n"
|
||
"- End with a short direct closing line. Do NOT include your name, phone number, email, "
|
||
"or website — the signature is appended automatically.\n"
|
||
"- Return ONLY the email body text (no subject line, no preamble).\n\n"
|
||
f"TAILORED RESUME:\n{tailored_resume[:4000]}"
|
||
f"{research_hint}"
|
||
)
|
||
body = llm.llm_chat(prompt)
|
||
body = _clean(body)
|
||
if body and not _looks_fabricated(body, company, title):
|
||
return _append_signature(body, from_name, phone, email, website)
|
||
return (
|
||
f"Hello,\n\n"
|
||
f"I came across the {title} opening at {company} and it lines up well with what I do. "
|
||
f"I'm a self-taught full-stack developer and infrastructure engineer — I build and run "
|
||
f"Python/Flask services, Linux servers, Proxmox virtualization, Docker, and agent/LLM "
|
||
f"tooling on hardware I operate myself.\n\n"
|
||
f"If you're open to it, I'd like to talk about how that hands-on background fits the "
|
||
f"team.\n\n"
|
||
f"Thanks,\n{from_name}\n{phone}\n{email}\n{website}"
|
||
)
|
||
|
||
|
||
def _append_signature(body, from_name, phone, email, website):
|
||
"""Deterministically append the signature block. The model reliably drops it, so
|
||
never trust it to sign its own output. Skips if a signature is already present."""
|
||
if (website and website in body) or (email and email in body):
|
||
return body
|
||
sig = f"{from_name}\n{phone}\n{email}\n{website}"
|
||
return (body.rstrip() + f"\n\n{sig}").strip()
|
||
|
||
|
||
def _clean(body):
|
||
"""Strip broken-tokenizer artifacts and a leading 'Subject:' line from LLM output.
|
||
The local models intermittently leak `<unusedNN>` / `????` tokens; if nothing
|
||
usable remains, return '' so the caller falls back to the safe template."""
|
||
import re
|
||
if not body:
|
||
return ''
|
||
body = re.sub(r'<unused\d+>', '', body)
|
||
body = re.sub(r'^[\s?]+', '', body)
|
||
lines = body.splitlines()
|
||
while lines and lines[0].strip().lower().startswith('subject:'):
|
||
lines = lines[1:]
|
||
cleaned = '\n'.join(lines).strip()
|
||
# if after cleanup it's essentially empty or pure punctuation, treat as garbage
|
||
if not cleaned or not any(ch.isalnum() for ch in cleaned):
|
||
return ''
|
||
return cleaned
|
||
|
||
|
||
def _looks_fabricated(body, company, title):
|
||
"""Deterministic guard: reject LLM output that claims the candidate currently
|
||
works at the target company or already holds the applied-for role (weak-model
|
||
hallucination). Returns True if the body looks fabricated."""
|
||
if not body:
|
||
return True
|
||
low = body.lower()
|
||
c = (company or '').lower().strip()
|
||
t = (title or '').lower().strip()
|
||
markers = ['right now i ', 'i currently ', 'currently work', "i'm currently",
|
||
'i am currently', 'i joined ', 'my role at', 'i work at', 'i own the ',
|
||
"i've been at ", 'i lead the ', 'i manage the ']
|
||
hits = [m for m in markers if m in low]
|
||
# present-tense claim tying the candidate to the company/title
|
||
if c and c in low:
|
||
for phrase in (f'at {c} where i', f'at {c}, where i', f'working at {c}',
|
||
f'i am a {t} at {c}', f"i'm a {t} at {c}"):
|
||
if phrase in low:
|
||
hits.append(phrase)
|
||
return bool(hits)
|
||
|
||
|
||
def make_subject(job):
|
||
title = job.get('title') or 'Open Role'
|
||
company = job.get('company') or ''
|
||
return f"Application: {title}" + (f" — {company}" if company else "")
|
||
|
||
|
||
# ===================== structured one-page resume =====================
|
||
# Verified facts only (from work-history.txt). Renders to a one-page PDF.
|
||
|
||
EXPERIENCE = [
|
||
{'title': 'Water System Technician', 'company': 'Northwest Water Systems Inc',
|
||
'dates': '2025', 'tracks': ['tech', 'driving'],
|
||
'bullets': [
|
||
'Maintained 45 pump houses across WA — install, repair, and troubleshoot water treatment & distribution infrastructure.',
|
||
'Field inspections, EPA-standard water sampling, emergency response, and compliance documentation.',
|
||
'Operated pumps, filtration, and chemical-dosing equipment.']},
|
||
{'title': 'Lead Field Operations Specialist', 'company': 'Lime',
|
||
'dates': '2023 – 2025', 'tracks': ['tech', 'driving'],
|
||
'bullets': [
|
||
'Led daily field operations to keep the scooter/bike fleet available; supervised 5 field agents.',
|
||
'Coordinated dispatch logistics, GPS-based asset recovery, and route optimization.',
|
||
'Repaired and replaced vehicle parts; directed drivers via two-way radio.']},
|
||
{'title': 'Master Grower / Cultivation Manager', 'company': 'Los Angeles, CA (controlled-environment agriculture)',
|
||
'dates': '2014 – 2023', 'tracks': ['hydro'],
|
||
'bullets': [
|
||
'Managed full cultivation cycles (propagation → harvest) and designed custom hydroponic systems.',
|
||
'Developed nutrient programs; maintained lighting, humidity, airflow, and temperature control.',
|
||
'Implemented integrated pest management; trained and supervised growers.']},
|
||
{'title': 'Lead Service Representative', 'company': 'LabCorp',
|
||
'dates': '2022 – 2023', 'tracks': ['driving', 'tech'],
|
||
'bullets': [
|
||
'Ran daily medical specimen pickup routes across hospitals and clinics with strict regulatory compliance.',
|
||
'Coordinated emergency pickups and route adjustments with dispatch.']},
|
||
{'title': 'Field Operations / Warehouse Specialist', 'company': 'Aboda',
|
||
'dates': '2019 – 2022', 'tracks': ['driving', 'tech'],
|
||
'bullets': [
|
||
'Managed logistics and setup for corporate housing units across Seattle and Bellevue.',
|
||
'Operated forklifts; maintained warehouse inventory and move-in-ready quality inspections.']},
|
||
{'title': 'Sales Manager', 'company': 'Mission Motors',
|
||
'dates': '2017 – 2019', 'tracks': ['driving'],
|
||
'bullets': ['Managed vehicle inventory and sales; guided customers through the full purchase process.']},
|
||
{'title': 'Logistics Coordinator', 'company': 'Salt Works',
|
||
'dates': '2012', 'tracks': ['driving'],
|
||
'bullets': ['Loaded pallets into shipping containers and box trucks; operated forklift and cherry picker.']},
|
||
]
|
||
|
||
PROJECTS = [
|
||
{'title': 'Self-Hosted Infrastructure (Proxmox homelab)',
|
||
'bullets': [
|
||
'Operate a multi-node Proxmox cluster running AI services, databases, websites, and automation.',
|
||
'Built a self-hosted SaaS ecosystem (Git, automation, cloud storage) with secure remote access.']},
|
||
{'title': 'AI Automation & Local LLM Pipelines',
|
||
'bullets': [
|
||
'Built local-LLM agent workflows and MCP integrations for research, monitoring, and data processing.']},
|
||
{'title': 'Embedded & RF Systems (ESP32, LoRa / Meshtastic)',
|
||
'bullets': [
|
||
'Engineered ESP32 telemetry and sensor devices with custom firmware; built LoRa/Meshtastic mesh networks.']},
|
||
]
|
||
|
||
SUMMARIES = {
|
||
'tech': 'Self-taught full-stack and embedded systems engineer who builds across the entire stack — '
|
||
'Python/Flask backends, Linux/Docker/Proxmox infrastructure, ESP32 firmware for embedded devices, '
|
||
'LoRa/Meshtastic mesh networks, and local AI/LLM agent tooling — on hardware I design and operate myself.',
|
||
'hydro': 'Self-taught horticulture specialist with 14+ years in hydroponics and controlled-environment '
|
||
'agriculture — custom grow systems, plant nutrition, environmental control, and high-yield production.',
|
||
'driving': 'Reliable logistics and field-operations professional with extensive route-driving, fleet, and '
|
||
'warehouse experience — safety-focused with strong dispatch coordination and problem-solving.',
|
||
}
|
||
|
||
SKILLS = {
|
||
'tech': ['Embedded firmware (ESP32)', 'LoRa / Meshtastic mesh networks', 'Embedded systems design',
|
||
'Python', 'Bash', 'Proxmox / virtualization', 'Linux administration', 'Docker',
|
||
'Networking / SSH / VPN', 'REST APIs', 'Git', 'AI / LLM infrastructure',
|
||
'Agent & MCP development', 'Monitoring / Grafana', 'Automation pipelines'],
|
||
'hydro': ['Hydroponic system design', 'Plant propagation', 'Nutrient programs', 'IPM',
|
||
'Environmental control', 'Grow-facility design', 'Grower training',
|
||
'Plant-health troubleshooting'],
|
||
'driving': ['Route driving & delivery', 'Forklift (certified)', 'Fleet tracking & recovery',
|
||
'Warehouse inventory', 'Field operations', 'GPS route optimization',
|
||
'Dispatch coordination', 'Customer service'],
|
||
}
|
||
|
||
EDUCATION = 'Lake Stevens High School — Diploma (Computer Applications & Technology)'
|
||
|
||
TRACK_HEADLINES = {
|
||
'tech': 'Full-Stack & Embedded Systems Engineer',
|
||
'hydro': 'Hydroponics & Controlled-Environment Agriculture Specialist',
|
||
'driving': 'Logistics & Field Operations Professional',
|
||
}
|
||
|
||
|
||
def _deterministic_structured(track, job):
|
||
"""Deterministic one-page structured resume from verified facts (no LLM)."""
|
||
headline = TRACK_HEADLINES.get(track, TRACK_HEADLINES['tech'])
|
||
exp = [e for e in EXPERIENCE if track in e['tracks']]
|
||
resume = {
|
||
'contact': 'Seattle, WA | 425-280-0023 | indianaholmes1@icloud.com',
|
||
'headline': headline,
|
||
'summary': SUMMARIES.get(track, SUMMARIES['tech']),
|
||
'skills': SKILLS.get(track, SKILLS['tech']),
|
||
'experience': exp,
|
||
'education': EDUCATION,
|
||
}
|
||
if track == 'tech':
|
||
resume['projects'] = PROJECTS
|
||
return resume
|
||
|
||
|
||
def structured_resume(job):
|
||
"""Deterministic one-page resume from verified facts. (Was LLM-first, but the JSON
|
||
generation call was slow under MacBook memory pressure and its failure tripped the
|
||
circuit breaker — blocking the email draft, which is the call that actually matters.
|
||
The deterministic path is honest and clean, so use it exclusively.)"""
|
||
track = classify_track(job)
|
||
return _deterministic_structured(track, job)
|
||
|
||
|
||
def structured_to_text(resume):
|
||
"""Render a structured resume dict to readable plain text (for display + audit)."""
|
||
lines = []
|
||
if resume.get('headline'):
|
||
lines.append(resume['headline'])
|
||
if resume.get('summary'):
|
||
lines.append(resume['summary'])
|
||
if resume.get('skills'):
|
||
lines.append('\nSKILLS: ' + ' • '.join(resume['skills']))
|
||
if resume.get('experience'):
|
||
lines.append('\nEXPERIENCE:')
|
||
for e in resume['experience']:
|
||
lines.append(f" {e.get('title')} — {e.get('company')} ({e.get('dates')})")
|
||
for b in e.get('bullets', []):
|
||
lines.append(f" - {b}")
|
||
if resume.get('projects'):
|
||
lines.append('\nPROJECTS:')
|
||
for p in resume['projects']:
|
||
lines.append(f" {p.get('title')}")
|
||
for b in p.get('bullets', []):
|
||
lines.append(f" - {b}")
|
||
if resume.get('education'):
|
||
lines.append('\nEDUCATION: ' + resume['education'])
|
||
return '\n'.join(lines)
|
||
|
||
|
||
# ---- ATS optimization & gap analysis (quality steps) ----
|
||
|
||
def extract_job_keywords(description, top_n=15):
|
||
"""Extract the most signal-bearing keywords from a job description."""
|
||
import re as _re
|
||
if not description:
|
||
return []
|
||
d = description.lower()
|
||
freq = {}
|
||
for kw in llm._skill_keywords():
|
||
if kw in d:
|
||
freq[kw] = d.count(kw)
|
||
# capitalized technical terms (e.g. "Kubernetes", "AWS", "Terraform")
|
||
for m in _re.findall(r'\b[A-Z][A-Za-z0-9+#./]{2,}\b', description):
|
||
k = m.lower()
|
||
if k not in freq and len(k) >= 3:
|
||
freq[k] = description.lower().count(k)
|
||
ranked = sorted(freq.items(), key=lambda x: -x[1])
|
||
return [k for k, _ in ranked[:top_n]]
|
||
|
||
|
||
def ats_coverage(text, keywords):
|
||
"""Return (coverage 0..1, keywords, missing_keywords) for ATS matching."""
|
||
if not keywords:
|
||
return 1.0, [], []
|
||
t = (text or '').lower()
|
||
missing = [k for k in keywords if k.lower() not in t]
|
||
hit = len(keywords) - len(missing)
|
||
return round(hit / len(keywords), 3), keywords, missing
|
||
|
||
|
||
def candidate_gaps(job):
|
||
"""Job keywords absent from the candidate's skill surface -> honest fit gaps."""
|
||
desc = (job.get('description') or '') + ' ' + (job.get('title') or '')
|
||
kws = extract_job_keywords(desc, top_n=12)
|
||
_tracks, master = build_master(job)
|
||
cov, _kws, missing = ats_coverage(master, kws)
|
||
return {'keywords': kws, 'missing': missing, 'coverage': cov}
|