Skip to content

Commit 6225046

Browse files
authored
Merge branch 'main' into motill/durable-support
2 parents b491999 + 2addf6b commit 6225046

19 files changed

Lines changed: 1110 additions & 293 deletions

File tree

.github/workflows/mypy.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
name: Mypy Type Check
22

33
on:
4-
push:
5-
branches: [ main ]
6-
pull_request:
7-
branches: [ main ]
4+
workflow_dispatch:
85

96
jobs:
107
mypy:

contributing/samples/api_registry_agent/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import os
1616

1717
from google.adk.agents.llm_agent import LlmAgent
18-
from google.adk.tools.api_registry import ApiRegistry
18+
from google.adk.integrations.api_registry import ApiRegistry
1919

2020
# TODO: Fill in with your GCloud project id and MCP server name
2121
PROJECT_ID = "your-google-cloud-project-id"

contributing/samples/bigtable/agent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@
6262
credentials_config=credentials_config, bigtable_tool_settings=tool_settings
6363
)
6464

65-
_BIGTABLE_PROJECT_ID = "google.com:cloud-bigtable-dev"
66-
_BIGTABLE_INSTANCE_ID = "annenguyen-bus-instance"
65+
_BIGTABLE_PROJECT_ID = ""
66+
_BIGTABLE_INSTANCE_ID = ""
6767

6868

6969
def search_hotels_by_location(

src/google/adk/cli/utils/agent_loader.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -335,13 +335,18 @@ def load_agent(self, agent_name: str) -> Union[BaseAgent, App]:
335335
def list_agents(self) -> list[str]:
336336
"""Lists all agents available in the agent loader (sorted alphabetically)."""
337337
base_path = Path.cwd() / self.agents_dir
338-
agent_names = [
339-
x
340-
for x in os.listdir(base_path)
341-
if os.path.isdir(os.path.join(base_path, x))
342-
and not x.startswith(".")
343-
and x != "__pycache__"
344-
]
338+
agent_names = []
339+
for x in os.listdir(base_path):
340+
if (
341+
os.path.isdir(os.path.join(base_path, x))
342+
and not x.startswith(".")
343+
and x != "__pycache__"
344+
):
345+
try:
346+
self._determine_agent_language(x)
347+
agent_names.append(x)
348+
except ValueError:
349+
continue
345350
agent_names.sort()
346351
return agent_names
347352

src/google/adk/evaluation/evaluation_generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ def convert_events_to_eval_invocations(
280280
invocations = []
281281
for invocation_id, events in events_by_invocation_id.items():
282282
final_response = None
283-
user_content = ""
283+
user_content = Content(parts=[])
284284
invocation_timestamp = 0
285285
app_details = None
286286
if (

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

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -730,41 +730,77 @@ async def _run_with_trace():
730730
# Make a deep copy to avoid being modified.
731731
function_response = None
732732

733-
# Handle before_tool_callbacks - iterate through the canonical callback
734-
# list
735-
for callback in agent.canonical_before_tool_callbacks:
736-
function_response = callback(
737-
tool=tool, args=function_args, tool_context=tool_context
738-
)
739-
if inspect.isawaitable(function_response):
740-
function_response = await function_response
741-
if function_response:
742-
break
733+
# Step 1: Check if plugin before_tool_callback overrides the function
734+
# response.
735+
function_response = (
736+
await invocation_context.plugin_manager.run_before_tool_callback(
737+
tool=tool, tool_args=function_args, tool_context=tool_context
738+
)
739+
)
743740

741+
# Step 2: If no overrides are provided from the plugins, further run the
742+
# canonical callback.
744743
if function_response is None:
745-
function_response = await _process_function_live_helper(
746-
tool,
747-
tool_context,
748-
function_call,
749-
function_args,
750-
invocation_context,
751-
streaming_lock,
752-
)
744+
for callback in agent.canonical_before_tool_callbacks:
745+
function_response = callback(
746+
tool=tool, args=function_args, tool_context=tool_context
747+
)
748+
if inspect.isawaitable(function_response):
749+
function_response = await function_response
750+
if function_response:
751+
break
753752

754-
# Calls after_tool_callback if it exists.
755-
altered_function_response = None
756-
for callback in agent.canonical_after_tool_callbacks:
757-
altered_function_response = callback(
758-
tool=tool,
759-
args=function_args,
760-
tool_context=tool_context,
761-
tool_response=function_response,
762-
)
763-
if inspect.isawaitable(altered_function_response):
764-
altered_function_response = await altered_function_response
765-
if altered_function_response:
766-
break
753+
# Step 3: Otherwise, proceed calling the tool normally.
754+
if function_response is None:
755+
try:
756+
function_response = await _process_function_live_helper(
757+
tool,
758+
tool_context,
759+
function_call,
760+
function_args,
761+
invocation_context,
762+
streaming_lock,
763+
)
764+
except Exception as tool_error:
765+
error_response = await _run_on_tool_error_callbacks(
766+
tool=tool,
767+
tool_args=function_args,
768+
tool_context=tool_context,
769+
error=tool_error,
770+
)
771+
if error_response is not None:
772+
function_response = error_response
773+
else:
774+
raise tool_error
767775

776+
# Step 4: Check if plugin after_tool_callback overrides the function
777+
# response.
778+
altered_function_response = (
779+
await invocation_context.plugin_manager.run_after_tool_callback(
780+
tool=tool,
781+
tool_args=function_args,
782+
tool_context=tool_context,
783+
result=function_response,
784+
)
785+
)
786+
787+
# Step 5: If no overrides are provided from the plugins, further run the
788+
# canonical after_tool_callbacks.
789+
if altered_function_response is None:
790+
for callback in agent.canonical_after_tool_callbacks:
791+
altered_function_response = callback(
792+
tool=tool,
793+
args=function_args,
794+
tool_context=tool_context,
795+
tool_response=function_response,
796+
)
797+
if inspect.isawaitable(altered_function_response):
798+
altered_function_response = await altered_function_response
799+
if altered_function_response:
800+
break
801+
802+
# Step 6: If alternative response exists from after_tool_callback, use it
803+
# instead of the original function response.
768804
if altered_function_response is not None:
769805
function_response = altered_function_response
770806

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
13+
from .api_registry import ApiRegistry
14+
15+
__all__ = [
16+
'ApiRegistry',
17+
]
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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+
from typing import Any
18+
from typing import Callable
19+
20+
from google.adk.agents.readonly_context import ReadonlyContext
21+
from google.adk.tools.base_toolset import ToolPredicate
22+
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
23+
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
24+
import google.auth
25+
import google.auth.transport.requests
26+
import httpx
27+
28+
API_REGISTRY_URL = "https://cloudapiregistry.googleapis.com"
29+
30+
31+
class ApiRegistry:
32+
"""Registry that provides McpToolsets for MCP servers registered in API Registry."""
33+
34+
def __init__(
35+
self,
36+
api_registry_project_id: str,
37+
location: str = "global",
38+
header_provider: (
39+
Callable[[ReadonlyContext], dict[str, str]] | None
40+
) = None,
41+
):
42+
"""Initialize the API Registry.
43+
44+
Args:
45+
api_registry_project_id: The project ID for the Google Cloud API Registry.
46+
location: The location of the API Registry resources.
47+
header_provider: Optional function to provide additional headers for MCP
48+
server calls.
49+
"""
50+
self.api_registry_project_id = api_registry_project_id
51+
self.location = location
52+
self._credentials, _ = google.auth.default()
53+
self._mcp_servers: dict[str, dict[str, Any]] = {}
54+
self._header_provider = header_provider
55+
56+
url = f"{API_REGISTRY_URL}/v1beta/projects/{self.api_registry_project_id}/locations/{self.location}/mcpServers"
57+
58+
try:
59+
headers = self._get_auth_headers()
60+
headers["Content-Type"] = "application/json"
61+
page_token = None
62+
with httpx.Client() as client:
63+
while True:
64+
params = {}
65+
if page_token:
66+
params["pageToken"] = page_token
67+
68+
response = client.get(url, headers=headers, params=params)
69+
response.raise_for_status()
70+
data = response.json()
71+
mcp_servers_list = data.get("mcpServers", [])
72+
for server in mcp_servers_list:
73+
server_name = server.get("name", "")
74+
if server_name:
75+
self._mcp_servers[server_name] = server
76+
77+
page_token = data.get("nextPageToken")
78+
if not page_token:
79+
break
80+
except (httpx.HTTPError, ValueError) as e:
81+
# Handle error in fetching or parsing tool definitions
82+
raise RuntimeError(
83+
f"Error fetching MCP servers from API Registry: {e}"
84+
) from e
85+
86+
def get_toolset(
87+
self,
88+
mcp_server_name: str,
89+
tool_filter: ToolPredicate | list[str] | None = None,
90+
tool_name_prefix: str | None = None,
91+
) -> McpToolset:
92+
"""Return the MCP Toolset based on the params.
93+
94+
Args:
95+
mcp_server_name: Filter to select the MCP server name to get tools from.
96+
tool_filter: Optional filter to select specific tools. Can be a list of
97+
tool names or a ToolPredicate function.
98+
tool_name_prefix: Optional prefix to prepend to the names of the tools
99+
returned by the toolset.
100+
101+
Returns:
102+
McpToolset: A toolset for the MCP server specified.
103+
"""
104+
server = self._mcp_servers.get(mcp_server_name)
105+
if not server:
106+
raise ValueError(
107+
f"MCP server {mcp_server_name} not found in API Registry."
108+
)
109+
if not server.get("urls"):
110+
raise ValueError(f"MCP server {mcp_server_name} has no URLs.")
111+
112+
mcp_server_url = server["urls"][0]
113+
headers = self._get_auth_headers()
114+
115+
# Only prepend "https://" if the URL doesn't already have a scheme
116+
if not mcp_server_url.startswith(("http://", "https://")):
117+
mcp_server_url = "https://" + mcp_server_url
118+
119+
return McpToolset(
120+
connection_params=StreamableHTTPConnectionParams(
121+
url=mcp_server_url,
122+
headers=headers,
123+
),
124+
tool_filter=tool_filter,
125+
tool_name_prefix=tool_name_prefix,
126+
header_provider=self._header_provider,
127+
)
128+
129+
def _get_auth_headers(self) -> dict[str, str]:
130+
"""Refreshes credentials and returns authorization headers."""
131+
request = google.auth.transport.requests.Request()
132+
self._credentials.refresh(request)
133+
headers = {
134+
"Authorization": f"Bearer {self._credentials.token}",
135+
}
136+
# Add quota project header if available in ADC
137+
quota_project_id = getattr(self._credentials, "quota_project_id", None)
138+
if quota_project_id:
139+
headers["x-goog-user-project"] = quota_project_id
140+
return headers

src/google/adk/models/lite_llm.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,10 +388,18 @@ def _convert_reasoning_value_to_parts(reasoning_value: Any) -> List[types.Part]:
388388

389389

390390
def _extract_reasoning_value(message: Message | Delta | None) -> Any:
391-
"""Fetches the reasoning payload from a LiteLLM message."""
391+
"""Fetches the reasoning payload from a LiteLLM message.
392+
393+
Checks for both 'reasoning_content' (LiteLLM standard, used by Azure/Foundry,
394+
Ollama via LiteLLM) and 'reasoning' (used by LM Studio, vLLM).
395+
Prioritizes 'reasoning_content' when both are present.
396+
"""
392397
if message is None:
393398
return None
394-
return message.get("reasoning_content")
399+
reasoning_content = message.get("reasoning_content")
400+
if reasoning_content is not None:
401+
return reasoning_content
402+
return message.get("reasoning")
395403

396404

397405
class ChatCompletionFileUrlObject(TypedDict, total=False):
@@ -1302,6 +1310,7 @@ def _has_meaningful_signal(message: Message | Delta | None) -> bool:
13021310
or message.get("tool_calls")
13031311
or message.get("function_call")
13041312
or message.get("reasoning_content")
1313+
or message.get("reasoning")
13051314
)
13061315

13071316
if isinstance(response, ModelResponseStream):

0 commit comments

Comments
 (0)