Skip to content

Phase 3: answer application questions from an answer bank, never guessing a fact - #26

Merged
coderback merged 2 commits into
mainfrom
feat/application-answers
Oct 16, 2026
Merged

coderback merged 2 commits into
mainfrom
feat/application-answers

Conversation

@coderback

Copy link
Copy Markdown
Owner

Phase 3, part 1: application questions are answered from an answer bank and the persona, and a fact is never guessed.

What it does

Every screening question the apply agent meets goes through a new answer_questions action (api/app/answers.py). The agent's task no longer lists facts, stories or screening answers for it to answer from. Each answer carries its source:

  • saved: your answer bank (new answer_bank table, /answers endpoints);
  • profile: a fact in your persona (right to work, sponsorship, notice period, nationality, a degree, a role's dates…);
  • drafted: a written answer ("Why us?", "A time when…") drafted for the job. doc_check checks it, and it is revised once if it carries an unsupported claim;
  • needs_you: nothing you've given states it, so the form leaves it blank.

A model maps each question to the fact that answers it. The value always comes from the fact, and code checks the model's reading:

  • it must cite a known fact;
  • it must be one of the form's options;
  • it must keep a yes/no fact's direction ("require sponsorship?" vs "work without sponsorship?");
  • it must not carry a UK fact to another country.

Contact details, salary, equality answers and the date of birth reach that model only as "this fact exists"; Windrush fills in the value.

In the app

  • Review (job workspace and Agent review queue): a "Questions on this form" panel lists each answer and its source. A blank question gets a box with the form's own options, and what you answer is saved for the next form. Drafts can be edited and saved.
  • Agent notes: the notes start with the questions only you can answer.
  • Persona: a new "Saved answers" section lets you read, edit, delete and add answers.
  • Applications: /apply stores the questions on the application.

How well it does

api/evals/test_answers_eval.py (opt-in, WINDRUSH_LLM_EVAL=1) puts 40 real application questions to a test persona with an answer bank. The questions include right to work, US authorisation, sponsorship worded both ways, salary bands, equality monitoring, declarations, convictions and four written questions.

Run Facts within their labels Answerable facts left for you Drafts with a flagged claim
1 36/36 2/27 0/4
2 36/36 3/27 0/4
3 36/36 2/27 0/4

The facts left for you were a salary band, a Kubernetes question your skills don't mention, and once a Skilled Worker visa question. The eval fails on any answer outside its labels, a flagged claim in a draft, or a private value in a prompt.

A live run of the real model on a local practice form went as intended:

  • one answer_questions call covered all 8 questions;
  • it filled right to work, sponsorship, notice, the written answer, the driving licence (from the bank) and gender;
  • it left the convictions question and the accuracy declaration blank.

That run also found the agent passing "(max 150 words)" as a 150-character box limit, which cut the draft to a sentence. That is fixed and tested.

Tests

  • API: 727 pass, including 42 new ones in test_answers.py.
  • Browser (real Chromium): 14 pass. A new one checks that the nested questions reach the action, that the written answer is pasted whole, and what the review lists.
  • Frontend: tsc, lint and build pass, and 12/12 Playwright smoke tests pass, including 2 new ones.

…essing a fact

Every screening question the apply agent meets now goes through an
answer_questions action (answers.py) instead of facts listed in its task.
Each answer says where it came from:
- saved: the answer bank (new answer_bank table, /answers endpoints);
- profile: a fact in the persona (right to work, notice, nationality, a degree);
- drafted: a written answer drafted for the job, checked by doc_check and
  revised once if it carries an unsupported claim;
- needs_you: nothing the user has given states it, so it is left blank.

A model maps each question to the fact that answers it; the value comes
from the fact, and code checks the reading: it must cite a known fact, be
one of the form's options, keep a yes/no fact's direction, and not carry a
UK fact to another country. Contact details, salary, equality answers and
the date of birth reach that model only as "this fact exists".

The review event carries every question with its answer and source, the
notes lead with the questions left blank, and /apply stores them on the
application. fill_answer pastes a written answer whole, as
paste_cover_letter does for the letter.

evals/test_answers_eval.py (opt-in) puts 40 real application questions to
a test persona with an answer bank; three runs: every fact within its
labelled answers, 2-3 of 27 answerable facts left for the candidate, no
flagged claim in any draft.
… in your persona

The review (in the job workspace and the Agent review queue) gets a
"Questions on this form" panel: every question the agent met, its answer,
where it came from (saved answer, your persona, drafted for this job, or
only you can answer) and what it rests on. A question left blank gets a
box with the form's own options; answering it saves it to your answer bank
for the next form. A drafted answer can be edited and saved for next time.

Persona gets a "Saved answers" section to read, edit, delete and add to
the answer bank. The job description now goes to /apply for written
answers. Two Playwright smoke tests cover both.
@coderback
coderback merged commit fd7ac3b into main Oct 16, 2026
4 checks passed
@coderback
coderback deleted the feat/application-answers branch October 16, 2026 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant