Skip to content

Commit 588b985

Browse files
authored
Merge pull request #1708 from gooddata/feat/chat-client-reasoning-steps
feat(gooddata-eval): capture agent reasoning steps in ChatResult
2 parents 47b3216 + 7ad177b commit 588b985

15 files changed

Lines changed: 627 additions & 28 deletions

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

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from gooddata_eval.core.agentic.search_tool import evaluate_agentic_search_tool
1717
from gooddata_eval.core.agentic.visualization import evaluate_agentic_visualization
1818
from gooddata_eval.core.config import ReasoningEffort
19-
from gooddata_eval.core.models import CreatedVisualization, DatasetItem
19+
from gooddata_eval.core.models import AgenticEvalOutcome, CreatedVisualization, DatasetItem
2020
from gooddata_eval.core.runner import EvalReport, ItemReport
2121

2222

@@ -86,8 +86,13 @@ 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+
) -> AgenticEvalOutcome | list[str] | None:
90+
"""Call the appropriate evaluate_agentic_* function for the item's test_kind.
91+
92+
Returns whatever that function returns -- alert_skill/metric_skill/conversation return
93+
an AgenticEvalOutcome; the rest still return None
94+
(unchanged).
95+
"""
9196
kind = item.test_kind
9297
eo = item.expected_output
9398
lf_kw: _LfKw = {
@@ -100,7 +105,7 @@ def _dispatch_agentic(
100105
}
101106

102107
if kind in ("vis_agentic", "agentic_visualization"):
103-
evaluate_agentic_visualization(
108+
return evaluate_agentic_visualization(
104109
host=host,
105110
token=token,
106111
workspace_id=workspace_id,
@@ -111,7 +116,7 @@ def _dispatch_agentic(
111116
**lf_kw,
112117
)
113118
elif kind == "agentic_metric_skill":
114-
evaluate_agentic_metric_skill(
119+
return evaluate_agentic_metric_skill(
115120
host=host,
116121
token=token,
117122
workspace_id=workspace_id,
@@ -122,7 +127,7 @@ def _dispatch_agentic(
122127
**lf_kw,
123128
)
124129
elif kind == "agentic_alert_skill":
125-
evaluate_agentic_alert_skill(
130+
return evaluate_agentic_alert_skill(
126131
host=host,
127132
token=token,
128133
workspace_id=workspace_id,
@@ -136,7 +141,7 @@ def _dispatch_agentic(
136141
eo_dict = eo if isinstance(eo, dict) else {}
137142
tool_call = eo_dict.get("tool_call", {})
138143
expected_args = tool_call.get("function_arguments", eo_dict)
139-
evaluate_agentic_search_tool(
144+
return evaluate_agentic_search_tool(
140145
host=host,
141146
token=token,
142147
workspace_id=workspace_id,
@@ -147,7 +152,7 @@ def _dispatch_agentic(
147152
**lf_kw,
148153
)
149154
elif kind == "agentic_general_question":
150-
evaluate_agentic_general_question(
155+
return evaluate_agentic_general_question(
151156
host=host,
152157
token=token,
153158
workspace_id=workspace_id,
@@ -158,7 +163,7 @@ def _dispatch_agentic(
158163
**lf_kw,
159164
)
160165
elif kind == "agentic_guardrail":
161-
evaluate_agentic_guardrail(
166+
return evaluate_agentic_guardrail(
162167
host=host,
163168
token=token,
164169
workspace_id=workspace_id,
@@ -180,7 +185,7 @@ def _dispatch_agentic(
180185
)
181186
elif kind == "agentic_conversation":
182187
fixture_data = eo.get("fixture") or eo if isinstance(eo, dict) else {}
183-
evaluate_agentic_conversation(
188+
return evaluate_agentic_conversation(
184189
host=host,
185190
token=token,
186191
workspace_id=workspace_id,
@@ -228,14 +233,26 @@ def run_agentic_items(
228233
)
229234
t0 = time.perf_counter()
230235
try:
231-
_dispatch_agentic(
236+
outcome = _dispatch_agentic(
232237
item, host, token, workspace_id, k, langfuse, run_ts, model_version, reasoning_effort, agent_id
233238
)
239+
if isinstance(outcome, AgenticEvalOutcome):
240+
reasoning_steps = outcome.reasoning_steps
241+
conversation_id = outcome.conversation_id
242+
response_id = outcome.response_id
243+
else:
244+
reasoning_steps, conversation_id, response_id = outcome, None, None
234245
item_report.pass_at_k = True
235246
item_report.runs = k
247+
item_report.reasoning_steps = reasoning_steps or []
248+
item_report.conversation_id = conversation_id
249+
item_report.response_id = response_id
236250
except AssertionError as exc:
237251
item_report.pass_at_k = False
238252
item_report.runs = k
253+
item_report.reasoning_steps = getattr(exc, "reasoning_steps", None) or []
254+
item_report.conversation_id = getattr(exc, "conversation_id", None)
255+
item_report.response_id = getattr(exc, "response_id", None)
239256
print(f"[agentic] {item.id} FAIL: {exc}", flush=True)
240257
except Exception as exc:
241258
item_report.error = f"{type(exc).__name__}: {exc}"

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

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
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
1313

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 ToolCallEvent
17+
from gooddata_eval.core.models import AgenticEvalOutcome, ToolCallEvent
1818

1919
try:
2020
from openai import OpenAI as _OpenAI
@@ -333,6 +333,8 @@ class AlertRunResult:
333333
alert_id: str | None
334334
eval: AlertEvaluation
335335
actual_alert_arguments: dict
336+
reasoning_steps: list[str] = field(default_factory=list)
337+
response_id: str | None = None
336338

337339

338340
@dataclass
@@ -481,13 +483,17 @@ def _run_once(conv_id: str) -> AlertRunResult:
481483
alert_id: str | None = None
482484
actual_args: dict = {}
483485
tool_called = False
486+
reasoning_steps: list[str] = []
487+
response_id: str | None = None
484488
# conversation_history stores prior turns for GPT-4o context.
485489
# Roles follow GPT-4o's perspective: "assistant"=agent text, "user"=sim-user reply.
486490
conversation_history: list = []
487491
current_question = question
488492

489493
for _iteration in range(max_iterations):
490494
chat_result = client.send_message(conv_id, current_question)
495+
reasoning_steps.extend(chat_result.reasoning_steps or [])
496+
response_id = chat_result.response_id or response_id
491497
alert_id, actual_args, tool_called = _extract_alert_call(chat_result.tool_call_events or [])
492498
if tool_called:
493499
alert_id_to_delete = alert_id
@@ -523,6 +529,8 @@ def _run_once(conv_id: str) -> AlertRunResult:
523529
alert_id=alert_id,
524530
eval=ev,
525531
actual_alert_arguments=actual_args,
532+
reasoning_steps=reasoning_steps,
533+
response_id=response_id,
526534
)
527535
finally:
528536
if alert_id_to_delete:
@@ -573,6 +581,9 @@ class AlertSkillAssertionError(AssertionError):
573581
"""Raised when an alert-skill evaluation fails."""
574582

575583
__tracebackhide__ = True
584+
reasoning_steps: list[str]
585+
conversation_id: str
586+
response_id: str | None
576587

577588

578589
def evaluate_agentic_alert_skill(
@@ -592,8 +603,16 @@ def evaluate_agentic_alert_skill(
592603
model_version_override: str | None = None,
593604
run_metadata_extra: dict | None = None,
594605
reasoning_effort: ReasoningEffort | None = None,
595-
) -> None:
596-
"""Run alert-skill evaluation, log to Langfuse, and raise AlertSkillAssertionError on failure."""
606+
) -> AgenticEvalOutcome:
607+
"""Run alert-skill evaluation, log to Langfuse, and raise AlertSkillAssertionError on failure.
608+
609+
Returns the best run's outcome (reasoning_steps, conversation_id, response_id) as an
610+
AgenticEvalOutcome on success; on failure the same three values are attached to the
611+
raised exception as
612+
``.reasoning_steps``/``.conversation_id``/``.response_id`` (mirrors the
613+
`conversation_id`-on-exception idiom in `ChatClient.ask()`) so callers can retrieve them
614+
either way.
615+
"""
597616
from datetime import datetime as _dt # noqa: PLC0415
598617
from datetime import timezone as _tz # noqa: PLC0415
599618

@@ -667,11 +686,20 @@ def evaluate_agentic_alert_skill(
667686
if not summary.pass_at_k:
668687
best = summary.best
669688
ev = best.eval
670-
raise AlertSkillAssertionError(
689+
exc = AlertSkillAssertionError(
671690
f"Alert skill assertion failed. strict_pass={ev.strict_pass}. "
672691
f"alert_created={ev.alert_created}, operator_correct={ev.operator_correct}, "
673692
f"threshold_correct={ev.threshold_correct}, trigger_correct={ev.trigger_correct}, "
674693
f"filters_correct={ev.filters_correct}, metric_correct={ev.metric_correct}, "
675694
f"recipients_correct={ev.recipients_correct}. "
676695
f"Actual args: {best.actual_alert_arguments}"
677696
)
697+
exc.reasoning_steps = best.reasoning_steps
698+
exc.conversation_id = best.conversation_id
699+
exc.response_id = best.response_id
700+
raise exc
701+
return AgenticEvalOutcome(
702+
reasoning_steps=summary.best.reasoning_steps,
703+
conversation_id=summary.best.conversation_id,
704+
response_id=summary.best.response_id,
705+
)

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

Lines changed: 33 additions & 5 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
@@ -15,7 +15,7 @@
1515
from gooddata_eval.core.agentic.metric_skill import _delete_metric, _extract_created_metric_ids
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 ChatResult, ToolCallEvent
18+
from gooddata_eval.core.models import AgenticEvalOutcome, ChatResult, ToolCallEvent
1919
from gooddata_eval.core.scoring import (
2020
check_filters,
2121
check_viz_type,
@@ -265,6 +265,8 @@ 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)
269+
response_id: str | None = None
268270

269271

270272
def run_agentic_conversation(
@@ -296,6 +298,8 @@ def run_agentic_conversation(
296298
# not persist in the (shared) workspace and get reused by a later test. Deferred to
297299
# the end — a later turn may $ref a metric an earlier turn created.
298300
created_metric_ids: list[str] = []
301+
reasoning_steps: list[str] = []
302+
response_id: str | None = None
299303

300304
try:
301305
if initial_conversation_id is not None:
@@ -332,6 +336,8 @@ def run_agentic_conversation(
332336
chat_result = client.send_message(conversation_id, current_message)
333337
final_result = chat_result
334338
all_tool_calls.extend(chat_result.tool_call_events or [])
339+
reasoning_steps.extend(chat_result.reasoning_steps or [])
340+
response_id = chat_result.response_id or response_id
335341

336342
if _check_output_present(resolved_turn, chat_result):
337343
break
@@ -395,13 +401,18 @@ def run_agentic_conversation(
395401
full_skill_coverage=full_skill_coverage,
396402
conversation_success=conversation_success,
397403
total_clarification_turns=total_clarification_turns,
404+
reasoning_steps=reasoning_steps,
405+
response_id=response_id,
398406
)
399407

400408

401409
class ConversationAssertionError(AssertionError):
402410
"""Raised when a conversation evaluation fails."""
403411

404412
__tracebackhide__ = True
413+
reasoning_steps: list[str]
414+
conversation_id: str
415+
response_id: str | None
405416

406417

407418
def evaluate_agentic_conversation(
@@ -419,8 +430,16 @@ def evaluate_agentic_conversation(
419430
model_version_override: str | None = None,
420431
run_metadata_extra: dict | None = None,
421432
reasoning_effort: ReasoningEffort | None = None,
422-
) -> None:
423-
"""Run conversation evaluation, log to Langfuse, and raise on failure."""
433+
) -> AgenticEvalOutcome:
434+
"""Run conversation evaluation, log to Langfuse, and raise on failure.
435+
436+
Returns the conversation's outcome (reasoning_steps, conversation_id, response_id) as
437+
an AgenticEvalOutcome on success; on failure the same three values are attached to the
438+
raised exception as
439+
``.reasoning_steps``/``.conversation_id``/``.response_id`` (mirrors the
440+
`conversation_id`-on-exception idiom in `ChatClient.ask()`) so callers can retrieve them
441+
either way.
442+
"""
424443
from datetime import datetime as _dt # noqa: PLC0415
425444
from datetime import timezone as _tz # noqa: PLC0415
426445

@@ -497,8 +516,17 @@ def evaluate_agentic_conversation(
497516

498517
if not result.conversation_success:
499518
failed_turns = [tr for tr in result.turn_results if not tr.skill_success]
500-
raise ConversationAssertionError(
519+
exc = ConversationAssertionError(
501520
f"Conversation assertion failed. "
502521
f"full_skill_coverage={result.full_skill_coverage}. "
503522
f"Failed turns: {[t.turn_id for t in failed_turns]}"
504523
)
524+
exc.reasoning_steps = result.reasoning_steps
525+
exc.conversation_id = result.conversation_id
526+
exc.response_id = result.response_id
527+
raise exc
528+
return AgenticEvalOutcome(
529+
reasoning_steps=result.reasoning_steps,
530+
conversation_id=result.conversation_id,
531+
response_id=result.response_id,
532+
)

0 commit comments

Comments
 (0)