Skip to content

Commit dd72e3a

Browse files
authored
Merge branch 'main' into feat/evaluate-full-response
2 parents e8b1608 + dcc485b commit dd72e3a

11 files changed

Lines changed: 548 additions & 86 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ test = [
123123
"a2a-sdk>=0.3.0,<0.4.0",
124124
"anthropic>=0.43.0", # For anthropic model tests
125125
"crewai[tools];python_version>='3.11' and python_version<'3.12'", # For CrewaiTool tests; chromadb/pypika fail on 3.12+
126+
"google-cloud-parametermanager>=0.4.0, <1.0.0",
126127
"kubernetes>=29.0.0", # For GkeCodeExecutor
127128
"langchain-community>=0.3.17",
128129
"langgraph>=0.2.60, <0.4.8", # For LangGraphAgent
@@ -157,6 +158,7 @@ extensions = [
157158
"beautifulsoup4>=3.2.2", # For load_web_page tool.
158159
"crewai[tools];python_version>='3.11' and python_version<'3.12'", # For CrewaiTool; chromadb/pypika fail on 3.12+
159160
"docker>=7.0.0", # For ContainerCodeExecutor
161+
"google-cloud-parametermanager>=0.4.0, <1.0.0",
160162
"kubernetes>=29.0.0", # For GkeCodeExecutor
161163
"k8s-agent-sandbox>=0.1.1.post3", # For GkeCodeExecutor sandbox mode
162164
"langgraph>=0.2.60, <0.4.8", # For LangGraphAgent

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,23 @@ def convert_event_to_a2a_events(
218218
),
219219
)
220220
)
221+
elif _serialize_value(event.actions) is not None:
222+
a2a_events.append(
223+
TaskStatusUpdateEvent(
224+
task_id=task_id,
225+
context_id=context_id,
226+
status=TaskStatus(
227+
state=TaskState.working,
228+
message=Message(
229+
message_id=str(uuid.uuid4()),
230+
role=Role.agent,
231+
parts=[],
232+
),
233+
timestamp=datetime.now(timezone.utc).isoformat(),
234+
),
235+
final=False,
236+
)
237+
)
221238

222239
a2a_events = _add_event_metadata(event, a2a_events)
223240
return a2a_events
@@ -280,7 +297,10 @@ def _add_event_metadata(
280297
metadata[_get_adk_metadata_key(field_name)] = value
281298

282299
for a2a_event in a2a_events:
283-
if isinstance(a2a_event, TaskStatusUpdateEvent):
300+
if (
301+
isinstance(a2a_event, TaskStatusUpdateEvent)
302+
and a2a_event.status.message
303+
):
284304
a2a_event.status.message.metadata = metadata.copy()
285305
elif isinstance(a2a_event, TaskArtifactUpdateEvent):
286306
a2a_event.artifact.metadata = metadata.copy()

src/google/adk/cli/cli_create.py

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -327,48 +327,61 @@ def _handle_login_with_google() -> (
327327
selected_project_id = projects[project_index - 1][0]
328328
region = _prompt_for_google_cloud_region(None)
329329
return None, selected_project_id, region
330-
else:
331-
if click.confirm(
332-
"No projects found automatically. Would you like to enter one"
333-
" manually?",
334-
default=False,
335-
):
336-
selected_project_id = _prompt_for_google_cloud(None)
337-
region = _prompt_for_google_cloud_region(None)
338-
return None, selected_project_id, region
339-
340-
# Check Express eligibility
341-
if gcp_utils.check_express_eligibility():
342-
click.secho(_EXPRESS_TOS_MSG, fg="yellow")
343-
if click.confirm("Do you accept the Terms of Service?", default=False):
344-
selected_region = click.prompt(
345-
"""\
330+
331+
click.secho(
332+
"A Google Cloud project is required to continue. You can enter an"
333+
" existing project ID or create an Express Mode project. Learn more:"
334+
" https://cloud.google.com/resources/cloud-express-faqs",
335+
fg="green",
336+
)
337+
action = click.prompt(
338+
"1. Enter an existing Google Cloud project ID\n"
339+
"2. Create a new project (Express Mode)\n"
340+
"3. Abandon\n"
341+
"Choose an action",
342+
type=click.Choice(["1", "2", "3"]),
343+
)
344+
345+
if action == "3":
346+
raise click.Abort()
347+
348+
if action == "1":
349+
google_cloud_project = _prompt_for_google_cloud(None)
350+
google_cloud_region = _prompt_for_google_cloud_region(None)
351+
return None, google_cloud_project, google_cloud_region
352+
353+
elif action == "2":
354+
if gcp_utils.check_express_eligibility():
355+
click.secho(_EXPRESS_TOS_MSG, fg="yellow")
356+
if click.confirm("Do you accept the Terms of Service?", default=False):
357+
selected_region = click.prompt(
358+
"""\
346359
Choose a region for Express Mode:
347360
1. us-central1
348361
2. europe-west1
349362
3. asia-southeast1
350363
Choose region""",
351-
type=click.Choice(["1", "2", "3"]),
352-
default="1",
353-
)
354-
region_map = {
355-
"1": "us-central1",
356-
"2": "europe-west1",
357-
"3": "asia-southeast1",
358-
}
359-
region = region_map[selected_region]
360-
express_info = gcp_utils.sign_up_express(location=region)
361-
api_key = express_info.get("api_key")
362-
project_id = express_info.get("project_id")
363-
region = express_info.get("region", region)
364-
click.secho(
365-
f"Express Mode project created: {project_id}",
366-
fg="green",
367-
)
368-
return api_key, project_id, region
369-
370-
click.secho(_NOT_ELIGIBLE_MSG, fg="red")
371-
raise click.Abort()
364+
type=click.Choice(["1", "2", "3"]),
365+
default="1",
366+
)
367+
region_map = {
368+
"1": "us-central1",
369+
"2": "europe-west1",
370+
"3": "asia-southeast1",
371+
}
372+
region = region_map[selected_region]
373+
express_info = gcp_utils.sign_up_express(location=region)
374+
api_key = express_info.get("api_key")
375+
project_id = express_info.get("project_id")
376+
region = express_info.get("region", region)
377+
click.secho(
378+
f"Express Mode project created: {project_id}",
379+
fg="green",
380+
)
381+
return api_key, project_id, region
382+
383+
click.secho(_NOT_ELIGIBLE_MSG, fg="red")
384+
raise click.Abort()
372385

373386

374387
def _prompt_to_choose_type() -> str:
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from .parameter_client import ParameterManagerClient
16+
17+
__all__ = [
18+
'ParameterManagerClient',
19+
]
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import json
18+
from typing import cast
19+
from typing import Optional
20+
21+
import google.auth
22+
from google.auth import default as default_service_credential
23+
import google.auth.transport.requests
24+
from google.cloud import parametermanager_v1
25+
from google.oauth2 import service_account
26+
27+
28+
class ParameterManagerClient:
29+
"""A client for interacting with Google Cloud Parameter Manager.
30+
31+
This class provides a simplified interface for retrieving parameters from
32+
Parameter Manager, handling authentication using either a service account
33+
JSON keyfile (passed as a string), a preexisting authorization token, or
34+
default credentials.
35+
36+
Attributes:
37+
_credentials: Google Cloud credentials object (ServiceAccountCredentials
38+
or Credentials).
39+
_client: Parameter Manager client instance.
40+
"""
41+
42+
def __init__(
43+
self,
44+
service_account_json: Optional[str] = None,
45+
auth_token: Optional[str] = None,
46+
location: Optional[str] = None,
47+
):
48+
"""Initializes the ParameterManagerClient.
49+
50+
If neither `service_account_json` nor `auth_token` is provided, default
51+
credentials are used.
52+
53+
Args:
54+
service_account_json: The content of a service account JSON keyfile (as
55+
a string), not the file path. Must be valid JSON.
56+
auth_token: An existing Google Cloud authorization token.
57+
location: The Google Cloud location (region) to use for the Parameter
58+
Manager service. If not provided, the global endpoint is used.
59+
60+
Raises:
61+
ValueError: If both 'service_account_json' and 'auth_token' are
62+
provided. Also raised if the 'service_account_json' is not valid JSON.
63+
google.auth.exceptions.GoogleAuthError: If authentication fails.
64+
"""
65+
if service_account_json and auth_token:
66+
raise ValueError(
67+
"Must provide either 'service_account_json' or 'auth_token', not"
68+
" both."
69+
)
70+
71+
if service_account_json:
72+
try:
73+
credentials = service_account.Credentials.from_service_account_info(
74+
json.loads(service_account_json)
75+
)
76+
except json.JSONDecodeError as e:
77+
raise ValueError(f"Invalid service account JSON: {e}") from e
78+
elif auth_token:
79+
credentials = google.auth.credentials.Credentials(
80+
token=auth_token,
81+
refresh_token=None,
82+
token_uri=None,
83+
client_id=None,
84+
client_secret=None,
85+
)
86+
request = google.auth.transport.requests.Request()
87+
credentials.refresh(request)
88+
else:
89+
try:
90+
credentials, _ = default_service_credential(
91+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
92+
)
93+
except Exception as e:
94+
raise ValueError(
95+
"'service_account_json' or 'auth_token' are both missing, and"
96+
" error occurred while trying to use default credentials: {e}"
97+
) from e
98+
99+
if not credentials:
100+
raise ValueError(
101+
"Failed to obtain credentials. Provide either 'service_account_json'"
102+
" or 'auth_token', not both. If neither is provided, default"
103+
" credentials are used."
104+
)
105+
106+
self._credentials = credentials
107+
108+
client_options = None
109+
if location:
110+
client_options = {
111+
"api_endpoint": f"parametermanager.{location}.rep.googleapis.com"
112+
}
113+
114+
self._client = parametermanager_v1.ParameterManagerClient(
115+
credentials=self._credentials, client_options=client_options
116+
)
117+
118+
def get_parameter(self, resource_name: str) -> str:
119+
"""Retrieves a rendered parameter value from Google Cloud Parameter Manager.
120+
121+
Args:
122+
resource_name: The full resource name of the parameter version, in the
123+
format "projects/*/locations/*/parameters/*/versions/*". Usually you
124+
want the "latest" version, e.g.,
125+
"projects/my-project/locations/global/parameters/my-param/versions/latest".
126+
127+
Returns:
128+
The rendered parameter value as a string.
129+
130+
Raises:
131+
google.api_core.exceptions.GoogleAPIError: If the Parameter Manager API
132+
returns an error (e.g., parameter not found, permission denied).
133+
"""
134+
request = parametermanager_v1.RenderParameterVersionRequest(
135+
name=resource_name
136+
)
137+
response = self._client.render_parameter_version(request=request)
138+
return cast(str, response.rendered_payload.decode("UTF-8"))

src/google/adk/tools/mcp_tool/mcp_toolset.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
from ..load_mcp_resource_tool import LoadMcpResourceTool
4949
from ..tool_configs import BaseToolConfig
5050
from ..tool_configs import ToolArgsConfig
51-
from ..tool_context import ToolContext
5251
from .mcp_session_manager import MCPSessionManager
5352
from .mcp_session_manager import retry_on_errors
5453
from .mcp_session_manager import SseConnectionParams
@@ -448,6 +447,20 @@ def from_config(
448447
use_mcp_resources=mcp_toolset_config.use_mcp_resources,
449448
)
450449

450+
def __getstate__(self):
451+
"""Custom pickling to exclude non-picklable runtime objects."""
452+
state = self.__dict__.copy()
453+
# Remove unpicklable file-like objects
454+
state.pop("_errlog", None)
455+
return state
456+
457+
def __setstate__(self, state):
458+
"""Custom unpickling to restore state."""
459+
self.__dict__.update(state)
460+
# Default to sys.stderr if _errlog was removed during pickling
461+
if not hasattr(self, "_errlog") or self._errlog is None:
462+
self._errlog = sys.stderr
463+
451464

452465
class MCPToolset(McpToolset):
453466
"""Deprecated name, use `McpToolset` instead."""

tests/unittests/a2a/integration/test_client_server.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from a2a.types import TextPart
2222
from google.adk.agents.remote_a2a_agent import A2A_METADATA_PREFIX
2323
from google.adk.events.event import Event
24+
from google.adk.events.event_actions import EventActions
2425
from google.adk.platform import uuid as platform_uuid
2526
from google.adk.runners import Runner
2627
from google.adk.sessions.in_memory_session_service import InMemorySessionService
@@ -47,6 +48,11 @@ async def mock_run_async(**kwargs):
4748
content=types.Content(parts=[types.Part(text=" world")]),
4849
partial=True,
4950
)
51+
yield Event(
52+
author="FakeAgent",
53+
partial=True,
54+
actions=EventActions(artifact_delta={"file1": 1}),
55+
)
5056
yield Event(
5157
author="FakeAgent",
5258
content=types.Content(parts=[types.Part(text="Hello world")]),
@@ -92,18 +98,23 @@ async def test_streaming_adk_to_streaming_a2a():
9298
new_message = types.Content(parts=[types.Part(text="Hi")], role="user")
9399

94100
texts = []
101+
actions = []
95102
async for event in client_runner.run_async(
96103
user_id="test_user", session_id="test_session", new_message=new_message
97104
):
98105
if event.content and event.content.parts:
99106
for p in event.content.parts:
100107
if p.text:
101108
texts.append(p.text)
109+
if event.actions and event.actions.artifact_delta:
110+
actions.append(event.actions)
102111

103112
assert len(received_requests) == 1
104113
assert received_requests[0]["session_id"] is not None
105114

106115
assert texts == ["Hello", " world", "Hello world"]
116+
assert len(actions) == 1
117+
assert actions[0].artifact_delta == {"file1": 1}
107118

108119

109120
@pytest.mark.asyncio

0 commit comments

Comments
 (0)