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.
- agentplatform/__init__.py +72 -0
- agentplatform/_genai/__init__.py +43 -0
- agentplatform/_genai/_agent_engines_utils.py +2341 -0
- agentplatform/_genai/_bigquery_utils.py +49 -0
- agentplatform/_genai/_datasets_utils.py +344 -0
- agentplatform/_genai/_evals_builtin_tools.py +209 -0
- agentplatform/_genai/_evals_common.py +4268 -0
- agentplatform/_genai/_evals_constant.py +122 -0
- agentplatform/_genai/_evals_data_converters.py +926 -0
- agentplatform/_genai/_evals_metric_handlers.py +1783 -0
- agentplatform/_genai/_evals_metric_loaders.py +401 -0
- agentplatform/_genai/_evals_utils.py +1043 -0
- agentplatform/_genai/_evals_visualization.py +2070 -0
- agentplatform/_genai/_gcs_utils.py +262 -0
- agentplatform/_genai/_logging_utils.py +47 -0
- agentplatform/_genai/_memory_bank_utils.py +206 -0
- agentplatform/_genai/_observability_data_converter.py +186 -0
- agentplatform/_genai/_operations_utils.py +94 -0
- agentplatform/_genai/_prompt_management_utils.py +147 -0
- agentplatform/_genai/_prompt_optimizer_utils.py +215 -0
- agentplatform/_genai/_skills_utils.py +69 -0
- agentplatform/_genai/_transformers.py +628 -0
- agentplatform/_genai/a2a_task_events.py +509 -0
- agentplatform/_genai/a2a_tasks.py +861 -0
- agentplatform/_genai/agent_engines.py +3931 -0
- agentplatform/_genai/client.py +519 -0
- agentplatform/_genai/datasets.py +3045 -0
- agentplatform/_genai/endpoints.py +1149 -0
- agentplatform/_genai/evals.py +6883 -0
- agentplatform/_genai/example_stores.py +1445 -0
- agentplatform/_genai/feedback_contexts.py +700 -0
- agentplatform/_genai/feedback_entries.py +1644 -0
- agentplatform/_genai/live.py +64 -0
- agentplatform/_genai/live_agent_engines.py +179 -0
- agentplatform/_genai/memories.py +2962 -0
- agentplatform/_genai/memory_banks.py +1927 -0
- agentplatform/_genai/memory_revisions.py +465 -0
- agentplatform/_genai/model_garden.py +2638 -0
- agentplatform/_genai/prompt_optimizer.py +995 -0
- agentplatform/_genai/prompts.py +4515 -0
- agentplatform/_genai/rag.py +4961 -0
- agentplatform/_genai/runtime_revisions.py +1257 -0
- agentplatform/_genai/runtimes.py +78 -0
- agentplatform/_genai/sandbox_snapshots.py +1015 -0
- agentplatform/_genai/sandbox_templates.py +1088 -0
- agentplatform/_genai/sandboxes.py +1604 -0
- agentplatform/_genai/session_events.py +543 -0
- agentplatform/_genai/sessions.py +1449 -0
- agentplatform/_genai/skill_revisions.py +377 -0
- agentplatform/_genai/skills.py +1708 -0
- agentplatform/_genai/types/__init__.py +4695 -0
- agentplatform/_genai/types/agent_engines.py +16 -0
- agentplatform/_genai/types/common.py +32784 -0
- agentplatform/_genai/types/evals.py +1031 -0
- agentplatform/_genai/types/prompt_optimizer.py +107 -0
- agentplatform/_genai/types/prompts.py +107 -0
- agentplatform/version.py +17 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/METADATA +79 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/RECORD +62 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/WHEEL +5 -0
- google_cloud_agentplatform-1.165.1.dev0.dist-info/licenses/LICENSE +202 -0
- 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
|