Highlights#
- Bootstrapped and architected the five-service platform (FastAPI, LangGraph, React 19, PostgreSQL/pgvector, nginx) behind procedimentipa.gov.it, the Italian Department of Public Administration’s live public catalog of ~700 procedures.
- Designed LangGraph research and generation workflows modeled on how legal experts actually work; one expert can now discover tens of candidate procedures from legislation and draft their reports in a single day.
- Replaced embedding-based RAG with an expert-curated legislative tree (law → article → paragraph) after retrieval proved untrustworthy on legal text, so every AI draft is grounded on an exact, auditable legal basis.
- Built the public hybrid-search microservice solo: pgvector semantic, full-text, and trigram retrieval fused with Reciprocal Rank Fusion, degrading gracefully to keyword-only search when embeddings fail.
- Built a legislative change-detection and notification system on the same tree: a nightly two-phase job diffs tracked laws against Normattiva node by node, reports changes down to a single paragraph, maps them to the procedures that cite them, and emails subscribers.
Architecture / How it works#
flowchart TD
experts(["Legal experts"]) --> spa
citizens(["Citizens"]) --> drupal
subgraph backoffice ["Backoffice platform — this repo"]
spa["React 19 SPA
9 streaming AI assistants"]
nginx["nginx proxy
JWT auth gate for AI streams"]
api["FastAPI backend
procedure lifecycle state machine"]
ai["LangGraph AI service
11 graphs, ~48 nodes, Gemini"]
search["Search microservice
hybrid RRF retrieval"]
end
norm["Normattiva / EUR-Lex"] -->|"Akoma Ntoso ingestion"| api
spa --> nginx
nginx --> api
nginx -->|"token streaming"| ai
api -->|"expert-picked legislative nodes"| ai
api -->|"publish approved only"| drupal["Drupal portal
procedimentipa.gov.it"]
drupal -->|"server-side, API key"| search
db[("PostgreSQL + pgvector
app DB · LangGraph checkpoints · search index")]
api --- db
ai --- db
search --- db
Portfolio Case Study#
Problem. Italian administrative procedures (authorizations, permits, obligations) exist only implicitly, scattered across statutes on Normattiva and EUR-Lex. The Department of Public Administration (DFP) wanted a single structured public catalog. Extracting a procedure from law is expert work: find the governing norms, derive phases, deadlines and participants, and keep every legal reference straight. Done by hand, it is slow, and it doesn’t scale to a national catalog.
Approach. We watched closely how a legal expert finds and writes a procedure, and replicated that workflow in LangGraph. It took a lot of iteration and a lot of worked examples from the experts’ side before a general common pattern emerged; that pattern became the platform’s workflow suite: legislative research, procedure discovery (map-reduce over segmented legislation), structured extraction and generation, and flowchart generation with a bounded self-critique loop. Experts stay in the loop at every step, skimming and refining what the workflows surface.
Two decisions define the system. First, grounding: the project started with Legal-BERT research and a ChromaDB RAG store, but retrieval wasn’t trustworthy on legal text; it surfaced wrong or partial articles, which is disqualifying when the output cites law. I replaced it with a hierarchical legislative tree ingested from Normattiva and EUR-Lex, plus a tree-selector UI where the expert hand-picks the exact legal basis, down to a single paragraph. The AI composes its context from exactly those nodes. The cost was real (a full ingestion pipeline, a materialized-path tree model, and a custom selector component), but every generated draft now carries provable provenance. Second, control: instead of complete auto-piloted AI agents, we used strictly controlled, state-machine-like AI workflows, because the system should behave in a consistent way. The same philosophy runs through the backend, where a procedure lifecycle state machine records 15 typed events per procedure, each attributed to a person and flagged AI-generated or manual.
The legislative tree paid off a second time: because every tracked law lives as structured nodes with stable identifiers — the same tree built for Normattiva and EUR-Lex ingestion — the platform can watch the law itself for change. I built a two-phase detection job that runs nightly: a cheap first pass asks Normattiva’s OpenData API for each tracked law’s consolidation date, about a second per law, and only the laws that actually moved get re-downloaded, re-parsed, and diffed node by node against the archived copy. That is what lets the system report “comma 3 of article 7 changed” rather than just “this law changed.” Every detected change becomes a reviewable issue listing the procedures whose citations point at the modified nodes, subscribers are emailed automatically when a run finds changes or fails, and reviewers resolve issues through explicit actions — update the archived law, confirm a procedure has been re-checked, or mark the change as not relevant. Detection never mutates the library on its own: updating an archived law is a separate, human-initiated action, and a procedure impact can’t be signed off until the law it cites has been updated first.
One data structure, two jobs. The expert-curated legislative tree grounds every AI draft on hand-picked nodes — the context is exactly what the expert selected — and the same tree is diffed nightly against Normattiva, reporting changes down to a single comma and the procedures that cite it. The discarded ChromaDB store marks the road not taken.
I started the repository in July 2025 and designed the service architecture, which is still the production topology a year later. On the five-person team I was the main architect of the backend and the primary author of the frontend, the AI service, and the search engine.
What was hard. Encoding the experts’ implicit process was the long grind: many iterations, many examples, and workflow revisions until outputs matched what a practitioner would write. The retrieval pivot meant discarding a working vector store in favor of months of ingestion and tree work, and that ingestion was a project inside the project. Normattiva serves law as Akoma Ntoso XML, which had to be parsed into legislation, article and attachment models with vigenza (time-validity) tracking before it could become a navigable tree; bringing EUR-Lex into the same hierarchy took a comparable effort, with its own fetcher, parser and tree builder for EU sources. On the frontend, long-running graph executions had to survive page refreshes and disconnects, which led to a session-resumption design (persisted run IDs, stream rejoin, server-side continuation) now shared by all nine AI assistants. Prompt quality was its own project: versioned prompts with fallbacks, then a measured optimization loop that lifted the procedure-finder’s golden-set score from 0.51 to 0.87.
Outcome. The public portal launched in July 2026, and the catalog — around 700 procedures today — is live for every Italian citizen, with the experts filling it through this backoffice. A single law expert can discover tens of candidate procedures and draft their reports in a day, where each report used to be a slow manual research task. The platform has been in continuous delivery since July 2025: a 7-container stack, tested and typed throughout, with a CI pipeline that deploys each service independently — and a measured prompt-optimization loop that lifted the procedure-finder’s golden-set score from 0.51 to 0.87 (see LMnetwork below).
Non-goals. The AI never publishes anything; every output passes expert approval, enforced by the lifecycle state machine. There are no autonomous agents by design. The platform serves one organization and is deliberately specialized to Italian and EU law rather than generic document processing.
Companion projects#
The platform spawned its own tooling ecosystem: five side projects I designed and built in separate repositories to close gaps the main repo couldn’t. Each has its own case study.
- md-italia: a Markdown-to-portal renderer plus a CodeMirror authoring editor. Experts write one Markdown file, and the same pure render function drives both the live preview and the portal-ready accessible page, removing the manual re-entry step where an editor retyped every report. Hundreds of real procedures have been rendered through it; deployment to the live portal is the step in front of it.
- Sportello (pc-chatbot): a self-hosted AI chat widget any site adds with one
<script>tag, an iframe-isolated React app streaming Gemini replies over SSE. Its procedure-search tool calls this platform’s search engine, giving the catalog a conversational front door. Designed and built solo in one week. - LMnetwork: a prompt optimizer that treats prompts like weights. It runs a LangGraph agent over a golden dataset, scores every output with an LLM judge, and evolves the prompt text through per-iteration strategy tournaments; it produced the 0.51 → 0.87 procedure-finder improvement cited above.
- procedure-wiki: compiles the catalog into an Obsidian-compatible knowledge graph, one page per procedure and one per legal article, stitched together by wikilinks, so an LLM agent (or a person) answers questions by following links instead of re-running retrieval every time.
- Procedure Agent (Scriba): a human-gated AI editing system for the official record. The agent drafts legislation-grounded edits but is structurally incapable of touching the live catalog: every turn surfaces as a reviewable diff, and only an officer’s approval merges it. Pre-alpha; the backend loop works end-to-end.
Stack#
Python 3.13 · FastAPI · SQLModel · PostgreSQL + pgvector · LangGraph · Gemini · React 19 · TypeScript · TanStack Query · Zustand · Docker · nginx · GitLab CI
