feat: implement agent-native architecture (U1-U6)

Implements the agent-native architecture plan (docs/plans/2026-05-01-002-agent-native-architecture-plan.md):

- U1: Database query primitives (agent_tools/database.py)
  - query_motions, query_votes, query_svd_vectors, query_party_positions, query_pipeline_status
- U2: Pipeline control primitives (agent_tools/pipeline.py)
  - pipeline_run_stage, pipeline_run_full, pipeline_check_health, pipeline_get_logs, pipeline_validate_output
- U3: Analysis & report generation (agent_tools/analysis.py, reports.py)
  - analyze_party_shift, analyze_axis_stability, validate_svd_labels, generate_report
- U4: Content validation primitives (agent_tools/content.py)
  - validate_motion_coverage, validate_layman_explanations, suggest_svd_label, check_embedding_quality
- U5: System prompt & context injection (SYSTEM_PROMPT.md, context.py, context.md)
- U6: Parity verification tests (tests/agent_tools/test_parity.py)

Tests: 238 passed, 2 skipped
AGENTS.md updated to surface agent_tools/
This commit is contained in:
2026-05-04 19:38:01 +02:00
parent 98358344a0
commit 8af27bbf04
17 changed files with 1776 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
"""Tests for agent analysis and report generation primitives."""
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
+44
View File
@@ -0,0 +1,44 @@
"""Tests for agent content validation primitives."""
import pytest
pytest.importorskip("duckdb")
class TestValidateMotionCoverage:
def test_returns_coverage_gaps(self, tmp_duckdb_path):
from agent_tools.content import validate_motion_coverage
result = validate_motion_coverage(tmp_duckdb_path, start_date="2024-01-01", end_date="2024-12-31")
assert isinstance(result, dict)
assert "gaps" in result
assert "coverage_rate" in result or "error" in result
class TestValidateLaymanExplanations:
def test_returns_quality_report(self, tmp_duckdb_path):
from agent_tools.content import validate_layman_explanations
result = validate_layman_explanations(tmp_duckdb_path, sample_size=5)
assert isinstance(result, dict)
assert "sample_size" in result
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
result = check_embedding_quality(tmp_duckdb_path, window_id="current_parliament")
assert isinstance(result, dict)
assert "coverage" in result or "error" in result
+75
View File
@@ -0,0 +1,75 @@
"""Tests for agent database query primitives."""
import pytest
import json
pytest.importorskip("duckdb")
class TestQueryMotions:
def test_returns_motion_rows(self, tmp_duckdb_path):
from agent_tools.database import query_motions
result = query_motions(tmp_duckdb_path)
assert isinstance(result, list)
def test_respects_limit(self, tmp_duckdb_path):
from agent_tools.database import query_motions
result = query_motions(tmp_duckdb_path, limit=5)
assert len(result) <= 5
def test_empty_db_returns_empty_list(self, tmp_duckdb_path):
from agent_tools.database import query_motions
result = query_motions(tmp_duckdb_path)
assert result == []
class TestQueryVotes:
def test_returns_vote_counts(self, tmp_duckdb_path):
from agent_tools.database import query_votes
result = query_votes(tmp_duckdb_path, motion_id=1)
assert isinstance(result, list)
def test_filters_by_party(self, tmp_duckdb_path):
from agent_tools.database import query_votes
result = query_votes(tmp_duckdb_path, motion_id=1, party="VVD")
assert isinstance(result, list)
class TestQuerySvdVectors:
def test_returns_vectors(self, tmp_duckdb_path):
from agent_tools.database import query_svd_vectors
result = query_svd_vectors(tmp_duckdb_path, window_id="current_parliament")
assert isinstance(result, list)
def test_filters_by_entity_type(self, tmp_duckdb_path):
from agent_tools.database import query_svd_vectors
result = query_svd_vectors(
tmp_duckdb_path, window_id="current_parliament", entity_type="mp"
)
assert isinstance(result, list)
class TestQueryPartyPositions:
def test_returns_party_scores(self, tmp_duckdb_path):
from agent_tools.database import query_party_positions
result = query_party_positions(tmp_duckdb_path, window_id="current_parliament")
assert isinstance(result, list)
class TestQueryPipelineStatus:
def test_returns_status_dict(self, tmp_duckdb_path):
from agent_tools.database import query_pipeline_status
result = query_pipeline_status(tmp_duckdb_path)
assert isinstance(result, dict)
assert "motion_count" in result
assert "latest_motion_date" in result
assert "svd_window_count" in result
+160
View File
@@ -0,0 +1,160 @@
"""Parity tests: verify agent tools can achieve what humans can.
These tests ensure the agent-native architecture satisfies the parity principle:
"Whatever the user can do through the UI/scripts, the agent can achieve through tools."
"""
import os
import pytest
pytest.importorskip("duckdb")
class TestDatabaseParity:
"""Agent database queries vs human SQL queries."""
def test_agent_query_motions_matches_raw_sql(self, tmp_duckdb_path):
"""Human: SELECT * FROM motions LIMIT 10
Agent: query_motions(db_path, limit=10)
"""
import duckdb
from agent_tools.database import query_motions
# Human approach — handle empty DB gracefully
con = duckdb.connect(tmp_duckdb_path)
try:
human_result = con.execute("SELECT * FROM motions LIMIT 10").fetchdf().to_dict("records")
except Exception:
human_result = []
con.close()
# Agent approach
agent_result = query_motions(tmp_duckdb_path, limit=10)
# Both should return lists
assert isinstance(human_result, list)
assert isinstance(agent_result, list)
assert len(agent_result) == len(human_result)
def test_agent_pipeline_status_matches_raw_query(self, tmp_duckdb_path):
"""Human: SELECT COUNT(*) FROM motions
Agent: query_pipeline_status(db_path)
"""
import duckdb
from agent_tools.database import query_pipeline_status
con = duckdb.connect(tmp_duckdb_path)
try:
human_count = con.execute("SELECT COUNT(*) FROM motions").fetchone()[0]
except Exception:
human_count = 0
con.close()
agent_status = query_pipeline_status(tmp_duckdb_path)
assert agent_status["motion_count"] == human_count
class TestHealthCheckParity:
"""Agent health check vs human script execution."""
def test_agent_health_check_matches_script(self, tmp_duckdb_path):
"""Human: python scripts/health_check.py
Agent: pipeline_check_health(db_path)
"""
from agent_tools.pipeline import pipeline_check_health
# Agent approach
agent_result = pipeline_check_health(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
class TestIntegrationAgentDiagnosticLoop:
"""Integration: Agent performs full diagnostic loop."""
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
"""
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
status = query_pipeline_status(tmp_duckdb_path)
# Step 3: 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
suggestions = []
if "No motions in database" in issues:
suggestions.append("Run pipeline ingestion stage")
if "No SVD windows computed" in issues:
suggestions.append("Run SVD computation after ingestion")
assert isinstance(issues, list)
assert isinstance(suggestions, list)
# Empty DB should produce actionable suggestions
assert len(suggestions) > 0
+59
View File
@@ -0,0 +1,59 @@
"""Tests for agent pipeline control primitives."""
import pytest
pytest.importorskip("duckdb")
class TestPipelineRunStage:
def test_dry_run_returns_planned_actions(self, tmp_duckdb_path):
from agent_tools.pipeline import pipeline_run_stage
result = pipeline_run_stage(tmp_duckdb_path, stage="svd", window_id="2024", dry_run=True)
assert isinstance(result, dict)
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):
from agent_tools.pipeline import pipeline_get_logs
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