The harness generalizes to any assistant that answers questions from published policy documents: benefits eligibility, licensing rules, housing programs. This page lists what changes and what carries over unchanged.
Two ways to use it, in increasing order of commitment:
- Read this page and hand-copy. Fine for a one-off experiment.
- Run
make template TARGET=../new-domain-assistant. Generates a starter skeleton in an empty directory, built fromtemplate/MANIFEST.yaml— the same claim this page makes, but as data a script and a test both check against the actual repo tree, so it can't silently drift the way prose can (seedocs/ROADMAP.mdP3-5, "Generalize the harness"). It copies the domain-agnostic modules verbatim, flags the handful that need a domain-specific edit (each with a marker to grep for), writes a stubbedsrc/assistant/domain.py, and drops aGETTING_STARTED.mdin the new tree pointing back at the checklist below. It does not touch corpus content, eval case content, or prompts — see items 1-4.
- The runner, deterministic check framework, judge plumbing, report
generator, regression gate, and CI wiring (
evals/). - The guard architecture: input checks before retrieval, output checks that
block and substitute rather than merely log (
src/assistant/guards.py). - The corpus discipline: a manifest with URLs, fetch dates, hashes, and license notes; committed snapshots; "as of" disclosure in every answer.
-
The domain profile. The transit-specific knobs are isolated in one object,
DomainProfileinsrc/assistant/domain.py: the scopes (agencies), the aliases users type for them, the adjacent topics to redirect, and the fallback contact. A new domain writes a new profile and registers it; the retrieval, guard, and config code reads the active profile unchanged. Thetest_a_new_domain_is_just_a_new_profilecase shows a housing-voucher profile reusing the whole pipeline. The active profile is late-bound:retrieve,guards, andconfigread it at call time, not import time, soFPA_DOMAINmay be set any time before a request is handled and the switch takes effect immediately (default_retriever()'s cache is keyed on the profile, so it switches too). What is deliberately not in the profile, because it is cross-domain safety rather than domain content, is the PII, injection, and eligibility-determination detectors inguards.py; those bind in every domain. -
Corpus manifest. Point
corpus/manifest.yamlat your documents. Check robots.txt and content signals; record your reading of them in the manifest, not just in your head. Re-runmake fetch && make ingest. -
The will-not-do list. Decide what your assistant must never do (for a benefits assistant: determine eligibility, advise on appeals, handle case numbers). Encode each rule three times: in the system prompt, in
guards.py, and as eval cases. The repetition is the design. -
Forbidden-language patterns. The determination-language detector is a phrase list with hedge awareness. Rewrite the phrases for your domain ("you are approved", "your claim will succeed") in every language you serve.
-
Eval cases. Author cases from your actual documents, during or right after ingest, while the boundary conditions are in front of you. The pattern to copy from
evals/suites/:- groundedness: facts a reader can verify against a named passage;
- refusal: PII, injection, determination-seeking, out-of-corpus topics;
- edge cases: the boundaries your documents actually publish (ages, income cutoffs, document alternatives, what stacks with what);
- multilingual: mirrored cases so parity is a number, not a hope;
- freshness: expired programs and "as of" behavior.
-
Agency/entity aliases. Set on the
DomainProfile(item 0): whatever your users call the programs or offices in your corpus.
Commit your first bad scoreboard. The improvement curve across commits is evidence your evals connect to reality.
Change prompts only against failing cases. Every prompt edit should name the case IDs it is trying to fix, and the regression gate catches what it broke.