Skip to content

Commit f8eed05

Browse files
authored
Merge branch 'main' into fix/mcp-mtls-auth-header-case
2 parents f8579c8 + 0f90ff4 commit f8eed05

21 files changed

Lines changed: 3214 additions & 151 deletions

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ optional-dependencies.extensions = [
154154
"llama-index-embeddings-google-genai>=0.3",
155155
"llama-index-readers-file>=0.4",
156156
"lxml>=5.3",
157+
"openai>=2.20,<3",
157158
"pypika>=0.50",
158159
"toolbox-adk>=1,<2",
159160
]
@@ -224,7 +225,7 @@ optional-dependencies.test = [
224225
"llama-index-readers-file>=0.4",
225226
"lxml>=5.3",
226227
"mcp>=1.24,<2",
227-
"openai>=1.100.2",
228+
"openai>=2.20,<3",
228229
"opentelemetry-exporter-gcp-logging>=1.9.0a0,<=1.12.0a0",
229230
"opentelemetry-exporter-gcp-monitoring>=1.9.0a0,<2",
230231
"opentelemetry-exporter-gcp-trace>=1.9,<2",

src/google/adk/agents/config_agent_utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,31 @@ def _resolve_agent_class(agent_class: str) -> type[BaseAgent]:
8282
)
8383

8484

85+
_BLOCKED_YAML_KEYS = frozenset({"args"})
86+
_ENFORCE_YAML_KEY_DENYLIST = False
87+
88+
89+
def _set_enforce_yaml_key_denylist(value: bool) -> None:
90+
global _ENFORCE_YAML_KEY_DENYLIST
91+
_ENFORCE_YAML_KEY_DENYLIST = value
92+
93+
94+
def _check_config_for_blocked_keys(node: Any, filename: str) -> None:
95+
"""Recursively check if the configuration contains any blocked keys."""
96+
if isinstance(node, dict):
97+
for key, value in node.items():
98+
if key in _BLOCKED_YAML_KEYS:
99+
raise ValueError(
100+
f"Blocked key {key!r} found in {filename!r}. "
101+
f"The '{key}' field is not allowed in agent configurations "
102+
"because it can execute arbitrary code."
103+
)
104+
_check_config_for_blocked_keys(value, filename)
105+
elif isinstance(node, list):
106+
for item in node:
107+
_check_config_for_blocked_keys(item, filename)
108+
109+
85110
def _load_config_from_path(config_path: str) -> AgentConfig:
86111
"""Load an agent's configuration from a YAML file.
87112
@@ -102,6 +127,9 @@ def _load_config_from_path(config_path: str) -> AgentConfig:
102127
with open(config_path, "r", encoding="utf-8") as f:
103128
config_data = yaml.safe_load(f)
104129

130+
if _ENFORCE_YAML_KEY_DENYLIST:
131+
_check_config_for_blocked_keys(config_data, config_path)
132+
105133
return AgentConfig.model_validate(config_data)
106134

107135

src/google/adk/cli/fast_api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,12 @@ def get_fast_api_app(
486486
The configured FastAPI application instance.
487487
"""
488488

489+
# Enable the YAML key denylist for config loads if the web UI is enabled.
490+
if web:
491+
from ..agents import config_agent_utils
492+
493+
config_agent_utils._set_enforce_yaml_key_denylist(True)
494+
489495
# Detect single agent mode
490496
agents_path = Path(agents_dir).resolve()
491497
is_single_agent = is_single_agent_directory(agents_path)

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ async def run_async(
6969
id_pairing_model_types.append(LiteLlm)
7070
except (ImportError, OSError):
7171
pass
72+
try:
73+
from ...labs.openai import OpenAIResponsesLlm
74+
75+
id_pairing_model_types.append(OpenAIResponsesLlm)
76+
except (ImportError, OSError):
77+
pass
7278
if isinstance(canonical_model, tuple(id_pairing_model_types)):
7379
preserve_function_call_ids = True
7480

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import contextvars
2424
import copy
2525
import inspect
26+
import json
2627
import logging
2728
import threading
2829
from typing import Any
@@ -1181,6 +1182,9 @@ def __build_response_event(
11811182
tool_context: ToolContext,
11821183
invocation_context: InvocationContext,
11831184
) -> Event:
1185+
# Capture the raw result for display purposes before any normalization.
1186+
display_result = function_result
1187+
11841188
# Specs requires the result to be a dict.
11851189
if not isinstance(function_result, dict):
11861190
function_result = {'result': function_result}
@@ -1198,6 +1202,25 @@ def __build_response_event(
11981202
function_response_parts,
11991203
)
12001204

1205+
# When summarization is skipped, ensure a displayable text part is added so
1206+
# the tool's output is not lost in UIs that don't render function responses.
1207+
# Control-flow tools (e.g. exit_loop) set skip_summarization but return no
1208+
# meaningful output; their None result is normalized to {'result': None}, so
1209+
# skip those to avoid emitting a noisy "null" text part.
1210+
has_displayable_result = display_result is not None and display_result != {
1211+
'result': None
1212+
}
1213+
if (
1214+
tool_context.actions.skip_summarization
1215+
and 'error' not in function_result
1216+
and has_displayable_result
1217+
):
1218+
if isinstance(display_result, str):
1219+
result_text = display_result
1220+
else:
1221+
result_text = json.dumps(display_result, ensure_ascii=False, default=str)
1222+
content.parts.append(types.Part.from_text(text=result_text))
1223+
12011224
function_response_event = Event(
12021225
invocation_id=invocation_context.invocation_id,
12031226
author=invocation_context.agent.name,

src/google/adk/labs/openai/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
# limitations under the License.
1414

1515
from ._openai_llm import OpenAILlm
16+
from ._openai_responses_llm import AzureOpenAIResponsesLlm
17+
from ._openai_responses_llm import OpenAIResponsesLlm
1618

1719
__all__ = [
20+
'AzureOpenAIResponsesLlm',
1821
'OpenAILlm',
22+
'OpenAIResponsesLlm',
1923
]

src/google/adk/labs/openai/_openai_llm.py

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from ...models.base_llm import BaseLlm
4747
from ...models.llm_request import LlmRequest
4848
from ...models.llm_response import LlmResponse
49+
from ._openai_schema import enforce_strict_openai_schema
4950

5051
logger = logging.getLogger("google_adk." + __name__)
5152

@@ -180,29 +181,6 @@ def _content_to_openai_messages(
180181
return messages
181182

182183

183-
def _enforce_strict_openai_schema(schema: dict[str, Any]) -> None:
184-
"""Recursively transforms a JSON schema for OpenAI strict structured outputs."""
185-
if not isinstance(schema, dict):
186-
return
187-
if "$ref" in schema:
188-
for key in list(schema.keys()):
189-
if key != "$ref":
190-
del schema[key]
191-
return
192-
if schema.get("type") == "object" and "properties" in schema:
193-
schema["additionalProperties"] = False
194-
schema["required"] = sorted(schema["properties"].keys())
195-
for defn in schema.get("$defs", {}).values():
196-
_enforce_strict_openai_schema(defn)
197-
for prop in schema.get("properties", {}).values():
198-
_enforce_strict_openai_schema(prop)
199-
for key in ("anyOf", "oneOf", "allOf"):
200-
for item in schema.get(key, []):
201-
_enforce_strict_openai_schema(item)
202-
if "items" in schema and isinstance(schema["items"], dict):
203-
_enforce_strict_openai_schema(schema["items"])
204-
205-
206184
def _update_type_string(value: Any):
207185
"""Lowercases nested JSON schema type strings for OpenAI compatibility."""
208186
if isinstance(value, list):
@@ -406,7 +384,7 @@ async def generate_content_async(
406384
schema_name = str(schema_dict["title"])
407385

408386
if schema_dict:
409-
_enforce_strict_openai_schema(schema_dict)
387+
enforce_strict_openai_schema(schema_dict)
410388
response_format = {
411389
"type": "json_schema",
412390
"json_schema": {

0 commit comments

Comments
 (0)