Skip to content

Commit 7d4496f

Browse files
committed
feat(gooddata-eval): capture reasoning_steps through the agentic-CLI path
6001d2f wired ChatResult.reasoning_steps through runner.py's generic single-turn path only. The agentic-CLI path (cli/agentic_runner.py -> evaluate_agentic_*) builds its own ItemReport and never touched it, so agentic_alert_skill/agentic_metric_skill/agentic_conversation items could never produce a reasoning trace, no matter what the platform emitted. Accumulates reasoning_steps across every send_message call in each of the three evaluators' run loops, attaches it to the run/turn result, and surfaces it from evaluate_agentic_* either as the return value (pass) or as an attribute on the raised exception (fail) -- mirroring the existing conversation_id-on-exception idiom in ChatClient.ask(). run_agentic_items picks it up from either path onto ItemReport.reasoning_steps, which json_report.py already serializes unconditionally. general_question/guardrail/search_tool/visualization are left untouched -- their evaluate_agentic_* functions still return None, unchanged.
1 parent d8d656a commit 7d4496f

8 files changed

Lines changed: 482 additions & 23 deletions

File tree

‎packages/gooddata-eval/src/gooddata_eval/cli/agentic_runner.py‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,12 @@ def _dispatch_agentic(
8686
model_version_override: str | None,
8787
reasoning_effort: ReasoningEffort | None = None,
8888
agent_id: str | None = None,
89-
) -> None:
90-
"""Call the appropriate evaluate_agentic_* function for the item's test_kind."""
89+
) -> list[str] | None:
90+
"""Call the appropriate evaluate_agentic_* function for the item's test_kind.
91+
92+
Returns whatever that function returns -- only alert_skill/metric_skill/conversation
93+
currently return their reasoning_steps; the rest still return None (unchanged).
94+
"""
9195
kind = item.test_kind
9296
eo = item.expected_output
9397
lf_kw: _LfKw = {
@@ -100,7 +104,7 @@ def _dispatch_agentic(
100104
}
101105

102106
if kind in ("vis_agentic", "agentic_visualization"):
103-
evaluate_agentic_visualization(
107+
return evaluate_agentic_visualization(
104108
host=host,
105109
token=token,
106110
workspace_id=workspace_id,
@@ -111,7 +115,7 @@ def _dispatch_agentic(
111115
**lf_kw,
112116
)
113117
elif kind == "agentic_metric_skill":
114-
evaluate_agentic_metric_skill(
118+
return evaluate_agentic_metric_skill(
115119
host=host,
116120
token=token,
117121
workspace_id=workspace_id,
@@ -122,7 +126,7 @@ def _dispatch_agentic(
122126
**lf_kw,
123127
)
124128
elif kind == "agentic_alert_skill":
125-
evaluate_agentic_alert_skill(
129+
return evaluate_agentic_alert_skill(
126130
host=host,
127131
token=token,
128132
workspace_id=workspace_id,
@@ -136,7 +140,7 @@ def _dispatch_agentic(
136140
eo_dict = eo if isinstance(eo, dict) else {}
137141
tool_call = eo_dict.get("tool_call", {})
138142
expected_args = tool_call.get("function_arguments", eo_dict)
139-
evaluate_agentic_search_tool(
143+
return evaluate_agentic_search_tool(
140144
host=host,
141145
token=token,
142146
workspace_id=workspace_id,
@@ -147,7 +151,7 @@ def _dispatch_agentic(
147151
**lf_kw,
148152
)
149153
elif kind == "agentic_general_question":
150-
evaluate_agentic_general_question(
154+
return evaluate_agentic_general_question(
151155
host=host,
152156
token=token,
153157
workspace_id=workspace_id,
@@ -158,7 +162,7 @@ def _dispatch_agentic(
158162
**lf_kw,
159163
)
160164
elif kind == "agentic_guardrail":
161-
evaluate_agentic_guardrail(
165+
return evaluate_agentic_guardrail(
162166
host=host,
163167
token=token,
164168
workspace_id=workspace_id,
@@ -180,7 +184,7 @@ def _dispatch_agentic(
180184
)
181185
elif kind == "agentic_conversation":
182186
fixture_data = eo.get("fixture") or eo if isinstance(eo, dict) else {}
183-
evaluate_agentic_conversation(
187+
return evaluate_agentic_conversation(
184188
host=host,
185189
token=token,
186190
workspace_id=workspace_id,
@@ -228,14 +232,16 @@ def run_agentic_items(
228232
)
229233
t0 = time.perf_counter()
230234
try:
231-
_dispatch_agentic(
235+
reasoning_steps = _dispatch_agentic(
232236
item, host, token, workspace_id, k, langfuse, run_ts, model_version, reasoning_effort, agent_id
233237
)
234238
item_report.pass_at_k = True
235239
item_report.runs = k
240+
item_report.reasoning_steps = reasoning_steps or []
236241
except AssertionError as exc:
237242
item_report.pass_at_k = False
238243
item_report.runs = k
244+
item_report.reasoning_steps = getattr(exc, "reasoning_steps", None) or []
239245
print(f"[agentic] {item.id} FAIL: {exc}", flush=True)
240246
except Exception as exc:
241247
item_report.error = f"{type(exc).__name__}: {exc}"

‎packages/gooddata-eval/src/gooddata_eval/core/agentic/alert_skill.py‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import json
77
import os
88
import re
9-
from dataclasses import dataclass
9+
from dataclasses import dataclass, field
1010
from typing import Any
1111

1212
from gooddata_sdk import GoodDataSdk
@@ -302,6 +302,7 @@ class AlertRunResult:
302302
alert_id: str | None
303303
eval: AlertEvaluation
304304
actual_alert_arguments: dict
305+
reasoning_steps: list[str] = field(default_factory=list)
305306

306307

307308
@dataclass
@@ -450,13 +451,15 @@ def _run_once(conv_id: str) -> AlertRunResult:
450451
alert_id: str | None = None
451452
actual_args: dict = {}
452453
tool_called = False
454+
reasoning_steps: list[str] = []
453455
# conversation_history stores prior turns for GPT-4o context.
454456
# Roles follow GPT-4o's perspective: "assistant"=agent text, "user"=sim-user reply.
455457
conversation_history: list = []
456458
current_question = question
457459

458460
for _iteration in range(max_iterations):
459461
chat_result = client.send_message(conv_id, current_question)
462+
reasoning_steps.extend(chat_result.reasoning_steps or [])
460463
alert_id, actual_args, tool_called = _extract_alert_call(chat_result.tool_call_events or [])
461464
if tool_called:
462465
alert_id_to_delete = alert_id
@@ -492,6 +495,7 @@ def _run_once(conv_id: str) -> AlertRunResult:
492495
alert_id=alert_id,
493496
eval=ev,
494497
actual_alert_arguments=actual_args,
498+
reasoning_steps=reasoning_steps,
495499
)
496500
finally:
497501
if alert_id_to_delete:
@@ -542,6 +546,7 @@ class AlertSkillAssertionError(AssertionError):
542546
"""Raised when an alert-skill evaluation fails."""
543547

544548
__tracebackhide__ = True
549+
reasoning_steps: list[str]
545550

546551

547552
def evaluate_agentic_alert_skill(
@@ -561,8 +566,13 @@ def evaluate_agentic_alert_skill(
561566
model_version_override: str | None = None,
562567
run_metadata_extra: dict | None = None,
563568
reasoning_effort: ReasoningEffort | None = None,
564-
) -> None:
565-
"""Run alert-skill evaluation, log to Langfuse, and raise AlertSkillAssertionError on failure."""
569+
) -> list[str]:
570+
"""Run alert-skill evaluation, log to Langfuse, and raise AlertSkillAssertionError on failure.
571+
572+
Returns the best run's reasoning_steps on success; on failure the same list is attached
573+
to the raised exception as ``.reasoning_steps`` (mirrors the `conversation_id`-on-exception
574+
idiom in `ChatClient.ask()`) so callers can retrieve it either way.
575+
"""
566576
from datetime import datetime as _dt # noqa: PLC0415
567577
from datetime import timezone as _tz # noqa: PLC0415
568578

@@ -636,11 +646,14 @@ def evaluate_agentic_alert_skill(
636646
if not summary.pass_at_k:
637647
best = summary.best
638648
ev = best.eval
639-
raise AlertSkillAssertionError(
649+
exc = AlertSkillAssertionError(
640650
f"Alert skill assertion failed. strict_pass={ev.strict_pass}. "
641651
f"alert_created={ev.alert_created}, operator_correct={ev.operator_correct}, "
642652
f"threshold_correct={ev.threshold_correct}, trigger_correct={ev.trigger_correct}, "
643653
f"filters_correct={ev.filters_correct}, metric_correct={ev.metric_correct}, "
644654
f"recipients_correct={ev.recipients_correct}. "
645655
f"Actual args: {best.actual_alert_arguments}"
646656
)
657+
exc.reasoning_steps = best.reasoning_steps
658+
raise exc
659+
return summary.best.reasoning_steps

‎packages/gooddata-eval/src/gooddata_eval/core/agentic/conversation.py‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import json
77
import re
8-
from dataclasses import dataclass
8+
from dataclasses import dataclass, field
99
from typing import Literal
1010

1111
from gooddata_sdk import GoodDataSdk
@@ -265,6 +265,7 @@ class ConversationResult:
265265
full_skill_coverage: bool
266266
conversation_success: bool
267267
total_clarification_turns: int
268+
reasoning_steps: list[str] = field(default_factory=list)
268269

269270

270271
def run_agentic_conversation(
@@ -296,6 +297,7 @@ def run_agentic_conversation(
296297
# not persist in the (shared) workspace and get reused by a later test. Deferred to
297298
# the end — a later turn may $ref a metric an earlier turn created.
298299
created_metric_ids: list[str] = []
300+
reasoning_steps: list[str] = []
299301

300302
try:
301303
if initial_conversation_id is not None:
@@ -332,6 +334,7 @@ def run_agentic_conversation(
332334
chat_result = client.send_message(conversation_id, current_message)
333335
final_result = chat_result
334336
all_tool_calls.extend(chat_result.tool_call_events or [])
337+
reasoning_steps.extend(chat_result.reasoning_steps or [])
335338

336339
if _check_output_present(resolved_turn, chat_result):
337340
break
@@ -395,13 +398,15 @@ def run_agentic_conversation(
395398
full_skill_coverage=full_skill_coverage,
396399
conversation_success=conversation_success,
397400
total_clarification_turns=total_clarification_turns,
401+
reasoning_steps=reasoning_steps,
398402
)
399403

400404

401405
class ConversationAssertionError(AssertionError):
402406
"""Raised when a conversation evaluation fails."""
403407

404408
__tracebackhide__ = True
409+
reasoning_steps: list[str]
405410

406411

407412
def evaluate_agentic_conversation(
@@ -419,8 +424,14 @@ def evaluate_agentic_conversation(
419424
model_version_override: str | None = None,
420425
run_metadata_extra: dict | None = None,
421426
reasoning_effort: ReasoningEffort | None = None,
422-
) -> None:
423-
"""Run conversation evaluation, log to Langfuse, and raise on failure."""
427+
) -> list[str]:
428+
"""Run conversation evaluation, log to Langfuse, and raise on failure.
429+
430+
Returns the conversation's reasoning_steps on success; on failure the same list is
431+
attached to the raised exception as ``.reasoning_steps`` (mirrors the
432+
`conversation_id`-on-exception idiom in `ChatClient.ask()`) so callers can retrieve it
433+
either way.
434+
"""
424435
from datetime import datetime as _dt # noqa: PLC0415
425436
from datetime import timezone as _tz # noqa: PLC0415
426437

@@ -497,8 +508,11 @@ def evaluate_agentic_conversation(
497508

498509
if not result.conversation_success:
499510
failed_turns = [tr for tr in result.turn_results if not tr.skill_success]
500-
raise ConversationAssertionError(
511+
exc = ConversationAssertionError(
501512
f"Conversation assertion failed. "
502513
f"full_skill_coverage={result.full_skill_coverage}. "
503514
f"Failed turns: {[t.turn_id for t in failed_turns]}"
504515
)
516+
exc.reasoning_steps = result.reasoning_steps
517+
raise exc
518+
return result.reasoning_steps

‎packages/gooddata-eval/src/gooddata_eval/core/agentic/metric_skill.py‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import os
77
import re
8-
from dataclasses import dataclass
8+
from dataclasses import dataclass, field
99
from typing import Any
1010

1111
from gooddata_sdk import GoodDataSdk
@@ -129,6 +129,7 @@ class MetricRunResult:
129129
actual_maql: str
130130
maql_correct: bool
131131
total_turns: float
132+
reasoning_steps: list[str] = field(default_factory=list)
132133

133134

134135
@dataclass
@@ -205,11 +206,13 @@ def _execute_single_metric_run(
205206
metric_id_to_delete: str | None = None
206207
turns = 0
207208
current_question = question
209+
reasoning_steps: list[str] = []
208210

209211
try:
210212
for _iteration in range(max_iterations):
211213
turns += 1
212214
chat_result = client.send_message(conversation_id, current_question)
215+
reasoning_steps.extend(chat_result.reasoning_steps or [])
213216
candidate = _extract_metric_result(chat_result.tool_call_events or [])
214217
if candidate is not None:
215218
metric_result = candidate
@@ -236,6 +239,7 @@ def _execute_single_metric_run(
236239
actual_maql=actual_maql,
237240
maql_correct=maql_correct,
238241
total_turns=float(turns),
242+
reasoning_steps=reasoning_steps,
239243
)
240244
finally:
241245
if metric_id_to_delete:
@@ -306,6 +310,7 @@ class MetricSkillAssertionError(AssertionError):
306310
"""Raised when a metric-skill evaluation fails."""
307311

308312
__tracebackhide__ = True
313+
reasoning_steps: list[str]
309314

310315

311316
def evaluate_agentic_metric_skill(
@@ -325,8 +330,13 @@ def evaluate_agentic_metric_skill(
325330
model_version_override: str | None = None,
326331
run_metadata_extra: dict | None = None,
327332
reasoning_effort: ReasoningEffort | None = None,
328-
) -> None:
329-
"""Run metric-skill evaluation, log to Langfuse, and raise MetricSkillAssertionError on failure."""
333+
) -> list[str]:
334+
"""Run metric-skill evaluation, log to Langfuse, and raise MetricSkillAssertionError on failure.
335+
336+
Returns the best run's reasoning_steps on success; on failure the same list is attached
337+
to the raised exception as ``.reasoning_steps`` (mirrors the `conversation_id`-on-exception
338+
idiom in `ChatClient.ask()`) so callers can retrieve it either way.
339+
"""
330340
from datetime import datetime as _dt # noqa: PLC0415
331341
from datetime import timezone as _tz # noqa: PLC0415
332342

@@ -391,9 +401,12 @@ def evaluate_agentic_metric_skill(
391401
best = summary.best
392402
expected_outputs_list: list[dict] = expected_output if isinstance(expected_output, list) else [expected_output]
393403
candidates_str = "; ".join(repr(c.get("maql", "")) for c in expected_outputs_list)
394-
raise MetricSkillAssertionError(
404+
exc = MetricSkillAssertionError(
395405
f"Metric skill assertion failed. "
396406
f"metric_created={best.metric_created}, maql_correct={best.maql_correct}. "
397407
f"Expected MAQL (candidates): {candidates_str}. "
398408
f"Actual MAQL: {best.actual_maql}."
399409
)
410+
exc.reasoning_steps = best.reasoning_steps
411+
raise exc
412+
return summary.best.reasoning_steps

0 commit comments

Comments
 (0)