- TypeScript 53.8%
- Python 34.8%
- CSS 9.5%
- JavaScript 1.2%
- Shell 0.5%
- Other 0.1%
|
All checks were successful
ragoon-ui-ci / validate (push) Successful in 6m9s
Reviewed-on: #24 |
||
|---|---|---|
| .forgejo/workflows | ||
| backend | ||
| docs | ||
| frontend | ||
| scripts | ||
| .dockerignore | ||
| .gitignore | ||
| Dockerfile | ||
| Makefile | ||
| README.md | ||
| THOUGHTS.md | ||
RAGOON
RAGOON is a local web interface for the Document RAG MVP API. It uploads and indexes documents and teaching recordings, lets users select the exact sources used for retrieval, optionally sends an editable JSON format to /generate, and renders answers with chunk-level source traceability.
Recorded Teaching Material
Video and audio uploads are transcribed by the RAG service using faster-whisper. Indexed recordings expose timestamped transcripts and participate in the same workspace permissions, source selection, vector retrieval, and reranking as documents. Upload acceptance does not mean indexing has finished; only indexed sources can be selected.
Supported recordings: MP4, M4V, MOV, MKV, WebM, AVI, MP3, WAV, M4A, FLAC, and OGG. The default recording limit is 256 MiB (MAX_MEDIA_UPLOAD_BYTES); configure the same limit in the RAG service. The RAG service also enforces a default one-hour duration limit. Transcription preserves the spoken language by default, rather than translating it.
The browser never connects to Ollama, Qdrant, or the upstream RAG service directly. RAGOON uses the Document RAG API for both retrieval and Ollama model access:
Browser :5173
→ relative /api/*
→ RAGOON adapter :8001
→ SSH tunnel on 127.0.0.1:8000
→ remote Document RAG API :8000
├─→ Ollama (model discovery and generation)
└─→ Qdrant (document retrieval)
The small FastAPI adapter keeps the upstream location configurable, provides same-origin browser calls, validates files and requests, normalizes errors, and serves the built frontend in production.
Deployment health
GET /api/health/live verifies that the adapter itself is running and is the
container liveness endpoint. GET /api/health additionally checks the upstream
RAG API, so it can return an error while the UI container remains healthy.
See CI/CD details for the validation and image-publication workflow.
Quick start
First time only
From this repository, install the dependencies and create the local configuration:
make install
cp backend/.env.example backend/.env
Start the application
First, open the SSH tunnel to the remote Document RAG API. This single forwarded API port carries both RAG operations and Ollama-backed model requests; Ollama's port does not need to be exposed to the local machine.
ssh -N \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-L 8000:127.0.0.1:8000 \
-p 2244 \
sergi@10.5.23.21
The command normally shows no output. Keep that terminal open for the entire RAGOON session; use Ctrl+C to close the tunnel. Reopen it after disconnecting, rebooting, or starting a new development session.
Once the tunnel is open, check both the RAG service and its Ollama model catalog through local port 8000:
curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/models
Do not start the adapter or UI until /health succeeds. /models should return the Ollama models available through the RAG API; if it fails while /health succeeds, check Ollama on the remote host.
Then open two more terminals in this repository.
Terminal 1 — start the adapter backend on port 8001:
make backend-dev
Terminal 2 — start the UI on port 5173:
make frontend-dev
Open http://localhost:5173 in your browser.
After the first-time setup, reopen the tunnel and run these two make commands whenever you want to start the application. The complete request flow is:
UI :5173 → adapter :8001 → tunnel :8000 → remote RAG API → Ollama/Qdrant
Prerequisites
- Python 3.11 or newer
- uv
- Node.js 20 or newer and npm
- SSH access to the remote host
- The Document RAG API, Ollama, and Qdrant running on the remote host
Install
make install
cp backend/.env.example backend/.env
This creates backend/.venv from backend/uv.lock and installs frontend dependencies from frontend/package-lock.json.
Configuration
RAG_API_BASE_URL=http://127.0.0.1:8000
RAG_CONNECT_TIMEOUT_SECONDS=5
RAG_READ_TIMEOUT_SECONDS=300
APP_HOST=127.0.0.1
APP_PORT=8001
CORS_ORIGINS=http://localhost:5173
LOG_LEVEL=INFO
FRONTEND_DIST_PATH=../frontend/dist
AUTH_DB_PATH=data/auth.db
AUTH_BOOTSTRAP_ADMINS=sergi,andrea
AUTH_BOOTSTRAP_PASSWORD=
AUTH_SESSION_HOURS=24
AUTH_COOKIE_SECURE=false
AUTH_LEGACY_WORKSPACE_ID=
AUTH_LEGACY_WORKSPACE_NAME=Legacy default workspace
AUTH_EMAIL_VERIFICATION_HOURS=24
AUTH_PASSWORD_RESET_MINUTES=60
AUTH_SIGNUP_LIMIT_PER_HOUR=5
PUBLIC_BASE_URL=http://localhost:5173
SMTP_HOST=
SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM=
SMTP_STARTTLS=true
CALENDAR_TIMEZONE=Europe/Madrid
MATRIX_HOMESERVER_URL=
MATRIX_ADMIN_TOKEN=
MATRIX_SERVER_NAME=localhost
MATRIX_CLIENT_URL=
RAG_API_BASE_URL is normalized to remove trailing slashes and is never exposed to browser code. The local .env and backend/data/ are ignored by Git.
For a fresh installation, temporarily set AUTH_BOOTSTRAP_PASSWORD before the first backend start. The listed bootstrap admins are created only when absent, each with a private default workspace. After the first successful startup, remove the plaintext bootstrap password from .env; password changes and later restarts do not require it.
Self-service sign-up and password reset send email through the SMTP server configured with the SMTP_* settings; until SMTP_HOST and SMTP_FROM are set, both endpoints answer 503 EMAIL_DELIVERY_UNAVAILABLE. New accounts must confirm their email address before they can sign in. PUBLIC_BASE_URL is the browser-facing origin used to build the links in those emails (/verify-email?token=… and /reset-password?token=…). Accounts created before email support existed have no email address: they keep signing in as before and can add one under Settings → Account → Email address. A new or changed address becomes active only after its confirmation link is opened, so the current address keeps working until then.
Every email first goes into an outbox table in the adapter database. A message the SMTP server does not accept is retried after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h and 12 h, then marked failed and logged; delivered messages are deleted. A background worker in the adapter retries the outbox and sends calendar reminders every 30 seconds while SMTP_HOST and SMTP_FROM are set. Deployments use our own mail server (see ragoon-deploy/docs/mail.md), never a third-party mail service.
Class calendars
A class teacher adds exams, deliveries and group tasks with Create event in the calendar. Each class owns its events; every member of the class sees them in the calendar, opened from the date at the top right. Inside a class (or one of its groups) the calendar shows that class only; the personal workspace shows every class, one muted color per class. Enrolled students with a confirmed email address get a notice when the event is created and a reminder at 08:00 on the day before (skipped when the event is created later than that). Event dates and times are wall-clock values in CALENDAR_TIMEZONE. Events cannot be edited or deleted yet.
Failed sign-ins are limited to five per username and client IP address in 15 minutes, and sign-up requests to AUTH_SIGNUP_LIMIT_PER_HOUR per client IP address. Both use the connecting address, so behind a reverse proxy start Uvicorn with --proxy-headers --forwarded-allow-ips=<proxy address>; otherwise every client shares the proxy's limit.
Existing installations can temporarily set AUTH_LEGACY_WORKSPACE_ID=default_workspace to register documents and folders indexed before authentication as a non-default workspace belonging to the first bootstrap administrator. New workspaces always receive random ws_… identifiers.
Run in development
First open the SSH tunnel to the Document RAG API and leave it running:
ssh -N \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-L 8000:127.0.0.1:8000 \
-p 2244 \
sergi@10.5.23.21
-L 8000:127.0.0.1:8000 maps local port 8000 to port 8000 on the remote host. The adapter accesses only this endpoint. The remote Document RAG API then talks to Ollama and Qdrant, so no separate local 11434 tunnel is required.
Keep the SSH terminal open. In another terminal, verify the RAG API and Ollama model discovery:
curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/models
Then use two terminals from this repository:
make backend-dev
make frontend-dev
Open http://localhost:5173. Vite proxies relative /api requests to the RAGOON adapter at http://127.0.0.1:8001.
Local RAG service
Instead of the SSH tunnel, the Document RAG API can run on this machine with small models, to try uploads, file previews and chat offline. It needs the ragoon-rag repository next to this one (or RAGOON_RAG_DIR), Ollama, and a Qdrant binary:
brew install ollama
(cd ../ragoon-rag && uv sync --extra rerank --extra transcription)
mkdir -p .local-stack/bin
curl -L https://github.com/qdrant/qdrant/releases/download/v1.11.5/qdrant-aarch64-apple-darwin.tar.gz | tar -xz -C .local-stack/bin
Close the SSH tunnel, since both use port 8000, then start it and leave it running:
make local-rag
It starts Qdrant and Ollama if they are not already running, pulls qwen3:1.7b and mxbai-embed-large on first use, and serves ragoon-rag on 127.0.0.1:8000, so make backend-dev and make frontend-dev work unchanged. Uploads, indexes and logs stay in the git-ignored .local-stack/; delete it to start over. LOCAL_LLM_MODEL=qwen3:4b make local-rag uses a larger model, which answers better but slower. Answers from small models are a weak guide to quality; use the remote service for that.
Local Matrix homeserver
For the chat trial (feat/matrix-spike), a local Synapse runs next to the rest:
make local-matrix
It installs a pinned Synapse with uv, serves only its client API on 127.0.0.1:8008, and keeps its SQLite database, keys and logs in .local-stack/synapse/; delete that folder to start over. Federation and sign-ups are off: only Ragoon creates accounts. The first run creates an admin account for the backend and prints MATRIX_HOMESERVER_URL and MATRIX_ADMIN_TOKEN to add to backend/.env.
With those set, POST /api/matrix/session gives the signed-in user a Matrix session: it creates their Matrix account on first use (without a password, so nobody signs in to Matrix directly) and returns a token that expires with the Ragoon session, the user ID and the homeserver address for the browser (MATRIX_CLIENT_URL when browsers reach it elsewhere than the backend does). The admin token never leaves the backend, the response is never cached, and account creation and every issued session are audited without the token. Without the settings the endpoint answers 503 MATRIX_NOT_CONFIGURED.
GET /api/matrix/workspaces/{workspace_id}/rooms returns a class's Matrix space with its Announcements room (only the class's owners post) and General room (everyone posts), creating them on first use and adding the caller after checking their membership of the class in Ragoon. The rooms are invite-only, closed to guests and absent from the room directory; only Ragoon's service account adds people or changes room settings, and class owners can remove messages and members. In the trial, rooms exist only for classes, not groups or personal workspaces.
When chat is set up (both MATRIX_* settings present; the session tells the browser through chat_enabled), the header's Messages button in a class opens the class's chat in the right-hand pane: its Announcements and General rooms, with a composer where the user may post. The pane asks the backend for a session and the rooms, connects to the homeserver with matrix-js-sdk (loaded only then), and signs out of Matrix when it closes, so its token stops working. To try it, run make local-matrix, make backend-dev and make frontend-dev, open a class as its teacher, and use a second browser profile signed in as a student who joined with the class's code. Decisions, security and legal notes are in ragoon-deploy/docs/matrix-chat.md.
Local email testing
Sign-up, password reset, and email changes need an SMTP server. For development, run a local server that prints every message instead of delivering it:
uvx --from aiosmtpd python -m aiosmtpd -n -l 127.0.0.1:1025
or, for a web inbox at http://127.0.0.1:8025, Mailpit (mailpit --smtp 127.0.0.1:1025 --listen 127.0.0.1:8025 --disable-version-check). Stopping it and starting it again is an easy way to watch the outbox retry.
and point backend/.env at it, then restart make backend-dev:
PUBLIC_BASE_URL=http://localhost:5173
SMTP_HOST=127.0.0.1
SMTP_PORT=1025
SMTP_FROM=RAGOON <ragoon@localhost>
SMTP_STARTTLS=false
The links appear in that terminal in quoted-printable form: read =3D as = and join lines that end with =.
Application workflow
The application uses a conventional chat layout:
- Sign in, or use Sign up to create an account and confirm it from the email link. Forgot password? emails a one-time reset link.
- Select, create, or join a shared workspace below New chat in the left sidebar. Switching workspace changes its conversations, shared memory, and knowledge-base files together.
- Select or start private conversations from the left sidebar.
- Create memory folders in the sidebar, then create or place conversations inside them.
- Use the collapsible right Knowledge base pane to search, upload, organize, and select files without leaving the main screen. Knowledge folders are shared with all workspace members and limited to one level. Checked files influence prompts; unchecked files remain available but are not sent for retrieval. A folder checkbox selects or clears every indexed file it contains. The composer
+can upload a local file or import a public webpage URL; either is indexed, added to the current workspace, and selected without clearing existing sources. - Questions use Normal response by default. Use the adjacent format control to select or create a visual response layout. Its Lexical editor supports titles, headings, emphasis, quotes, code, and lists; RAGOON exports that document to Markdown and converts it into the JSON Schema sent to the RAG API.
- Choose an available Ollama model from the compact model submenu in the composer. The preference is saved per authenticated user and the exact model name is sent with each answer request.
- Enter a prompt. Active knowledge-base files apply to every conversation; with none active, requests go directly to the model's general knowledge.
After the first successful message, the UI makes a separate structured, non-RAG generation request for a short conversation title. This request has no chat_id or folder_id, so it does not affect chat or shared-folder memory.
Each conversation has an actions menu for manual renaming, moving it into or out of a memory folder, and permanent deletion. Deleting a conversation calls DELETE /chats/{chat_id} and removes its browser entry only after the upstream service has deleted that user's private chat memory. It does not delete knowledge-base files, memory folders, or the shared memory previously contributed to a folder.
Normal response sends no custom format field and displays the backend's default answer value as regular text. A visual response layout is edited with Lexical and stored internally as Markdown using Lexical's official import/export transformers. The UI compiles its ordered blocks into an object-shaped JSON Schema, sends it to the upstream API, and renders the returned fields back into Markdown. The generated schema remains available in a collapsed read-only disclosure for debugging. Legacy placeholder-based templates are converted into visual examples when loaded. Each chat response stores the layout used for that request so later edits do not change historical answers. User-created templates remain browser-local under rag-box.formats.v2.
Authentication, users, sessions, workspaces, memberships, knowledge folders, and document folder placement are stored in the adapter's SQLite database. Conversation navigation and active file selection remain browser-local MVP features. Chats are namespaced by the authenticated user and workspace. Active file IDs are stored once per workspace and reused across all conversations in it.
Passwords are stored only as salted scrypt hashes. Session tokens are random, stored server-side as hashes, and exchanged through HttpOnly, SameSite=Strict cookies. Password changes invalidate the user's other sessions. Only administrators can create users; every new user receives a private workspace.
Knowledge-base IDs are random permanent workspace identifiers and remain hidden from the UI. PATCH /api/workspaces/{id} takes a new name, an icon (one emoji, or null to remove it) or both, from the owner only; every member sees the icon. The owner sees the class's current invite code under Invite, copies it and sends it to another authenticated user; GET /api/workspaces/{id}/join-code returns it to the owner only. New code replaces it after a confirmation, and the previous code stops working. Joining still looks codes up by hash; codes created before the code was stored show nothing until the owner makes a new one.
Adapter API
The browser-facing adapter exposes:
GET /api/health
GET /api/models
POST /api/auth/login
POST /api/auth/logout
GET /api/auth/me
POST /api/auth/signup
POST /api/auth/verify-email
POST /api/auth/password-reset
POST /api/auth/password-reset/confirm
POST /api/auth/email
POST /api/auth/change-password
POST /api/admin/users
GET /api/admin/users
PUT /api/admin/users/{username}/role
DELETE /api/admin/users/{username}
GET /api/workspaces
POST /api/workspaces
PATCH /api/workspaces/{workspace_id}
DELETE /api/workspaces/{workspace_id}
POST /api/workspaces/join
POST /api/workspaces/{workspace_id}/join-code
POST /api/workspaces/{workspace_id}/events
GET /api/calendar/events?start=YYYY-MM-DD&end=YYYY-MM-DD
GET /api/documents
POST /api/documents/upload
POST /api/documents/from-url
GET /api/documents/{document_id}/file?workspace={workspace_id}
PUT /api/documents/{document_id}/file
POST /api/documents/{document_id}/reindex
DELETE /api/documents/{document_id}
PATCH /api/documents/{document_id}/knowledge-folder
GET /api/knowledge-organization
POST /api/knowledge-folders
PATCH /api/knowledge-folders/{folder_id}
DELETE /api/knowledge-folders/{folder_id}?documents=all|shared|private
GET /api/folders
GET /api/folders/{folder_id}
POST /api/folders
PATCH /api/folders/{folder_id}
DELETE /api/folders/{folder_id}
DELETE /api/chats/{chat_id}
POST /api/generate
The adapter derives X-User-Id from the authenticated server session. The browser sends only its selected X-Workspace-Id, and the adapter verifies membership before forwarding either identity upstream. Documents and folders are visible within a workspace; chat memory is isolated by workspace, user, and chat_id. Chats placed inside a folder also send folder_id, enabling shared folder memory. Conversation-memory folders remain separate from the adapter-managed knowledge folders that organize context files.
Workspace members have read and write access, but destructive operations on shared documents, knowledge folders, memory folders, and workspaces are owner-only. Personal default workspaces cannot be deleted. Every member can delete their own private conversations because the upstream operation is isolated by authenticated user, workspace, and chat ID. Deleting a private chat does not erase shared folder memory.
Workspace owners can rename their workspaces. The upstream RAG contract does not expose folder updates, so memory-folder renames are stored as workspace-shared aliases in the adapter while retaining the original folder_id; renaming therefore does not reset or delete shared memory.
The Improve prompt switch controls use_prompt_improver. It defaults to off for faster responses; turn it on when an extra LLM rewrite may help with an unclear prompt. A selected response layout compiles into a validated JSON Schema; the default Normal response omits a custom schema. The adapter also supports the upstream chat_id, folder_id, use_rag, top_k, max_output_tokens, and llm_model fields. Available model names come from upstream /models; RAGOON sends the selected name exactly and displays the model reported on each response. When the upstream API returns timings, the UI displays retrieval, generation, and total durations.
The Response depth selector provides three fixed latency/capacity presets:
| Level | top_k with RAG |
max_output_tokens |
|---|---|---|
| Fast | 4 | 1024 |
| Balanced | 8 | 2048 |
| Deep | 12 | 4096 |
These fixed settings control retrieval breadth and the maximum answer length, not a separate model reasoning mode. They apply equally to normal and structured responses; the UI does not infer a preset from the wording of the question. The selected visual layout guides the answer's headings, emphasis, and list structure.
Uploads must be real multipart files in the file field. The adapter accepts PDF, DOCX, TXT, Markdown, and HTML files with a 50 MiB limit, and the recordings listed above with a separately configurable limit. Public http/https URLs are forwarded to /documents/from-url; the upstream service remains responsible for redirect, address, timeout, and download-size safety checks. Generation requires a non-empty prompt. Documents are required only when use_rag is explicitly true; the UI sets it according to whether documents are selected.
GET /api/documents/{document_id}/file streams the original upload for viewing, to whoever RAG lets read the document. It forwards Range and cache headers, so recordings can seek, and accepts the workspace as ?workspace= as well as in the header, since a new tab or a player's src cannot send headers. Responses carry X-Content-Type-Options: nosniff and Cache-Control: private, no-cache; HTML, SVG and XML files also get a sandboxing Content-Security-Policy, so an uploaded page cannot run scripts as the person who opens it. PUT with {"content": "..."} replaces an uploaded Markdown or text file (uploader only) and indexes it again. Both need the RAG routes of the same name.
Tests and checks
make test
make typecheck
make lint
make format
make check
make build
Backend tests mock the upstream RAG API. Frontend tests mock the adapter, so neither suite requires Ollama, Qdrant, or a live RAG service.
Production mode locally
make production
This builds frontend/dist and serves the complete application from http://127.0.0.1:8001. The Document RAG API must still be reachable at RAG_API_BASE_URL.
Container image
The production image builds the Vite frontend and serves it through the FastAPI
adapter on port 3000. Authentication state is stored in /app/data and must be
mounted as persistent storage.
make docker-build
docker run --rm -p 3000:3000 \
-e RAG_API_BASE_URL=http://host.docker.internal:8000 \
-v ragoon-ui-data:/app/data \
ragoon-ui:local
Troubleshooting
- Local port
8000already in use: find and stop the process using it, or close an older SSH tunnel before opening a new one. - RAG API unavailable: reopen the SSH tunnel, then verify
curl http://127.0.0.1:8000/healthandRAG_API_BASE_URL. - No Ollama models available: check
curl http://127.0.0.1:8000/models. If/healthworks but/modelsfails or is empty, inspect Ollama and the RAG API configuration on the remote host. - Upload rejected: send a supported real file, not a file path string.
- Document failed: use Reindex; inspect the upstream service if the failure repeats.
- No documents selectable: only documents with
indexedstatus can be selected. - Generation rejected: ensure the prompt is non-empty and the selected format is a valid JSON object. Documents are required only for RAG requests.
- Long generation timeout: increase
RAG_READ_TIMEOUT_SECONDSif the upstream model needs more than five minutes. - Upstream Qdrant/Ollama errors: diagnose those services on the RAG API host; this UI does not connect to them.
Current limitations
The adapter does not terminate TLS or provide MFA or centralized secret management. Password recovery and email delivery require the configured SMTP service. Set AUTH_COOKIE_SECURE=true and terminate TLS before exposing the service beyond a trusted local environment.