AI knowledge architecture
In three lines. The AI drafts each phase's formal documents and gives a reasoned Sim/Não — grounded on retrieved law and anonymised precedent, never on fine-tuned weights. Every legal-safety rule is enforced in code, not in a prompt. A human always signs off.
| If you want… | Go to |
|---|---|
| The one-picture version | The shape of it |
| Why not fine-tuning | Why RAG wins here |
| What the AI is allowed to read | Four knowledge sources |
| How privacy is handled | The anonymiser |
| What happens on one request | A request, step by step |
| What can't go wrong | Enforced in code · When things fail |
| What we still need | Corpus status |
Serves the AI requirements. Scope is the knowledge layer plus the first
slice — Abertura drafting. The Sim/Não verdict reuses all of this and gets its own page.
The shape of it
Two halves. Ingestion is offline and happens once per corpus. Everything on the right runs per request, and nothing reaches the reviewer without citations attached.
Why RAG wins here
Fine-tuning has to clear three gates. It fails all three.
| Gate | Fine-tuning | Retrieval |
|---|---|---|
Traceable sources (AI-6) | Weights have no citations | Every answer carries a Citation |
GDPR deletion (AI-10) | Data is baked in | Delete the row |
| Needs a dataset | Yes, hundreds of examples | No |
The real point. Analysis quality is not a fine-tuning problem. It is a retrieval-quality problem, a validator problem, and a human-gate problem — and
AI-7mandates the human gate regardless.
Deferred, with an entry condition: LoRA fine-tuning to lock the house document format, once roughly 300 approved deliverables exist. It would improve form, never facts, and still could not cite. Not planned work.
Decisions
| # | Decision | Because |
|---|---|---|
| D1 | Past cases influence the answer, not just statistics | AI-4 — numbers alone can't tell a handler what to watch for. Settles Q1. |
| D2 | Anonymise at ingestion | Makes cross-company precedent defensible. Settles Q2. |
| D3 | Confidence = rubric in code | Local models are badly calibrated; a wrong "95%" is worse than no number. |
| D4 | Local models now, provider seam kept | GDPR-simplest for the PoC; get_chat_model(purpose) makes a swap one file. |
| D5 | RAG only | The three gates above. |
| D6 | First slice = Abertura drafting | Reuses the graph that exists, on the one phase already fully documented. |
Four knowledge sources
Deliberately separate: they differ in privacy class and in how often they change.
The right-hand two are worth stating explicitly, because treating them as retrieval is the
obvious wrong turn: there are only ~25 templates and they're selected by document_type, so
embedding them buys nothing; and the live case must be read exactly, not approximately.
The anonymiser
Identities in a case are already known — employee_name, employee_email,
employee_department, intervenientes are structured columns. So redaction is exact
substitution of known values, not a guess.
What it looks like in practice:
| Before | After |
|---|---|
Maria Santos, Financeira | TRABALHADOR_A, DEP_1 |
[email protected] | [EMAIL] |
2026-03-14 (facts), 2026-03-20 (knowledge) | day 0, day +6 |
| "faltou 4 dias sem aviso" | unchanged — this is the part worth learning from |
Why not a model-based NER pass? It would be probabilistic. Probabilistic is the wrong risk profile for the one component standing between this product and a GDPR finding.
Two boundaries that matter: a case the anonymiser can't fully account for is skipped, not indexed-and-hopefully-clean; and anonymisation applies only to the precedent index. A live draft uses real names — it's a legal instrument about a named person.
A request, step by step
Note step 2: an incomplete case never reaches a model. No cost, no hallucinated facts, no prompt that can be talked out of it.
Enforced in code
route_after_validate already decides on state and caps its own loop rather than trusting model
judgement. That instinct carries the whole safety layer.
| Rule | Where | Enforces |
|---|---|---|
| Base fields complete before drafting | check_completeness node | AI-2, AI-3 |
| Every claim carries a citation | validate node | AI-6 |
| Revision loop is capped | route_after_validate | can't spin forever |
| Confidence is measured, not asserted | rubric.py | AI-5 |
The rubric takes four measurable inputs and no model opinion:
The graph
Only two things are new: check_completeness, and the citation rule inside validate.
When things fail
The useful part is how little new machinery this needs.
research is deliberately best-effort — losing it degrades a run. Retrieval failure is a
different class and must not inherit that behaviour. If retrieval returns nothing, the draft has
no citations → validate rejects → revise retries → the existing cap routes to
validation_failed. So an outage surfaces as an honest "couldn't produce a defensible draft",
never as a confident document citing nothing.
| Failure | What happens |
|---|---|
| Retrieval returns nothing | Falls through the existing cap → validation_failed |
| pgvector unreachable | Fail at check_completeness. Don't start drafting. |
| Base fields incomplete | Deterministic END with the missing list. No model call. |
| Anonymiser hits an unknown string | Skip that case. Log the case id only, never the string. |
Logging follows research: run id and node name only — never prompts, findings or case content.
Storage and new modules
One database, mylegalteam_ai, on the existing Postgres. The backend owns its schema via EF Core
migrations; a separate database keeps ingestion tables clear of those migrations. Infra cost is one
line: postgres:16-alpine → pgvector/pgvector:pg16.
| Module | Does |
|---|---|
infrastructure/embeddings.py | get_embedding_model(), mirroring llm.py. Local bge-m3 — multilingual, so Portuguese embeds properly. nomic-embed-text is English-leaning and would degrade retrieval quietly. |
infrastructure/vectorstore.py | pgvector client — the only module that knows SQL |
knowledge/anonymise.py | The gate above |
knowledge/ingest/legislation.py · precedent.py | One command per corpus |
knowledge/retrieve.py | Returns Citation, never bare strings |
knowledge/rubric.py | Pure function, no model call |
agents/tools/knowledge.py | search_legislation · search_precedent · get_case_facts · get_case_documents |
Citation(source, ref, snippet, score) is a first-class type; DraftState gains
citations: list[Citation].
Testing
| What | How | Why that way |
|---|---|---|
anonymise.py | Property-based (hypothesis): no known identity value survives in the output, for any case | Its bugs are regulatory, so generated inputs beat a handful of examples — same reasoning as FsCheck on the backend |
check_completeness, rubric | Plain unit tests | Pure functions over state |
| Retrieval, ingestion | Integration, real pgvector | Assert an article boundary survives chunking |
| The graph | End-to-end, fake model | A draft with no citations must reach validation_failed, not assemble |
Corpus status
The Código do Trabalho is already in this repo — a dated, article-by-article snapshot with a 16-rule register, structured the way the ingester wants, licensing already settled there. It needs no acquisition work; it's the first ingestion source.
The rest: LTFP for public-sector cases, PSP deontology for that specialisation, CITE for the pregnancy/parental-leave parecer path, and precedent — which is blocked until someone confirms what past-case data exists and in what format.
Authority, not convenience. That register warns the Diário da República consolidated text is the legal authority and the in-repo page is a developer reference. The same caveat rides along with anything the AI cites from it — which is exactly why every claim carries a citation the reviewer can follow back.
Out of scope
| Not here | Where it goes |
|---|---|
Sim/Não verdict rendering + role gating (AI-5, AI-8) | Own page, reuses this layer |
Reviewer like/dislike loop (AI-5) | Unspecified by the client Q&A |
| Deadline orchestration and alerting | Not committed to by the client |
Fixed vs per-client workflow (Q3) | Open |