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:
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user