feat: add health mission category, trim-to-sign-off, max_tokens cap for cover letters

- _MISSION_SIGNALS: add health category (pharma, clinical, patient care, etc.)
  listed last so music/animals/education/social_impact take priority
- _MISSION_DEFAULTS: health note steers toward people-first framing, not
  industry enthusiasm — focuses on patients navigating rare/invisible journeys
- _trim_to_letter_end(): cuts output at first sign-off + first name to prevent
  fine-tuned models from looping into repetitive garbage after completing letter
- generate(): pass max_tokens=1200 to router (prevents runaway output)
- user.yaml.example: add health + social_impact to mission_preferences,
  add candidate_voice field for per-user voice/personality context
This commit is contained in:
pyr0ball 2026-02-27 12:31:06 -08:00
parent 9b24599832
commit 30a2962797
2 changed files with 53 additions and 2 deletions

View file

@ -20,6 +20,14 @@ mission_preferences:
music: "" # e.g. "I've played in bands for 15 years and care deeply about how artists get paid"
animal_welfare: "" # e.g. "I volunteer at my local shelter every weekend"
education: "" # e.g. "I tutored underserved kids for 3 years and care deeply about literacy"
social_impact: "" # e.g. "I want my work to reach people who need help most"
health: "" # e.g. "I care about people navigating rare or poorly-understood health conditions"
# Note: if left empty, Para 3 defaults to focusing on the people the company
# serves — not the industry. Fill in for a more personal connection.
# Optional: how you write and communicate. Used to shape cover letter voice.
# e.g. "Warm and direct. Cares about people first. Finds rare and complex situations fascinating."
candidate_voice: ""
# Set to true to include optional identity-related sections in research briefs.
# Both are for your personal decision-making only — never included in applications.

View file

@ -73,6 +73,20 @@ _MISSION_SIGNALS: dict[str, list[str]] = {
"social good", "civic", "public health", "mental health", "food security",
"housing", "homelessness", "poverty", "workforce development",
],
# Health is listed last — it's a genuine but lower-priority connection than
# music/animals/education/social_impact. detect_mission_alignment returns on first
# match, so dict order = preference order.
"health": [
"patient", "patients", "healthcare", "health tech", "healthtech",
"pharma", "pharmaceutical", "clinical", "medical",
"hospital", "clinic", "therapy", "therapist",
"rare disease", "life sciences", "life science",
"treatment", "prescription", "biotech", "biopharma", "medtech",
"behavioral health", "population health",
"care management", "care coordination", "oncology", "specialty pharmacy",
"provider network", "payer", "health plan", "benefits administration",
"ehr", "emr", "fhir", "hipaa",
],
}
_candidate = _profile.name if _profile else "the candidate"
@ -99,6 +113,15 @@ _MISSION_DEFAULTS: dict[str, str] = {
f"cause {_candidate} cares deeply about. Para 3 should warmly reflect their genuine "
"desire to apply their skills to work that makes a real difference in people's lives."
),
"health": (
f"This company works in healthcare, life sciences, or patient care. "
f"Do NOT write about {_candidate}'s passion for pharmaceuticals or healthcare as an "
"industry. Instead, Para 3 should reflect genuine care for the PEOPLE these companies "
"exist to serve — those navigating complex, often invisible, or unusual health journeys; "
"patients facing rare or poorly understood conditions; individuals whose situations don't "
"fit a clean category. The connection is to the humans behind the data, not the industry. "
"If the user has provided a personal note, use that to anchor Para 3 specifically."
),
}
@ -189,6 +212,24 @@ def build_prompt(
return "\n".join(parts)
def _trim_to_letter_end(text: str) -> str:
"""Remove repetitive hallucinated content after the first complete sign-off.
Fine-tuned models sometimes loop after completing the letter. This cuts at
the first closing + candidate name so only the intended letter is saved.
"""
candidate_first = (_profile.name.split()[0] if _profile else "").strip()
pattern = (
r'(?:Warm regards|Sincerely|Best regards|Kind regards|Thank you)[,.]?\s*\n+\s*'
+ (re.escape(candidate_first) if candidate_first else r'\w+')
+ r'\b'
)
m = re.search(pattern, text, re.IGNORECASE)
if m:
return text[:m.end()].strip()
return text.strip()
def generate(
title: str,
company: str,
@ -227,8 +268,10 @@ def generate(
if feedback:
print("[cover-letter] Refinement mode: feedback provided", file=sys.stderr)
result = _router.complete(prompt)
return result.strip()
# max_tokens=1200 caps generation at ~900 words — enough for any cover letter
# and prevents fine-tuned models from looping into repetitive garbage output.
result = _router.complete(prompt, max_tokens=1200)
return _trim_to_letter_end(result)
def main() -> None: