Skip to content

Commit ce930f2

Browse files
authored
Merge branch 'main' into fix/memory-non-latin-search
2 parents 06b5744 + 88421f8 commit ce930f2

13 files changed

Lines changed: 396 additions & 46 deletions

File tree

contributing/samples/hello_world/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ async def check_prime(nums: list[int]) -> str:
6565

6666

6767
root_agent = Agent(
68-
model='gemini-2.5-flash',
68+
model='projects/adk-cat/locations/us-central1/publishers/google/models/gemini-2.5-flash',
6969
name='hello_world_agent',
7070
description=(
7171
'hello world agent that can roll a dice of 8 sides and check prime'

src/google/adk/cli/utils/gcp_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ def sign_up_express(
139139
"POST",
140140
":signUp",
141141
location=location,
142-
data={"region": location, "tos_accepted": True},
142+
data={
143+
"region": location,
144+
"tos_accepted": True,
145+
"get_default_api_key": True,
146+
},
143147
)
144148
return {
145149
"project_id": project.get("projectId"),

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

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
import logging
1818
from typing import ClassVar
19-
from typing import Optional
2019

2120
from google.genai import types as genai_types
2221
from pydantic import Field
@@ -72,7 +71,7 @@ class LlmBackedUserSimulatorConfig(BaseUserSimulatorConfig):
7271
(Not recommended) If you don't want a limit, you can set the value to -1.""",
7372
)
7473

75-
custom_instructions: Optional[str] = Field(
74+
custom_instructions: str | None = Field(
7675
default=None,
7776
description="""Custom instructions for the LlmBackedUserSimulator. The
7877
instructions must contain the following formatting placeholders following Jinja syntax:
@@ -88,7 +87,7 @@ class LlmBackedUserSimulatorConfig(BaseUserSimulatorConfig):
8887

8988
@field_validator("custom_instructions")
9089
@classmethod
91-
def validate_custom_instructions(cls, value: Optional[str]) -> Optional[str]:
90+
def validate_custom_instructions(cls, value: str | None) -> str | None:
9291
if value is None:
9392
return value
9493
if not is_valid_user_simulator_template(
@@ -158,11 +157,11 @@ def _summarize_conversation(
158157
async def _get_llm_response(
159158
self,
160159
rewritten_dialogue: str,
161-
) -> str:
162-
"""Sends a user message generation request to the LLM and returns the full response."""
160+
) -> tuple[str, str | None]:
161+
"""Sends a user message generation request to the LLM and returns the full response and potential error reason."""
163162
if self._invocation_count == 0:
164163
# first invocation - send the static starting prompt
165-
return self._conversation_scenario.starting_prompt
164+
return self._conversation_scenario.starting_prompt, None
166165

167166
user_agent_instructions = get_llm_backed_user_simulator_prompt(
168167
conversation_plan=self._conversation_scenario.conversation_plan,
@@ -187,19 +186,44 @@ async def _get_llm_response(
187186
add_default_retry_options_if_not_present(llm_request)
188187

189188
response = ""
189+
error_reason = None
190+
has_thought_tokens = False
190191
async with Aclosing(self._llm.generate_content_async(llm_request)) as agen:
191192
async for llm_response in agen:
193+
error_code = llm_response.error_code
194+
if error_code:
195+
logger.warning(
196+
"User simulator LLM returned error: code=%s, message=%s",
197+
error_code,
198+
getattr(llm_response, "error_message", ""),
199+
)
200+
error_reason = f"safety filters or other error (code={error_code})"
201+
response = ""
202+
break
203+
192204
generated_content: genai_types.Content = llm_response.content
193205
if (
194206
not generated_content
195207
or not hasattr(generated_content, "parts")
196208
or not generated_content.parts
197209
):
198210
continue
211+
199212
for part in generated_content.parts:
200-
if part.text and not part.thought:
213+
if part.thought:
214+
has_thought_tokens = True
215+
elif part.text:
201216
response += part.text
202-
return response
217+
218+
if not response:
219+
if error_reason:
220+
pass # Keep the error reason from error_code
221+
elif has_thought_tokens:
222+
error_reason = "LLM returned only thinking tokens"
223+
else:
224+
error_reason = "LLM returned empty response"
225+
226+
return response, error_reason
203227

204228
@override
205229
async def get_next_user_message(
@@ -234,11 +258,11 @@ async def get_next_user_message(
234258
rewritten_dialogue = self._summarize_conversation(events)
235259

236260
# query the LLM for the next user message
237-
response = await self._get_llm_response(rewritten_dialogue)
261+
response, error_reason = await self._get_llm_response(rewritten_dialogue)
238262
self._invocation_count += 1
239263

240264
# is the conversation over? (Has the user simulator output the stop signal?)
241-
if _STOP_SIGNAL.lower() in response.lower():
265+
if response and _STOP_SIGNAL.lower() in response.lower():
242266
logger.info(
243267
"Stopping user message generation as the stop signal was detected."
244268
)
@@ -256,11 +280,11 @@ async def get_next_user_message(
256280

257281
# if we are here, the user agent failed to generate a message, which is not
258282
# a valid result for the LLM backed user simulator.
259-
raise RuntimeError("Failed to generate a user message")
283+
raise RuntimeError(f"Failed to generate a user message: {error_reason}")
260284

261285
@override
262286
def get_simulation_evaluator(
263287
self,
264-
) -> Optional[Evaluator]:
288+
) -> Evaluator | None:
265289
"""Returns an Evaluator that evaluates if the simulation was successful or not."""
266290
raise NotImplementedError()

src/google/adk/flows/llm_flows/contents.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -795,8 +795,8 @@ def _is_request_input_event(event: Event) -> bool:
795795
return _is_function_call_event(event, REQUEST_INPUT_FUNCTION_CALL_NAME)
796796

797797

798-
def _is_live_model_audio_event_with_inline_data(event: Event) -> bool:
799-
"""Check if the event is a live/bidi audio event with inline data.
798+
def _is_live_model_media_event_with_inline_data(event: Event) -> bool:
799+
"""Check if the event is a live/bidi media event (audio, video, image) with inline data.
800800
801801
There are two possible cases and we only care about the second case:
802802
content=Content(
@@ -826,12 +826,14 @@ def _is_live_model_audio_event_with_inline_data(event: Event) -> bool:
826826
if not event.content or not event.content.parts:
827827
return False
828828
for part in event.content.parts:
829-
if (
830-
part.inline_data
831-
and part.inline_data.mime_type
832-
and part.inline_data.mime_type.startswith('audio/')
833-
):
834-
return True
829+
if part.inline_data and part.inline_data.mime_type:
830+
mime = part.inline_data.mime_type.lower()
831+
if (
832+
mime.startswith('audio/')
833+
or mime.startswith('video/')
834+
or mime.startswith('image/')
835+
):
836+
return True
835837
return False
836838

837839

src/google/adk/runners.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -786,17 +786,17 @@ async def _compute_artifact_delta_for_rewind(
786786

787787
def _should_append_event(self, event: Event, is_live_call: bool) -> bool:
788788
"""Checks if an event should be appended to the session."""
789-
# Don't append audio response from model in live mode to session.
789+
# Don't append media (audio/video/image) response from model in live mode to session.
790790
# The data is appended to artifacts with a reference in file_data in the
791-
# event.
791+
# event if save_live_blob is True.
792792
# We should append non-partial events only.For example, non-finished(partial)
793793
# transcription events should not be appended.
794794
# Function call and function response events should be appended.
795795
# Other control events should be appended.
796-
if is_live_call and contents._is_live_model_audio_event_with_inline_data(
796+
if is_live_call and contents._is_live_model_media_event_with_inline_data(
797797
event
798798
):
799-
# We don't append live model audio events with inline data to avoid
799+
# We don't append live model media events with inline data to avoid
800800
# storing large blobs in the session. However, events with file_data
801801
# (references to artifacts) should be appended.
802802
return False
@@ -1616,6 +1616,10 @@ async def close(self):
16161616
if self.plugin_manager:
16171617
await self.plugin_manager.close()
16181618

1619+
# Close Session Service
1620+
if self.session_service:
1621+
await self.session_service.flush()
1622+
16191623
logger.info('Runner closed.')
16201624

16211625
if sys.version_info < (3, 11):

src/google/adk/sessions/base_session_service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ async def append_event(self, session: Session, event: Event) -> Event:
124124
session.events.append(event)
125125
return event
126126

127+
async def flush(self):
128+
"""Flushes any buffered events.
129+
130+
For non-buffering implementations, this can be a no-op.
131+
"""
132+
pass
133+
127134
def _apply_temp_state(self, session: Session, event: Event) -> None:
128135
"""Applies temp-scoped state delta to the in-memory session state.
129136

src/google/adk/tools/environment/_environment_toolset.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,21 @@ def __init__(
6060
self,
6161
*,
6262
environment: BaseEnvironment,
63+
max_output_chars: Optional[int] = None,
6364
**kwargs: Any,
6465
):
6566
"""Create an environment toolset.
6667
6768
Args:
68-
environment: The environment used to execute commands and
69-
perform file I/O.
69+
environment: The environment used to execute commands and perform file
70+
I/O.
71+
max_output_chars: Maximum character limit for stdout/stderr/file
72+
truncation.
7073
**kwargs: Forwarded to ``BaseToolset.__init__``.
7174
"""
7275
super().__init__(**kwargs)
7376
self._environment = environment
77+
self._max_output_chars = max_output_chars
7478
self._environment_initialized = False
7579

7680
@override
@@ -82,8 +86,10 @@ async def get_tools(
8286
await self._environment.initialize()
8387
self._environment_initialized = True
8488
return [
85-
ExecuteTool(self._environment),
86-
ReadFileTool(self._environment),
89+
ExecuteTool(self._environment, max_output_chars=self._max_output_chars),
90+
ReadFileTool(
91+
self._environment, max_output_chars=self._max_output_chars
92+
),
8793
EditFileTool(self._environment),
8894
WriteFileTool(self._environment),
8995
]

src/google/adk/tools/environment/_tools.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,20 @@ def _truncate(text: str, limit: int = MAX_OUTPUT_CHARS) -> str:
5858
class ExecuteTool(BaseTool):
5959
"""Run a shell command in the environment's working directory."""
6060

61-
def __init__(self, environment: BaseEnvironment):
61+
def __init__(
62+
self,
63+
environment: BaseEnvironment,
64+
*,
65+
max_output_chars: Optional[int] = None,
66+
):
6267
super().__init__(
6368
name='Execute',
6469
description=_EXECUTE_TOOL_DESCRIPTION,
6570
)
6671
self._environment = environment
72+
self._max_output_chars = (
73+
max_output_chars if max_output_chars is not None else MAX_OUTPUT_CHARS
74+
)
6775

6876
@override
6977
def _get_declaration(self) -> Optional[types.FunctionDeclaration]:
@@ -111,9 +119,15 @@ async def run_async(
111119

112120
result: dict[str, Any] = {'status': 'ok'}
113121
if execution_result.stdout:
114-
result['stdout'] = _truncate(execution_result.stdout)
122+
result['stdout'] = _truncate(
123+
execution_result.stdout,
124+
limit=self._max_output_chars,
125+
)
115126
if execution_result.stderr:
116-
result['stderr'] = _truncate(execution_result.stderr)
127+
result['stderr'] = _truncate(
128+
execution_result.stderr,
129+
limit=self._max_output_chars,
130+
)
117131
if execution_result.exit_code != 0:
118132
result['status'] = 'error'
119133
result['exit_code'] = execution_result.exit_code
@@ -127,7 +141,12 @@ async def run_async(
127141
class ReadFileTool(BaseTool):
128142
"""Read a file from the environment."""
129143

130-
def __init__(self, environment: BaseEnvironment):
144+
def __init__(
145+
self,
146+
environment: BaseEnvironment,
147+
*,
148+
max_output_chars: Optional[int] = None,
149+
):
131150
super().__init__(
132151
name='ReadFile',
133152
description=(
@@ -136,6 +155,9 @@ def __init__(self, environment: BaseEnvironment):
136155
),
137156
)
138157
self._environment = environment
158+
self._max_output_chars = (
159+
max_output_chars if max_output_chars is not None else MAX_OUTPUT_CHARS
160+
)
139161

140162
@override
141163
def _get_declaration(self) -> Optional[types.FunctionDeclaration]:
@@ -190,7 +212,13 @@ async def run_async(
190212
cmd = f"cat -n '{path}' | sed -n '{sed_range}p'"
191213
res = await self._environment.execute(cmd)
192214
if res.exit_code == 0:
193-
return {'status': 'ok', 'content': _truncate(res.stdout)}
215+
return {
216+
'status': 'ok',
217+
'content': _truncate(
218+
res.stdout,
219+
limit=self._max_output_chars,
220+
),
221+
}
194222

195223
try:
196224
data_bytes = await self._environment.read_file(path)
@@ -217,7 +245,13 @@ async def run_async(
217245
numbered = ''.join(
218246
f'{start + i:6d}\t{line}' for i, line in enumerate(selected)
219247
)
220-
result = {'status': 'ok', 'content': _truncate(numbered)}
248+
result = {
249+
'status': 'ok',
250+
'content': _truncate(
251+
numbered,
252+
limit=self._max_output_chars,
253+
),
254+
}
221255
if start > 1 or end < total:
222256
result['total_lines'] = total
223257
return result

tests/unittests/cli/utils/test_gcp_utils.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,19 @@ def test_sign_up_express(self, mock_auth_default, mock_session_cls):
126126
result = gcp_utils.sign_up_express()
127127
self.assertEqual(result["project_id"], "new-project")
128128
self.assertEqual(result["api_key"], "new-api-key")
129-
args, _ = mock_session.post.call_args
129+
args, kwargs = mock_session.post.call_args
130130
self.assertEqual(
131131
args[0],
132132
"https://us-central1-aiplatform.googleapis.com/v1beta1/vertexExpress:signUp",
133133
)
134+
self.assertEqual(
135+
kwargs["json"],
136+
{
137+
"region": "us-central1",
138+
"tos_accepted": True,
139+
"get_default_api_key": True,
140+
},
141+
)
134142

135143
@mock.patch(
136144
"google.adk.cli.utils.gcp_utils.resourcemanager_v3.ProjectsClient"

0 commit comments

Comments
 (0)