A FastAPI backend that answers business questions about Al Dente S.r.l.
(a dry-pasta maker selling to GDO supermarkets, distributors and HoReCa). It runs
a bounded tool-calling agent over the company's live CRM/ERP/call-log APIs and a
compiled knowledge wiki, paired with a Next.js + React frontend (frontend/)
that ships an enterprise multi-turn chat UI with a 3D knowledge graph.
Two apps:
backend/is the API + agent (the gradedPOST /asklives here);frontend/is the Next.js UI, which recreates the endpoint surface as route handlers that proxy to the backend. Run both locally (see Run locally).
Live UI:
https://company-brain-frontend-production-f513.up.railway.appBackend API (graded/ask):https://company-brain-aldente-production-1f3f.up.railway.app(the backend's/redirects to the UI). Both run as services in one Railway project. Built for the Coding Agent Hackathon (Yellow Tech, Milano). Original challenge spec inBRIEF.md/AGENTS.md/API.md; operational run book inRUN_REPORT.md.
Two kinds of knowledge are treated differently:
- Compiled (immutable). The 35 knowledge-base documents in
backend/data/kb/(product specs, allergens, shelf life, returns/quality policies, the 2026 price list) never change. They are compiled once, offline into an interlinked Obsidian-style wiki (backend/data/wiki/, ~36 pages). At query time the agent reads that wiki — zero API calls, near-zero latency, perfect grounding, and the documentation traps (a superseded allergen note, a legacy shelf life, an unapproved defect) are already resolved. - Live (volatile). Customers, opportunities, orders, invoices, inventory, production lots, suppliers, shipments and calls are fetched on demand through the Al Dente mock API with exact server-side filters and full pagination. Every number, count, total and supply-chain hop is computed in Python inside the tools — never guessed by the model.
This split drives the three things that matter: completeness (the model never invents a figure it can compute), honesty (a fact either comes from a source or the agent abstains), and efficiency (the static half costs nothing at query time). The same entity model also powers the knowledge-graph UI.
┌─ frontend/ (Next.js) ─┐ ┌──────────────────── backend/ (FastAPI service) ──────────────────────┐
│ 3D graph + chat UI │ │ │
│ /api/chat ─────────► │ ─────► │ /chat ─► agent/loop.py (bounded tool-calling loop, mistral-small) │
│ /api/graph ─────────► │ ─────► │ /graph ─► graph/build_graph.py (wiki backbone + live overlay) │
│ /files/* ─────────► │ ─────► │ │ │ │
└───────────────────────┘ │ │ ├─► agent/tools.py ─► clients/aldente.py ─► mock API │
│ │ └─► kb/retrieval.py ─► data/wiki/ (BM25, 0 API cost) │
Evaluator ─ POST /ask ─────────► │ main.py ─► agent/honesty.py ─► verticale + sources + grounding gate │
│ └────────────────────────────────► AskResponse (HTTP 200) │
└───────────────────────────────────────────────────────────────────────┘
The frontend was migrated out of the old backend/static/index.html single file
into a componentised Next.js + React app. Its route handlers (/api/chat,
/api/graph, /api/ask) proxy to the backend, and /files/* is rewritten to the
backend so artifact downloads work unchanged. Proxy target is set by BACKEND_URL.
4-slide HTML deck generated on demand — artifact rendered inline in a sandboxed <iframe>, sourced from live CRM + KB data, downloadable in one click.
Opportunities table — multi-source answer (CRM + calls + KB) rendered as a markdown table; the knowledge graph on the right highlights the evidence subgraph in real time.
Request flow for /ask: classify nothing up front — the loop forces a first
tool call (tool_choice="required") so the agent always grounds in a real source,
runs up to MAX_STEPS=5 bounded steps (parallel tool calls per step), then an
honesty/grounding gate assembles {answer, sources, verticale, artifact_url} and
returns HTTP 200 always (any failure degrades to an honest abstention, never a
5xx), well under the 30 s budget (~2–3 s typical).
| Method · Path | Purpose |
|---|---|
POST /ask |
Frozen contract (the graded endpoint). Body {"question": str} → {"answer", "sources": [...], "verticale": "crm|erp|calls|kb", "artifact_url": str|null}. Stateless, single synchronous JSON, 200 on every path. |
POST /chat |
Multi-turn sibling for the UI. Body {"messages": [{"role":"user|assistant","content":str}, ...]} → same response shape, with conversation context. Additive — does not touch /ask. |
GET /graph |
Knowledge-graph {nodes, edges} for the UI (wiki backbone + bounded live overlay, in-memory TTL cache). |
GET /health |
{"status":"ok"} (no auth, instant — used by the Railway healthcheck). |
GET /files/* |
Generated artifacts (docx/pptx/pdf/xlsx), served with Content-Disposition: attachment. |
The UI is no longer served by the backend — it now lives in the separate Next.js
frontend/ app, which exposes POST /api/chat, GET /api/graph, POST /api/ask
(proxying to the backend) and rewrites /files/* to it.
/ask and /chat return the same four-field shape. verticale is the dominant
source bucket; sources are the real endpoints + DOC ids the answer rests on
(derived from what the client actually hit, not the model's say-so).
backend/
├── main.py # FastAPI app: /ask (frozen), /chat, /graph, /health, /files; security headers + CSP
├── config.py # Settings (env), constants (MAX_STEPS, timeouts, caps), ClientBundle DI factory
├── agent/
│ ├── loop.py # run_agent(question, cb, history=None): bounded tool-calling loop, forced 1st tool call, grounding gate
│ ├── tools.py # 16 tool schemas + dispatch (thin endpoint tools + thick skill tools); filter validation
│ ├── prompts.py # short system prompt: grounding-first + honesty + routing hints
│ └── honesty.py # entity resolution, abstention, verticale derivation
├── clients/
│ ├── aldente.py # async Al Dente API client: pagination (limit=200), per-request cache, source tracking, SSRF-safe
│ └── llm.py # OpenAI-compatible client + retry/backoff + tool_choice fallback + <think> stripping
├── kb/
│ ├── wiki.py # WikiStore: lazy-load the compiled vault, parse frontmatter, expose pages
│ └── retrieval.py # kb_search: index-first BM25 over whole pages (zero API/LLM), returns DOC ids
├── artifacts/ # base.py (secure dispatch) + html_deck / pptx / docx / pdf / xlsx generators
├── graph/build_graph.py # /graph payload: wiki frontmatter + bounded live overlay, TTL cache
├── data/
│ ├── kb/ # 35 immutable source documents
│ └── wiki/ # COMPILED vault (committed): CLAUDE.md schema, index.md, log.md, products/ policies/ pricing/ suppliers/ materials/
├── scripts/
│ ├── build_wiki.py # offline llm-wiki compiler (reproduce the vault; never run on Railway)
│ ├── selftest.py # run the 12 SAMPLE_QUESTIONS against a deployed /ask, scored
│ ├── record_fixtures.py # record the live datasets ONCE → replay_fixtures.json (for zero-cost testing)
│ ├── benchmark_models.py # benchmark candidate LLMs on replay fixtures (0 metered calls)
│ ├── diagnose.py # broad accuracy battery on replay fixtures (ground truth from fixtures, 0 metered)
│ └── sample_questions.json
├── static/files/ # generated binary artifacts, served at /files/* (index.html removed — UI moved to frontend/)
├── tests/ # 37 pytest cases (mocked API + fake LLM) — no live keys needed
├── Dockerfile, docker-compose.yml, railway.json, pyproject.toml, .env.example
frontend/ # Next.js + React UI (migrated from the old static/index.html)
├── app/
│ ├── layout.tsx # root layout (fonts, ambient background, metadata)
│ ├── page.tsx # main client app: chat send flow, layout toggles, graph wiring
│ ├── globals.css # full ported stylesheet
│ └── api/ # route handlers proxying the backend: chat/, graph/, ask/
├── components/ # Sidebar, Thread, MessageUser/Assistant, Composer, Graph (3d-force-graph), DownloadBar, Toast, icons
├── lib/ # store (localStorage chats), markdown, download, followups, constants, backend proxy
├── next.config.mjs # /files/* rewrite to BACKEND_URL
└── package.json, tsconfig.json, .env.example
agent/loop.py:run_agent(question, cb, history=None):
- Builds
[system] + history + user.history=Noneis the frozen/askpath;/chatpasses prior turns so the agent resolves references ("its supplier", "that customer"). - Up to
MAX_STEPS=5turns. The first turn forces a tool call (tool_choice="required", with a graceful fallback toauto) so the agent never answers company-data questions from memory. Subsequent turns areauto. - Tool calls within one step run concurrently (
asyncio.gather); LLM turns are sequential (run in a worker thread so the event loop stays free). Tool outputs are trimmed before re-entering context. - When the model stops calling tools (or after an artifact short-circuit), the
answer is finalized:
sourcescome from the endpoints the client actually hit plus the DOC ids the KB returned;verticaleis the dominant bucket; a grounding backstop abstains if a live-data answer rests on no live source; entity ids are normalized (unicode hyphens → ASCII) for exact-match grading.
Model: Regolo mistral-small-4-119b — a fast, non-reasoning, strong
tool-caller, selected empirically by scripts/benchmark_models.py (12/12 on
the samples at ~1.4 s avg, vs reasoning models that ran 4–8× slower). The model is
env-configurable (MODEL); the loop also handles reasoning-style models
(<think> stripping, reasoning_content).
Thin endpoint tools (auto-paginate, exact-match filters with value validation
- case normalization, source-tracked, project rows down to what matters):
kb_search,crm_customers,crm_orders,crm_invoices,erp_inventory,erp_suppliers,erp_production_orders,erp_shipments,calls_search,call_details,call_transcript. Every list tool also accepts the entity's ownidfor a direct by-id lookup (order/invoice/lot/supplier/call/customer).
Thick "skill" tools (do the arithmetic / multi-hop joins in Python and return finished, sourced results):
| Tool | What it computes |
|---|---|
opportunity_summary |
count + total value by stage, optionally grouped by customer channel |
defect_complaint_count |
exact count of complaints about a defect across the entire call log (paginates fully) |
bom_supply_chain |
finished SKU → BOM → raw materials → supplier → stock; for a RAW-* sku resolves its supplier directly |
returns_eligibility |
applies the returns policy (DOC-011: 15-day window, lot + photo, covered defects) to a customer's latest complaint |
make_artifact |
renders an inline HTML deck or a downloadable docx/pptx/pdf/xlsx |
Filter values are validated against the allowed sets and normalized to canonical
case before hitting the API — so a model that emits an invalid value (e.g.
type:"object") or wrong case (gdo) doesn't silently zero out the query.
kb_search is index-first BM25 over whole compiled pages — no embeddings, no
vector DB, no model in the retrieval path, zero API cost. Whole-page retrieval
beats chunking here (the docs are small and near-identical, so it keeps shelf
life + allergens + price together). The DOC ids backing the selected pages flow
straight into the response sources. The vault is compiled offline by
scripts/build_wiki.py (the committed pages are the runtime source of truth).
A Next.js + React three-pane app (editorial ivory palette, Geist type, GSAP
motion, honors prefers-reduced-motion):
- Sidebar — saved chats (client-side
localStorage): new / select / search / rename / delete, relative timestamps, persists across reloads. - Conversation — multi-turn thread (
POST /chat): user bubbles + assistant cards with markdown tables, theverticaletag, source chips, a copy button, an artifact download bar, and suggested follow-ups; full-HTML deck answers render in a sandboxed<iframe>. - 3D knowledge graph —
3d-force-graph(Three.js/WebGL): customers · suppliers · products · raw materials, colored by type, glowing nodes with flow particles. After each answer the source subgraph lights up (the evidence); hovering a node highlights its neighbors, clicking pins + pre-fills a question.
make_artifact produces either an inline HTML deck (returned in answer,
artifact_url = null) or a downloadable file (pptx/docx/pdf/xlsx via
python-pptx / python-docx / fpdf2 / openpyxl) saved to static/files/<uuid>.<ext>
and returned as an absolute artifact_url. Filenames are uuid4 + an extension
allow-list with a containment check (no path traversal); xlsx cells are guarded
against formula injection.
- Grounding / no hallucination — forced first tool call + a grounding gate; read-only API tools; the model never sees the Bearer token.
- SSRF — the agent never chooses a URL (tools take ids/params; base URL is
fixed);
httpx(follow_redirects=False). - XSS — model output is rendered via
DOMPurify.sanitize(markdown) and a sandboxed<iframe>withoutallow-scripts(HTML decks); strict CSP; all CDN scripts pinned with Subresource Integrity. - Path traversal — uuid filenames + extension allow-list + containment;
/filesservesContent-Disposition: attachment+nosniff. - Secrets — only from env, never logged, never in responses,
.envgit-ignored. - Denial-of-wallet — bounded loop,
max_tokens, question-length cap, per- request timeout. Dependencies audited clean (June 2026).
You run two apps: the backend (API + agent) and the frontend (Next.js UI).
cd backend
cp .env.example .env # then fill in the keys below
uv sync # install dependencies
uv run uvicorn main:app --host 0.0.0.0 --port 8000Or with Docker (production parity):
cd backend
docker compose up --build # http://localhost:8000Environment variables (backend/.env):
| Var | Value |
|---|---|
LLM_BASE_URL |
https://api.regolo.ai/v1 |
LLM_API_KEY |
your Regolo key |
MODEL |
mistral-small-4-119b (any Regolo tool-calling model works) |
MOCK_API_BASE_URL |
https://aldente.yellowtest.it |
MOCK_API_TOKEN |
your personal dashboard token (efficiency is metered on it) |
PUBLIC_BASE_URL |
this service's public URL (for absolute artifact_url) |
In a second terminal (keep the backend running):
cd frontend
cp .env.example .env # set BACKEND_URL only if backend isn't on :8000
npm install
npm run dev # open http://localhost:3000BACKEND_URL (default http://localhost:8000) is read at startup — if you run
the backend on another port, set it before npm run dev (e.g.
BACKEND_URL=http://localhost:8123). For a production build: npm run build && npm run start.
# backend (the graded contract)
curl localhost:8000/health
curl -X POST localhost:8000/ask -H 'Content-Type: application/json' \
-d '{"question":"Is SKU PAS-PEN-500 below its minimum stock? Give the on-hand quantity."}'
# frontend proxy (UI calls these)
curl localhost:3000/api/graph
curl -X POST localhost:3000/api/chat -H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"Shelf life of PAS-SPA-500?"}]}'cd backend
uv run pytest -q # 37 offline tests (mocked API + fake LLM, no keys)
uv run python scripts/selftest.py # 12 sample questions vs a running /ask (scored)
uv run python scripts/diagnose.py # broad accuracy battery on replay fixtures (0 metered)
uv run python scripts/benchmark_models.py mistral-small-4-119b gemma4-31b # compare models (0 metered)record_fixtures.py records the live datasets once (one metered pass) so
diagnose.py and benchmark_models.py can replay them with zero metered API
cost — useful to validate model/tool changes without touching the efficiency
budget.
Deployed as a single service via Railpack (backend/railway.json); a Dockerfile
docker-compose.ymlgive local production parity. Full step-by-step (env vars, domain, smoke test) is inRUN_REPORT.md§5.
cd backend
railway up --service company-brain-aldenteBackend: FastAPI · uvicorn · httpx (async) · OpenAI SDK (→ Regolo
mistral-small-4-119b) · rank-bm25 · python-pptx / python-docx / fpdf2 / openpyxl
· slowapi · uv · pytest + respx · Docker · Railway.
Frontend: Next.js 14 (App Router) · React 18 · TypeScript · 3d-force-graph
(Three.js) · GSAP · marked + DOMPurify.
See RUN_REPORT.md for the operational run book and the ~200-word submission
description.

