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