google-cloud-agentplatform 1.165.1.dev0__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 (62) hide show
  1. agentplatform/__init__.py +72 -0
  2. agentplatform/_genai/__init__.py +43 -0
  3. agentplatform/_genai/_agent_engines_utils.py +2341 -0
  4. agentplatform/_genai/_bigquery_utils.py +49 -0
  5. agentplatform/_genai/_datasets_utils.py +344 -0
  6. agentplatform/_genai/_evals_builtin_tools.py +209 -0
  7. agentplatform/_genai/_evals_common.py +4268 -0
  8. agentplatform/_genai/_evals_constant.py +122 -0
  9. agentplatform/_genai/_evals_data_converters.py +926 -0
  10. agentplatform/_genai/_evals_metric_handlers.py +1783 -0
  11. agentplatform/_genai/_evals_metric_loaders.py +401 -0
  12. agentplatform/_genai/_evals_utils.py +1043 -0
  13. agentplatform/_genai/_evals_visualization.py +2070 -0
  14. agentplatform/_genai/_gcs_utils.py +262 -0
  15. agentplatform/_genai/_logging_utils.py +47 -0
  16. agentplatform/_genai/_memory_bank_utils.py +206 -0
  17. agentplatform/_genai/_observability_data_converter.py +186 -0
  18. agentplatform/_genai/_operations_utils.py +94 -0
  19. agentplatform/_genai/_prompt_management_utils.py +147 -0
  20. agentplatform/_genai/_prompt_optimizer_utils.py +215 -0
  21. agentplatform/_genai/_skills_utils.py +69 -0
  22. agentplatform/_genai/_transformers.py +628 -0
  23. agentplatform/_genai/a2a_task_events.py +509 -0
  24. agentplatform/_genai/a2a_tasks.py +861 -0
  25. agentplatform/_genai/agent_engines.py +3931 -0
  26. agentplatform/_genai/client.py +519 -0
  27. agentplatform/_genai/datasets.py +3045 -0
  28. agentplatform/_genai/endpoints.py +1149 -0
  29. agentplatform/_genai/evals.py +6883 -0
  30. agentplatform/_genai/example_stores.py +1445 -0
  31. agentplatform/_genai/feedback_contexts.py +700 -0
  32. agentplatform/_genai/feedback_entries.py +1644 -0
  33. agentplatform/_genai/live.py +64 -0
  34. agentplatform/_genai/live_agent_engines.py +179 -0
  35. agentplatform/_genai/memories.py +2962 -0
  36. agentplatform/_genai/memory_banks.py +1927 -0
  37. agentplatform/_genai/memory_revisions.py +465 -0
  38. agentplatform/_genai/model_garden.py +2638 -0
  39. agentplatform/_genai/prompt_optimizer.py +995 -0
  40. agentplatform/_genai/prompts.py +4515 -0
  41. agentplatform/_genai/rag.py +4961 -0
  42. agentplatform/_genai/runtime_revisions.py +1257 -0
  43. agentplatform/_genai/runtimes.py +78 -0
  44. agentplatform/_genai/sandbox_snapshots.py +1015 -0
  45. agentplatform/_genai/sandbox_templates.py +1088 -0
  46. agentplatform/_genai/sandboxes.py +1604 -0
  47. agentplatform/_genai/session_events.py +543 -0
  48. agentplatform/_genai/sessions.py +1449 -0
  49. agentplatform/_genai/skill_revisions.py +377 -0
  50. agentplatform/_genai/skills.py +1708 -0
  51. agentplatform/_genai/types/__init__.py +4695 -0
  52. agentplatform/_genai/types/agent_engines.py +16 -0
  53. agentplatform/_genai/types/common.py +32784 -0
  54. agentplatform/_genai/types/evals.py +1031 -0
  55. agentplatform/_genai/types/prompt_optimizer.py +107 -0
  56. agentplatform/_genai/types/prompts.py +107 -0
  57. agentplatform/version.py +17 -0
  58. google_cloud_agentplatform-1.165.1.dev0.dist-info/METADATA +79 -0
  59. google_cloud_agentplatform-1.165.1.dev0.dist-info/RECORD +62 -0
  60. google_cloud_agentplatform-1.165.1.dev0.dist-info/WHEEL +5 -0
  61. google_cloud_agentplatform-1.165.1.dev0.dist-info/licenses/LICENSE +202 -0
  62. google_cloud_agentplatform-1.165.1.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2341 @@
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 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
+ """Utility functions for agent engines."""
16
+
17
+ import abc
18
+ import asyncio
19
+ import base64
20
+ import dataclasses
21
+ from importlib import metadata as importlib_metadata
22
+ import inspect
23
+ import io
24
+ import json
25
+ import logging
26
+ import os
27
+ import re
28
+ import sys
29
+ import tarfile
30
+ import time
31
+ import types
32
+ import typing
33
+ from typing import (
34
+ Any,
35
+ AsyncIterator,
36
+ Callable,
37
+ Coroutine,
38
+ Dict,
39
+ Iterator,
40
+ List,
41
+ Mapping,
42
+ Optional,
43
+ Protocol,
44
+ Sequence,
45
+ Set,
46
+ TypedDict,
47
+ Union,
48
+ )
49
+
50
+ import httpx
51
+
52
+ import proto
53
+
54
+ from google.api_core import exceptions
55
+ from google.genai import types as google_genai_types
56
+ from google.api import httpbody_pb2
57
+ from google.protobuf import struct_pb2
58
+ from google.protobuf import json_format
59
+
60
+ from . import types as genai_types
61
+
62
+
63
+ if sys.version_info >= (3, 10):
64
+ from typing import TypeAlias
65
+ else:
66
+ from typing_extensions import TypeAlias
67
+
68
+
69
+ try:
70
+ _BUILTIN_MODULE_NAMES: Sequence[str] = sys.builtin_module_names
71
+ except AttributeError:
72
+ _BUILTIN_MODULE_NAMES: Sequence[str] = [] # type: ignore[no-redef]
73
+
74
+ try:
75
+ _PACKAGE_DISTRIBUTIONS: Mapping[str, Sequence[str]] = (
76
+ importlib_metadata.packages_distributions()
77
+ )
78
+ except AttributeError:
79
+ _PACKAGE_DISTRIBUTIONS: Mapping[str, Sequence[str]] = {} # type: ignore[no-redef]
80
+
81
+ try:
82
+ # sys.stdlib_module_names is available from Python 3.10 onwards.
83
+ _STDLIB_MODULE_NAMES: frozenset[str] = sys.stdlib_module_names
84
+ except AttributeError:
85
+ _STDLIB_MODULE_NAMES: frozenset[str] = frozenset() # type: ignore[no-redef]
86
+
87
+
88
+ if typing.TYPE_CHECKING:
89
+ from google.cloud import storage # type: ignore[attr-defined]
90
+
91
+ _StorageBucket: TypeAlias = storage.Bucket
92
+ else:
93
+ try:
94
+ from google.cloud import storage # type: ignore[attr-defined]
95
+
96
+ _StorageBucket: type[Any] = storage.Bucket
97
+ except (ImportError, AttributeError):
98
+ _StorageBucket: type[Any] = Any # type: ignore[no-redef]
99
+
100
+
101
+ if typing.TYPE_CHECKING:
102
+ import packaging
103
+
104
+ _SpecifierSet = packaging.specifiers.SpecifierSet
105
+ else:
106
+ try:
107
+ import packaging
108
+
109
+ _SpecifierSet: type[Any] = packaging.specifiers.SpecifierSet
110
+ except (ImportError, AttributeError):
111
+ _SpecifierSet: type[Any] = Any # type: ignore[no-redef]
112
+
113
+
114
+ try:
115
+ from a2a.types import AgentCard
116
+ from a2a.client import ClientConfig, ClientFactory
117
+ from a2a.utils.constants import TransportProtocol
118
+ except (ImportError, AttributeError):
119
+ AgentCard = None
120
+ TransportProtocol = None
121
+ ClientConfig = None
122
+ ClientFactory = None
123
+ SendMessageRequest = None
124
+ GetTaskRequest = None
125
+ CancelTaskRequest = None
126
+ GetExtendedAgentCardRequest = None
127
+ try:
128
+ from autogen.agentchat import chat
129
+
130
+ AutogenChatResult = chat.ChatResult
131
+ except ImportError:
132
+ AutogenChatResult = Any
133
+ try:
134
+ from autogen.io import run_response
135
+
136
+ AutogenRunResponse = run_response.RunResponse
137
+ except ImportError:
138
+ AutogenRunResponse = Any
139
+ try:
140
+ from llama_index.core.base.response import schema as llama_index_schema
141
+ from llama_index.core.base.llms import types as llama_index_types
142
+
143
+ LlamaIndexResponse = llama_index_schema.Response
144
+ LlamaIndexBaseModel = llama_index_schema.BaseModel
145
+ LlamaIndexChatResponse = llama_index_types.ChatResponse
146
+ except ImportError:
147
+ LlamaIndexResponse = Any
148
+ LlamaIndexBaseModel = Any
149
+ LlamaIndexChatResponse = Any
150
+ try:
151
+ import pydantic
152
+
153
+ BaseModel = pydantic.BaseModel
154
+ except ImportError:
155
+ BaseModel = Any
156
+
157
+ JsonDict = Dict[str, Any]
158
+
159
+ _ACTIONS_KEY = "actions"
160
+ _ACTION_APPEND = "append"
161
+ _AGENT_FRAMEWORK_ATTR = "agent_framework"
162
+ _ASYNC_API_MODE = "async"
163
+ _ASYNC_STREAM_API_MODE = "async_stream"
164
+ _BIDI_STREAM_API_MODE = "bidi_stream"
165
+ _BASE_MODULES = set(_BUILTIN_MODULE_NAMES).union(_STDLIB_MODULE_NAMES)
166
+ _BLOB_FILENAME = "agent_engine.pkl"
167
+ _DEFAULT_AGENT_FRAMEWORK = "custom"
168
+ _SUPPORTED_AGENT_FRAMEWORKS = frozenset(
169
+ [
170
+ "google-adk",
171
+ "langchain",
172
+ "langgraph",
173
+ "ag2",
174
+ "llama-index",
175
+ "custom",
176
+ "a2a",
177
+ ]
178
+ )
179
+ _DEFAULT_ASYNC_METHOD_NAME = "async_query"
180
+ _DEFAULT_ASYNC_METHOD_RETURN_TYPE = "Coroutine[Any]"
181
+ _DEFAULT_ASYNC_STREAM_METHOD_NAME = "async_stream_query"
182
+ _DEFAULT_ASYNC_STREAM_METHOD_RETURN_TYPE = "AsyncIterable[Any]"
183
+ _DEFAULT_GCS_DIR_NAME = "agent_engine"
184
+ _DEFAULT_METHOD_DOCSTRING_TEMPLATE = """
185
+ Runs the Agent Engine to serve the user request.
186
+ This will be based on the `.{method_name}(...)` of the python object that
187
+ was passed in when creating the Agent Engine. The method will invoke the
188
+ `{default_method_name}` API client of the python object.
189
+ Args:
190
+ **kwargs:
191
+ Optional. The arguments of the `.{method_name}(...)` method.
192
+ Returns:
193
+ {return_type}: The response from serving the user request.
194
+ """
195
+ _DEFAULT_METHOD_NAME = "query"
196
+ _DEFAULT_METHOD_RETURN_TYPE = "dict[str, Any]"
197
+ _DEFAULT_STREAM_METHOD_RETURN_TYPE = "Iterable[Any]"
198
+ _DEFAULT_REQUIRED_PACKAGES = frozenset(["cloudpickle", "pydantic"])
199
+ _DEFAULT_STREAM_METHOD_NAME = "stream_query"
200
+ _DEFAULT_BIDI_STREAM_METHOD_NAME = "bidi_stream_query"
201
+ _EXTRA_PACKAGES_FILE = "dependencies.tar.gz"
202
+ _FAILED_TO_REGISTER_API_METHODS_WARNING_TEMPLATE = (
203
+ "Failed to register API methods. Please follow the guide to "
204
+ "register the API methods: "
205
+ "https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/develop/custom#custom-methods. "
206
+ "Error: {%s}"
207
+ )
208
+ _INSTALLATION_SUBDIR = "installation_scripts"
209
+ _METHOD_NAME_KEY_IN_SCHEMA = "name"
210
+ _MODE_KEY_IN_SCHEMA = "api_mode"
211
+ _REQUIREMENTS_FILE = "requirements.txt"
212
+ _STANDARD_API_MODE = ""
213
+ _STREAM_API_MODE = "stream"
214
+ _A2A_EXTENSION_MODE = "a2a_extension"
215
+ _A2A_AGENT_CARD = "a2a_agent_card"
216
+ _WARNINGS_KEY = "warnings"
217
+ _WARNING_MISSING = "missing"
218
+ _WARNING_INCOMPATIBLE = "incompatible"
219
+
220
+ _DEFAULT_METHOD_NAME_MAP = {
221
+ _STANDARD_API_MODE: _DEFAULT_METHOD_NAME,
222
+ _ASYNC_API_MODE: _DEFAULT_ASYNC_METHOD_NAME,
223
+ _STREAM_API_MODE: _DEFAULT_STREAM_METHOD_NAME,
224
+ _ASYNC_STREAM_API_MODE: _DEFAULT_ASYNC_STREAM_METHOD_NAME,
225
+ }
226
+ _DEFAULT_METHOD_RETURN_TYPE_MAP = {
227
+ _STANDARD_API_MODE: _DEFAULT_METHOD_RETURN_TYPE,
228
+ _ASYNC_API_MODE: _DEFAULT_ASYNC_METHOD_RETURN_TYPE,
229
+ _STREAM_API_MODE: _DEFAULT_STREAM_METHOD_RETURN_TYPE,
230
+ _ASYNC_STREAM_API_MODE: _DEFAULT_ASYNC_STREAM_METHOD_RETURN_TYPE,
231
+ }
232
+
233
+
234
+ logger = logging.getLogger("agentplatform_genai.agentengines")
235
+
236
+
237
+ def has_field(obj: Union[BaseModel, JsonDict], field_name: str) -> bool:
238
+ """Returns whether `obj` has `field_name` set to a non-None value.
239
+
240
+ Supports both pydantic models (or any attribute-bearing object) and dicts.
241
+
242
+ Args:
243
+ obj: The object to inspect. May be a pydantic model, a dict, or None.
244
+ field_name: The name of the field to check for.
245
+
246
+ Returns:
247
+ True if `obj` is non-empty and `field_name` is set to a non-None value,
248
+ False otherwise.
249
+ """
250
+ if not obj:
251
+ return False
252
+ if isinstance(obj, dict):
253
+ return obj.get(field_name) is not None
254
+ return getattr(obj, field_name, None) is not None
255
+
256
+
257
+ @typing.runtime_checkable
258
+ class Queryable(Protocol):
259
+ """Protocol for Agent Engines that can be queried."""
260
+
261
+ @abc.abstractmethod
262
+ def query(self, **kwargs): # type: ignore[no-untyped-def]
263
+ """Runs the Agent Engine to serve the user query."""
264
+
265
+
266
+ @typing.runtime_checkable
267
+ class AsyncQueryable(Protocol):
268
+ """Protocol for Agent Engines that can be queried asynchronously."""
269
+
270
+ @abc.abstractmethod
271
+ def async_query(self, **kwargs): # type: ignore[no-untyped-def]
272
+ """Runs the Agent Engine to serve the user query asynchronously."""
273
+
274
+
275
+ @typing.runtime_checkable
276
+ class AsyncStreamQueryable(Protocol):
277
+ """Protocol for Agent Engines that can stream responses asynchronously."""
278
+
279
+ @abc.abstractmethod
280
+ async def async_stream_query(self, **kwargs) -> AsyncIterator[Any]: # type: ignore[no-untyped-def]
281
+ """Asynchronously stream responses to serve the user query."""
282
+
283
+
284
+ @typing.runtime_checkable
285
+ class StreamQueryable(Protocol):
286
+ """Protocol for Agent Engines that can stream responses."""
287
+
288
+ @abc.abstractmethod
289
+ def stream_query(self, **kwargs) -> Iterator[Any]: # type: ignore[no-untyped-def]
290
+ """Stream responses to serve the user query."""
291
+
292
+
293
+ @typing.runtime_checkable
294
+ class BidiStreamQueryable(Protocol):
295
+ """Protocol for Agent Engines that can stream requests and responses."""
296
+
297
+ @abc.abstractmethod
298
+ async def bidi_stream_query(
299
+ self, input_queue: asyncio.Queue[Any]
300
+ ) -> AsyncIterator[Any]:
301
+ """Stream requests and responses to serve the user queries."""
302
+
303
+
304
+ @typing.runtime_checkable
305
+ class Cloneable(Protocol):
306
+ """Protocol for Agent Engines that can be cloned."""
307
+
308
+ @abc.abstractmethod
309
+ def clone(self) -> Any:
310
+ """Return a clone of the object."""
311
+
312
+
313
+ @typing.runtime_checkable
314
+ class OperationRegistrable(Protocol):
315
+ """Protocol for agents that have registered operations."""
316
+
317
+ @abc.abstractmethod
318
+ def register_operations(self, **kwargs: Any) -> dict[str, list[str]]:
319
+ """Register the user provided operations (modes and methods)."""
320
+ pass
321
+
322
+
323
+ if typing.TYPE_CHECKING:
324
+ from google.adk.agents import BaseAgent
325
+
326
+ ADKAgent: TypeAlias = BaseAgent
327
+ else:
328
+ try:
329
+ from google.adk.agents import BaseAgent
330
+
331
+ ADKAgent: Optional[TypeAlias] = BaseAgent
332
+ except (ImportError, AttributeError):
333
+ ADKAgent = None # type: ignore[no-redef]
334
+
335
+ _AgentEngineInterface = Union[
336
+ ADKAgent,
337
+ AsyncQueryable,
338
+ AsyncStreamQueryable,
339
+ OperationRegistrable,
340
+ Queryable,
341
+ StreamQueryable,
342
+ BidiStreamQueryable,
343
+ ]
344
+
345
+
346
+ class _ModuleAgentAttributes(TypedDict, total=False):
347
+ module_name: str
348
+ agent_name: str
349
+ register_operations: Dict[str, list[str]]
350
+ sys_paths: Optional[Sequence[str]]
351
+ agent: _AgentEngineInterface
352
+
353
+
354
+ class ModuleAgent(Cloneable, OperationRegistrable):
355
+ """Agent that is defined by a module and an agent name.
356
+
357
+ This agent is instantiated by importing a module and instantiating an agent
358
+ from that module. It also allows to register operations that are defined in
359
+ the agent.
360
+ """
361
+
362
+ def __init__(
363
+ self,
364
+ *,
365
+ module_name: str,
366
+ agent_name: str,
367
+ register_operations: Dict[str, list[str]],
368
+ sys_paths: Optional[Sequence[str]] = None,
369
+ ):
370
+ """Initializes a module-based agent.
371
+
372
+ Args:
373
+ module_name (str):
374
+ Required. The name of the module to import.
375
+ agent_name (str):
376
+ Required. The name of the agent in the module to instantiate.
377
+ register_operations (Dict[str, list[str]]):
378
+ Required. A dictionary of API modes to a list of method names.
379
+ sys_paths (Sequence[str]):
380
+ Optional. The system paths to search for the module. It should
381
+ be relative to the directory where the code will be running.
382
+ I.e. it should correspond to the directory being passed to
383
+ `extra_packages=...` in the create method. It will be appended
384
+ to the system path in the sequence being specified here, and
385
+ only be appended if it is not already in the system path.
386
+ """
387
+ self._tmpl_attrs: _ModuleAgentAttributes = {
388
+ "module_name": module_name,
389
+ "agent_name": agent_name,
390
+ "register_operations": register_operations,
391
+ "sys_paths": sys_paths,
392
+ }
393
+
394
+ def clone(self) -> "ModuleAgent":
395
+ """Return a clone of the agent."""
396
+ return ModuleAgent(
397
+ module_name=self._tmpl_attrs.get("module_name"),
398
+ agent_name=self._tmpl_attrs.get("agent_name"),
399
+ register_operations=self._tmpl_attrs.get("register_operations"),
400
+ sys_paths=self._tmpl_attrs.get("sys_paths"),
401
+ )
402
+
403
+ def register_operations(self, **kwargs: Any) -> dict[str, list[str]]:
404
+ reg_operations = self._tmpl_attrs.get("register_operations")
405
+ if reg_operations is None:
406
+ raise ValueError("Register operations is not set.")
407
+ return reg_operations
408
+
409
+ def set_up(self) -> None:
410
+ """Sets up the agent for execution of queries at runtime.
411
+
412
+ It runs the code to import the agent from the module, and registers the
413
+ operations of the agent.
414
+ """
415
+ sys_paths = self._tmpl_attrs.get("sys_paths")
416
+ if isinstance(sys_paths, Sequence):
417
+ import sys
418
+
419
+ for sys_path in sys_paths:
420
+ abs_path = os.path.abspath(sys_path)
421
+ if abs_path not in sys.path:
422
+ sys.path.append(abs_path)
423
+
424
+ import importlib
425
+
426
+ module = importlib.import_module(self._tmpl_attrs.get("module_name"))
427
+ try:
428
+ importlib.reload(module)
429
+ except Exception as e:
430
+ logger.warning(
431
+ f"Failed to reload module {self._tmpl_attrs.get('module_name')}: {e}"
432
+ )
433
+ agent_name = self._tmpl_attrs.get("agent_name")
434
+ try:
435
+ agent = getattr(module, agent_name)
436
+ except AttributeError as e:
437
+ raise AttributeError(
438
+ f"Agent {agent_name} not found in module "
439
+ f"{self._tmpl_attrs.get('module_name')}"
440
+ ) from e
441
+ self._tmpl_attrs["agent"] = agent
442
+ if hasattr(agent, "set_up"):
443
+ agent.set_up()
444
+ for operations in self.register_operations().values():
445
+ for operation in operations:
446
+ op = _wrap_agent_operation(agent=agent, operation=operation)
447
+ setattr(self, operation, types.MethodType(op, self))
448
+
449
+
450
+ class _RequirementsValidationActions(TypedDict):
451
+ append: Set[str]
452
+
453
+
454
+ class _RequirementsValidationWarnings(TypedDict):
455
+ missing: Set[str]
456
+ incompatible: Set[str]
457
+
458
+
459
+ class _RequirementsValidationResult(TypedDict):
460
+ warnings: _RequirementsValidationWarnings
461
+ actions: _RequirementsValidationActions
462
+
463
+
464
+ AgentEngineOperationUnion = Union[genai_types.AgentEngineOperation]
465
+
466
+
467
+ class GetOperationFunction(Protocol):
468
+ def __call__(
469
+ self, *, operation_name: str, **kwargs: Any
470
+ ) -> AgentEngineOperationUnion:
471
+ pass
472
+
473
+
474
+ class GetAsyncOperationFunction(Protocol):
475
+ async def __call__(
476
+ self, *, operation_name: str, **kwargs: Any
477
+ ) -> AgentEngineOperationUnion:
478
+ pass
479
+
480
+
481
+ def _get_reasoning_engine_id(operation_name: str = "", resource_name: str = "") -> str:
482
+ """Returns reasoning engine ID from operation name or resource name."""
483
+ if not resource_name and not operation_name:
484
+ raise ValueError("Resource name or operation name cannot be empty.")
485
+
486
+ if resource_name:
487
+ match = re.match(
488
+ r"^projects/[^/]+/locations/[^/]+/reasoningEngines/([^/]+)$",
489
+ resource_name,
490
+ )
491
+ if match:
492
+ return match.group(1)
493
+ else:
494
+ raise ValueError(
495
+ "Failed to parse reasoning engine ID from resource name: "
496
+ f"`{resource_name}`"
497
+ )
498
+
499
+ if not operation_name:
500
+ raise ValueError("Operation name cannot be empty.")
501
+
502
+ match = re.match(
503
+ r"^projects/[^/]+/locations/[^/]+/reasoningEngines/([^/]+)/operations/[^/]+$",
504
+ operation_name,
505
+ )
506
+ if match:
507
+ return match.group(1)
508
+ raise ValueError(
509
+ "Failed to parse reasoning engine ID from operation name: "
510
+ f"`{operation_name}`"
511
+ )
512
+
513
+
514
+ async def _await_async_operation(
515
+ *,
516
+ operation_name: str,
517
+ get_operation_fn: GetAsyncOperationFunction,
518
+ poll_interval_seconds: float = 10,
519
+ ) -> Any:
520
+ """Waits for the operation for creating an agent engine to complete.
521
+
522
+ Args:
523
+ operation_name (str):
524
+ Required. The name of the operation for creating the Agent Engine.
525
+ poll_interval_seconds (float):
526
+ The number of seconds to wait between each poll.
527
+ get_operation_fn (Callable[[str], Awaitable[Any]]):
528
+ Optional. The async function to use for getting the operation. If not
529
+ provided, `self._get_agent_operation` will be used.
530
+
531
+ Returns:
532
+ The operation that has completed (i.e. `operation.done==True`).
533
+ """
534
+ operation = await get_operation_fn(operation_name=operation_name)
535
+ while not operation.done:
536
+ await asyncio.sleep(poll_interval_seconds)
537
+ operation = await get_operation_fn(operation_name=operation.name)
538
+
539
+ return operation
540
+
541
+
542
+ def _await_operation(
543
+ *,
544
+ operation_name: str,
545
+ get_operation_fn: GetOperationFunction,
546
+ poll_interval_seconds: float = 10,
547
+ ) -> Any:
548
+ """Waits for the operation for creating an agent engine to complete.
549
+
550
+ Args:
551
+ operation_name (str):
552
+ Required. The name of the operation for creating the Agent Engine.
553
+ poll_interval_seconds (float):
554
+ The number of seconds to wait between each poll.
555
+ get_operation_fn (Callable[[str], Any]):
556
+ Optional. The function to use for getting the operation. If not
557
+ provided, `self._get_agent_operation` will be used.
558
+
559
+ Returns:
560
+ The operation that has completed (i.e. `operation.done==True`).
561
+ """
562
+ operation = get_operation_fn(operation_name=operation_name)
563
+ while not operation.done:
564
+ time.sleep(poll_interval_seconds)
565
+ operation = get_operation_fn(operation_name=operation.name)
566
+
567
+ return operation
568
+
569
+
570
+ def _compare_requirements(
571
+ *,
572
+ requirements: Mapping[str, str],
573
+ constraints: Union[Sequence[str], Mapping[str, Optional["_SpecifierSet"]]],
574
+ required_packages: Optional[Iterator[str]] = None,
575
+ ) -> _RequirementsValidationResult:
576
+ """Compares the requirements with the constraints.
577
+
578
+ Args:
579
+ requirements (Mapping[str, str]):
580
+ Required. The packages (and their versions) to compare with the constraints.
581
+ This is assumed to be the result of `scan_requirements`.
582
+ constraints (Union[Sequence[str], Mapping[str, SpecifierSet]]):
583
+ Required. The package constraints to compare against. This is assumed
584
+ to be the result of `parse_constraints`.
585
+ required_packages (Iterator[str]):
586
+ Optional. The set of packages that are required to be in the
587
+ constraints. It defaults to the set of packages that are required
588
+ for deployment on Agent Engine.
589
+
590
+ Returns:
591
+ dict[str, dict[str, Any]]: The comparison result as a dictionary containing:
592
+ * warnings:
593
+ * missing: The set of packages that are not in the constraints.
594
+ * incompatible: The set of packages that are in the constraints
595
+ but have versions that are not in the constraint specifier.
596
+ * actions:
597
+ * append: The set of packages that are not in the constraints
598
+ but should be appended to the constraints.
599
+ """
600
+ packaging_version = _import_packaging_version_or_raise()
601
+ if required_packages is None:
602
+ required_packages = _DEFAULT_REQUIRED_PACKAGES # type: ignore[assignment]
603
+ result = _RequirementsValidationResult(
604
+ warnings=_RequirementsValidationWarnings(missing=set(), incompatible=set()),
605
+ actions=_RequirementsValidationActions(append=set()),
606
+ )
607
+ if isinstance(constraints, list):
608
+ constraints = _parse_constraints(constraints=constraints)
609
+ for package, package_version in requirements.items():
610
+ if package not in constraints:
611
+ result[_WARNINGS_KEY][_WARNING_MISSING].add(package) # type: ignore[literal-required]
612
+ if package in required_packages: # type: ignore[operator]
613
+ result[_ACTIONS_KEY][_ACTION_APPEND].add( # type: ignore[literal-required]
614
+ f"{package}=={package_version}"
615
+ )
616
+ continue
617
+ if package_version:
618
+ package_specifier = constraints[package] # type: ignore[call-overload]
619
+ if not package_specifier:
620
+ continue
621
+ if packaging_version.Version(package_version) not in package_specifier:
622
+ result[_WARNINGS_KEY][_WARNING_INCOMPATIBLE].add( # type: ignore[literal-required]
623
+ f"{package}=={package_version} (required: {str(package_specifier)})"
624
+ )
625
+ return result
626
+
627
+
628
+ def _generate_class_methods_spec_or_raise(
629
+ *,
630
+ agent: _AgentEngineInterface,
631
+ operations: Dict[str, List[str]],
632
+ ) -> List[proto.Message]:
633
+ """Generates a ReasoningEngineSpec based on the registered operations.
634
+
635
+ Args:
636
+ agent: The AgentEngine instance.
637
+ operations: A dictionary of API modes and method names.
638
+
639
+ Returns:
640
+ A list of ReasoningEngineSpec.ClassMethod messages.
641
+
642
+ Raises:
643
+ ValueError: If a method defined in `register_operations` is not found on
644
+ the AgentEngine.
645
+ """
646
+ if isinstance(agent, ModuleAgent):
647
+ # We do a dry-run of setting up the agent engine to have the operations
648
+ # needed for registration.
649
+ agent: ModuleAgent = agent.clone() # type: ignore[no-redef]
650
+ try:
651
+ agent.set_up()
652
+ except Exception as e:
653
+ raise ValueError(f"Failed to set up agent {agent}: {e}") from e
654
+ class_methods_spec = []
655
+ for mode, method_names in operations.items():
656
+ for method_name in method_names:
657
+ if not hasattr(agent, method_name):
658
+ raise ValueError(
659
+ f"Method `{method_name}` defined in `register_operations`"
660
+ " not found on agent."
661
+ )
662
+
663
+ method = getattr(agent, method_name)
664
+ try:
665
+ schema_dict = _generate_schema(method, schema_name=method_name)
666
+ except Exception as e:
667
+ logger.warning(f"failed to generate schema for {method_name}: {e}")
668
+ continue
669
+
670
+ class_method = _to_proto(schema_dict)
671
+ class_method[_MODE_KEY_IN_SCHEMA] = mode
672
+ if hasattr(agent, "agent_card"):
673
+ class_method[_A2A_AGENT_CARD] = json_format.MessageToJson(
674
+ getattr(agent, "agent_card")
675
+ )
676
+ class_methods_spec.append(class_method)
677
+
678
+ return class_methods_spec
679
+
680
+
681
+ def _class_methods_to_class_methods_spec(
682
+ class_methods: List[dict[str, Any]],
683
+ ) -> List[proto.Message]:
684
+ """Converts a list of class methods to a list of ReasoningEngineSpec.ClassMethod messages."""
685
+ return [_to_proto(class_method) for class_method in class_methods]
686
+
687
+
688
+ def _is_pydantic_serializable(param: inspect.Parameter) -> bool:
689
+ """Checks if the parameter is pydantic serializable."""
690
+
691
+ if param.annotation == inspect.Parameter.empty:
692
+ return True
693
+
694
+ if "ForwardRef" in repr(param.annotation):
695
+ return True
696
+
697
+ if isinstance(param.annotation, str):
698
+ return False
699
+
700
+ pydantic = _import_pydantic_or_raise()
701
+ try:
702
+ pydantic.TypeAdapter(param.annotation)
703
+ return True
704
+ except Exception:
705
+ return False
706
+
707
+
708
+ def _generate_schema(
709
+ f: Callable[..., Any],
710
+ *,
711
+ schema_name: Optional[str] = None,
712
+ descriptions: Mapping[str, str] = {},
713
+ required: Sequence[str] = [],
714
+ ) -> Dict[str, Any]:
715
+ """Generates the OpenAPI Schema for a callable object.
716
+
717
+ Only positional and keyword arguments of the function `f` will be supported
718
+ in the OpenAPI Schema that is generated. I.e. `*args` and `**kwargs` will
719
+ not be present in the OpenAPI schema returned from this function. For those
720
+ cases, you can either include it in the docstring for `f`, or modify the
721
+ OpenAPI schema returned from this function to include additional arguments.
722
+
723
+ Args:
724
+ f (Callable):
725
+ Required. The function to generate an OpenAPI Schema for.
726
+ schema_name (str):
727
+ Optional. The name for the OpenAPI schema. If unspecified, the name
728
+ of the Callable will be used.
729
+ descriptions (Mapping[str, str]):
730
+ Optional. A `{name: description}` mapping for annotating input
731
+ arguments of the function with user-provided descriptions. It
732
+ defaults to an empty dictionary (i.e. there will not be any
733
+ description for any of the inputs).
734
+ required (Sequence[str]):
735
+ Optional. For the user to specify the set of required arguments in
736
+ function calls to `f`. If specified, it will be automatically
737
+ inferred from `f`.
738
+
739
+ Returns:
740
+ dict[str, Any]: The OpenAPI Schema for the function `f` in JSON format.
741
+ """
742
+ pydantic = _import_pydantic_or_raise()
743
+ defaults = dict(inspect.signature(f).parameters)
744
+ fields_dict = {
745
+ name: (
746
+ # 1. We infer the argument type here: use Any rather than None so
747
+ # it will not try to auto-infer the type based on the default value.
748
+ (
749
+ param.annotation
750
+ if param.annotation != inspect.Parameter.empty
751
+ and "ForwardRef" not in repr(param.annotation)
752
+ else Any
753
+ ),
754
+ pydantic.Field(
755
+ # 2. We do not support default values for now.
756
+ # default=(
757
+ # param.default if param.default != inspect.Parameter.empty
758
+ # else None
759
+ # ),
760
+ # 3. We support user-provided descriptions.
761
+ description=descriptions.get(name, None),
762
+ ),
763
+ )
764
+ for name, param in defaults.items()
765
+ # We do not support *args or **kwargs
766
+ if param.kind
767
+ in (
768
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
769
+ inspect.Parameter.KEYWORD_ONLY,
770
+ inspect.Parameter.POSITIONAL_ONLY,
771
+ )
772
+ # For a bidi endpoint, it requires an asyncio.Queue as the input, but
773
+ # it is not JSON serializable. We hence exclude it from the schema.
774
+ and param.annotation != asyncio.Queue and _is_pydantic_serializable(param)
775
+ }
776
+ parameters = pydantic.create_model(f.__name__, **fields_dict).schema()
777
+ # Postprocessing
778
+ # 4. Suppress unnecessary title generation:
779
+ # * https://github.com/pydantic/pydantic/issues/1051
780
+ # * http://cl/586221780
781
+ parameters.pop("title", "")
782
+ for name, function_arg in parameters.get("properties", {}).items():
783
+ function_arg.pop("title", "")
784
+ annotation = defaults[name].annotation
785
+ # 5. Nullable fields:
786
+ # * https://github.com/pydantic/pydantic/issues/1270
787
+ # * https://stackoverflow.com/a/58841311
788
+ # * https://github.com/pydantic/pydantic/discussions/4872
789
+ if typing.get_origin(annotation) is Union and type(None) in typing.get_args(
790
+ annotation
791
+ ):
792
+ # for "typing.Optional" arguments, function_arg might be a
793
+ # dictionary like
794
+ #
795
+ # {'anyOf': [{'type': 'integer'}, {'type': 'null'}]
796
+ for schema in function_arg.pop("anyOf", []):
797
+ schema_type = schema.get("type")
798
+ if schema_type and schema_type != "null":
799
+ function_arg["type"] = schema_type
800
+ break
801
+ function_arg["nullable"] = True
802
+ # 6. Annotate required fields.
803
+ if required:
804
+ # We use the user-provided "required" fields if specified.
805
+ parameters["required"] = required
806
+ else:
807
+ # Otherwise we infer it from the function signature.
808
+ parameters["required"] = [
809
+ k
810
+ for k in defaults
811
+ if (
812
+ defaults[k].default == inspect.Parameter.empty
813
+ and defaults[k].kind
814
+ in (
815
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
816
+ inspect.Parameter.KEYWORD_ONLY,
817
+ inspect.Parameter.POSITIONAL_ONLY,
818
+ )
819
+ )
820
+ ]
821
+ schema = dict(name=f.__name__, description=f.__doc__, parameters=parameters)
822
+ if schema_name:
823
+ schema["name"] = schema_name
824
+ return schema
825
+
826
+
827
+ def _get_agent_framework(
828
+ *,
829
+ agent_framework: Optional[str],
830
+ agent: _AgentEngineInterface,
831
+ ) -> Union[str, Any]:
832
+ """Gets the agent framework to use.
833
+
834
+ The agent framework is determined in the following order of priority:
835
+ 1. The `agent_framework` passed to this function.
836
+ 2. The `agent_framework` attribute on the `agent` object.
837
+ 3. The default framework, "custom".
838
+
839
+ Args:
840
+ agent_framework (str):
841
+ The agent framework provided by the user.
842
+ agent (_AgentEngineInterface):
843
+ The agent engine instance.
844
+
845
+ Returns:
846
+ str: The name of the agent framework to use.
847
+ """
848
+ if agent_framework is not None and agent_framework in _SUPPORTED_AGENT_FRAMEWORKS:
849
+ logger.info(f"Using agent framework: {agent_framework}")
850
+ return agent_framework
851
+ if hasattr(agent, _AGENT_FRAMEWORK_ATTR):
852
+ agent_framework_attr = getattr(agent, _AGENT_FRAMEWORK_ATTR)
853
+ if (
854
+ agent_framework_attr is not None
855
+ and isinstance(agent_framework_attr, str)
856
+ and agent_framework_attr in _SUPPORTED_AGENT_FRAMEWORKS
857
+ ):
858
+ logger.info(f"Using agent framework: {agent_framework_attr}")
859
+ return agent_framework_attr
860
+ logger.info(
861
+ f"The provided agent framework {agent_framework} is not supported."
862
+ f" Defaulting to {_DEFAULT_AGENT_FRAMEWORK}."
863
+ )
864
+ return _DEFAULT_AGENT_FRAMEWORK
865
+
866
+
867
+ def _get_gcs_bucket(
868
+ *,
869
+ project: str,
870
+ location: str,
871
+ staging_bucket: str,
872
+ credentials: Optional[Any] = None,
873
+ ) -> _StorageBucket:
874
+ """Gets or creates the GCS bucket."""
875
+ storage = _import_cloud_storage_or_raise()
876
+ storage_client = storage.Client(project=project, credentials=credentials)
877
+ staging_bucket = staging_bucket.replace("gs://", "")
878
+ try:
879
+ gcs_bucket = storage_client.get_bucket(staging_bucket)
880
+ logger.info(f"Using bucket {staging_bucket}")
881
+ except exceptions.NotFound:
882
+ new_bucket = storage_client.bucket(staging_bucket)
883
+ gcs_bucket = storage_client.create_bucket(new_bucket, location=location)
884
+ logger.info(f"Creating bucket {staging_bucket} in {location=}")
885
+ return gcs_bucket
886
+
887
+
888
+ def _get_registered_operations(
889
+ *,
890
+ agent: _AgentEngineInterface,
891
+ ) -> dict[str, list[str]]:
892
+ """Retrieves registered operations for a AgentEngine."""
893
+ if isinstance(agent, OperationRegistrable):
894
+ return agent.register_operations()
895
+
896
+ operations = {}
897
+ if isinstance(agent, Queryable):
898
+ operations[_STANDARD_API_MODE] = [_DEFAULT_METHOD_NAME]
899
+ if isinstance(agent, AsyncQueryable):
900
+ operations[_ASYNC_API_MODE] = [_DEFAULT_ASYNC_METHOD_NAME]
901
+ if isinstance(agent, StreamQueryable):
902
+ operations[_STREAM_API_MODE] = [_DEFAULT_STREAM_METHOD_NAME]
903
+ if isinstance(agent, AsyncStreamQueryable):
904
+ operations[_ASYNC_STREAM_API_MODE] = [_DEFAULT_ASYNC_STREAM_METHOD_NAME]
905
+ if isinstance(agent, BidiStreamQueryable):
906
+ operations[_BIDI_STREAM_API_MODE] = [_DEFAULT_BIDI_STREAM_METHOD_NAME]
907
+ return operations
908
+
909
+
910
+ def _import_cloudpickle_or_raise() -> types.ModuleType:
911
+ """Tries to import the cloudpickle module."""
912
+ try:
913
+ import cloudpickle # noqa:F401
914
+ except ImportError as e:
915
+ raise ImportError(
916
+ "cloudpickle is not installed. Please call "
917
+ "'pip install google-cloud-aiplatform[agent_engines]'."
918
+ ) from e
919
+ return cloudpickle # type: ignore[no-any-return]
920
+
921
+
922
+ def _import_cloud_storage_or_raise() -> types.ModuleType:
923
+ """Tries to import the Cloud Storage module."""
924
+ try:
925
+ from google.cloud import storage # type: ignore[attr-defined]
926
+ except ImportError as e:
927
+ raise ImportError(
928
+ "Cloud Storage is not installed. Please call "
929
+ "'pip install google-cloud-aiplatform[agent_engines]'."
930
+ ) from e
931
+ return storage # type: ignore[no-any-return]
932
+
933
+
934
+ def _import_packaging_requirements_or_raise() -> types.ModuleType:
935
+ """Tries to import the packaging.requirements module."""
936
+ try:
937
+ from packaging import requirements
938
+ except ImportError as e:
939
+ raise ImportError(
940
+ "packaging.requirements is not installed. Please call "
941
+ "'pip install google-cloud-aiplatform[agent_engines]'."
942
+ ) from e
943
+ return requirements
944
+
945
+
946
+ def _import_packaging_version_or_raise() -> types.ModuleType:
947
+ """Tries to import the packaging.requirements module."""
948
+ try:
949
+ from packaging import version
950
+ except ImportError as e:
951
+ raise ImportError(
952
+ "packaging.version is not installed. Please call "
953
+ "'pip install google-cloud-aiplatform[agent_engines]'."
954
+ ) from e
955
+ return version
956
+
957
+
958
+ def _import_pydantic_or_raise() -> types.ModuleType:
959
+ """Tries to import the pydantic module."""
960
+ try:
961
+ import pydantic
962
+
963
+ _ = pydantic.Field
964
+ except AttributeError:
965
+ from pydantic import v1 as pydantic # type: ignore[no-redef]
966
+ except ImportError as e:
967
+ raise ImportError(
968
+ "pydantic is not installed. Please call "
969
+ "'pip install google-cloud-aiplatform[agent_engines]'."
970
+ ) from e
971
+ return pydantic
972
+
973
+
974
+ def _parse_constraints(
975
+ *,
976
+ constraints: Sequence[str],
977
+ ) -> Mapping[str, Optional["_SpecifierSet"]]:
978
+ """Parses a list of constraints into a dict of requirements.
979
+
980
+ Args:
981
+ constraints (list[str]):
982
+ Required. The list of package requirements to parse. This is assumed
983
+ to come from the `requirements.txt` file.
984
+
985
+ Returns:
986
+ dict[str, SpecifierSet]: The specifiers for each package.
987
+ """
988
+ requirements = _import_packaging_requirements_or_raise()
989
+ result: Dict[str, Optional[_SpecifierSet]] = {}
990
+ for constraint in constraints:
991
+ try:
992
+ if constraint.endswith(".whl"):
993
+ constraint = os.path.basename(constraint)
994
+ requirement = requirements.Requirement(constraint)
995
+ except Exception as e:
996
+ logger.warning(f"Failed to parse constraint: {constraint}. Exception: {e}")
997
+ continue
998
+ result[requirement.name] = requirement.specifier or None
999
+ return result
1000
+
1001
+
1002
+ def _prepare(
1003
+ *,
1004
+ agent: Optional[_AgentEngineInterface],
1005
+ requirements: Optional[Sequence[str]],
1006
+ extra_packages: Optional[Sequence[str]],
1007
+ project: str,
1008
+ location: str,
1009
+ staging_bucket: str,
1010
+ gcs_dir_name: str,
1011
+ credentials: Optional[Any] = None,
1012
+ ) -> None:
1013
+ """Prepares the agent engine for creation or updates in Vertex AI.
1014
+
1015
+ This involves packaging and uploading artifacts to Cloud Storage. Note that
1016
+ 1. This does not actually update the Agent Engine in Vertex AI.
1017
+ 2. This will only generate and upload a pickled object if specified.
1018
+ 3. This will only generate and upload the dependencies.tar.gz file if
1019
+ extra_packages is non-empty.
1020
+
1021
+ Args:
1022
+ agent: The agent engine to be prepared.
1023
+ requirements (Sequence[str]): The set of PyPI dependencies needed.
1024
+ extra_packages (Sequence[str]): The set of extra user-provided packages.
1025
+ project (str): The project for the staging bucket.
1026
+ location (str): The location for the staging bucket.
1027
+ staging_bucket (str): The staging bucket name in the form "gs://...".
1028
+ gcs_dir_name (str): The GCS bucket directory under `staging_bucket` to use
1029
+ for staging the artifacts needed.
1030
+ credentials: The credentials to use for the storage client.
1031
+ """
1032
+ if agent is None:
1033
+ return
1034
+ gcs_bucket = _get_gcs_bucket(
1035
+ project=project,
1036
+ location=location,
1037
+ staging_bucket=staging_bucket,
1038
+ credentials=credentials,
1039
+ )
1040
+ _upload_agent_engine(
1041
+ agent=agent,
1042
+ gcs_bucket=gcs_bucket,
1043
+ gcs_dir_name=gcs_dir_name,
1044
+ )
1045
+ if requirements is not None:
1046
+ _upload_requirements(
1047
+ requirements=requirements,
1048
+ gcs_bucket=gcs_bucket,
1049
+ gcs_dir_name=gcs_dir_name,
1050
+ )
1051
+ if extra_packages is not None:
1052
+ _upload_extra_packages(
1053
+ extra_packages=extra_packages,
1054
+ gcs_bucket=gcs_bucket,
1055
+ gcs_dir_name=gcs_dir_name,
1056
+ )
1057
+
1058
+
1059
+ def _register_api_methods_or_raise(
1060
+ *,
1061
+ agent_engine: genai_types.AgentEngine | genai_types.AgentEngineRuntimeRevision,
1062
+ wrap_operation_fn: Optional[
1063
+ dict[str, Callable[[str, str], Callable[..., Any]]]
1064
+ ] = None,
1065
+ ) -> None:
1066
+ """Registers Agent Engine API methods based on operation schemas.
1067
+
1068
+ This function iterates through operation schemas provided by the
1069
+ `agent_engine`. Each schema defines an API mode and method name.
1070
+ It dynamically creates and registers methods on the `agent_engine`
1071
+ to handle API calls based on the specified API mode.
1072
+ Currently, only standard API mode `` is supported.
1073
+
1074
+ Args:
1075
+ agent_engine: The AgentEngine to augment with API methods.
1076
+ wrap_operation_fn: A dictionary of API modes and method wrapping
1077
+ functions.
1078
+
1079
+ Raises:
1080
+ ValueError: If the API mode is not supported or if the operation schema
1081
+ is missing any required fields (e.g. `api_mode` or `name`).
1082
+ """
1083
+ operation_schemas = agent_engine.operation_schemas()
1084
+ if not operation_schemas:
1085
+ return
1086
+ for operation_schema in operation_schemas:
1087
+ if _MODE_KEY_IN_SCHEMA not in operation_schema:
1088
+ raise ValueError(
1089
+ f"Operation schema {operation_schema} does not"
1090
+ f" contain an `{_MODE_KEY_IN_SCHEMA}` field."
1091
+ )
1092
+ api_mode = operation_schema.get(_MODE_KEY_IN_SCHEMA)
1093
+ # For bidi stream api mode, we don't need to wrap the operation.
1094
+ if api_mode == _BIDI_STREAM_API_MODE:
1095
+ continue
1096
+
1097
+ if _METHOD_NAME_KEY_IN_SCHEMA not in operation_schema:
1098
+ raise ValueError(
1099
+ f"Operation schema {operation_schema} does not"
1100
+ f" contain a `{_METHOD_NAME_KEY_IN_SCHEMA}` field."
1101
+ )
1102
+ method_name = operation_schema.get(_METHOD_NAME_KEY_IN_SCHEMA)
1103
+ if not isinstance(method_name, str):
1104
+ raise ValueError(
1105
+ "Operation schema has a non-string value for"
1106
+ f" `{_METHOD_NAME_KEY_IN_SCHEMA}`: {method_name}"
1107
+ )
1108
+ method_description = operation_schema.get(
1109
+ "description",
1110
+ _DEFAULT_METHOD_DOCSTRING_TEMPLATE.format(
1111
+ method_name=method_name,
1112
+ default_method_name=_DEFAULT_METHOD_NAME_MAP.get(
1113
+ api_mode, _DEFAULT_METHOD_NAME
1114
+ ),
1115
+ return_type=_DEFAULT_METHOD_RETURN_TYPE_MAP.get(
1116
+ api_mode,
1117
+ _DEFAULT_METHOD_RETURN_TYPE,
1118
+ ),
1119
+ ),
1120
+ )
1121
+ _wrap_operation_map = {
1122
+ _STANDARD_API_MODE: _wrap_query_operation,
1123
+ _ASYNC_API_MODE: _wrap_async_query_operation,
1124
+ _STREAM_API_MODE: _wrap_stream_query_operation,
1125
+ _ASYNC_STREAM_API_MODE: _wrap_async_stream_query_operation,
1126
+ _A2A_EXTENSION_MODE: _wrap_a2a_operation,
1127
+ }
1128
+ if isinstance(wrap_operation_fn, dict) and api_mode in wrap_operation_fn:
1129
+ # Override the default function with user-specified function if it exists.
1130
+ _wrap_operation = wrap_operation_fn[api_mode]
1131
+ elif api_mode in _wrap_operation_map:
1132
+ _wrap_operation = _wrap_operation_map[api_mode] # type: ignore[assignment]
1133
+ else:
1134
+ supported_api_modes = ", ".join(
1135
+ f"`{mode}`" for mode in sorted(_wrap_operation_map.keys())
1136
+ )
1137
+ raise ValueError(
1138
+ f"Unsupported api mode: `{api_mode}`,"
1139
+ f" Supported modes are: {supported_api_modes}."
1140
+ )
1141
+
1142
+ # Bind the method to the object.
1143
+ if api_mode == _A2A_EXTENSION_MODE:
1144
+ agent_card = operation_schema.get(_A2A_AGENT_CARD)
1145
+ method = _wrap_operation(
1146
+ method_name=method_name, agent_card=agent_card
1147
+ ) # type: ignore[call-arg]
1148
+ else:
1149
+ method = _wrap_operation(method_name=method_name) # type: ignore[call-arg]
1150
+ method.__name__ = method_name
1151
+ if method_description and isinstance(method_description, str):
1152
+ method.__doc__ = method_description
1153
+ setattr(agent_engine, method_name, types.MethodType(method, agent_engine))
1154
+
1155
+
1156
+ def _scan_requirements(
1157
+ *,
1158
+ obj: Any,
1159
+ ignore_modules: Optional[Sequence[str]] = None,
1160
+ package_distributions: Optional[Mapping[str, Sequence[str]]] = None,
1161
+ inspect_getmembers_kwargs: Optional[Mapping[str, Any]] = None,
1162
+ ) -> Mapping[str, str]:
1163
+ """Scans the object for modules and returns the requirements discovered.
1164
+
1165
+ This is not a comprehensive scan of the object, and only detects for common
1166
+ cases based on the members of the object returned by `dir(obj)`.
1167
+
1168
+ Args:
1169
+ obj (Any):
1170
+ Required. The object to scan for package requirements.
1171
+ ignore_modules (Sequence[str]):
1172
+ Optional. The set of modules to ignore. It defaults to the set of
1173
+ built-in and stdlib modules.
1174
+ package_distributions (Mapping[str, Sequence[str]]):
1175
+ Optional. The mapping of module names to the set of packages that
1176
+ contain them. It defaults to the set of packages from
1177
+ `importlib_metadata.packages_distributions()`.
1178
+ inspect_getmembers_kwargs (Mapping[str, Any]):
1179
+ Optional. The keyword arguments to pass to `inspect.getmembers`. It
1180
+ defaults to an empty dictionary.
1181
+
1182
+ Returns:
1183
+ Sequence[str]: The list of requirements that were discovered.
1184
+ """
1185
+ if ignore_modules is None:
1186
+ ignore_modules = _BASE_MODULES # type: ignore[assignment]
1187
+ if package_distributions is None:
1188
+ package_distributions = _PACKAGE_DISTRIBUTIONS
1189
+ modules_found = set(_DEFAULT_REQUIRED_PACKAGES)
1190
+ inspect_getmembers_kwargs = inspect_getmembers_kwargs or {}
1191
+ for _, attr in inspect.getmembers(obj, **inspect_getmembers_kwargs):
1192
+ if not attr or inspect.isbuiltin(attr) or not hasattr(attr, "__module__"):
1193
+ continue
1194
+ module_name = (attr.__module__ or "").split(".")[0]
1195
+ if module_name and module_name not in ignore_modules: # type: ignore[operator]
1196
+ for module in package_distributions.get(module_name, []):
1197
+ modules_found.add(module)
1198
+ return {module: importlib_metadata.version(module) for module in modules_found}
1199
+
1200
+
1201
+ def _to_dict(message: proto.Message) -> Dict[str, Any]:
1202
+ """Converts the contents of the protobuf message to JSON format.
1203
+
1204
+ Args:
1205
+ message (proto.Message):
1206
+ Required. The proto message to be converted to a JSON dictionary.
1207
+
1208
+ Returns:
1209
+ dict[str, Any]: A dictionary containing the contents of the proto.
1210
+ """
1211
+ try:
1212
+ # Best effort attempt to convert the message into a JSON dictionary.
1213
+ result: Dict[str, Any] = json.loads(
1214
+ json_format.MessageToJson(
1215
+ message._pb,
1216
+ preserving_proto_field_name=True,
1217
+ )
1218
+ )
1219
+ except AttributeError:
1220
+ result: Dict[str, Any] = json.loads( # type: ignore[no-redef]
1221
+ json_format.MessageToJson(
1222
+ message,
1223
+ preserving_proto_field_name=True,
1224
+ )
1225
+ )
1226
+ return result
1227
+
1228
+
1229
+ def _to_proto(
1230
+ obj: Union[Dict[str, Any], proto.Message],
1231
+ message: Optional[proto.Message] = None,
1232
+ ) -> proto.Message:
1233
+ """Parses a JSON-like object into a message.
1234
+
1235
+ If the object is already a message, this will return the object as-is. If
1236
+ the object is a JSON Dict, this will parse and merge the object into the
1237
+ message.
1238
+
1239
+ Args:
1240
+ obj (Union[dict[str, Any], proto.Message]):
1241
+ Required. The object to convert to a proto message.
1242
+ message (proto.Message):
1243
+ Optional. A protocol buffer message to merge the obj into. It
1244
+ defaults to Struct() if unspecified.
1245
+
1246
+ Returns:
1247
+ proto.Message: The same message passed as argument.
1248
+ """
1249
+ if message is None:
1250
+ message = struct_pb2.Struct()
1251
+ if isinstance(obj, (proto.Message, struct_pb2.Struct)):
1252
+ return obj
1253
+ try:
1254
+ json_format.ParseDict(obj, message._pb)
1255
+ except AttributeError:
1256
+ json_format.ParseDict(obj, message)
1257
+ return message
1258
+
1259
+
1260
+ def _upload_agent_engine(
1261
+ *,
1262
+ agent: _AgentEngineInterface,
1263
+ gcs_bucket: _StorageBucket,
1264
+ gcs_dir_name: str,
1265
+ ) -> None:
1266
+ """Uploads the agent engine to GCS."""
1267
+ cloudpickle = _import_cloudpickle_or_raise()
1268
+ blob = gcs_bucket.blob(f"{gcs_dir_name}/{_BLOB_FILENAME}")
1269
+ with blob.open("wb") as f:
1270
+ try:
1271
+ cloudpickle.dump(agent, f)
1272
+ except Exception as e:
1273
+ url = "https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/develop/custom#deployment-considerations"
1274
+ error_msg = f"Failed to serialize agent engine. Visit {url} for details."
1275
+ if "google._upb._message" in str(e) or "Descriptor" in str(e):
1276
+ error_msg += (
1277
+ " This is often caused by protobuf objects (like Part, AgentCard) "
1278
+ "being imported at the global module level. Please move these "
1279
+ "imports inside the functions or methods where they are used. "
1280
+ "Alternatively, you can import the entire module: "
1281
+ "`from a2a import types`."
1282
+ )
1283
+ raise TypeError(error_msg) from e
1284
+ with blob.open("rb") as f:
1285
+ try:
1286
+ _ = cloudpickle.load(f)
1287
+ except Exception as e:
1288
+ raise TypeError("Agent engine serialized to an invalid format") from e
1289
+ dir_name = f"gs://{gcs_bucket.name}/{gcs_dir_name}"
1290
+ logger.info(f"Wrote to {dir_name}/{_BLOB_FILENAME}")
1291
+
1292
+
1293
+ def _upload_requirements(
1294
+ *,
1295
+ requirements: Sequence[str],
1296
+ gcs_bucket: _StorageBucket,
1297
+ gcs_dir_name: str,
1298
+ ) -> None:
1299
+ """Uploads the requirements file to GCS."""
1300
+ blob = gcs_bucket.blob(f"{gcs_dir_name}/{_REQUIREMENTS_FILE}")
1301
+ blob.upload_from_string("\n".join(requirements))
1302
+ dir_name = f"gs://{gcs_bucket.name}/{gcs_dir_name}"
1303
+ logger.info(f"Writing to {dir_name}/{_REQUIREMENTS_FILE}")
1304
+
1305
+
1306
+ def _upload_extra_packages(
1307
+ *,
1308
+ extra_packages: Sequence[str],
1309
+ gcs_bucket: _StorageBucket,
1310
+ gcs_dir_name: str,
1311
+ ) -> None:
1312
+ """Uploads extra packages to GCS."""
1313
+ logger.info("Creating in-memory tarfile of extra_packages")
1314
+ tar_fileobj = io.BytesIO()
1315
+ with tarfile.open(fileobj=tar_fileobj, mode="w|gz") as tar:
1316
+ for file in extra_packages:
1317
+ tar.add(file)
1318
+ tar_fileobj.seek(0)
1319
+ blob = gcs_bucket.blob(f"{gcs_dir_name}/{_EXTRA_PACKAGES_FILE}")
1320
+ blob.upload_from_string(tar_fileobj.read())
1321
+ dir_name = f"gs://{gcs_bucket.name}/{gcs_dir_name}"
1322
+ logger.info(f"Writing to {dir_name}/{_EXTRA_PACKAGES_FILE}")
1323
+
1324
+
1325
+ def _create_base64_encoded_tarball(
1326
+ *,
1327
+ source_packages: Sequence[str],
1328
+ ) -> str:
1329
+ """Creates a base64 encoded tarball from the source packages."""
1330
+ logger.info("Creating in-memory tarfile of source_packages")
1331
+ tar_fileobj = io.BytesIO()
1332
+ project_dir = os.path.realpath(os.getcwd())
1333
+ with tarfile.open(fileobj=tar_fileobj, mode="w|gz") as tar:
1334
+ for file in source_packages:
1335
+ real_file_path = os.path.realpath(file)
1336
+ if real_file_path != project_dir and not real_file_path.startswith(
1337
+ project_dir + os.sep
1338
+ ):
1339
+ raise ValueError(
1340
+ f"File path '{file}' is outside the project directory "
1341
+ f"'{project_dir}'."
1342
+ )
1343
+ tar.add(file)
1344
+ tar_fileobj.seek(0)
1345
+ tarball_bytes = tar_fileobj.read()
1346
+ return base64.b64encode(tarball_bytes).decode("utf-8")
1347
+
1348
+
1349
+ def _validate_packages_or_raise(
1350
+ *,
1351
+ packages: Sequence[str],
1352
+ build_options: Optional[Dict[str, Sequence[str]]] = None,
1353
+ ) -> Sequence[str]:
1354
+ """Tries to validates the packages."""
1355
+ packages = packages or []
1356
+ if build_options and _INSTALLATION_SUBDIR in build_options:
1357
+ _validate_installation_scripts_or_raise(
1358
+ script_paths=build_options[_INSTALLATION_SUBDIR],
1359
+ packages=packages,
1360
+ )
1361
+ for package in packages:
1362
+ if not os.path.exists(package):
1363
+ raise FileNotFoundError(f"Package specified but not found: {package=}")
1364
+ return packages
1365
+
1366
+
1367
+ def _validate_installation_scripts_or_raise(
1368
+ *,
1369
+ script_paths: Sequence[str],
1370
+ packages: Sequence[str],
1371
+ ) -> None:
1372
+ """Validates the installation scripts' path explicitly provided by the user.
1373
+
1374
+ Args:
1375
+ script_paths (Sequence[str]):
1376
+ Required. The paths to the installation scripts.
1377
+ packages (Sequence[str]):
1378
+ Required. The user-provided packages.
1379
+
1380
+ Raises:
1381
+ ValueError: If a user-defined script is not under the expected
1382
+ subdirectory, or not in `packages`, or if a package is
1383
+ in the installation scripts subdirectory, but is not specified as an
1384
+ installation script.
1385
+ """
1386
+ for script_path in script_paths:
1387
+ if not script_path.startswith(_INSTALLATION_SUBDIR):
1388
+ logger.warning(
1389
+ f"User-defined installation script '{script_path}' is not in "
1390
+ f"the expected '{_INSTALLATION_SUBDIR}' subdirectory. "
1391
+ f"Ensure it is placed in '{_INSTALLATION_SUBDIR}' within your "
1392
+ f"'extra_packages' or 'source_packages'."
1393
+ )
1394
+ raise ValueError(
1395
+ f"Required installation script '{script_path}' "
1396
+ f"is not under '{_INSTALLATION_SUBDIR}'"
1397
+ )
1398
+
1399
+ if script_path not in packages:
1400
+ logger.warning(
1401
+ f"User-defined installation script '{script_path}' is not in "
1402
+ f"'extra_packages' or 'source_packages'. Ensure it is added to "
1403
+ f"'extra_packages' or 'source_packages'."
1404
+ )
1405
+ raise ValueError(
1406
+ f"User-defined installation script '{script_path}' "
1407
+ f"does not exist in 'extra_packages' or 'source_packages'."
1408
+ )
1409
+
1410
+ for package in packages:
1411
+ if package.startswith(_INSTALLATION_SUBDIR) and package not in script_paths:
1412
+ logger.warning(
1413
+ f"Package '{package}' is in the installation "
1414
+ "scripts subdirectory, but is not specified as an installation "
1415
+ "script in `build_options`. "
1416
+ "Ensure it is added to installation_scripts for "
1417
+ "automatic execution."
1418
+ )
1419
+ raise ValueError(
1420
+ f"Package '{package}' is in the installation "
1421
+ "scripts subdirectory, but is not specified as an installation "
1422
+ "script in `build_options`."
1423
+ )
1424
+ return
1425
+
1426
+
1427
+ def _validate_staging_bucket_or_raise(*, staging_bucket: str) -> str:
1428
+ """Tries to validate the staging bucket."""
1429
+ if not staging_bucket:
1430
+ raise ValueError(
1431
+ "Please provide a `staging_bucket` in `client.agent_engines.create(...)`."
1432
+ )
1433
+ if not staging_bucket.startswith("gs://"):
1434
+ raise ValueError(f"{staging_bucket=} must start with `gs://`")
1435
+ return staging_bucket
1436
+
1437
+
1438
+ def _validate_requirements_or_warn(
1439
+ *,
1440
+ obj: Any,
1441
+ requirements: List[str],
1442
+ ) -> List[str]:
1443
+ """Compiles the requirements into a list of requirements."""
1444
+ requirements = requirements.copy()
1445
+ try:
1446
+ current_requirements = _scan_requirements(obj=obj)
1447
+ logger.info(f"Identified the following requirements: {current_requirements}")
1448
+ constraints = _parse_constraints(constraints=requirements)
1449
+ missing_requirements = _compare_requirements(
1450
+ requirements=current_requirements,
1451
+ constraints=constraints,
1452
+ )
1453
+ for warning_type, warnings in missing_requirements["warnings"].items():
1454
+ if warnings:
1455
+ logger.warning(
1456
+ f"The following requirements are {warning_type}: {warnings}"
1457
+ )
1458
+ for action_type, actions in missing_requirements["actions"].items():
1459
+ if actions and action_type == _ACTION_APPEND:
1460
+ for action in actions: # type: ignore[attr-defined]
1461
+ requirements.append(action)
1462
+ logger.info(f"The following requirements are appended: {actions}")
1463
+ except Exception as e:
1464
+ logger.warning(f"Failed to compile requirements: {e}")
1465
+ return requirements
1466
+
1467
+
1468
+ def _validate_requirements_or_raise(
1469
+ *,
1470
+ agent: Any,
1471
+ requirements: Optional[Sequence[str]] = None,
1472
+ ) -> Sequence[str]:
1473
+ """Tries to validate the requirements."""
1474
+ if requirements is None:
1475
+ requirements = []
1476
+ elif isinstance(requirements, str):
1477
+ try:
1478
+ logger.info(f"Reading requirements from {requirements=}")
1479
+ with open(requirements) as f:
1480
+ requirements = f.read().splitlines()
1481
+ logger.info(f"Read the following lines: {requirements}")
1482
+ except IOError as err:
1483
+ raise IOError(f"Failed to read requirements from {requirements=}") from err
1484
+ requirements = _validate_requirements_or_warn(
1485
+ obj=agent,
1486
+ requirements=requirements,
1487
+ )
1488
+ logger.info(f"The final list of requirements: {requirements}")
1489
+ return requirements
1490
+
1491
+
1492
+ def _validate_agent_or_raise(
1493
+ *,
1494
+ agent: _AgentEngineInterface,
1495
+ ) -> _AgentEngineInterface:
1496
+ """Tries to validate the agent engine.
1497
+
1498
+ The agent engine must have one of the following:
1499
+ * a callable method named `query`
1500
+ * a callable method named `stream_query`
1501
+ * a callable method named `async_stream_query`
1502
+ * a callable method named `bidi_stream_query`
1503
+ * a callable method named `register_operations`
1504
+
1505
+ Args:
1506
+ agent: The agent to be validated.
1507
+
1508
+ Returns:
1509
+ The validated agent engine.
1510
+
1511
+ Raises:
1512
+ TypeError: If `agent_engine` has no callable method named `query`,
1513
+ `stream_query` or `register_operations`.
1514
+ ValueError: If `agent_engine` has an invalid `query`, `stream_query` or
1515
+ `register_operations` signature.
1516
+ """
1517
+ try:
1518
+ from google.adk.agents import BaseAgent
1519
+
1520
+ if isinstance(agent, BaseAgent):
1521
+ logger.info("Deploying google.adk.agents.Agent as an application.")
1522
+ from agentplatform import agent_engines
1523
+
1524
+ agent = agent_engines.AdkApp(agent=agent)
1525
+ except Exception:
1526
+ pass
1527
+ is_queryable = isinstance(agent, Queryable) and callable(agent.query)
1528
+ is_async_queryable = isinstance(agent, AsyncQueryable) and callable(
1529
+ agent.async_query
1530
+ )
1531
+ is_stream_queryable = isinstance(agent, StreamQueryable) and callable(
1532
+ agent.stream_query
1533
+ )
1534
+ is_async_stream_queryable = isinstance(agent, AsyncStreamQueryable) and callable(
1535
+ agent.async_stream_query
1536
+ )
1537
+ is_bidi_stream_queryable = isinstance(agent, BidiStreamQueryable) and callable(
1538
+ agent.bidi_stream_query
1539
+ )
1540
+ is_operation_registrable = isinstance(agent, OperationRegistrable) and callable(
1541
+ agent.register_operations
1542
+ )
1543
+
1544
+ if not (
1545
+ is_queryable
1546
+ or is_async_queryable
1547
+ or is_stream_queryable
1548
+ or is_operation_registrable
1549
+ or is_bidi_stream_queryable
1550
+ or is_async_stream_queryable
1551
+ ):
1552
+ raise TypeError(
1553
+ "agent_engine has none of the following callable methods: "
1554
+ "`query`, `async_query`, `stream_query`, `async_stream_query`, "
1555
+ "`bidi_stream_query`, or `register_operations`."
1556
+ )
1557
+
1558
+ if is_queryable:
1559
+ try:
1560
+ inspect.signature(getattr(agent, "query"))
1561
+ except ValueError as err:
1562
+ raise ValueError(
1563
+ "Invalid query signature. This might be due to a missing "
1564
+ "`self` argument in the agent.query method."
1565
+ ) from err
1566
+
1567
+ if is_async_queryable:
1568
+ try:
1569
+ inspect.signature(getattr(agent, "async_query"))
1570
+ except ValueError as err:
1571
+ raise ValueError(
1572
+ "Invalid async_query signature. This might be due to a missing "
1573
+ "`self` argument in the agent.async_query method."
1574
+ ) from err
1575
+
1576
+ if is_stream_queryable:
1577
+ try:
1578
+ inspect.signature(getattr(agent, "stream_query"))
1579
+ except ValueError as err:
1580
+ raise ValueError(
1581
+ "Invalid stream_query signature. This might be due to a missing"
1582
+ " `self` argument in the agent.stream_query method."
1583
+ ) from err
1584
+
1585
+ if is_async_stream_queryable:
1586
+ try:
1587
+ inspect.signature(getattr(agent, "async_stream_query"))
1588
+ except ValueError as err:
1589
+ raise ValueError(
1590
+ "Invalid async_stream_query signature. This might be due to a "
1591
+ " missing `self` argument in the agent.async_stream_query method."
1592
+ ) from err
1593
+
1594
+ if is_bidi_stream_queryable:
1595
+ try:
1596
+ inspect.signature(getattr(agent, "bidi_stream_query"))
1597
+ except ValueError as err:
1598
+ raise ValueError(
1599
+ "Invalid bidi_stream_query signature. This might be due to a "
1600
+ " missing `self` argument in the agent.bidi_stream_query method."
1601
+ ) from err
1602
+
1603
+ if is_operation_registrable:
1604
+ try:
1605
+ inspect.signature(getattr(agent, "register_operations"))
1606
+ except ValueError as err:
1607
+ raise ValueError(
1608
+ "Invalid register_operations signature. This might be due to a "
1609
+ "missing `self` argument in the agent.register_operations method."
1610
+ ) from err
1611
+
1612
+ if isinstance(agent, Cloneable):
1613
+ # Avoid undeployable states.
1614
+ agent = agent.clone()
1615
+ return agent
1616
+
1617
+
1618
+ def _wrap_agent_operation(*, agent: Any, operation: str) -> Callable[..., Any]:
1619
+ """Wraps an agent operation into a method (works for all API modes)."""
1620
+
1621
+ def _method(self, **kwargs) -> Any: # type: ignore[no-untyped-def]
1622
+ if not self._tmpl_attrs.get("agent"):
1623
+ self.set_up()
1624
+ return getattr(self._tmpl_attrs["agent"], operation)(**kwargs)
1625
+
1626
+ _method.__name__ = operation
1627
+ _method.__doc__ = getattr(agent, operation).__doc__
1628
+ return _method
1629
+
1630
+
1631
+ def _wrap_query_operation(*, method_name: str) -> Callable[..., Any]:
1632
+ """Wraps an Agent Engine method, creating a callable for `query` API.
1633
+
1634
+ This function creates a callable object that executes the specified
1635
+ Agent Engine method using the `query` API. It handles the creation of
1636
+ the API request and the processing of the API response.
1637
+
1638
+ The reserved keyword argument `http_options` is consumed by this
1639
+ wrapper (rather than being forwarded to the deployed agent as part of
1640
+ `input`) and is propagated to the underlying HTTP call. Use it to set
1641
+ per-call HTTP options such as custom headers. For example:
1642
+
1643
+ from google.genai.types import HttpOptions
1644
+
1645
+ agent_engine.query(
1646
+ input="hello",
1647
+ http_options=HttpOptions(headers={"x-my-header": "value"}),
1648
+ )
1649
+
1650
+ Args:
1651
+ method_name: The name of the Agent Engine method to call.
1652
+ doc: Documentation string for the method.
1653
+
1654
+ Returns:
1655
+ A callable object that executes the method on the Agent Engine via
1656
+ the `query` API.
1657
+ """
1658
+
1659
+ def _method(self: genai_types.AgentEngine, **kwargs) -> Any: # type: ignore[no-untyped-def]
1660
+ if not self.api_client:
1661
+ raise ValueError("api_client is not initialized.")
1662
+ if not self.api_resource:
1663
+ raise ValueError("api_resource is not initialized.")
1664
+ http_options = kwargs.pop("http_options", None)
1665
+ response = self.api_client._query(
1666
+ name=self.api_resource.name,
1667
+ config={
1668
+ "class_method": method_name,
1669
+ "input": kwargs,
1670
+ "include_all_fields": True,
1671
+ "http_options": http_options,
1672
+ },
1673
+ )
1674
+ return response.output
1675
+
1676
+ return _method
1677
+
1678
+
1679
+ def _wrap_async_query_operation(
1680
+ *, method_name: str
1681
+ ) -> Callable[..., Coroutine[Any, Any, Any]]:
1682
+ """Wraps an Agent Engine method, creating an async callable for `query` API.
1683
+
1684
+ This function creates a callable object that executes the specified
1685
+ Agent Engine method asynchronously using the `query` API. It handles the
1686
+ creation of the API request and the processing of the API response.
1687
+
1688
+ The reserved keyword argument `http_options` is consumed by this
1689
+ wrapper (rather than being forwarded to the deployed agent as part of
1690
+ `input`) and is propagated to the underlying HTTP call.
1691
+
1692
+ Args:
1693
+ method_name: The name of the Agent Engine method to call.
1694
+ doc: Documentation string for the method.
1695
+
1696
+ Returns:
1697
+ A callable object that executes the method on the Agent Engine via
1698
+ the `query` API.
1699
+ """
1700
+
1701
+ async def _method(
1702
+ self: genai_types.AgentEngine, **kwargs: Any
1703
+ ) -> Union[Coroutine[Any, Any, Any], Any]:
1704
+ if not self.api_async_client:
1705
+ raise ValueError("api_async_client is not initialized.")
1706
+ if not self.api_resource:
1707
+ raise ValueError("api_resource is not initialized.")
1708
+ http_options = kwargs.pop("http_options", None)
1709
+ response = await self.api_async_client._query(
1710
+ name=self.api_resource.name,
1711
+ config={
1712
+ "class_method": method_name,
1713
+ "input": kwargs,
1714
+ "include_all_fields": True,
1715
+ "http_options": http_options,
1716
+ },
1717
+ )
1718
+ return response.output
1719
+
1720
+ return _method
1721
+
1722
+
1723
+ def _wrap_stream_query_operation(*, method_name: str) -> Callable[..., Iterator[Any]]:
1724
+ """Wraps an Agent Engine method, creating a callable for `stream_query` API.
1725
+
1726
+ This function creates a callable object that executes the specified
1727
+ Agent Engine method using the `stream_query` API. It handles the
1728
+ creation of the API request and the processing of the API response.
1729
+
1730
+ The reserved keyword argument `http_options` is consumed by this
1731
+ wrapper (rather than being forwarded to the deployed agent as part of
1732
+ `input`) and is propagated to the underlying HTTP call.
1733
+
1734
+ Args:
1735
+ method_name: The name of the Agent Engine method to call.
1736
+ doc: Documentation string for the method.
1737
+
1738
+ Returns:
1739
+ A callable object that executes the method on the Agent Engine via
1740
+ the `stream_query` API.
1741
+ """
1742
+
1743
+ def _method(self: genai_types.AgentEngine, **kwargs) -> Iterator[Any]: # type: ignore[no-untyped-def]
1744
+ if not self.api_client:
1745
+ raise ValueError("api_client is not initialized.")
1746
+ if not self.api_resource:
1747
+ raise ValueError("api_resource is not initialized.")
1748
+ http_options = kwargs.pop("http_options", None)
1749
+ for http_response in self.api_client._stream_query(
1750
+ name=self.api_resource.name,
1751
+ config={
1752
+ "class_method": method_name,
1753
+ "input": kwargs,
1754
+ "include_all_fields": True,
1755
+ "http_options": http_options,
1756
+ },
1757
+ ):
1758
+ for line in _yield_parsed_json(http_response=http_response):
1759
+ if line is not None:
1760
+ yield line
1761
+
1762
+ return _method
1763
+
1764
+
1765
+ def _wrap_async_stream_query_operation(
1766
+ *, method_name: str
1767
+ ) -> Callable[..., AsyncIterator[Any]]:
1768
+ """Wraps an Agent Engine method, creating an async callable for `stream_query` API.
1769
+
1770
+ This function creates a callable object that executes the specified
1771
+ Agent Engine method using the `stream_query` API. It handles the
1772
+ creation of the API request and the processing of the API response.
1773
+
1774
+ The reserved keyword argument `http_options` is consumed by this
1775
+ wrapper (rather than being forwarded to the deployed agent as part of
1776
+ `input`) and is propagated to the underlying HTTP call.
1777
+
1778
+ Args:
1779
+ method_name: The name of the Agent Engine method to call.
1780
+ doc: Documentation string for the method.
1781
+
1782
+ Returns:
1783
+ A callable object that executes the method on the Agent Engine via
1784
+ the `stream_query` API.
1785
+ """
1786
+
1787
+ async def _method(self: genai_types.AgentEngine, **kwargs) -> AsyncIterator[Any]: # type: ignore[no-untyped-def]
1788
+ if not self.api_client:
1789
+ raise ValueError("api_client is not initialized.")
1790
+ if not self.api_resource:
1791
+ raise ValueError("api_resource is not initialized.")
1792
+ http_options = kwargs.pop("http_options", None)
1793
+ async for http_response in self.api_client._async_stream_query(
1794
+ name=self.api_resource.name,
1795
+ config={
1796
+ "class_method": method_name,
1797
+ "input": kwargs,
1798
+ "include_all_fields": True,
1799
+ "http_options": http_options,
1800
+ },
1801
+ ):
1802
+ for line in _yield_parsed_json(http_response=http_response):
1803
+ if line is not None:
1804
+ yield line
1805
+
1806
+ return _method
1807
+
1808
+
1809
+ def _wrap_a2a_operation(method_name: str, agent_card: str) -> Callable[..., list[Any]]:
1810
+ """Wraps an Agent Engine method, creating a callable for A2A API.
1811
+
1812
+ Args:
1813
+ method_name: The name of the Agent Engine method to call.
1814
+ agent_card: The agent card to use for the A2A API call.
1815
+ Example: { 'name': 'Sample Agent', 'description': ( 'A helpful
1816
+ assistant agent that can answer questions.' ),
1817
+ 'supportedInterfaces': [{ 'url': 'http://localhost:8080/a2a/rest/',
1818
+ 'protocolBinding': 'HTTP+JSON', 'protocolVersion': '1.0', }],
1819
+ 'version': '1.0.0', 'capabilities': { 'streaming': True,
1820
+ 'pushNotifications': False, 'extendedAgentCard': True, },
1821
+ 'defaultInputModes': ['text'], 'defaultOutputModes': ['text'],
1822
+ 'skills': [{ 'id': 'question_answer', 'name': 'Q&A Agent',
1823
+ 'description': ( 'A helpful assistant agent that can answer
1824
+ questions.' ), 'tags': ['Question-Answer'], 'examples': [ 'Who is
1825
+ leading 2025 F1 Standings?', 'Where can i find an active volcano?',
1826
+ ], 'inputModes': ['text'], 'outputModes': ['text'], }], }
1827
+
1828
+ Returns:
1829
+ A callable object that executes the method on the Agent Engine via
1830
+ the A2A API.
1831
+ """
1832
+
1833
+ async def _method(self, **kwargs) -> Any: # type: ignore[no-untyped-def]
1834
+ if not self.api_client:
1835
+ raise ValueError("api_client is not initialized.")
1836
+ if not self.api_resource:
1837
+ raise ValueError("api_resource is not initialized.")
1838
+
1839
+ a2a_agent_card = AgentCard()
1840
+ json_format.ParseDict(
1841
+ json.loads(agent_card), a2a_agent_card, ignore_unknown_fields=True
1842
+ )
1843
+
1844
+ if a2a_agent_card.supported_interfaces:
1845
+ interface = a2a_agent_card.supported_interfaces[0]
1846
+ if interface.protocol_binding != TransportProtocol.HTTP_JSON:
1847
+ raise ValueError(
1848
+ "Only HTTP+JSON is supported for preferred transport on agent card"
1849
+ )
1850
+ else:
1851
+ raise ValueError("Agent card does not define any supported interfaces.")
1852
+
1853
+ base_url = self.api_client._api_client._http_options.base_url.rstrip("/")
1854
+ api_version = self.api_client._api_client._http_options.api_version
1855
+ a2a_agent_card.supported_interfaces[0].url = (
1856
+ f"{base_url}/{api_version}/{self.api_resource.name}/a2a"
1857
+ )
1858
+
1859
+ config = ClientConfig(
1860
+ supported_protocol_bindings=[
1861
+ TransportProtocol.HTTP_JSON,
1862
+ ],
1863
+ use_client_preference=True,
1864
+ httpx_client=httpx.AsyncClient(
1865
+ headers={
1866
+ "Authorization": (
1867
+ f"Bearer {self.api_client._api_client._credentials.token}"
1868
+ )
1869
+ },
1870
+ timeout=(
1871
+ self.api_client._api_client._http_options.timeout / 1000.0
1872
+ if self.api_client._api_client._http_options.timeout
1873
+ else None
1874
+ ),
1875
+ ),
1876
+ )
1877
+ factory = ClientFactory(config)
1878
+ client = factory.create(a2a_agent_card)
1879
+
1880
+ context = kwargs.pop("context", None)
1881
+ if context is not None:
1882
+ from a2a.client.client import ClientCallContext
1883
+
1884
+ if not isinstance(context, ClientCallContext):
1885
+ actual_context = ClientCallContext()
1886
+ if hasattr(context, "state"):
1887
+ actual_context.state = context.state
1888
+ elif isinstance(context, dict):
1889
+ actual_context.state = context
1890
+ context = actual_context
1891
+
1892
+ req = kwargs["request"]
1893
+ if method_name == "on_message_send":
1894
+ response = client.send_message(req, context=context)
1895
+ chunks = []
1896
+ async for chunk in response:
1897
+ chunks.append(chunk)
1898
+ return chunks
1899
+ elif method_name == "on_get_task":
1900
+ return await client.get_task(req, context=context)
1901
+ elif method_name == "on_cancel_task":
1902
+ return await client.cancel_task(req, context=context)
1903
+ elif method_name == "on_get_extended_agent_card":
1904
+ return await client.get_extended_agent_card(req, context=context)
1905
+ else:
1906
+ raise ValueError(f"Unknown method name: {method_name}")
1907
+
1908
+ return _method # type: ignore[return-value]
1909
+
1910
+
1911
+ _SSE_DATA_PREFIX = "data:"
1912
+ _STREAMABLE_CONTENT_TYPES = ("application/json", "text/event-stream")
1913
+
1914
+
1915
+ def _strip_sse_framing(line: str) -> str:
1916
+ """Returns the payload of a Server-Sent Events `data:` line.
1917
+
1918
+ Streaming responses are newline-delimited JSON. A response may instead
1919
+ arrive as Server-Sent Events, in which case each JSON object is wrapped in a
1920
+ `data:` frame; removing that framing here lets both shapes be parsed the
1921
+ same way
1922
+ (https://github.com/googleapis/python-aiplatform/issues/5586).
1923
+
1924
+ A serialized JSON value never begins with `data:` -- it begins with `{`,
1925
+ `[`, `"`, a digit, or one of `true`/`false`/`null` -- so stripping the
1926
+ prefix unconditionally cannot corrupt a newline-delimited JSON response. A
1927
+ chunk whose value is the string `data: hello` is serialized as
1928
+ `"data: hello"`, with the quote first.
1929
+
1930
+ Args:
1931
+ line: A single line of the response body.
1932
+
1933
+ Returns:
1934
+ The line with any SSE `data:` framing removed.
1935
+ """
1936
+ line = line.rstrip("\r")
1937
+ if not line.startswith(_SSE_DATA_PREFIX):
1938
+ return line
1939
+ # The single space after the colon is optional per the SSE specification.
1940
+ payload = line[len(_SSE_DATA_PREFIX) :]
1941
+ return payload[1:] if payload.startswith(" ") else payload
1942
+
1943
+
1944
+ def _yield_parsed_json(http_response: google_genai_types.HttpResponse) -> Iterator[Any]:
1945
+ """Converts the body of the HTTP Response message to JSON format.
1946
+
1947
+ Args:
1948
+ http_response (google.genai.types.HttpResponse):
1949
+ Required. The httpbody body to be converted to JSON object(s).
1950
+
1951
+ Yields:
1952
+ Any: A JSON object or line of the original body or None.
1953
+ """
1954
+ if not http_response.body:
1955
+ yield None
1956
+ return
1957
+
1958
+ # Handle the case of multiple dictionaries delimited by newlines.
1959
+ for line in http_response.body.split("\n"):
1960
+ # Strip before the emptiness check so the blank line that terminates an
1961
+ # SSE frame, and a `data:` line with an empty payload, are both skipped.
1962
+ line = _strip_sse_framing(line)
1963
+ if line:
1964
+ try:
1965
+ line = json.loads(line)
1966
+ except Exception as e:
1967
+ logger.warning(f"failed to parse json: {line}. Exception: {e}")
1968
+ yield line
1969
+
1970
+
1971
+ def _yield_parsed_json_from_httpbody(body: httpbody_pb2.HttpBody) -> Iterator[Any]:
1972
+ """Converts the contents of an `HttpBody` proto message to JSON format.
1973
+
1974
+ Unlike `_yield_parsed_json`, which parses a `google.genai.types.HttpResponse`
1975
+ (with a `body` attribute), this helper parses the gRPC `httpbody_pb2.HttpBody`
1976
+ protos yielded by `stream_query_reasoning_engine`, which expose
1977
+ `content_type` and `data` instead.
1978
+
1979
+ Args:
1980
+ body (httpbody_pb2.HttpBody):
1981
+ Required. The httpbody proto to be converted to JSON object(s).
1982
+
1983
+ Yields:
1984
+ Any: A JSON object, a line of the original body, or the original body if
1985
+ it is not JSON, or None.
1986
+ """
1987
+ content_type = getattr(body, "content_type", None)
1988
+ data = getattr(body, "data", None)
1989
+
1990
+ if (
1991
+ content_type is None
1992
+ or data is None
1993
+ or not any(t in content_type for t in _STREAMABLE_CONTENT_TYPES)
1994
+ ):
1995
+ yield body
1996
+ return
1997
+
1998
+ try:
1999
+ utf8_data = data.decode("utf-8")
2000
+ except Exception as e:
2001
+ logger.warning(f"Failed to decode data: {data!r}. Exception: {e}")
2002
+ yield body
2003
+ return
2004
+
2005
+ if not utf8_data:
2006
+ yield None
2007
+ return
2008
+
2009
+ # Handle the case of multiple dictionaries delimited by newlines.
2010
+ for line in utf8_data.split("\n"):
2011
+ # Strip before the emptiness check so the blank line that terminates an
2012
+ # SSE frame, and a `data:` line with an empty payload, are both skipped.
2013
+ line = _strip_sse_framing(line)
2014
+ if line:
2015
+ try:
2016
+ line = json.loads(line)
2017
+ except Exception as e:
2018
+ logger.warning(f"failed to parse json: {line}. Exception: {e}")
2019
+ yield line
2020
+
2021
+
2022
+ def _validate_resource_limits_or_raise(resource_limits: dict[str, str]) -> None:
2023
+ """Validates the resource limits.
2024
+
2025
+ Checks that the resource limits are a dict with 'cpu' and 'memory' keys.
2026
+ Checks that the 'cpu' value is one of 1, 2, 4, 6, 8.
2027
+ Checks that the 'memory' value is a string ending with 'Gi'.
2028
+ Checks that the memory size is smaller than 32Gi.
2029
+ Checks that the memory size requires at least the specified number of CPUs.
2030
+
2031
+ Args:
2032
+ resource_limits: The resource limits to be validated.
2033
+
2034
+ Raises:
2035
+ TypeError: If the resource limits are not a dict.
2036
+ KeyError: If the resource limits do not contain 'cpu' and 'memory' keys.
2037
+ ValueError: If the 'cpu' value is not one of 1, 2, 4, 6, 8.
2038
+ ValueError: If the 'memory' value is not a string ending with 'Gi'.
2039
+ ValueError: If the memory size is too large.
2040
+ ValueError: If the memory size requires more CPUs than the specified
2041
+ 'cpu' value.
2042
+ """
2043
+ if not isinstance(resource_limits, dict):
2044
+ raise TypeError(f"resource_limits must be a dict. Got {type(resource_limits)}")
2045
+ if "cpu" not in resource_limits or "memory" not in resource_limits:
2046
+ raise KeyError("resource_limits must contain 'cpu' and 'memory' keys.")
2047
+
2048
+ cpu = int(resource_limits["cpu"])
2049
+ memory_str = resource_limits["memory"]
2050
+
2051
+ if cpu not in [1, 2, 4, 6, 8]:
2052
+ raise ValueError(
2053
+ "resource_limits['cpu'] must be one of 1, 2, 4, 6, 8. Got" f" {cpu}"
2054
+ )
2055
+
2056
+ if not isinstance(memory_str, str) or not memory_str.endswith("Gi"):
2057
+ raise ValueError(
2058
+ "resource_limits['memory'] must be a string ending with 'Gi'."
2059
+ f" Got {memory_str}"
2060
+ )
2061
+
2062
+ try:
2063
+ memory_gb = int(memory_str[:-2])
2064
+ except ValueError:
2065
+ raise ValueError(
2066
+ f"Invalid memory value: {memory_str}. Must be an integer"
2067
+ " followed by 'Gi'."
2068
+ )
2069
+
2070
+ # https://cloud.google.com/run/docs/configuring/memory-limits
2071
+ if memory_gb > 32:
2072
+ raise ValueError(
2073
+ f"Memory size of {memory_str} is too large. Must be smaller than 32Gi."
2074
+ )
2075
+ if memory_gb > 24:
2076
+ min_cpu = 8
2077
+ elif memory_gb > 16:
2078
+ min_cpu = 6
2079
+ elif memory_gb > 8:
2080
+ min_cpu = 4
2081
+ elif memory_gb > 4:
2082
+ min_cpu = 2
2083
+ else:
2084
+ min_cpu = 1
2085
+
2086
+ if cpu < min_cpu:
2087
+ raise ValueError(
2088
+ f"Memory size of {memory_str} requires at least {min_cpu} CPUs."
2089
+ f" Got {cpu}"
2090
+ )
2091
+
2092
+
2093
+ def _is_adk_agent(agent_engine: _AgentEngineInterface) -> bool:
2094
+ """Checks if the agent engine is an ADK agent.
2095
+
2096
+ Args:
2097
+ agent_engine: The agent engine to check.
2098
+
2099
+ Returns:
2100
+ True if the agent engine is an ADK agent, False otherwise.
2101
+ """
2102
+
2103
+ from agentplatform.agent_engines.templates import adk
2104
+
2105
+ return isinstance(agent_engine, adk.AdkApp)
2106
+
2107
+
2108
+ def _add_telemetry_enablement_env(
2109
+ env_vars: Optional[Dict[str, Union[str, Any]]]
2110
+ ) -> Optional[Dict[str, Union[str, Any]]]:
2111
+ """Adds telemetry enablement env var to the env vars.
2112
+
2113
+ This is in order to achieve default-on telemetry.
2114
+ If the telemetry enablement env var is already set, we do not override it.
2115
+
2116
+ Args:
2117
+ env_vars: The env vars to add the telemetry enablement env var to.
2118
+
2119
+ Returns:
2120
+ The env vars with the telemetry enablement env var added.
2121
+ """
2122
+
2123
+ GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY = (
2124
+ "GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY"
2125
+ )
2126
+ env_to_add = {GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY: "unspecified"}
2127
+
2128
+ if env_vars is None:
2129
+ return env_to_add
2130
+
2131
+ if not isinstance(env_vars, dict):
2132
+ raise TypeError(f"env_vars must be a dict, but got {type(env_vars)}.")
2133
+
2134
+ if GOOGLE_CLOUD_AGENT_ENGINE_ENABLE_TELEMETRY in env_vars:
2135
+ return env_vars
2136
+
2137
+ return env_vars | env_to_add
2138
+
2139
+
2140
+ def _dataclass_to_dict_or_raise(obj: Any) -> Dict[str, Any]:
2141
+ """Converts a dataclass to a JSON dictionary."""
2142
+ if not dataclasses.is_dataclass(obj):
2143
+ raise TypeError(f"Object is not a dataclass: {obj}")
2144
+ return json.loads(json.dumps(dataclasses.asdict(obj)))
2145
+
2146
+
2147
+ def _autogen_run_response_protocol_to_dict(
2148
+ obj: AutogenRunResponse,
2149
+ ) -> Dict[str, Any]:
2150
+ """Converts an AutogenRunResponse object into a JSON-serializable dictionary."""
2151
+ if hasattr(obj, "process"):
2152
+ obj.process()
2153
+ last_speaker = None
2154
+ if getattr(obj, "last_speaker", None) is not None:
2155
+ last_speaker = {
2156
+ "name": getattr(obj.last_speaker, "name", None),
2157
+ "description": getattr(obj.last_speaker, "description", None),
2158
+ }
2159
+ cost = None
2160
+ if getattr(obj, "cost", None) is not None:
2161
+ if hasattr(obj.cost, "model_dump_json"):
2162
+ cost = json.loads(obj.cost.model_dump_json())
2163
+ else:
2164
+ cost = str(obj.cost)
2165
+ result = {
2166
+ "summary": getattr(obj, "summary", None),
2167
+ "messages": list(getattr(obj, "messages", [])),
2168
+ "context_variables": getattr(obj, "context_variables", None),
2169
+ "last_speaker": last_speaker,
2170
+ "cost": cost,
2171
+ }
2172
+ return json.loads(json.dumps(result))
2173
+
2174
+
2175
+ def to_json_serializable_autogen_object(
2176
+ obj: Union[
2177
+ AutogenChatResult,
2178
+ AutogenRunResponse,
2179
+ ],
2180
+ ) -> Dict[str, Any]:
2181
+ """Converts an Autogen object to a JSON serializable object."""
2182
+ if isinstance(obj, AutogenChatResult):
2183
+ return _dataclass_to_dict_or_raise(obj)
2184
+ return _autogen_run_response_protocol_to_dict(obj)
2185
+
2186
+
2187
+ def _llama_index_response_to_dict(obj: LlamaIndexResponse) -> Any:
2188
+ response = {}
2189
+ if hasattr(obj, "response"):
2190
+ response["response"] = obj.response
2191
+ if hasattr(obj, "source_nodes"):
2192
+ response["source_nodes"] = [node.model_dump_json() for node in obj.source_nodes]
2193
+ if hasattr(obj, "metadata"):
2194
+ response["metadata"] = obj.metadata
2195
+ return json.loads(json.dumps(response))
2196
+
2197
+
2198
+ def _llama_index_chat_response_to_dict(obj: LlamaIndexChatResponse) -> Any:
2199
+ return json.loads(obj.message.model_dump_json())
2200
+
2201
+
2202
+ def _llama_index_base_model_to_dict(obj: LlamaIndexBaseModel) -> Any:
2203
+ return json.loads(obj.model_dump_json())
2204
+
2205
+
2206
+ def to_json_serializable_llama_index_object(
2207
+ obj: Union[
2208
+ LlamaIndexResponse,
2209
+ LlamaIndexBaseModel,
2210
+ LlamaIndexChatResponse,
2211
+ Sequence[LlamaIndexBaseModel],
2212
+ ],
2213
+ ) -> Union[str, Dict[str, Any], Sequence[Union[str, Dict[str, Any]]]]:
2214
+ """Converts a LlamaIndexResponse to a JSON serializable object."""
2215
+ if isinstance(obj, LlamaIndexResponse):
2216
+ return _llama_index_response_to_dict(obj)
2217
+ if isinstance(obj, LlamaIndexChatResponse):
2218
+ return _llama_index_chat_response_to_dict(obj)
2219
+ if isinstance(obj, Sequence):
2220
+ seq_result = []
2221
+ for item in obj:
2222
+ if isinstance(item, LlamaIndexBaseModel):
2223
+ seq_result.append(_llama_index_base_model_to_dict(item))
2224
+ continue
2225
+ seq_result.append(str(item))
2226
+ return seq_result
2227
+ if isinstance(obj, LlamaIndexBaseModel):
2228
+ return _llama_index_base_model_to_dict(obj)
2229
+ return str(obj)
2230
+
2231
+
2232
+ def is_noop_or_proxy_tracer_provider(tracer_provider) -> bool:
2233
+ """Returns True if the tracer_provider is Proxy or NoOp."""
2234
+ opentelemetry = _import_opentelemetry_or_warn()
2235
+ if not opentelemetry:
2236
+ return False
2237
+ ProxyTracerProvider = opentelemetry.trace.ProxyTracerProvider
2238
+ NoOpTracerProvider = opentelemetry.trace.NoOpTracerProvider
2239
+ return isinstance(tracer_provider, (NoOpTracerProvider, ProxyTracerProvider))
2240
+
2241
+
2242
+ def dump_event_for_json(event: BaseModel) -> Dict[str, Any]:
2243
+ """Dumps an ADK event to a JSON-serializable dictionary."""
2244
+ return json.loads(event.model_dump_json(exclude_none=True))
2245
+
2246
+
2247
+ def _import_opentelemetry_or_warn() -> Optional[types.ModuleType]:
2248
+ """Tries to import the opentelemetry module."""
2249
+ try:
2250
+ import opentelemetry
2251
+
2252
+ return opentelemetry
2253
+ except ImportError:
2254
+ logger.warning(
2255
+ "opentelemetry-api is not installed. Please call "
2256
+ "'pip install google-cloud-aiplatform[agent_engines]'."
2257
+ )
2258
+ return None
2259
+
2260
+
2261
+ def _import_opentelemetry_sdk_trace_or_warn() -> Optional[types.ModuleType]:
2262
+ """Tries to import the opentelemetry.sdk.trace module."""
2263
+ try:
2264
+ import opentelemetry.sdk.trace
2265
+
2266
+ return opentelemetry.sdk.trace
2267
+ except ImportError:
2268
+ logger.warning(
2269
+ "opentelemetry-sdk is not installed. Please call "
2270
+ "'pip install google-cloud-aiplatform[agent_engines]'."
2271
+ )
2272
+ return None
2273
+
2274
+
2275
+ def _import_openinference_langchain_or_warn() -> Optional[types.ModuleType]:
2276
+ """Tries to import the openinference.instrumentation.langchain module."""
2277
+ try:
2278
+ import openinference.instrumentation.langchain
2279
+
2280
+ return openinference.instrumentation.langchain
2281
+ except ImportError:
2282
+ logger.warning(
2283
+ "openinference-instrumentation-langchain is not installed. Please "
2284
+ "call 'pip install google-cloud-aiplatform[langchain]'."
2285
+ )
2286
+ return None
2287
+
2288
+
2289
+ def _import_openinference_autogen_or_warn() -> Optional[types.ModuleType]:
2290
+ """Tries to import the openinference.instrumentation.autogen module."""
2291
+ try:
2292
+ import openinference.instrumentation.autogen
2293
+
2294
+ return openinference.instrumentation.autogen
2295
+ except ImportError:
2296
+ logger.warning(
2297
+ "openinference-instrumentation-autogen is not installed. Please "
2298
+ "call 'pip install google-cloud-aiplatform[ag2]'."
2299
+ )
2300
+ return None
2301
+
2302
+
2303
+ def _import_openinference_llama_index_or_warn() -> Optional[types.ModuleType]:
2304
+ """Tries to import the openinference.instrumentation.llama_index module."""
2305
+ try:
2306
+ import openinference.instrumentation.llama_index # noqa:F401
2307
+
2308
+ return openinference.instrumentation.llama_index
2309
+ except ImportError:
2310
+ logger.warning(
2311
+ "openinference-instrumentation-llama_index is not installed. Please "
2312
+ "call 'pip install google-cloud-aiplatform[llama_index]'."
2313
+ )
2314
+ return None
2315
+
2316
+
2317
+ def _import_nest_asyncio_or_warn() -> Optional[types.ModuleType]:
2318
+ """Tries to import the nest_asyncio module."""
2319
+ try:
2320
+ import nest_asyncio
2321
+
2322
+ return nest_asyncio
2323
+ except ImportError:
2324
+ logger.warning(
2325
+ "nest_asyncio is not installed. Please call: `pip install nest-asyncio`"
2326
+ )
2327
+ return None
2328
+
2329
+
2330
+ def _import_autogen_tools_or_warn() -> Optional[types.ModuleType]:
2331
+ """Tries to import the autogen.tools module."""
2332
+ try:
2333
+ from autogen import tools
2334
+
2335
+ return tools
2336
+ except ImportError:
2337
+ logger.warning(
2338
+ "autogen.tools is not installed. Please "
2339
+ "call `pip install google-cloud-aiplatform[ag2]`."
2340
+ )
2341
+ return None