feat: agent-native refactor, SVD consistency fixes, UX cleanup, mobile support

- Refactor agent_tools to atomic primitives (24 tools, delete workflows)
- Fix SVD component score inconsistency between single-window and trajectory views
  (same PCA basis, same flip handling, same active-MP filter for current_parliament)
- Fix Dutch spelling: Huidig parliament -> Huidig parlement
- Remove all decorative emojis from UI (app.py, explorer.py, analysis tabs)
- Add dark theme matching sgeboers.nl (mint accent on dark background)
- Remove browser tab favicon and Streamlit chrome (deploy button, running status)
- Remove trajectories debug UI and EMA settings (hardcoded smooth_alpha=0.35)
- Switch layout to centered for mobile readability
- Add responsive CSS for mobile (touch targets, font sizing, overflow prevention)
- Update AGENTS.md and SYSTEM_PROMPT.md with active tool instructions
- Add compound docs for SVD consistency bug
- Update tests: 214 passed, 3 skipped
This commit is contained in:
2026-05-04 21:56:40 +02:00
parent efb3a8fbd2
commit 272d839a42
33 changed files with 854 additions and 1108 deletions
+16 -1
View File
@@ -718,16 +718,23 @@ def _get_aligned_trajectory_scores(
Uses compute_nd_axes to get PCA-projected, flip-corrected scores across all windows,
ensuring consistency with the single-window SVD components view.
Computes the global PCA basis on *all* uniform-dim windows (matching
get_aligned_party_scores) so that trajectory scores are numerically
consistent with the single-window view even when the caller passes a
subset of windows for display.
"""
from analysis.political_axis import compute_nd_axes
all_uniform_windows = get_uniform_dim_windows(db_path)
scores_by_window, _ = compute_nd_axes(
db_path, window_ids=windows, n_components=n_components
db_path, window_ids=all_uniform_windows, n_components=n_components
)
if not scores_by_window:
return {}
party_map = load_party_map(db_path)
active_mps = load_active_mps(db_path)
result: Dict[str, Dict[str, List[float]]] = {}
for window in windows:
@@ -735,6 +742,14 @@ def _get_aligned_trajectory_scores(
if not window_scores:
continue
# For current_parliament, match single-window view by filtering to
# only MPs who are still seated (active). Historical windows include
# all MPs present in that window.
if window == "current_parliament":
window_scores = {
mp: sc for mp, sc in window_scores.items() if mp in active_mps
}
party_vecs: Dict[str, List[np.ndarray]] = {}
for mp_name, scores in window_scores.items():
party = party_map.get(
+7 -6
View File
@@ -612,6 +612,7 @@ def _render_svd_time_trajectory(
return
idx = comp_sel - 1
flip = theme.get("flip", False)
party_trajectories: Dict[str, List[Tuple[str, float]]] = {}
@@ -631,6 +632,8 @@ def _render_svd_time_trajectory(
if scores and len(scores) > idx:
try:
score = float(scores[idx])
if flip:
score = -score
party_trajectories.setdefault(party, []).append((window, score))
except (ValueError, TypeError):
continue
@@ -766,15 +769,13 @@ def _render_voting_results(voting_results_json) -> None:
vote_str = str(vote).lower().strip()
by_vote.setdefault(vote_str, []).append(str(actor))
vote_order = ["voor", "tegen", "onthouden", "afwezig"]
vote_emoji = {"voor": "", "tegen": "", "onthouden": "🟡", "afwezig": ""}
rows_shown = False
for v in vote_order + [k for k in by_vote if k not in vote_order]:
actors = by_vote.get(v)
if not actors:
continue
emoji = vote_emoji.get(v, "▪️")
st.markdown(
f"**{emoji} {v.capitalize()}** ({len(actors)}): {', '.join(sorted(actors))}"
f"**{v.capitalize()}** ({len(actors)}): {', '.join(sorted(actors))}"
)
rows_shown = True
if not rows_shown:
@@ -784,7 +785,7 @@ def _render_voting_results(voting_results_json) -> None:
def _add_y_direction_annotations(fig: go.Figure) -> None:
"""Add Progressief / Conservatief labels above and below the Y axis."""
"""Add Progressief / Conservatief labels above and below the Y axis."""
common = dict(
xref="paper",
yref="paper",
@@ -792,5 +793,5 @@ def _add_y_direction_annotations(fig: go.Figure) -> None:
showarrow=False,
font=dict(size=11, color="#666666"),
)
fig.add_annotation(**common, y=1.02, text="▲ Progressief", xanchor="center")
fig.add_annotation(**common, y=-0.06, text="▼ Conservatief", xanchor="center")
fig.add_annotation(**common, y=1.02, text="Progressief", xanchor="center")
fig.add_annotation(**common, y=-0.06, text="Conservatief", xanchor="center")
+2 -2
View File
@@ -74,12 +74,12 @@ def build_browser_tab(db_path: str, show_rejected: bool) -> None:
st.markdown(f"### {row.get('title') or 'Onbekend'}")
date_str = row["date"].strftime("%d %b %Y") if pd.notna(row["date"]) else "?"
st.caption(
f"📅 {date_str} | 🔥 Controverse: {row.get('controversy_score', 0):.2f}"
f"{date_str} | Controverse: {row.get('controversy_score', 0):.2f}"
)
url = row.get("url")
if url and str(url).startswith("http"):
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({url})")
st.markdown(f"[Bekijk op Tweede Kamer]({url})")
st.markdown("**Stemuitslag:**")
_render_voting_results(row.get("voting_results"))
-2
View File
@@ -51,8 +51,6 @@ def build_compass_tab(db_path: str, window_size: str) -> None:
def _window_label(w: str) -> str:
if w == "current_parliament":
return "Huidig parlement"
if w in _SPARSE_YEARS:
return f"{w} ⚠️"
return w
col1, col2 = st.columns([3, 1])
+8 -10
View File
@@ -39,7 +39,7 @@ def build_svd_components_tab(db_path: str) -> None:
Components 1-2 use aligned PCA positions (consistent with compass).
Components 3-10 use raw SVD scores.
"""
st.subheader("🔬 SVD Assen — politieke polarisatiethema's")
st.subheader("SVD Assen — politieke polarisatiethema's")
st.markdown(
"Elke SVD-as representeert een latente politieke dimensie afgeleid uit stempatronen "
"van alle Kamerleden. De top-10 moties per as zijn uniek (geen overlap) en illustreren "
@@ -166,7 +166,7 @@ def build_svd_components_tab(db_path: str) -> None:
def _svd_window_label(w: str) -> str:
if w == "current_parliament":
return "Huidig parliament"
return "Huidig parlement"
return w
with col1:
@@ -321,11 +321,9 @@ def build_svd_components_tab(db_path: str) -> None:
if flip:
left_pole, right_pole = pos_pole, neg_pole
left_motions, right_motions = pos_motions, neg_motions
left_arrow, right_arrow = "", ""
else:
left_pole, right_pole = neg_pole, pos_pole
left_motions, right_motions = neg_motions, pos_motions
left_arrow, right_arrow = "", ""
lcol, rcol = st.columns(2)
@@ -334,16 +332,16 @@ def build_svd_components_tab(db_path: str) -> None:
for m in left_motions:
mid = m.get("motion_id")
raw_title = m.get("title") or f"Motie #{mid}"
with st.expander(f"{left_arrow} {raw_title}"):
with st.expander(raw_title):
row = motion_details.get(int(mid)) if mid is not None else None
if row:
try:
date_str = str(row[2])[:10]
except Exception:
date_str = "?"
st.caption(f"📅 {date_str} | {row[3] or ''}")
st.caption(f"{date_str} | {row[3] or ''}")
if row[4] and str(row[4]).startswith("http"):
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({row[4]})")
st.markdown(f"[Bekijk op Tweede Kamer]({row[4]})")
if row[5]:
with st.expander("Toon volledige tekst"):
st.write(row[5])
@@ -356,16 +354,16 @@ def build_svd_components_tab(db_path: str) -> None:
for m in right_motions:
mid = m.get("motion_id")
raw_title = m.get("title") or f"Motie #{mid}"
with st.expander(f"{right_arrow} {raw_title}"):
with st.expander(raw_title):
row = motion_details.get(int(mid)) if mid is not None else None
if row:
try:
date_str = str(row[2])[:10]
except Exception:
date_str = "?"
st.caption(f"📅 {date_str} | {row[3] or ''}")
st.caption(f"{date_str} | {row[3] or ''}")
if row[4] and str(row[4]).startswith("http"):
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({row[4]})")
st.markdown(f"[Bekijk op Tweede Kamer]({row[4]})")
if row[5]:
with st.expander("Toon volledige tekst"):
st.write(row[5])
+1 -1
View File
@@ -18,7 +18,7 @@ def build_mp_quiz_tab(db_path: str) -> None:
- if multiple candidates remain, call choose_discriminating_motions to pick next question
- stop when unique MP found or no discriminating motions remain
"""
st.subheader("🧑‍⚖️ Welk tweede kamerlid ben jij?")
st.subheader("Welk tweede kamerlid ben jij?")
st.markdown(
"Beantwoord een paar eenvoudige ja/nee/onthoud vragen over moties om te zien welk Kamerlid het meest op jou lijkt."
)
+2 -2
View File
@@ -56,7 +56,7 @@ def build_search_tab(db_path: str, show_rejected: bool) -> None:
title = row.get("title") or f"Motie #{row['id']}"
date_str = row["date"].strftime("%d %b %Y") if pd.notna(row["date"]) else "?"
controversy = row.get("controversy_score") or 0
with st.expander(f"**{title}** — {date_str} 🔥 {controversy:.2f}"):
with st.expander(f"**{title}** — {date_str}{controversy:.2f}"):
cols = st.columns(3)
cols[0].metric("Controverse", f"{controversy:.2f}")
cols[1].metric("Marge", f"{row.get('winning_margin', 0):.2f}")
@@ -66,7 +66,7 @@ def build_search_tab(db_path: str, show_rejected: bool) -> None:
url = row.get("url")
if url and str(url).startswith("http"):
st.markdown(f"[🔗 Bekijk op Tweede Kamer]({url})")
st.markdown(f"[Bekijk op Tweede Kamer]({url})")
sim = explorer_data.query_similar(db_path, int(row["id"]), top_k=5)
if not sim.empty:
+2 -106
View File
@@ -516,57 +516,7 @@ def build_trajectories_tab(db_path: str, window_size: str) -> None:
st.plotly_chart(fig, use_container_width=True)
return
try:
debug_checkbox = False
try:
debug_checkbox = st.checkbox(
"Enable trajectories diagnostics (show extra info)",
value=get_debug_trajectories_enabled(),
)
except Exception:
debug_checkbox = get_debug_trajectories_enabled()
if debug_checkbox:
try:
with st.expander(
"DEBUG: Trajectories data (showing diagnostics)", expanded=False
):
st.write("windows (count):", len(windows))
st.write("windows sample:", windows[:10])
st.write("party_map entries:", len(party_map))
st.write("parties with centroids:", len(all_parties_sorted))
st.write("default_parties:", default_parties)
st.write("selected_parties:", selected_parties)
st.write("min_mps setting:", 3)
sample = {
p: len(centroids.get(p, {}))
for p in list(all_parties_sorted)[:8]
}
st.write("sample centroid window counts per party:", sample)
except Exception:
pass
except Exception:
pass
smoothing_method = st.selectbox(
"Smoothing methode",
options=["EMA", "Spline", "None"],
index=0,
help="EMA = exponential moving average; Spline = low-degree polynomial spline fit; None = raw centroids",
)
smooth_alpha = 1.0
if smoothing_method == "EMA":
smooth_alpha = st.slider(
"Glad maken (EMA-\u03b1)",
min_value=0.1,
max_value=1.0,
value=0.35,
step=0.05,
help=(
"\u03b1=1.0 toont de ruwe data; lagere waarden maken de lijn gladder. "
"Standaard 0.35 voor een goed evenwicht tussen detail en ruis."
),
)
smooth_alpha = 0.35
def _spline_smooth(values: List[float]) -> List[float]:
n = len(values)
@@ -712,63 +662,9 @@ def build_trajectories_tab(db_path: str, window_size: str) -> None:
"sample_size": len(sample_mps),
}
if trace_count == 0:
st.info("📊 **Geen trajecten getekend**")
with st.expander("🔍 Diagnostische informatie"):
st.write("**Data status:**")
st.write(
f"- Positie vensters: {len(positions_by_window) if positions_by_window else 0}"
)
st.write(f"- Party mappings: {len(party_map) if party_map else 0}")
st.write(
f"- Geselecteerde partijen: {len(selected_parties) if selected_parties else 0}"
)
if "centroid_diagnostics" in locals():
st.write("**Centroid berekening:**")
st.write(
f"- Partijen met posities: {len(centroid_diagnostics.get('parties_with_positions', []))}"
)
st.write(
f"- Partijen met alleen NaN: {len(centroid_diagnostics.get('parties_all_nan', []))}"
)
st.write("\n**Mogelijke oorzaken:**")
st.write("1. Geen SVD vectoren berekend voor de geselecteerde vensters")
st.write("2. MP namen in posities komen niet overeen met party_map")
st.write("3. Alle geselecteerde partijen hebben te weinig MPs (< 5)")
if st.button("🔧 Database diagnostiek uitvoeren"):
with st.spinner("Bezig met diagnostiek..."):
from scripts.diagnose_trajectories_cli import (
run as diagnose_trajectories,
)
results = diagnose_trajectories(db_path)
st.json(results)
st.info("**Geen trajecten getekend**")
else:
try:
st.info(
f"[DEBUG] trace_count={trace_count}, fig data count={len(fig.data)}, layout title={fig.layout.title.text if fig.layout.title else 'none'}"
)
except Exception:
pass
try:
logging.getLogger(__name__).debug(
"[TRAJ DEBUG] About to render plotly chart — trace_count=%d, banner=%s, fig has %d traces",
trace_count,
banner_text,
len(fig.data),
)
st.plotly_chart(fig, use_container_width=True)
except Exception as e:
st.error(f"Trajectories rendering failed: {e}")
if get_debug_trajectories_enabled():
try:
st.json(_last_trajectories_diagnostics)
except Exception:
st.text_area(
"Trajectories diagnostics (JSON failed)",
json.dumps(_last_trajectories_diagnostics, default=str),
height=240,
)