Let an attorney redo one section of a draft
Context
Dana, a patent attorney at one of our firms, says two things about the draft list.
"Drafts that the model is still writing show up in my list as Ready, so I open them
and the sections are half empty." And: "When the claims section comes back wrong, my
only option is to throw the whole draft away and start again, which loses the two
sections that were fine."
Your task
Three phases. Start at the top and go as far as you get.
-
Fix the status label. A draft is pending while the model is writing it,
ready when it is finished and failed when the model gave up. One of those
three is shown wrongly in the draft list. Find it and fix it. Small, a few minutes.
-
Add "regenerate one section", back end and front end. There is nothing for
this in the code today, so you are adding it end to end. The exact contract is
below. This is the main part of the exercise, and it is meant to touch both the
Python and the browser code.
-
Stretch, only if you have time: a limit of three regenerations per draft.
See below. Most people do not reach this phase, and that is fine. We are not
expecting all three phases to be finished. Phases 1 and 2 done well beat all three
done in a rush.
The contract for phase 2
POST /api/drafts/<id>/regenerate with a JSON body {"section": "background"}.
section is one of background, summary or claims.
- It rewrites that one section and leaves the other two exactly as they were.
- The new text has to be different from the text that was there.
DraftingModel in
model_client.py takes an attempt number: draft_section(title, jurisdiction, matter_title, section, attempt=2) returns different text for a different attempt
number, with the same text every time for the same number.
- Success is 200 with the same JSON shape
POST /api/drafts returns, so
id, matterId, title, jurisdiction, status, createdAt and sections
(all three sections, with the new text in the one that was regenerated).
- A draft whose status is not
ready is 409 {"error": "draft_not_ready"}.
- An id that is not in the database is 404
{"error": "draft_not_found"}.
- A missing or unknown
section is 400 {"error": "invalid_section"}.
- An id that is not a number is 400
{"error": "invalid_draft_id"}.
In the browser:
renderDraftList in web/render.js puts, on ready drafts only, a
<select class="regen-section" data-draft-id="{id}"> with the three section names
as its option values, and next to it one
<button type="button" data-action="regen" data-draft-id="{id}">.
Drafts that are not ready get no button.
web/api.js gets regenerateSection(draftId, section), which posts to the
endpoint above. web/main.js wires the click to it and refreshes the list.
The contract for phase 3 (stretch)
- A draft can be regenerated at most 3 times, counted over the life of the draft.
Nothing resets, and there is no time window to worry about.
- The 4th attempt is 429
{"error": "regen_limit_reached"} and changes nothing.
- The regenerate response and every item in
GET /api/drafts carry a
regensRemaining number: 3 on a fresh draft, then 2, 1, 0.
- The button label carries the count, exactly
Regenerate (2 left). At 0 the button
is still in the list and is disabled.
Start here
draft_service.py — every rule about drafts. Read create_draft first; the new
rule belongs next to it.
server.py — the HTTP layer: the parse_* input helpers at the top, then the
routes inside create_app.
web/render.js — turns draft data into HTML strings. Phase 1 lives in here.
What's here
server.py HTTP layer, input parsing, the router, create_app(options)
draft_service.py the rules about drafts, plus DraftError
repository.py the only module with SQL in it: schema, seed data, queries
model_client.py DraftingModel, a deterministic stand-in for the real model
notifier.py builds the "your draft is ready" email
mailer.py Mailer (logs the message) and FakeMailer (keeps a list)
web/render.js pure functions that turn draft data into HTML strings
web/api.js every call to the backend
web/main.js the only file that touches the DOM
web/index.html the page
tests/test_drafts_api.py current backend behaviour, all passing
tests/render.test.js current render behaviour, all passing
Things you should keep as they are, because we run our own tests against them:
create_app(options) and its option keys (db_path, mailer, model, clock,
app_url, seed); app.request(method, path, body, headers) returning something
with .status, .body and .json(); the drafts and draft_sections tables and
their columns; the existing responses of POST /api/drafts and GET /api/drafts
(extra fields are fine); the names renderDraftList, regenerateSection. You can
add tables, columns, files and functions freely.
The drafting model is a local stand-in, not a network call, so the same inputs always
produce the same text. Python 3.11 standard library only, Node 22 or newer. No
installs, no network.
Running it
python3 -m unittest discover -s tests
node --test --no-warnings tests/render.test.js
python3 server.py # then open http://localhost:8000
python3 server.py writes to drafts.db in the current directory. Delete that file
to start over.
Time
About 30 minutes. Phase 1 is a few minutes, phase 2 is the bulk of it, phase 3 is a
stretch that most people will not get to. Finishing all three is not expected. We
care much more about how you work and about the quality of what you do finish than
about how far down the list you get. If you run out of time mid-phase, leave a
sentence or two in a scratch file saying what you would do next.