Skip to content

Commit a181a39

Browse files
doughaydenGWeale
authored andcommitted
fix: Make EUC request args JSON-serializable
Merge #6007 ### Link to Issue or Description of Change - Closes: #6006 **Problem:** Two call sites dump `AuthToolArguments` with the default `mode="python"`, leaving the auth scheme's `type` field as a live `SecuritySchemeType` enum inside the `adk_request_credential` FunctionCall args: - `build_auth_request_event` in `flows/llm_flows/functions.py` (the LLM-flow EUC path) - `create_auth_request_event` in `workflow/utils/_workflow_hitl_utils.py` (the v2 workflow HITL path) The in-memory `Event.content` is therefore not JSON-serializable: any consumer that `json.dumps` it before it round-trips the session DB raises `TypeError: Object of type SecuritySchemeType is not JSON serializable`. Memory ingestion (`VertexAiMemoryBankService`) is a concrete trigger. It stays hidden in most flows because the session DB launders enums to strings on write and the auth preprocessor re-validates from either form. **Solution:** Dump with `mode="json"` at both sites, which coerces the enum to its `"oauth2"` string while preserving `by_alias`/`exclude_none`. Parse-back is unaffected: pydantic validation already accepts the string form, exactly what DB-read events carry. Two one-line changes, each with a regression test. ### Testing Plan **Unit Tests:** - [x] I have added or updated unit tests for my change. - [x] All unit tests pass locally. Both tests build an EUC/auth request, assert the args are JSON-serializable, and assert the auth scheme type is the string `"oauth2"`. Each fails before its change and passes after. - `test_function_request_euc_args_are_json_serializable` in `tests/unittests/flows/llm_flows/test_functions_request_euc.py` (LLM-flow path) - `test_args_are_json_serializable` in `tests/unittests/workflow/utils/test_workflow_hitl_utils.py` (workflow HITL path) ```text $ pytest tests/unittests/flows/llm_flows/test_functions_request_euc.py -q 4 passed, 24 warnings in 1.25s $ pytest tests/unittests/workflow/utils/test_workflow_hitl_utils.py -q 12 passed, 5 warnings in 0.67s ``` **Manual End-to-End (E2E) Tests:** The inline repro in the linked issue is the manual check: `json.dumps` on the args dict raises `TypeError` before the change and passes after. Verified against google-adk 2.2.0 (== current `main`). ### Checklist - [x] I have read the CONTRIBUTING.md document. - [x] I have performed a self-review of my own code. - [x] I have commented my code, particularly in hard-to-understand areas. - [x] I have added tests that prove my fix is effective or that my feature works. - [x] New and existing unit tests pass locally with my changes. - [x] I have manually tested my changes end-to-end. - [ ] Any dependent changes have been merged and published in downstream modules. ### Additional context The workflow HITL site was folded in per @surajksharma07's suggestion on the issue, so both auth-request builders are now covered. The remaining sibling `model_dump(exclude_none=True, by_alias=True)` call sites in `functions.py` (the tool-confirmation args near `:368`/`:371` and the event-actions dump near `:1241`) follow the same python-mode pattern and may carry the same hazard if their models contain enums, but they are a different feature path with no confirmed repro, so I left them out of scope. Happy to fold them in if you'd prefer consistency across the file. Co-authored-by: George Weale <gweale@google.com> COPYBARA_INTEGRATE_REVIEW=#6007 from doughayden:fix/euc-request-args-json-serializable 71f02d9 PiperOrigin-RevId: 933934084
1 parent ea325c7 commit a181a39

4 files changed

Lines changed: 106 additions & 2 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ def build_auth_request_event(
302302
args=AuthToolArguments(
303303
function_call_id=function_call_id,
304304
auth_config=auth_config,
305-
).model_dump(exclude_none=True, by_alias=True),
305+
).model_dump(mode='json', exclude_none=True, by_alias=True),
306306
)
307307
long_running_tool_ids.add(request_euc_function_call.id)
308308
parts.append(types.Part(function_call=request_euc_function_call))

src/google/adk/workflow/utils/_workflow_hitl_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def create_auth_request_event(
172172
args = AuthToolArguments(
173173
function_call_id=interrupt_id,
174174
auth_config=auth_request,
175-
).model_dump(exclude_none=True, by_alias=True)
175+
).model_dump(mode='json', exclude_none=True, by_alias=True)
176176

177177
# Add message so the UI / CLI knows what to display.
178178
args['message'] = _build_auth_message(auth_config)

tests/unittests/flows/llm_flows/test_functions_request_euc.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import json
1516
from typing import Any
1617
from typing import Optional
1718

@@ -155,6 +156,66 @@ def call_external_api2(tool_context: ToolContext) -> Optional[int]:
155156
assert len(mock_model.requests) == 1
156157

157158

159+
def test_function_request_euc_args_are_json_serializable():
160+
responses = [
161+
[
162+
types.Part.from_function_call(name='call_external_api', args={}),
163+
],
164+
[
165+
types.Part.from_text(text='response1'),
166+
],
167+
]
168+
169+
auth_config = AuthConfig(
170+
auth_scheme=OAuth2(
171+
flows=OAuthFlows(
172+
authorizationCode=OAuthFlowAuthorizationCode(
173+
authorizationUrl='https://accounts.google.com/o/oauth2/auth',
174+
tokenUrl='https://oauth2.googleapis.com/token',
175+
scopes={
176+
'https://www.googleapis.com/auth/calendar': (
177+
'See, edit, share, and permanently delete all the'
178+
' calendars you can access using Google Calendar'
179+
)
180+
},
181+
)
182+
)
183+
),
184+
raw_auth_credential=AuthCredential(
185+
auth_type=AuthCredentialTypes.OAUTH2,
186+
oauth2=OAuth2Auth(
187+
client_id='oauth_client_id',
188+
client_secret='oauth_client_secret',
189+
),
190+
),
191+
)
192+
193+
mock_model = testing_utils.MockModel.create(responses=responses)
194+
195+
def call_external_api(tool_context: ToolContext) -> Optional[int]:
196+
tool_context.request_credential(auth_config)
197+
198+
agent = Agent(
199+
name='root_agent',
200+
model=mock_model,
201+
tools=[call_external_api],
202+
)
203+
runner = testing_utils.InMemoryRunner(agent)
204+
events = runner.run('test')
205+
206+
request_euc_function_call = events[1].content.parts[0].function_call
207+
assert (
208+
request_euc_function_call.name == functions.REQUEST_EUC_FUNCTION_CALL_NAME
209+
)
210+
211+
# python-mode dump leaves auth_scheme.type a live enum, breaking json.dumps
212+
json.dumps(request_euc_function_call.args)
213+
assert (
214+
request_euc_function_call.args['authConfig']['authScheme']['type']
215+
== 'oauth2'
216+
)
217+
218+
158219
def test_function_get_auth_response():
159220
id_1 = 'id_1'
160221
id_2 = 'id_2'

tests/unittests/workflow/utils/test_workflow_hitl_utils.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
from __future__ import annotations
1616

17+
import json
18+
1719
from google.adk.events.event import Event
1820
from google.adk.events.event import NodeInfo
1921
from google.adk.events.request_input import RequestInput
@@ -172,5 +174,46 @@ def test_creates_credential_request(self):
172174
assert fc.id == "auth-id-1"
173175
assert "authConfig" in fc.args
174176

177+
def test_args_are_json_serializable(self):
178+
from fastapi.openapi.models import OAuth2
179+
from fastapi.openapi.models import OAuthFlowAuthorizationCode
180+
from fastapi.openapi.models import OAuthFlows
181+
from google.adk.auth.auth_credential import AuthCredential
182+
from google.adk.auth.auth_credential import AuthCredentialTypes
183+
from google.adk.auth.auth_credential import OAuth2Auth
184+
from google.adk.auth.auth_tool import AuthConfig
185+
186+
auth_config = AuthConfig(
187+
auth_scheme=OAuth2(
188+
flows=OAuthFlows(
189+
authorizationCode=OAuthFlowAuthorizationCode(
190+
authorizationUrl=(
191+
"https://accounts.google.com/o/oauth2/auth"
192+
),
193+
tokenUrl="https://oauth2.googleapis.com/token",
194+
scopes={
195+
"https://www.googleapis.com/auth/calendar": (
196+
"See calendars"
197+
)
198+
},
199+
)
200+
)
201+
),
202+
raw_auth_credential=AuthCredential(
203+
auth_type=AuthCredentialTypes.OAUTH2,
204+
oauth2=OAuth2Auth(
205+
client_id="oauth_client_id",
206+
client_secret="oauth_client_secret",
207+
),
208+
),
209+
)
210+
event = create_auth_request_event(auth_config, "auth-id-1")
211+
212+
fc = event.content.parts[0].function_call
213+
214+
# python-mode dump leaves auth_scheme.type a live enum, breaking json.dumps
215+
json.dumps(fc.args)
216+
assert fc.args["authConfig"]["authScheme"]["type"] == "oauth2"
217+
175218

176219
#

0 commit comments

Comments
 (0)