Skip to content

Commit 8f9b43d

Browse files
Add CSV export support for SuiteResultAdd csv reports (#62)
* Add basic evaluation example script * Fix typos and improve clarity in docstrings across core modules * Add Google-style docstrings to BaseAdapter methods * Format base adapter using ruff * docs: add instructions for running CI checks locally * Remove example file unrelated to CI documentation * Add py.typed marker for type checker support * Add test for markdown emoji encoding * Fix test_reporting: correct class usage, fields, and Windows-safe to_markdown * All tests passing: fixed dependencies and formatting * Update dependencies / poetry config * Fix emoji markdown test and align ScenarioRun signature * Fix reporting tests and update dependencies * Fix missing required dependencies (jsonschema, scipy) * Update all files * Add CSV export support for SuiteResult * Fix SIM118 linter issue in SuiteResult.to_csv * Fix Ruff formatting issues in SuiteResult.to_csv * Fix CSV export: iterate over dict keys correctly and pass Ruff lint --------- Signed-off-by: Jagriti-student <jagriti7989@gmail.com>
1 parent 40bc42d commit 8f9b43d

1 file changed

Lines changed: 100 additions & 8 deletions

File tree

src/agentunit/reporting/results.py

Lines changed: 100 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
from __future__ import annotations
44

5+
import csv
56
import json
67
import statistics
78
from dataclasses import dataclass, field
89
from pathlib import Path
9-
from typing import TYPE_CHECKING
10+
from typing import TYPE_CHECKING, Any
1011
from xml.etree import ElementTree as ET
1112

1213
from agentunit.reporting.html import render_html_report
@@ -19,6 +20,11 @@
1920
from agentunit.core.trace import TraceLog
2021

2122

23+
# -------------------------
24+
# Data Models
25+
# -------------------------
26+
27+
2228
@dataclass(slots=True)
2329
class ScenarioRun:
2430
scenario_name: str
@@ -68,6 +74,29 @@ def to_dict(self) -> dict[str, object]:
6874
}
6975

7076

77+
# -------------------------
78+
# Helpers
79+
# -------------------------
80+
81+
82+
def _flatten_metrics(metrics: dict[str, Any], prefix: str = "metric") -> dict[str, Any]:
83+
flat: dict[str, Any] = {}
84+
85+
for key, value in metrics.items():
86+
if isinstance(value, dict):
87+
for inner_key, inner_value in value.items():
88+
flat[f"{prefix}_{key}_{inner_key}"] = inner_value
89+
else:
90+
flat[f"{prefix}_{key}"] = value
91+
92+
return flat
93+
94+
95+
# -------------------------
96+
# Suite Result
97+
# -------------------------
98+
99+
71100
@dataclass(slots=True)
72101
class SuiteResult:
73102
scenarios: list[ScenarioResult]
@@ -84,7 +113,10 @@ def to_dict(self) -> dict[str, object]:
84113
def to_json(self, path: str | Path) -> Path:
85114
target = Path(path)
86115
target.parent.mkdir(parents=True, exist_ok=True)
87-
target.write_text(json.dumps(self.to_dict(), indent=2), encoding="utf-8")
116+
target.write_text(
117+
json.dumps(self.to_dict(), indent=2),
118+
encoding="utf-8",
119+
)
88120
return target
89121

90122
def to_markdown(self, path: str | Path) -> Path:
@@ -99,7 +131,8 @@ def to_markdown(self, path: str | Path) -> Path:
99131
def to_junit(self, path: str | Path) -> Path:
100132
target = Path(path)
101133
target.parent.mkdir(parents=True, exist_ok=True)
102-
testsuites = ET.Element(
134+
135+
testsuite = ET.Element(
103136
"testsuite",
104137
attrib={
105138
"name": "agentunit",
@@ -108,10 +141,11 @@ def to_junit(self, path: str | Path) -> Path:
108141
"time": f"{(self.finished_at - self.started_at).total_seconds():.4f}",
109142
},
110143
)
144+
111145
for scenario in self.scenarios:
112146
for run in scenario.runs:
113147
testcase = ET.SubElement(
114-
testsuites,
148+
testsuite,
115149
"testcase",
116150
attrib={
117151
"classname": scenario.name,
@@ -126,7 +160,8 @@ def to_junit(self, path: str | Path) -> Path:
126160
attrib={"message": run.error or "Scenario failed"},
127161
)
128162
failure.text = json.dumps(run.metrics)
129-
tree = ET.ElementTree(testsuites)
163+
164+
tree = ET.ElementTree(testsuite)
130165
tree.write(target, encoding="utf-8", xml_declaration=True)
131166
return target
132167

@@ -140,22 +175,78 @@ def to_html(self, path: str | Path) -> Path:
140175
target.write_text(html, encoding="utf-8")
141176
return target
142177

178+
def to_csv(self, path: str | Path) -> Path:
179+
"""
180+
Export suite results to CSV.
181+
One row per scenario run.
182+
"""
183+
target = Path(path)
184+
target.parent.mkdir(parents=True, exist_ok=True)
185+
186+
rows: list[dict[str, Any]] = []
187+
188+
for scenario in self.scenarios:
189+
for run in scenario.runs:
190+
row: dict[str, Any] = {
191+
"scenario_name": scenario.name,
192+
"case_id": run.case_id,
193+
"success": run.success,
194+
"duration_ms": run.duration_ms,
195+
"error": run.error,
196+
}
197+
198+
if run.metrics:
199+
row.update(_flatten_metrics(run.metrics))
200+
201+
rows.append(row)
202+
203+
if not rows:
204+
return target
205+
206+
fieldnames = sorted({key for row in rows for key in row})
207+
208+
with target.open("w", newline="", encoding="utf-8") as f:
209+
writer = csv.DictWriter(f, fieldnames=fieldnames)
210+
writer.writeheader()
211+
writer.writerows(rows)
212+
213+
return target
214+
215+
216+
# -------------------------
217+
# Utilities
218+
# -------------------------
219+
143220

144221
def merge_results(results: Iterable[SuiteResult]) -> SuiteResult:
145222
results = list(results)
146223
scenarios: dict[str, ScenarioResult] = {}
224+
147225
for result in results:
148226
for scenario in result.scenarios:
149227
existing = scenarios.setdefault(scenario.name, ScenarioResult(name=scenario.name))
150228
for run in scenario.runs:
151229
existing.add_run(run)
230+
152231
started = min(result.started_at for result in results)
153232
finished = max(result.finished_at for result in results)
154-
return SuiteResult(scenarios=list(scenarios.values()), started_at=started, finished_at=finished)
233+
234+
return SuiteResult(
235+
scenarios=list(scenarios.values()),
236+
started_at=started,
237+
finished_at=finished,
238+
)
155239

156240

157-
def _render_markdown_scenario(scenario: ScenarioResult) -> list[str]:
158-
lines = [f"## {scenario.name}", f"Success rate: {scenario.success_rate:.2%}", ""]
241+
def _render_markdown_scenario(
242+
scenario: ScenarioResult,
243+
) -> list[str]:
244+
lines = [
245+
f"## {scenario.name}",
246+
f"Success rate: {scenario.success_rate:.2%}",
247+
"",
248+
]
249+
159250
for run in scenario.runs:
160251
lines.append(f"- **{run.case_id}**: {'✅' if run.success else '❌'}")
161252
metrics_repr = ", ".join(
@@ -165,5 +256,6 @@ def _render_markdown_scenario(scenario: ScenarioResult) -> list[str]:
165256
lines.append(f" - Metrics: {metrics_repr}")
166257
if run.error:
167258
lines.append(f" - Error: {run.error}")
259+
168260
lines.append("")
169261
return lines

0 commit comments

Comments
 (0)