Skip to content

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.ts defaults to port 8000; frontend/src/lib/utils.ts (books) defaults to 8001. Set NEXT_PUBLIC_AI_API_URL explicitly to avoid mismatches.


How to start the service

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

  1. User authenticates via Supabase (middleware in frontend/middleware.ts).
  2. BookSelector loads books from GET /books (distinct titles from book_vector_pages).
  3. User sends a message; Chat calls POST /answer/stream with { query, source } where source is the book id (ISBN/URL key).
  4. Backend Phase 1 โ€” RAG:
  5. Embed query via Ollama nomic-embed-text
  6. Supabase RPC match_book_vector_pages (filtered by metadata.source)
  7. Expand neighbouring chunks (ยฑRAG_CONTEXT_WINDOW, default 1)
  8. Backend Phase 2 โ€” stream: Build prompt with context; stream tokens via OpenAI-compatible client (Ollama or DeepSeek).
  9. 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 PydanticAIDeps at startup.
  • CORS: Open (allow_origins=["*"]) for development.
  • Dynamic book scope: config.source is set per request from the source field (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, via OLLAMA_HOST + OLLAMA_MODEL
  • deepseek โ€” cloud, requires DEEPSEEK_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 locales en, 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.


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