Skip to content

Commit af6f089

Browse files
committed
fix(evals): normalize multi-turn agent responses
1 parent 89d2b91 commit af6f089

3 files changed

Lines changed: 152 additions & 5 deletions

File tree

agentplatform/_genai/_evals_common.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1958,6 +1958,53 @@ def _is_multi_turn_agent_simulation(
19581958
)
19591959

19601960

1961+
def _normalize_agent_event(event: Any) -> Any:
1962+
"""Normalizes raw agent event dictionaries for AgentEvent validation."""
1963+
if not isinstance(event, dict):
1964+
return event
1965+
return {
1966+
key: value
1967+
for key, value in event.items()
1968+
if key in {"author", "content", "event_time", "state_delta", "active_tools"}
1969+
}
1970+
1971+
1972+
def _normalize_conversation_turns(resp_item: Any) -> Any:
1973+
"""Normalizes raw multi-turn responses for ConversationTurn validation."""
1974+
if not isinstance(resp_item, list):
1975+
return resp_item
1976+
1977+
turns = []
1978+
for turn_index, turn in enumerate(resp_item):
1979+
if not isinstance(turn, dict):
1980+
turns.append(turn)
1981+
continue
1982+
1983+
normalized_turn = {
1984+
key: value
1985+
for key, value in turn.items()
1986+
if key in {"turn_index", "turn_id", "events"}
1987+
}
1988+
if normalized_turn.get("turn_index") is None:
1989+
normalized_turn["turn_index"] = turn_index
1990+
if normalized_turn.get("turn_id") is None:
1991+
turn_id = turn.get("turn_id") or turn.get("id") or turn.get("invocation_id")
1992+
if turn_id is not None:
1993+
normalized_turn["turn_id"] = turn_id
1994+
1995+
if "events" in normalized_turn and normalized_turn["events"] is not None:
1996+
normalized_turn["events"] = [
1997+
_normalize_agent_event(event) for event in normalized_turn["events"]
1998+
]
1999+
else:
2000+
event = _normalize_agent_event(turn)
2001+
if event:
2002+
normalized_turn["events"] = [event]
2003+
2004+
turns.append(normalized_turn)
2005+
return turns
2006+
2007+
19612008
def _process_multi_turn_agent_response(
19622009
resp_item: Any,
19632010
agent_data_agents: Optional[dict[str, Any]],
@@ -1966,7 +2013,7 @@ def _process_multi_turn_agent_response(
19662013
if isinstance(resp_item, dict) and "error" in resp_item:
19672014
return json.dumps(resp_item)
19682015
return types.evals.AgentData(
1969-
turns=resp_item,
2016+
turns=_normalize_conversation_turns(resp_item),
19702017
agents=agent_data_agents,
19712018
).model_dump(exclude_unset=True)
19722019

tests/unit/agentplatform/genai/test_evals.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4438,6 +4438,61 @@ def test_run_agent_internal_multi_turn_success(self, mock_run_agent):
44384438
{"turn_index": 1, "turn_id": "t2", "events": []},
44394439
]
44404440

4441+
@mock.patch.object(_evals_common, "_run_agent")
4442+
def test_run_agent_internal_multi_turn_normalizes_raw_events(self, mock_run_agent):
4443+
mock_run_agent.return_value = [
4444+
[
4445+
{
4446+
"id": "event-1",
4447+
"author": "agent",
4448+
"content": {"parts": [{"text": "hello"}]},
4449+
"model_version": "gemini-2.5-flash",
4450+
"timestamp": "2026-01-01T00:00:00Z",
4451+
},
4452+
{
4453+
"invocation_id": "event-2",
4454+
"author": "agent",
4455+
"content": {"parts": [{"text": "bye"}]},
4456+
"actions": {"state_delta": {}},
4457+
"usage_metadata": {"total_token_count": 12},
4458+
},
4459+
]
4460+
]
4461+
prompt_dataset = pd.DataFrame({"prompt": ["p1"], "conversation_plan": ["plan"]})
4462+
mock_agent_engine = mock.Mock()
4463+
mock_api_client = mock.Mock()
4464+
4465+
result_df = _evals_common._run_agent_internal(
4466+
api_client=mock_api_client,
4467+
agent_engine=mock_agent_engine,
4468+
agent=None,
4469+
prompt_dataset=prompt_dataset,
4470+
)
4471+
4472+
agent_data = result_df["agent_data"][0]
4473+
assert agent_data["turns"] == [
4474+
{
4475+
"turn_index": 0,
4476+
"turn_id": "event-1",
4477+
"events": [
4478+
{
4479+
"author": "agent",
4480+
"content": {"parts": [{"text": "hello"}]},
4481+
}
4482+
],
4483+
},
4484+
{
4485+
"turn_index": 1,
4486+
"turn_id": "event-2",
4487+
"events": [
4488+
{
4489+
"author": "agent",
4490+
"content": {"parts": [{"text": "bye"}]},
4491+
}
4492+
],
4493+
},
4494+
]
4495+
44414496
@mock.patch.object(_evals_common, "_run_agent")
44424497
def test_run_agent_internal_multi_turn_with_agent(self, mock_run_agent):
44434498
mock_run_agent.return_value = [
@@ -6578,9 +6633,7 @@ def test_groundedness_aliases_grounding_v1(self):
65786633
assert lazy_metric._get_api_metric_spec_name() == "grounding_v1"
65796634

65806635
def test_groundedness_logs_field_difference_warning(self, caplog):
6581-
loader_logger = (
6582-
"agentplatform._genai._evals_metric_loaders"
6583-
)
6636+
loader_logger = "agentplatform._genai._evals_metric_loaders"
65846637
with caplog.at_level("WARNING", logger=loader_logger):
65856638
_ = agentplatform_genai_types.RubricMetric.GROUNDEDNESS
65866639
messages = [r.getMessage() for r in caplog.records if r.name == loader_logger]

vertexai/_genai/_evals_common.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1939,6 +1939,53 @@ def _is_multi_turn_agent_simulation(
19391939
)
19401940

19411941

1942+
def _normalize_agent_event(event: Any) -> Any:
1943+
"""Normalizes raw agent event dictionaries for AgentEvent validation."""
1944+
if not isinstance(event, dict):
1945+
return event
1946+
return {
1947+
key: value
1948+
for key, value in event.items()
1949+
if key in {"author", "content", "event_time", "state_delta", "active_tools"}
1950+
}
1951+
1952+
1953+
def _normalize_conversation_turns(resp_item: Any) -> Any:
1954+
"""Normalizes raw multi-turn responses for ConversationTurn validation."""
1955+
if not isinstance(resp_item, list):
1956+
return resp_item
1957+
1958+
turns = []
1959+
for turn_index, turn in enumerate(resp_item):
1960+
if not isinstance(turn, dict):
1961+
turns.append(turn)
1962+
continue
1963+
1964+
normalized_turn = {
1965+
key: value
1966+
for key, value in turn.items()
1967+
if key in {"turn_index", "turn_id", "events"}
1968+
}
1969+
if normalized_turn.get("turn_index") is None:
1970+
normalized_turn["turn_index"] = turn_index
1971+
if normalized_turn.get("turn_id") is None:
1972+
turn_id = turn.get("turn_id") or turn.get("id") or turn.get("invocation_id")
1973+
if turn_id is not None:
1974+
normalized_turn["turn_id"] = turn_id
1975+
1976+
if "events" in normalized_turn and normalized_turn["events"] is not None:
1977+
normalized_turn["events"] = [
1978+
_normalize_agent_event(event) for event in normalized_turn["events"]
1979+
]
1980+
else:
1981+
event = _normalize_agent_event(turn)
1982+
if event:
1983+
normalized_turn["events"] = [event]
1984+
1985+
turns.append(normalized_turn)
1986+
return turns
1987+
1988+
19421989
def _process_multi_turn_agent_response(
19431990
resp_item: Any,
19441991
agent_data_agents: Optional[dict[str, Any]],
@@ -1947,7 +1994,7 @@ def _process_multi_turn_agent_response(
19471994
if isinstance(resp_item, dict) and "error" in resp_item:
19481995
return json.dumps(resp_item)
19491996
return types.evals.AgentData(
1950-
turns=resp_item,
1997+
turns=_normalize_conversation_turns(resp_item),
19511998
agents=agent_data_agents,
19521999
).model_dump(exclude_unset=True)
19532000

0 commit comments

Comments
 (0)