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,94 @@
|
|
|
1
|
+
# Copyright 2026 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 Operations."""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import datetime
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any, Awaitable, Callable
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def await_operation(
|
|
24
|
+
*,
|
|
25
|
+
operation_name: str,
|
|
26
|
+
get_operation_fn: Callable[..., Any],
|
|
27
|
+
poll_interval: datetime.timedelta | float = 10.0,
|
|
28
|
+
timeout_seconds: float = 300.0,
|
|
29
|
+
) -> Any:
|
|
30
|
+
"""Waits for a long running operation to complete.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
operation_name (str): Required. The name of the operation.
|
|
34
|
+
get_operation_fn (Callable): Required. Function to get the operation
|
|
35
|
+
status.
|
|
36
|
+
poll_interval (datetime.timedelta | float): The interval between polls.
|
|
37
|
+
timeout_seconds (float): The maximum wait duration in seconds.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Any: The completed operation.
|
|
41
|
+
"""
|
|
42
|
+
if isinstance(poll_interval, datetime.timedelta):
|
|
43
|
+
poll_seconds = poll_interval.total_seconds()
|
|
44
|
+
else:
|
|
45
|
+
poll_seconds = float(poll_interval)
|
|
46
|
+
|
|
47
|
+
start_time = time.time()
|
|
48
|
+
operation = get_operation_fn(operation_name=operation_name)
|
|
49
|
+
while not operation.done:
|
|
50
|
+
if (time.time() - start_time) > timeout_seconds:
|
|
51
|
+
raise TimeoutError(
|
|
52
|
+
f"Operation {operation_name} did not complete within the timeout "
|
|
53
|
+
f"of {timeout_seconds} seconds."
|
|
54
|
+
)
|
|
55
|
+
time.sleep(poll_seconds)
|
|
56
|
+
operation = get_operation_fn(operation_name=operation.name)
|
|
57
|
+
return operation
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def await_operation_async(
|
|
61
|
+
*,
|
|
62
|
+
operation_name: str,
|
|
63
|
+
get_operation_fn: Callable[..., Awaitable[Any]],
|
|
64
|
+
poll_interval: datetime.timedelta | float = 10.0,
|
|
65
|
+
timeout_seconds: float = 300.0,
|
|
66
|
+
) -> Any:
|
|
67
|
+
"""Waits for a long running operation to complete asynchronously.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
operation_name (str): Required. The name of the operation.
|
|
71
|
+
get_operation_fn (Callable): Required. Async function to get the operation
|
|
72
|
+
status.
|
|
73
|
+
poll_interval (datetime.timedelta | float): The interval between polls.
|
|
74
|
+
timeout_seconds (float): The maximum wait duration in seconds.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Any: The completed operation.
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(poll_interval, datetime.timedelta):
|
|
80
|
+
poll_seconds = poll_interval.total_seconds()
|
|
81
|
+
else:
|
|
82
|
+
poll_seconds = float(poll_interval)
|
|
83
|
+
|
|
84
|
+
start_time = time.time()
|
|
85
|
+
operation = await get_operation_fn(operation_name=operation_name)
|
|
86
|
+
while not operation.done:
|
|
87
|
+
if (time.time() - start_time) > timeout_seconds:
|
|
88
|
+
raise TimeoutError(
|
|
89
|
+
f"Operation {operation_name} did not complete within the timeout "
|
|
90
|
+
f"of {timeout_seconds} seconds."
|
|
91
|
+
)
|
|
92
|
+
await asyncio.sleep(poll_seconds)
|
|
93
|
+
operation = await get_operation_fn(operation_name=operation.name)
|
|
94
|
+
return operation
|
|
@@ -0,0 +1,147 @@
|
|
|
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 prompt management."""
|
|
16
|
+
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from google.genai import types as genai_types
|
|
20
|
+
|
|
21
|
+
from . import types
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
DEFAULT_API_SCHEMA_VERSION = "1.0.0"
|
|
25
|
+
PROMPT_SCHEMA_URI = (
|
|
26
|
+
"gs://google-cloud-aiplatform/schema/dataset/metadata/text_prompt_1.0.0.yaml"
|
|
27
|
+
)
|
|
28
|
+
PROMPT_TYPE = "multimodal_freeform"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _create_dataset_metadata_from_prompt(
|
|
32
|
+
prompt: types.Prompt,
|
|
33
|
+
variables: Optional[list[dict[str, genai_types.Part]]] = None,
|
|
34
|
+
) -> types.SchemaTextPromptDatasetMetadata:
|
|
35
|
+
"""Convert a types.Prompt into types.SchemaTextPromptDatasetMetadata."""
|
|
36
|
+
|
|
37
|
+
prompt_metadata = types.SchemaTextPromptDatasetMetadata()
|
|
38
|
+
|
|
39
|
+
prompt_api_schema = types.SchemaPromptApiSchema()
|
|
40
|
+
prompt_api_schema.multimodal_prompt = types.SchemaPromptSpecMultimodalPrompt(
|
|
41
|
+
prompt_message=prompt.prompt_data
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
prompt_api_schema.api_schema_version = DEFAULT_API_SCHEMA_VERSION
|
|
45
|
+
|
|
46
|
+
prompt_metadata.has_prompt_variable = bool(variables)
|
|
47
|
+
|
|
48
|
+
if variables:
|
|
49
|
+
prompt_execution_list = []
|
|
50
|
+
for prompt_var in variables:
|
|
51
|
+
prompt_instance_execution = types.SchemaPromptInstancePromptExecution()
|
|
52
|
+
prompt_instance_execution.arguments = {}
|
|
53
|
+
for key, val in prompt_var.items():
|
|
54
|
+
prompt_instance_execution.arguments[key] = (
|
|
55
|
+
types.SchemaPromptInstanceVariableValue(
|
|
56
|
+
part_list=types.SchemaPromptSpecPartList(parts=[val])
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
prompt_execution_list.append(prompt_instance_execution)
|
|
60
|
+
prompt_api_schema.executions = prompt_execution_list
|
|
61
|
+
|
|
62
|
+
# Need to exclude variables from the prompt message as it is a client side
|
|
63
|
+
# only field
|
|
64
|
+
if prompt_api_schema.multimodal_prompt.prompt_message:
|
|
65
|
+
prompt_message_dict = (
|
|
66
|
+
prompt_api_schema.multimodal_prompt.prompt_message.model_dump(
|
|
67
|
+
exclude=["variables"], exclude_none=True
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
prompt_api_schema.multimodal_prompt.prompt_message = (
|
|
71
|
+
types.SchemaPromptSpecPromptMessage(**prompt_message_dict)
|
|
72
|
+
)
|
|
73
|
+
prompt_metadata.prompt_api_schema = prompt_api_schema
|
|
74
|
+
|
|
75
|
+
prompt_metadata.prompt_type = PROMPT_TYPE
|
|
76
|
+
|
|
77
|
+
return prompt_metadata
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _create_prompt_from_dataset_metadata(
|
|
81
|
+
dataset: types.Dataset,
|
|
82
|
+
) -> types.Prompt:
|
|
83
|
+
"""Constructs a types.Prompt from a types.Dataset resource returned from the API.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
dataset: The types.Dataset object containing the prompt metadata.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
A types.Prompt object reconstructed from the dataset metadata.
|
|
90
|
+
"""
|
|
91
|
+
if (
|
|
92
|
+
not hasattr(dataset, "metadata")
|
|
93
|
+
or dataset.metadata is None
|
|
94
|
+
or not isinstance(dataset.metadata, types.SchemaTextPromptDatasetMetadata)
|
|
95
|
+
):
|
|
96
|
+
raise ValueError(
|
|
97
|
+
"Error retrieving prompt: prompt dataset resource is missing 'metadata'."
|
|
98
|
+
)
|
|
99
|
+
api_schema = dataset.metadata.prompt_api_schema
|
|
100
|
+
prompt = types.Prompt()
|
|
101
|
+
|
|
102
|
+
if api_schema is None:
|
|
103
|
+
return prompt
|
|
104
|
+
|
|
105
|
+
if api_schema.multimodal_prompt:
|
|
106
|
+
|
|
107
|
+
prompt_message = api_schema.multimodal_prompt.prompt_message
|
|
108
|
+
prompt.prompt_data = prompt_message
|
|
109
|
+
|
|
110
|
+
if api_schema.executions:
|
|
111
|
+
executions = api_schema.executions
|
|
112
|
+
if executions and prompt.prompt_data is not None:
|
|
113
|
+
prompt.prompt_data.variables = []
|
|
114
|
+
for execution in executions:
|
|
115
|
+
if execution.arguments:
|
|
116
|
+
args = execution.arguments
|
|
117
|
+
var_map = {}
|
|
118
|
+
for key, val in args.items():
|
|
119
|
+
if (
|
|
120
|
+
val.part_list is not None
|
|
121
|
+
and val.part_list.parts is not None
|
|
122
|
+
):
|
|
123
|
+
part_list = val.part_list.parts
|
|
124
|
+
if part_list and part_list[0].text:
|
|
125
|
+
var_map[key] = part_list[0]
|
|
126
|
+
if var_map and prompt.prompt_data.variables is not None:
|
|
127
|
+
prompt.prompt_data.variables.append(var_map)
|
|
128
|
+
|
|
129
|
+
return prompt
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _raise_for_invalid_prompt(
|
|
133
|
+
prompt: types.Prompt,
|
|
134
|
+
) -> None:
|
|
135
|
+
|
|
136
|
+
if not prompt.prompt_data:
|
|
137
|
+
raise ValueError("Prompt data must be provided.")
|
|
138
|
+
if not prompt.prompt_data.contents:
|
|
139
|
+
raise ValueError("Prompt contents must be provided.")
|
|
140
|
+
if not prompt.prompt_data.model:
|
|
141
|
+
raise ValueError("Model name must be provided.")
|
|
142
|
+
if (
|
|
143
|
+
prompt.prompt_data
|
|
144
|
+
and prompt.prompt_data.contents
|
|
145
|
+
and len(prompt.prompt_data.contents) > 1
|
|
146
|
+
):
|
|
147
|
+
raise ValueError("Multi-turn prompts are not currently supported.")
|
|
@@ -0,0 +1,215 @@
|
|
|
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 prompt optimizer."""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from typing import Any, Optional, Union
|
|
19
|
+
from typing_extensions import TypeAlias
|
|
20
|
+
|
|
21
|
+
from pydantic import ValidationError
|
|
22
|
+
|
|
23
|
+
from . import types
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import pandas as pd # pylint: disable=g-import-not-at-top
|
|
27
|
+
|
|
28
|
+
PandasDataFrame: TypeAlias = pd.DataFrame
|
|
29
|
+
except ImportError:
|
|
30
|
+
pd = None
|
|
31
|
+
PandasDataFrame = Any # type: ignore[misc]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _construct_input_prompt(
|
|
35
|
+
example_df: PandasDataFrame,
|
|
36
|
+
*,
|
|
37
|
+
prompt_col_name: str,
|
|
38
|
+
model_response_col_name: str,
|
|
39
|
+
rubrics_col_name: str,
|
|
40
|
+
rubrics_evaluations_col_name: str,
|
|
41
|
+
target_response_col_name: str,
|
|
42
|
+
system_instruction: Optional[str] = None,
|
|
43
|
+
) -> str:
|
|
44
|
+
"""Construct the input prompt for the few shot prompt optimizer."""
|
|
45
|
+
|
|
46
|
+
all_prompts = []
|
|
47
|
+
for row in example_df.to_dict(orient="records"):
|
|
48
|
+
example_data = {
|
|
49
|
+
"prompt": row[prompt_col_name],
|
|
50
|
+
"model_response": row[model_response_col_name],
|
|
51
|
+
}
|
|
52
|
+
if rubrics_col_name:
|
|
53
|
+
example_data["rubrics"] = row[rubrics_col_name]
|
|
54
|
+
if rubrics_evaluations_col_name:
|
|
55
|
+
example_data["rubrics_evaluations"] = row[rubrics_evaluations_col_name]
|
|
56
|
+
if target_response_col_name:
|
|
57
|
+
example_data["target_response"] = row[target_response_col_name]
|
|
58
|
+
|
|
59
|
+
json_str = json.dumps(example_data, indent=2)
|
|
60
|
+
all_prompts.append(f"```JSON\n{json_str}\n```")
|
|
61
|
+
|
|
62
|
+
all_prompts_str = "\n\n".join(all_prompts)
|
|
63
|
+
|
|
64
|
+
if system_instruction is None:
|
|
65
|
+
system_instruction = ""
|
|
66
|
+
|
|
67
|
+
return "\n".join(
|
|
68
|
+
[
|
|
69
|
+
"Original System Instructions:\n",
|
|
70
|
+
system_instruction,
|
|
71
|
+
"Examples:\n",
|
|
72
|
+
all_prompts_str,
|
|
73
|
+
"\nNew Output:\n",
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_few_shot_prompt(
|
|
79
|
+
system_instruction: str,
|
|
80
|
+
config: types.OptimizeConfig,
|
|
81
|
+
) -> str:
|
|
82
|
+
"""Builds the few shot prompt."""
|
|
83
|
+
|
|
84
|
+
if config.examples_dataframe is None:
|
|
85
|
+
raise ValueError("The 'examples_dataframe' is required in the config.")
|
|
86
|
+
|
|
87
|
+
if "prompt" not in config.examples_dataframe.columns:
|
|
88
|
+
raise ValueError("'prompt' is required in the examples_dataframe.")
|
|
89
|
+
|
|
90
|
+
if "prompt" not in config.examples_dataframe.columns:
|
|
91
|
+
raise ValueError("'prompt' is required in the examples_dataframe.")
|
|
92
|
+
prompt_col_name = "prompt"
|
|
93
|
+
|
|
94
|
+
if "model_response" not in config.examples_dataframe.columns:
|
|
95
|
+
raise ValueError("'model_response' is required in the example_df.")
|
|
96
|
+
model_response_col_name = "model_response"
|
|
97
|
+
|
|
98
|
+
target_response_col_name = ""
|
|
99
|
+
rubrics_col_name = ""
|
|
100
|
+
rubrics_evaluations_col_name = ""
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
config.optimization_target
|
|
104
|
+
== types.OptimizeTarget.OPTIMIZATION_TARGET_FEW_SHOT_TARGET_RESPONSE
|
|
105
|
+
):
|
|
106
|
+
if "target_response" not in config.examples_dataframe.columns:
|
|
107
|
+
raise ValueError("'target_response' is required in the examples_dataframe.")
|
|
108
|
+
target_response_col_name = "target_response"
|
|
109
|
+
if "rubrics" in config.examples_dataframe.columns:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
"Only 'target_response' should be provided "
|
|
112
|
+
"for OPTIMIZATION_TARGET_FEW_SHOT_TARGET_RESPONSE "
|
|
113
|
+
"but 'rubrics' was provided."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
elif (
|
|
117
|
+
config.optimization_target
|
|
118
|
+
== types.OptimizeTarget.OPTIMIZATION_TARGET_FEW_SHOT_RUBRICS
|
|
119
|
+
):
|
|
120
|
+
if not {"rubrics", "rubrics_evaluations"}.issubset(
|
|
121
|
+
config.examples_dataframe.columns
|
|
122
|
+
):
|
|
123
|
+
raise ValueError(
|
|
124
|
+
"rubrics and rubrics_evaluations is required in the"
|
|
125
|
+
"examples_dataframe when rubrics is set."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
rubrics_col_name = "rubrics"
|
|
129
|
+
rubrics_evaluations_col_name = "rubrics_evaluations"
|
|
130
|
+
if "target_response" in config.examples_dataframe.columns:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
"Only 'rubrics' and 'rubrics_evaluations' should be provided "
|
|
133
|
+
"for OPTIMIZATION_TARGET_FEW_SHOT_RUBRICS "
|
|
134
|
+
"but target_response was provided."
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
raise ValueError("One of 'target_response' or 'rubrics' must be provided.")
|
|
138
|
+
|
|
139
|
+
return _construct_input_prompt(
|
|
140
|
+
config.examples_dataframe,
|
|
141
|
+
prompt_col_name=prompt_col_name,
|
|
142
|
+
model_response_col_name=model_response_col_name,
|
|
143
|
+
rubrics_col_name=rubrics_col_name,
|
|
144
|
+
rubrics_evaluations_col_name=rubrics_evaluations_col_name,
|
|
145
|
+
target_response_col_name=target_response_col_name,
|
|
146
|
+
system_instruction=system_instruction,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _get_service_account(
|
|
151
|
+
config: types.PromptOptimizerConfigOrDict,
|
|
152
|
+
) -> str:
|
|
153
|
+
"""Get the service account from the config for the custom job."""
|
|
154
|
+
if isinstance(config, dict):
|
|
155
|
+
config = types.PromptOptimizerConfig.model_validate(config)
|
|
156
|
+
|
|
157
|
+
if (
|
|
158
|
+
config.service_account and config.service_account_project_number
|
|
159
|
+
): # pytype: disable=attribute-error
|
|
160
|
+
raise ValueError(
|
|
161
|
+
"Only one of service_account or "
|
|
162
|
+
"service_account_project_number can be provided."
|
|
163
|
+
)
|
|
164
|
+
elif config.service_account: # pytype: disable=attribute-error
|
|
165
|
+
return config.service_account # pytype: disable=attribute-error
|
|
166
|
+
elif config.service_account_project_number: # pytype: disable=attribute-error
|
|
167
|
+
return f"{config.service_account_project_number}-compute@developer.gserviceaccount.com" # pytype: disable=attribute-error
|
|
168
|
+
else:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
"Either service_account or service_account_project_number " "is required."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _clean_and_parse_optimized_prompt(output_str: str) -> Optional[Any]:
|
|
175
|
+
"""Cleans a string response returned from the prompt optimizer endpoint.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
output_str: The optimized prompt string containing the JSON data,
|
|
179
|
+
potentially with markdown formatting like ```json ... ```.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
The parsed JSON data, or None if parsing fails.
|
|
183
|
+
"""
|
|
184
|
+
lines = output_str.strip().split("\n")
|
|
185
|
+
# Remove markdown delimiters
|
|
186
|
+
if lines and lines[0].strip().startswith("```"):
|
|
187
|
+
cleaned_string = "\n".join(lines[1:-1])
|
|
188
|
+
else:
|
|
189
|
+
cleaned_string = output_str
|
|
190
|
+
|
|
191
|
+
# remove any 'json' labels if they exist on the first line.
|
|
192
|
+
if cleaned_string.strip().startswith("json"):
|
|
193
|
+
cleaned_string = cleaned_string.strip()[4:].strip()
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
return json.loads(cleaned_string)
|
|
197
|
+
except json.JSONDecodeError as e:
|
|
198
|
+
# TODO(b/437144880): raise errors.ClientError here instead
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"Failed to parse the response from prompt optimizer endpoint. {e}"
|
|
201
|
+
) from e
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _parse(
|
|
205
|
+
output_str: str,
|
|
206
|
+
) -> Union[
|
|
207
|
+
types.prompts.ParsedResponse,
|
|
208
|
+
types.prompts.ParsedResponseFewShot,
|
|
209
|
+
]:
|
|
210
|
+
"""Parses the output string from the prompt optimizer endpoint."""
|
|
211
|
+
parsed_out = _clean_and_parse_optimized_prompt(output_str)
|
|
212
|
+
try:
|
|
213
|
+
return types.prompts.ParsedResponse(**parsed_out)
|
|
214
|
+
except ValidationError:
|
|
215
|
+
return types.prompts.ParsedResponseFewShot(**parsed_out)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Copyright 2026 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 Skills."""
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import io
|
|
19
|
+
import os
|
|
20
|
+
import pathlib
|
|
21
|
+
import zipfile
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def zip_directory(directory_path: pathlib.Path | str) -> bytes:
|
|
25
|
+
"""Zips a directory into memory and returns the bytes.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
directory_path (pathlib.Path | str): Required. The local path to the
|
|
29
|
+
directory.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
bytes: The zipped directory content.
|
|
33
|
+
"""
|
|
34
|
+
directory_str = os.fspath(directory_path)
|
|
35
|
+
if not os.path.isdir(directory_str):
|
|
36
|
+
raise ValueError(f"Path is not a directory: {directory_str}")
|
|
37
|
+
|
|
38
|
+
zip_buffer = io.BytesIO()
|
|
39
|
+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
|
40
|
+
for root, _, files in os.walk(directory_str):
|
|
41
|
+
for file in files:
|
|
42
|
+
file_path = os.path.join(root, file)
|
|
43
|
+
arcname = os.path.relpath(file_path, directory_str)
|
|
44
|
+
|
|
45
|
+
# Read actual file data
|
|
46
|
+
with open(file_path, "rb") as f:
|
|
47
|
+
file_data = f.read()
|
|
48
|
+
|
|
49
|
+
# Use deterministic ZipInfo (mtime: 1980-01-01 00:00:00)
|
|
50
|
+
zinfo = zipfile.ZipInfo(arcname, date_time=(1980, 1, 1, 0, 0, 0))
|
|
51
|
+
zinfo.compress_type = zipfile.ZIP_DEFLATED
|
|
52
|
+
zinfo.external_attr = 0o644 << 16 # Constant file permissions
|
|
53
|
+
|
|
54
|
+
zip_file.writestr(zinfo, file_data)
|
|
55
|
+
return zip_buffer.getvalue()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_zipped_filesystem_payload(directory_path: pathlib.Path | str) -> str:
|
|
59
|
+
"""Zips a directory and base64-encodes the result to a UTF-8 string.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
directory_path (pathlib.Path | str): Required. The local path to the
|
|
63
|
+
directory.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
str: The base64-encoded zipped directory.
|
|
67
|
+
"""
|
|
68
|
+
zip_bytes = zip_directory(directory_path)
|
|
69
|
+
return base64.b64encode(zip_bytes).decode("utf-8")
|