Skip to content

Commit 8010bd4

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 da26fe4 commit 8010bd4

8 files changed

Lines changed: 487 additions & 22 deletions

File tree

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

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,12 @@ def _dispatch_agentic(
8383
run_ts: str,
8484
model_version_override: str | None,
8585
reasoning_effort: ReasoningEffort | None = None,
86-
) -> None:
87-
"""Call the appropriate evaluate_agentic_* function for the item's test_kind."""
86+
) -> list[str] | None:
87+
"""Call the appropriate evaluate_agentic_* function for the item's test_kind.
88+
89+
Returns whatever that function returns -- only alert_skill/metric_skill/conversation
90+
currently return their reasoning_steps; the rest still return None (unchanged).
91+
"""
8892
kind = item.test_kind
8993
eo = item.expected_output
9094
lf_kw: _LfKw = {
@@ -97,7 +101,7 @@ def _dispatch_agentic(
97101
}
98102

99103
if kind in ("vis_agentic", "agentic_visualization"):
100-
evaluate_agentic_visualization(
104+
return evaluate_agentic_visualization(
101105
host=host,
102106
token=token,
103107
workspace_id=workspace_id,
@@ -107,7 +111,7 @@ def _dispatch_agentic(
107111
**lf_kw,
108112
)
109113
elif kind == "agentic_metric_skill":
110-
evaluate_agentic_metric_skill(
114+
return evaluate_agentic_metric_skill(
111115
host=host,
112116
token=token,
113117
workspace_id=workspace_id,
@@ -117,7 +121,7 @@ def _dispatch_agentic(
117121
**lf_kw,
118122
)
119123
elif kind == "agentic_alert_skill":
120-
evaluate_agentic_alert_skill(
124+
return evaluate_agentic_alert_skill(
121125
host=host,
122126
token=token,
123127
workspace_id=workspace_id,
@@ -130,7 +134,7 @@ def _dispatch_agentic(
130134
eo_dict = eo if isinstance(eo, dict) else {}
131135
tool_call = eo_dict.get("tool_call", {})
132136
expected_args = tool_call.get("function_arguments", eo_dict)
133-
evaluate_agentic_search_tool(
137+
return evaluate_agentic_search_tool(
134138
host=host,
135139
token=token,
136140
workspace_id=workspace_id,
@@ -140,7 +144,7 @@ def _dispatch_agentic(
140144
**lf_kw,
141145
)
142146
elif kind == "agentic_general_question":
143-
evaluate_agentic_general_question(
147+
return evaluate_agentic_general_question(
144148
host=host,
145149
token=token,
146150
workspace_id=workspace_id,
@@ -150,7 +154,7 @@ def _dispatch_agentic(
150154
**lf_kw,
151155
)
152156
elif kind == "agentic_guardrail":
153-
evaluate_agentic_guardrail(
157+
return evaluate_agentic_guardrail(
154158
host=host,
155159
token=token,
156160
workspace_id=workspace_id,
@@ -161,7 +165,7 @@ def _dispatch_agentic(
161165
)
162166
elif kind == "agentic_conversation":
163167
fixture_data = eo.get("fixture") or eo if isinstance(eo, dict) else {}
164-
evaluate_agentic_conversation(
168+
return evaluate_agentic_conversation(
165169
host=host,
166170
token=token,
167171
workspace_id=workspace_id,
@@ -207,12 +211,16 @@ def run_agentic_items(
207211
)
208212
t0 = time.perf_counter()
209213
try:
210-
_dispatch_agentic(item, host, token, workspace_id, k, langfuse, run_ts, model_version, reasoning_effort)
214+
reasoning_steps = _dispatch_agentic(
215+
item, host, token, workspace_id, k, langfuse, run_ts, model_version, reasoning_effort
216+
)
211217
item_report.pass_at_k = True
212218
item_report.runs = k
219+
item_report.reasoning_steps = reasoning_steps or []
213220
except AssertionError as exc:
214221
item_report.pass_at_k = False
215222
item_report.runs = k
223+
item_report.reasoning_steps = getattr(exc, "reasoning_steps", None) or []
216224
print(f"[agentic] {item.id} FAIL: {exc}", flush=True)
217225
except Exception as exc:
218226
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
@@ -233,6 +233,7 @@ class AlertRunResult:
233233
alert_id: str | None
234234
eval: AlertEvaluation
235235
actual_alert_arguments: dict
236+
reasoning_steps: list[str] = field(default_factory=list)
236237

237238

238239
@dataclass
@@ -357,13 +358,15 @@ def _run_once(conv_id: str) -> AlertRunResult:
357358
alert_id: str | None = None
358359
actual_args: dict = {}
359360
tool_called = False
361+
reasoning_steps: list[str] = []
360362
# conversation_history stores prior turns for GPT-4o context.
361363
# Roles follow GPT-4o's perspective: "assistant"=agent text, "user"=sim-user reply.
362364
conversation_history: list = []
363365
current_question = question
364366

365367
for _iteration in range(max_iterations):
366368
chat_result = client.send_message(conv_id, current_question)
369+
reasoning_steps.extend(chat_result.reasoning_steps or [])
367370
alert_id, actual_args, tool_called = _extract_alert_call(chat_result.tool_call_events or [])
368371
if tool_called:
369372
alert_id_to_delete = alert_id
@@ -397,6 +400,7 @@ def _run_once(conv_id: str) -> AlertRunResult:
397400
alert_id=alert_id,
398401
eval=ev,
399402
actual_alert_arguments=actual_args,
403+
reasoning_steps=reasoning_steps,
400404
)
401405
finally:
402406
if alert_id_to_delete:
@@ -447,6 +451,7 @@ class AlertSkillAssertionError(AssertionError):
447451
"""Raised when an alert-skill evaluation fails."""
448452

449453
__tracebackhide__ = True
454+
reasoning_steps: list[str]
450455

451456

452457
def evaluate_agentic_alert_skill(
@@ -465,8 +470,13 @@ def evaluate_agentic_alert_skill(
465470
model_version_override: str | None = None,
466471
run_metadata_extra: dict | None = None,
467472
reasoning_effort: ReasoningEffort | None = None,
468-
) -> None:
469-
"""Run alert-skill evaluation, log to Langfuse, and raise AlertSkillAssertionError on failure."""
473+
) -> list[str]:
474+
"""Run alert-skill evaluation, log to Langfuse, and raise AlertSkillAssertionError on failure.
475+
476+
Returns the best run's reasoning_steps on success; on failure the same list is attached
477+
to the raised exception as ``.reasoning_steps`` (mirrors the `conversation_id`-on-exception
478+
idiom in `ChatClient.ask()`) so callers can retrieve it either way.
479+
"""
470480
from datetime import datetime as _dt # noqa: PLC0415
471481
from datetime import timezone as _tz # noqa: PLC0415
472482

@@ -539,11 +549,14 @@ def evaluate_agentic_alert_skill(
539549
if not summary.pass_at_k:
540550
best = summary.best
541551
ev = best.eval
542-
raise AlertSkillAssertionError(
552+
exc = AlertSkillAssertionError(
543553
f"Alert skill assertion failed. strict_pass={ev.strict_pass}. "
544554
f"alert_created={ev.alert_created}, operator_correct={ev.operator_correct}, "
545555
f"threshold_correct={ev.threshold_correct}, trigger_correct={ev.trigger_correct}, "
546556
f"filters_correct={ev.filters_correct}, metric_correct={ev.metric_correct}, "
547557
f"recipients_correct={ev.recipients_correct}. "
548558
f"Actual args: {best.actual_alert_arguments}"
549559
)
560+
exc.reasoning_steps = best.reasoning_steps
561+
raise exc
562+
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
@@ -270,6 +270,7 @@ class ConversationResult:
270270
full_skill_coverage: bool
271271
conversation_success: bool
272272
total_clarification_turns: int
273+
reasoning_steps: list[str] = field(default_factory=list)
273274

274275

275276
def run_agentic_conversation(
@@ -298,6 +299,7 @@ def run_agentic_conversation(
298299
# not persist in the (shared) workspace and get reused by a later test. Deferred to
299300
# the end — a later turn may $ref a metric an earlier turn created.
300301
created_metric_ids: list[str] = []
302+
reasoning_steps: list[str] = []
301303

302304
try:
303305
if initial_conversation_id is not None:
@@ -320,6 +322,7 @@ def run_agentic_conversation(
320322
chat_result = client.send_message(conversation_id, current_message)
321323
final_result = chat_result
322324
all_tool_calls.extend(chat_result.tool_call_events or [])
325+
reasoning_steps.extend(chat_result.reasoning_steps or [])
323326

324327
if _check_output_present(resolved_turn, chat_result):
325328
break
@@ -383,13 +386,15 @@ def run_agentic_conversation(
383386
full_skill_coverage=full_skill_coverage,
384387
conversation_success=conversation_success,
385388
total_clarification_turns=total_clarification_turns,
389+
reasoning_steps=reasoning_steps,
386390
)
387391

388392

389393
class ConversationAssertionError(AssertionError):
390394
"""Raised when a conversation evaluation fails."""
391395

392396
__tracebackhide__ = True
397+
reasoning_steps: list[str]
393398

394399

395400
def evaluate_agentic_conversation(
@@ -406,8 +411,14 @@ def evaluate_agentic_conversation(
406411
model_version_override: str | None = None,
407412
run_metadata_extra: dict | None = None,
408413
reasoning_effort: ReasoningEffort | None = None,
409-
) -> None:
410-
"""Run conversation evaluation, log to Langfuse, and raise on failure."""
414+
) -> list[str]:
415+
"""Run conversation evaluation, log to Langfuse, and raise on failure.
416+
417+
Returns the conversation's reasoning_steps on success; on failure the same list is
418+
attached to the raised exception as ``.reasoning_steps`` (mirrors the
419+
`conversation_id`-on-exception idiom in `ChatClient.ask()`) so callers can retrieve it
420+
either way.
421+
"""
411422
from datetime import datetime as _dt # noqa: PLC0415
412423
from datetime import timezone as _tz # noqa: PLC0415
413424

@@ -483,8 +494,11 @@ def evaluate_agentic_conversation(
483494

484495
if not result.conversation_success:
485496
failed_turns = [tr for tr in result.turn_results if not tr.skill_success]
486-
raise ConversationAssertionError(
497+
exc = ConversationAssertionError(
487498
f"Conversation assertion failed. "
488499
f"full_skill_coverage={result.full_skill_coverage}. "
489500
f"Failed turns: {[t.turn_id for t in failed_turns]}"
490501
)
502+
exc.reasoning_steps = result.reasoning_steps
503+
raise exc
504+
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
@@ -109,6 +109,7 @@ class MetricRunResult:
109109
actual_maql: str
110110
maql_correct: bool
111111
total_turns: float
112+
reasoning_steps: list[str] = field(default_factory=list)
112113

113114

114115
@dataclass
@@ -192,11 +193,13 @@ def _execute_single_metric_run(
192193
metric_id_to_delete: str | None = None
193194
turns = 0
194195
current_question = question
196+
reasoning_steps: list[str] = []
195197

196198
try:
197199
for _iteration in range(max_iterations):
198200
turns += 1
199201
chat_result = client.send_message(conversation_id, current_question)
202+
reasoning_steps.extend(chat_result.reasoning_steps or [])
200203
candidate = _extract_metric_result(chat_result.tool_call_events or [])
201204
if candidate is not None:
202205
metric_result = candidate
@@ -218,6 +221,7 @@ def _execute_single_metric_run(
218221
actual_maql=actual_maql,
219222
maql_correct=maql_correct,
220223
total_turns=float(turns),
224+
reasoning_steps=reasoning_steps,
221225
)
222226
finally:
223227
if metric_id_to_delete:
@@ -285,6 +289,7 @@ class MetricSkillAssertionError(AssertionError):
285289
"""Raised when a metric-skill evaluation fails."""
286290

287291
__tracebackhide__ = True
292+
reasoning_steps: list[str]
288293

289294

290295
def evaluate_agentic_metric_skill(
@@ -303,8 +308,13 @@ def evaluate_agentic_metric_skill(
303308
model_version_override: str | None = None,
304309
run_metadata_extra: dict | None = None,
305310
reasoning_effort: ReasoningEffort | None = None,
306-
) -> None:
307-
"""Run metric-skill evaluation, log to Langfuse, and raise MetricSkillAssertionError on failure."""
311+
) -> list[str]:
312+
"""Run metric-skill evaluation, log to Langfuse, and raise MetricSkillAssertionError on failure.
313+
314+
Returns the best run's reasoning_steps on success; on failure the same list is attached
315+
to the raised exception as ``.reasoning_steps`` (mirrors the `conversation_id`-on-exception
316+
idiom in `ChatClient.ask()`) so callers can retrieve it either way.
317+
"""
308318
from datetime import datetime as _dt # noqa: PLC0415
309319
from datetime import timezone as _tz # noqa: PLC0415
310320

@@ -368,9 +378,12 @@ def evaluate_agentic_metric_skill(
368378
best = summary.best
369379
expected_outputs_list: list[dict] = expected_output if isinstance(expected_output, list) else [expected_output]
370380
candidates_str = "; ".join(repr(c.get("maql", "")) for c in expected_outputs_list)
371-
raise MetricSkillAssertionError(
381+
exc = MetricSkillAssertionError(
372382
f"Metric skill assertion failed. "
373383
f"metric_created={best.metric_created}, maql_correct={best.maql_correct}. "
374384
f"Expected MAQL (candidates): {candidates_str}. "
375385
f"Actual MAQL: {best.actual_maql}."
376386
)
387+
exc.reasoning_steps = best.reasoning_steps
388+
raise exc
389+
return summary.best.reasoning_steps

0 commit comments

Comments
 (0)