diff --git a/.env.example b/.env.example index b5f12e9..4fe37a7 100644 --- a/.env.example +++ b/.env.example @@ -60,10 +60,11 @@ AUTH_PRELOGIN_SECRET= # download to reuse it rather than fetching again. PLENUM_DATA_DIR= -# Use a parliament.yaml / prompt tree outside the repository, so a deployment's -# own values never show up in a diff against upstream. +# Point these outside the repository and a deployment's own values never show up +# in a diff against upstream — config, prompts and site copy all overridable. # PARLIAMENT_CONFIG=/etc/plenum/parliament.yaml # PROMPTS_DIR=/etc/plenum/prompts +# CONTENT_DIR=/etc/plenum/content # Re-read prompt files on every call. Development only. # PROMPTS_RELOAD=1 diff --git a/README.md b/README.md index 4ae5d85..cfe0849 100644 --- a/README.md +++ b/README.md @@ -101,11 +101,20 @@ deploy/examples/ systemd units and an nginx site, with placeholders All settings are environment variables, documented in `.env.example`. Two paths let a deployment keep its own values outside the repository entirely: -- `PARLIAMENT_CONFIG` — path to a `parliament.yaml` elsewhere on disk -- `PROMPTS_DIR` — path to a prompt tree elsewhere on disk +- `PARLIAMENT_CONFIG` — a `parliament.yaml` elsewhere on disk +- `PROMPTS_DIR` — a prompt tree elsewhere on disk +- `CONTENT_DIR` — site copy (explainer, guide) elsewhere on disk Set `PROMPTS_RELOAD=1` in development to re-read prompt files on every call. +### Running a fork + +If you maintain a deployment as a fork, keep it differing from upstream only in +files upstream does not have. Everything branded or private is either an env-var +pointer (the three above), an untracked file (`.env`, `providers.yaml`), or an +addition under `deploy/prod/`. `make check-fork-divergence` fails the build if +anything else drifts, which turns a merge conflict into a caught mistake. + ## Security The `database_query` tool executes model-authored SQL. Give it a database role with diff --git a/backend/app.py b/backend/app.py index 4db9a0f..4011cca 100644 --- a/backend/app.py +++ b/backend/app.py @@ -110,12 +110,15 @@ def _reap_abandoned_jobs() -> None: @app.get("/api/guide", response_class=PlainTextResponse) def get_guide() -> str: - """Serve user-guide.md as plain text. Single source of truth for all guide links.""" - guide_path = os.path.join(os.path.dirname(__file__), "..", "user-guide.md") - if not os.path.exists(guide_path): - raise HTTPException(status_code=404, detail="Guide not found") - with open(guide_path, encoding="utf-8") as f: - return f.read() + """Serve the user guide as plain text — the single source for all guide links. + + Resolved through CONTENT_DIR, so a deployment can ship its own guide without + editing the repository. + """ + guide = PARLIAMENT.read_content("guide_file") + if not guide: + raise HTTPException(status_code=404, detail="No guide configured") + return guide @app.get("/api/meta") diff --git a/bootstrap.py b/bootstrap.py index c8420da..8714c24 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -5,8 +5,8 @@ the repository root is not on ``sys.path`` and relative paths would resolve agai whatever directory the caller happened to be in. Importing this module fixes both, deriving the root from this file's own location. -The predecessor hardcoded ``/home/lasse/riksdagen`` in 22 files, which is why the -project could only ever run on one machine. +The predecessor hardcoded one absolute path in 22 files, which is why it could +only ever run on a single machine. """ from __future__ import annotations diff --git a/content/sv/user-guide.md b/content/sv/user-guide.md index 5640d0f..af50f8b 100644 --- a/content/sv/user-guide.md +++ b/content/sv/user-guide.md @@ -4,11 +4,11 @@ Den här guiden förklarar hur Riksdagen-appen är byggd, vilka val som gjorts, --- -## Om rixdagen.se +## Om den här sajten -Sajten är utvecklad av [Lasse Edfast](https://lasseedfast.se), hittills utan ekonomiskt stöd. Den är gratis att använda och ingen information sparas utöver de sju dagarna som en chatt sparas i. +Sajten drivs utan ekonomiskt stöd. Den är gratis att använda och ingen information sparas utöver de sju dagarna som en chatt sparas i. -Vill du vara med och utveckla, eller bara har frågor, kontakta gärna Lasse via [e-post](mailto:lasse@edfast.se). +Vill du vara med och utveckla, eller bara har frågor, hör gärna av dig. --- @@ -207,11 +207,11 @@ En API-nyckel är ett lösenord som identifierar dig mot en AI-leverantörs serv - **OpenRouter** – [openrouter.ai](https://openrouter.ai) (aggregator med tillgång till hundratals modeller) - **OpenAI** – [platform.openai.com](https://platform.openai.com) (samma modeller som driver ChatGPT) -Användning av externa modeller kostar pengar och debiteras direkt från ditt konto hos leverantören – inte via rixdagen.se. +Användning av externa modeller kostar pengar och debiteras direkt från ditt konto hos leverantören – inte via den här sajten. ### Hur nyckeln lagras -Din nyckel sparas **enbart lokalt i din webbläsare** (`localStorage`). Den skickas till rixdagen.se:s server bara för att vidarebefordras till leverantören när ett anrop görs – den loggas inte, sparas inte i databasen och syns aldrig i någon annan användares session. +Din nyckel sparas **enbart lokalt i din webbläsare** (`localStorage`). Den skickas till sajtens server bara för att vidarebefordras till leverantören när ett anrop görs – den loggas inte, sparas inte i databasen och syns aldrig i någon annan användares session. Konkret: - Nyckeln ligger kvar i webbläsaren tills du rensar den (knappen ✕ i inställningspanelen) eller tömmer webbläsarens lagringsdata. diff --git a/docs/eval-harness.md b/docs/eval-harness.md index e8343fc..61efec5 100644 --- a/docs/eval-harness.md +++ b/docs/eval-harness.md @@ -97,12 +97,12 @@ via sigmoid so 0.5 = neutral, >0.7 = likely grounded, <0.3 = likely hallucinated `NULL` means the scorer endpoint was unreachable when the judgment was recorded. -Start the scorer (first run downloads the model to `/home/lasse/models`): +Start the scorer (first run downloads the model to `$HOME/models`): ```bash nohup vllm serve BAAI/bge-reranker-v2-m3 \ --port 8001 \ - --download-dir /home/lasse/models \ + --download-dir $HOME/models \ --gpu-memory-utilization 0.2 \ --max-model-len 8192 \ --trust-remote-code \ diff --git a/parliament.py b/parliament.py index bfae36e..cee4fd0 100644 --- a/parliament.py +++ b/parliament.py @@ -24,6 +24,11 @@ import yaml _ROOT = Path(__file__).resolve().parent +# Site copy — the explainer, the limit warning, the user guide. Overridable so a +# deployment's own wording lives outside the repository, the same way PROMPTS_DIR +# works for prompts. +CONTENT_DIR = Path(os.environ.get("CONTENT_DIR") or _ROOT / "content") + # A Postgres text-search configuration name. It is interpolated into SQL rather # than passed as a parameter (identifiers cannot be bound), so it is validated # on load and never trusted from arbitrary input. @@ -129,7 +134,7 @@ class Parliament: rel = self.site.get(key) if not rel: return "" - path = _ROOT / rel + path = CONTENT_DIR / rel return path.read_text(encoding="utf-8") if path.exists() else "" # -- serialisation ------------------------------------------------------ diff --git a/parliament.yaml b/parliament.yaml index 2e05f7b..289e5a6 100644 --- a/parliament.yaml +++ b/parliament.yaml @@ -174,13 +174,18 @@ embeddings: # Served to the frontend via GET /api/meta. site: - title: rixdagen.se + # Deployment identity, not country data. Upstream ships neutral values; a + # deployment overrides them in its own PARLIAMENT_CONFIG file so its branding + # never appears in a diff against upstream. + title: plenum tagline: "Sök i riksdagens anföranden och motioner" # Markdown, so a deployment can rewrite its own copy without touching source. - explainer_file: content/sv/explainer.md - limit_warning_file: content/sv/limit_warning.md + # Paths are relative to CONTENT_DIR (default: ./content). + explainer_file: sv/explainer.md + limit_warning_file: sv/limit_warning.md # Upstream ships these empty. A deployment fills them in via its own # PARLIAMENT_CONFIG file, which keeps personal contact details out of the repo. + guide_file: sv/user-guide.md contact: email: null url: null