Skip to content

Commit faacc4f

Browse files
authored
Merge branch 'main' into fix/openapi-tool-timeout
2 parents 1e9537a + dbd6420 commit faacc4f

24 files changed

+4560
-1068
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ dependencies = [
5656
"opentelemetry-resourcedetector-gcp>=1.9.0a0, <2.0.0",
5757
"opentelemetry-sdk>=1.36.0, <1.40.0",
5858
"pyarrow>=14.0.0",
59-
"pydantic>=2.0, <3.0.0", # For data validation/models
59+
"pydantic>=2.7.0, <3.0.0", # For data validation/models
6060
"python-dateutil>=2.9.0.post0, <3.0.0", # For Vertext AI Session Service
6161
"python-dotenv>=1.0.0, <2.0.0", # To manage environment variables
6262
"requests>=2.32.4, <3.0.0",
@@ -108,6 +108,7 @@ community = [
108108

109109
eval = [
110110
# go/keep-sorted start
111+
"Jinja2>=3.1.4,<4.0.0", # For eval template rendering
111112
"google-cloud-aiplatform[evaluation]>=1.100.0",
112113
"pandas>=2.2.3",
113114
"rouge-score>=0.1.2",

src/google/adk/evaluation/conversation_scenarios.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
1414

1515
from __future__ import annotations
1616

17+
from typing import Optional
18+
1719
from pydantic import Field
20+
from pydantic import field_validator
1821

1922
from .common import EvalBaseModel
23+
from .simulation.pre_built_personas import get_default_persona_registry
24+
from .simulation.user_simulator_personas import UserPersona
2025

2126

2227
class ConversationScenario(EvalBaseModel):
@@ -48,6 +53,18 @@ class ConversationScenario(EvalBaseModel):
4853
your overall goal is complete.
4954
"""
5055

56+
user_persona: Optional[UserPersona] = Field(default=None)
57+
"""User persona that the user simulator should adopt. If a persona id is specified instead, we will try to use one of our default personas."""
58+
59+
@field_validator("user_persona", mode="before")
60+
@classmethod
61+
def validate_user_persona(
62+
cls, value: Optional[UserPersona | str]
63+
) -> Optional[UserPersona]:
64+
if value is not None and isinstance(value, str):
65+
return get_default_persona_registry().get_persona(value)
66+
return value
67+
5168

5269
class ConversationScenarios(EvalBaseModel):
5370
"""A simple container for the list of ConversationScenario.

src/google/adk/evaluation/simulation/llm_backed_user_simulator.py

Lines changed: 23 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
from .._retry_options_utils import add_default_retry_options_if_not_present
3232
from ..conversation_scenarios import ConversationScenario
3333
from ..evaluator import Evaluator
34+
from .llm_backed_user_simulator_prompts import get_llm_backed_user_simulator_prompt
35+
from .llm_backed_user_simulator_prompts import is_valid_user_simulator_template
3436
from .user_simulator import BaseUserSimulatorConfig
3537
from .user_simulator import NextUserMessage
3638
from .user_simulator import Status
@@ -41,63 +43,6 @@
4143
_AUTHOR_USER = "user"
4244
_STOP_SIGNAL = "</finished>"
4345

44-
_DEFAULT_USER_AGENT_INSTRUCTIONS = """You are a Simulated User designed to test an AI Agent.
45-
46-
Your single most important job is to react logically to the Agent's last message.
47-
The Conversation Plan is your canonical grounding, not a script; your response MUST be dictated by what the Agent just said.
48-
49-
# Primary Operating Loop
50-
51-
You MUST follow this three-step process while thinking:
52-
53-
Step 1: Analyze what the Agent just said or did. Specifically, is the Agent asking you a question, reporting a successful or unsuccessful operation, or saying something incorrect or unexpected?
54-
55-
Step 2: Choose one action based on your analysis:
56-
* ANSWER any questions the Agent asked.
57-
* ADVANCE to the next request as per the Conversation Plan if the Agent succeeds in satisfying your current request.
58-
* INTERVENE if the Agent is yet to complete your current request and the Conversation Plan requires you to modify it.
59-
* CORRECT the Agent if it is making a mistake or failing.
60-
* END the conversation if any of the below stopping conditions are met:
61-
- The Agent has completed all your requests from the Conversation Plan.
62-
- The Agent has failed to fulfill a request *more than once*.
63-
- The Agent has performed an incorrect operation and informs you that it is unable to correct it.
64-
- The Agent ends the conversation on its own by transferring you to a *human/live agent* (NOT another AI Agent).
65-
66-
Step 3: Formulate a response based on the chosen action and the below Action Protocols and output it.
67-
68-
# Action Protocols
69-
70-
**PROTOCOL: ANSWER**
71-
* Only answer the Agent's questions using information from the Conversation Plan.
72-
* Do NOT provide any additional information the Agent did not explicitly ask for.
73-
* If you do not have the information requested by the Agent, inform the Agent. Do NOT make up information that is not in the Conversation Plan.
74-
* Do NOT advance to the next request in the Conversation Plan.
75-
76-
**PROTOCOL: ADVANCE**
77-
* Make the next request from the Conversation Plan.
78-
* Skip redundant requests already fulfilled by the Agent.
79-
80-
**PROTOCOL: INTERVENE**
81-
* Change your current request as directed by the Conversation Plan with natural phrasing.
82-
83-
**PROTOCOL: CORRECT**
84-
* Challenge illogical or incorrect statements made by the Agent.
85-
* If the Agent did an incorrect operation, ask the Agent to fix it.
86-
* If this is the FIRST time the Agent failed to satisfy your request, ask the Agent to try again.
87-
88-
**PROTOCOL: END**
89-
* End the conversation only when any of the stopping conditions are met; do NOT end prematurely.
90-
* Output `{stop_signal}` to indicate that the conversation with the AI Agents is over.
91-
92-
# Conversation Plan
93-
94-
{conversation_plan}
95-
96-
# Conversation History
97-
98-
{conversation_history}
99-
"""
100-
10146

10247
class LlmBackedUserSimulatorConfig(BaseUserSimulatorConfig):
10348
"""Contains configurations required by an LLM backed user simulator."""
@@ -130,32 +75,34 @@ class LlmBackedUserSimulatorConfig(BaseUserSimulatorConfig):
13075
custom_instructions: Optional[str] = Field(
13176
default=None,
13277
description="""Custom instructions for the LlmBackedUserSimulator. The
133-
instructions must contain the following formatting placeholders:
134-
* {stop_signal} : text to be generated when the user simulator decides that the
78+
instructions must contain the following formatting placeholders following Jinja syntax:
79+
* {{ stop_signal }} : text to be generated when the user simulator decides that the
13580
conversation is over.
136-
* {conversation_plan} : the overall plan for the conversation that the user
81+
* {{ conversation_plan }} : the overall plan for the conversation that the user
13782
simulator must follow.
138-
* {conversation_history} : the conversation between the user and the agent so
139-
far.""",
83+
* {{ conversation_history }} : the conversation between the user and the agent so
84+
far.
85+
* {{ persona }} : Only needed if specifying user_persona in the conversation scenario.
86+
""",
14087
)
14188

14289
@field_validator("custom_instructions")
14390
@classmethod
14491
def validate_custom_instructions(cls, value: Optional[str]) -> Optional[str]:
14592
if value is None:
14693
return value
147-
if not all(
148-
placeholder in value
149-
for placeholder in [
150-
"{stop_signal}",
151-
"{conversation_plan}",
152-
"{conversation_history}",
153-
]
94+
if not is_valid_user_simulator_template(
95+
value,
96+
required_params=[
97+
"stop_signal",
98+
"conversation_plan",
99+
"conversation_history",
100+
],
154101
):
155102
raise ValueError(
156103
"custom_instructions must contain each of the following formatting"
157-
" placeholders:"
158-
" {stop_signal}, {conversation_plan}, {conversation_history}"
104+
" placeholders using Jinja syntax: {{ stop_signal }}, {{"
105+
" conversation_plan }}, {{ conversation_history }}"
159106
)
160107
return value
161108

@@ -180,11 +127,7 @@ def __init__(
180127
llm_registry = LLMRegistry()
181128
llm_class = llm_registry.resolve(self._config.model)
182129
self._llm = llm_class(model=self._config.model)
183-
self._instructions = (
184-
self._config.custom_instructions
185-
if self._config.custom_instructions
186-
else _DEFAULT_USER_AGENT_INSTRUCTIONS
187-
)
130+
self._user_persona = self._conversation_scenario.user_persona
188131

189132
@classmethod
190133
def _summarize_conversation(
@@ -221,10 +164,12 @@ async def _get_llm_response(
221164
# first invocation - send the static starting prompt
222165
return self._conversation_scenario.starting_prompt
223166

224-
user_agent_instructions = self._instructions.format(
225-
stop_signal=_STOP_SIGNAL,
167+
user_agent_instructions = get_llm_backed_user_simulator_prompt(
226168
conversation_plan=self._conversation_scenario.conversation_plan,
227169
conversation_history=rewritten_dialogue,
170+
stop_signal=_STOP_SIGNAL,
171+
custom_instructions=self._config.custom_instructions,
172+
user_persona=self._user_persona,
228173
)
229174

230175
llm_request = LlmRequest(

0 commit comments

Comments
 (0)