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.
Files changed (110) hide show
  1. google/adk/a2a/converters/event_converter.py +5 -85
  2. google/adk/a2a/converters/request_converter.py +1 -2
  3. google/adk/a2a/executor/a2a_agent_executor.py +45 -16
  4. google/adk/a2a/logs/log_utils.py +1 -2
  5. google/adk/a2a/utils/__init__.py +0 -0
  6. google/adk/a2a/utils/agent_card_builder.py +544 -0
  7. google/adk/a2a/utils/agent_to_a2a.py +118 -0
  8. google/adk/agents/__init__.py +5 -0
  9. google/adk/agents/agent_config.py +46 -0
  10. google/adk/agents/base_agent.py +239 -41
  11. google/adk/agents/callback_context.py +41 -0
  12. google/adk/agents/common_configs.py +79 -0
  13. google/adk/agents/config_agent_utils.py +184 -0
  14. google/adk/agents/config_schemas/AgentConfig.json +566 -0
  15. google/adk/agents/invocation_context.py +5 -1
  16. google/adk/agents/live_request_queue.py +15 -0
  17. google/adk/agents/llm_agent.py +201 -9
  18. google/adk/agents/loop_agent.py +35 -1
  19. google/adk/agents/parallel_agent.py +24 -3
  20. google/adk/agents/remote_a2a_agent.py +17 -5
  21. google/adk/agents/sequential_agent.py +22 -1
  22. google/adk/artifacts/gcs_artifact_service.py +110 -20
  23. google/adk/auth/auth_handler.py +3 -3
  24. google/adk/auth/credential_manager.py +23 -23
  25. google/adk/auth/credential_service/base_credential_service.py +6 -6
  26. google/adk/auth/credential_service/in_memory_credential_service.py +10 -8
  27. google/adk/auth/credential_service/session_state_credential_service.py +8 -8
  28. google/adk/auth/exchanger/oauth2_credential_exchanger.py +3 -3
  29. google/adk/auth/oauth2_credential_util.py +2 -2
  30. google/adk/auth/refresher/oauth2_credential_refresher.py +4 -4
  31. google/adk/cli/agent_graph.py +3 -1
  32. google/adk/cli/browser/index.html +2 -2
  33. google/adk/cli/browser/main-W7QZBYAR.js +3914 -0
  34. google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
  35. google/adk/cli/cli_eval.py +87 -12
  36. google/adk/cli/cli_tools_click.py +143 -82
  37. google/adk/cli/fast_api.py +150 -69
  38. google/adk/cli/utils/agent_loader.py +35 -1
  39. google/adk/code_executors/base_code_executor.py +14 -19
  40. google/adk/code_executors/built_in_code_executor.py +4 -1
  41. google/adk/evaluation/base_eval_service.py +46 -2
  42. google/adk/evaluation/eval_metrics.py +4 -0
  43. google/adk/evaluation/eval_sets_manager.py +5 -1
  44. google/adk/evaluation/evaluation_generator.py +1 -1
  45. google/adk/evaluation/final_response_match_v2.py +2 -2
  46. google/adk/evaluation/gcs_eval_sets_manager.py +2 -1
  47. google/adk/evaluation/in_memory_eval_sets_manager.py +151 -0
  48. google/adk/evaluation/local_eval_service.py +389 -0
  49. google/adk/evaluation/local_eval_set_results_manager.py +2 -2
  50. google/adk/evaluation/local_eval_sets_manager.py +24 -9
  51. google/adk/evaluation/metric_evaluator_registry.py +16 -6
  52. google/adk/evaluation/vertex_ai_eval_facade.py +7 -1
  53. google/adk/events/event.py +7 -2
  54. google/adk/flows/llm_flows/auto_flow.py +6 -11
  55. google/adk/flows/llm_flows/base_llm_flow.py +66 -29
  56. google/adk/flows/llm_flows/contents.py +16 -10
  57. google/adk/flows/llm_flows/functions.py +89 -52
  58. google/adk/memory/in_memory_memory_service.py +21 -15
  59. google/adk/memory/vertex_ai_memory_bank_service.py +12 -10
  60. google/adk/models/anthropic_llm.py +46 -6
  61. google/adk/models/base_llm_connection.py +2 -0
  62. google/adk/models/gemini_llm_connection.py +17 -6
  63. google/adk/models/google_llm.py +46 -11
  64. google/adk/models/lite_llm.py +52 -22
  65. google/adk/plugins/__init__.py +17 -0
  66. google/adk/plugins/base_plugin.py +317 -0
  67. google/adk/plugins/plugin_manager.py +265 -0
  68. google/adk/runners.py +122 -18
  69. google/adk/sessions/database_session_service.py +51 -52
  70. google/adk/sessions/vertex_ai_session_service.py +27 -12
  71. google/adk/tools/__init__.py +2 -0
  72. google/adk/tools/_automatic_function_calling_util.py +20 -2
  73. google/adk/tools/agent_tool.py +15 -3
  74. google/adk/tools/apihub_tool/apihub_toolset.py +38 -39
  75. google/adk/tools/application_integration_tool/application_integration_toolset.py +35 -37
  76. google/adk/tools/application_integration_tool/integration_connector_tool.py +2 -3
  77. google/adk/tools/base_tool.py +9 -9
  78. google/adk/tools/base_toolset.py +29 -5
  79. google/adk/tools/bigquery/__init__.py +3 -3
  80. google/adk/tools/bigquery/metadata_tool.py +2 -0
  81. google/adk/tools/bigquery/query_tool.py +15 -1
  82. google/adk/tools/computer_use/__init__.py +13 -0
  83. google/adk/tools/computer_use/base_computer.py +265 -0
  84. google/adk/tools/computer_use/computer_use_tool.py +166 -0
  85. google/adk/tools/computer_use/computer_use_toolset.py +220 -0
  86. google/adk/tools/enterprise_search_tool.py +4 -2
  87. google/adk/tools/exit_loop_tool.py +1 -0
  88. google/adk/tools/google_api_tool/google_api_tool.py +16 -1
  89. google/adk/tools/google_api_tool/google_api_toolset.py +9 -7
  90. google/adk/tools/google_api_tool/google_api_toolsets.py +41 -20
  91. google/adk/tools/google_search_tool.py +4 -2
  92. google/adk/tools/langchain_tool.py +16 -6
  93. google/adk/tools/long_running_tool.py +21 -0
  94. google/adk/tools/mcp_tool/mcp_toolset.py +27 -28
  95. google/adk/tools/openapi_tool/openapi_spec_parser/openapi_spec_parser.py +5 -0
  96. google/adk/tools/openapi_tool/openapi_spec_parser/openapi_toolset.py +8 -8
  97. google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +4 -6
  98. google/adk/tools/retrieval/vertex_ai_rag_retrieval.py +3 -2
  99. google/adk/tools/tool_context.py +0 -10
  100. google/adk/tools/url_context_tool.py +4 -2
  101. google/adk/tools/vertex_ai_search_tool.py +4 -2
  102. google/adk/utils/model_name_utils.py +90 -0
  103. google/adk/version.py +1 -1
  104. {google_adk-1.6.1.dist-info → google_adk-1.8.0.dist-info}/METADATA +3 -2
  105. {google_adk-1.6.1.dist-info → google_adk-1.8.0.dist-info}/RECORD +108 -91
  106. google/adk/cli/browser/main-RXDVX3K6.js +0 -3914
  107. google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -17
  108. {google_adk-1.6.1.dist-info → google_adk-1.8.0.dist-info}/WHEEL +0 -0
  109. {google_adk-1.6.1.dist-info → google_adk-1.8.0.dist-info}/entry_points.txt +0 -0
  110. {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 = 1024
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
- if part.function_call:
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
- if part.function_response:
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
- raise NotImplementedError("Not supported yet.")
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": [part_to_message_block(part) for part in content.parts or []],
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
  )
@@ -12,6 +12,8 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ from __future__ import annotations
16
+
15
17
  from abc import abstractmethod
16
18
  from typing import AsyncGenerator
17
19
 
@@ -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, blob: types.Blob):
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
- blob: The blob to send to the model.
103
+ input: The input to send to the model.
101
104
  """
102
-
103
- input_blob = blob.model_dump()
104
- logger.debug('Sending LLM Blob: %s', input_blob)
105
- await self._gemini_session.send(input=input_blob)
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.
@@ -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.info(_build_request_log(llm_request))
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.info(_build_response_log(response))
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
- if (
154
- (text or thought_text)
155
- and response
156
- and response.candidates
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(_build_response_log(response))
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 _preprocess_request(self, llm_request: LlmRequest) -> None:
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,
@@ -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
- ChatCompletionImageUrlObject(
258
+ ChatCompletionImageObject(
254
259
  type="image_url",
255
- image_url=data_uri,
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
- ChatCompletionVideoUrlObject(
269
+ ChatCompletionVideoObject(
261
270
  type="video_url",
262
- video_url=data_uri,
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
- """Recursively converts a types.Schema to a dictionary.
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"] = schema_dict["type"].lower()
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
- if isinstance(schema_dict["items"], dict):
311
- schema_dict["items"] = _schema_to_dict(
312
- types.Schema.model_validate(schema_dict["items"])
313
- )
314
- elif isinstance(schema_dict["items"]["type"], types.Type):
315
- schema_dict["items"]["type"] = TYPE_LABELS[
316
- schema_dict["items"]["type"].value
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
- properties = {}
343
+ new_props = {}
320
344
  for key, value in schema_dict["properties"].items():
321
- if isinstance(value, types.Schema):
322
- properties[key] = _schema_to_dict(value)
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
- properties[key] = value
325
- if "type" in properties[key]:
326
- properties[key]["type"] = properties[key]["type"].lower()
327
- schema_dict["properties"] = 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']