Skip to content

Commit a703a06

Browse files
First pass - chatbot/assistant interface for constrained drafting
1 parent ef3e5e2 commit a703a06

26 files changed

Lines changed: 11297 additions & 285 deletions

architecture.md

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,40 @@ documents cannot be identified safely, the scoped save is rejected and the user
5858
must use full source mode. This is an interim safeguard pending the general
5959
revisioned source-patch model.
6060

61-
The general patch beta is implemented at `POST /al/editor/api/file/patch` and is
62-
disabled by default behind `WEAVER_ENABLE_PATCH_MODEL`. A request supplies the
61+
Weaver's editor settings use Docassemble's own key style — lowercase words,
62+
grouped under one heading:
63+
64+
```yaml
65+
weaver:
66+
assistant: True # the editing assistant; on unless set to False
67+
assistant model: gpt-5-mini
68+
runtime inspector: False # opt-in
69+
source patch api: False # opt-in
70+
```
71+
72+
A flat `weaver assistant:` key works too, and the older `WEAVER_ENABLE_*`
73+
spellings — plus the matching environment variables — are still honoured so
74+
existing installs keep working.
75+
76+
The general patch beta ("source patch api") is implemented at
77+
`POST /al/editor/api/file/patch` and is disabled by default. A request supplies the
6378
expected SHA-256 source revision and one or more non-overlapping
6479
`replace-range` operations. Weaver validates every range, applies the full set in
65-
memory, reparses the result, and performs one Playground write only if the result
66-
is structurally valid. The response includes the exact resulting text, new
67-
revision, applied operations, diagnostics, and a unified source diff. A stale
68-
revision returns HTTP 409 with current and optional base source for a three-way
69-
merge; it never overwrites the newer file.
80+
memory, runs the whole result through `validate_candidate_source()`, and performs
81+
one Playground write only if no diagnostic is an error. The response includes the
82+
exact resulting text, new revision, applied operations, diagnostics, and a
83+
unified source diff; a rejected patch returns HTTP 422 with the same diagnostic
84+
list. A stale revision returns HTTP 409 with current and optional base source for
85+
a three-way merge; it never overwrites the newer file.
86+
87+
`editor_agent_validation.py` owns the single answer to "may Weaver present this
88+
source as a valid edit?". Its pipeline is `parse_source_document()` → YAML stream
89+
check → `parse_interview_yaml()` → Weaver source diagnostics → DAYamlChecker →
90+
an optional deterministic ALDashboard lint that always passes `include_llm=False`.
91+
An error-severity diagnostic blocks acceptance; warnings and infos are reported
92+
to both the developer and the agent without blocking. The patch API, the agent's
93+
tools and the editor's unsaved-source check all call it, so agent editing can
94+
never acquire a weaker standard than ordinary editing.
7095

7196
`source_document.py` retains the original text and exact document offsets as the
7297
authoritative representation. Parsed mappings and top-level property ranges are
@@ -92,6 +117,77 @@ token into bootstrap state, and the centralized client sends it on every write.
92117
No editor route is CSRF-exempt and no wildcard CORS policy is installed. Any
93118
separate API-key integration remains outside the browser editor route family.
94119

120+
The editing assistant is on unless `weaver: assistant: False` turns it off. It
121+
does not depend on the source-patch API: the agent compiles its own range
122+
operations in process and never calls that endpoint. What it does need is a
123+
language model, so the page bootstrap carries an `assistant_status` saying
124+
whether one is reachable — ALToolbox leaves its client as `None` when it finds
125+
no credentials, which is the signal used rather than a guess at config key
126+
names. When no model is configured the panel is still offered but explains what
127+
is missing instead of showing a composer that would fail on submit, and the
128+
endpoints answer 503 rather than pretending the feature does not exist.
129+
130+
Its fundamental invariant is that the LLM proposes semantic
131+
actions, while Weaver produces source, validates source and controls
132+
persistence. The browser sends a working-source snapshot — the saved file with
133+
every unsaved buffer folded in, built by `editor_validation_source.js` — and the
134+
server binds the resulting session to one owner, project and filename. No tool
135+
argument can change that target; unknown properties in a tool call are a schema
136+
error precisely so a model cannot smuggle in a `project` or `filename`.
137+
138+
Each turn runs a bounded server-side loop: the model returns one JSON action,
139+
`editor_agent_tools.py` compiles it into an exact source replacement against an
140+
in-memory candidate, and the whole candidate is validated before the mutation is
141+
kept. Rejected mutations return structured diagnostics to the model and leave the
142+
candidate at its last valid revision, so candidate validity is monotonic. Only
143+
low-risk tools and a small set of deliberately implemented medium-risk ones are
144+
registered; blocks that `source_document.py` marks unsupported are readable but
145+
never rewritten. Runtime tools require `WEAVER_ENABLE_RUNTIME_INSPECTOR`, wrap
146+
the existing allowlisted `al_weaver.inspect_*` actions, and label their results
147+
`observed_runtime` so the model cannot present a static prediction — or a seeded
148+
scenario fixture — as observed behaviour.
149+
150+
Two deterministic operations are worth calling out because neither is safe as
151+
free-text editing. `editor_agent_repair.py` fixes the two blocking diagnostics
152+
that dominate real files — a question block with no `id`, and two blocks sharing
153+
one — by patching exact ranges and re-validating; a repair pass is kept only if
154+
it leaves strictly fewer blocking diagnostics. It is offered at session creation
155+
behind an explicit `auto_heal` flag, the repairs become part of the candidate so
156+
they appear in the diff, and Reset returns to the repaired baseline rather than
157+
to source the validator would reject. `editor_agent_rename.py` renames variables
158+
by classifying every appearance of a name: a reference it recognises is
159+
rewritten, prose is left alone and reported, and anything it cannot tell apart
160+
from a reference — a name inside a string, a call, a longer path built on the
161+
name, an `objects:` declaration that would become an attribute path — refuses
162+
the whole batch. `suggest_object_conversion` maps a flat family such as
163+
`persons1_name` onto `persons[0].name.first` using the same table the Weaver
164+
uses for PDF fields, skipping targets that are display expressions or that would
165+
collapse two variables into one.
166+
167+
A turn outlives any HTTP request — the editor's own client gives up first, and
168+
nginx closes an idle upstream read at sixty seconds by default — so it runs as
169+
the named `weaver_editor_agent_turn_task` in Docassemble's Celery worker,
170+
alongside project generation, and never in an in-process thread. Starting a turn
171+
returns 202 immediately. The loop publishes each event to a short-lived,
172+
owner-scoped progress record as it happens, and the finished turn's result lands
173+
there too, because that is the only place the browser can still read it. That
174+
record has its own Redis key rather than living in the session, because Stop and
175+
the polling reads touch the session concurrently and would otherwise clobber the
176+
turn's own writes. A record nothing has written to for two minutes is treated as
177+
abandoned rather than believed forever.
178+
179+
The assistant is for small, discrete edits, so a chat is capped at ten requests
180+
and counts down toward a prompt to apply and start a fresh one; a long
181+
conversation makes each turn slower and vaguer and its candidate harder to
182+
review as a single diff.
183+
184+
No agent step writes to the Playground. Apply re-checks that the saved file has
185+
not moved on, re-validates the candidate, and hands the source back to the
186+
browser as unsaved editor state that stays dirty against the revision actually on
187+
disk; the existing Save path is what persists it. Sessions live in owner-scoped
188+
expiring Redis records, and server logs record tool names, revisions and
189+
validator counts but never interview text, prompts or runtime variable values.
190+
95191
Unsaved interview edits are tracked by `editor_dirty_state.js` per filename and
96192
block ID, with separate source-dirty and pending-command state. Each loaded file
97193
also has a deep-cloned saved model. Discard restores that model, while a
@@ -172,6 +268,14 @@ kinds of interviews that the Weaver can produce.
172268
- `draggable_table.py` is used by the Weaver frontend to allow rearranging long lists of fields
173269
- `field_grouping.py` is a copy of some features from [FormyFyxer](https://github.com/SuffolkLITLab/FormFyxer) that power the "I'm feeling lucky" button (should be deprecated)
174270
- `generator_constants.py` contains several lists of rules for how to transform PDF field names like `users_name_full` into Docassemble objects like `users[0].name`, as well as indicating reserved DOCX variable names that are handled by questions in the AssemblyLine's question library
271+
- `api_editor.py` is HTTP orchestration for the graphical editor; the editing business logic lives in the modules below
272+
- `editor_agent_validation.py` is the one whole-candidate validator, plus the diagnostic normalisation the editor's error drawer consumes
273+
- `editor_agent_models.py` holds the agent session, candidate, turn and tool-result records and their owner-scoped Redis persistence
274+
- `editor_agent_tools.py` is the semantic tool registry — the security and accuracy boundary for everything the model can do
275+
- `editor_agent_repair.py` deterministically fixes missing and duplicate block ids so a mechanical problem does not stop the assistant from starting
276+
- `editor_agent_rename.py` classifies every appearance of a variable name and renames only the references it can positively recognise
277+
- `editor_agent_context.py` assembles the compact interview context a turn is given, fencing untrusted reference material
278+
- `editor_agent.py` runs the bounded agent loop and the explicit final validation pass
175279

176280
## Testing
177281

0 commit comments

Comments
 (0)