diff --git a/api/app/agent.py b/api/app/agent.py index 2b105f2..cd91940 100644 --- a/api/app/agent.py +++ b/api/app/agent.py @@ -659,13 +659,15 @@ async def _revise_cv_text(cv: dict, findings: list[dict], candidate: dict, job: "the candidate data states; no generic claims ('collaborated with cross-functional teams', 'worked on " "various projects'). 3. SUMMARY: a specific specialism and domain plus one concrete strength; no cliché " "openers or closing filler, no pronouns. 4. PROJECTS: what it does, how it works, and a real result, framed " - "for this job's domain. 5. ORDER: the most job-relevant bullet comes first in each role." + "for this job's domain. 5. ORDER: the most job-relevant bullet comes first in each role; the most relevant " + "role may carry a third bullet when the data backs one, the others one or two." ) _LETTER_RUBRIC = ( "The letter follows the candidate's own structure; judge each part against it, never reshape it into another. " "1. HOOK: opens on a specific challenge from one of the candidate's own projects and what it shows about how " - "they work, tied to the exact role and one concrete fact about this employer; it passes the swap test (another " + "they work, tied to the exact role and one concrete fact about this employer, and says why that fact draws " + "the candidate through their own data (not a bare 'this is relevant'); it passes the swap test (another " "employer's name couldn't replace theirs); never 'I am writing to apply'. 2. EVIDENCE: two named projects, the " "first in precise detail with a real number matched to a requirement in the ad, the second a judgement call, " "joined by the discipline both show. 3. HONEST GAP: one real requirement from the ad the candidate is lighter " @@ -1226,8 +1228,10 @@ async def execute_tool(name: str, tool_input: dict) -> dict: "technical challenge in it and what actually made it hard (the shape 'the hardest challenge wasn't X, " "but Y'), told only from what that project's data says. Say what it shows about how the candidate works " "(an instinct or habit), then tie that same instinct to the EXACT role and ONE concrete, specific fact " - "about this employer from the COMPANY RESEARCH or the ad (what they build, or how they work), ending on " - "why this is the place to start their career. NEVER begin with 'I am writing'. It MUST fail the " + "about this employer from the COMPANY RESEARCH or the ad (what they build, or how they work). Then say " + "why that fact draws THIS candidate, through something in their data (a project, a subject they have " + "worked on, a stated goal), never just that it is 'relevant'; end on why this is the place to start " + "their career. NEVER begin with 'I am writing'. It MUST fail the " "'swap-the-company-name' test: if another employer's name could replace theirs, it is too generic. " "(2) EVIDENCE ~110 words — start 'Two projects demonstrate the I'd bring to " "your .' First, one named project in precise technical detail with a real number from the " @@ -1447,8 +1451,11 @@ async def execute_tool(name: str, tool_input: dict) -> dict: "ATS RULES: use standard industry skill names and SPELL OUT acronyms next to the abbreviation, " "e.g. 'Search Engine Optimization (SEO)'. Keep every date exactly as the candidate data gives it " "(MM/YYYY); never reformat one. " - "BULLETS: 1-2 per role (one is right when the role is one story), each one or two lines " - "(~20-35 words) and no more than ~60 words per role in all, beginning with a strong, varied action verb (never 'Responsible for' or 'Helped " + "BULLETS: the ONE role most relevant to this job gets 3 bullets whenever its data holds three or " + "more distinct achievements (up to 3 for the ONE role, fewer only when the data has fewer); every " + "other role gets 1-2 (one is right when the role is one story). Each bullet one or two lines " + "(~20-35 words); no more than ~60 words per role in all (~90 for that most relevant role); " + "each beginning with a strong, varied action verb (never 'Responsible for' or 'Helped " "with'); follow Action + Task/Tool + quantified Result, and combine closely related achievements " "into one bullet rather than listing them. Quantify ONLY with metrics actually present in the candidate data; if a bullet has no " "source number, convey scope/scale with concrete specifics instead — NEVER invent, estimate or " diff --git a/api/tests/test_doc_check.py b/api/tests/test_doc_check.py index 98afbfa..ab7e886 100644 --- a/api/tests/test_doc_check.py +++ b/api/tests/test_doc_check.py @@ -216,6 +216,17 @@ def test_a_revision_in_the_wrong_shape_is_dropped(replies): assert _generate_cv()["experience"][0]["bullets"] == ["Cut API latency by 35% with Redis caching."] +def test_the_most_relevant_role_keeps_a_third_bullet(replies): + bullets = ["Built a FastAPI service ingesting 40k readings a day into PostgreSQL.", + "Wrote tests for the ingestion service in Python.", + "Documented the service's API for the analytics team."] + model = _model_cv(bullets[0]) + model["experience"][0]["bullets"] = bullets + replies.queue = [json.dumps(model)] + assert _generate_cv()["experience"][0]["bullets"] == bullets + assert "up to 3 for the ONE role" in replies.prompts[0] + + def test_a_letter_with_a_flagged_word_is_revised(replies): first = {"salutation": "Dear Hiring Manager,", "signoff": "Yours faithfully,", "headline": "Graduate", "paragraphs": ["I leverage FastAPI daily.", "Your team of 40 engineers ships daily."]} diff --git a/api/tests/test_documents.py b/api/tests/test_documents.py index 0bed541..d8a1ca4 100644 --- a/api/tests/test_documents.py +++ b/api/tests/test_documents.py @@ -491,6 +491,13 @@ def test_the_letter_ends_on_the_candidates_closing_line(llm, close): "I'd welcome the chance to talk. Thank you for your time and consideration and I hope to hear from you soon.") +def test_the_hook_says_why_the_employer_draws_the_candidate(llm): + llm.reply = json.dumps({"headline": "Computer Science Graduate", "salutation": "Dear Hiring Manager", + "paragraphs": _letter_paragraphs(), "signoff": "Kind Regards,"}) + _cover_letter() + assert "why that fact draws THIS candidate" in llm.prompts[0] + + def test_c_plus_plus_stays_on_one_line(): html = doc_render.render_letter_html({"candidate_name": "Sam Okafor", "paragraphs": ["I write C++ daily."]}) assert "C+⁠+ daily" in html # a word joiner between the plus signs