Skip to content

Commit f909859

Browse files
authored
Merge branch 'main' into feat/bigtable-parameterized-views
2 parents f5902fd + 2e28e5d commit f909859

8 files changed

Lines changed: 136 additions & 121 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ part before or alongside your code PR.
133133
1. **Clone the repository:**
134134

135135
```shell
136-
gh repo clone google/adk-python -- -b v2
136+
gh repo clone google/adk-python
137137
cd adk-python
138138
```
139139

src/google/adk/errors/malformed_function_call_error.py

Lines changed: 0 additions & 26 deletions
This file was deleted.

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ def _rearrange_events_for_async_function_responses_in_history(
108108
events: list[Event],
109109
) -> list[Event]:
110110
"""Rearrange the async function_response events in the history."""
111-
112111
function_call_id_to_response_events_index: dict[str, int] = {}
113112
for i, event in enumerate(events):
114113
function_responses = event.get_function_responses()
@@ -117,6 +116,9 @@ def _rearrange_events_for_async_function_responses_in_history(
117116
function_call_id = function_response.id
118117
function_call_id_to_response_events_index[function_call_id] = i
119118

119+
if not function_call_id_to_response_events_index:
120+
return events
121+
120122
result_events: list[Event] = []
121123
for event in events:
122124
if event.get_function_responses():

src/google/adk/models/anthropic_llm.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -553,11 +553,14 @@ async def generate_content_async(
553553
else NOT_GIVEN
554554
)
555555
thinking = _build_anthropic_thinking_param(llm_request.config)
556+
system = NOT_GIVEN
557+
if llm_request.config.system_instruction is not None:
558+
system = llm_request.config.system_instruction
556559

557560
if not stream:
558561
message = await self._anthropic_client.messages.create(
559562
model=model_to_use,
560-
system=llm_request.config.system_instruction,
563+
system=system,
561564
messages=messages,
562565
tools=tools,
563566
tool_choice=tool_choice,
@@ -567,14 +570,15 @@ async def generate_content_async(
567570
yield message_to_generate_content_response(message)
568571
else:
569572
async for response in self._generate_content_streaming(
570-
llm_request, messages, tools, tool_choice, thinking
573+
llm_request, messages, system, tools, tool_choice, thinking
571574
):
572575
yield response
573576

574577
async def _generate_content_streaming(
575578
self,
576579
llm_request: LlmRequest,
577580
messages: list[anthropic_types.MessageParam],
581+
system: Union[str, types.Content, NotGiven],
578582
tools: Union[Iterable[anthropic_types.ToolUnionParam], NotGiven],
579583
tool_choice: Union[anthropic_types.ToolChoiceParam, NotGiven],
580584
thinking: Union[
@@ -591,7 +595,7 @@ async def _generate_content_streaming(
591595
model_to_use = self._resolve_model_name(llm_request.model)
592596
raw_stream = await self._anthropic_client.messages.create(
593597
model=model_to_use,
594-
system=llm_request.config.system_instruction,
598+
system=system,
595599
messages=messages,
596600
tools=tools,
597601
tool_choice=tool_choice,

src/google/adk/models/google_llm.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
from google.genai.errors import ClientError
3434
from typing_extensions import override
3535

36-
from ..errors.malformed_function_call_error import MalformedFunctionCallError
3736
from ..utils._google_client_headers import get_tracking_headers
3837
from ..utils._google_client_headers import merge_tracking_headers
3938
from ..utils.context_utils import Aclosing
@@ -63,27 +62,6 @@
6362
"""
6463

6564

66-
def _raise_for_malformed_function_call(llm_response: LlmResponse) -> None:
67-
"""Raises when the model returned a malformed function call with no content.
68-
69-
A ``MALFORMED_FUNCTION_CALL`` finish reason yields a response that carries an
70-
error code but nothing the agent can act on. Left alone it builds a
71-
content-free event with no function call, which silently ends the invocation.
72-
Raising routes it through the on_model_error callbacks so they can recover,
73-
mirroring the LiteLlm malformed-arguments path. The error subclasses
74-
``ValueError`` so callbacks can match this case specifically without breaking
75-
existing handlers.
76-
"""
77-
if (
78-
llm_response.finish_reason == types.FinishReason.MALFORMED_FUNCTION_CALL
79-
and not (llm_response.content and llm_response.content.parts)
80-
):
81-
raise MalformedFunctionCallError(
82-
llm_response.error_message
83-
or 'Model returned a malformed function call.'
84-
)
85-
86-
8765
class _ResourceExhaustedError(ClientError):
8866
"""Represents a resources exhausted error received from the Model."""
8967

@@ -280,7 +258,6 @@ async def generate_content_async(
280258
aggregator.process_response(response)
281259
) as aggregator_gen:
282260
async for llm_response in aggregator_gen:
283-
_raise_for_malformed_function_call(llm_response)
284261
yield llm_response
285262
if (close_result := aggregator.close()) is not None:
286263
# Populate cache metadata in the final aggregated response for
@@ -289,7 +266,6 @@ async def generate_content_async(
289266
cache_manager.populate_cache_metadata_in_response(
290267
close_result, cache_metadata
291268
)
292-
_raise_for_malformed_function_call(close_result)
293269
yield close_result
294270

295271
else:
@@ -307,7 +283,6 @@ async def generate_content_async(
307283
cache_manager.populate_cache_metadata_in_response(
308284
llm_response, cache_metadata
309285
)
310-
_raise_for_malformed_function_call(llm_response)
311286
yield llm_response
312287
except ClientError as ce:
313288
if ce.code == 429:

tests/unittests/flows/llm_flows/test_contents.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,3 +1306,37 @@ def test_get_contents_live_history_rebuild():
13061306

13071307
assert result[1].role == "user"
13081308
assert "returned result" in result[1].parts[1].text
1309+
1310+
1311+
def test_rearrange_async_function_responses_early_returns_when_no_responses():
1312+
"""Rearrangement is a no-op when no event carries function_responses."""
1313+
events = [
1314+
Event(
1315+
invocation_id="inv1",
1316+
author="user",
1317+
content=types.UserContent("hi"),
1318+
),
1319+
Event(
1320+
invocation_id="inv2",
1321+
author="test_agent",
1322+
content=types.ModelContent("hello"),
1323+
),
1324+
Event(
1325+
invocation_id="inv3",
1326+
author="test_agent",
1327+
content=types.Content(
1328+
role="model",
1329+
parts=[
1330+
types.Part(
1331+
function_call=types.FunctionCall(
1332+
id="adk-1", name="tool", args={}
1333+
)
1334+
)
1335+
],
1336+
),
1337+
),
1338+
]
1339+
result = contents._rearrange_events_for_async_function_responses_in_history( # pylint: disable=protected-access
1340+
events
1341+
)
1342+
assert result is events

tests/unittests/models/test_anthropic_llm.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from unittest.mock import AsyncMock
2222
from unittest.mock import MagicMock
2323

24+
from anthropic import NOT_GIVEN
2425
from anthropic import types as anthropic_types
2526
from google.adk import version as adk_version
2627
from google.adk.models import anthropic_llm
@@ -2134,3 +2135,93 @@ async def test_generate_content_async_pairs_invalid_tool_ids(
21342135
]
21352136
assert len(set(use_ids)) == expected_unique
21362137
assert set(use_ids) == set(result_ids)
2138+
2139+
2140+
@pytest.mark.asyncio
2141+
async def test_non_streaming_no_system_instruction_passes_not_given():
2142+
"""system=NOT_GIVEN when LlmRequest has no system_instruction."""
2143+
llm = AnthropicLlm(model="claude-sonnet-4-20250514")
2144+
2145+
mock_message = anthropic_types.Message(
2146+
id="msg_test",
2147+
content=[
2148+
anthropic_types.TextBlock(text="ok", type="text", citations=None)
2149+
],
2150+
model="claude-sonnet-4-20250514",
2151+
role="assistant",
2152+
stop_reason="end_turn",
2153+
stop_sequence=None,
2154+
type="message",
2155+
usage=anthropic_types.Usage(
2156+
input_tokens=1,
2157+
output_tokens=1,
2158+
cache_creation_input_tokens=0,
2159+
cache_read_input_tokens=0,
2160+
server_tool_use=None,
2161+
service_tier=None,
2162+
),
2163+
)
2164+
2165+
mock_client = MagicMock()
2166+
mock_client.messages.create = AsyncMock(return_value=mock_message)
2167+
2168+
request = LlmRequest(
2169+
model="claude-sonnet-4-20250514",
2170+
contents=[Content(role="user", parts=[Part.from_text(text="Hi")])],
2171+
)
2172+
assert request.config.system_instruction is None
2173+
2174+
with mock.patch.object(llm, "_anthropic_client", mock_client):
2175+
_ = [r async for r in llm.generate_content_async(request, stream=False)]
2176+
2177+
mock_client.messages.create.assert_called_once()
2178+
_, kwargs = mock_client.messages.create.call_args
2179+
assert kwargs["system"] is NOT_GIVEN
2180+
2181+
2182+
@pytest.mark.asyncio
2183+
async def test_streaming_no_system_instruction_passes_not_given():
2184+
"""system=NOT_GIVEN on the streaming path when no system_instruction."""
2185+
llm = AnthropicLlm(model="claude-sonnet-4-20250514")
2186+
2187+
events = [
2188+
MagicMock(
2189+
type="message_start",
2190+
message=MagicMock(usage=MagicMock(input_tokens=1, output_tokens=0)),
2191+
),
2192+
MagicMock(
2193+
type="content_block_start",
2194+
index=0,
2195+
content_block=anthropic_types.TextBlock(text="", type="text"),
2196+
),
2197+
MagicMock(
2198+
type="content_block_delta",
2199+
index=0,
2200+
delta=anthropic_types.TextDelta(text="ok", type="text_delta"),
2201+
),
2202+
MagicMock(type="content_block_stop", index=0),
2203+
MagicMock(
2204+
type="message_delta",
2205+
delta=MagicMock(stop_reason="end_turn"),
2206+
usage=MagicMock(output_tokens=1),
2207+
),
2208+
MagicMock(type="message_stop"),
2209+
]
2210+
2211+
mock_client = MagicMock()
2212+
mock_client.messages.create = AsyncMock(
2213+
return_value=_make_mock_stream_events(events)
2214+
)
2215+
2216+
request = LlmRequest(
2217+
model="claude-sonnet-4-20250514",
2218+
contents=[Content(role="user", parts=[Part.from_text(text="Hi")])],
2219+
)
2220+
assert request.config.system_instruction is None
2221+
2222+
with mock.patch.object(llm, "_anthropic_client", mock_client):
2223+
_ = [r async for r in llm.generate_content_async(request, stream=True)]
2224+
2225+
mock_client.messages.create.assert_called_once()
2226+
_, kwargs = mock_client.messages.create.call_args
2227+
assert kwargs["system"] is NOT_GIVEN

tests/unittests/models/test_google_llm.py

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
from google.adk import version as adk_version
2323
from google.adk.agents.context_cache_config import ContextCacheConfig
24-
from google.adk.errors.malformed_function_call_error import MalformedFunctionCallError
2524
from google.adk.models.cache_metadata import CacheMetadata
2625
from google.adk.models.gemini_llm_connection import GeminiLlmConnection
2726
from google.adk.models.google_llm import _build_function_declaration_log
@@ -323,70 +322,6 @@ async def mock_coro():
323322
mock_client.aio.models.generate_content.assert_called_once()
324323

325324

326-
@pytest.mark.asyncio
327-
async def test_generate_content_async_malformed_function_call_raises(
328-
gemini_llm, llm_request
329-
):
330-
malformed_response = types.GenerateContentResponse(
331-
candidates=[
332-
types.Candidate(
333-
content=None,
334-
finish_reason=types.FinishReason.MALFORMED_FUNCTION_CALL,
335-
finish_message="Malformed function call: print(default_api.f(x=",
336-
)
337-
]
338-
)
339-
with mock.patch.object(gemini_llm, "api_client") as mock_client:
340-
341-
async def mock_coro():
342-
return malformed_response
343-
344-
mock_client.aio.models.generate_content.return_value = mock_coro()
345-
346-
with pytest.raises(
347-
MalformedFunctionCallError, match="alformed function call"
348-
):
349-
_ = [
350-
resp
351-
async for resp in gemini_llm.generate_content_async(
352-
llm_request, stream=False
353-
)
354-
]
355-
356-
357-
@pytest.mark.asyncio
358-
async def test_generate_content_async_stream_malformed_function_call_raises(
359-
gemini_llm, llm_request
360-
):
361-
mock_responses = [
362-
types.GenerateContentResponse(
363-
candidates=[
364-
types.Candidate(
365-
content=None,
366-
finish_reason=types.FinishReason.MALFORMED_FUNCTION_CALL,
367-
finish_message="Malformed function call",
368-
)
369-
]
370-
),
371-
]
372-
with mock.patch.object(gemini_llm, "api_client") as mock_client:
373-
374-
async def mock_coro():
375-
return MockAsyncIterator(mock_responses)
376-
377-
mock_client.aio.models.generate_content_stream.return_value = mock_coro()
378-
379-
with pytest.raises(
380-
MalformedFunctionCallError, match="alformed function call"
381-
):
382-
_ = [
383-
resp
384-
async for resp in gemini_llm.generate_content_async(
385-
llm_request, stream=True
386-
)
387-
]
388-
389-
390325
@pytest.mark.asyncio
391326
async def test_generate_content_async_stream(gemini_llm, llm_request):
392327
with mock.patch.object(gemini_llm, "api_client") as mock_client:

0 commit comments

Comments
 (0)