Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions ag2/eval/scorers/attribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ async def _attribute(


def _detect_mechanical(trace: Trace, agent_name: str | None) -> Attribution | None:
"""Attribute unambiguous mechanical failures; ``None`` to defer to the LLM."""
"""Attribute mechanical failures using the final model response; ``None`` to defer to the LLM."""
events = trace.events

if trace.exception is not None:
Expand All @@ -157,7 +157,8 @@ def _detect_mechanical(trace: Trace, agent_name: str | None) -> Attribution | No
f"run raised {type(trace.exception).__name__}: {trace.exception}",
)

has_answer = any(r.content for r in trace.events_of(ModelResponse))
responses = trace.events_of(ModelResponse)
has_answer = bool(responses and responses[-1].content)
tool_errors = trace.events_of(ToolErrorEvent)
if tool_errors and not has_answer:
return _mech(
Expand Down
41 changes: 40 additions & 1 deletion test/eval/scorers/test_attribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
"""Tests for failure attribution (deterministic detectors + LLM attributor)."""

import pytest
from dirty_equals import IsPartialDict

from ag2.eval import InMemoryTraceSource, TraceRef, evaluate_traces
from ag2.eval.dataset.task import Task
from ag2.eval.scorers import failure_attribution
from ag2.eval.trace import Trace
from ag2.events import ModelMessage, ModelResponse, ToolCallEvent, ToolErrorEvent
from ag2.events import ModelMessage, ModelResponse, ToolCallEvent, ToolErrorEvent, ToolResultEvent
from ag2.testing import TestConfig


Expand Down Expand Up @@ -50,6 +51,44 @@ async def test_tool_failure_records_decisive_step() -> None:
assert fb.detail["decisive_step"] == 1 # index of the error event


@pytest.mark.asyncio()
@pytest.mark.parametrize(
("tool_failed", "final_answer", "expected_mode"),
[
(False, None, "no_answer"),
(True, None, "tool_failure"),
(False, "", "no_answer"),
(True, "", "tool_failure"),
(False, "The answer is 42.", "none"),
(True, "The answer is 42.", "none"),
],
)
async def test_interim_text_does_not_hide_final_answer_state(
tool_failed: bool, final_answer: str | None, expected_mode: str
) -> None:
call = ToolCallEvent("lookup", arguments="{}")
tool_result = (
ToolErrorEvent.from_call(call, RuntimeError("lookup failed"))
if tool_failed
else ToolResultEvent.from_call(call, result="42")
)
trace = _trace([
ModelResponse(message=ModelMessage("I will look that up.")),
call,
tool_result,
ModelResponse(message=ModelMessage(final_answer) if final_answer is not None else None),
])

[fb] = await _attribute(failure_attribution(), trace, outputs={"body": final_answer})

assert fb.value == expected_mode
assert fb.detail == IsPartialDict({
"failed": not bool(final_answer),
"error_mode": expected_mode,
"decisive_step": 2 if expected_mode == "tool_failure" else None,
})


@pytest.mark.asyncio()
async def test_clean_run_without_llm_is_none() -> None:
[fb] = await _attribute(
Expand Down
Loading