Skip to content

Commit 65cc58e

Browse files
committed
feat(gooddata-eval): record the turns and steps each agentic run took
The effort comparison in gdc-nas reads these. A pass/fail bit cannot separate two reasoning efforts, while the step count moves with the effort, and the turn count tells a wrong answer from a run max_iterations cut short. alert_skill and kda_skill gained the counters; metric_skill and conversation already had them and only needed the write. conversation also logs clarification_turns, since its total includes the turns its fixture asks for.
1 parent 8bfa6ff commit 65cc58e

6 files changed

Lines changed: 143 additions & 0 deletions

File tree

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,8 @@ class AlertRunResult:
477477
alert_id: str | None
478478
eval: AlertEvaluation
479479
actual_alert_arguments: dict
480+
total_turns: float = 0.0
481+
total_steps: float = 0.0
480482
reasoning_steps: list[str] = field(default_factory=list)
481483
response_id: str | None = None
482484
tool_call_events: list[ToolCallEvent] = field(default_factory=list)
@@ -676,9 +678,13 @@ def _run_once(conv_id: str) -> AlertRunResult:
676678
# Roles follow GPT-4o's perspective: "assistant"=agent text, "user"=sim-user reply.
677679
conversation_history: list = []
678680
current_question = question
681+
turns = 0
682+
steps = 0.0
679683

680684
for _iteration in range(max_iterations):
681685
chat_result = client.send_message(conv_id, current_question)
686+
turns += 1
687+
steps += float(chat_result.reasoning_step_count)
682688
reasoning_steps.extend(chat_result.reasoning_steps or [])
683689
response_id = chat_result.response_id or response_id
684690
turn_offset, tool_index_offset, reasoning_index_offset = shift_and_index_events(
@@ -728,6 +734,8 @@ def _run_once(conv_id: str) -> AlertRunResult:
728734
alert_id=alert_id,
729735
eval=ev,
730736
actual_alert_arguments=actual_args,
737+
total_turns=float(turns),
738+
total_steps=steps,
731739
reasoning_steps=reasoning_steps,
732740
response_id=response_id,
733741
tool_call_events=all_tool_call_events,
@@ -851,6 +859,8 @@ def _write_scores(ctx: RunTraceContext) -> None:
851859
with ctx.observe(pt, run_idx, conversation_id=run.conversation_id, output=strict_checks) as tid:
852860
for score_name, value in strict_checks.items():
853861
ctx.score(tid, name=score_name, value=float(value), data_type="BOOLEAN")
862+
ctx.score(tid, name="turns", value=run.total_turns, data_type="NUMERIC")
863+
ctx.score(tid, name="steps", value=run.total_steps, data_type="NUMERIC")
854864
log_gate_scores(ctx, tid, gate=gate, pass_at_k=summary.pass_at_k, pass_power_k=summary.pass_power_k)
855865
ctx.quality(
856866
tid,

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ class ConversationResult:
336336
full_skill_coverage: bool
337337
conversation_success: bool
338338
total_clarification_turns: int
339+
total_steps: float = 0.0
339340
reasoning_steps: list[str] = field(default_factory=list)
340341
response_id: str | None = None
341342
tool_call_events: list[ToolCallEvent] = field(default_factory=list)
@@ -365,6 +366,7 @@ def run_agentic_conversation(
365366
turn_results: list[TurnResult] = []
366367
turn_outputs: dict[str, dict] = {}
367368
total_clarification_turns = 0
369+
total_steps = 0.0
368370
conversation_id: str = ""
369371
owns_conversation = False
370372
# Metrics created during this conversation, deleted after it completes so they do
@@ -434,6 +436,7 @@ def run_agentic_conversation(
434436
for _iter in range(max_clarification_turns + 1):
435437
chat_result = client.send_message(conversation_id, current_message)
436438
final_result = chat_result
439+
total_steps += float(chat_result.reasoning_step_count)
437440
turn_offset, tool_index_offset, reasoning_index_offset = shift_and_index_events(
438441
chat_result,
439442
turn_offset=turn_offset,
@@ -522,6 +525,7 @@ def run_agentic_conversation(
522525
full_skill_coverage=full_skill_coverage,
523526
conversation_success=conversation_success,
524527
total_clarification_turns=total_clarification_turns,
528+
total_steps=total_steps,
525529
reasoning_steps=reasoning_steps,
526530
response_id=response_id,
527531
tool_call_events=conversation_tool_call_events,
@@ -611,6 +615,22 @@ def _write_scores(ctx: RunTraceContext) -> None:
611615
value=float(result.full_skill_coverage),
612616
data_type="BOOLEAN",
613617
)
618+
# One turn per fixture turn, plus every simulated-user round the agent triggered.
619+
# The clarification count alone hides how much of the conversation the fixture
620+
# asked for, so the comparison needs the total.
621+
ctx.score(
622+
tid,
623+
name="turns",
624+
value=float(len(result.turn_results) + result.total_clarification_turns),
625+
data_type="NUMERIC",
626+
)
627+
ctx.score(tid, name="steps", value=result.total_steps, data_type="NUMERIC")
628+
ctx.score(
629+
tid,
630+
name="clarification_turns",
631+
value=float(result.total_clarification_turns),
632+
data_type="NUMERIC",
633+
)
614634
for tr in result.turn_results:
615635
ctx.score(
616636
tid,

packages/gooddata-eval/src/gooddata_eval/core/agentic/kda_skill.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ class KdaRunResult:
185185
# Wall-clock time of the turn that called create (None if create never happened) --
186186
# not any earlier disambiguation turn. See run_agentic_kda_skill's _run_once.
187187
turn_wall_clock_sec: float | None = None
188+
total_turns: float = 0.0
189+
total_steps: float = 0.0
188190
reasoning_steps: list[str] = field(default_factory=list)
189191
response_id: str | None = None
190192
tool_call_events: list[ToolCallEvent] = field(default_factory=list)
@@ -276,6 +278,9 @@ def _accumulate(result: ChatResult) -> None:
276278
all_tool_call_events.extend(result.tool_call_events or [])
277279
all_reasoning_step_events.extend(result.reasoning_step_events or [])
278280

281+
turns = 0
282+
steps = 0.0
283+
279284
for iteration in range(max_iterations):
280285
try:
281286
chat_result = client.send_message(conv_id, current_question)
@@ -291,6 +296,8 @@ def _accumulate(result: ChatResult) -> None:
291296
turn_wall_clock_sec = partial.turn_wall_clock_sec
292297
turn_completed = False
293298
break
299+
turns += 1
300+
steps += float(chat_result.reasoning_step_count)
294301
reasoning_steps.extend(chat_result.reasoning_steps or [])
295302
response_id = chat_result.response_id or response_id
296303
_accumulate(chat_result)
@@ -330,6 +337,8 @@ def _accumulate(result: ChatResult) -> None:
330337
actual_create_args=create_args,
331338
actual_execute_result=execute_result,
332339
turn_wall_clock_sec=turn_wall_clock_sec,
340+
total_turns=float(turns),
341+
total_steps=steps,
333342
reasoning_steps=reasoning_steps,
334343
response_id=response_id,
335344
tool_call_events=all_tool_call_events,
@@ -442,6 +451,8 @@ def _write_scores(ctx: RunTraceContext) -> None:
442451
for score_name, value in strict_checks.items():
443452
ctx.score(tid, name=score_name, value=float(value), data_type="BOOLEAN")
444453
ctx.score(tid, name="kda_disambiguated", value=float(ev.disambiguated), data_type="BOOLEAN")
454+
ctx.score(tid, name="turns", value=run.total_turns, data_type="NUMERIC")
455+
ctx.score(tid, name="steps", value=run.total_steps, data_type="NUMERIC")
445456
if turn_wall_clock_sec is not None:
446457
# combo_report.py reads this score directly -- no trace re-resolution needed.
447458
ctx.score(

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ class MetricRunResult:
163163
actual_maql: str
164164
maql_correct: bool
165165
total_turns: float
166+
total_steps: float = 0.0
166167
reasoning_steps: list[str] = field(default_factory=list)
167168
response_id: str | None = None
168169
tool_call_events: list[ToolCallEvent] = field(default_factory=list)
@@ -253,6 +254,7 @@ def _execute_single_metric_run(
253254
metric_result: dict | None = None
254255
created_metric_ids: list[str] = []
255256
turns = 0
257+
steps = 0.0
256258
current_question = question
257259
reasoning_steps: list[str] = []
258260
response_id: str | None = None
@@ -280,6 +282,7 @@ def _execute_single_metric_run(
280282
)
281283
all_tool_call_events.extend(chat_result.tool_call_events or [])
282284
all_reasoning_step_events.extend(chat_result.reasoning_step_events or [])
285+
steps += float(chat_result.reasoning_step_count)
283286
for metric_id in _extract_created_metric_ids(chat_result.tool_call_events or []):
284287
if metric_id not in created_metric_ids:
285288
created_metric_ids.append(metric_id)
@@ -331,6 +334,7 @@ def _execute_single_metric_run(
331334
actual_maql=actual_maql,
332335
maql_correct=maql_correct,
333336
total_turns=float(turns),
337+
total_steps=steps,
334338
reasoning_steps=reasoning_steps,
335339
response_id=response_id,
336340
tool_call_events=all_tool_call_events,
@@ -466,6 +470,8 @@ def _write_scores(ctx: RunTraceContext) -> None:
466470
) as tid:
467471
ctx.score(tid, name="metric_created", value=float(run.metric_created), data_type="BOOLEAN")
468472
ctx.score(tid, name="maql_correct", value=float(run.maql_correct), data_type="BOOLEAN")
473+
ctx.score(tid, name="turns", value=run.total_turns, data_type="NUMERIC")
474+
ctx.score(tid, name="steps", value=run.total_steps, data_type="NUMERIC")
469475
log_gate_scores(ctx, tid, gate=gate, pass_at_k=summary.pass_at_k, pass_power_k=summary.pass_power_k)
470476
ctx.quality(
471477
tid,

packages/gooddata-eval/tests/test_agentic_conversation.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,3 +1025,49 @@ def test_evaluate_agentic_conversation_attaches_reasoning_steps_to_exception_on_
10251025
],
10261026
"latency_breakdown": [],
10271027
}
1028+
1029+
1030+
def test_run_agentic_conversation_sums_the_reasoning_steps_of_every_turn():
1031+
"""QA-29110: the effort comparison reads `steps`. A clarification round is part of the
1032+
work the effort setting changes, so its steps count with the rest."""
1033+
proposal_turn = ChatResult.model_validate(
1034+
{
1035+
"text_response": None,
1036+
"alertProposals": [{"cta": "Should I create this alert?", "recipients": [{"email": "a@b.com"}]}],
1037+
"reasoningStepCount": 2,
1038+
"toolCallEvents": [
1039+
{"functionName": "set_skills", "functionArguments": '{"skills": ["alert"]}', "result": None},
1040+
{"functionName": "prepare_metric_alert_proposal", "functionArguments": "{}", "result": None},
1041+
],
1042+
}
1043+
)
1044+
created_turn = ChatResult.model_validate(
1045+
{
1046+
"text_response": "Alert created.",
1047+
"reasoningStepCount": 3,
1048+
"toolCallEvents": [
1049+
{"functionName": "create_metric_alert", "functionArguments": "{}", "result": '{"id": "alert-1"}'}
1050+
],
1051+
}
1052+
)
1053+
mock_client = MagicMock()
1054+
mock_client.create_conversation.return_value = "conv-1"
1055+
mock_client.send_message.side_effect = [proposal_turn, created_turn]
1056+
1057+
with (
1058+
patch("gooddata_eval.core.agentic.conversation.ChatClient", return_value=mock_client),
1059+
patch("gooddata_eval.core.agentic.conversation.GoodDataSdk"),
1060+
patch(
1061+
"gooddata_eval.core.agentic.conversation._get_sim_user_response",
1062+
return_value="Yes, please create it.",
1063+
),
1064+
):
1065+
result = run_agentic_conversation(
1066+
host="http://host/api/v1/actions/workspaces/ws1/ai",
1067+
token="tok",
1068+
workspace_id="ws1",
1069+
fixture=_alert_turn_fixture(),
1070+
)
1071+
1072+
assert result.total_steps == 5.0
1073+
assert result.total_clarification_turns == 1

packages/gooddata-eval/tests/test_agentic_kda_skill.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,3 +1195,53 @@ def test_evaluate_agentic_kda_skill_preserves_reasoning_from_a_chat_error_partia
11951195

11961196
assert exc_info.value.reasoning_steps == ["analyzing before cutoff"]
11971197
assert exc_info.value.response_id == "resp-3"
1198+
1199+
1200+
def test_run_agentic_kda_skill_counts_the_turns_and_reasoning_steps_it_used():
1201+
"""QA-29110: the effort comparison reads these. A binary pass/fail cannot separate two
1202+
efforts on a nightly's sample, while the reasoning-step count moves with the effort."""
1203+
mock_client = MagicMock()
1204+
mock_client.create_conversation.return_value = "conv-1"
1205+
# Turn 1 asks for clarification, turn 2 runs the analysis: 2 turns, 1 step each.
1206+
mock_client.send_message.side_effect = [
1207+
_no_kda_chat_result("Which metric did you mean?"),
1208+
_kda_chat_result(success=True),
1209+
]
1210+
1211+
with (
1212+
patch("gooddata_eval.core.agentic.kda_skill.ChatClient", return_value=mock_client),
1213+
patch("gooddata_eval.core.agentic.kda_skill.generate_simulated_kda_response", return_value="Revenue"),
1214+
):
1215+
summary = run_agentic_kda_skill(
1216+
host="http://host/api/v1/actions/workspaces/ws1/ai",
1217+
token="tok",
1218+
workspace_id="ws1",
1219+
question="What drove the change?",
1220+
expected_output=_EXPECTED,
1221+
k=1,
1222+
max_iterations=2,
1223+
)
1224+
1225+
assert summary.best.total_turns == 2.0
1226+
assert summary.best.total_steps == 2.0
1227+
1228+
1229+
def test_run_agentic_kda_skill_reports_no_turns_when_the_first_send_fails():
1230+
"""A run that never got a reply must not report a turn it did not take."""
1231+
mock_client = MagicMock()
1232+
mock_client.create_conversation.return_value = "conv-1"
1233+
mock_client.send_message.side_effect = RuntimeError("stream died")
1234+
1235+
with patch("gooddata_eval.core.agentic.kda_skill.ChatClient", return_value=mock_client):
1236+
summary = run_agentic_kda_skill(
1237+
host="http://host/api/v1/actions/workspaces/ws1/ai",
1238+
token="tok",
1239+
workspace_id="ws1",
1240+
question="What drove the change?",
1241+
expected_output=_EXPECTED,
1242+
k=1,
1243+
max_iterations=1,
1244+
)
1245+
1246+
assert summary.best.total_turns == 0.0
1247+
assert summary.best.total_steps == 0.0

0 commit comments

Comments
 (0)