Everything that made this Sweden-only in code now lives in one configuration file: party list and colours, chamber activity types, the Postgres text-search dictionary, the party-code pattern used to verify attributions, identifier shapes, source URLs, the person-photo URL template, embedding model and dimension, and the site copy. info.py is deleted. Its party_colors_lighten table is gone too — the tint is now computed from the party colour, so there is no second palette to keep in sync. Its dead select_columns and css strings go with it. The stylesheet no longer carries per-party rules. It had --party-M through --party-NYD plus twenty [data-party="..."] selectors, which no amount of configuration could adapt: CSS cannot read a dict. App.tsx now publishes the configured palette as custom properties from /api/meta, and one color-mix rule covers every party in any country. The author's email and Twitter handle are out of the source entirely. Explainer copy moved to content/sv/*.md, and contact details come from `site.contact`, which upstream ships empty for a deployment to fill in via its own PARLIAMENT_CONFIG. Two startup assertions added, both for failures that are otherwise silent: a text-search config mismatch returns near-zero rows with no error, and a vector column that disagrees with embeddings.dimension fails deep inside pgvector with a message that never mentions configuration. Verified: five searches spanning phrase, exclusion and prefix syntax — exercising all three tsquery builders that changed — return byte-identical payloads against production, with differing hit counts and first hits proving the queries really differ. Frontend builds, and tsc reports the same 9 pre-existing errors as before, none new. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>main
parent
104cd7d795
commit
0be70f9307
25 changed files with 597 additions and 260 deletions
@ -0,0 +1,12 @@ |
|||||||
|
Det här är en databas över vad svenska riksdagspolitiker har sagt i olika debatter i Riksdagen sedan 1993. |
||||||
|
Datan kommer dels från data.riksdagen.se och dels från transkriberingar av vad som sänts i Riksdagens videotjänst (från år 2000). |
||||||
|
|
||||||
|
- Börja med att skriva ett eller flera sökord nedan. Du kan använda asterisk (\*), minus (-), citattecken (""), OR och år\:yyyy-yyyy. Sökningen |
||||||
|
`energikris* baskraft OR kärnkraft "fossilfria energikällor" -vindkraft år:2015-2022` söker anföranden som\: |
||||||
|
- nämner "energikris" (inkl. ex. "energikris*en*") |
||||||
|
- nämner antingen "baskraft" *eller* "kärnkraft" |
||||||
|
- nämner den *exakta frasen* "fossilfria energikällor" |
||||||
|
- *inte* nämner "vindkraft" |
||||||
|
- återfinns under åren 2015-2022 |
||||||
|
- När du fått dina resultat kan du sedan klicka bort partier eller ändra vilka år och debattyper du är intresserad av. |
||||||
|
- Under "Längre utdrag" kan du välja att se hela anförandet i text, och under texten finns länkar till Riksdagens Webb-TV och nedladdningsbart ljud (i de fall där debatten har sänts). |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
Din sökning ger fler än 10 000 träffar. Försök göra den mer specifik, exempelvis genom att |
||||||
|
använda minustecken eller specificera årtal genom att skriva år\:yyyy-yyyy (ex. år:2019-2020, utan mellanrum efter kolon). |
||||||
|
Gränsen på 10 000 träffar finns för att servern inte ska överbelastas. |
||||||
@ -1,180 +0,0 @@ |
|||||||
""" Information and constants are put here and imported into app.py. """ |
|
||||||
|
|
||||||
party_colors = { |
|
||||||
"MP": "#83CF39", |
|
||||||
"V": "#b51a0e", |
|
||||||
"S": "#E8112d", |
|
||||||
"C": "#009933", |
|
||||||
"M": "#52BDEC", |
|
||||||
"KD": "#000077", |
|
||||||
"SD": "#DDDD00", |
|
||||||
"L": "#006AB3", |
|
||||||
"NYD": "#ffff2b", |
|
||||||
'': 'white', |
|
||||||
'-': 'white' |
|
||||||
} |
|
||||||
|
|
||||||
select_columns = ''' |
|
||||||
_id, |
|
||||||
dok_id, |
|
||||||
"anforandetext" AS "Text", |
|
||||||
anforande_nummer AS number, |
|
||||||
kammaraktivitet as debatetype, |
|
||||||
talare AS "Talare", |
|
||||||
datum AS "Datum", |
|
||||||
year AS År, |
|
||||||
debateurl AS url_session, |
|
||||||
parti AS "Parti", |
|
||||||
audiofileurl as url_audio, |
|
||||||
startpos as start, |
|
||||||
intressent_id |
|
||||||
''' |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 70 % lighter party colors. |
|
||||||
party_colors_lighten = { |
|
||||||
"MP": '#daf1c4', |
|
||||||
"V": '#f8ada7', |
|
||||||
"S": '#fab6bf', |
|
||||||
"C": '#94ffb8', |
|
||||||
"M": '#cbebf9', |
|
||||||
"KD": "#b1b1ff", # 80 % |
|
||||||
"SD": "#ffffa8", |
|
||||||
"L": "#9cd6ff", |
|
||||||
"NYD": "#ffffbf", |
|
||||||
'': 'white', |
|
||||||
'-': 'white' |
|
||||||
} |
|
||||||
|
|
||||||
css = """ <style> |
|
||||||
a:link { |
|
||||||
color: black; |
|
||||||
} |
|
||||||
a:visited { |
|
||||||
color: black; |
|
||||||
} |
|
||||||
a:hover { |
|
||||||
color: grey; |
|
||||||
} |
|
||||||
""" |
|
||||||
for p, c in party_colors.items(): |
|
||||||
if p == 'NYD': |
|
||||||
c = '#FFC000' |
|
||||||
if p == 'SD': |
|
||||||
c = '#E5AC00' |
|
||||||
if p in ['', '-']: |
|
||||||
c = 'black' |
|
||||||
css += f"\n.{p} a{{color: {c};}}" |
|
||||||
css += '\n</style>' |
|
||||||
|
|
||||||
# css = ''' |
|
||||||
# <style> |
|
||||||
# .C a{ |
|
||||||
# color: green; |
|
||||||
# } |
|
||||||
# </style> |
|
||||||
# ''' |
|
||||||
|
|
||||||
months_conversion = { |
|
||||||
'januari': '01', |
|
||||||
'februari': '02', |
|
||||||
'mars': '03', |
|
||||||
'april': '04', |
|
||||||
'maj': '05', |
|
||||||
'juni': '06', |
|
||||||
'juli': '07', |
|
||||||
'augusti': '08', |
|
||||||
'september': '09', |
|
||||||
'oktober': '10', |
|
||||||
'november': '11', |
|
||||||
'december': '12' |
|
||||||
} |
|
||||||
explainer = """Det här är en databas över vad svenska riksdagspolitiker har sagt i olika debatter i Riksdagen sedan 1993. |
|
||||||
Datan kommer dels från data.riksdagen.se och dels från transkriberingar av vad som sänts i Riksdagens videotjänst (från år 2000). |
|
||||||
- Börja med att skriva ett eller flera sökord nedan. Du kan använda asterix (*), minus(-), citattecken (""), OR och år\:yyyy-yyyy. Sökningen |
|
||||||
`energikris* baskraft OR kärnkraft "fossilfria energikällor" -vindkraft år:2015-2022` söker anföranden som\: |
|
||||||
- nämner "energikris" (inkl. ex. "energikris*en*") |
|
||||||
- nämner antingen "baskraft" *eller* "kärnkraft" |
|
||||||
- nämner den *exakta frasen* "fossilfria energikällor" |
|
||||||
- *inte* nämner "vindkraft" |
|
||||||
- återfinns under åren 2015-2022 |
|
||||||
- När du fått dina resultat kan sedan klicka bort parier eller ändra vilka år och debatttyper du är intresserad av. |
|
||||||
- Under "Längre utdrag" kan du välja att se hela anförandet i text, och under texten finns länkar till Riksdagens Webb-TV och nedladdningsbart ljud (i de fall |
|
||||||
där debatten har sänts). |
|
||||||
|
|
||||||
Berätta gärna hur du skulle vilja använda datan och om sånt som inte funkar. [Mejla mig](mailto:lasse@edfast.se) eller [skriv till mig på Twitter](https://twitter.com/lasseedfast). |
|
||||||
Jag som gjort den här sidan heter [Lasse Edfast och är journalist](https://lasseedfast.se). |
|
||||||
""" |
|
||||||
|
|
||||||
debate_types = { |
|
||||||
"kam-vo": { |
|
||||||
"title": "Beslut", |
|
||||||
"description": "Riksdagen fattar det formella beslutet i ärendet." |
|
||||||
}, |
|
||||||
"bet": { |
|
||||||
"title": "Debatt om beslut", |
|
||||||
"description": "Ledamöterna debatterar ett förslag innan beslut fattas." |
|
||||||
}, |
|
||||||
"kam-fs": { |
|
||||||
"title": "Frågestund", |
|
||||||
"description": "Ministrar svarar direkt på frågor från ledamöter." |
|
||||||
}, |
|
||||||
"kam-ar": { |
|
||||||
"title": "Information från regeringen", |
|
||||||
"description": "Regeringen informerar riksdagen i en aktuell fråga." |
|
||||||
}, |
|
||||||
"ip": { |
|
||||||
"title": "Interpellationsdebatt", |
|
||||||
"description": "Debatt om en skriftlig fråga till en minister." |
|
||||||
}, |
|
||||||
"kam-sf": { |
|
||||||
"title": "Statsministerns frågestund", |
|
||||||
"description": "Statsministern svarar på frågor från ledamöter." |
|
||||||
}, |
|
||||||
"sam-ou": { |
|
||||||
"title": "Öppen utfrågning", |
|
||||||
"description": "Utskottet frågar ut experter eller ansvariga personer offentligt." |
|
||||||
}, |
|
||||||
"kam-ad": { |
|
||||||
"title": "Aktuell debatt", |
|
||||||
"description": "Snabb debatt om en aktuell politisk fråga." |
|
||||||
}, |
|
||||||
"kam-al": { |
|
||||||
"title": "Allmänpolitisk debatt", |
|
||||||
"description": "Ledamöterna debatterar valfria politiska ämnen." |
|
||||||
}, |
|
||||||
"kam-bu": { |
|
||||||
"title": "Budgetdebatt", |
|
||||||
"description": "Debatt om regeringens budgetförslag." |
|
||||||
}, |
|
||||||
"kam-bp": { |
|
||||||
"title": "Bordläggning", |
|
||||||
"description": "Ett ärende anmäls i kammaren inför fortsatt behandling." |
|
||||||
}, |
|
||||||
"kam-pd": { |
|
||||||
"title": "Partiledardebatt", |
|
||||||
"description": "Partiledarna debatterar politikens stora frågor." |
|
||||||
}, |
|
||||||
"kam-dv": { |
|
||||||
"title": "Debatt med anledning av vårpropositionen", |
|
||||||
"description": "Debatt om regeringens ekonomiska vårproposition." |
|
||||||
}, |
|
||||||
"sam-se": { |
|
||||||
"title": "Öppet seminarium", |
|
||||||
"description": "Öppet seminarium eller samtal om ett särskilt tema." |
|
||||||
}, |
|
||||||
"kam-ud": { |
|
||||||
"title": "Utrikespolitisk debatt", |
|
||||||
"description": "Debatt om Sveriges utrikespolitik." |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
limit_warning = ''' |
|
||||||
Din sökning ger fler än 10 000 träffar. Försök gör den mer specifik, exempelvis genom att |
|
||||||
använda minustecken eller specificera årtal genom att skriva år\:yyyy-yyyy (ex. år:2019-2020, utan mellanrum efter kolon). |
|
||||||
Gränsen på 10 000 träffar finns för att servern inte ska krascha och kommer att höjas när jag har en starkare server. |
|
||||||
''' |
|
||||||
@ -0,0 +1,218 @@ |
|||||||
|
"""Load parliament.yaml — every country-specific value in one place. |
||||||
|
|
||||||
|
Lives at the repository root rather than under ``backend/`` so the ingest CLI and |
||||||
|
the maintenance scripts can import it without pulling in FastAPI. |
||||||
|
|
||||||
|
from parliament import PARLIAMENT |
||||||
|
PARLIAMENT.language.fts_config # 'swedish' |
||||||
|
PARLIAMENT.party_color('S') # '#E8112d' |
||||||
|
PARLIAMENT.vocabulary['speech'] # 'anförande' |
||||||
|
|
||||||
|
Mirrors the loading style already used by backend/services/provider_registry.py: |
||||||
|
a module-level singleton, ``yaml.safe_load``, frozen dataclasses. |
||||||
|
""" |
||||||
|
from __future__ import annotations |
||||||
|
|
||||||
|
import os |
||||||
|
import re |
||||||
|
from dataclasses import dataclass, field |
||||||
|
from functools import cached_property |
||||||
|
from pathlib import Path |
||||||
|
from typing import Any, Optional |
||||||
|
|
||||||
|
import yaml |
||||||
|
|
||||||
|
_ROOT = Path(__file__).resolve().parent |
||||||
|
|
||||||
|
# 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. |
||||||
|
_FTS_CONFIG_RE = re.compile(r"^[a-z_][a-z0-9_]*$") |
||||||
|
|
||||||
|
|
||||||
|
class ConfigError(ValueError): |
||||||
|
"""parliament.yaml is missing, malformed, or internally inconsistent.""" |
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True) |
||||||
|
class Party: |
||||||
|
code: str |
||||||
|
name: str |
||||||
|
color: str |
||||||
|
active: bool = True |
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True) |
||||||
|
class Language: |
||||||
|
fts_config: str |
||||||
|
prompt_language: str |
||||||
|
locale: str |
||||||
|
preserve_characters: str |
||||||
|
months: dict[str, str] = field(default_factory=dict) |
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True) |
||||||
|
class Embeddings: |
||||||
|
model: str |
||||||
|
dimension: int |
||||||
|
base_url_env: str |
||||||
|
chunk_chars: int |
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True) |
||||||
|
class Parliament: |
||||||
|
"""The active parliament's configuration.""" |
||||||
|
|
||||||
|
meta: dict[str, Any] |
||||||
|
language: Language |
||||||
|
vocabulary: dict[str, str] |
||||||
|
parties: list[Party] |
||||||
|
party_defaults: dict[str, str] |
||||||
|
activity_types: dict[str, dict[str, str]] |
||||||
|
document_subtypes: dict[str, str] |
||||||
|
decisions: dict[str, dict[str, str]] |
||||||
|
sessions: dict[str, Any] |
||||||
|
ids: dict[str, dict[str, str]] |
||||||
|
urls: dict[str, str] |
||||||
|
sources: dict[str, Any] |
||||||
|
embeddings: Embeddings |
||||||
|
site: dict[str, Any] |
||||||
|
path: Path |
||||||
|
|
||||||
|
# -- lookups ------------------------------------------------------------ |
||||||
|
|
||||||
|
@cached_property |
||||||
|
def _by_code(self) -> dict[str, Party]: |
||||||
|
return {p.code: p for p in self.parties} |
||||||
|
|
||||||
|
def party(self, code: Optional[str]) -> Optional[Party]: |
||||||
|
return self._by_code.get((code or "").strip().upper()) |
||||||
|
|
||||||
|
def party_color(self, code: Optional[str]) -> str: |
||||||
|
"""Colour for a party code, or the neutral colour for unknown/independent.""" |
||||||
|
found = self.party(code) |
||||||
|
return found.color if found else self.party_defaults["unknown_color"] |
||||||
|
|
||||||
|
def party_highlight_color(self, code: Optional[str], amount: float = 0.75) -> str: |
||||||
|
"""A pale tint of the party colour, for text highlighting. |
||||||
|
|
||||||
|
Computed rather than configured — the predecessor kept a second |
||||||
|
hand-maintained table of lightened colours that could drift from the first. |
||||||
|
""" |
||||||
|
found = self.party(code) |
||||||
|
if not found: |
||||||
|
return "#f0f0f0" |
||||||
|
r, g, b = (int(found.color.lstrip("#")[i : i + 2], 16) for i in (0, 2, 4)) |
||||||
|
blend = lambda c: round(c + (255 - c) * amount) # noqa: E731 |
||||||
|
return f"#{blend(r):02x}{blend(g):02x}{blend(b):02x}" |
||||||
|
|
||||||
|
@cached_property |
||||||
|
def party_codes(self) -> list[str]: |
||||||
|
return [p.code for p in self.parties if p.active] |
||||||
|
|
||||||
|
def activity_title(self, code: Optional[str]) -> str: |
||||||
|
return self.activity_types.get(code or "", {}).get("title", code or "") |
||||||
|
|
||||||
|
def person_photo_url(self, person_id: str) -> str: |
||||||
|
return self.urls["person_photo"].format(person_id=person_id) |
||||||
|
|
||||||
|
def session_label(self, start_year: int) -> str: |
||||||
|
"""Render a session label, e.g. 2022 -> "2022/23".""" |
||||||
|
return self.sessions["label_format"].format( |
||||||
|
start=start_year, end_short=f"{(start_year + 1) % 100:02d}", end=start_year + 1 |
||||||
|
) |
||||||
|
|
||||||
|
def read_content(self, key: str) -> str: |
||||||
|
"""Read one of the markdown files referenced under `site:`.""" |
||||||
|
rel = self.site.get(key) |
||||||
|
if not rel: |
||||||
|
return "" |
||||||
|
path = _ROOT / rel |
||||||
|
return path.read_text(encoding="utf-8") if path.exists() else "" |
||||||
|
|
||||||
|
# -- serialisation ------------------------------------------------------ |
||||||
|
|
||||||
|
def public_meta(self) -> dict[str, Any]: |
||||||
|
"""The payload served at GET /api/meta. |
||||||
|
|
||||||
|
Party colours ship to the client so the stylesheet does not have to |
||||||
|
hardcode one rule per party — which is what made the previous CSS |
||||||
|
Sweden-only in a way no configuration could fix. |
||||||
|
""" |
||||||
|
return { |
||||||
|
"parliament": { |
||||||
|
"name": self.meta.get("name"), |
||||||
|
"name_en": self.meta.get("name_en"), |
||||||
|
"country": self.meta.get("country"), |
||||||
|
"data_start_year": self.meta.get("data_start_year"), |
||||||
|
}, |
||||||
|
"parties": [ |
||||||
|
{"code": p.code, "name": p.name, "color": p.color, "active": p.active} |
||||||
|
for p in self.parties |
||||||
|
], |
||||||
|
"party_defaults": self.party_defaults, |
||||||
|
"activity_types": self.activity_types, |
||||||
|
"vocabulary": self.vocabulary, |
||||||
|
"urls": self.urls, |
||||||
|
"site": { |
||||||
|
**{k: v for k, v in self.site.items() if not k.endswith("_file")}, |
||||||
|
"explainer": self.read_content("explainer_file"), |
||||||
|
"limit_warning": self.read_content("limit_warning_file"), |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
def _require(data: dict, key: str) -> Any: |
||||||
|
if key not in data: |
||||||
|
raise ConfigError(f"parliament.yaml is missing the required `{key}:` section") |
||||||
|
return data[key] |
||||||
|
|
||||||
|
|
||||||
|
def load(path: Optional[Path] = None) -> Parliament: |
||||||
|
"""Read and validate a parliament configuration.""" |
||||||
|
path = Path(path or os.environ.get("PARLIAMENT_CONFIG") or _ROOT / "parliament.yaml") |
||||||
|
if not path.exists(): |
||||||
|
raise ConfigError( |
||||||
|
f"No parliament configuration at {path}. Copy parliament.yaml from the " |
||||||
|
f"repository root, or set PARLIAMENT_CONFIG to point at yours." |
||||||
|
) |
||||||
|
|
||||||
|
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} |
||||||
|
|
||||||
|
lang = Language(**_require(data, "language")) |
||||||
|
if not _FTS_CONFIG_RE.match(lang.fts_config): |
||||||
|
raise ConfigError( |
||||||
|
f"language.fts_config {lang.fts_config!r} is not a valid Postgres " |
||||||
|
f"identifier. It is interpolated into SQL, so it must match " |
||||||
|
f"{_FTS_CONFIG_RE.pattern}. List valid names with: " |
||||||
|
f"SELECT cfgname FROM pg_ts_config;" |
||||||
|
) |
||||||
|
|
||||||
|
parties = [Party(**p) for p in data.get("parties", [])] |
||||||
|
if not parties: |
||||||
|
raise ConfigError("parliament.yaml declares no parties") |
||||||
|
|
||||||
|
embeddings = Embeddings(**_require(data, "embeddings")) |
||||||
|
if embeddings.dimension <= 0: |
||||||
|
raise ConfigError("embeddings.dimension must be a positive integer") |
||||||
|
|
||||||
|
return Parliament( |
||||||
|
meta=_require(data, "parliament"), |
||||||
|
language=lang, |
||||||
|
vocabulary=data.get("vocabulary", {}), |
||||||
|
parties=parties, |
||||||
|
party_defaults=data.get("party_defaults", {"unknown_color": "#9aa5b8"}), |
||||||
|
activity_types=data.get("activity_types", {}), |
||||||
|
document_subtypes=data.get("document_subtypes", {}), |
||||||
|
decisions=data.get("decisions", {}), |
||||||
|
sessions=data.get("sessions", {}), |
||||||
|
ids=data.get("ids", {}), |
||||||
|
urls=data.get("urls", {}), |
||||||
|
sources=data.get("sources", {}), |
||||||
|
embeddings=embeddings, |
||||||
|
site=data.get("site", {}), |
||||||
|
path=path, |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
PARLIAMENT: Parliament = load() |
||||||
@ -0,0 +1,184 @@ |
|||||||
|
# Everything specific to one parliament, in one file. Nothing secret belongs here. |
||||||
|
# |
||||||
|
# This is the Swedish Riksdag configuration, shipped as the worked example. To |
||||||
|
# target a different parliament, copy this file, change the values, and write an |
||||||
|
# ingest adapter — see docs/PORTING.md. |
||||||
|
# |
||||||
|
# Override the path with PARLIAMENT_CONFIG=/etc/plenum/parliament.yaml so a |
||||||
|
# deployment's own values live outside the repository and never appear in a diff. |
||||||
|
|
||||||
|
schema_version: 1 |
||||||
|
|
||||||
|
parliament: |
||||||
|
id: se-riksdagen |
||||||
|
name: Sveriges riksdag # in the language set below |
||||||
|
name_en: Swedish Parliament |
||||||
|
country: SE |
||||||
|
chamber: unicameral # unicameral | lower | upper |
||||||
|
open_data_url: https://data.riksdagen.se |
||||||
|
data_start_year: 1993 |
||||||
|
|
||||||
|
language: |
||||||
|
# Postgres text-search configuration. Must match the database's app.fts_config |
||||||
|
# setting — the application refuses to start on a mismatch, because a silent |
||||||
|
# mismatch returns near-zero rows with no error. |
||||||
|
# See: SELECT cfgname FROM pg_ts_config; |
||||||
|
fts_config: swedish |
||||||
|
prompt_language: sv # selects prompts/<lang>/ and the answer language |
||||||
|
locale: sv-SE |
||||||
|
# Characters that must survive verbatim into search queries. Transliterating |
||||||
|
# these silently breaks matching on most Swedish terms. |
||||||
|
preserve_characters: "åäöÅÄÖ" |
||||||
|
months: |
||||||
|
januari: "01" |
||||||
|
februari: "02" |
||||||
|
mars: "03" |
||||||
|
april: "04" |
||||||
|
maj: "05" |
||||||
|
juni: "06" |
||||||
|
juli: "07" |
||||||
|
augusti: "08" |
||||||
|
september: "09" |
||||||
|
oktober: "10" |
||||||
|
november: "11" |
||||||
|
december: "12" |
||||||
|
|
||||||
|
# Domain words injected into prompts, written in `prompt_language`. These exist |
||||||
|
# because "motion", "yrkande" and "riksmöte" have no stable English equivalents; |
||||||
|
# the database columns are neutral and the country's own word lives here. |
||||||
|
vocabulary: |
||||||
|
member: riksdagsledamot |
||||||
|
member_plural: riksdagsledamöter |
||||||
|
chamber: kammaren |
||||||
|
speech: anförande |
||||||
|
speech_plural: anföranden |
||||||
|
document: motion |
||||||
|
document_plural: motioner |
||||||
|
proposal: yrkande |
||||||
|
proposal_plural: yrkanden |
||||||
|
committee: utskott |
||||||
|
constituency: valkrets |
||||||
|
session: riksmöte |
||||||
|
|
||||||
|
# Display order is list order. `active: false` keeps historical parties |
||||||
|
# renderable without offering them as a filter. |
||||||
|
parties: |
||||||
|
- {code: S, name: Socialdemokraterna, color: "#E8112d", active: true} |
||||||
|
- {code: M, name: Moderaterna, color: "#52BDEC", active: true} |
||||||
|
- {code: SD, name: Sverigedemokraterna, color: "#DDDD00", active: true} |
||||||
|
- {code: C, name: Centerpartiet, color: "#009933", active: true} |
||||||
|
- {code: V, name: Vänsterpartiet, color: "#b51a0e", active: true} |
||||||
|
- {code: KD, name: Kristdemokraterna, color: "#000077", active: true} |
||||||
|
- {code: MP, name: Miljöpartiet de gröna, color: "#83CF39", active: true} |
||||||
|
- {code: L, name: Liberalerna, color: "#006AB3", active: true} |
||||||
|
- {code: NYD, name: Ny demokrati, color: "#ffff2b", active: false} |
||||||
|
|
||||||
|
party_defaults: |
||||||
|
# Independents and unknowns. |
||||||
|
unknown_color: "#9aa5b8" |
||||||
|
# How a party code appears in rendered text, e.g. "Anna Andersson (MP)". |
||||||
|
# Used to detect and verify attributions in generated answers. |
||||||
|
code_pattern: "[A-ZÅÄÖ]{1,3}" |
||||||
|
|
||||||
|
# Keys are the values stored in talks.kammaraktivitet. Titles and descriptions |
||||||
|
# are shown in the UI and are in `prompt_language`. |
||||||
|
activity_types: |
||||||
|
kam-vo: {title: Beslut, description: "Riksdagen fattar det formella beslutet i ärendet."} |
||||||
|
bet: {title: Debatt om beslut, description: "Ledamöterna debatterar ett förslag innan beslut fattas."} |
||||||
|
kam-fs: {title: Frågestund, description: "Ministrar svarar direkt på frågor från ledamöter."} |
||||||
|
kam-ar: {title: Information från regeringen, description: "Regeringen informerar riksdagen i en aktuell fråga."} |
||||||
|
ip: {title: Interpellationsdebatt, description: "Debatt om en skriftlig fråga till en minister."} |
||||||
|
kam-sf: {title: Statsministerns frågestund, description: "Statsministern svarar på frågor från ledamöter."} |
||||||
|
sam-ou: {title: Öppen utfrågning, description: "Utskottet frågar ut experter eller ansvariga personer offentligt."} |
||||||
|
kam-ad: {title: Aktuell debatt, description: "Snabb debatt om en aktuell politisk fråga."} |
||||||
|
kam-al: {title: Allmänpolitisk debatt, description: "Ledamöterna debatterar valfria politiska ämnen."} |
||||||
|
kam-bu: {title: Budgetdebatt, description: "Debatt om regeringens budgetförslag."} |
||||||
|
kam-bp: {title: Bordläggning, description: "Ett ärende anmäls i kammaren inför fortsatt behandling."} |
||||||
|
kam-pd: {title: Partiledardebatt, description: "Partiledarna debatterar politikens stora frågor."} |
||||||
|
kam-dv: {title: Debatt med anledning av vårpropositionen, description: "Debatt om regeringens ekonomiska vårproposition."} |
||||||
|
sam-se: {title: Öppet seminarium, description: "Öppet seminarium eller samtal om ett särskilt tema."} |
||||||
|
kam-ud: {title: Utrikespolitisk debatt, description: "Debatt om Sveriges utrikespolitik."} |
||||||
|
|
||||||
|
# Glosses for values stored in the data. The values themselves are never |
||||||
|
# translated — only explained, so prompts can reason about them. |
||||||
|
document_subtypes: |
||||||
|
Enskild motion: "Motion från en eller ett fåtal ledamöter" |
||||||
|
Kommittémotion: "Motion från ett partis utskottsgrupp" |
||||||
|
Partimotion: "Motion från ett helt parti, ofta undertecknad av partiledaren" |
||||||
|
|
||||||
|
decisions: |
||||||
|
Bifall: {label: Bifall, meaning: approved} |
||||||
|
Avslag: {label: Avslag, meaning: rejected} |
||||||
|
|
||||||
|
# Annual parliamentary session. Deliberately not called "term": the European |
||||||
|
# Parliament uses that for its five-year cycle, so it would be ambiguous. |
||||||
|
sessions: |
||||||
|
label_format: "{start}/{end_short}" # 2022 -> "2022/23" |
||||||
|
starts_month: 9 |
||||||
|
first_year: 1990 |
||||||
|
|
||||||
|
# Identifier shapes, templated into prompts and tool descriptions so the model |
||||||
|
# can recognise and construct them without them being hardcoded in Python. |
||||||
|
ids: |
||||||
|
person_id: |
||||||
|
example: "0448485371626" |
||||||
|
pattern: "^[0-9]{10,13}$" |
||||||
|
description: "Riksdagen intressent-id, a numeric string" |
||||||
|
speech_id: |
||||||
|
example: "GH09116-16" |
||||||
|
pattern: "^[A-ZÅÄÖ0-9]+-[0-9]+$" |
||||||
|
description: "{source_doc_id}-{sequence}" |
||||||
|
doc_id: |
||||||
|
example: "HD02846" |
||||||
|
pattern: "^[A-ZÅÄÖ0-9]+$" |
||||||
|
description: "Document id from the open-data portal" |
||||||
|
debate_id: |
||||||
|
example: "2021-06-17:42" |
||||||
|
pattern: '^\d{4}-\d{2}-\d{2}:\d+$' |
||||||
|
description: "{date}:{index within that day}" |
||||||
|
|
||||||
|
urls: |
||||||
|
person_photo: "https://data.riksdagen.se/filarkiv/bilder/ledamot/{person_id}_192.jpg" |
||||||
|
person_page: "https://www.riksdagen.se/sv/ledamoter-partier/ledamot/_{person_id}" |
||||||
|
document_page: "https://www.riksdagen.se/sv/dokument-och-lagar/dokument/_{doc_id}" |
||||||
|
|
||||||
|
# Consumed by the ingest CLI. `adapter` names the module that knows this |
||||||
|
# source's JSON shape; everything else is data. |
||||||
|
sources: |
||||||
|
adapter: ingest.adapters.riksdagen |
||||||
|
speeches: |
||||||
|
kind: zip-dataset |
||||||
|
url_template: "https://data.riksdagen.se/dataset/anforande/anforande-{range}.json.zip" |
||||||
|
dest_dir: talks |
||||||
|
documents: |
||||||
|
kind: zip-dataset |
||||||
|
url_template: "https://data.riksdagen.se/dataset/dokument/mot-{range}.json.zip" |
||||||
|
ranges: ["1990-1997", "1998-2001", "2002-2005", "2006-2009", |
||||||
|
"2010-2013", "2014-2017", "2018-2021", "2022-2025"] |
||||||
|
dest_dir: motioner |
||||||
|
people: |
||||||
|
kind: json |
||||||
|
url: "https://data.riksdagen.se/personlista/?utformat=json" |
||||||
|
dest_dir: personer |
||||||
|
|
||||||
|
embeddings: |
||||||
|
model: qwen3-embedding # LLM_MODEL_EMBEDDING overrides |
||||||
|
# Must equal the vector(N) width of the embedding columns. Checked at startup |
||||||
|
# against the actual column type, because a mismatch fails deep inside pgvector |
||||||
|
# with an error that says nothing about configuration. |
||||||
|
dimension: 384 |
||||||
|
base_url_env: EMBEDDING_BASE_URL |
||||||
|
chunk_chars: 500 |
||||||
|
|
||||||
|
# Served to the frontend via GET /api/meta. |
||||||
|
site: |
||||||
|
title: rixdagen.se |
||||||
|
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 |
||||||
|
# Upstream ships these empty. A deployment fills them in via its own |
||||||
|
# PARLIAMENT_CONFIG file, which keeps personal contact details out of the repo. |
||||||
|
contact: |
||||||
|
email: null |
||||||
|
url: null |
||||||
Loading…
Reference in new issue