Skip to content

Commit fab5347

Browse files
anxkhncopybara-github
authored andcommitted
fix: percent-encode context id fields so ids with separators round-trip
Merge google#6407 PiperOrigin-RevId: 952391123
1 parent 217a90a commit fab5347

2 files changed

Lines changed: 54 additions & 18 deletions

File tree

src/google/adk/a2a/converters/utils.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
from __future__ import annotations
1616

17+
from urllib.parse import quote
18+
from urllib.parse import unquote
19+
1720
ADK_METADATA_KEY_PREFIX = "adk_"
1821
ADK_CONTEXT_ID_PREFIX = "ADK"
1922
ADK_CONTEXT_ID_SEPARATOR = "/"
@@ -54,18 +57,23 @@ def _to_a2a_context_id(app_name: str, user_id: str, session_id: str) -> str:
5457
raise ValueError(
5558
"All parameters (app_name, user_id, session_id) must be non-empty"
5659
)
57-
return ADK_CONTEXT_ID_SEPARATOR.join(
58-
[ADK_CONTEXT_ID_PREFIX, app_name, user_id, session_id]
59-
)
60+
return ADK_CONTEXT_ID_SEPARATOR.join([
61+
ADK_CONTEXT_ID_PREFIX,
62+
quote(app_name, safe=""),
63+
quote(user_id, safe=""),
64+
quote(session_id, safe=""),
65+
])
6066

6167

6268
def _from_a2a_context_id(
6369
context_id: str | None,
6470
) -> tuple[str, str, str] | tuple[None, None, None]:
6571
"""Converts an A2A context id to app name, user id and session id.
66-
if context_id is None, return None, None, None
67-
if context_id is not None, but not in the format of
68-
ADK$app_name$user_id$session_id, return None, None, None
72+
73+
Reverses ``_to_a2a_context_id``. The context id is expected to be
74+
``ADK/<app_name>/<user_id>/<session_id>`` with each field percent-encoded, so
75+
that ids containing the separator still round-trip. Returns (None, None, None)
76+
if context_id is None or does not match this format.
6977
7078
Args:
7179
context_id: The A2A context id.
@@ -76,16 +84,18 @@ def _from_a2a_context_id(
7684
if not context_id:
7785
return None, None, None
7886

79-
try:
80-
parts = context_id.split(ADK_CONTEXT_ID_SEPARATOR)
81-
if len(parts) != 4:
82-
return None, None, None
83-
84-
prefix, app_name, user_id, session_id = parts
85-
if prefix == ADK_CONTEXT_ID_PREFIX and app_name and user_id and session_id:
86-
return app_name, user_id, session_id
87-
except ValueError:
88-
# Handle any split errors gracefully
89-
pass
87+
parts = context_id.split(ADK_CONTEXT_ID_SEPARATOR)
88+
if len(parts) != 4:
89+
return None, None, None
90+
91+
prefix, app_name, user_id, session_id = parts
92+
if prefix != ADK_CONTEXT_ID_PREFIX:
93+
return None, None, None
94+
95+
app_name = unquote(app_name)
96+
user_id = unquote(user_id)
97+
session_id = unquote(session_id)
98+
if app_name and user_id and session_id:
99+
return app_name, user_id, session_id
90100

91101
return None, None, None

tests/unittests/a2a/converters/test_utils.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def test_to_a2a_context_id_special_characters(self):
112112

113113
result = _to_a2a_context_id(app_name, user_id, session_id)
114114

115-
expected = f"{ADK_CONTEXT_ID_PREFIX}/test-app@2024/user_123/session-456"
115+
expected = f"{ADK_CONTEXT_ID_PREFIX}/test-app%402024/user_123/session-456"
116116
assert result == expected
117117

118118
def test_from_a2a_context_id_success(self):
@@ -202,3 +202,29 @@ def test_from_a2a_context_id_special_characters(self):
202202
assert app_name == "test-app@2024"
203203
assert user_id == "user_123"
204204
assert session_id == "session-456"
205+
206+
@pytest.mark.parametrize(
207+
"app_name, user_id, session_id",
208+
[
209+
("app", "user", "projects/p/sessions/s"),
210+
("app", "projects/p/subscriptions/sub", "session"),
211+
("apps/a/versions/v", "user", "session"),
212+
("app", "user", "a/b/c"),
213+
],
214+
)
215+
def test_roundtrip_context_id_with_separator_in_ids(
216+
self, app_name, user_id, session_id
217+
):
218+
"""Test roundtrip conversion when ids contain the separator character.
219+
220+
App names, user ids and session ids can legitimately contain the separator
221+
(for example fully-qualified resource paths). The context id must still
222+
round-trip so A2A session resolution does not silently fail.
223+
"""
224+
context_id = _to_a2a_context_id(app_name, user_id, session_id)
225+
226+
parsed_app, parsed_user, parsed_session = _from_a2a_context_id(context_id)
227+
228+
assert parsed_app == app_name
229+
assert parsed_user == user_id
230+
assert parsed_session == session_id

0 commit comments

Comments
 (0)