google-adk 1.6.1__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google/adk/a2a/converters/event_converter.py +5 -85
- google/adk/a2a/converters/request_converter.py +1 -2
- google/adk/a2a/executor/a2a_agent_executor.py +45 -16
- google/adk/a2a/logs/log_utils.py +1 -2
- google/adk/a2a/utils/__init__.py +0 -0
- google/adk/a2a/utils/agent_card_builder.py +544 -0
- google/adk/a2a/utils/agent_to_a2a.py +118 -0
- google/adk/agents/__init__.py +5 -0
- google/adk/agents/agent_config.py +46 -0
- google/adk/agents/base_agent.py +239 -41
- google/adk/agents/callback_context.py +41 -0
- google/adk/agents/common_configs.py +79 -0
- google/adk/agents/config_agent_utils.py +184 -0
- google/adk/agents/config_schemas/AgentConfig.json +566 -0
- google/adk/agents/invocation_context.py +5 -1
- google/adk/agents/live_request_queue.py +15 -0
- google/adk/agents/llm_agent.py +201 -9
- google/adk/agents/loop_agent.py +35 -1
- google/adk/agents/parallel_agent.py +24 -3
- google/adk/agents/remote_a2a_agent.py +17 -5
- google/adk/agents/sequential_agent.py +22 -1
- google/adk/artifacts/gcs_artifact_service.py +110 -20
- google/adk/auth/auth_handler.py +3 -3
- google/adk/auth/credential_manager.py +23 -23
- google/adk/auth/credential_service/base_credential_service.py +6 -6
- google/adk/auth/credential_service/in_memory_credential_service.py +10 -8
- google/adk/auth/credential_service/session_state_credential_service.py +8 -8
- google/adk/auth/exchanger/oauth2_credential_exchanger.py +3 -3
- google/adk/auth/oauth2_credential_util.py +2 -2
- google/adk/auth/refresher/oauth2_credential_refresher.py +4 -4
- google/adk/cli/agent_graph.py +3 -1
- google/adk/cli/browser/index.html +2 -2
- google/adk/cli/browser/main-W7QZBYAR.js +3914 -0
- google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
- google/adk/cli/cli_eval.py +87 -12
- google/adk/cli/cli_tools_click.py +143 -82
- google/adk/cli/fast_api.py +150 -69
- google/adk/cli/utils/agent_loader.py +35 -1
- google/adk/code_executors/base_code_executor.py +14 -19
- google/adk/code_executors/built_in_code_executor.py +4 -1
- google/adk/evaluation/base_eval_service.py +46 -2
- google/adk/evaluation/eval_metrics.py +4 -0
- google/adk/evaluation/eval_sets_manager.py +5 -1
- google/adk/evaluation/evaluation_generator.py +1 -1
- google/adk/evaluation/final_response_match_v2.py +2 -2
- google/adk/evaluation/gcs_eval_sets_manager.py +2 -1
- google/adk/evaluation/in_memory_eval_sets_manager.py +151 -0
- google/adk/evaluation/local_eval_service.py +389 -0
- google/adk/evaluation/local_eval_set_results_manager.py +2 -2
- google/adk/evaluation/local_eval_sets_manager.py +24 -9
- google/adk/evaluation/metric_evaluator_registry.py +16 -6
- google/adk/evaluation/vertex_ai_eval_facade.py +7 -1
- google/adk/events/event.py +7 -2
- google/adk/flows/llm_flows/auto_flow.py +6 -11
- google/adk/flows/llm_flows/base_llm_flow.py +66 -29
- google/adk/flows/llm_flows/contents.py +16 -10
- google/adk/flows/llm_flows/functions.py +89 -52
- google/adk/memory/in_memory_memory_service.py +21 -15
- google/adk/memory/vertex_ai_memory_bank_service.py +12 -10
- google/adk/models/anthropic_llm.py +46 -6
- google/adk/models/base_llm_connection.py +2 -0
- google/adk/models/gemini_llm_connection.py +17 -6
- google/adk/models/google_llm.py +46 -11
- google/adk/models/lite_llm.py +52 -22
- google/adk/plugins/__init__.py +17 -0
- google/adk/plugins/base_plugin.py +317 -0
- google/adk/plugins/plugin_manager.py +265 -0
- google/adk/runners.py +122 -18
- google/adk/sessions/database_session_service.py +51 -52
- google/adk/sessions/vertex_ai_session_service.py +27 -12
- google/adk/tools/__init__.py +2 -0
- google/adk/tools/_automatic_function_calling_util.py +20 -2
- google/adk/tools/agent_tool.py +15 -3
- google/adk/tools/apihub_tool/apihub_toolset.py +38 -39
- google/adk/tools/application_integration_tool/application_integration_toolset.py +35 -37
- google/adk/tools/application_integration_tool/integration_connector_tool.py +2 -3
- google/adk/tools/base_tool.py +9 -9
- google/adk/tools/base_toolset.py +29 -5
- google/adk/tools/bigquery/__init__.py +3 -3
- google/adk/tools/bigquery/metadata_tool.py +2 -0
- google/adk/tools/bigquery/query_tool.py +15 -1
- google/adk/tools/computer_use/__init__.py +13 -0
- google/adk/tools/computer_use/base_computer.py +265 -0
- google/adk/tools/computer_use/computer_use_tool.py +166 -0
- google/adk/tools/computer_use/computer_use_toolset.py +220 -0
- google/adk/tools/enterprise_search_tool.py +4 -2
- google/adk/tools/exit_loop_tool.py +1 -0
- google/adk/tools/google_api_tool/google_api_tool.py +16 -1
- google/adk/tools/google_api_tool/google_api_toolset.py +9 -7
- google/adk/tools/google_api_tool/google_api_toolsets.py +41 -20
- google/adk/tools/google_search_tool.py +4 -2
- google/adk/tools/langchain_tool.py +16 -6
- google/adk/tools/long_running_tool.py +21 -0
- google/adk/tools/mcp_tool/mcp_toolset.py +27 -28
- google/adk/tools/openapi_tool/openapi_spec_parser/openapi_spec_parser.py +5 -0
- google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +8 -8
- google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +4 -6
- google/adk/tools/retrieval/vertex_ai_rag_retrieval.py +3 -2
- google/adk/tools/tool_context.py +0 -10
- google/adk/tools/url_context_tool.py +4 -2
- google/adk/tools/vertex_ai_search_tool.py +4 -2
- google/adk/utils/model_name_utils.py +90 -0
- google/adk/version.py +1 -1
- {google_adk-1.6.1.dist-info → google_adk-1.8.0.dist-info}/METADATA +3 -2
- {google_adk-1.6.1.dist-info → google_adk-1.8.0.dist-info}/RECORD +108 -91
- google/adk/cli/browser/main-RXDVX3K6.js +0 -3914
- google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -17
- {google_adk-1.6.1.dist-info → google_adk-1.8.0.dist-info}/WHEEL +0 -0
- {google_adk-1.6.1.dist-info → google_adk-1.8.0.dist-info}/entry_points.txt +0 -0
- {google_adk-1.6.1.dist-info → google_adk-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -16,6 +16,7 @@
|
|
16
16
|
|
17
17
|
from __future__ import annotations
|
18
18
|
|
19
|
+
import base64
|
19
20
|
from functools import cached_property
|
20
21
|
import logging
|
21
22
|
import os
|
@@ -45,7 +46,7 @@ __all__ = ["Claude"]
|
|
45
46
|
|
46
47
|
logger = logging.getLogger("google_adk." + __name__)
|
47
48
|
|
48
|
-
MAX_TOKEN =
|
49
|
+
MAX_TOKEN = 8192
|
49
50
|
|
50
51
|
|
51
52
|
class ClaudeRequest(BaseModel):
|
@@ -70,6 +71,14 @@ def to_google_genai_finish_reason(
|
|
70
71
|
return "FINISH_REASON_UNSPECIFIED"
|
71
72
|
|
72
73
|
|
74
|
+
def _is_image_part(part: types.Part) -> bool:
|
75
|
+
return (
|
76
|
+
part.inline_data
|
77
|
+
and part.inline_data.mime_type
|
78
|
+
and part.inline_data.mime_type.startswith("image")
|
79
|
+
)
|
80
|
+
|
81
|
+
|
73
82
|
def part_to_message_block(
|
74
83
|
part: types.Part,
|
75
84
|
) -> Union[
|
@@ -80,7 +89,7 @@ def part_to_message_block(
|
|
80
89
|
]:
|
81
90
|
if part.text:
|
82
91
|
return anthropic_types.TextBlockParam(text=part.text, type="text")
|
83
|
-
|
92
|
+
elif part.function_call:
|
84
93
|
assert part.function_call.name
|
85
94
|
|
86
95
|
return anthropic_types.ToolUseBlockParam(
|
@@ -89,7 +98,7 @@ def part_to_message_block(
|
|
89
98
|
input=part.function_call.args,
|
90
99
|
type="tool_use",
|
91
100
|
)
|
92
|
-
|
101
|
+
elif part.function_response:
|
93
102
|
content = ""
|
94
103
|
if (
|
95
104
|
"result" in part.function_response.response
|
@@ -105,15 +114,45 @@ def part_to_message_block(
|
|
105
114
|
content=content,
|
106
115
|
is_error=False,
|
107
116
|
)
|
108
|
-
|
117
|
+
elif _is_image_part(part):
|
118
|
+
data = base64.b64encode(part.inline_data.data).decode()
|
119
|
+
return anthropic_types.ImageBlockParam(
|
120
|
+
type="image",
|
121
|
+
source=dict(
|
122
|
+
type="base64", media_type=part.inline_data.mime_type, data=data
|
123
|
+
),
|
124
|
+
)
|
125
|
+
elif part.executable_code:
|
126
|
+
return anthropic_types.TextBlockParam(
|
127
|
+
type="text",
|
128
|
+
text="Code:```python\n" + part.executable_code.code + "\n```",
|
129
|
+
)
|
130
|
+
elif part.code_execution_result:
|
131
|
+
return anthropic_types.TextBlockParam(
|
132
|
+
text="Execution Result:```code_output\n"
|
133
|
+
+ part.code_execution_result.output
|
134
|
+
+ "\n```",
|
135
|
+
type="text",
|
136
|
+
)
|
137
|
+
|
138
|
+
raise NotImplementedError(f"Not supported yet: {part}")
|
109
139
|
|
110
140
|
|
111
141
|
def content_to_message_param(
|
112
142
|
content: types.Content,
|
113
143
|
) -> anthropic_types.MessageParam:
|
144
|
+
message_block = []
|
145
|
+
for part in content.parts or []:
|
146
|
+
# Image data is not supported in Claude for model turns.
|
147
|
+
if _is_image_part(part):
|
148
|
+
logger.warning("Image data is not supported in Claude for model turns.")
|
149
|
+
continue
|
150
|
+
|
151
|
+
message_block.append(part_to_message_block(part))
|
152
|
+
|
114
153
|
return {
|
115
154
|
"role": to_claude_role(content.role),
|
116
|
-
"content":
|
155
|
+
"content": message_block,
|
117
156
|
}
|
118
157
|
|
119
158
|
|
@@ -135,7 +174,8 @@ def content_block_to_part(
|
|
135
174
|
def message_to_generate_content_response(
|
136
175
|
message: anthropic_types.Message,
|
137
176
|
) -> LlmResponse:
|
138
|
-
logger.info(
|
177
|
+
logger.info("Received response from Claude.")
|
178
|
+
logger.debug(
|
139
179
|
"Claude response: %s",
|
140
180
|
message.model_dump_json(indent=2, exclude_none=True),
|
141
181
|
)
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
16
16
|
|
17
17
|
import logging
|
18
18
|
from typing import AsyncGenerator
|
19
|
+
from typing import Union
|
19
20
|
|
20
21
|
from google.genai import live
|
21
22
|
from google.genai import types
|
@@ -25,6 +26,8 @@ from .llm_response import LlmResponse
|
|
25
26
|
|
26
27
|
logger = logging.getLogger('google_adk.' + __name__)
|
27
28
|
|
29
|
+
RealtimeInput = Union[types.Blob, types.ActivityStart, types.ActivityEnd]
|
30
|
+
|
28
31
|
|
29
32
|
class GeminiLlmConnection(BaseLlmConnection):
|
30
33
|
"""The Gemini model connection."""
|
@@ -93,16 +96,24 @@ class GeminiLlmConnection(BaseLlmConnection):
|
|
93
96
|
)
|
94
97
|
)
|
95
98
|
|
96
|
-
async def send_realtime(self,
|
99
|
+
async def send_realtime(self, input: RealtimeInput):
|
97
100
|
"""Sends a chunk of audio or a frame of video to the model in realtime.
|
98
101
|
|
99
102
|
Args:
|
100
|
-
|
103
|
+
input: The input to send to the model.
|
101
104
|
"""
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
105
|
+
if isinstance(input, types.Blob):
|
106
|
+
input_blob = input.model_dump()
|
107
|
+
logger.debug('Sending LLM Blob: %s', input_blob)
|
108
|
+
await self._gemini_session.send(input=input_blob)
|
109
|
+
elif isinstance(input, types.ActivityStart):
|
110
|
+
logger.debug('Sending LLM activity start signal')
|
111
|
+
await self._gemini_session.send_realtime_input(activity_start=input)
|
112
|
+
elif isinstance(input, types.ActivityEnd):
|
113
|
+
logger.debug('Sending LLM activity end signal')
|
114
|
+
await self._gemini_session.send_realtime_input(activity_end=input)
|
115
|
+
else:
|
116
|
+
raise ValueError('Unsupported input type: %s' % type(input))
|
106
117
|
|
107
118
|
def __build_full_text_response(self, text: str):
|
108
119
|
"""Builds a full text response.
|
google/adk/models/google_llm.py
CHANGED
@@ -27,6 +27,7 @@ from typing import Union
|
|
27
27
|
|
28
28
|
from google.genai import Client
|
29
29
|
from google.genai import types
|
30
|
+
from google.genai.types import FinishReason
|
30
31
|
from typing_extensions import override
|
31
32
|
|
32
33
|
from .. import version
|
@@ -67,6 +68,8 @@ class Gemini(BaseLlm):
|
|
67
68
|
|
68
69
|
return [
|
69
70
|
r'gemini-.*',
|
71
|
+
# model optimizer pattern
|
72
|
+
r'model-optimizer-.*',
|
70
73
|
# fine-tuned vertex endpoint pattern
|
71
74
|
r'projects\/.+\/locations\/.+\/endpoints\/.+',
|
72
75
|
# vertex gemini long name
|
@@ -85,7 +88,7 @@ class Gemini(BaseLlm):
|
|
85
88
|
Yields:
|
86
89
|
LlmResponse: The model response.
|
87
90
|
"""
|
88
|
-
self._preprocess_request(llm_request)
|
91
|
+
await self._preprocess_request(llm_request)
|
89
92
|
self._maybe_append_user_content(llm_request)
|
90
93
|
logger.info(
|
91
94
|
'Sending out request, model: %s, backend: %s, stream: %s',
|
@@ -93,7 +96,7 @@ class Gemini(BaseLlm):
|
|
93
96
|
self._api_backend,
|
94
97
|
stream,
|
95
98
|
)
|
96
|
-
logger.
|
99
|
+
logger.debug(_build_request_log(llm_request))
|
97
100
|
|
98
101
|
# add tracking headers to custom headers given it will override the headers
|
99
102
|
# set in the api client constructor
|
@@ -118,7 +121,7 @@ class Gemini(BaseLlm):
|
|
118
121
|
# previous partial content. The only difference is bidi rely on
|
119
122
|
# complete_turn flag to detect end while sse depends on finish_reason.
|
120
123
|
async for response in responses:
|
121
|
-
logger.
|
124
|
+
logger.debug(_build_response_log(response))
|
122
125
|
llm_response = LlmResponse.create(response)
|
123
126
|
usage_metadata = llm_response.usage_metadata
|
124
127
|
if (
|
@@ -150,12 +153,10 @@ class Gemini(BaseLlm):
|
|
150
153
|
thought_text = ''
|
151
154
|
text = ''
|
152
155
|
yield llm_response
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
and response.candidates[0].finish_reason == types.FinishReason.STOP
|
158
|
-
):
|
156
|
+
|
157
|
+
# generate an aggregated content at the end regardless the
|
158
|
+
# response.candidates[0].finish_reason
|
159
|
+
if (text or thought_text) and response and response.candidates:
|
159
160
|
parts = []
|
160
161
|
if thought_text:
|
161
162
|
parts.append(types.Part(text=thought_text, thought=True))
|
@@ -163,6 +164,12 @@ class Gemini(BaseLlm):
|
|
163
164
|
parts.append(types.Part.from_text(text=text))
|
164
165
|
yield LlmResponse(
|
165
166
|
content=types.ModelContent(parts=parts),
|
167
|
+
error_code=None
|
168
|
+
if response.candidates[0].finish_reason == FinishReason.STOP
|
169
|
+
else response.candidates[0].finish_reason,
|
170
|
+
error_message=None
|
171
|
+
if response.candidates[0].finish_reason == FinishReason.STOP
|
172
|
+
else response.candidates[0].finish_message,
|
166
173
|
usage_metadata=usage_metadata,
|
167
174
|
)
|
168
175
|
|
@@ -172,7 +179,8 @@ class Gemini(BaseLlm):
|
|
172
179
|
contents=llm_request.contents,
|
173
180
|
config=llm_request.config,
|
174
181
|
)
|
175
|
-
logger.info(
|
182
|
+
logger.info('Response received from the model.')
|
183
|
+
logger.debug(_build_response_log(response))
|
176
184
|
yield LlmResponse.create(response)
|
177
185
|
|
178
186
|
@cached_property
|
@@ -262,7 +270,22 @@ class Gemini(BaseLlm):
|
|
262
270
|
) as live_session:
|
263
271
|
yield GeminiLlmConnection(live_session)
|
264
272
|
|
265
|
-
def
|
273
|
+
async def _adapt_computer_use_tool(self, llm_request: LlmRequest) -> None:
|
274
|
+
"""Adapt the google computer use predefined functions to the adk computer use toolset."""
|
275
|
+
|
276
|
+
from ..tools.computer_use.computer_use_toolset import ComputerUseToolset
|
277
|
+
|
278
|
+
async def convert_wait_to_wait_5_seconds(wait_func):
|
279
|
+
async def wait_5_seconds():
|
280
|
+
return await wait_func(5)
|
281
|
+
|
282
|
+
return wait_5_seconds
|
283
|
+
|
284
|
+
await ComputerUseToolset.adapt_computer_use_tool(
|
285
|
+
'wait', convert_wait_to_wait_5_seconds, llm_request
|
286
|
+
)
|
287
|
+
|
288
|
+
async def _preprocess_request(self, llm_request: LlmRequest) -> None:
|
266
289
|
|
267
290
|
if self._api_backend == GoogleLLMVariant.GEMINI_API:
|
268
291
|
# Using API key from Google AI Studio to call model doesn't support labels.
|
@@ -277,6 +300,18 @@ class Gemini(BaseLlm):
|
|
277
300
|
_remove_display_name_if_present(part.inline_data)
|
278
301
|
_remove_display_name_if_present(part.file_data)
|
279
302
|
|
303
|
+
# Initialize config if needed
|
304
|
+
if llm_request.config and llm_request.config.tools:
|
305
|
+
# Check if computer use is configured
|
306
|
+
for tool in llm_request.config.tools:
|
307
|
+
if (
|
308
|
+
isinstance(tool, (types.Tool, types.ToolDict))
|
309
|
+
and hasattr(tool, 'computer_use')
|
310
|
+
and tool.computer_use
|
311
|
+
):
|
312
|
+
llm_request.config.system_instruction = None
|
313
|
+
await self._adapt_computer_use_tool(llm_request)
|
314
|
+
|
280
315
|
|
281
316
|
def _build_function_declaration_log(
|
282
317
|
func_decl: types.FunctionDeclaration,
|
google/adk/models/lite_llm.py
CHANGED
@@ -35,11 +35,14 @@ from litellm import acompletion
|
|
35
35
|
from litellm import ChatCompletionAssistantMessage
|
36
36
|
from litellm import ChatCompletionAssistantToolCall
|
37
37
|
from litellm import ChatCompletionDeveloperMessage
|
38
|
+
from litellm import ChatCompletionFileObject
|
39
|
+
from litellm import ChatCompletionImageObject
|
38
40
|
from litellm import ChatCompletionImageUrlObject
|
39
41
|
from litellm import ChatCompletionMessageToolCall
|
40
42
|
from litellm import ChatCompletionTextObject
|
41
43
|
from litellm import ChatCompletionToolMessage
|
42
44
|
from litellm import ChatCompletionUserMessage
|
45
|
+
from litellm import ChatCompletionVideoObject
|
43
46
|
from litellm import ChatCompletionVideoUrlObject
|
44
47
|
from litellm import completion
|
45
48
|
from litellm import CustomStreamWrapper
|
@@ -249,17 +252,31 @@ def _get_content(
|
|
249
252
|
data_uri = f"data:{part.inline_data.mime_type};base64,{base64_string}"
|
250
253
|
|
251
254
|
if part.inline_data.mime_type.startswith("image"):
|
255
|
+
# Extract format from mime type (e.g., "image/png" -> "png")
|
256
|
+
format_type = part.inline_data.mime_type.split("/")[-1]
|
252
257
|
content_objects.append(
|
253
|
-
|
258
|
+
ChatCompletionImageObject(
|
254
259
|
type="image_url",
|
255
|
-
image_url=
|
260
|
+
image_url=ChatCompletionImageUrlObject(
|
261
|
+
url=data_uri, format=format_type
|
262
|
+
),
|
256
263
|
)
|
257
264
|
)
|
258
265
|
elif part.inline_data.mime_type.startswith("video"):
|
266
|
+
# Extract format from mime type (e.g., "video/mp4" -> "mp4")
|
267
|
+
format_type = part.inline_data.mime_type.split("/")[-1]
|
259
268
|
content_objects.append(
|
260
|
-
|
269
|
+
ChatCompletionVideoObject(
|
261
270
|
type="video_url",
|
262
|
-
video_url=
|
271
|
+
video_url=ChatCompletionVideoUrlObject(
|
272
|
+
url=data_uri, format=format_type
|
273
|
+
),
|
274
|
+
)
|
275
|
+
)
|
276
|
+
elif part.inline_data.mime_type == "application/pdf":
|
277
|
+
content_objects.append(
|
278
|
+
ChatCompletionFileObject(
|
279
|
+
type="file", file={"file_data": data_uri, "format": "pdf"}
|
263
280
|
)
|
264
281
|
)
|
265
282
|
else:
|
@@ -294,7 +311,9 @@ TYPE_LABELS = {
|
|
294
311
|
|
295
312
|
|
296
313
|
def _schema_to_dict(schema: types.Schema) -> dict:
|
297
|
-
"""
|
314
|
+
"""
|
315
|
+
Recursively converts a types.Schema to a pure-python dict
|
316
|
+
with all enum values written as lower-case strings.
|
298
317
|
|
299
318
|
Args:
|
300
319
|
schema: The schema to convert.
|
@@ -302,29 +321,40 @@ def _schema_to_dict(schema: types.Schema) -> dict:
|
|
302
321
|
Returns:
|
303
322
|
The dictionary representation of the schema.
|
304
323
|
"""
|
305
|
-
|
324
|
+
# Dump without json encoding so we still get Enum members
|
306
325
|
schema_dict = schema.model_dump(exclude_none=True)
|
326
|
+
|
327
|
+
# ---- normalise this level ------------------------------------------------
|
307
328
|
if "type" in schema_dict:
|
308
|
-
schema_dict["type"]
|
329
|
+
# schema_dict["type"] can be an Enum or a str
|
330
|
+
t = schema_dict["type"]
|
331
|
+
schema_dict["type"] = (t.value if isinstance(t, types.Type) else t).lower()
|
332
|
+
|
333
|
+
# ---- recurse into `items` -----------------------------------------------
|
309
334
|
if "items" in schema_dict:
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
]
|
335
|
+
schema_dict["items"] = _schema_to_dict(
|
336
|
+
schema.items
|
337
|
+
if isinstance(schema.items, types.Schema)
|
338
|
+
else types.Schema.model_validate(schema_dict["items"])
|
339
|
+
)
|
340
|
+
|
341
|
+
# ---- recurse into `properties` ------------------------------------------
|
318
342
|
if "properties" in schema_dict:
|
319
|
-
|
343
|
+
new_props = {}
|
320
344
|
for key, value in schema_dict["properties"].items():
|
321
|
-
|
322
|
-
|
345
|
+
# value is a dict → rebuild a Schema object and recurse
|
346
|
+
if isinstance(value, dict):
|
347
|
+
new_props[key] = _schema_to_dict(types.Schema.model_validate(value))
|
348
|
+
# value is already a Schema instance
|
349
|
+
elif isinstance(value, types.Schema):
|
350
|
+
new_props[key] = _schema_to_dict(value)
|
351
|
+
# plain dict without nested schemas
|
323
352
|
else:
|
324
|
-
|
325
|
-
if "type" in
|
326
|
-
|
327
|
-
schema_dict["properties"] =
|
353
|
+
new_props[key] = value
|
354
|
+
if "type" in new_props[key]:
|
355
|
+
new_props[key]["type"] = new_props[key]["type"].lower()
|
356
|
+
schema_dict["properties"] = new_props
|
357
|
+
|
328
358
|
return schema_dict
|
329
359
|
|
330
360
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Copyright 2025 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 in 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 .base_plugin import BasePlugin
|
16
|
+
|
17
|
+
__all__ = ['BasePlugin']
|