Skip to content

Commit cbe60c4

Browse files
ankursharmascopybara-github
authored andcommitted
feat: Adds data model to support UserSimulation
Details: - Introduces a concept of `ConversationScenario` to represent a scenario that user simulator is supposed to follow. - Introduces a `UserSimulator` interface, that one should implement. UserSimulator interface will be integrated with LocalEvalService in subsequent PRs. PiperOrigin-RevId: 816883699
1 parent 2efaa57 commit cbe60c4

File tree

5 files changed

+277
-2
lines changed

5 files changed

+277
-2
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import Union
18+
19+
from google.genai import types as genai_types
20+
from pydantic import Field
21+
22+
from .common import EvalBaseModel
23+
24+
25+
class ConversationScenario(EvalBaseModel):
26+
"""Scenario for a conversation between a simulated user and the Agent."""
27+
28+
starting_prompt: Union[str, genai_types.Content]
29+
"""Starting prompt for the conversation.
30+
31+
This prompt acts as the first user message that is given to the Agent. Any
32+
subsequent user messages are obtained by the system that is simulating the
33+
user.
34+
"""
35+
36+
conversation_plan: str
37+
"""A plan that user simulation system needs to follow as it plays out the conversation.
38+
39+
Example:
40+
For a Travel Agent that has tools that let it book a flight and car, a sample
41+
starting prompt could be:
42+
43+
`I need to book a flight.`
44+
45+
A conversation plan could look like:
46+
47+
First, you want to book a one-way flight from SFO to LAX for next Tuesday.
48+
You prefer a morning flight and your budget is under $150. If the agent finds
49+
a valid flight, confirm the booking. Once confirmed, your next goal is to rent
50+
a standard-size car for three days from the airport. Once both tasks are done,
51+
your overall goal is complete.
52+
"""
53+
54+
55+
class ConversationScenarios(EvalBaseModel):
56+
"""A simple container for the list of ConversationScenario.
57+
58+
Mainly serves the purpose of helping with serialization and deserialization.
59+
"""
60+
61+
scenarios: list[ConversationScenario] = Field(
62+
default_factory=list, description="""A list of ConversationScenario."""
63+
)

src/google/adk/evaluation/eval_case.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from .app_details import AppDetails
2626
from .common import EvalBaseModel
27+
from .conversation_scenarios import ConversationScenario
2728
from .eval_rubrics import Rubric
2829

2930

@@ -119,14 +120,29 @@ class SessionInput(EvalBaseModel):
119120
"""The state of the session."""
120121

121122

123+
StaticConversation: TypeAlias = list[Invocation]
124+
"""A conversation where user's query for each invocation is already specified."""
125+
126+
122127
class EvalCase(EvalBaseModel):
123128
"""An eval case."""
124129

125130
eval_id: str
126131
"""Unique identifier for the evaluation case."""
127132

128-
conversation: list[Invocation]
129-
"""A conversation between the user and the Agent. The conversation can have any number of invocations."""
133+
conversation: Optional[StaticConversation] = None
134+
"""A static conversation between the user and the Agent.
135+
136+
While creating an eval case you should specify either a `conversation` or a
137+
`conversation_scenario`, but not both.
138+
"""
139+
140+
conversation_scenario: Optional[ConversationScenario] = None
141+
"""A conversation scenario that should be used by a UserSimulator.
142+
143+
While creating an eval case you should specify either a `conversation` or a
144+
`conversation_scenario`, but not both.
145+
"""
130146

131147
session_input: Optional[SessionInput] = None
132148
"""Session input that will be passed on to the Agent during eval.

src/google/adk/evaluation/eval_config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from ..evaluation.eval_metrics import EvalMetric
2727
from .eval_metrics import BaseCriterion
2828
from .eval_metrics import Threshold
29+
from .user_simulator import BaseUserSimulatorConfig
2930

3031
logger = logging.getLogger("google_adk." + __name__)
3132

@@ -70,6 +71,11 @@ class EvalConfig(BaseModel):
7071
""",
7172
)
7273

74+
user_simulator_config: Optional[BaseUserSimulatorConfig] = Field(
75+
default=None,
76+
description="""Config to be used by the user simulator.""",
77+
)
78+
7379

7480
_DEFAULT_EVAL_CONFIG = EvalConfig(
7581
criteria={"tool_trajectory_avg_score": 1.0, "response_match_score": 0.8}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import ClassVar
18+
from typing import Optional
19+
20+
from google.genai import types as genai_types
21+
from pydantic import Field
22+
from typing_extensions import override
23+
24+
from ..events.event import Event
25+
from ..utils.feature_decorator import experimental
26+
from .evaluator import Evaluator
27+
from .user_simulator import BaseUserSimulatorConfig
28+
from .user_simulator import NextUserMessage
29+
from .user_simulator import UserSimulator
30+
31+
32+
class LlmBackedUserSimulatorConfig(BaseUserSimulatorConfig):
33+
"""Contains configurations required by an LLM backed user simulator."""
34+
35+
model: str = Field(
36+
default="gemini-2.5-flash",
37+
description="The model to use for user simulation.",
38+
)
39+
40+
model_config: Optional[genai_types.GenerateContentConfig] = Field(
41+
default=genai_types.GenerateContentConfig,
42+
description="The configuration for the model.",
43+
)
44+
45+
max_allowed_invocations: int = Field(
46+
default=20,
47+
description="""Maximum number of invocations allowed by the simulated
48+
interaction. This property allows us to stop a run-off conversation, where the
49+
agent and the user simulator get into an never ending loop.
50+
51+
(Not recommended)If you don't want a limit, you can set the value to -1.
52+
""",
53+
)
54+
55+
56+
@experimental
57+
class LlmBackedUserSimulator(UserSimulator):
58+
"""A UserSimulator that uses a LLM to generate messages on behalf of the user."""
59+
60+
config_type: ClassVar[type[LlmBackedUserSimulatorConfig]] = (
61+
LlmBackedUserSimulatorConfig
62+
)
63+
64+
def __init__(self, *, config: BaseUserSimulatorConfig):
65+
super().__init__(config, config_type=LlmBackedUserSimulator.config_type)
66+
67+
@override
68+
async def get_next_user_message(
69+
self,
70+
conversation_plan: str,
71+
events: list[Event],
72+
) -> NextUserMessage:
73+
"""Returns the next user message to send to the agent with help from a LLM.
74+
75+
Args:
76+
conversation_plan: A plan that user simulation system needs to follow as
77+
it plays out the conversation.
78+
events: The unaltered conversation history between the user and the
79+
agent(s) under evaluation.
80+
"""
81+
raise NotImplementedError()
82+
83+
@override
84+
def get_simulation_evaluator(
85+
self,
86+
) -> Evaluator:
87+
"""Returns an Evaluator that evaluates if the simulation was successful or not."""
88+
raise NotImplementedError()
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from abc import ABC
18+
import enum
19+
from typing import Optional
20+
21+
from google.genai import types as genai_types
22+
from pydantic import alias_generators
23+
from pydantic import BaseModel
24+
from pydantic import ConfigDict
25+
from pydantic import Field
26+
from pydantic import ValidationError
27+
28+
from ..events.event import Event
29+
from ..utils.feature_decorator import experimental
30+
from .common import EvalBaseModel
31+
from .evaluator import Evaluator
32+
33+
34+
class BaseUserSimulatorConfig(BaseModel):
35+
"""Base class for configurations pertaining to User Simulator."""
36+
37+
model_config = ConfigDict(
38+
alias_generator=alias_generators.to_camel,
39+
populate_by_name=True,
40+
extra="allow",
41+
)
42+
43+
44+
class Status(enum.Enum):
45+
"""The resulting status of get_next_user_message()."""
46+
47+
SUCCESS = "success"
48+
TURN_LIMIT_REACHED = "turn_limit_reached"
49+
STOP_SIGNAL_DETECTED = "stop_signal_detected"
50+
NO_MESSAGE_GENERATED = "no_message_generated"
51+
52+
53+
class NextUserMessage(EvalBaseModel):
54+
status: Status = Field(
55+
description="""The resulting status of `get_next_user_message()`.
56+
57+
The caller of `get_next_user_message()` should inspect this field to determine
58+
if the user simulator was able to successfully generate a message or why it was
59+
not able to do so."""
60+
)
61+
62+
user_message: Optional[genai_types.Content] = Field(
63+
description="""The next user message."""
64+
)
65+
66+
67+
@experimental
68+
class UserSimulator(ABC):
69+
"""A user simulator for the purposes of automating interaction with an Agent."""
70+
71+
def __init__(
72+
self,
73+
config: BaseUserSimulatorConfig,
74+
config_type: type[BaseUserSimulatorConfig],
75+
):
76+
# Unpack the config to a specific type needed by the class implementing this
77+
# interface.
78+
try:
79+
self._config = config_type.model_validate(config.model_dump())
80+
except ValidationError as e:
81+
raise ValueError(f"Expect config of type `{config_type}`.") from e
82+
83+
async def get_next_user_message(
84+
self,
85+
conversation_plan: str,
86+
events: list[Event],
87+
) -> NextUserMessage:
88+
"""Returns the next user message to send to the agent.
89+
90+
Args:
91+
conversation_plan: A plan that user simulation system needs to follow as
92+
it plays out the conversation.
93+
events: The unaltered conversation history between the user and the
94+
agent(s) under evaluation.
95+
"""
96+
raise NotImplementedError()
97+
98+
def get_simulation_evaluator(
99+
self,
100+
) -> Evaluator:
101+
"""Returns an instnace of an Evaluator that evaluates if the simulation was successful or not."""
102+
raise NotImplementedError()

0 commit comments

Comments
 (0)