Architecture¶
A structural tour of the codebase — directory layout, backend and frontend
architecture, core data flow, and conventions. Included verbatim from the
repository root AGENTS.md, the guide used by both human and AI contributors.
AGENTS.md — LLMAIx Web Codebase Guide¶
This file explains the structure, concepts, and conventions of the LLMAIx Web repository to help AI agents (and humans) work with the codebase effectively.
What is LLMAIx Web?¶
A web application for extracting structured JSON data from unstructured medical/lab documents using LLMs. Users upload files (PDFs, images, spreadsheets), preprocess them via OCR/text extraction, define JSON schemas and prompts, run LLM-based extraction "trials", and evaluate results against ground truth data. Privacy-first: can run fully local or with self-hosted LLM providers.
Tech stack: Vue 3 + Vite + TypeScript + TailwindCSS (frontend), FastAPI (backend), SQLAlchemy (ORM), Celery (async tasks), PostgreSQL (primary DB, SQLite for dev), Pydantic (config).
Top-Level Directory Layout¶
llmaixweb/
├── backend/src/ # FastAPI Python backend
│ ├── main.py # App entry point, CORS, middleware, Celery worker spawn
│ ├── core/ # Config, security, dynamic settings, rate limiting
│ ├── db/ # SQLAlchemy engine, session, base
│ ├── models/ # SQLAlchemy ORM models (project, user, sso, admin, audit)
│ ├── schemas/ # Pydantic request/response schemas
│ ├── middleware/ # Security headers, request-context/correlation-id, global error handlers
│ ├── routers/v1/endpoints/ # FastAPI route handlers
│ ├── services/ # External service integrations (OCR engines, OIDC)
│ ├── celery/ # Celery app config + tasks
│ └── utils/ # Enums, helpers, evaluation, audit, structured logging
├── frontend/ # Vue 3 frontend
│ ├── views/ # Page-level components (routed)
│ ├── components/ # Reusable UI components (common/, + per-domain folders)
│ ├── composables/ # Reusable composition functions (download, pagination, WS updates, toasts…)
│ ├── stores/ # Pinia stores (auth, firstAdmin, publicSettings, toast)
│ ├── services/ # Axios client (`api.ts`) + per-resource API modules (`*Api.ts`)
│ ├── router/ # Vue Router config
│ ├── types/ # Hand-written TS domain types mirroring backend Pydantic schemas
│ └── utils/ # Formatters, date/err/markdown/schema helpers
├── alembic/ # Database migrations
├── docs/ # MkDocs documentation source (see "Documentation Site" below)
├── mkdocs.yml # MkDocs Material config (site → GitHub Pages)
├── .github/workflows/ # CI: tests.yml, docs.yml, security.yml, docker-publish.yml
├── LICENSE # AGPL-3.0-or-later
├── THIRD_PARTY_NOTICES.md # Bundled OSS components + licenses
├── CITATION.cff # Academic citation metadata
├── CHANGELOG.md # Keep a Changelog format
├── pyproject.toml # Python deps, Ruff config, project metadata
├── compose.yml # Docker Compose (CPU)
├── compose.dev.yml # Docker Compose (dev hot-reload)
├── compose.deepseek.yml # Optional: self-hosted Mistral OCR (DeepSeek-OCR-2 + KatDocExtract, GPU)
└── compose.vllm.yml # Optional: self-hosted vLLM endpoint (e.g. Gemma 4 for Vision OCR, GPU)
Core Concepts & Data Flow¶
The Project Workflow (Step-by-Step)¶
A Project is the top-level container. Within a project, users follow this sequence:
1. Files¶
Users upload files to a project. Supported types: PDF, DOC/DOCX, PNG/JPEG, CSV/XLSX, TXT. Files are stored either in local storage (LOCAL_DIRECTORY) or S3-compatible storage (via boto3). Each file gets a UUID-based filename for storage. Duplicate detection uses SHA-256 hashes.
Key model: File (models/project.py:119)
- Links to project via project_id
- Has a preprocessing_strategy (for CSV/XLSX: full_document or row_by_row)
- Has file_metadata JSON for CSV import config (text columns, delimiters, etc.)
- file_creator distinguishes user-uploaded vs system-generated files
2. Preprocessing¶
Files are processed asynchronously via Celery to extract text content. The PreprocessingPipeline class (utils/preprocessing.py) handles this.
Key models:
- PreprocessingTask — overall task for a batch of files (tracks progress, cancellation)
- FilePreprocessingTask — per-file subtask (status, errors, warnings)
- PreprocessingConfiguration — reusable settings presets (which OCR engine, parameters)
OCR engines (for PDFs/images):
1. Docling-serve — remote service for embedded text extraction and Tesseract OCR (default, tried first for PDFs)
2. Local Docling fallback — embedded Docling when docling-serve is unavailable (optional, requires DOCLING_LOCAL_FALLBACK=true)
3. Mistral OCR — API-based OCR via Mistral or self-hosted DeepSeek-OCR-2 + KatDocExtract
4. Vision LLM OCR — API-based OCR via any OpenAI-compatible vision model
Both Mistral OCR and Vision LLM OCR can use self-hosted backends:
- compose.deepseek.yml provides a local Mistral OCR API (DeepSeek-OCR-2 + KatDocExtract)
- compose.vllm.yml provides a local OpenAI-compatible endpoint for Vision OCR
Point MISTRAL_API_BASE / VISION_OCR_API_BASE at the respective service.
For CSV/XLSX files:
- full_document strategy: entire table converted to text
- row_by_row strategy: each row becomes a separate document (with configurable text columns and optional case ID column)
FilePreprocessingTask.warnings stores processing warnings (e.g., skipped rows).
3. Documents¶
The output of preprocessing — each Document has extracted text content ready for LLM extraction. Documents link back to their original file and preprocessing config. They can be organized into DocumentSets (groups of documents for trial execution).
Key model: Document (models/project.py:188)
- text — the extracted text content (what gets sent to the LLM)
- document_name — human-readable name
- meta_data — tracks extraction method, OCR engine used, row index, etc.
- Has a uniqueness constraint on (original_file_id, preprocessing_config_id, document_name)
DocumentSet allows grouping documents. Trials are run against all documents in a set.
4. Schemas & Prompts¶
Before running LLM extraction, users define: - Schema — a JSON schema defining the structured output format (nested objects, arrays, all JSON types). Has a visual tree editor in the frontend. - Prompt — system prompt and user prompt templates. The user prompt typically receives the document text as input; the system prompt instructs the LLM on extraction rules using the schema.
Key models:
- Schema (models/project.py:467) — schema_name + schema_definition (JSON)
- Prompt (models/project.py:448) — system_prompt + user_prompt
5. Trials¶
A Trial runs LLM-based extraction against a set of documents using a specific schema + prompt + LLM model combination. Results are stored as TrialResult objects (one per document).
Key model: Trial (models/project.py:492)
- References schema_id, prompt_id, document_set_id
- llm_model, api_key, base_url — the LLM endpoint configuration (OpenAI-compatible)
- bypass_celery — if True, runs synchronously (for small trials)
- advanced_options — JSON for temperature, max_tokens, etc.
- document_ids — explicit list of document IDs to process
- Supports cancellation with optional rollback
TrialResult — one per document per trial, stores the result JSON and additional_content.
Important: The system uses any OpenAI-compatible API. This includes official OpenAI, self-hosted vLLM, llama.cpp, Ollama, or custom gateways.
6. Evaluation¶
Users can upload GroundTruth files (CSV/XLSX with correct extraction values) and compare trial results against them via Evaluations.
Key models:
- GroundTruth — uploaded file with cached data, links to a project
- FieldMapping — maps ground truth columns to schema fields, with comparison_method (exact, fuzzy, numeric, etc.)
- Evaluation — stores overall metrics, per-field metrics, per-document metrics, confusion matrices
- EvaluationMetric — individual field-level correctness for each document
Creating an evaluation requires: a trial, a ground truth file, field mappings associating ground truth columns with schema fields. The system then computes accuracy metrics.
Backend Architecture¶
API Structure¶
All endpoints are under /api/v1/. The main router (main.py:78-83) includes:
- /api/v1/auth/* — login, token refresh, logout, public settings (incl. SSO provider list)
- /api/v1/auth/sso/* — OIDC SSO flow (login redirect + callback with JIT provisioning)
- /api/v1/user/* — user CRUD, invitations, self-service linked identities (/me/identities)
- /api/v1/project/* — projects (with sub-routers for files, documents, trials, etc.)
- /api/v1/admin/* — admin settings, Celery monitoring, audit log + error log (/admin/audit, /admin/audit/export, /admin/errors)
- /api/v1/admin/sso/* — admin CRUD for OIDC identity providers
Projects router (routers/v1/endpoints/projects.py) has nested sub-routers via APIRouter.include_router. Each sub-resource (files, preprocess, documents, schemas, prompts, trials, groundtruth, evaluations, and llm — model listing / connection & model testing) is a separate module with its own prefix.
Key Backend Files & Their Roles¶
| File | Purpose |
|---|---|
main.py |
FastAPI app, lifespan (DB init + Celery worker spawn), CORS, middleware wiring (security headers, request-context, error handlers), rate limiter, audit router, Redis subscriber (task updates + settings-cache invalidation) |
core/config.py |
Pydantic Settings class with lazy init, all env vars, runtime validation |
core/security.py |
JWT token create/verify, password hashing, OAuth2 scheme, admin guard, refresh-token create/verify/revoke, account lockout |
core/dynamic_settings.py |
DB-backed runtime settings override (cached via lru_cache) |
core/rate_limit.py |
Shared slowapi Limiter (Redis-backed when broker is Redis); gated by DISABLE_RATE_LIMIT |
middleware/security_headers.py |
SecurityHeadersMiddleware (X-Frame-Options, CSP, Referrer-Policy, conditional HSTS, etc.) |
middleware/request_context.py |
RequestContextMiddleware — stamps per-request correlation id |
middleware/error_handlers.py |
Global exception handlers → error_logs row + safe {error_id, message} response |
utils/logging_config.py |
setup_logging() — text or LOG_FORMAT=json structured logs with correlation id (wired into API + Celery workers) |
utils/audit.py |
record_audit(...) — append-only audit trail helper |
models/audit.py |
AuditLog (append-only trail) + ErrorLog (one row per unhandled error, keyed by user-facing error_id) |
utils/deletion.py |
cascade_delete_project and related cascade cleanup |
utils/csv_safety.py |
CSV-injection-safe writer (used by audit/metrics export) |
db/session.py |
SQLAlchemy engine + session factory, init_db() |
dependencies.py |
OpenAI client, S3 client, file get/save/remove, get_db() |
celery/celery_config.py |
Celery app with 2 queues (default, preprocess) |
celery/preprocessing.py |
process_files_async Celery task |
celery/info_extraction.py |
extract_info_celery Celery task |
celery/task_signals.py |
Celery signal hooks + sweeper: real-time status updates, crash messages, orphaned-task cleanup, WebSocket broadcast of task updates |
utils/preprocessing.py |
PreprocessingPipeline class — main preprocessing orchestrator |
utils/info_extraction.py |
LLM extraction logic (called by trial executor) |
utils/evaluation.py |
Evaluation metrics computation |
utils/password_policy.py |
Shared password validator (complexity rules, config-driven) |
utils/crypto.py |
Fernet encrypt/decrypt for secrets at rest (e.g. SSO client secrets) |
services/oidc_service.py |
OIDC discovery, authorize URL (PKCE + signed state), code exchange, userinfo |
routers/v1/endpoints/sso.py |
OIDC SSO login/callback flow + JIT user provisioning |
routers/v1/endpoints/admin_sso.py |
Admin CRUD for OIDC identity providers |
utils/enums.py |
All shared enums (FileType, PreprocessingStrategy, AuditAction, AuditOutcome, etc.) |
utils/url_safety.py |
SSRF guardrails for user-supplied custom API endpoints (blocks cloud-metadata, restricts schemes, disables redirect-following) |
Configuration System¶
Settingsclass (core/config.py): Uses PydanticBaseSettings. Reads from.envfile (path inENV_PATHenv var, defaults tobackend/.env) or environment variables. Validates OpenAI API connection and storage on init.- Runtime overrides: Admin panel changes are stored in
app_settingsDB table and loaded viadynamic_settings.pywith LRU cache. Only non-readonly settings can be overridden. - Skip runtime checks: Set
SKIP_RUNTIME_CHECKS=truefor Alembic migrations (bypasses OpenAI/S3 validation). - Security / ops env vars (documented in
.env.example, worth knowing): account lockout (LOGIN_MAX_ATTEMPTS,LOGIN_LOCKOUT_MINUTES), egress allowlists for PHI destinations (ALLOWED_LLM_ENDPOINTS,ALLOWED_OCR_ENDPOINTS), admin cross-project visibility (ADMIN_ALL_PROJECT_ACCESS), structured logging (LOG_FORMAT), rate limiting (DISABLE_RATE_LIMIT), short access tokens + refresh (ACCESS_TOKEN_EXPIRE_MINUTESnow 60,REFRESH_TOKEN_EXPIRE_DAYS), password policy (PASSWORD_POLICY_*), SSO (SSO_*), site banner (BANNER_ENABLED/BANNER_TEXT/BANNER_COLOR),MAX_UPLOAD_SIZE_BYTES.
Database¶
- Uses SQLAlchemy ORM with
declarative_base init_db()creates all tables viaBase.metadata.create_all()- Alembic for migrations (auto-generated from model changes)
- Migration command prefix:
SKIP_RUNTIME_CHECKS=true uv run alembic ...
Celery¶
- Two task queues:
default(general tasks, 4 workers) andpreprocess(heavy OCR, 1 worker) - Workers are spawned as
multiprocessing.Processobjects in the FastAPI lifespan - Pool type configurable via
CELERY_DEV_POOL(default:threads) andCELERY_PREPROCESS_POOL - Task safety:
task_acks_late=True,task_reject_on_worker_lost=True,worker_prefetch_multiplier=1 - Celery beat runs embedded in the single
defaultworker (with_beat=True) to drive the periodic orphan/stuck-task sweeper CELERY_PREPROCESS_POOLdefaults toauto(→soloon macOS,preforkelsewhere); soft/hard time limits fromCELERY_TASK_SOFT_TIME_LIMIT_SECONDS(6h)
Frontend Architecture¶
Routing Structure (router/index.ts)¶
/ → AppLayout (navbar)
/ (Landing) → Landing
/projects → ProjectOverview
/projects/:projectId → ProjectDetail (workflow tabs: Files → Preprocessing → Documents → Schemas → Trials → Evaluation)
/account → AccountSettings (profile, change password, connected SSO accounts)
/admin → AdminDashboard
/admin/settings → AdminSettings
/admin/sso → AdminSSO (OIDC provider management)
/admin/celery → AdminCelery
/admin/audit → AdminAudit (audit log + error log viewer)
/admin/user-management → AdminUserManagement (admin only)
/ → AuthLayout (no navbar)
/login → Login (shows SSO "Continue with…" buttons when enabled)
/register → Register
/invitation/:token → InvitationLanding
/auth/sso/complete → SsoComplete (SSO callback: reads tokens from URL fragment)
/first-admin → FirstAdminSetup
/forgot-password → ForgotPassword
/reset-password/:token → ResetPassword
Route guards:
- requiresAuth → redirects to /login with return URL
- adminOnly → redirects non-admins to /
- First admin check runs before all routes; redirects to /first-admin if no admin exists
- Logged-in users are redirected away from /login, /register, /invitation/*, /forgot-password, /reset-password/*
State Management¶
- Pinia stores in
stores/(all.ts): auth.ts— user session, token (localStorage), login/logout,isAuthenticated/isAdmincomputed propertiesfirstAdmin.ts— checks if any admin user exists (for first-run setup flow)publicSettings.ts— public config (SSO providers, banner, registration flags) fetched pre-logintoast.ts— global toast queue (paired withuseToast+ToastContainer/ToastItem)
API Client (services/api.ts)¶
- Axios instance with dynamic base URL (runtime config > build-time env var > default)
- Request interceptor: adds Bearer token from localStorage
- Response interceptor: auto-logout on 401/403, toast notification
API Service Layer (services/*Api.ts)¶
Per-resource modules wrap the raw api instance — components import functions from these, not the raw api instance. One module per backend resource: authApi, usersApi, ssoApi, projectsApi, llmApi, filesApi, preprocessingApi, documentsApi, documentSetsApi, schemasApi, promptsApi, trialsApi, groundtruthApi, evaluationsApi, adminApi, auditApi, versionApi. Each exports typed functions (e.g. trialsApi.list(projectId, params), documentsApi.delete(projectId, docId)) with Promise<AxiosResponse<T>> returns tied to the @/types domain interfaces. The only legitimate direct api import outside services/ is stores/auth.ts (for the auth-header config). services/websocket.ts holds the shared WebSocket client that the live-update composables build on.
Shared Primitives & Composables¶
The frontend has a layer of reusable building blocks — prefer these over re-implementing:
components/common/—BaseModal(Teleport + ref-counted scroll lock + ESC/backdrop close; the keystone most*Modal.vuecomponents build on),SlideOver/PanelLayout/SplitPane(drawer/panel/split layouts),BaseButton,BaseSegmentedControl,BaseTabGroup,DataTable(the shared sortable/filterable table — replaced the former ag-grid usage),ConfirmationDialog,StatusBadge(+utils/statusStyles.ts),FilterChip,FilterBar,SearchInput,PaginationControls,PageHeader,BatchActionBar,Callout,GlassCard,JsonViewer,LoadingSpinner,SkeletonRow/SkeletonTable,EmptyState,ErrorBanner,Tooltip,FileIcon,AppBanner,AppBrand,PasswordInput(password field w/ strength meter + show/hide, used by all auth/admin password flows),FormField,ToastContainer/ToastItem(global toast UI).composables/—useScrollLock(ref-counted body scroll lock for stacked modals),useFileDownload(blob→objectURL→revoke),usePagination/useDocumentPagination,usePreprocessingUpdates/useTrialUpdates/useWsEntityUpdates(WebSocket subscribe + project-id filtering for live task/entity progress),useModelTesting,useSchemaKeyboard,useToast(toast store wrapper),useClickOutside,useDebouncedSearch,useNavContext(contextual navbar state),useTableClasses(sharedDataTablestyling).utils/—formatters.ts(dates, file sizes, durations),dateRange.ts,errors.ts(extractErrorMessage),markdown.ts,schemaTypeIcons.ts(shared type→icon/color maps for the schema editor),schemaTemplates.ts+promptTemplates.ts,ocrLabels.ts.types/— hand-written TypeScript domain types mirroring the backend Pydantic schemas (@/typesbarrel). One file per domain (auth, user, sso, project, file, document, documentSet, schema, prompt, trial, preprocessing, groundtruth, evaluation, admin, audit, llm, enums, api helpers, websocket payloads). Import these for API responses, props, and composable signatures.
Key UI Components¶
Large components are orchestration shells — they compose smaller sibling components (in the same folder) and move heavy logic into composables. When editing a feature, the shell is the entry point but the real UI/logic usually lives in its extracted children.
Views (page-level):
- Landing.vue — landing page; composes the 10 components in components/landing/ (hero, pipeline visualization, interactive demo, feature grid)
- ProjectOverview.vue — project list grid
- ProjectDetail.vue — project detail with tabbed workflow interface (Files → Preprocessing → Documents → Schemas → Trials → Evaluation)
- AdminUserManagement.vue — user and invitation management (composes InviteUserModal/EditUserModal + UserGrid/InvitationGrid, built on common/DataTable)
- AdminSettings.vue — system settings configuration
- AdminSSO.vue — OIDC identity provider CRUD (admin tab under /admin/sso)
- AdminAudit.vue — audit-log + error-log viewer with filtering and export (/admin/audit)
- AccountSettings.vue — self-service profile, change password, connected SSO accounts, sign out (/account)
- AdminCelery.vue — Celery task monitoring
Components (reusable, by folder):
- files/ — FilesAndProcessing.vue (upload + preprocessing shell) with FilesFilterBar, FileDropzone, UploadFilesModal, FilesTable, PreprocessingHistoryPanel, PreprocessingConfigPanel, FilePreviewModal, DuplicatePreviewModal, FileImportConfigModal
- documents/ — DocumentsManagement.vue (list/filter shell) with DocumentsFilters, DocumentsTable, PaginationControls, BatchActionsModal (mode-aware: documents or trials), DocumentsGroups, DocumentViewer (+ DocumentViewerHeader, DocumentTextView, DocumentFilePreview, DocumentCompareView, VersionHistorySidebar, DocumentInfoSidebar, ExtractionMethodBadge)
- schemas/ — SchemaManagement.vue (lists schemas + prompts) with SchemaListSection/PromptListSection, SchemaFormModal/PromptFormModal, SchemaViewModal/PromptViewModal, SchemaTemplatesModal; VisualSchemaEditor.vue (full tree editor) + SimpleSchemaEditor.vue (simpler variant) with SchemaTree/TreeNode/SchemaBlock/PropertyDetailsEditor and AddPropertyModal/EditPropertyModal/DeletePropertyModal/SchemaEditorToolbar/SchemaEditorHelpModal
- trials/ — TrialsManagement.vue (list/filter shell, batch select+delete) with TrialCard, TrialFiltersPanel, CreateTrialModal (+ TrialMetadataCard, TrialSchemaSelect/TrialPromptSelect/TrialModelSelect, DocumentSelectionPanel + pickers, AdvancedSettingsPanel, CustomApiSettingsPanel, ModelTestCard), TrialResults (+ TrialResultCard, TrialMetaHeader, ResultReasoningPanel, ResultDocumentPreview, TrialDocumentErrors), RenameTrialModal, DownloadModal, TrialSelectorModal, TrialSchemaModal, TrialPromptModal
- evaluation/ — EvaluationView.vue / EvaluationOverview.vue (metrics + per-field accuracy), FieldErrorAnalysis, DocumentAnalysis, EvaluationAnalysisModal, MetricsExportModal
- groundtruth/ — GroundTruthManager.vue / GroundTruthUploadModal.vue / GroundTruthPreviewModal.vue, FieldTree, MappingList, IdFieldSelector, GroundTruthSample, ValidationBanner
- admin/ — UserGrid / InvitationGrid (built on common/DataTable), ActivityBell, InviteUserModal, EditUserModal
- projects/ — ProjectGrid.vue (built on common/DataTable), ProjectSettingsModal, DeleteProjectDialog, CreateProjectButton
Documentation Site & Governance¶
Documentation Site (MkDocs)¶
User-facing and operator documentation lives in docs/ and is built with MkDocs Material (mkdocs.yml). It is the single source of truth — the root README.md was trimmed to a landing page that links into the site. The site auto-deploys to GitHub Pages (https://katherlab.github.io/llmaixweb/) via .github/workflows/docs.yml on push to main.
The docs/ tree is organized by nav section: Getting started (installation, quickstart, concepts), User guide (files, preprocessing, ocr-engines, documents, schemas-and-prompts, trials, ground-truth, evaluation, account), Administration (settings, user-management, sso, celery), Operations (deployment, configuration, upgrading, backup-restore, troubleshooting), Security & governance (SECURITY.md, THREAT_MODEL.md, DATA_FLOW.md, DATA_RETENTION.md, AUDIT_LOGGING.md, RISK_REGISTER.md, DPIA_TEMPLATE.md), and Development (contributing, developer-guide, architecture).
Build locally: uv run --only-group docs mkdocs build (or mkdocs serve). When you change user-facing behavior, config, or the workflow, update the relevant page under docs/ — that is where end users and operators read it, not the README.
Screenshots live in docs/assets/screenshots/ and are generated, not hand-captured: npm run screenshots drives the whole workflow with the sample data and rewrites every image (see the Playwright harness under "Running Tests"). Pages embed them as Material <figure markdown> blocks (md_in_html + attr_list are enabled) with a directory-relative prefix (../assets/screenshots/ from a section page, assets/screenshots/ from the docs root). USAGE.md (included into the quickstart) uses plain-markdown images with the docs/assets/screenshots/ prefix so they resolve both on GitHub and through the include. Re-run npm run screenshots after any UI change that affects a documented screen, and avoid citing exact numbers from a screenshot in prose (they drift when the images regenerate).
Licensing & Governance Files (repo root)¶
LICENSE— AGPL-3.0-or-later. Note:pymupdfis AGPL, which currently blocks any future MIT relicense.THIRD_PARTY_NOTICES.md— bundled OSS components and their licenses.CITATION.cff— academic citation metadata (links the npj Precision Oncology 2025 paper).CHANGELOG.md— Keep a Changelog format; update it on every version bump (see Release Checklist below). Keep entries brief and focused on what affects setup or end users (new features, config/env changes, breaking changes, notable fixes); skip purely internal changes (tests, CI, refactors, dependency bumps)..github/SECURITY.md— vulnerability-disclosure policy (points todocs/SECURITY.md).
CI Workflows (.github/workflows/)¶
tests.yml— backend ruff lint/format, frontend lint + types + build + Vitest, backend pytest with a coverage floor, Alembic migration round-trip against real PostgreSQL, and a Celery+Redis integration test.docs.yml— builds the MkDocs site (PR) and deploys to GitHub Pages (push tomain).security.yml— CodeQL SAST, dependency audit (pip-audit + npm audit), Trivy fs scan, license-compliance scan; weekly cron. Most gates are informational.docker-publish.yml— builds backend + frontend images (amd64+arm64) on PR/push; pushes toghcr.io/katherlab/*only on published release..github/dependabot.yml— weekly updates for github-actions, uv, and npm.
Important Development Conventions¶
Naming & Imports¶
- Backend:
snake_casefor files, functions, variables;PascalCasefor models and schemas - Frontend:
PascalCase.vuefor components,camelCasefor JS variables - API routes use plural nouns (
/files,/projects,/trials/)
Frontend Patterns¶
- Build modals on
BaseModal(Teleport + ref-counted scroll lock + ESC/backdrop). Don't hand-roll Teleport/backdrop/scroll-lock — nested modals will break. - Call the API through
services/*Api.ts, not the rawapiinstance. Add a function to the relevant module if one is missing. - Reuse
composables/andcomponents/common/before duplicating: downloads (useFileDownload), pagination (usePagination+PaginationControls), live task progress (usePreprocessingUpdates/useTrialUpdates), tables (common/DataTable), status pills (StatusBadge), toasts (useToast), error messages (utils/errors.tsextractErrorMessage), dates/sizes (utils/formatters.ts). definePropsstyle: use the TypeScript generic formdefineProps<Props>()+withDefaults(defineProps<Props>(), {...})where defaults exist. Define aPropsinterface per component, importing shared domain types from@/types(e.g.Trial,Document,File). For string-literal-union props (the ones that previously usedvalidator:), declare a union type.defineEmitsusesdefineEmits<{ (e: 'event', payload: T): void }>();defineModelusesdefineModel<T>().- Dark mode: AppLayout owns the
darkModetoggle (writeslocalStorage['darkMode']+ toggles thedarkclass on<html>). Tailwind isdarkMode: 'class'. Components style dark variants directly via Tailwinddark:utilities (no grid-theme layer — the former ag-grid/useGridThemesetup was replaced bycommon/DataTable). - Verification gate:
npm run check(prettier + eslint +vue-tsc --noEmit, 0 errors required) andnpm run buildmust pass before committing.
Linting & Formatting¶
Always run these checks before committing changes:
Frontend:
npm run check # prettier --check + eslint + vue-tsc --noEmit
npm run type-check # vue-tsc --noEmit (type checking only)
npm run format # prettier --write (auto-fix)
npm run lint:fix # eslint --fix
Pre-commit hook auto-runs lint-staged (eslint + prettier on staged .js/.ts/.vue files via simple-git-hooks).
Backend:
Build Check¶
Always verify the frontend builds cleanly:
Environment Variables¶
.env.exampledocuments all available settingsENV_PATHenv var controls.envfile location (defaults tobackend/.env)- Frontend runtime config via
window.__APP_CONFIG__(injected in Docker) - Frontend build-time vars via
VITE_*prefix
Running Tests¶
Backend:
Frontend (Vitest + Vue Test Utils, jsdom environment):
Specs live next to the code they cover as*.test.ts (e.g. utils/*.test.ts,
composables/*.test.ts). Config is in vitest.config.ts at the repo root
(separate from frontend/vite.config.js so the test toolchain stays out of the
production bundle). Current coverage is the pure utils/ helpers and
logic-heavy composables; component-level coverage is not set up yet.
End-to-end + documentation screenshots (Playwright):
npm run test:e2e # smoke: boots fake-LLM + SQLite backend + Vite, drives the full workflow
npm run screenshots # regenerates docs/assets/screenshots/*.png from the real sample data
backend/.env.e2e, and the Vite dev server)
and reset backend state first via e2e/support/reset.mjs. The smoke test
(e2e/tests/workflow.spec.ts, playwright.config.ts) asserts the workflow
works; the screenshot harness (e2e/screenshots/workflow.screens.spec.ts,
playwright.screenshots.config.ts) walks the same path and captures every
docs image, backed by a realistic per-document fake LLM
(e2e/screenshots/support/fake-llm-realistic.mjs). Re-run npm run screenshots
after any UI change that affects the documentation images.
Docker¶
- Four compose files compose together via
-fflag pattern: compose.yml— base (CPU), always requiredcompose.dev.yml— local code hot-reloadcompose.deepseek.yml— self-hosted Mistral OCR (vLLM + KatDocExtract, GPU)compose.vllm.yml— self-hosted OpenAI-compatible endpoint (GPU)- Images:
ghcr.io/katherlab/llmaixweb-backend:latest,ghcr.io/katherlab/llmaixweb-frontend:latest - Database migrations run automatically on backend container startup
Versioning¶
- Separate versions for frontend (
package.jsonroot → auto-synced tofrontend/version.js) and backend (pyproject.toml-llmaixwebpackage version) - Both displayed in UI footer with git commit hashes on hover
- Backend commit hash read at runtime via
git rev-parse --short HEAD; frontend commit hash injected at build time viaGIT_COMMIT_HASHbuild arg
Release Checklist¶
- Bump frontend version in
package.json(root) — theprebuildscript auto-syncs tofrontend/version.jsat build time - Bump backend version in
pyproject.toml(e.g.version = "0.3.3") - Lock dependencies — run
uv lockif any dependencies changed; theuv.lockfile also tracks thellmaixwebversion and will update automatically - Update
CHANGELOG.md— move[Unreleased]entries into a new[x.y.z] — YYYY-MM-DDsection (add the compare links at the bottom). Keep it brief and user/setup-focused; leave out internal-only changes (tests, CI, refactors). - Build and push images — handled by GitHub Actions on release tag
- Tag the release:
- Frontend and backend are separate Docker images, so they can be updated independently
Common Pitfalls & Patterns¶
- Settings initialization uses lazy loading to avoid requiring
.envfor Alembic — useSKIP_RUNTIME_CHECKS=truefor migration commands - Storage abstraction:
dependencies.pyprovidesget_file()/save_file()/remove_file()that work with both local and S3 storage — always use these, never access storage directly - Cancellation pattern: Both
PreprocessingTaskandTrialhaveis_cancelled+rollback_on_cancelflags. The Celery task checksis_cancelledperiodically. Rollback deletes produced documents/results. - DB session: Use
get_db()dependency injection in routes, ordb_session()context manager for standalone operations - Frontend token: Stored in
localStorageunder key"token", managed by theauth.tsPinia store (refresh token handled alongside it) - File UUIDs: Storage uses UUID-based filenames, the
file_uuidfield links DB records to stored files - Docling-serve: For PDFs, docling-serve is tried first for embedded text. If insufficient (< 100 chars by default), falls back to configured OCR engine. Set
force_ocr=trueto skip.
When Adding New Features¶
- Backend model → add to
models/project.py, create Alembic migration - Pydantic schema → add to
schemas/project.pyor new schema file - API endpoint → add as new router file in
routers/v1/endpoints/, register inprojects.pysub-router or inmain.pytop-level router - Frontend component → add to
components/<domain>/orviews/as a<script setup lang="ts">SFC, add route inrouter/index.ts. Add API calls to the matchingservices/*Api.tsmodule (type the request/response against@/types); build modals onBaseModaland reusecomposables/+components/common/per the Frontend Patterns above. Shared domain types live infrontend/types/(importable as@/types) — add new interfaces there, hand-written to match the backend Pydantic schemas. - Celery task → add to
celery/directory, register incelery_config.pyincludelist
Async Task Lifecycle¶
- API endpoint creates a
PreprocessingTaskorTrialwithPENDINGstatus - Celery task is dispatched with the DB record ID
- Worker sets status to
IN_PROGRESS, processes data, updates progress - On completion:
COMPLETEDorFAILEDstatus - Frontend gets live updates via WebSocket (broadcast by
celery/task_signals.py); components subscribe through theusePreprocessingUpdates/useTrialUpdatescomposables (which filter by project id and merge updates). Some views also poll the status endpoint as a fallback.