d8bcdce879
commit 0d624137c2557c6eeb87020749e4977b821c2b5c Author: Adrien <adrien.cesaro@proton.me> Date: Thu Apr 9 11:55:22 2026 +0200 backend native image setup
237 lines
8.6 KiB
Markdown
237 lines
8.6 KiB
Markdown
# AI Teacher — Neurosurgeon RAG Learning Platform
|
|
|
|
A web application for neurosurgeons to upload medical textbooks (PDF), have them
|
|
embedded into a pgvector store, then select from a predefined topic list to receive
|
|
AI-generated cross-book summaries, and engage in grounded RAG chat.
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
graph TD
|
|
User["Neurosurgeon (Browser)"]
|
|
Login["Login Page\n(username + password form)"]
|
|
FE["Frontend\nVue.js 3 / Vite\n:5173"]
|
|
BE["Backend\nSpring Boot 4 / Spring AI\n:8080"]
|
|
Auth["Spring Security\nHTTP Basic Auth"]
|
|
DB["PostgreSQL + pgvector\n(source of truth)"]
|
|
FS["File Store\nuploads/ (local disk)\nExtracted figure PNGs"]
|
|
LLM["LLM Provider\n(OpenAI)\nEmbeddings + Chat + Vision"]
|
|
|
|
User -->|"First visit / unauthenticated"| Login
|
|
Login -->|"POST credentials\n(GET /api/v1/auth/check)"| Auth
|
|
Auth -->|"401 → back to login\n200 → app access"| Login
|
|
Login -->|"Authenticated"| FE
|
|
FE -->|"REST /api/v1/...\n(HTTP Basic on every request)"| Auth
|
|
Auth --> BE
|
|
BE -->|"JDBC — books, chapters,\nsections, figures, refs"| DB
|
|
BE -->|"pgvector — text chunks\n+ figure caption vectors"| DB
|
|
BE -->|"PNG read/write\n(figure extraction)"| FS
|
|
FE -->|"GET /api/v1/figures/**\n(static file serving)"| BE
|
|
BE -->|"Embedding + Chat\n+ Vision (image description)"| LLM
|
|
|
|
subgraph "Embedding Pipeline (per PDF upload)"
|
|
EP1["Parse pages → SectionEntity"]
|
|
EP2["Extract images → FigureEntity"]
|
|
EP3["Vision describe → embed caption"]
|
|
EP4["Chunk text → embed chunks"]
|
|
EP5["Link chunks ↔ figures"]
|
|
EP1 --> EP2
|
|
EP1 --> EP4
|
|
EP2 --> EP3
|
|
EP4 --> EP5
|
|
EP3 --> EP5
|
|
end
|
|
|
|
subgraph "Retrieval Pipeline (per chat query)"
|
|
RP0["Query expansion\n(QueryExpansionService)\nlay → clinical terms"]
|
|
RP1["Text chunk search (topK=5)"]
|
|
RP2["Figure caption search (topK=3)"]
|
|
RP3["Expand chunks → ±1-page section text"]
|
|
RP4["Fetch linked figures (chunk_figure_ref)"]
|
|
RP5["Merge + deduplicate figures"]
|
|
RP6["Build labelled prompt\n[S1],[F1]… tags"]
|
|
RP7["LLM chat call"]
|
|
RP8["Citation validation\n(CitationValidatorService)\nstrip hallucinated refs"]
|
|
RP0 --> RP1
|
|
RP0 --> RP2
|
|
RP1 --> RP3
|
|
RP1 --> RP4
|
|
RP2 --> RP5
|
|
RP4 --> RP5
|
|
RP3 --> RP6
|
|
RP5 --> RP6
|
|
RP6 --> RP7
|
|
RP7 --> RP8
|
|
end
|
|
```
|
|
|
|
## Marker API Response Structure
|
|
|
|
The PDF parsing pipeline calls a local [Marker](https://github.com/VikParuchuri/marker) server (`POST /marker/upload`).
|
|
|
|
### Top-level envelope
|
|
|
|
```json
|
|
{
|
|
"format": "json",
|
|
"output": "<JSON-encoded string>"
|
|
}
|
|
```
|
|
|
|
`output` is a **JSON-encoded string** (not a nested object) and must be parsed a second time to get the document tree.
|
|
|
|
### Parsed `output` shape
|
|
|
|
```
|
|
{
|
|
"children": [ <Page block>, ... ]
|
|
}
|
|
```
|
|
|
|
### Block types
|
|
|
|
Every block shares these fields:
|
|
|
|
| Field | Type | Notes |
|
|
|------------------|-------------------|--------------------------------------------|
|
|
| `id` | string | e.g. `/page/0/Picture/2` |
|
|
| `block_type` | string | see table below |
|
|
| `html` | string | rendered HTML; may contain `<content-ref>` |
|
|
| `bbox` | `[x0,y0,x1,y1]` | bounding box in page coordinates |
|
|
| `children` | array or null | nested blocks |
|
|
| `images` | object or null | base64 PNG map (leaf image blocks only) |
|
|
| `section_hierarchy` | object | heading ancestry |
|
|
|
|
#### Known `block_type` values
|
|
|
|
| block_type | Category | Notes |
|
|
|------------------|----------|-------------------------------------------------------|
|
|
| `Page` | structure | Top-level; direct children are the page content |
|
|
| `SectionHeader` | text | Section / chapter heading |
|
|
| `Text` | text | |
|
|
| `TextInlineMath` | text | |
|
|
| `ListItem` | text | |
|
|
| `Table` | text | |
|
|
| `Code` | text | |
|
|
| `Equation` | text | |
|
|
| `Footnote` | text | |
|
|
| `Caption` | text | Usually a child of a `*Group` block |
|
|
| `PageHeader` | text | |
|
|
| `PageFooter` | text | |
|
|
| `Handwriting` | text | |
|
|
| `Picture` | image | Leaf block; `images` map holds base64 PNG keyed by ID |
|
|
| `Figure` | image | Leaf block; same as `Picture` |
|
|
| `PictureGroup` | container | Wraps one `Picture` + one `Caption` child |
|
|
| `FigureGroup` | container | Wraps one `Figure` + one `Caption` child |
|
|
|
|
### Image extraction
|
|
|
|
Images are only present on **leaf** image blocks (`Picture`, `Figure`).
|
|
Group blocks (`PictureGroup`, `FigureGroup`) have `images: null` — the base64 PNG lives on the child leaf block.
|
|
|
|
```
|
|
PictureGroup
|
|
├── Picture ← images: { "/page/0/Picture/2": "<base64 PNG>" }
|
|
└── Caption ← html: "<p>Figure 1 — ...</p>"
|
|
```
|
|
|
|
## Stack
|
|
|
|
- **Backend**: Spring Boot 4.0.5 + Spring AI 2.0.0-M4, Java 25, Maven
|
|
- **Frontend**: Vue.js 3 + Vite + TypeScript + Pinia + Axios
|
|
- **Database**: PostgreSQL 16 + pgvector extension
|
|
- **Auth**: HTTP Basic (single shared in-memory user)
|
|
|
|
## Quick Start
|
|
|
|
See [specs/001-neuro-rag-learning/quickstart.md](specs/001-neuro-rag-learning/quickstart.md) for full instructions.
|
|
|
|
### Local Dev (JVM)
|
|
|
|
```bash
|
|
# Start the database
|
|
docker compose up -d
|
|
|
|
# Backend
|
|
cd backend
|
|
mvn spring-boot:run
|
|
|
|
# Frontend
|
|
cd frontend
|
|
npm install
|
|
npm run dev
|
|
```
|
|
|
|
### Native Image Build
|
|
|
|
Produces a GraalVM native binary packaged into a minimal Docker image via Jib.
|
|
|
|
**Prerequisite**: GraalVM 25 must be installed and set as `JAVA_HOME`.
|
|
|
|
```bash
|
|
# Install GraalVM 25 CE via sdkman (one-time)
|
|
sdk install java 25-graalce
|
|
sdk use java 25-graalce
|
|
|
|
# Build native executable + Docker image (requires Docker daemon)
|
|
cd backend
|
|
mvn -Pnative package jib:dockerBuild -DskipTests
|
|
```
|
|
|
|
The image `ai-teacher-backend:latest` will appear in your local Docker. It starts in under 1 second and uses significantly less memory than the JVM image.
|
|
|
|
### Run Native Stack (Docker Compose)
|
|
|
|
```bash
|
|
# Copy and fill in secrets
|
|
cp .env.example .env
|
|
# edit .env — add OPENAI_API_KEY at minimum
|
|
|
|
# Start PostgreSQL + native backend
|
|
docker compose -f docker-compose.native.yml up
|
|
```
|
|
|
|
App available at `http://localhost:8080`.
|
|
|
|
### Build Pipeline (Native)
|
|
|
|
```mermaid
|
|
graph LR
|
|
SRC["Source Code\n(Java 25)"]
|
|
AOT["Spring Boot AOT\n(process-aot)"]
|
|
NI["GraalVM native-image\n(native-maven-plugin)"]
|
|
EXE["Native Executable\ntarget/ai-teacher-backend"]
|
|
JIB["Jib\n(jib-native-image-extension)"]
|
|
IMG["Docker Image\nai-teacher-backend:latest\n(distroless base)"]
|
|
|
|
SRC --> AOT
|
|
AOT --> NI
|
|
NI --> EXE
|
|
EXE --> JIB
|
|
JIB --> IMG
|
|
```
|
|
|
|
### Environment Variables
|
|
|
|
#### Backend
|
|
|
|
| Variable | Required | Description |
|
|
|----------|----------|-------------|
|
|
| `OPENAI_API_KEY` | Yes | OpenAI API key for embeddings and chat |
|
|
| `APP_PASSWORD` | Yes | Shared password for HTTP Basic auth |
|
|
| `DB_URL` | Yes | JDBC URL, e.g. `jdbc:postgresql://localhost:5432/aiteacher` |
|
|
| `DB_USERNAME` | Yes | Database username |
|
|
| `DB_PASSWORD` | Yes | Database password |
|
|
| `FIGURE_STORAGE_PATH` | No | Base path for uploaded PDFs and extracted figures (default: `./uploads`) |
|
|
| `UPLOAD_ENABLED` | No | Set to `false` to disable the book upload endpoint (default: `true`) |
|
|
| `DELETE_ENABLED` | No | Set to `false` to disable the book delete endpoint (default: `true`) |
|
|
|
|
#### Frontend
|
|
|
|
| Variable | Required | Description |
|
|
|----------|----------|-------------|
|
|
| `VITE_API_URL` | No | Backend API base URL (default: `/api/v1`) |
|
|
| `VITE_APP_PASSWORD` | Yes | Shared password for HTTP Basic auth (must match `APP_PASSWORD`) |
|
|
| `VITE_UPLOAD_ENABLED` | No | Set to `false` to hide the upload UI (default: `true`) |
|
|
| `VITE_DELETE_ENABLED` | No | Set to `false` to hide the delete button (default: `true`) |
|