You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
motief/agent_tools/reports.py

149 lines
5.5 KiB

"""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)