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
9 changes: 6 additions & 3 deletions api/app/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,8 @@ async def _revise_cv_text(cv: dict, findings: list[dict], candidate: dict, job:
"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 "
"joined by the discipline both show; neither is the hook's project when the candidate has three or more, and "
"no detail is told twice. 3. HONEST GAP: one real requirement from the ad the candidate is lighter "
"on, stated plainly with what they do have and how they'd close it; no invented weakness, no self-deprecation "
"(or, if nothing is missing, a third piece of evidence). 4. CLOSE: short; no perks or salary. 5. VOICE: first "
"person, direct and specific, no hedging filler or clichés."
Expand Down Expand Up @@ -1233,6 +1234,8 @@ async def execute_tool(name: str, tool_input: dict) -> dict:
"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. "
"Whenever the data has three or more projects, the hook's project is a different one from the two in "
"EVIDENCE, so no project or detail is told twice. "
"(2) EVIDENCE ~110 words — start 'Two projects demonstrate the <quality this role needs> I'd bring to "
"your <team/work>.' First, one named project in precise technical detail with a real number from the "
"candidate data, matched to a requirement in the ad (use the ad's own terms). Then 'Separately, ...' a "
Expand Down Expand Up @@ -1263,8 +1266,8 @@ 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 (draw the hook and the evidence from these, "
f"in priority order): {', '.join(top_project_names)}" if top_project_names else "")
+ (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\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
63 changes: 48 additions & 15 deletions api/app/autopilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,31 +289,62 @@ def _pdf(doc_id: str) -> str:
return path if path and os.path.exists(path) else ""


def _earlier_documents(user_id: str, job: dict) -> dict | None:
"""The CV and cover letter the last run for this job wrote, when both PDFs are still there."""
docs = tracker.autopilot_documents(user_id, str(job.get("id", "")))
return docs if docs and _pdf(docs["cv_doc_id"]) and _pdf(docs["letter_doc_id"]) else None


async def _write_documents(state: _Run, job: dict, persona: dict) -> dict:
"""A tailored CV and cover letter for the job, rendered: {cv, template_id, letter, research, cv_doc_id,
letter_doc_id}."""
state.step = "Writing the tailored CV"
cv_result = await agent.execute_tool("generate_tailored_cv", {"job": job, "persona": persona})
cv, template_id = cv_result.get("cv"), cv_result.get("template_id")
if not cv:
raise _Stop("Couldn't write the tailored CV.")
state.step = "Writing the cover letter"
letter_result = await agent.execute_tool("generate_cover_letter", {"job": job, "persona": persona})
letter = letter_result.get("letter") or {}
if not letter.get("paragraphs"):
raise _Stop("Couldn't write the cover letter.")
return {"cv": cv, "template_id": template_id, "letter": letter, "research": letter_result.get("research") or "",
"cv_doc_id": await doc_render.render_pdf("cv", cv, template_id),
"letter_doc_id": await doc_render.render_pdf("cover_letter", letter, template_id)}


def _keep_documents(run_id: str, user_id: str, docs: dict) -> None:
tracker.update_autopilot_run(run_id, user_id=user_id, cv_doc_id=docs["cv_doc_id"],
letter_doc_id=docs["letter_doc_id"],
documents={k: docs.get(k) for k in ("cv", "template_id", "letter", "research")})


async def _apply(state: _Run, user_id: str, job: dict, verdict: dict, cfg: dict, persona: dict) -> str:
"""Try one job: documents, then the form, then send or hold. Returns the run's outcome."""
live = cfg["live"]
# A live send uses the CV and cover letter the last run for this job wrote (the practice run the user looked
# at), so what goes out is what they saw. Read before start_autopilot_run replaces that run; practice always
# writes them afresh.
earlier = _earlier_documents(user_id, job) if live else None
run_id = tracker.start_autopilot_run(user_id, job, practice=not live, fit_score=verdict.get("fit_score"))
if earlier:
_keep_documents(run_id, user_id, earlier) # at once, so a retry after a failure still has them
run = tracker.get_autopilot_run(user_id, run_id)
state.job = f"{job.get('title', '')} at {job.get('company', '')}"
held: list[str] = []
final: dict = {}
questions: list[dict] = []
try:
job = await jobs_db.ensure_full_description(job)
state.step = "Writing the tailored CV"
cv_result = await agent.execute_tool("generate_tailored_cv", {"job": job, "persona": persona})
cv, template_id = cv_result.get("cv"), cv_result.get("template_id")
if not cv:
raise _Stop("Couldn't write the tailored CV.")
state.step = "Writing the cover letter"
letter_result = await agent.execute_tool("generate_cover_letter", {"job": job, "persona": persona})
letter = letter_result.get("letter") or {}
if not letter.get("paragraphs"):
raise _Stop("Couldn't write the cover letter.")
problems = document_problems(cv, letter, persona, job, letter_result.get("research") or "")
cv_doc_id = await doc_render.render_pdf("cv", cv, template_id)
letter_doc_id = await doc_render.render_pdf("cover_letter", letter, template_id)
tracker.update_autopilot_run(run_id, user_id=user_id, cv_doc_id=cv_doc_id, letter_doc_id=letter_doc_id)
if earlier:
state.step = "Using the CV and cover letter from the last run"
docs = earlier
else:
docs = await _write_documents(state, job, persona)
_keep_documents(run_id, user_id, docs)
cv, letter = docs["cv"], docs["letter"]
cv_doc_id, letter_doc_id = docs["cv_doc_id"], docs["letter_doc_id"]
problems = document_problems(cv, letter, persona, job, docs.get("research") or "")

text = letter_text(letter)
answer_job = {"title": job.get("title", ""), "company": job.get("company", ""),
Expand Down Expand Up @@ -380,8 +411,10 @@ async def decide(filled: dict) -> bool:
"needs_you": "Autopilot filled this in and held it for you."}[outcome]
tracker.update_status(app_id, status, note, user_id=user_id)
tracker.set_application_questions(app_id, questions, user_id=user_id)
reused = (f"Used the CV and cover letter from the {'practice ' if earlier['practice'] else ''}run of "
f"{earlier['updated_at'][:10]}. " if earlier else "")
tracker.update_autopilot_run(run_id, user_id=user_id, outcome=outcome, reasons=reasons, questions=questions,
notes=str(final.get("notes") or "")[:2000], application_id=app_id)
notes=(reused + str(final.get("notes") or ""))[:2000], application_id=app_id)
logger.info("autopilot: %s -> %s (%d reasons)", state.job, outcome, len(reasons))
return outcome

Expand Down
26 changes: 25 additions & 1 deletion api/app/tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
cv_doc_id TEXT,
letter_doc_id TEXT,
application_id TEXT,
documents TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
Expand Down Expand Up @@ -162,6 +163,8 @@ def init_db(db_path: str = "") -> None:
"ALTER TABLE users ADD COLUMN job_password_enc TEXT",
# The questions the agent met on the application's form, with each answer and its source.
"ALTER TABLE applications ADD COLUMN questions TEXT",
# The CV and cover letter a run wrote, so a live send uses the ones the practice run showed.
"ALTER TABLE autopilot_runs ADD COLUMN documents TEXT",
]:
try:
con.execute(stmt)
Expand Down Expand Up @@ -676,6 +679,7 @@ def _autopilot_run(row: sqlite3.Row) -> dict:
run["practice"] = bool(run["practice"])
run["reasons"] = json.loads(run["reasons"] or "[]")
run["questions"] = json.loads(run["questions"] or "[]")
run.pop("documents", None) # the documents' content: autopilot_documents reads it
return run


Expand All @@ -699,7 +703,7 @@ def start_autopilot_run(user_id: str, job: dict, outcome: str = "running", *, pr


_RUN_FIELDS = {"outcome", "practice", "fit_score", "reasons", "questions", "notes", "cv_doc_id", "letter_doc_id",
"application_id"}
"application_id", "documents"}


def update_autopilot_run(run_id: str, *, user_id: str, **fields) -> bool:
Expand All @@ -711,6 +715,8 @@ def update_autopilot_run(run_id: str, *, user_id: str, **fields) -> bool:
for name in ("reasons", "questions"):
if name in fields:
fields[name] = json.dumps(list(fields[name]), ensure_ascii=False)
if "documents" in fields:
fields["documents"] = json.dumps(dict(fields["documents"]), ensure_ascii=False)
if "practice" in fields:
fields["practice"] = int(bool(fields["practice"]))
fields["updated_at"] = _now()
Expand All @@ -730,6 +736,24 @@ def get_autopilot_run(user_id: str, run_id: str) -> dict | None:
return _autopilot_run(row) if row else None


def autopilot_documents(user_id: str, job_id: str) -> dict | None:
"""The CV and cover letter the last run for this job wrote: {cv, template_id, letter, research, cv_doc_id,
letter_doc_id, practice, updated_at}, or None when it kept none."""
con = sqlite3.connect(_DB_PATH)
con.row_factory = sqlite3.Row
row = con.execute("SELECT documents, cv_doc_id, letter_doc_id, practice, updated_at FROM autopilot_runs "
"WHERE user_id=? AND job_id=?", (user_id, str(job_id))).fetchone()
con.close()
try:
docs = json.loads(row["documents"]) if row and row["documents"] else None
except ValueError:
docs = None
if not isinstance(docs, dict) or not docs.get("cv") or not (docs.get("letter") or {}).get("paragraphs"):
return None
return {**docs, "cv_doc_id": row["cv_doc_id"] or "", "letter_doc_id": row["letter_doc_id"] or "",
"practice": bool(row["practice"]), "updated_at": row["updated_at"]}


def list_autopilot_runs(user_id: str, limit: int = 200) -> list[dict]:
"""The user's autopilot runs, latest first."""
con = sqlite3.connect(_DB_PATH)
Expand Down
42 changes: 41 additions & 1 deletion api/tests/test_autopilot.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Autopilot: which jobs it tries, when it sends a form and when it holds it for the user."""
import asyncio
import base64
import json
import os
import uuid

Expand Down Expand Up @@ -138,6 +139,7 @@ def __init__(self):
self.filled: list[str] = [] # companies whose form was filled
self.decided: dict[str, bool] = {}
self.cv_paths: list[str] = []
self.written: list[str] = [] # the documents written, as "cv" / "letter"


@pytest.fixture
Expand All @@ -150,8 +152,10 @@ async def execute_tool(name, tool_input):
"level_match": w.fit[j["company"]][1], "reasoning": "why"}
for j in tool_input["jobs"]]}
if name == "generate_tailored_cv":
return {"cv": {"summary": "Engineer"}, "template_id": "classic"}
w.written.append("cv")
return {"cv": {"summary": f"Engineer, draft {len(w.written)}"}, "template_id": "classic"}
if name == "generate_cover_letter":
w.written.append("letter")
return {"letter": {"salutation": "Dear Hiring Manager,", "paragraphs": ["I'd like to join."],
"signoff": "Yours faithfully,", "candidate_name": "Ada Lovelace"}, "research": ""}
raise AssertionError(name)
Expand Down Expand Up @@ -288,6 +292,42 @@ def test_each_job_is_tried_once_but_practice_runs_dont_count_once_live(ada, worl
assert len(world.filled) == 6


def test_a_live_send_uses_the_documents_the_practice_run_showed(ada, world):
_run(ada) # practice: writes and keeps a CV and letter per job
practice = _runs(ada)
assert len(world.written) == 6 and "documents" not in practice["alpha"] # listings stay small
_run(ada, live=True)
live = _runs(ada)
assert len(world.written) == 6 # nothing rewritten: what the user saw is what went up
assert sorted(world.cv_paths[3:]) == sorted(world.cv_paths[:3])
for name in ("alpha", "beta", "gamma"):
assert (live[name]["cv_doc_id"], live[name]["letter_doc_id"]) == \
(practice[name]["cv_doc_id"], practice[name]["letter_doc_id"])
assert live[name]["notes"].startswith("Used the CV and cover letter from the practice run of ")
sent_cv = {a["company"]: a["tailored_cv"] for a in tracker.list_applications(ada)}["alpha"]
assert json.loads(sent_cv) == tracker.autopilot_documents(ada, practice["alpha"]["job_id"])["cv"]


def test_a_practice_run_writes_new_documents_each_time(ada, world):
_run(ada)
first = _runs(ada)["alpha"]
asyncio.run(autopilot.run(ada, first["job_id"])) # a practice retry of one job
again = _runs(ada)["alpha"]
assert len(world.written) == 8 and again["cv_doc_id"] != first["cv_doc_id"]
assert tracker.autopilot_documents(ada, first["job_id"])["cv"]["summary"] == "Engineer, draft 7"


def test_a_live_send_writes_documents_when_the_earlier_ones_are_gone(ada, world):
_run(ada)
alpha = _runs(ada)["alpha"]
os.remove(pdf_generator.get_pdf_path(alpha["letter_doc_id"]))
_run(ada, live=True)
live = _runs(ada)
assert len(world.written) == 8 # alpha's pair again; beta's and gamma's kept
assert live["alpha"]["cv_doc_id"] != alpha["cv_doc_id"] and not live["alpha"]["notes"].startswith("Used")
assert live["beta"]["notes"].startswith("Used the CV and cover letter")


def test_jobs_already_in_the_tracker_are_left_alone(ada, world):
tracker.add_application(ada, {"job_id": "x", "title": "Graduate Engineer alpha", "company": "alpha"}, {}, "", {},
status="Saved")
Expand Down
1 change: 1 addition & 0 deletions api/tests/test_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ def test_the_hook_says_why_the_employer_draws_the_candidate(llm):
"paragraphs": _letter_paragraphs(), "signoff": "Kind Regards,"})
_cover_letter()
assert "why that fact draws THIS candidate" in llm.prompts[0]
assert "the hook's project is a different one from the two in EVIDENCE" in llm.prompts[0]


def test_c_plus_plus_stays_on_one_line():
Expand Down
Loading