Skip to content

Commit 0e0f3dd

Browse files
authored
Merge pull request #1758 from gooddata/feat/tool-call-latency-breakdown
Per-tool-call/reasoning-step latency breakdown (detail.latency_breakdown)
2 parents 9f97835 + 595c98c commit 0e0f3dd

20 files changed

Lines changed: 623 additions & 21 deletions

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from gooddata_eval.core.agentic._catalog import CatalogMetricAlert
1515
from gooddata_eval.core.chat.sse_client import ChatClient
1616
from gooddata_eval.core.config import ReasoningEffort
17-
from gooddata_eval.core.models import AgenticEvalOutcome, ToolCallEvent
17+
from gooddata_eval.core.models import AgenticEvalOutcome, ReasoningStepEvent, ToolCallEvent, build_latency_breakdown
1818

1919
try:
2020
from openai import OpenAI as _OpenAI
@@ -345,6 +345,8 @@ class AlertRunResult:
345345
actual_alert_arguments: dict
346346
reasoning_steps: list[str] = field(default_factory=list)
347347
response_id: str | None = None
348+
tool_call_events: list[ToolCallEvent] = field(default_factory=list)
349+
reasoning_step_events: list[ReasoningStepEvent] = field(default_factory=list)
348350

349351

350352
@dataclass
@@ -495,6 +497,11 @@ def _run_once(conv_id: str) -> AlertRunResult:
495497
tool_called = False
496498
reasoning_steps: list[str] = []
497499
response_id: str | None = None
500+
all_tool_call_events: list[ToolCallEvent] = []
501+
all_reasoning_step_events: list[ReasoningStepEvent] = []
502+
turn_offset = 0.0 # each turn's call_ts/ts restarts near 0 -- shift by prior turns' wall time
503+
tool_index_offset = 0
504+
reasoning_index_offset = 0
498505
# conversation_history stores prior turns for GPT-4o context.
499506
# Roles follow GPT-4o's perspective: "assistant"=agent text, "user"=sim-user reply.
500507
conversation_history: list = []
@@ -504,6 +511,21 @@ def _run_once(conv_id: str) -> AlertRunResult:
504511
chat_result = client.send_message(conv_id, current_question)
505512
reasoning_steps.extend(chat_result.reasoning_steps or [])
506513
response_id = chat_result.response_id or response_id
514+
for tc in chat_result.tool_call_events or []:
515+
if tc.call_ts is not None:
516+
tc.call_ts += turn_offset
517+
if tc.result_ts is not None:
518+
tc.result_ts += turn_offset
519+
if tc.index is not None:
520+
tc.index += tool_index_offset
521+
for rs in chat_result.reasoning_step_events or []:
522+
rs.ts += turn_offset
523+
rs.index += reasoning_index_offset
524+
all_tool_call_events.extend(chat_result.tool_call_events or [])
525+
all_reasoning_step_events.extend(chat_result.reasoning_step_events or [])
526+
tool_index_offset += len(chat_result.tool_call_events or [])
527+
reasoning_index_offset += len(chat_result.reasoning_step_events or [])
528+
turn_offset += chat_result.turn_wall_clock_sec or 0.0
507529
alert_id, actual_args, tool_called = _extract_alert_call(chat_result.tool_call_events or [])
508530
if tool_called:
509531
alert_id_to_delete = alert_id
@@ -541,6 +563,8 @@ def _run_once(conv_id: str) -> AlertRunResult:
541563
actual_alert_arguments=actual_args,
542564
reasoning_steps=reasoning_steps,
543565
response_id=response_id,
566+
tool_call_events=all_tool_call_events,
567+
reasoning_step_events=all_reasoning_step_events,
544568
)
545569
finally:
546570
if alert_id_to_delete:
@@ -717,6 +741,7 @@ def evaluate_agentic_alert_skill(
717741
"metric_correct": ev.metric_correct,
718742
"recipients_correct": ev.recipients_correct,
719743
"actual_alert_arguments": best.actual_alert_arguments,
744+
"latency_breakdown": build_latency_breakdown(best.tool_call_events, best.reasoning_step_events),
720745
}
721746
raise exc
722747
best = summary.best
@@ -734,5 +759,6 @@ def evaluate_agentic_alert_skill(
734759
"metric_correct": ev.metric_correct,
735760
"recipients_correct": ev.recipients_correct,
736761
"actual_alert_arguments": best.actual_alert_arguments,
762+
"latency_breakdown": build_latency_breakdown(best.tool_call_events, best.reasoning_step_events),
737763
},
738764
)

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
from gooddata_eval.core.agentic.metric_skill import _delete_metric, _extract_created_metric_ids, _extract_metric_result
1616
from gooddata_eval.core.chat.sse_client import ChatClient
1717
from gooddata_eval.core.config import ReasoningEffort
18-
from gooddata_eval.core.models import AgenticEvalOutcome, ChatResult, ToolCallEvent
18+
from gooddata_eval.core.models import (
19+
AgenticEvalOutcome,
20+
ChatResult,
21+
ReasoningStepEvent,
22+
ToolCallEvent,
23+
build_latency_breakdown,
24+
)
1925
from gooddata_eval.core.scoring import (
2026
check_filters,
2127
check_viz_type,
@@ -254,6 +260,8 @@ class ConversationResult:
254260
total_clarification_turns: int
255261
reasoning_steps: list[str] = field(default_factory=list)
256262
response_id: str | None = None
263+
tool_call_events: list[ToolCallEvent] = field(default_factory=list)
264+
reasoning_step_events: list[ReasoningStepEvent] = field(default_factory=list)
257265

258266

259267
def run_agentic_conversation(
@@ -287,6 +295,14 @@ def run_agentic_conversation(
287295
created_metric_ids: list[str] = []
288296
reasoning_steps: list[str] = []
289297
response_id: str | None = None
298+
conversation_tool_call_events: list[ToolCallEvent] = []
299+
conversation_reasoning_step_events: list[ReasoningStepEvent] = []
300+
# Every send_message() call (across every logical turn AND every clarification
301+
# sub-turn within it) restarts call_ts/ts near 0 -- these run across the whole
302+
# conversation, not reset per logical turn, so every one of those calls shifts them.
303+
turn_offset = 0.0
304+
tool_index_offset = 0
305+
reasoning_index_offset = 0
290306

291307
try:
292308
if initial_conversation_id is not None:
@@ -322,7 +338,22 @@ def run_agentic_conversation(
322338
for _iter in range(max_clarification_turns + 1):
323339
chat_result = client.send_message(conversation_id, current_message)
324340
final_result = chat_result
341+
for tc in chat_result.tool_call_events or []:
342+
if tc.call_ts is not None:
343+
tc.call_ts += turn_offset
344+
if tc.result_ts is not None:
345+
tc.result_ts += turn_offset
346+
if tc.index is not None:
347+
tc.index += tool_index_offset
348+
for rs in chat_result.reasoning_step_events or []:
349+
rs.ts += turn_offset
350+
rs.index += reasoning_index_offset
325351
all_tool_calls.extend(chat_result.tool_call_events or [])
352+
conversation_tool_call_events.extend(chat_result.tool_call_events or [])
353+
conversation_reasoning_step_events.extend(chat_result.reasoning_step_events or [])
354+
tool_index_offset += len(chat_result.tool_call_events or [])
355+
reasoning_index_offset += len(chat_result.reasoning_step_events or [])
356+
turn_offset += chat_result.turn_wall_clock_sec or 0.0
326357
reasoning_steps.extend(chat_result.reasoning_steps or [])
327358
response_id = chat_result.response_id or response_id
328359

@@ -390,6 +421,8 @@ def run_agentic_conversation(
390421
total_clarification_turns=total_clarification_turns,
391422
reasoning_steps=reasoning_steps,
392423
response_id=response_id,
424+
tool_call_events=conversation_tool_call_events,
425+
reasoning_step_events=conversation_reasoning_step_events,
393426
)
394427

395428

@@ -408,6 +441,7 @@ def _conversation_detail(result: ConversationResult) -> dict:
408441
}
409442
for tr in result.turn_results
410443
],
444+
"latency_breakdown": build_latency_breakdown(result.tool_call_events, result.reasoning_step_events),
411445
}
412446

413447

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from gooddata_eval.core.chat.sse_client import ChatClient
99
from gooddata_eval.core.config import ReasoningEffort
1010
from gooddata_eval.core.evaluators._llm_judge import LLMJudge
11-
from gooddata_eval.core.models import AgenticEvalOutcome
11+
from gooddata_eval.core.models import AgenticEvalOutcome, ReasoningStepEvent, ToolCallEvent, build_latency_breakdown
1212

1313
_DEFAULT_K = 1
1414

@@ -52,6 +52,8 @@ class GuardrailResult:
5252
reasoning: str
5353
reasoning_steps: list[str] = field(default_factory=list)
5454
response_id: str | None = None
55+
tool_call_events: list[ToolCallEvent] = field(default_factory=list)
56+
reasoning_step_events: list[ReasoningStepEvent] = field(default_factory=list)
5557

5658

5759
@dataclass
@@ -100,6 +102,8 @@ def run_agentic_guardrail(
100102
reasoning=reasoning,
101103
reasoning_steps=list(chat_result.reasoning_steps or []),
102104
response_id=chat_result.response_id,
105+
tool_call_events=list(chat_result.tool_call_events or []),
106+
reasoning_step_events=list(chat_result.reasoning_step_events or []),
103107
)
104108
)
105109
finally:
@@ -124,6 +128,8 @@ def run_agentic_guardrail(
124128
reasoning=reasoning,
125129
reasoning_steps=list(chat_result.reasoning_steps or []),
126130
response_id=chat_result.response_id,
131+
tool_call_events=list(chat_result.tool_call_events or []),
132+
reasoning_step_events=list(chat_result.reasoning_step_events or []),
127133
)
128134
)
129135
finally:
@@ -245,6 +251,7 @@ def evaluate_agentic_guardrail(
245251
"judge_passed": best.passed,
246252
"judge_reasoning": best.reasoning,
247253
"actual_output": best.actual_output,
254+
"latency_breakdown": build_latency_breakdown(best.tool_call_events, best.reasoning_step_events),
248255
}
249256
raise exc
250257
best = summary.best
@@ -256,5 +263,6 @@ def evaluate_agentic_guardrail(
256263
"judge_passed": best.passed,
257264
"judge_reasoning": best.reasoning,
258265
"actual_output": best.actual_output,
266+
"latency_breakdown": build_latency_breakdown(best.tool_call_events, best.reasoning_step_events),
259267
},
260268
)

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from gooddata_eval.core.chat.sse_client import ChatClient
1414
from gooddata_eval.core.config import ReasoningEffort
15-
from gooddata_eval.core.models import AgenticEvalOutcome, ToolCallEvent
15+
from gooddata_eval.core.models import AgenticEvalOutcome, ReasoningStepEvent, ToolCallEvent, build_latency_breakdown
1616

1717
try:
1818
from openai import OpenAI as _OpenAI
@@ -150,6 +150,8 @@ class MetricRunResult:
150150
total_turns: float
151151
reasoning_steps: list[str] = field(default_factory=list)
152152
response_id: str | None = None
153+
tool_call_events: list[ToolCallEvent] = field(default_factory=list)
154+
reasoning_step_events: list[ReasoningStepEvent] = field(default_factory=list)
153155

154156

155157
@dataclass
@@ -239,13 +241,33 @@ def _execute_single_metric_run(
239241
current_question = question
240242
reasoning_steps: list[str] = []
241243
response_id: str | None = None
244+
all_tool_call_events: list[ToolCallEvent] = []
245+
all_reasoning_step_events: list[ReasoningStepEvent] = []
246+
turn_offset = 0.0 # each turn's call_ts/ts restarts near 0 -- shift by prior turns' wall time
247+
tool_index_offset = 0
248+
reasoning_index_offset = 0
242249

243250
try:
244251
for _iteration in range(max_iterations):
245252
turns += 1
246253
chat_result = client.send_message(conversation_id, current_question)
247254
reasoning_steps.extend(chat_result.reasoning_steps or [])
248255
response_id = chat_result.response_id or response_id
256+
for tc in chat_result.tool_call_events or []:
257+
if tc.call_ts is not None:
258+
tc.call_ts += turn_offset
259+
if tc.result_ts is not None:
260+
tc.result_ts += turn_offset
261+
if tc.index is not None:
262+
tc.index += tool_index_offset
263+
for rs in chat_result.reasoning_step_events or []:
264+
rs.ts += turn_offset
265+
rs.index += reasoning_index_offset
266+
all_tool_call_events.extend(chat_result.tool_call_events or [])
267+
all_reasoning_step_events.extend(chat_result.reasoning_step_events or [])
268+
tool_index_offset += len(chat_result.tool_call_events or [])
269+
reasoning_index_offset += len(chat_result.reasoning_step_events or [])
270+
turn_offset += chat_result.turn_wall_clock_sec or 0.0
249271
for metric_id in _extract_created_metric_ids(chat_result.tool_call_events or []):
250272
if metric_id not in created_metric_ids:
251273
created_metric_ids.append(metric_id)
@@ -276,6 +298,8 @@ def _execute_single_metric_run(
276298
total_turns=float(turns),
277299
reasoning_steps=reasoning_steps,
278300
response_id=response_id,
301+
tool_call_events=all_tool_call_events,
302+
reasoning_step_events=all_reasoning_step_events,
279303
)
280304
finally:
281305
for metric_id in created_metric_ids:
@@ -457,6 +481,7 @@ def evaluate_agentic_metric_skill(
457481
"maql_correct": best.maql_correct,
458482
"expected_maql_candidates": [c.get("maql", "") for c in expected_outputs_list],
459483
"actual_maql": best.actual_maql,
484+
"latency_breakdown": build_latency_breakdown(best.tool_call_events, best.reasoning_step_events),
460485
}
461486
raise exc
462487
best = summary.best
@@ -470,5 +495,6 @@ def evaluate_agentic_metric_skill(
470495
"maql_correct": best.maql_correct,
471496
"expected_maql_candidates": [c.get("maql", "") for c in expected_outputs_list],
472497
"actual_maql": best.actual_maql,
498+
"latency_breakdown": build_latency_breakdown(best.tool_call_events, best.reasoning_step_events),
473499
},
474500
)

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@
1818
_evaluate_against_candidates,
1919
evaluation_result_detail,
2020
)
21-
from gooddata_eval.core.models import AgenticEvalOutcome, CreatedVisualization, ToolCallEvent
21+
from gooddata_eval.core.models import (
22+
AgenticEvalOutcome,
23+
CreatedVisualization,
24+
ReasoningStepEvent,
25+
ToolCallEvent,
26+
build_latency_breakdown,
27+
)
2228
from gooddata_eval.core.scoring import get_dimension_uri_set, get_metric_uri_set, uri_to_display_name
2329

2430
_DEFAULT_K = 2
@@ -37,6 +43,8 @@ class RunResult:
3743
total_steps: float
3844
reasoning_steps: list[str] = field(default_factory=list)
3945
response_id: str | None = None
46+
tool_call_events: list[ToolCallEvent] = field(default_factory=list)
47+
reasoning_step_events: list[ReasoningStepEvent] = field(default_factory=list)
4048

4149

4250
@dataclass
@@ -161,18 +169,36 @@ def _execute_single_run(
161169
total_turns = 0.0
162170
total_steps = 0.0
163171
all_tool_call_events: list[ToolCallEvent] = []
172+
all_reasoning_step_events: list[ReasoningStepEvent] = []
164173
reasoning_steps: list[str] = []
165174
response_id: str | None = None
175+
turn_offset = 0.0 # each turn's call_ts/ts restarts near 0 -- shift by prior turns' wall time
176+
reasoning_index_offset = 0 # ditto for ReasoningStepEvent.index, which also restarts per turn
177+
tool_index_offset = 0 # ditto for ToolCallEvent.index
166178
simulated_response_guide = expected_outputs[0] # primary candidate guides the simulated user
167179

168180
current_result = client.send_message(conversation_id, question)
169181

170182
for iteration in range(max_iterations):
171183
total_turns += 1.0
172184
total_steps += float(current_result.reasoning_step_count)
185+
for tc in current_result.tool_call_events:
186+
if tc.call_ts is not None:
187+
tc.call_ts += turn_offset
188+
if tc.result_ts is not None:
189+
tc.result_ts += turn_offset
190+
if tc.index is not None:
191+
tc.index += tool_index_offset
192+
for rs in current_result.reasoning_step_events:
193+
rs.ts += turn_offset
194+
rs.index += reasoning_index_offset
173195
all_tool_call_events.extend(current_result.tool_call_events)
196+
all_reasoning_step_events.extend(current_result.reasoning_step_events)
197+
tool_index_offset += len(current_result.tool_call_events)
198+
reasoning_index_offset += len(current_result.reasoning_step_events)
174199
reasoning_steps.extend(current_result.reasoning_steps or [])
175200
response_id = current_result.response_id or response_id
201+
turn_offset += current_result.turn_wall_clock_sec or 0.0
176202

177203
viz_produced = bool(current_result.created_visualizations and current_result.created_visualizations.objects)
178204
if viz_produced:
@@ -201,6 +227,8 @@ def _execute_single_run(
201227
total_steps=total_steps,
202228
reasoning_steps=reasoning_steps,
203229
response_id=response_id,
230+
tool_call_events=all_tool_call_events,
231+
reasoning_step_events=all_reasoning_step_events,
204232
)
205233

206234

@@ -434,12 +462,18 @@ def evaluate_agentic_visualization(
434462
exc.reasoning_steps = best.reasoning_steps
435463
exc.conversation_id = best.conversation_id
436464
exc.response_id = best.response_id
437-
exc.detail = evaluation_result_detail(ev)
465+
exc.detail = {
466+
**evaluation_result_detail(ev),
467+
"latency_breakdown": build_latency_breakdown(best.tool_call_events, best.reasoning_step_events),
468+
}
438469
raise exc
439470
best = summary.best
440471
return AgenticEvalOutcome(
441472
reasoning_steps=best.reasoning_steps,
442473
conversation_id=best.conversation_id,
443474
response_id=best.response_id,
444-
detail=evaluation_result_detail(best.eval_result),
475+
detail={
476+
**evaluation_result_detail(best.eval_result),
477+
"latency_breakdown": build_latency_breakdown(best.tool_call_events, best.reasoning_step_events),
478+
},
445479
)

0 commit comments

Comments
 (0)