Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions api/app/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,16 @@ def _persona_projects_for_llm(persona: dict) -> list[dict]:
"using", "into", "across", "within", "including", "etc", "via", "per", "new", "use",
"used", "build", "building", "built", "develop", "developed", "developing", "design",
"designing", "designed", "candidate", "company", "join", "looking", "passionate",
# Everyday words in nearly every ad, and the job-ad vocabulary itself: these say nothing about fit, and they ranked
# a project about job hunting (Windrush) first for ads it had nothing to do with.
"about", "against", "all", "also", "any", "best", "but", "can", "day", "each", "every", "first", "how", "like",
"may", "more", "next", "not", "one", "out", "over", "own", "part", "set", "since", "such", "than", "their", "them",
"they", "through", "time", "was", "were", "what", "when", "where", "which", "who", "why", "world", "would",
"application", "applications", "apply", "based", "benefits", "business", "candidates", "career", "careers",
"challenges", "committed", "communication", "core", "degree", "development", "directly", "driven", "employer",
"employers", "end", "engineering", "exposure", "full", "global", "hiring", "hours", "human", "impact", "job", "jobs",
"learn", "level", "live", "london", "office", "open", "people", "problem", "process", "quality", "real",
"recruitment", "requirements", "roles", "salary", "search", "sponsorship", "teams", "technical", "value", "years",
})


Expand Down Expand Up @@ -464,6 +474,28 @@ def _rank_projects_by_relevance(job: dict, projects: list[dict]) -> list[dict]:
return [p for _, _, p in scored]


def _project_short_name(name: str) -> str:
"""'Windrush: Autonomous AI Career Advisor · AI London 2026' -> 'Windrush'."""
return re.split(r"[:·]", name or "")[0].strip()


def opening_names_barred_project(letter: dict, persona: dict) -> list[str]:
"""Projects the candidate keeps out of a letter's opening (Project.not_in_opening) that its first paragraph names."""
first = next(iter(letter.get("paragraphs") or []), "")
names = [_project_short_name(p.get("name", "")) for p in persona.get("projects") or [] if p.get("not_in_opening")]
return [n for n in names if n and re.search(rf"\b{re.escape(n)}\b", first, re.I)]


def _letter_findings(letter: dict, persona: dict, job: dict, research: str, hook: str) -> list[dict]:
"""What the letter's one targeted revision has to fix: unbacked claims, machine-sounding words, a barred opening."""
return (doc_check.check_facts("cover_letter", letter, persona, job, research)
+ doc_check.check_style("cover_letter", letter)
+ [{"kind": "opening", "field": "paragraphs.0", "label": "First paragraph", "value": name,
"message": f"it names {name}, which the candidate keeps out of the opening"
+ (f": open on {_project_short_name(hook)} instead" if hook else "")}
for name in opening_names_barred_project(letter, persona)])


# How many skills a tailored CV lists: the candidate's own CV design shows about 30, in 5 rows.
_MIN_CV_SKILLS, _MAX_CV_SKILLS = 20, 30
_CV_SKILLS_CEILING = 40 # a choice a little over the asked-for range is still better than the full list
Expand Down Expand Up @@ -604,8 +636,9 @@ def _finding_keys(findings: list[dict]) -> set[tuple[str, str]]:
def _issue_lines(findings: list[dict]) -> str:
lines = []
for f in findings:
fix = ("use plain wording instead" if f["kind"] == "word"
else "remove it, or replace it with a fact the candidate data states")
fix = {"word": "use plain wording instead",
"opening": "tell the paragraph's story from that project, using only what the candidate data states",
}.get(f["kind"], "remove it, or replace it with a fact the candidate data states")
lines.append(f"- {f['label']}: {f['message']} — {fix}")
return "\n".join(lines)

Expand Down Expand Up @@ -1185,10 +1218,13 @@ async def execute_tool(name: str, tool_input: dict) -> dict:
core = persona.get("core_info", {})
full_name = f"{core.get('first_name', '')} {core.get('last_name', '')}".strip()
level = _seniority(persona)
# Surface the most domain-relevant projects so the EVIDENCE paragraph can anchor on
# the best-fitting project (not always the internship) when one fits the role better.
# The most domain-relevant projects, named by role: the model opens on whichever project it's told to (with the
# top three in reverse, every letter opened on the third), so the code picks the hook, never one the candidate
# keeps out of openings (Project.not_in_opening); those can still be evidence.
ranked_projects = _rank_projects_by_relevance(job, _persona_projects_for_llm(persona))
top_project_names = [p["name"] for p in ranked_projects[:3]]
barred = {p.get("name") for p in persona.get("projects") or [] if p.get("not_in_opening")}
hook_project = next((p["name"] for p in ranked_projects if p["name"] not in barred), "")
evidence_projects = [p["name"] for p in ranked_projects if p["name"] != hook_project][:2]
# Live company research grounds the "Why them?" paragraph (best-effort; "" on failure).
company_research = await _research_company(job.get("company", ""), job.get("title", ""))

Expand Down Expand Up @@ -1225,7 +1261,7 @@ async def execute_tool(name: str, tool_input: dict) -> dict:
"'headline' = the candidate's concise professional identity in 2-4 words "
"(e.g. 'Computer Science Graduate'); do NOT put the role being applied for. "
"Write EXACTLY 4 paragraphs, one page total (~320-380 words), in this order: "
"(1) HOOK ~90 words — open on a specific moment from ONE of the candidate's own projects: the hardest "
"(1) HOOK ~90 words — open on a specific moment from the PROJECT FOR THE HOOK named below: the hardest "
"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 "
Expand Down Expand Up @@ -1266,8 +1302,11 @@ async def execute_tool(name: str, tool_input: dict) -> dict:
f"CANDIDATE PERSONA (JSON): {json.dumps(_llm_persona(persona))}\n\n"
f"JOB: {job.get('title')} at {job.get('company')}\n"
f"DESCRIPTION: {job.get('description', '')[:2500]}"
+ (f"\n\nMOST DOMAIN-RELEVANT PROJECTS for this role, in priority order (one for the hook, a "
f"different two for the evidence): {', '.join(top_project_names)}" if top_project_names else "")
+ (f"\n\nPROJECT FOR THE HOOK: {hook_project}" if hook_project else "")
+ (f"\nPROJECTS FOR THE EVIDENCE, in priority order: {', '.join(evidence_projects)}"
if evidence_projects else "")
+ (f"\nDo NOT name {', '.join(_project_short_name(b) for b in sorted(barred))} in the first paragraph."
if barred else "")
+ (f"\n\nCOMPANY RESEARCH (take the hook's fact about the employer from this and tie it to the "
f"candidate — do NOT restate it):\n{company_research}" if company_research else "")
),
Expand Down Expand Up @@ -1307,16 +1346,14 @@ async def execute_tool(name: str, tool_input: dict) -> dict:
# machine-sounding words: one targeted revision, kept only if it clears them.
revision = "fallback"
if data:
findings = (doc_check.check_facts("cover_letter", letter, persona, job, company_research)
+ doc_check.check_style("cover_letter", letter))
findings = _letter_findings(letter, persona, job, company_research, hook_project)
revision = "clean"
if findings:
revised = await _revise_letter_text(letter, findings, persona, job)
if revised is None:
revision = f"{len(findings)} found, bad reply"
else:
after = (doc_check.check_facts("cover_letter", revised, persona, job, company_research)
+ doc_check.check_style("cover_letter", revised))
after = _letter_findings(revised, persona, job, company_research, hook_project)
kept = _better(findings, after)
revision = f"{len(findings)}->{len(after)} {'kept' if kept else 'rejected'}"
if kept:
Expand Down
3 changes: 3 additions & 0 deletions api/app/autopilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ def document_problems(cv: dict, letter: dict, persona: dict, job: dict, research
messages = list(dict.fromkeys(f["message"] for f in findings))
out.append(f"{name} says something your profile doesn't back: " + "; ".join(messages[:3])
+ (f" and {len(messages) - 3} more" if len(messages) > 3 else ""))
barred = agent.opening_names_barred_project(letter or {}, persona or {})
if barred:
out.append(f"The cover letter opens on {', '.join(barred)}, which you keep out of openings.")
return out


Expand Down
25 changes: 24 additions & 1 deletion api/app/doc_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,12 +338,35 @@ def build_letter_template_data(letter: dict, fit: tuple[str, ...] = ()) -> dict:
}


_COUNTRY = r"(?:united kingdom|great britain|england|scotland|wales|northern ireland|gbr?|uk|united states|usa|us)"
# Before the town: "GB - London", "GBR-Nottingham-1 Chapel Qtr", "Hybrid - London", "Full-Time – London", "August 2026 - London".
_NOT_THE_TOWN_YET = re.compile(
rf"^\s*(?:{_COUNTRY}|hybrid|remote|on-?site|home|office(?: based)?|(?:full|part)-?time|[a-z]+ \d{{4}})\s*[-–—]\s*", re.I)
# After it: "Glasgow Campus", "Belfast United Kingdom", "Berlin Metropolitan Area", "Massachusetts Remote Work".
_NOT_THE_TOWN_ANY_MORE = re.compile(rf"\s+(?:campus|metropol[a-z]+ area|remote(?: work)?|{_COUNTRY})$", re.I)
_NOT_A_TOWN = re.compile(rf"^(?:{_COUNTRY}\b|(?:hybrid|remote|on-?site|office|home|multiple locations|various|see listing|telecommute)$)", re.I)


def _letter_town(location: str) -> str:
"""The first town in a job's location, whichever way the employer's system writes it: no country, work setting or
street address ("United Kingdom - London", "Belfast 10 Mays Meadow", "LONDON, United Kingdom" -> a town each)."""
for part in re.split(r"[,;/|]", re.sub(r"\([^)]*\)", " ", location or "")):
while _NOT_THE_TOWN_YET.match(part):
part = _NOT_THE_TOWN_YET.sub("", part, count=1)
# What follows it: a street number ("-1 Chapel Qtr", " 10 Mays Meadow"), a site ("London - Shell Centre").
part = re.split(r"[\s-]+\d|^\d|\s+[-–—]\s+", part.strip())[0]
part = _NOT_THE_TOWN_ANY_MORE.sub("", part).strip(" -–—")
if part and not _NOT_A_TOWN.match(part):
return part.title() if part.isupper() and len(part) > 3 else part
return ""


def _letter_recipient_lines(company: str, location: str) -> list[str]:
"""Who the letter is to, under the contact block: "Recruitment Team," / "Acme," / "London." The town is the
first part of the job's location, without a work setting ("London, UK (Hybrid)" -> "London")."""
if not company:
return []
town = re.sub(r"\s*\([^)]*\)", "", re.split(r"[,;/]", location or "")[0]).strip()
town = _letter_town(location)
return ["Recruitment Team,", f"{company}{',' if town else '.'}"] + ([f"{town}."] if town else [])


Expand Down
1 change: 1 addition & 0 deletions api/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class Project(BaseModel):
outcomes: str = ""
summary: str = ""
is_ongoing: bool = False
not_in_opening: bool = False # never the project a cover letter opens on (it can still be evidence)

class BehavioralStory(BaseModel):
title: str
Expand Down
9 changes: 9 additions & 0 deletions api/tests/test_autopilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ def test_a_document_claim_the_profile_doesnt_back_holds_the_form():
assert autopilot.hold_reasons({"ready": True, "error": "", "questions": [], "empty": []}, problems) == problems


def test_a_letter_opening_on_a_barred_project_holds_the_form():
persona = {"core_info": {"first_name": "Ada"}, "projects": [{"name": "Windrush: Career Advisor", "not_in_opening": True}]}
job = {"title": "Engineer", "description": ""}
empty = {"summary": "", "experience": [], "projects": []}
assert autopilot.document_problems(empty, {"paragraphs": ["Building Windrush taught me.", "Next."]}, persona, job) == [
"The cover letter opens on Windrush, which you keep out of openings."]
assert autopilot.document_problems(empty, {"paragraphs": ["Other.", "Windrush taught me."]}, persona, job) == []


@pytest.mark.parametrize("url, ats", [
("https://boards.greenhouse.io/acme/jobs/1", "Greenhouse"),
("https://job-boards.greenhouse.io/acme/jobs/1", "Greenhouse"),
Expand Down
2 changes: 1 addition & 1 deletion api/tests/test_cv_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def test_upload_keeps_bullets_stack_projects_and_certifications(client, user, mo
assert {"grade": "First Class Honours", "end_date": "2025"}.items() <= p["education"][0].items() # a lone year ends it
assert p["projects"] == [{"name": "Engine Sim", "url": "https://github.com/ada/engine",
"problem_solved": "Simulates the engine.", "technologies": ["Python", "Numba"],
"outcomes": "10x faster than the 1843 notes", "summary": "", "is_ongoing": False}]
"outcomes": "10x faster than the 1843 notes", "summary": "", "is_ongoing": False, "not_in_opening": False}]
assert p["certifications"][0]["issuing_organization"] == "Amazon"
assert p["summary"] == "Engineer who ships ML products."

Expand Down
35 changes: 35 additions & 0 deletions api/tests/test_doc_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,41 @@ def test_a_letter_with_a_flagged_word_is_revised(replies):
f"Your team of 40 engineers ships daily. {agent._CLOSING_LINE}"]


def test_job_ad_vocabulary_doesnt_rank_a_project_first():
job = {"title": "Graduate Developer", "description": "Our global teams value communication and quality. Apply for "
"this career in London: the application process, job requirements and hiring. You will price options."}
careers = {"name": "Careers", "problem_solved": "A job search and application process for global teams, on quality "
"and communication.", "outcomes": "Career and hiring requirements in London.", "technologies": []}
options = {"name": "Pricer", "problem_solved": "Options pricing.", "outcomes": "", "technologies": []}
assert agent._rank_projects_by_relevance(job, [careers, options])[0]["name"] == "Pricer"


def _with_barred_project() -> dict:
persona = copy.deepcopy(GRADUATE)
persona["projects"] = [{"name": "Windrush: Career Advisor", "problem_solved": "FastAPI Python services.",
"outcomes": "", "technologies": ["FastAPI", "Python"], "not_in_opening": True},
{"name": "Pantry Planner", "problem_solved": "A meal planner.", "outcomes": "", "technologies": []}]
return persona


def test_the_letter_opens_on_the_best_project_the_candidate_allows_there(replies):
replies.queue = [json.dumps({"paragraphs": ["Pantry Planner was hard.", "Windrush used FastAPI."]})]
letter = asyncio.run(agent.execute_tool("generate_cover_letter", {"job": AD, "persona": _with_barred_project()}))["letter"]
assert letter["paragraphs"][0] == "Pantry Planner was hard."
assert "PROJECT FOR THE HOOK: Pantry Planner" in replies.prompts[0]
assert "PROJECTS FOR THE EVIDENCE, in priority order: Windrush: Career Advisor" in replies.prompts[0]
assert "Do NOT name Windrush in the first paragraph." in replies.prompts[0]
assert len(replies.prompts) == 1 # naming it as evidence isn't an issue, so no revision


def test_a_letter_opening_on_a_barred_project_is_revised(replies):
replies.queue = [json.dumps({"paragraphs": ["Building Windrush was hard.", "Pantry Planner too."]}),
json.dumps({"paragraphs": ["Pantry Planner was hard.", "Windrush too."]})]
letter = asyncio.run(agent.execute_tool("generate_cover_letter", {"job": AD, "persona": _with_barred_project()}))["letter"]
assert letter["paragraphs"][0] == "Pantry Planner was hard."
assert "open on Pantry Planner instead" in replies.prompts[1]


# ── Critique and revise (writing quality) ─────────────────────────────────────────────────────

@pytest.fixture
Expand Down
31 changes: 31 additions & 0 deletions api/tests/test_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,11 +516,42 @@ def test_the_letter_leaves_out_the_street_address():
("London, UK (Hybrid)", ["Recruitment Team,", "Acme Robotics,", "London."]),
("London / Remote", ["Recruitment Team,", "Acme Robotics,", "London."]),
("", ["Recruitment Team,", "Acme Robotics."]),
("United Kingdom", ["Recruitment Team,", "Acme Robotics."]),
])
def test_the_letter_is_addressed_to_the_employers_recruitment_team(location, lines):
assert doc_render._letter_recipient_lines("Acme Robotics", location) == lines


@pytest.mark.parametrize("location, town", [
("United Kingdom - London", "London"),
("GB - London, United Kingdom (Hybrid)", "London"),
("GBR-Nottingham-1 Chapel Qtr", "Nottingham"),
("Belfast 10 Mays Meadow; Der-Derry/Londonderry (Hybrid)", "Belfast"),
("Canary Wharf, 1 Churchill Place", "Canary Wharf"),
("Glasgow Campus", "Glasgow"),
("Hybrid - London", "London"),
("Office Based - London, UK", "London"),
("August 2026 - London", "London"),
("London - Shell Centre", "London"),
("London, United Kingdom - Deliveroo", "London"),
("LONDON, United Kingdom", "London"),
("BURGESS HILL, WEST SUSSEX, United Kingdom", "Burgess Hill"),
("Edinburgh, UK (ZUK129)", "Edinburgh"),
("Home - UK- England", ""),
("United Kingdom Offsite (ZUK99)", ""),
("Multiple Locations", ""),
("See listing", ""),
("USA", ""),
("US-Remote", ""),
("Full-Time – London", "London"),
("Belfast United Kingdom", "Belfast"),
("London Metropolitain Area", "London"),
("Remote", ""),
])
def test_the_letter_takes_the_town_from_however_the_employer_writes_it(location, town):
assert doc_render._letter_town(location) == town


def test_a_job_without_a_location_leaves_the_employers_location_blank(llm):
persona = copy.deepcopy(GRADUATE)
persona["core_info"]["country"] = "Nigeria"
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/app/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,10 @@ export default function ProfilePage() {
<input type="checkbox" checked={p.is_ongoing} onChange={(e) => projects.edit(i, { is_ongoing: e.target.checked })}
className="w-4 h-4" style={{ accentColor: "var(--teal)" }} />Still working on it
</label>
<label className="flex items-center gap-2 text-[13.5px]">
<input type="checkbox" checked={!!p.not_in_opening} onChange={(e) => projects.edit(i, { not_in_opening: e.target.checked })}
className="w-4 h-4" style={{ accentColor: "var(--teal)" }} />Don&apos;t open cover letters with this project
</label>
</div>
}>
{(p.summary || p.problem_solved) && <p className="m-0 text-[13.5px] leading-[1.5] text-ink-2">{p.summary || p.problem_solved}</p>}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export interface Project {
outcomes: string;
summary: string;
is_ongoing: boolean;
not_in_opening?: boolean;
}

export interface Certification {
Expand Down
Loading