Skip to content

Commit 389905b

Browse files
committed
feat(gooddata-eval): capture agent reasoning steps in ChatResult
The SSE reasoning events were already being read to produce reasoning_step_count, but the step text itself was discarded. Keep it as reasoning_steps on ChatResult/ItemReport and surface it in the JSON report so eval consumers can inspect the agent's actual reasoning trace, not just how many steps it took.
1 parent 17bcb5c commit 389905b

7 files changed

Lines changed: 35 additions & 0 deletions

File tree

packages/gooddata-eval/src/gooddata_eval/core/chat/sse_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def _build_chat_result(acc: _SseAccumulator) -> ChatResult:
171171
"alertProposals": acc.alert_proposals,
172172
"toolCallEvents": acc.tool_call_events,
173173
"reasoningStepCount": len(acc.reasoning_steps),
174+
"reasoningSteps": [step["summary"] for step in acc.reasoning_steps],
174175
}
175176
if acc.visualizations:
176177
payload["createdVisualizations"] = {

packages/gooddata-eval/src/gooddata_eval/core/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class ChatResult(BaseModel):
9898
alert_proposals: list[dict] = Field(default_factory=list, alias="alertProposals")
9999
tool_call_events: list[ToolCallEvent] = Field(default_factory=list, alias="toolCallEvents")
100100
reasoning_step_count: int = Field(default=0, alias="reasoningStepCount")
101+
reasoning_steps: list[str] = Field(default_factory=list, alias="reasoningSteps")
101102
conversation_id: str | None = Field(default=None, alias="conversationId")
102103
response_id: str | None = Field(default=None, alias="responseId")
103104

packages/gooddata-eval/src/gooddata_eval/core/reporting/json_report.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def _build_run_dict(report: EvalReport) -> dict:
3636
"detail": item.best_detail,
3737
"conversation_id": item.conversation_id,
3838
"response_id": item.response_id,
39+
"reasoning": item.reasoning_steps,
3940
}
4041
for item in report.items
4142
},

packages/gooddata-eval/src/gooddata_eval/core/runner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class ItemReport:
3333
best_detail: dict = field(default_factory=dict)
3434
conversation_id: str | None = None
3535
response_id: str | None = None
36+
reasoning_steps: list[str] = field(default_factory=list)
3637

3738
@property
3839
def avg_latency_s(self) -> float:
@@ -116,6 +117,7 @@ def _run_one_item(
116117
chat_result = backend.ask(item)
117118
report.conversation_id = getattr(chat_result, "conversation_id", None) or report.conversation_id
118119
report.response_id = getattr(chat_result, "response_id", None) or report.response_id
120+
report.reasoning_steps = getattr(chat_result, "reasoning_steps", None) or report.reasoning_steps
119121
evaluation = evaluator.evaluate(item, chat_result)
120122
latency = time.perf_counter() - t0
121123
report.runs += 1

packages/gooddata-eval/tests/test_reporting.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def _report() -> EvalReport:
2323
pass_at_k=True,
2424
runs=2,
2525
latency_s=2.5,
26+
reasoning_steps=["step one", "step two"],
2627
),
2728
ItemReport(
2829
id="i2",
@@ -48,6 +49,8 @@ def test_build_json_report_keyed_by_item_id():
4849
assert data["items"]["i1"]["pass_at_k"] is True
4950
assert data["items"]["i1"]["latency_s"] == 2.5
5051
assert data["items"]["i1"]["avg_latency_s"] == 1.25
52+
assert data["items"]["i1"]["reasoning"] == ["step one", "step two"]
53+
assert data["items"]["i2"]["reasoning"] == []
5154

5255

5356
def test_write_json_report_creates_file(tmp_path):

packages/gooddata-eval/tests/test_runner.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,25 @@ def ask(self, item: DatasetItem) -> ChatResult:
258258
assert "conversation_id" not in report.items[0].error
259259

260260

261+
def test_run_items_carries_reasoning_steps_from_chat_result():
262+
"""reasoning_steps from the ChatResult surfaces on the item report, same as conversation_id."""
263+
264+
class _ReasoningBackend:
265+
def ask(self, item: DatasetItem) -> ChatResult:
266+
return ChatResult.model_validate(
267+
{"textResponse": "which metric?", "reasoningSteps": ["step one", "step two"]}
268+
)
269+
270+
report = run_items([_item()], _ReasoningBackend(), runs=1)
271+
assert report.items[0].reasoning_steps == ["step one", "step two"]
272+
273+
274+
def test_run_items_reasoning_steps_empty_when_chat_result_has_none():
275+
backend = _FakeBackend([_empty_chat()])
276+
report = run_items([_item()], backend, runs=1)
277+
assert report.items[0].reasoning_steps == []
278+
279+
261280
def test_run_items_callback_exception_is_logged_not_swallowed(capsys):
262281
"""A raising callback prints a traceback to stderr but the run continues."""
263282
backend = _FakeBackend([_chat_with(_viz_obj())] * 2)

packages/gooddata-eval/tests/test_sse_client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,17 @@ def test_parse_sse_lines_counts_reasoning_steps():
6060
]
6161
result = parse_sse_lines(lines)
6262
assert result.reasoning_step_count == 2
63+
assert result.reasoning_steps == ["step one", "step two"]
6364
assert result.text_response == "Done"
6465

6566

67+
def test_parse_sse_lines_reasoning_steps_empty_when_no_reasoning_events():
68+
lines = ['data: {"item": {"role": "assistant", "content": {"type": "text", "text": "Done"}}}']
69+
result = parse_sse_lines(lines)
70+
assert result.reasoning_step_count == 0
71+
assert result.reasoning_steps == []
72+
73+
6674
def test_parse_sse_lines_prefers_multipart_viz_over_adhoc_fallback():
6775
"""Real multipart visualization takes priority over adhoc tool call stash."""
6876

0 commit comments

Comments
 (0)