fix: Set role='model' for request_input event Content

Set role='model' in the Content object generated by create_request_input_event()
to ensure proper role attribution when returning function call events to the client.

Co-authored-by: Shangjie Chen <deanchen@google.com>
PiperOrigin-RevId: 929538818
This commit is contained in:
Shangjie Chen
2026-06-09 19:19:03 -07:00
committed by Copybara-Service
parent 048deeaeb7
commit 0c6974cbc4
15 changed files with 91 additions and 18 deletions
@@ -94,7 +94,7 @@
}
}
],
"role": "user"
"role": "model"
},
"id": "e-3",
"invocationId": "i-1",
@@ -64,7 +64,7 @@
}
}
],
"role": "user"
"role": "model"
},
"id": "e-3",
"invocationId": "i-1",
@@ -66,7 +66,7 @@
}
}
],
"role": "user"
"role": "model"
},
"id": "e-3",
"invocationId": "i-1",
@@ -66,7 +66,7 @@
}
}
],
"role": "user"
"role": "model"
},
"id": "e-3",
"invocationId": "i-1",
@@ -208,7 +208,7 @@
}
}
],
"role": "user"
"role": "model"
},
"id": "e-9",
"invocationId": "i-1",
@@ -176,7 +176,7 @@
}
}
],
"role": "user"
"role": "model"
},
"id": "e-9",
"invocationId": "i-3",
@@ -43,7 +43,8 @@
"name": "adk_request_credential"
}
}
]
],
"role": "model"
},
"id": "e-2",
"invocationId": "i-1",
@@ -73,7 +73,8 @@
"name": "adk_request_input"
}
}
]
],
"role": "model"
},
"id": "e-4",
"invocationId": "i-1",
@@ -162,7 +163,8 @@
"name": "adk_request_input"
}
}
]
],
"role": "model"
},
"id": "e-8",
"invocationId": "i-1",
@@ -72,7 +72,8 @@
"name": "adk_request_input"
}
}
]
],
"role": "model"
},
"id": "e-4",
"invocationId": "i-1",
@@ -92,7 +92,8 @@
"name": "adk_request_input"
}
}
]
],
"role": "model"
},
"id": "e-3",
"invocationId": "i-1",
@@ -73,7 +73,8 @@
"name": "adk_request_input"
}
}
]
],
"role": "model"
},
"id": "e-4",
"invocationId": "i-1",
@@ -162,7 +163,8 @@
"name": "adk_request_input"
}
}
]
],
"role": "model"
},
"id": "e-8",
"invocationId": "i-1",
+65
View File
@@ -22,6 +22,7 @@ from typing import AsyncGenerator
from typing import Optional
from unittest import mock
from google.adk.agents.base_agent import BaseAgent
from google.adk.apps.app import App
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
from google.adk.cli.utils.agent_loader import AgentLoader
@@ -249,6 +250,33 @@ def _make_nodes_sequential(obj, visited=None):
_make_nodes_sequential(obj._node, visited)
def _get_all_agent_names(obj, visited=None):
if visited is None:
visited = set()
if id(obj) in visited:
return set()
visited.add(id(obj))
from google.adk.workflow._parallel_worker import _ParallelWorker
from google.adk.workflow._workflow import Workflow
names = set()
if isinstance(obj, BaseAgent) and hasattr(obj, "name"):
names.add(obj.name)
if hasattr(obj, "sub_agents") and obj.sub_agents:
for sub in obj.sub_agents:
names.update(_get_all_agent_names(sub, visited))
elif isinstance(obj, Workflow):
if obj.graph and obj.graph.nodes:
for node in obj.graph.nodes:
names.update(_get_all_agent_names(node, visited))
elif isinstance(obj, _ParallelWorker):
if hasattr(obj, "_node"):
names.update(_get_all_agent_names(obj._node, visited))
return names
def _extract_user_content(event: dict) -> Optional[types.Content]:
"""Extracts user content from an event dict and returns a types.Content object.
@@ -444,6 +472,26 @@ def test_agent_replay(agent_dir, test_file, monkeypatch):
else agent_or_app
)
_make_nodes_sequential(root_agent)
agent_names = _get_all_agent_names(root_agent)
import inspect
# Dynamically locate the loaded agent module from sys.modules
mod = sys.modules.get(f"{agent_dir.name}.agent") or sys.modules.get(
agent_dir.name
)
if not mod:
# Fallback for namespace packages or nested imports
for k, v in sys.modules.items():
if k.endswith(f"{agent_dir.name}.agent") or k.endswith(agent_dir.name):
mod = v
break
# Reflectively find all Agent instances defined in the module (e.g. dynamic agents)
if mod:
for _, obj in inspect.getmembers(mod):
if isinstance(obj, BaseAgent) and hasattr(obj, "name"):
agent_names.add(obj.name)
with open(test_file, "r") as f:
session_data = json.load(f)
@@ -488,6 +536,23 @@ def test_agent_replay(agent_dir, test_file, monkeypatch):
last_was_set_model_response = False
continue
if ev.get("author", "") not in agent_names:
continue
parts = content_dict.get("parts", [])
is_sys_hitl = False
for part in parts:
if "functionCall" in part:
fc_name = part["functionCall"].get("name")
if fc_name in (
"adk_request_confirmation",
"adk_request_credential",
):
is_sys_hitl = True
break
if is_sys_hitl:
continue
try:
content_obj = types.Content.model_validate(content_dict)
all_responses.append(
+1 -3
View File
@@ -381,9 +381,7 @@ def generate_request_confirmation_event(
invocation_id=invocation_context.invocation_id,
author=invocation_context.agent.name,
branch=invocation_context.branch,
content=types.Content(
parts=parts, role=function_response_event.content.role
),
content=types.Content(parts=parts, role='model'),
long_running_tool_ids=long_running_tool_ids,
)
@@ -55,6 +55,7 @@ def create_request_input_event(request_input: RequestInput) -> Event:
)
return Event(
content=types.Content(
role='model',
parts=[
types.Part(
function_call=types.FunctionCall(
@@ -63,7 +64,7 @@ def create_request_input_event(request_input: RequestInput) -> Event:
id=request_input.interrupt_id,
)
)
]
],
),
long_running_tool_ids=[request_input.interrupt_id],
)
@@ -179,6 +180,7 @@ def create_auth_request_event(
return Event(
content=types.Content(
role='model',
parts=[
types.Part(
function_call=types.FunctionCall(
@@ -187,7 +189,7 @@ def create_auth_request_event(
args=args,
)
)
]
],
),
long_running_tool_ids=[interrupt_id],
)
@@ -40,6 +40,7 @@ class TestCreateRequestInputEvent:
assert event.long_running_tool_ids == {"test-id"}
assert event.content is not None
assert event.content.role == "model"
fc = event.content.parts[0].function_call
assert fc.name == "adk_request_input"
assert fc.id == "test-id"