LNA AI โ Project Architecture
RAG-powered chat over African Digital Library (LNA) book content. Users pick a book, ask questions in English or French, and receive streamed answers grounded in indexed book passages.
System at a glance
flowchart LR
Browser["Browser\n(Next.js UI)"]
Auth["Supabase Auth"]
API["FastAPI\nlna-ai-api"]
DB["Supabase Postgres\npgvector"]
Ollama["Ollama\nLLM + embeddings"]
Profile["Profile API\n(external)"]
Browser -->|login / session| Auth
Browser -->|GET /books\nPOST /answer/stream| API
Browser -->|profile CRUD| Profile
API -->|vector search| DB
API -->|chat + embed| Ollama
| Layer | Technology | Role |
|---|---|---|
| Frontend | Next.js 15, React 19, Tailwind, next-intl | Chat UI, auth, i18n (en/fr) |
| AI backend | FastAPI, uvicorn, pydantic-ai | REST API, RAG, streaming |
| Vector store | Supabase (book_vector_pages + RPC) |
Chunk storage and similarity search |
| LLM | Ollama (default) or DeepSeek | Chat completion |
| Embeddings | Ollama nomic-embed-text:latest |
Always via Ollama (768-dim vectors) |
| Auth | Supabase Auth | User sessions in the frontend |
| Profile API | Not in this repo | User profiles at NEXT_PUBLIC_API_URL |
Repository layout
lna-ai-api/
โโโ lna_ai_agent.py # AI agent, RAG tools, LLM/embed config (canonical)
โโโ backend/
โ โโโ main.py # FastAPI app, HTTP endpoints, streaming
โ โโโ start_server.sh # Local dev: uvicorn on :8001
โ โโโ lna_ai_agent.py # Copy of root agent (keep in sync)
โ โโโ docker/ # Backend container build
โโโ frontend/
โ โโโ src/
โ โ โโโ app/[locale]/ # Next.js App Router (en, fr)
โ โ โโโ components/ # Chat, BookSelector, auth forms, UI
โ โ โโโ lib/ # API client, Supabase clients, utils
โ โ โโโ services/ # Profile API client (external backend)
โ โโโ docker/ # Frontend container build
โโโ docker-compose.yml # Local full-stack (backend + frontend)
โโโ docs/ # Design and deployment docs
โโโ test/ # API and agent tests
Import rule: backend/main.py imports lna_ai_agent from the repo root (sys.path is adjusted). Docker copies lna_ai_agent.py into /app/. Edit the root file; sync backend/lna_ai_agent.py if you maintain both copies.
Ports and environments
Ports differ by how you run the stack. Use one column consistently.
| Service | Docker Compose | Local native dev | Production (deploy scripts) |
|---|---|---|---|
| AI API (this repo) | 8001 |
8001 |
5000 or 8000 (see deploy env) |
| Frontend | 3000 |
3000 (next dev) |
3001 (PM2) behind nginx |
| Ollama | host/LAN IP :11434 |
11434 |
same host or remote |
| Profile API | not included | 5000 (external) |
5000 (external) |
| nginx | not used | optional | 80 โ frontend |
Frontend env URLs must match the AI API port you actually use:
# Docker Compose / typical local dev
NEXT_PUBLIC_AI_API_URL=http://localhost:8001
NEXT_PUBLIC_API_URL=http://localhost:8001 # or external profile API URL
# Production (example from deploy docs)
NEXT_PUBLIC_AI_API_URL=http://localhost:8000
NEXT_PUBLIC_API_URL=http://localhost:5000
Note:
frontend/src/lib/api.tsdefaults to port8000;frontend/src/lib/utils.ts(books) defaults to8001. SetNEXT_PUBLIC_AI_API_URLexplicitly to avoid mismatches.
How to start the service
Option A โ Docker Compose (recommended for local full-stack)
Prerequisites: Docker, Docker Compose, Supabase credentials, Ollama reachable from the backend container.
# From repo root
cp backend/docker/.env.backend.example backend/docker/.env.backend # if example exists
cp frontend/docker/.env.frontend.example frontend/docker/.env.frontend
# Edit env files โ minimum:
# backend: SUPABASE_URL, SUPABASE_KEY, OLLAMA_HOST (use host LAN IP, not localhost, from inside Docker)
# frontend: NEXT_PUBLIC_AI_API_URL=http://localhost:8001
docker compose up --build
| URL | Purpose |
|---|---|
| http://localhost:8001/health | Backend health |
| http://localhost:3000 | Frontend (redirects to /en/...) |
See deployment/local-testing-docker-compose.md for troubleshooting.
Option B โ Native backend only
cd /path/to/lna-ai-api
python3 -m venv venv && source venv/bin/activate
pip install -r backend/docker/requirements.txt # minimal runtime set
# Or: pip install -r requirements.txt # full dev dependencies
# Create .env at repo root (or export vars)
export SUPABASE_URL=...
export SUPABASE_KEY=...
export OLLAMA_HOST=http://localhost:11434/v1 # chat via OpenAI-compatible API
export LNA_LLM_PROVIDER=ollama
cd backend
uvicorn main:app --host 0.0.0.0 --reload --port 8001
# Or: ./start_server.sh
Verify: curl http://localhost:8001/health
Option C โ Native frontend only
Requires a running AI API and Supabase project.
cd frontend
npm install
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://<project>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon-key>
NEXT_PUBLIC_AI_API_URL=http://localhost:8001
NEXT_PUBLIC_APP_URL=http://localhost:3000
npm run dev # http://localhost:3000
Supabase dashboard โ Authentication โ URL Configuration: add http://localhost:3000/* and locale paths (/en/*, /fr/*) as redirect URLs.
Ollama prerequisites
ollama pull qwen3:8b # default chat model
ollama pull nomic-embed-text # embeddings (required for RAG)
Embeddings always use the native Ollama API (no /v1). Chat uses the OpenAI-compatible endpoint; set OLLAMA_HOST=http://host:11434/v1 or let the code normalize it.
Request flow: chat
- User authenticates via Supabase (middleware in
frontend/middleware.ts). BookSelectorloads books fromGET /books(distinct titles frombook_vector_pages).- User sends a message;
ChatcallsPOST /answer/streamwith{ query, source }wheresourceis the bookid(ISBN/URL key). - Backend Phase 1 โ RAG:
- Embed query via Ollama
nomic-embed-text - Supabase RPC
match_book_vector_pages(filtered bymetadata.source) - Expand neighbouring chunks (ยฑ
RAG_CONTEXT_WINDOW, default 1) - Backend Phase 2 โ stream: Build prompt with context; stream tokens via OpenAI-compatible client (Ollama or DeepSeek).
- Frontend parses SSE (
data: {"type":"chunk"|"done"|"error", ...}) and appends to the assistant message.
Alternative paths:
| Endpoint | Behaviour |
|---|---|
POST /answer |
Full pydantic-ai agent loop (tools, retries) โ single JSON response |
POST /stream |
Agent run_stream โ SSE via agent framework |
POST /answer/stream |
Primary โ direct RAG + LLM streaming (fastest UX) |
Backend
backend/main.py
FastAPI application entry point.
- Lifespan: Creates async Supabase client and
PydanticAIDepsat startup. - CORS: Open (
allow_origins=["*"]) for development. - Dynamic book scope:
config.sourceis set per request from thesourcefield (book id).
| Method | Path | Description |
|---|---|---|
| GET | /health |
Liveness check |
| GET | /test |
CORS smoke test |
| GET | /books |
Distinct books from vector table |
| POST | /answer |
Agent-based Q&A (JSON) |
| POST | /answer/stream |
RAG + token streaming (SSE) |
| POST | /stream |
Agent streaming (SSE) |
| POST | /answer/stream2 |
Raw Ollama passthrough (debug) |
Default port: PORT env or 8001.
lna_ai_agent.py
Core AI logic (root of repo).
| Piece | Purpose |
|---|---|
lna_agent |
pydantic-ai Agent with system prompt and tools |
Config |
Mutable source (book id), default 978-2-917591-70-3 |
PydanticAIDeps |
Injected: Supabase client, LLM model, config |
get_embedding() |
Ollama embeddings for vector search |
retrieve_relevant_documentation |
RAG + neighbour expansion |
get_chunks_with_context |
Widen window around a specific chunk |
list_book_pages |
List page titles for a book |
get_book_pages_content |
Full page content by title |
LLM provider (LNA_LLM_PROVIDER):
ollama(default) โ local, viaOLLAMA_HOST+OLLAMA_MODELdeepseekโ cloud, requiresDEEPSEEK_API_KEY
RAG tuning:
| Variable | Default | Effect |
|---|---|---|
RAG_MATCH_COUNT |
10 |
Initial vector hits |
RAG_CONTEXT_WINDOW |
1 |
Neighbour chunks per hit |
Detailed API reference: Design/API_DOCUMENTATION.md.
Frontend
Routing and i18n
- App Router under
src/app/[locale]/with localesen,fr. middleware.ts: next-intl routing + Supabase session guard (unauthenticated users โ/auth/login).- Home page (
[locale]/page.tsx) renders<Chat />.
Key files
| File | Responsibility |
|---|---|
components/Chat.tsx |
Message state, streaming, book selection |
components/BookSelector.tsx |
Loads books from backend |
lib/api.ts |
sendMessage, sendMessageStream โ AI API |
lib/utils.ts |
fetchBooksFromSupabase โ GET /books |
services/api.ts |
Profile CRUD โ external API (NEXT_PUBLIC_API_URL) |
lib/supabase/client.ts |
Auth Supabase client |
components/AuthProvider.tsx |
Session context |
Auth vs AI data
- Auth:
NEXT_PUBLIC_SUPABASE_URL+NEXT_PUBLIC_SUPABASE_ANON_KEY - Book vectors / chat: AI backend + Supabase credentials on the server side (not exposed to browser for RAG)
Database (Supabase)
Table: book_vector_pages
Stores chunked book text with embeddings.
| Column | Role |
|---|---|
url |
Book identifier (used as source in API requests) |
chunk_number |
Order within a page |
title |
Page/section title |
content |
Chunk text |
metadata |
JSONB; includes book_title, source |
embedding |
768-dim vector |
RPC: match_book_vector_pages
Vector similarity search with optional filter {'source': '<book-id>'}.
Environment variables (quick reference)
Backend (.env at repo root or backend/docker/.env.backend)
| Variable | Required | Default | Notes |
|---|---|---|---|
SUPABASE_URL |
yes | โ | Supabase project URL |
SUPABASE_KEY |
yes | โ | Service or anon key with RPC access |
LNA_LLM_PROVIDER |
no | ollama |
ollama or deepseek |
OLLAMA_HOST |
if ollama | http://localhost:11434 |
Use /v1 for chat API |
OLLAMA_EMBEDDING_HOST |
no | same as OLLAMA_HOST |
Native API (no /v1) |
OLLAMA_MODEL |
no | qwen3:8b |
Chat model |
DEEPSEEK_API_KEY |
if deepseek | โ | Cloud LLM |
DEEPSEEK_BASE_URL |
no | https://api.deepseek.com |
|
DEEPSEEK_MODEL |
no | deepseek-chat |
|
RAG_MATCH_COUNT |
no | 10 |
|
RAG_CONTEXT_WINDOW |
no | 1 |
|
PORT |
no | 8001 |
uvicorn bind port |
Frontend (.env.local or frontend/docker/.env.frontend)
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Auth |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Auth |
NEXT_PUBLIC_AI_API_URL |
AI backend base URL |
NEXT_PUBLIC_API_URL |
External profile API |
NEXT_PUBLIC_APP_URL |
Public app URL (auth redirects) |
PORT |
Next.js listen port (Docker: 3000) |
Common code changes
| Goal | Where to edit |
|---|---|
| Add/change API endpoint | backend/main.py |
| Change RAG behaviour, prompts, tools | lna_ai_agent.py (root) |
| Change streaming UX | backend/main.py (/answer/stream) + frontend/src/lib/api.ts |
| Change chat UI | frontend/src/components/Chat.tsx |
| Add locale / translations | messages/en.json, messages/fr.json, src/i18n/ |
| Change default book | Config.source in lna_ai_agent.py |
| Switch LLM provider | LNA_LLM_PROVIDER env |
| Container build | backend/docker/Dockerfile, frontend/docker/Dockerfile |
| Local stack wiring | docker-compose.yml |
Testing
# From repo root with venv active
python -m pytest test/
# Manual API checks
curl http://localhost:8001/health
curl http://localhost:8001/books
curl -X POST http://localhost:8001/answer \
-H "Content-Type: application/json" \
-d '{"query":"What is this book about?","source":"978-2-917591-70-3"}'
Streaming HTML test: test/test_streaming.html.
Related documentation
| Document | Contents |
|---|---|
| Design/API_DOCUMENTATION.md | Full endpoint and SSE protocol reference |
| Design/LNA_AI_System_Design_Document.md | Extended design notes and schema |
| deployment/local-testing-docker-compose.md | Docker Compose walkthrough |
| deployment/dokploy-hetzner-hostinger.md | VPS deployment |
| frontend/QUICK-START.md | Production frontend deploy |
MkDocs integration (optional)
Add to mkdocs.yml at the repo root:
site_name: LNA AI
nav:
- Architecture: architecture.md
- API Reference: Design/API_DOCUMENTATION.md
- Local Docker: deployment/local-testing-docker-compose.md
Requires docs_dir: docs (default).