Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 12 additions & 17 deletions src/google/adk/cli/conformance/cli_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,23 +72,18 @@ async def _create_conformance_test_files(
# long-running tool. Replace the function call ID with the actual
# function call ID. This is needed because the function call ID is not
# known when writing the test case.
if (
user_message.content.parts
and user_message.content.parts[0].function_response
and user_message.content.parts[0].function_response.name
):
if (
user_message.content.parts[0].function_response.name
not in function_call_name_to_id_map
):
raise ValueError(
"Function response for"
f" {user_message.content.parts[0].function_response.name} does"
" not match any pending function call."
)
content.parts[0].function_response.id = function_call_name_to_id_map[
user_message.content.parts[0].function_response.name
]
if user_message.content.parts:
for part in content.parts:
if part.function_response and part.function_response.name:
if part.function_response.name not in function_call_name_to_id_map:
raise ValueError(
"Function response for"
f" {part.function_response.name} does"
" not match any pending function call."
)
part.function_response.id = function_call_name_to_id_map[
part.function_response.name
]
elif user_message.text is not None:
content = types.UserContent(parts=[types.Part(text=user_message.text)])
else:
Expand Down
29 changes: 12 additions & 17 deletions src/google/adk/cli/conformance/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,23 +142,18 @@ async def _run_user_messages(
# long-running tool. Replace the function call ID with the actual
# function call ID. This is needed because the function call ID is not
# known when writing the test case.
if (
user_message.content.parts
and user_message.content.parts[0].function_response
and user_message.content.parts[0].function_response.name
):
if (
user_message.content.parts[0].function_response.name
not in function_call_name_to_id_map
):
raise ValueError(
"Function response for"
f" {user_message.content.parts[0].function_response.name} does"
" not match any pending function call."
)
content.parts[0].function_response.id = function_call_name_to_id_map[
user_message.content.parts[0].function_response.name
]
if user_message.content.parts:
for part in content.parts:
if part.function_response and part.function_response.name:
if part.function_response.name not in function_call_name_to_id_map:
raise ValueError(
"Function response for"
f" {part.function_response.name} does"
" not match any pending function call."
)
part.function_response.id = function_call_name_to_id_map[
part.function_response.name
]
elif user_message.text is not None:
content = types.UserContent(parts=[types.Part(text=user_message.text)])
else:
Expand Down
20 changes: 8 additions & 12 deletions src/google/adk/flows/llm_flows/base_llm_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,12 +714,10 @@ async def run_live(
# the same function response. By handling agent transfer here,
# we ensure that only child agent processes its own function
# responses after the transfer.
if (
event.content
and event.content.parts
and event.content.parts[0].function_response
and event.content.parts[0].function_response.name
== 'transfer_to_agent'
if event.content and event.content.parts and any(
part.function_response
and part.function_response.name == 'transfer_to_agent'
for part in event.content.parts
):
await asyncio.sleep(DEFAULT_TRANSFER_AGENT_DELAY)
# cancel the tasks that belongs to the closed connection.
Expand Down Expand Up @@ -751,12 +749,10 @@ async def run_live(
) as agen:
async for item in agen:
yield item
if (
event.content
and event.content.parts
and event.content.parts[0].function_response
and event.content.parts[0].function_response.name
== 'task_completed'
if event.content and event.content.parts and any(
part.function_response
and part.function_response.name == 'task_completed'
for part in event.content.parts
):
# this is used for sequential agent to signal the end of the agent.
await asyncio.sleep(DEFAULT_TASK_COMPLETION_DELAY)
Expand Down
23 changes: 16 additions & 7 deletions src/google/adk/integrations/oci/_oci_genai_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,15 @@ def _content_to_oci_message(content: types.Content) -> Any:

# Tool results map to ToolMessage (one per result)
if tool_results:
call_id, result_text = tool_results[0]
return oci_models.ToolMessage(
role=oci_models.ToolMessage.ROLE_TOOL,
tool_call_id=call_id,
content=[oci_models.TextContent(type="TEXT", text=result_text)],
)
tool_messages = [
oci_models.ToolMessage(
role=oci_models.ToolMessage.ROLE_TOOL,
tool_call_id=call_id,
content=[oci_models.TextContent(type="TEXT", text=result_text)],
)
for call_id, result_text in tool_results
]
return tool_messages if len(tool_messages) > 1 else tool_messages[0]

if role == "ASSISTANT":
oci_content: list[Any] = []
Expand Down Expand Up @@ -451,7 +454,13 @@ def _build_chat_details(
"""Build OCI ChatDetails from an LlmRequest."""
import oci.generative_ai_inference.models as oci_models

messages = [_content_to_oci_message(c) for c in llm_request.contents or []]
messages = []
for c in llm_request.contents or []:
message_or_list = _content_to_oci_message(c)
if isinstance(message_or_list, list):
messages.extend(message_or_list)
else:
messages.append(message_or_list)

# Prepend SystemMessage when a system instruction is present
if llm_request.config and llm_request.config.system_instruction:
Expand Down
7 changes: 5 additions & 2 deletions src/google/adk/telemetry/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,11 @@ def trace_tool_call(
and function_response_event.content is not None
and function_response_event.content.parts
):
response_parts = function_response_event.content.parts
function_response = response_parts[0].function_response
function_response = None
for part in function_response_event.content.parts:
if part.function_response and part.function_response.name == tool.name:
function_response = part.function_response
break
if function_response is not None:
if function_response.id is not None:
tool_call_id = function_response.id
Expand Down
79 changes: 40 additions & 39 deletions src/google/adk/tools/load_artifacts_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,46 +286,47 @@ async def _append_artifacts_to_llm_request(
# Attach the content of the artifacts if the model requests them.
# This only adds the content to the model request, instead of the session.
if llm_request.contents and llm_request.contents[-1].parts:
function_response = llm_request.contents[-1].parts[0].function_response
if function_response and function_response.name == 'load_artifacts':
response = function_response.response or {}
artifact_names = response.get('artifact_names', [])
for artifact_name in artifact_names:
# Try session-scoped first (default behavior)
artifact = await tool_context.load_artifact(artifact_name)

# If not found and name doesn't already have user: prefix,
# try cross-session artifacts with user: prefix
if artifact is None and not artifact_name.startswith('user:'):
prefixed_name = f'user:{artifact_name}'
artifact = await tool_context.load_artifact(prefixed_name)

if artifact is None:
logger.warning('Artifact "%s" not found, skipping', artifact_name)
continue

artifact_part = _as_safe_part_for_llm(artifact, artifact_name)
if artifact_part is not artifact:
mime_type = (
artifact.inline_data.mime_type if artifact.inline_data else None
)
logger.debug(
'Converted artifact "%s" (mime_type=%s) to text Part',
artifact_name,
mime_type,
)

llm_request.contents.append(
types.Content(
role='user',
parts=[
types.Part.from_text(
text=f'Artifact {artifact_name} is:'
),
artifact_part,
],
for part in llm_request.contents[-1].parts:
function_response = part.function_response
if function_response and function_response.name == 'load_artifacts':
response = function_response.response or {}
artifact_names = response.get('artifact_names', [])
for artifact_name in artifact_names:
# Try session-scoped first (default behavior)
artifact = await tool_context.load_artifact(artifact_name)

# If not found and name doesn't already have user: prefix,
# try cross-session artifacts with user: prefix
if artifact is None and not artifact_name.startswith('user:'):
prefixed_name = f'user:{artifact_name}'
artifact = await tool_context.load_artifact(prefixed_name)

if artifact is None:
logger.warning('Artifact "%s" not found, skipping', artifact_name)
continue

artifact_part = _as_safe_part_for_llm(artifact, artifact_name)
if artifact_part is not artifact:
mime_type = (
artifact.inline_data.mime_type if artifact.inline_data else None
)
logger.debug(
'Converted artifact "%s" (mime_type=%s) to text Part',
artifact_name,
mime_type,
)
)

llm_request.contents.append(
types.Content(
role='user',
parts=[
types.Part.from_text(
text=f'Artifact {artifact_name} is:'
),
artifact_part,
],
)
)


load_artifacts_tool = LoadArtifactsTool()
51 changes: 26 additions & 25 deletions src/google/adk/tools/load_mcp_resource_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,32 +124,33 @@ async def _append_resources_to_llm_request(

# Attach content
if llm_request.contents and llm_request.contents[-1].parts:
function_response = llm_request.contents[-1].parts[0].function_response
if function_response and function_response.name == self.name:
response = function_response.response or {}
resource_names = response.get("resource_names", [])
for resource_name in resource_names:
try:
contents = await self._mcp_toolset.read_resource(resource_name)

for content in contents:
part = self._mcp_content_to_part(content, resource_name)
llm_request.contents.append(
types.Content(
role="user",
parts=[
types.Part.from_text(
text=f"Resource {resource_name} is:"
),
part,
],
)
for part in llm_request.contents[-1].parts:
function_response = part.function_response
if function_response and function_response.name == self.name:
response = function_response.response or {}
resource_names = response.get("resource_names", [])
for resource_name in resource_names:
try:
contents = await self._mcp_toolset.read_resource(resource_name)

for content in contents:
part = self._mcp_content_to_part(content, resource_name)
llm_request.contents.append(
types.Content(
role="user",
parts=[
types.Part.from_text(
text=f"Resource {resource_name} is:"
),
part,
],
)
)
except Exception as e:
logger.warning(
"Failed to read MCP resource '%s': %s", resource_name, e
)
except Exception as e:
logger.warning(
"Failed to read MCP resource '%s': %s", resource_name, e
)
continue
continue

def _mcp_content_to_part(
self, content: Any, resource_name: str
Expand Down
54 changes: 54 additions & 0 deletions tests/unittests/integrations/oci/test_oci_genai_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,60 @@ def test_content_to_oci_message_function_response():
assert msg.content[0].text


def test_content_to_oci_message_multiple_function_responses():
import oci.generative_ai_inference.models as oci_models

part1 = Part.from_function_response(
name="get_weather", response={"result": "Sunny, 22°C"}
)
part1.function_response.id = "call_A"

part2 = Part.from_function_response(
name="get_price", response={"result": "$150"}
)
part2.function_response.id = "call_B"

content = Content(role="user", parts=[part1, part2])
msgs = _content_to_oci_message(content)

assert isinstance(msgs, list)
assert len(msgs) == 2

assert isinstance(msgs[0], oci_models.ToolMessage)
assert msgs[0].tool_call_id == "call_A"

assert isinstance(msgs[1], oci_models.ToolMessage)
assert msgs[1].tool_call_id == "call_B"


def test_build_chat_details_flattens_multiple_tool_messages(oci_llm):
import oci.generative_ai_inference.models as oci_models

part1 = Part.from_function_response(
name="get_weather", response={"result": "Sunny, 22°C"}
)
part1.function_response.id = "call_A"

part2 = Part.from_function_response(
name="get_price", response={"result": "$150"}
)
part2.function_response.id = "call_B"

request = LlmRequest(
model="google.gemini-2.5-flash",
contents=[Content(role="user", parts=[part1, part2])]
)

chat_details = oci_llm._build_chat_details(request)
messages = chat_details.chat_request.messages

assert len(messages) == 2
assert isinstance(messages[0], oci_models.ToolMessage)
assert messages[0].tool_call_id == "call_A"
assert isinstance(messages[1], oci_models.ToolMessage)
assert messages[1].tool_call_id == "call_B"


# ---------------------------------------------------------------------------
# _oci_response_to_llm_response
# ---------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions tests/unittests/telemetry/test_spans.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ def test_trace_tool_call_with_scalar_response(
types.Part(
function_response=types.FunctionResponse(
id=test_tool_call_id,
name='test_function_1',
name=mock_tool_fixture.name,
response={'result': scalar_function_response},
)
),
Expand Down Expand Up @@ -602,7 +602,7 @@ def test_trace_tool_call_with_dict_response(
types.Part(
function_response=types.FunctionResponse(
id=test_tool_call_id,
name='test_function_1',
name=mock_tool_fixture.name,
response=dict_function_response,
)
),
Expand Down