Skip to main content

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 versionThe shape of it
Why not fine-tuningWhy RAG wins here
What the AI is allowed to readFour knowledge sources
How privacy is handledThe anonymiser
What happens on one requestA request, step by step
What can't go wrongEnforced in code · When things fail
What we still needCorpus 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.

GateFine-tuningRetrieval
Traceable sources (AI-6)Weights have no citationsEvery answer carries a Citation
GDPR deletion (AI-10)Data is baked inDelete the row
Needs a datasetYes, hundreds of examplesNo

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-7 mandates 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

#DecisionBecause
D1Past cases influence the answer, not just statisticsAI-4 — numbers alone can't tell a handler what to watch for. Settles Q1.
D2Anonymise at ingestionMakes cross-company precedent defensible. Settles Q2.
D3Confidence = rubric in codeLocal models are badly calibrated; a wrong "95%" is worse than no number.
D4Local models now, provider seam keptGDPR-simplest for the PoC; get_chat_model(purpose) makes a swap one file.
D5RAG onlyThe three gates above.
D6First slice = Abertura draftingReuses 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 knownemployee_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:

BeforeAfter
Maria Santos, FinanceiraTRABALHADOR_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.

RuleWhereEnforces
Base fields complete before draftingcheck_completeness nodeAI-2, AI-3
Every claim carries a citationvalidate nodeAI-6
Revision loop is cappedroute_after_validatecan't spin forever
Confidence is measured, not assertedrubric.pyAI-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.

FailureWhat happens
Retrieval returns nothingFalls through the existing cap → validation_failed
pgvector unreachableFail at check_completeness. Don't start drafting.
Base fields incompleteDeterministic END with the missing list. No model call.
Anonymiser hits an unknown stringSkip 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-alpinepgvector/pgvector:pg16.

ModuleDoes
infrastructure/embeddings.pyget_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.pypgvector client — the only module that knows SQL
knowledge/anonymise.pyThe gate above
knowledge/ingest/legislation.py · precedent.pyOne command per corpus
knowledge/retrieve.pyReturns Citation, never bare strings
knowledge/rubric.pyPure function, no model call
agents/tools/knowledge.pysearch_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

WhatHowWhy that way
anonymise.pyProperty-based (hypothesis): no known identity value survives in the output, for any caseIts bugs are regulatory, so generated inputs beat a handful of examples — same reasoning as FsCheck on the backend
check_completeness, rubricPlain unit testsPure functions over state
Retrieval, ingestionIntegration, real pgvectorAssert an article boundary survives chunking
The graphEnd-to-end, fake modelA draft with no citations must reach validation_failed, not assemble

Corpus status

The Código do Trabalho is already in this repoa 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 hereWhere 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 alertingNot committed to by the client
Fixed vs per-client workflow (Q3)Open