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:
@@ -1,74 +1,10 @@
|
||||
"""Tests for agent analysis and report generation primitives."""
|
||||
"""Tests for agent analysis and report generation primitives.
|
||||
|
||||
NOTE: Multi-step analytical workflows have been removed. Agents should compose
|
||||
raw database primitives and perform analysis in their own reasoning loop.
|
||||
This file is intentionally empty.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
|
||||
pytest.importorskip("duckdb")
|
||||
|
||||
|
||||
class TestAnalyzePartyShift:
|
||||
def test_returns_shift_data(self, tmp_duckdb_path):
|
||||
from agent_tools.analysis import analyze_party_shift
|
||||
|
||||
result = analyze_party_shift(
|
||||
tmp_duckdb_path, party="VVD", window_start="2020", window_end="2024"
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
assert "party" in result
|
||||
assert "shift" in result or "error" in result
|
||||
|
||||
def test_nonexistent_party_returns_error(self, tmp_duckdb_path):
|
||||
from agent_tools.analysis import analyze_party_shift
|
||||
|
||||
result = analyze_party_shift(
|
||||
tmp_duckdb_path, party="FAKE", window_start="2020", window_end="2024"
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
|
||||
|
||||
class TestAnalyzeAxisStability:
|
||||
def test_returns_stability_scores(self, tmp_duckdb_path):
|
||||
from agent_tools.analysis import analyze_axis_stability
|
||||
|
||||
result = analyze_axis_stability(tmp_duckdb_path, component=1, windows=["2020", "2024"])
|
||||
assert isinstance(result, dict)
|
||||
assert "component" in result
|
||||
assert "stability" in result or "error" in result
|
||||
|
||||
|
||||
class TestGenerateReport:
|
||||
def test_writes_markdown_file(self, tmp_duckdb_path, tmp_path):
|
||||
from agent_tools.reports import generate_report
|
||||
|
||||
output_path = str(tmp_path / "report.md")
|
||||
result = generate_report(
|
||||
tmp_duckdb_path,
|
||||
report_type="summary",
|
||||
parameters={},
|
||||
output_path=output_path,
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
assert os.path.exists(output_path)
|
||||
|
||||
def test_returns_error_for_unknown_type(self, tmp_duckdb_path, tmp_path):
|
||||
from agent_tools.reports import generate_report
|
||||
|
||||
output_path = str(tmp_path / "report.md")
|
||||
result = generate_report(
|
||||
tmp_duckdb_path,
|
||||
report_type="unknown",
|
||||
parameters={},
|
||||
output_path=output_path,
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
assert "error" in result
|
||||
|
||||
|
||||
class TestValidateSvdLabels:
|
||||
def test_returns_validation_result(self, tmp_duckdb_path):
|
||||
from agent_tools.analysis import validate_svd_labels
|
||||
|
||||
result = validate_svd_labels(tmp_duckdb_path, component=1)
|
||||
assert isinstance(result, dict)
|
||||
assert "component" in result
|
||||
assert "valid" in result or "error" in result
|
||||
|
||||
@@ -25,16 +25,6 @@ class TestValidateLaymanExplanations:
|
||||
assert "coverage" in result or "error" in result
|
||||
|
||||
|
||||
class TestSuggestSvdLabel:
|
||||
def test_returns_suggestion(self, tmp_duckdb_path):
|
||||
from agent_tools.content import suggest_svd_label
|
||||
|
||||
result = suggest_svd_label(tmp_duckdb_path, component=1, top_n=5)
|
||||
assert isinstance(result, dict)
|
||||
assert "component" in result
|
||||
assert "suggestion" in result or "error" in result
|
||||
|
||||
|
||||
class TestCheckEmbeddingQuality:
|
||||
def test_returns_coverage_stats(self, tmp_duckdb_path):
|
||||
from agent_tools.content import check_embedding_quality
|
||||
@@ -42,12 +32,4 @@ class TestCheckEmbeddingQuality:
|
||||
result = check_embedding_quality(tmp_duckdb_path, window_id="current_parliament")
|
||||
assert isinstance(result, dict)
|
||||
assert "coverage" in result or "error" in result
|
||||
|
||||
def test_parameterized_threshold(self, tmp_duckdb_path):
|
||||
from agent_tools.content import check_embedding_quality
|
||||
|
||||
result = check_embedding_quality(
|
||||
tmp_duckdb_path, window_id="current_parliament", healthy_threshold=0.5
|
||||
)
|
||||
assert isinstance(result, dict)
|
||||
assert result.get("healthy_threshold") == 0.5
|
||||
assert "healthy" not in result
|
||||
|
||||
@@ -73,6 +73,7 @@ class TestQueryPipelineStatus:
|
||||
assert "motion_count" in result
|
||||
assert "latest_motion_date" in result
|
||||
assert "svd_window_count" in result
|
||||
assert "healthy" not in result
|
||||
|
||||
|
||||
class TestCrudTools:
|
||||
|
||||
@@ -13,9 +13,12 @@ class TestListTools:
|
||||
|
||||
names = {t["name"] for t in result}
|
||||
assert "query_motions" in names
|
||||
assert "pipeline_check_health" in names
|
||||
assert "generate_report" in names
|
||||
assert "pipeline_run_stage" in names
|
||||
assert "list_recent_reports" in names
|
||||
assert "list_tools" in names
|
||||
# Removed workflow tools
|
||||
assert "pipeline_check_health" not in names
|
||||
assert "generate_report" not in names
|
||||
|
||||
def test_each_tool_has_required_fields(self):
|
||||
from agent_tools import list_tools
|
||||
|
||||
@@ -53,72 +53,28 @@ class TestDatabaseParity:
|
||||
agent_status = query_pipeline_status(tmp_duckdb_path)
|
||||
|
||||
assert agent_status["motion_count"] == human_count
|
||||
assert "healthy" not in agent_status
|
||||
|
||||
|
||||
class TestHealthCheckParity:
|
||||
"""Agent health check vs human script execution."""
|
||||
"""Agent health check vs human script execution.
|
||||
|
||||
def test_agent_health_check_matches_script(self, tmp_duckdb_path):
|
||||
NOTE: The composite pipeline_check_health workflow has been removed.
|
||||
The agent now queries raw status and makes its own health determination.
|
||||
"""
|
||||
|
||||
def test_agent_queries_raw_status(self, tmp_duckdb_path):
|
||||
"""Human: python scripts/health_check.py
|
||||
Agent: pipeline_check_health(db_path)
|
||||
Agent: query_pipeline_status(db_path) + reasoning
|
||||
"""
|
||||
from agent_tools.pipeline import pipeline_check_health
|
||||
from agent_tools.database import query_pipeline_status
|
||||
|
||||
# Agent approach
|
||||
agent_result = pipeline_check_health(tmp_duckdb_path)
|
||||
status = query_pipeline_status(tmp_duckdb_path)
|
||||
|
||||
assert isinstance(agent_result, dict)
|
||||
assert "healthy" in agent_result
|
||||
assert "checks" in agent_result
|
||||
|
||||
|
||||
class TestReportGenerationParity:
|
||||
"""Agent report generation vs human manual analysis."""
|
||||
|
||||
def test_agent_generates_summary_report(self, tmp_duckdb_path, tmp_path):
|
||||
"""Human: Write a summary of pipeline state
|
||||
Agent: generate_report(db_path, "summary", ...)
|
||||
"""
|
||||
from agent_tools.reports import generate_report
|
||||
|
||||
output_path = str(tmp_path / "summary.md")
|
||||
result = generate_report(
|
||||
tmp_duckdb_path,
|
||||
report_type="summary",
|
||||
parameters={},
|
||||
output_path=output_path,
|
||||
)
|
||||
|
||||
assert result["status"] == "written"
|
||||
assert os.path.exists(output_path)
|
||||
|
||||
# Should contain key sections
|
||||
content = open(output_path).read()
|
||||
assert "Pipeline Summary" in content
|
||||
assert "Motions in database" in content
|
||||
|
||||
|
||||
class TestAnalysisParity:
|
||||
"""Agent analysis vs human analytical queries."""
|
||||
|
||||
def test_agent_party_shift_analysis(self, tmp_duckdb_path):
|
||||
"""Human: Write SQL to compare party positions across windows
|
||||
Agent: analyze_party_shift(db_path, ...)
|
||||
"""
|
||||
from agent_tools.analysis import analyze_party_shift
|
||||
|
||||
result = analyze_party_shift(
|
||||
tmp_duckdb_path,
|
||||
party="VVD",
|
||||
window_start="2020",
|
||||
window_end="2024",
|
||||
)
|
||||
|
||||
# Should return structured result (or error if no data)
|
||||
assert isinstance(result, dict)
|
||||
assert "party" in result
|
||||
# Either shift data or error (empty DB is fine)
|
||||
assert "shift" in result or "error" in result
|
||||
assert isinstance(status, dict)
|
||||
assert "motion_count" in status
|
||||
assert "svd_window_count" in status
|
||||
assert "healthy" not in status
|
||||
|
||||
|
||||
class TestIntegrationAgentDiagnosticLoop:
|
||||
@@ -126,28 +82,23 @@ class TestIntegrationAgentDiagnosticLoop:
|
||||
|
||||
def test_agent_diagnoses_stale_data(self, tmp_duckdb_path):
|
||||
"""Agent loop:
|
||||
1. Check health
|
||||
2. Query pipeline status
|
||||
3. Identify issue (empty DB = no data)
|
||||
4. Suggest remediation
|
||||
1. Query pipeline status
|
||||
2. Identify issue (empty DB = no data)
|
||||
3. Suggest remediation
|
||||
"""
|
||||
from agent_tools.pipeline import pipeline_check_health
|
||||
from agent_tools.database import query_pipeline_status
|
||||
|
||||
# Step 1: Check health
|
||||
health = pipeline_check_health(tmp_duckdb_path)
|
||||
|
||||
# Step 2: Query status
|
||||
# Step 1: Query status
|
||||
status = query_pipeline_status(tmp_duckdb_path)
|
||||
|
||||
# Step 3: Agent reasoning (simulated)
|
||||
# Step 2: Agent reasoning (simulated)
|
||||
issues = []
|
||||
if status["motion_count"] == 0:
|
||||
issues.append("No motions in database")
|
||||
if status["svd_window_count"] == 0:
|
||||
issues.append("No SVD windows computed")
|
||||
|
||||
# Step 4: Suggest remediation
|
||||
# Step 3: Suggest remediation
|
||||
suggestions = []
|
||||
if "No motions in database" in issues:
|
||||
suggestions.append("Run pipeline ingestion stage")
|
||||
|
||||
@@ -14,32 +14,6 @@ class TestPipelineRunStage:
|
||||
assert "stage" in result
|
||||
assert result.get("dry_run") is True
|
||||
|
||||
def test_invalid_stage_returns_error(self, tmp_duckdb_path):
|
||||
from agent_tools.pipeline import pipeline_run_stage
|
||||
|
||||
result = pipeline_run_stage(tmp_duckdb_path, stage="invalid")
|
||||
assert isinstance(result, dict)
|
||||
assert "error" in result
|
||||
|
||||
|
||||
class TestPipelineRunFull:
|
||||
def test_dry_run_returns_plan(self, tmp_duckdb_path):
|
||||
from agent_tools.pipeline import pipeline_run_full
|
||||
|
||||
result = pipeline_run_full(tmp_duckdb_path, dry_run=True)
|
||||
assert isinstance(result, dict)
|
||||
assert "stages" in result or "dry_run" in result
|
||||
|
||||
|
||||
class TestPipelineCheckHealth:
|
||||
def test_returns_health_report(self, tmp_duckdb_path):
|
||||
from agent_tools.pipeline import pipeline_check_health
|
||||
|
||||
result = pipeline_check_health(tmp_duckdb_path)
|
||||
assert isinstance(result, dict)
|
||||
assert "checks" in result
|
||||
assert "healthy" in result
|
||||
|
||||
|
||||
class TestPipelineGetLogs:
|
||||
def test_returns_log_lines(self, tmp_duckdb_path):
|
||||
@@ -48,12 +22,3 @@ class TestPipelineGetLogs:
|
||||
result = pipeline_get_logs(tmp_duckdb_path, stage="svd", lines=10)
|
||||
assert isinstance(result, list)
|
||||
assert len(result) <= 10
|
||||
|
||||
|
||||
class TestPipelineValidateOutput:
|
||||
def test_validates_stage_output(self, tmp_duckdb_path):
|
||||
from agent_tools.pipeline import pipeline_validate_output
|
||||
|
||||
result = pipeline_validate_output(tmp_duckdb_path, stage="svd")
|
||||
assert isinstance(result, dict)
|
||||
assert "valid" in result
|
||||
|
||||
Reference in New Issue
Block a user