Keep deployment identity out of upstream

Site copy now resolves through CONTENT_DIR, alongside PARLIAMENT_CONFIG and
PROMPTS_DIR. All three let a deployment keep its own wording, branding and contact
details outside the repository, so they never appear in a diff against upstream —
which is what keeps a production fork mergeable.

parliament.yaml's site.title was "rixdagen.se". That is the deployment's identity,
not Sweden's data, so upstream now ships a neutral default. The Swedish user guide
keeps its content but loses the personal contact details and site-specific wording;
a deployment ships its own guide by pointing CONTENT_DIR at it.

Fixes /api/guide, which still read ../user-guide.md after that file moved to
content/sv/ and had been returning 404.

Deliberately kept: the author metadata in pyproject.toml and the copyright line in
LICENSE, which are attribution rather than configuration, and the README's note that
this runs in production as rixdagen.se, which is provenance worth stating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main
Lasse Edfast 1 week ago
parent ac181022e5
commit dbbc264a0a
  1. 5
      .env.example
  2. 13
      README.md
  3. 15
      backend/app.py
  4. 4
      bootstrap.py
  5. 10
      content/sv/user-guide.md
  6. 4
      docs/eval-harness.md
  7. 7
      parliament.py
  8. 11
      parliament.yaml

@ -60,10 +60,11 @@ AUTH_PRELOGIN_SECRET=
# download to reuse it rather than fetching again. # download to reuse it rather than fetching again.
PLENUM_DATA_DIR= PLENUM_DATA_DIR=
# Use a parliament.yaml / prompt tree outside the repository, so a deployment's # Point these outside the repository and a deployment's own values never show up
# own values never show up in a diff against upstream. # in a diff against upstream — config, prompts and site copy all overridable.
# PARLIAMENT_CONFIG=/etc/plenum/parliament.yaml # PARLIAMENT_CONFIG=/etc/plenum/parliament.yaml
# PROMPTS_DIR=/etc/plenum/prompts # PROMPTS_DIR=/etc/plenum/prompts
# CONTENT_DIR=/etc/plenum/content
# Re-read prompt files on every call. Development only. # Re-read prompt files on every call. Development only.
# PROMPTS_RELOAD=1 # PROMPTS_RELOAD=1

@ -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 All settings are environment variables, documented in `.env.example`. Two paths let a
deployment keep its own values outside the repository entirely: deployment keep its own values outside the repository entirely:
- `PARLIAMENT_CONFIG` — path to a `parliament.yaml` elsewhere on disk - `PARLIAMENT_CONFIG` — a `parliament.yaml` elsewhere on disk
- `PROMPTS_DIR` — path to a prompt tree 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. 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 ## Security
The `database_query` tool executes model-authored SQL. Give it a database role with The `database_query` tool executes model-authored SQL. Give it a database role with

@ -110,12 +110,15 @@ def _reap_abandoned_jobs() -> None:
@app.get("/api/guide", response_class=PlainTextResponse) @app.get("/api/guide", response_class=PlainTextResponse)
def get_guide() -> str: def get_guide() -> str:
"""Serve user-guide.md as plain text. Single source of truth for all guide links.""" """Serve the user guide as plain text — the single source for all guide links.
guide_path = os.path.join(os.path.dirname(__file__), "..", "user-guide.md")
if not os.path.exists(guide_path): Resolved through CONTENT_DIR, so a deployment can ship its own guide without
raise HTTPException(status_code=404, detail="Guide not found") editing the repository.
with open(guide_path, encoding="utf-8") as f: """
return f.read() guide = PARLIAMENT.read_content("guide_file")
if not guide:
raise HTTPException(status_code=404, detail="No guide configured")
return guide
@app.get("/api/meta") @app.get("/api/meta")

@ -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, whatever directory the caller happened to be in. Importing this module fixes both,
deriving the root from this file's own location. deriving the root from this file's own location.
The predecessor hardcoded ``/home/lasse/riksdagen`` in 22 files, which is why the The predecessor hardcoded one absolute path in 22 files, which is why it could
project could only ever run on one machine. only ever run on a single machine.
""" """
from __future__ import annotations from __future__ import annotations

@ -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) - **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) - **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 ### 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: Konkret:
- Nyckeln ligger kvar i webbläsaren tills du rensar den (knappen ✕ i inställningspanelen) eller tömmer webbläsarens lagringsdata. - Nyckeln ligger kvar i webbläsaren tills du rensar den (knappen ✕ i inställningspanelen) eller tömmer webbläsarens lagringsdata.

@ -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. `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 ```bash
nohup vllm serve BAAI/bge-reranker-v2-m3 \ nohup vllm serve BAAI/bge-reranker-v2-m3 \
--port 8001 \ --port 8001 \
--download-dir /home/lasse/models \ --download-dir $HOME/models \
--gpu-memory-utilization 0.2 \ --gpu-memory-utilization 0.2 \
--max-model-len 8192 \ --max-model-len 8192 \
--trust-remote-code \ --trust-remote-code \

@ -24,6 +24,11 @@ import yaml
_ROOT = Path(__file__).resolve().parent _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 # 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 # than passed as a parameter (identifiers cannot be bound), so it is validated
# on load and never trusted from arbitrary input. # on load and never trusted from arbitrary input.
@ -129,7 +134,7 @@ class Parliament:
rel = self.site.get(key) rel = self.site.get(key)
if not rel: if not rel:
return "" return ""
path = _ROOT / rel path = CONTENT_DIR / rel
return path.read_text(encoding="utf-8") if path.exists() else "" return path.read_text(encoding="utf-8") if path.exists() else ""
# -- serialisation ------------------------------------------------------ # -- serialisation ------------------------------------------------------

@ -174,13 +174,18 @@ embeddings:
# Served to the frontend via GET /api/meta. # Served to the frontend via GET /api/meta.
site: 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" tagline: "Sök i riksdagens anföranden och motioner"
# Markdown, so a deployment can rewrite its own copy without touching source. # Markdown, so a deployment can rewrite its own copy without touching source.
explainer_file: content/sv/explainer.md # Paths are relative to CONTENT_DIR (default: ./content).
limit_warning_file: content/sv/limit_warning.md explainer_file: sv/explainer.md
limit_warning_file: sv/limit_warning.md
# Upstream ships these empty. A deployment fills them in via its own # Upstream ships these empty. A deployment fills them in via its own
# PARLIAMENT_CONFIG file, which keeps personal contact details out of the repo. # PARLIAMENT_CONFIG file, which keeps personal contact details out of the repo.
guide_file: sv/user-guide.md
contact: contact:
email: null email: null
url: null url: null

Loading…
Cancel
Save