Files
wehub-resource-sync ec2b666284
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:25:13 +08:00

194 lines
6.6 KiB
Python

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Library for rating agent trajectories."""
from __future__ import annotations
import re
from typing import Any
from absl import logging
from google.genai import types
import jinja2
from retry import retry
from google import genai
def parse_rubric_validation_response(
rubric_val_response: str,
) -> dict[str, str]:
"""Parses rubric validation response text into a dictionary.
Args:
rubric_val_response: The text response from rubric validation.
Returns:
A dictionary containing parsed property, evidence, rationale, and verdict.
"""
PROPERTY_PATTERN = (
r'Property:\s*([\s\S]*?)(?=(?:Evidence:|Rationale:|Verdict:|$))'
)
EVIDENCE_PATTERN = r'Evidence:\s*([\s\S]*?)(?=(?:Rationale:|Verdict:|$))'
RATIONALE_PATTERN = r'Rationale:\s*([\s\S]*?)(?=(?:Evidence:|Verdict:|$))'
VERDICT_PATTERN = r'Verdict:\s*([\s\S]*?)(?=(?:Evidence:|Rationale:|$))'
property_list = []
evidence_list = []
rationale_list = []
fulfillment_list = []
property_blocks = rubric_val_response.split('Property: ')[1:]
for property_block in property_blocks:
property_name = re.search(PROPERTY_PATTERN, 'Property: ' + property_block)
if property_name is None:
continue
property_name = property_name.group(1).strip()
property_list.append(property_name)
evidence_match = re.search(EVIDENCE_PATTERN, property_block, re.DOTALL)
evidence = evidence_match.group(1).strip() if evidence_match else ''
evidence_list.append(evidence)
rationale_match = re.search(RATIONALE_PATTERN, property_block, re.DOTALL)
rationale = rationale_match.group(1).strip() if rationale_match else ''
rationale_list.append(rationale)
verdict = re.search(VERDICT_PATTERN, property_block)
if verdict is None:
verdict_str = 'not_found'
else:
verdict_str = verdict.group(1).strip().lower()
if 'yes' in verdict_str:
verdict_str = 'yes'
elif 'no' in verdict_str:
verdict_str = 'no'
elif 'unknown' in verdict_str:
verdict_str = 'unknown'
else:
verdict_str = 'not_found'
fulfillment_list.append(verdict_str)
return dict(
property=property_list[0],
evidence=evidence_list[0],
rationale=rationale_list[0],
verdict=fulfillment_list[0],
)
def format_user_agent_conversation(conv: list[dict[str, Any]]) -> str:
"""Formats a conversation between user and agent into a string.
Args:
conv: A list of conversation turns.
Returns:
A formatted string representing the conversation.
"""
# conv is a list in this eval data
# if not, manually convert to list to re-use these logics
# if not isinstance(conv, list):
# conv = [conv]
res = ''
turn_idx = 1
for turn in conv:
# if 'request' in conv[turn]:\
role = turn['role']
for part in turn['parts']:
if role == 'user' and (txt := part.get('text')):
res = res + f'USER TURN {turn_idx}:\n' + txt + '\n'
turn_idx += 1
elif role == 'model' and (txt := part.get('text')):
res = res + f'The agent response is: {txt}' + '\n'
elif fc := part.get('function_call'):
res = (
res
+ f'The agent called the function {fc["name"]} with the following'
f' function arguments: {fc["args"]}.\n'
)
elif fc := part.get('function_response'):
res = (
res
+ 'The execution result from the agent of function'
f' {fc["name"]} is: \n{fc["response"]}\n'
)
return res
_COMPLETION_RUBRIC_CRITERIA = """The agent fulfilled the user's primary request. Description: It measures if the agent successfully completed the action the user initiated the contact for (e.g., processed a return, provided a tracking number, answered a policy question). A "yes" requires confirmed completion within the transcript."""
class Rater:
"""Rates agent trajectories using an LLM based on rubrics."""
def __init__(
self,
tool_declarations: str,
developer_instructions: str = '',
rubric: str = _COMPLETION_RUBRIC_CRITERIA,
validation_template_path: str = 'rubric_validation_template.txt',
):
"""Initializes the Rater.
Args:
tool_declarations: JSON string of tool declarations for the agent.
developer_instructions: Developer instructions.
rubric: rubric.
validation_template_path: Path to rubric validation template.
"""
self._client = genai.Client()
self._tool_declarations = tool_declarations
self._developer_instructions = developer_instructions
with open(validation_template_path) as f:
self._rubric_validation_template = f.read().strip()
logging.info(
'Loaded rubric validate template from path=%s', validation_template_path
)
self._rubric = rubric
@retry(tries=3, delay=2, backoff=2)
def __call__(self, messages: list[dict[str, Any]]) -> dict[str, Any]:
"""Rates a conversation based on rubric criteria.
Args:
messages: A list of conversation messages between user and agent.
Returns:
A dictionary containing rating information including score.
"""
env = jinja2.Environment(autoescape=True)
env.globals['user_input'] = (
messages[0].get('parts', [{}])[0].get('text', '') if messages else ''
)
env.globals['developer_instructions'] = self._developer_instructions
env.globals['tool_declarations'] = self._tool_declarations
env.globals['model_response'] = format_user_agent_conversation(messages)
env.globals['decomposed_rubric'] = '* ' + self._rubric
contents = env.from_string(self._rubric_validation_template).render()
resp = self._client.models.generate_content(
model='gemini-2.5-pro',
contents=contents,
config=types.GenerateContentConfig(
candidate_count=1,
thinking_config=types.ThinkingConfig(
include_thoughts=True, thinking_budget=-1
),
),
)
got = parse_rubric_validation_response(resp.text)
got = dict(got)
got['score'] = float(got['verdict'] == 'yes')
got['rating_criteria'] = got.pop('property')
return got