"""Report generation primitives for agent operation. Agents call these to write structured markdown reports to the reports/ directory. """ from __future__ import annotations import logging import os from datetime import datetime from typing import Any, Dict from agent_tools.database import query_pipeline_status logger = logging.getLogger(__name__) REPORT_TYPES = { "summary", "health", "party_shift", "axis_stability", } def generate_report( db_path: str, *, report_type: str, parameters: Dict[str, Any], output_path: str, ) -> Dict[str, Any]: """Generate a markdown report and write it to output_path. Args: db_path: Path to DuckDB database report_type: One of REPORT_TYPES parameters: Type-specific parameters output_path: Where to write the markdown file Returns: dict with "output_path" and "status" keys, or "error" on failure """ if report_type not in REPORT_TYPES: return { "error": f"Unknown report type '{report_type}'. Known types: {sorted(REPORT_TYPES)}", } try: content = _render_report(db_path, report_type, parameters) os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) with open(output_path, "w", encoding="utf-8") as f: f.write(content) return {"output_path": output_path, "status": "written"} except Exception as e: logger.exception("generate_report failed") return {"error": str(e)} def _render_report(db_path: str, report_type: str, parameters: Dict[str, Any]) -> str: """Render report content as markdown.""" lines = [ f"# Stemwijzer Report: {report_type.replace('_', ' ').title()}", f"", f"Generated: {datetime.now().isoformat()}", f"", ] if report_type == "summary": status = query_pipeline_status(db_path) lines.extend([ "## Pipeline Summary", f"", f"- **Motions in database:** {status.get('motion_count', 0):,}", f"- **Latest motion date:** {status.get('latest_motion_date', 'N/A')}", f"- **SVD windows computed:** {status.get('svd_window_count', 0)}", f"- **Motion embeddings:** {status.get('embedding_count', 0):,}", f"- **Overall health:** {'✅ Healthy' if status.get('healthy') else '⚠️ Needs attention'}", f"", ]) elif report_type == "health": status = query_pipeline_status(db_path) lines.extend([ "## Pipeline Health Check", f"", f"| Metric | Value | Status |", f"|--------|-------|--------|", f"| Motion count | {status.get('motion_count', 0):,} | {'✅' if status.get('motion_count', 0) > 0 else '⚠️'} |", f"| Latest motion | {status.get('latest_motion_date', 'N/A')} | {'✅' if status.get('latest_motion_date') else '⚠️'} |", f"| SVD windows | {status.get('svd_window_count', 0)} | {'✅' if status.get('svd_window_count', 0) > 0 else '⚠️'} |", f"| Embeddings | {status.get('embedding_count', 0):,} | {'✅' if status.get('embedding_count', 0) > 0 else '⚠️'} |", f"", ]) elif report_type == "party_shift": from agent_tools.analysis import analyze_party_shift party = parameters.get("party", "VVD") start = parameters.get("window_start", "2020") end = parameters.get("window_end", "2024") result = analyze_party_shift(db_path, party, start, end) if "error" in result: lines.extend(["## Party Shift Analysis", f"", f"Error: {result['error']}", f""]) else: lines.extend([ "## Party Shift Analysis", f"", f"**Party:** {result['party']}", f"**Period:** {result['window_start']} → {result['window_end']}", f"**Shift magnitude:** {result['shift']}", f"**Direction:** dx={result['direction']['dx']}, dy={result['direction']['dy']}", f"", f"### Start position", f"- Axis 1: {result['start_position']['axis_1']}", f"- Axis 2: {result['start_position']['axis_2']}", f"", f"### End position", f"- Axis 1: {result['end_position']['axis_1']}", f"- Axis 2: {result['end_position']['axis_2']}", f"", ]) elif report_type == "axis_stability": from agent_tools.analysis import analyze_axis_stability component = parameters.get("component", 1) windows = parameters.get("windows", ["2020", "2021", "2022", "2023", "2024"]) result = analyze_axis_stability(db_path, component, windows) if "error" in result: lines.extend(["## Axis Stability Analysis", f"", f"Error: {result['error']}", f""]) else: lines.extend([ "## Axis Stability Analysis", f"", f"**Component:** {result['component']}", f"**Average stability:** {result['stability']}", f"", f"### Pairwise correlations", f"", f"| From | To | Correlation |", f"|------|-----|-------------|", ]) for pair in result.get("pairwise", []): lines.append(f"| {pair['from_window']} | {pair['to_window']} | {pair['correlation']} |") lines.append("") return "\n".join(lines)