Skip to content

Repository files navigation

Al Dente — Company Brain

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 graded POST /ask lives 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.app Backend 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 in BRIEF.md / AGENTS.md / API.md; operational run book in RUN_REPORT.md.


The core idea — compiled vs. live knowledge

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.


Architecture

  ┌─ 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.


Screenshots

4-slide HTML deck generated on demand — artifact rendered inline in a sandboxed <iframe>, sourced from live CRM + KB data, downloadable in one click.

HTML deck generated for a customer visit

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.

Opportunities answer with knowledge graph


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).


Endpoints

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).


Repository layout

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

The agent loop

agent/loop.py:run_agent(question, cb, history=None):

  1. Builds [system] + history + user. history=None is the frozen /ask path; /chat passes prior turns so the agent resolves references ("its supplier", "that customer").
  2. Up to MAX_STEPS=5 turns. The first turn forces a tool call (tool_choice="required", with a graceful fallback to auto) so the agent never answers company-data questions from memory. Subsequent turns are auto.
  3. 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.
  4. When the model stops calling tools (or after an artifact short-circuit), the answer is finalized: sources come from the endpoints the client actually hit plus the DOC ids the KB returned; verticale is 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).


Tools (16, exposed to the model)

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 own id for 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.


Knowledge wiki / RAG

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).


Knowledge graph + chat UI (frontend/)

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, the verticale tag, source chips, a copy button, an artifact download bar, and suggested follow-ups; full-HTML deck answers render in a sandboxed <iframe>.
  • 3D knowledge graph3d-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.

Artifacts

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.


Security

  • 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> without allow-scripts (HTML decks); strict CSP; all CDN scripts pinned with Subresource Integrity.
  • Path traversal — uuid filenames + extension allow-list + containment; /files serves Content-Disposition: attachment + nosniff.
  • Secrets — only from env, never logged, never in responses, .env git-ignored.
  • Denial-of-wallet — bounded loop, max_tokens, question-length cap, per- request timeout. Dependencies audited clean (June 2026).

Run locally

You run two apps: the backend (API + agent) and the frontend (Next.js UI).

1. Backend — http://localhost:8000

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 8000

Or with Docker (production parity):

cd backend
docker compose up --build       # http://localhost:8000

Environment 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)

2. Frontend — http://localhost:3000

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:3000

BACKEND_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.

3. Quick check

# 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?"}]}'

Testing & diagnostics

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.


Deploy (Railway)

Deployed as a single service via Railpack (backend/railway.json); a Dockerfile

  • docker-compose.yml give local production parity. Full step-by-step (env vars, domain, smoke test) is in RUN_REPORT.md §5.
cd backend
railway up --service company-brain-aldente

Tech stack

Backend: 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.

About

AI "company brain" for a pasta maker (Al Dente S.r.l.): a FastAPI bounded tool-calling agent over live CRM/ERP/call APIs + a compiled RAG wiki, with a Next.js/React UI featuring a 3D knowledge graph. Frozen POST /ask contract, honest abstention over hallucination.

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages