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,926 @@
|
|
|
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
|
+
"""Dataset converters for evals."""
|
|
16
|
+
|
|
17
|
+
import copy
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any, Optional, Union
|
|
21
|
+
|
|
22
|
+
from google.genai import _common
|
|
23
|
+
from google.genai import types as genai_types
|
|
24
|
+
from pydantic import ValidationError
|
|
25
|
+
from typing_extensions import override
|
|
26
|
+
|
|
27
|
+
from . import _evals_utils
|
|
28
|
+
from . import _observability_data_converter
|
|
29
|
+
from . import types
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("agentplatform_genai._evals_data_converters")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EvalDatasetSchema(_common.CaseInSensitiveEnum):
|
|
36
|
+
"""Represents the schema of an evaluation dataset."""
|
|
37
|
+
|
|
38
|
+
GEMINI = "gemini"
|
|
39
|
+
FLATTEN = "flatten"
|
|
40
|
+
OPENAI = "openai"
|
|
41
|
+
OBSERVABILITY = "observability"
|
|
42
|
+
UNKNOWN = "unknown"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_PLACEHOLDER_RESPONSE_TEXT = "Error: Missing response for this candidate"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _create_placeholder_response_candidate(
|
|
49
|
+
text: str = _PLACEHOLDER_RESPONSE_TEXT,
|
|
50
|
+
) -> types.ResponseCandidate:
|
|
51
|
+
"""Creates a ResponseCandidate with placeholder text."""
|
|
52
|
+
return types.ResponseCandidate(
|
|
53
|
+
response=genai_types.Content(parts=[genai_types.Part(text=text)])
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _GeminiEvalDataConverter(_evals_utils.EvalDataConverter):
|
|
58
|
+
"""Converter for dataset in the Gemini format."""
|
|
59
|
+
|
|
60
|
+
def _parse_request(self, request_data: dict[str, Any]) -> tuple[
|
|
61
|
+
genai_types.Content,
|
|
62
|
+
genai_types.Content,
|
|
63
|
+
list[types.evals.Message],
|
|
64
|
+
types.ResponseCandidate,
|
|
65
|
+
]:
|
|
66
|
+
"""Parses a request from a Gemini dataset."""
|
|
67
|
+
system_instruction = genai_types.Content()
|
|
68
|
+
prompt = genai_types.Content()
|
|
69
|
+
reference = types.ResponseCandidate()
|
|
70
|
+
conversation_history = []
|
|
71
|
+
|
|
72
|
+
if "system_instruction" in request_data:
|
|
73
|
+
system_instruction = genai_types.Content.model_validate(
|
|
74
|
+
request_data["system_instruction"]
|
|
75
|
+
)
|
|
76
|
+
for turn_id, content_dict in enumerate(request_data.get("contents", [])):
|
|
77
|
+
if not isinstance(content_dict, dict):
|
|
78
|
+
raise TypeError(
|
|
79
|
+
"Expected a dictionary for content at turn %s, but got %s: %s"
|
|
80
|
+
% (turn_id, type(content_dict).__name__, content_dict)
|
|
81
|
+
)
|
|
82
|
+
if "parts" not in content_dict:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"Missing 'parts' key in content structure at turn %s: %s"
|
|
85
|
+
% (turn_id, content_dict)
|
|
86
|
+
)
|
|
87
|
+
conversation_history.append(
|
|
88
|
+
types.evals.Message(
|
|
89
|
+
turn_id=str(turn_id),
|
|
90
|
+
content=genai_types.Content.model_validate(content_dict),
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
if conversation_history:
|
|
94
|
+
last_message = conversation_history.pop()
|
|
95
|
+
last_message_role = (
|
|
96
|
+
last_message.content.role if last_message.content else "user"
|
|
97
|
+
)
|
|
98
|
+
if last_message_role in ["user", None]:
|
|
99
|
+
prompt = (
|
|
100
|
+
last_message.content
|
|
101
|
+
if last_message.content
|
|
102
|
+
else genai_types.Content()
|
|
103
|
+
)
|
|
104
|
+
elif last_message_role == "model":
|
|
105
|
+
reference = types.ResponseCandidate(response=last_message.content)
|
|
106
|
+
if conversation_history:
|
|
107
|
+
second_to_last_message = conversation_history.pop()
|
|
108
|
+
prompt = (
|
|
109
|
+
second_to_last_message.content
|
|
110
|
+
if second_to_last_message.content
|
|
111
|
+
else genai_types.Content()
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
prompt = genai_types.Content()
|
|
115
|
+
|
|
116
|
+
return prompt, system_instruction, conversation_history, reference
|
|
117
|
+
|
|
118
|
+
@override
|
|
119
|
+
def convert(self, raw_data: list[dict[str, Any]]) -> types.EvaluationDataset:
|
|
120
|
+
"""Converts a list of raw data into an EvaluationDataset."""
|
|
121
|
+
eval_cases = []
|
|
122
|
+
|
|
123
|
+
for i, item in enumerate(raw_data):
|
|
124
|
+
eval_case_id = "gemini_eval_case_%s" % i
|
|
125
|
+
request_data = item.get("request", {})
|
|
126
|
+
response_data = item.get("response", {})
|
|
127
|
+
|
|
128
|
+
(
|
|
129
|
+
prompt,
|
|
130
|
+
system_instruction,
|
|
131
|
+
conversation_history,
|
|
132
|
+
reference,
|
|
133
|
+
) = self._parse_request(request_data)
|
|
134
|
+
|
|
135
|
+
responses = []
|
|
136
|
+
if isinstance(response_data, str):
|
|
137
|
+
responses.append(
|
|
138
|
+
types.ResponseCandidate(
|
|
139
|
+
response=genai_types.Content(
|
|
140
|
+
parts=[genai_types.Part(text=response_data)]
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
elif isinstance(response_data, dict):
|
|
145
|
+
try:
|
|
146
|
+
generate_content_response = (
|
|
147
|
+
genai_types.GenerateContentResponse.model_validate(
|
|
148
|
+
response_data
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
if generate_content_response.candidates:
|
|
152
|
+
candidate = generate_content_response.candidates[0]
|
|
153
|
+
if candidate.content:
|
|
154
|
+
responses.append(
|
|
155
|
+
types.ResponseCandidate(
|
|
156
|
+
response=genai_types.Content.model_validate(
|
|
157
|
+
candidate.content
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
responses.append(_create_placeholder_response_candidate())
|
|
163
|
+
except Exception:
|
|
164
|
+
responses.append(_create_placeholder_response_candidate())
|
|
165
|
+
else:
|
|
166
|
+
responses.append(_create_placeholder_response_candidate())
|
|
167
|
+
|
|
168
|
+
eval_case = types.EvalCase(
|
|
169
|
+
eval_case_id=eval_case_id,
|
|
170
|
+
prompt=prompt,
|
|
171
|
+
responses=responses,
|
|
172
|
+
reference=reference,
|
|
173
|
+
system_instruction=system_instruction,
|
|
174
|
+
conversation_history=conversation_history,
|
|
175
|
+
)
|
|
176
|
+
eval_cases.append(eval_case)
|
|
177
|
+
|
|
178
|
+
return types.EvaluationDataset(eval_cases=eval_cases)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class _FlattenEvalDataConverter(_evals_utils.EvalDataConverter):
|
|
182
|
+
"""Converter for datasets in a structured table format."""
|
|
183
|
+
|
|
184
|
+
def convert(self, raw_data: list[dict[str, Any]]) -> types.EvaluationDataset:
|
|
185
|
+
"""Converts a list of raw data into an EvaluationDataset."""
|
|
186
|
+
eval_cases = []
|
|
187
|
+
for i, item_dict in enumerate(raw_data):
|
|
188
|
+
if not isinstance(item_dict, dict):
|
|
189
|
+
raise TypeError(
|
|
190
|
+
"Expected a dictionary for item at index %s, but got %s: %s"
|
|
191
|
+
% (i, type(item_dict).__name__, item_dict)
|
|
192
|
+
)
|
|
193
|
+
item = copy.deepcopy(item_dict)
|
|
194
|
+
eval_case_id = "eval_case_%s" % i
|
|
195
|
+
prompt_data = item.pop("prompt", None)
|
|
196
|
+
if not prompt_data:
|
|
197
|
+
prompt_data = item.pop("source", None)
|
|
198
|
+
|
|
199
|
+
conversation_history_data = item.pop("conversation_history", None)
|
|
200
|
+
if conversation_history_data is None:
|
|
201
|
+
conversation_history_data = item.pop("history", None)
|
|
202
|
+
response_data = item.pop("response", None)
|
|
203
|
+
reference_data = item.pop("reference", None)
|
|
204
|
+
system_instruction_data = item.pop("instruction", None)
|
|
205
|
+
rubric_groups_data = item.pop("rubric_groups", None)
|
|
206
|
+
intermediate_events_data = item.pop("intermediate_events", None)
|
|
207
|
+
agent_data_raw = item.pop("agent_data", None)
|
|
208
|
+
|
|
209
|
+
if not response_data and not agent_data_raw:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
"Response is required but missing for %s." % eval_case_id
|
|
212
|
+
)
|
|
213
|
+
if not prompt_data and not agent_data_raw:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
"Prompt is required but missing for %s." % eval_case_id
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
prompt: Optional[genai_types.Content] = None
|
|
219
|
+
if isinstance(prompt_data, str):
|
|
220
|
+
prompt = genai_types.Content(parts=[genai_types.Part(text=prompt_data)])
|
|
221
|
+
elif isinstance(prompt_data, dict):
|
|
222
|
+
prompt = genai_types.Content.model_validate(prompt_data)
|
|
223
|
+
elif isinstance(prompt_data, genai_types.Content):
|
|
224
|
+
prompt = prompt_data
|
|
225
|
+
elif not agent_data_raw:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
"Invalid prompt type for case %s: %s" % (i, type(prompt_data))
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
conversation_history: Optional[list[types.evals.Message]] = None
|
|
231
|
+
if isinstance(conversation_history_data, list):
|
|
232
|
+
conversation_history = []
|
|
233
|
+
for turn_id, content in enumerate(conversation_history_data):
|
|
234
|
+
if isinstance(content, genai_types.Content):
|
|
235
|
+
conversation_history.append(
|
|
236
|
+
types.evals.Message(
|
|
237
|
+
turn_id=str(turn_id),
|
|
238
|
+
content=content,
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
elif isinstance(content, dict):
|
|
242
|
+
try:
|
|
243
|
+
validated_content = genai_types.Content.model_validate(
|
|
244
|
+
content
|
|
245
|
+
)
|
|
246
|
+
conversation_history.append(
|
|
247
|
+
types.evals.Message(
|
|
248
|
+
turn_id=str(turn_id),
|
|
249
|
+
content=validated_content,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
except ValidationError as e:
|
|
253
|
+
logger.warning(
|
|
254
|
+
"Item at index %s in 'history' column for case "
|
|
255
|
+
" %s is a dict but could not be validated as"
|
|
256
|
+
" genai_types.Content: %s",
|
|
257
|
+
turn_id,
|
|
258
|
+
eval_case_id,
|
|
259
|
+
e,
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
logger.warning(
|
|
263
|
+
"Invalid type in 'history' column for case %s at index %s. "
|
|
264
|
+
"Expected genai_types.Content or dict, but got %s. "
|
|
265
|
+
"Skipping this history item.",
|
|
266
|
+
eval_case_id,
|
|
267
|
+
turn_id,
|
|
268
|
+
type(content),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
responses: Optional[list[types.ResponseCandidate]] = None
|
|
272
|
+
if isinstance(response_data, dict):
|
|
273
|
+
responses = [
|
|
274
|
+
types.ResponseCandidate(
|
|
275
|
+
response=genai_types.Content.model_validate(response_data)
|
|
276
|
+
)
|
|
277
|
+
]
|
|
278
|
+
elif isinstance(response_data, str):
|
|
279
|
+
responses = [
|
|
280
|
+
types.ResponseCandidate(
|
|
281
|
+
response=genai_types.Content(
|
|
282
|
+
parts=[genai_types.Part(text=response_data)]
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
]
|
|
286
|
+
elif isinstance(response_data, genai_types.Content):
|
|
287
|
+
responses = [types.ResponseCandidate(response=response_data)]
|
|
288
|
+
elif not agent_data_raw:
|
|
289
|
+
raise ValueError(
|
|
290
|
+
"Invalid response type for case %s: %s" % (i, type(response_data))
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
reference: Optional[types.ResponseCandidate] = None
|
|
294
|
+
if reference_data:
|
|
295
|
+
if isinstance(reference_data, dict):
|
|
296
|
+
reference = types.ResponseCandidate(
|
|
297
|
+
response=genai_types.Content.model_validate(reference_data)
|
|
298
|
+
)
|
|
299
|
+
elif isinstance(reference_data, str):
|
|
300
|
+
reference = types.ResponseCandidate(
|
|
301
|
+
response=genai_types.Content(
|
|
302
|
+
parts=[genai_types.Part(text=reference_data)]
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
elif isinstance(reference_data, genai_types.Content):
|
|
306
|
+
reference = types.ResponseCandidate(response=reference_data)
|
|
307
|
+
|
|
308
|
+
system_instruction: Optional[genai_types.Content] = None
|
|
309
|
+
if system_instruction_data:
|
|
310
|
+
if isinstance(system_instruction_data, dict):
|
|
311
|
+
system_instruction = genai_types.Content.model_validate(
|
|
312
|
+
system_instruction_data
|
|
313
|
+
)
|
|
314
|
+
elif isinstance(system_instruction_data, str):
|
|
315
|
+
system_instruction = genai_types.Content(
|
|
316
|
+
parts=[genai_types.Part(text=system_instruction_data)]
|
|
317
|
+
)
|
|
318
|
+
elif isinstance(system_instruction_data, genai_types.Content):
|
|
319
|
+
system_instruction = system_instruction_data
|
|
320
|
+
|
|
321
|
+
rubric_groups: Optional[dict[str, types.RubricGroup]] = None
|
|
322
|
+
if rubric_groups_data:
|
|
323
|
+
if isinstance(rubric_groups_data, dict):
|
|
324
|
+
rubric_groups = {}
|
|
325
|
+
for key, value in rubric_groups_data.items():
|
|
326
|
+
if isinstance(value, list):
|
|
327
|
+
try:
|
|
328
|
+
validated_rubrics = [
|
|
329
|
+
(
|
|
330
|
+
types.evals.Rubric.model_validate(r)
|
|
331
|
+
if isinstance(r, dict)
|
|
332
|
+
else r
|
|
333
|
+
)
|
|
334
|
+
for r in value
|
|
335
|
+
]
|
|
336
|
+
if all(
|
|
337
|
+
isinstance(r, types.evals.Rubric)
|
|
338
|
+
for r in validated_rubrics
|
|
339
|
+
):
|
|
340
|
+
rubric_groups[key] = types.RubricGroup(
|
|
341
|
+
rubrics=validated_rubrics
|
|
342
|
+
)
|
|
343
|
+
else:
|
|
344
|
+
logger.warning(
|
|
345
|
+
"Invalid item type in rubric list for group '%s' in case %s.",
|
|
346
|
+
key,
|
|
347
|
+
i,
|
|
348
|
+
)
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.warning(
|
|
351
|
+
"Failed to validate rubrics for group '%s' in case %s: %s",
|
|
352
|
+
key,
|
|
353
|
+
i,
|
|
354
|
+
e,
|
|
355
|
+
)
|
|
356
|
+
elif isinstance(value, types.RubricGroup):
|
|
357
|
+
rubric_groups[key] = value
|
|
358
|
+
elif isinstance(value, dict):
|
|
359
|
+
try:
|
|
360
|
+
rubric_groups[key] = types.RubricGroup.model_validate(
|
|
361
|
+
value
|
|
362
|
+
)
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.warning(
|
|
365
|
+
"Failed to validate RubricGroup dict for group '%s' in case %s: %s",
|
|
366
|
+
key,
|
|
367
|
+
i,
|
|
368
|
+
e,
|
|
369
|
+
)
|
|
370
|
+
else:
|
|
371
|
+
logger.warning(
|
|
372
|
+
"Invalid type for rubric group '%s' in case %s."
|
|
373
|
+
" Expected list of rubrics, dict, or RubricGroup.",
|
|
374
|
+
key,
|
|
375
|
+
i,
|
|
376
|
+
)
|
|
377
|
+
else:
|
|
378
|
+
logger.warning(
|
|
379
|
+
"Invalid type for rubric_groups in case %s. Expected dict.",
|
|
380
|
+
i,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
intermediate_events: Optional[list[types.evals.Event]] = None
|
|
384
|
+
if intermediate_events_data:
|
|
385
|
+
if isinstance(intermediate_events_data, list):
|
|
386
|
+
intermediate_events = []
|
|
387
|
+
for event in intermediate_events_data:
|
|
388
|
+
if isinstance(event, dict):
|
|
389
|
+
try:
|
|
390
|
+
validated_event = types.evals.Event.model_validate(
|
|
391
|
+
event
|
|
392
|
+
)
|
|
393
|
+
intermediate_events.append(validated_event)
|
|
394
|
+
except Exception as e:
|
|
395
|
+
logger.warning(
|
|
396
|
+
"Failed to validate intermediate event dict for"
|
|
397
|
+
" case %s: %s",
|
|
398
|
+
i,
|
|
399
|
+
e,
|
|
400
|
+
)
|
|
401
|
+
elif isinstance(event, types.evals.Event):
|
|
402
|
+
intermediate_events.append(event)
|
|
403
|
+
else:
|
|
404
|
+
logger.warning(
|
|
405
|
+
"Invalid type for intermediate_event in case"
|
|
406
|
+
" %s. Expected list of dicts or list of"
|
|
407
|
+
" types.evals.Event objects.",
|
|
408
|
+
i,
|
|
409
|
+
)
|
|
410
|
+
else:
|
|
411
|
+
logger.warning(
|
|
412
|
+
"Invalid type for intermediate_events in case %s. Expected"
|
|
413
|
+
" list of types.evals.Event objects.",
|
|
414
|
+
i,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
agent_data: Optional[types.evals.AgentData] = None
|
|
418
|
+
if agent_data_raw:
|
|
419
|
+
if isinstance(agent_data_raw, str):
|
|
420
|
+
try:
|
|
421
|
+
agent_data_dict = json.loads(agent_data_raw)
|
|
422
|
+
agent_data = types.evals.AgentData.model_validate(
|
|
423
|
+
agent_data_dict
|
|
424
|
+
)
|
|
425
|
+
except json.JSONDecodeError:
|
|
426
|
+
logger.warning(
|
|
427
|
+
"Could not decode agent_data JSON string for case %s.", i
|
|
428
|
+
)
|
|
429
|
+
except ValidationError as e:
|
|
430
|
+
logger.warning(
|
|
431
|
+
"Failed to validate agent_data for case %s: %s", i, e
|
|
432
|
+
)
|
|
433
|
+
elif isinstance(agent_data_raw, dict):
|
|
434
|
+
try:
|
|
435
|
+
agent_data = types.evals.AgentData.model_validate(
|
|
436
|
+
agent_data_raw
|
|
437
|
+
)
|
|
438
|
+
except ValidationError as e:
|
|
439
|
+
logger.warning(
|
|
440
|
+
"Failed to validate agent_data for case %s: %s", i, e
|
|
441
|
+
)
|
|
442
|
+
elif isinstance(agent_data_raw, types.evals.AgentData):
|
|
443
|
+
agent_data = agent_data_raw
|
|
444
|
+
else:
|
|
445
|
+
logger.warning(
|
|
446
|
+
"Invalid type for agent_data in case %s. Expected str, dict"
|
|
447
|
+
" or types.evals.AgentData object. Got %s",
|
|
448
|
+
i,
|
|
449
|
+
type(agent_data_raw),
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
eval_case = types.EvalCase(
|
|
453
|
+
eval_case_id=eval_case_id,
|
|
454
|
+
prompt=prompt,
|
|
455
|
+
responses=responses,
|
|
456
|
+
reference=reference,
|
|
457
|
+
conversation_history=conversation_history,
|
|
458
|
+
system_instruction=system_instruction,
|
|
459
|
+
rubric_groups=rubric_groups,
|
|
460
|
+
intermediate_events=intermediate_events,
|
|
461
|
+
agent_data=agent_data,
|
|
462
|
+
**item, # Pass remaining columns as extra fields to EvalCase.
|
|
463
|
+
# They can be used for custom metric prompt templates.
|
|
464
|
+
)
|
|
465
|
+
eval_cases.append(eval_case)
|
|
466
|
+
|
|
467
|
+
return types.EvaluationDataset(eval_cases=eval_cases)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class _OpenAIDataConverter(_evals_utils.EvalDataConverter):
|
|
471
|
+
"""Converter for dataset in OpenAI's Chat Completion format."""
|
|
472
|
+
|
|
473
|
+
def _parse_messages(self, messages: list[dict[str, Any]]) -> tuple[
|
|
474
|
+
Optional[genai_types.Content],
|
|
475
|
+
list[types.evals.Message],
|
|
476
|
+
Optional[genai_types.Content],
|
|
477
|
+
Optional[types.ResponseCandidate],
|
|
478
|
+
]:
|
|
479
|
+
"""Parses a list of messages into instruction, history, prompt, and reference."""
|
|
480
|
+
system_instruction = None
|
|
481
|
+
prompt = None
|
|
482
|
+
reference = None
|
|
483
|
+
conversation_history = []
|
|
484
|
+
|
|
485
|
+
if messages and messages[0].get("role") in ["system", "developer"]:
|
|
486
|
+
system_instruction = genai_types.Content(
|
|
487
|
+
parts=[genai_types.Part(text=messages[0].get("content"))]
|
|
488
|
+
)
|
|
489
|
+
messages = messages[1:]
|
|
490
|
+
|
|
491
|
+
for turn_id, msg in enumerate(messages):
|
|
492
|
+
role = msg.get("role", "user")
|
|
493
|
+
content = msg.get("content", "")
|
|
494
|
+
conversation_history.append(
|
|
495
|
+
types.evals.Message(
|
|
496
|
+
turn_id=str(turn_id),
|
|
497
|
+
content=genai_types.Content(
|
|
498
|
+
parts=[genai_types.Part(text=content)], role=role
|
|
499
|
+
),
|
|
500
|
+
author=role,
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
if conversation_history:
|
|
505
|
+
last_message = conversation_history.pop()
|
|
506
|
+
if last_message.content and last_message.content.role == "user":
|
|
507
|
+
prompt = last_message.content
|
|
508
|
+
elif last_message.content and last_message.content.role == "assistant":
|
|
509
|
+
reference = types.ResponseCandidate(response=last_message.content)
|
|
510
|
+
if conversation_history:
|
|
511
|
+
second_to_last_message = conversation_history.pop()
|
|
512
|
+
prompt = second_to_last_message.content
|
|
513
|
+
|
|
514
|
+
return system_instruction, conversation_history, prompt, reference
|
|
515
|
+
|
|
516
|
+
@override
|
|
517
|
+
def convert(self, raw_data: list[dict[str, Any]]) -> types.EvaluationDataset:
|
|
518
|
+
"""Converts a list of OpenAI ChatCompletion data into an EvaluationDataset."""
|
|
519
|
+
eval_cases = []
|
|
520
|
+
for i, item in enumerate(raw_data):
|
|
521
|
+
eval_case_id = "openai_eval_case_%s" % i
|
|
522
|
+
|
|
523
|
+
if "request" not in item or "response" not in item:
|
|
524
|
+
logger.warning(
|
|
525
|
+
"Skipping case %s due to missing 'request' or 'response' key.", i
|
|
526
|
+
)
|
|
527
|
+
continue
|
|
528
|
+
|
|
529
|
+
request_data = item.get("request", {})
|
|
530
|
+
response_data_raw = item.get("response", {})
|
|
531
|
+
|
|
532
|
+
response_data = {}
|
|
533
|
+
if isinstance(response_data_raw, str):
|
|
534
|
+
try:
|
|
535
|
+
loaded_json = json.loads(response_data_raw)
|
|
536
|
+
if isinstance(loaded_json, dict):
|
|
537
|
+
response_data = loaded_json
|
|
538
|
+
else:
|
|
539
|
+
logger.warning(
|
|
540
|
+
"Decoded response JSON is not a dictionary for case"
|
|
541
|
+
" %s. Type: %s",
|
|
542
|
+
i,
|
|
543
|
+
type(loaded_json),
|
|
544
|
+
)
|
|
545
|
+
except json.JSONDecodeError:
|
|
546
|
+
logger.warning(
|
|
547
|
+
"Could not decode response JSON string for case %s."
|
|
548
|
+
" Treating as empty response.",
|
|
549
|
+
i,
|
|
550
|
+
)
|
|
551
|
+
elif isinstance(response_data_raw, dict):
|
|
552
|
+
response_data = response_data_raw
|
|
553
|
+
|
|
554
|
+
messages = request_data.get("messages", [])
|
|
555
|
+
choices = response_data.get("choices", [])
|
|
556
|
+
|
|
557
|
+
(
|
|
558
|
+
system_instruction,
|
|
559
|
+
conversation_history,
|
|
560
|
+
prompt,
|
|
561
|
+
reference,
|
|
562
|
+
) = self._parse_messages(messages)
|
|
563
|
+
|
|
564
|
+
if prompt is None and reference is None:
|
|
565
|
+
logger.warning(
|
|
566
|
+
"Could not determine a user prompt or reference for case %s."
|
|
567
|
+
" Skipping.",
|
|
568
|
+
i,
|
|
569
|
+
)
|
|
570
|
+
continue
|
|
571
|
+
|
|
572
|
+
responses = []
|
|
573
|
+
if (
|
|
574
|
+
choices
|
|
575
|
+
and isinstance(choices, list)
|
|
576
|
+
and isinstance(choices[0], dict)
|
|
577
|
+
and choices[0].get("message")
|
|
578
|
+
):
|
|
579
|
+
response_content = choices[0]["message"].get("content", "")
|
|
580
|
+
responses.append(
|
|
581
|
+
types.ResponseCandidate(
|
|
582
|
+
response=genai_types.Content(
|
|
583
|
+
parts=[genai_types.Part(text=response_content)]
|
|
584
|
+
)
|
|
585
|
+
)
|
|
586
|
+
)
|
|
587
|
+
else:
|
|
588
|
+
responses.append(_create_placeholder_response_candidate())
|
|
589
|
+
|
|
590
|
+
other_fields = {
|
|
591
|
+
k: v for k, v in item.items() if k not in ["request", "response"]
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
eval_case = types.EvalCase(
|
|
595
|
+
eval_case_id=eval_case_id,
|
|
596
|
+
prompt=prompt,
|
|
597
|
+
responses=responses,
|
|
598
|
+
reference=reference,
|
|
599
|
+
system_instruction=system_instruction,
|
|
600
|
+
conversation_history=conversation_history,
|
|
601
|
+
**other_fields,
|
|
602
|
+
)
|
|
603
|
+
eval_cases.append(eval_case)
|
|
604
|
+
|
|
605
|
+
return types.EvaluationDataset(eval_cases=eval_cases)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def auto_detect_dataset_schema(
|
|
609
|
+
raw_dataset: list[dict[str, Any]],
|
|
610
|
+
) -> Union[EvalDatasetSchema, str]:
|
|
611
|
+
"""Detects the schema of a raw dataset."""
|
|
612
|
+
if not raw_dataset:
|
|
613
|
+
return EvalDatasetSchema.UNKNOWN
|
|
614
|
+
|
|
615
|
+
first_item = raw_dataset[0]
|
|
616
|
+
keys = set(first_item.keys())
|
|
617
|
+
|
|
618
|
+
if "format" in keys:
|
|
619
|
+
format_content = first_item.get("format", "")
|
|
620
|
+
if isinstance(format_content, str) and format_content == "observability":
|
|
621
|
+
return EvalDatasetSchema.OBSERVABILITY
|
|
622
|
+
|
|
623
|
+
if "request" in keys and "response" in keys:
|
|
624
|
+
request_content = first_item.get("request", {})
|
|
625
|
+
if isinstance(request_content, dict) and "contents" in request_content:
|
|
626
|
+
contents_list = request_content.get("contents")
|
|
627
|
+
if (
|
|
628
|
+
contents_list
|
|
629
|
+
and isinstance(contents_list, list)
|
|
630
|
+
and isinstance(contents_list[0], dict)
|
|
631
|
+
):
|
|
632
|
+
if "parts" in contents_list[0]:
|
|
633
|
+
return EvalDatasetSchema.GEMINI
|
|
634
|
+
|
|
635
|
+
if "request" in keys and "response" in keys:
|
|
636
|
+
request_content = first_item.get("request", {})
|
|
637
|
+
if isinstance(request_content, dict) and "messages" in request_content:
|
|
638
|
+
messages_list = request_content.get("messages")
|
|
639
|
+
if (
|
|
640
|
+
messages_list
|
|
641
|
+
and isinstance(messages_list, list)
|
|
642
|
+
and isinstance(messages_list[0], dict)
|
|
643
|
+
):
|
|
644
|
+
if "role" in messages_list[0] and "content" in messages_list[0]:
|
|
645
|
+
return EvalDatasetSchema.OPENAI
|
|
646
|
+
|
|
647
|
+
if "agent_data" in keys:
|
|
648
|
+
return EvalDatasetSchema.FLATTEN
|
|
649
|
+
|
|
650
|
+
if {"prompt", "response"}.issubset(keys) or {
|
|
651
|
+
"response",
|
|
652
|
+
"reference",
|
|
653
|
+
}.issubset(keys):
|
|
654
|
+
return EvalDatasetSchema.FLATTEN
|
|
655
|
+
else:
|
|
656
|
+
return EvalDatasetSchema.UNKNOWN
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
_CONVERTER_REGISTRY = {
|
|
660
|
+
EvalDatasetSchema.GEMINI: _GeminiEvalDataConverter,
|
|
661
|
+
EvalDatasetSchema.FLATTEN: _FlattenEvalDataConverter,
|
|
662
|
+
EvalDatasetSchema.OPENAI: _OpenAIDataConverter,
|
|
663
|
+
EvalDatasetSchema.OBSERVABILITY: _observability_data_converter.ObservabilityDataConverter,
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def get_dataset_converter(
|
|
668
|
+
dataset_schema: EvalDatasetSchema,
|
|
669
|
+
) -> _evals_utils.EvalDataConverter:
|
|
670
|
+
"""Returns the appropriate dataset converter for the given schema."""
|
|
671
|
+
if dataset_schema in _CONVERTER_REGISTRY:
|
|
672
|
+
return _CONVERTER_REGISTRY[dataset_schema]() # type: ignore[abstract]
|
|
673
|
+
else:
|
|
674
|
+
raise ValueError("Unsupported dataset schema: %s" % dataset_schema)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _get_content_text(content: genai_types.Content) -> str:
|
|
678
|
+
"""Safely extracts text from all parts of a content.
|
|
679
|
+
|
|
680
|
+
If the content has multiple parts, text from all parts is concatenated.
|
|
681
|
+
If a part is not text, it is ignored. If no text parts are found,
|
|
682
|
+
an empty string is returned.
|
|
683
|
+
"""
|
|
684
|
+
text_parts = []
|
|
685
|
+
if (
|
|
686
|
+
content
|
|
687
|
+
and hasattr(content, "parts")
|
|
688
|
+
and isinstance(content.parts, list)
|
|
689
|
+
and content.parts
|
|
690
|
+
):
|
|
691
|
+
for part in content.parts:
|
|
692
|
+
if hasattr(part, "text") and part.text is not None:
|
|
693
|
+
text_parts.append(str(part.text))
|
|
694
|
+
return "".join(text_parts)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _get_text_from_reference(
|
|
698
|
+
reference: Optional[types.ResponseCandidate],
|
|
699
|
+
) -> Optional[str]:
|
|
700
|
+
"""Safely extracts text from a reference field."""
|
|
701
|
+
if reference and hasattr(reference, "response") and reference.response:
|
|
702
|
+
return _get_content_text(reference.response)
|
|
703
|
+
return None
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _validate_case_consistency(
|
|
707
|
+
base_case: types.EvalCase,
|
|
708
|
+
current_case: types.EvalCase,
|
|
709
|
+
case_idx: int,
|
|
710
|
+
dataset_idx: int,
|
|
711
|
+
) -> None:
|
|
712
|
+
"""Logs warnings if prompt or reference mismatches occur."""
|
|
713
|
+
if base_case.prompt != current_case.prompt:
|
|
714
|
+
base_prompt_text_preview = _get_content_text(base_case.prompt)[:50]
|
|
715
|
+
current_prompt_text_preview = _get_content_text(current_case.prompt)[:50]
|
|
716
|
+
logger.warning(
|
|
717
|
+
"Prompt mismatch for case index %d between base dataset (0)"
|
|
718
|
+
" and dataset %d. Using prompt from base. Base prompt"
|
|
719
|
+
" preview: '%s...', Dataset"
|
|
720
|
+
" %d prompt preview: '%s...'",
|
|
721
|
+
case_idx,
|
|
722
|
+
dataset_idx,
|
|
723
|
+
base_prompt_text_preview,
|
|
724
|
+
dataset_idx,
|
|
725
|
+
current_prompt_text_preview,
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
base_ref_text = _get_text_from_reference(base_case.reference)
|
|
729
|
+
current_ref_text = _get_text_from_reference(current_case.reference)
|
|
730
|
+
|
|
731
|
+
if bool(base_case.reference) != bool(current_case.reference):
|
|
732
|
+
logger.warning(
|
|
733
|
+
"Reference presence mismatch for case index %d between base"
|
|
734
|
+
" dataset (0) and dataset %d. Using reference (or lack"
|
|
735
|
+
" thereof) from base.",
|
|
736
|
+
case_idx,
|
|
737
|
+
dataset_idx,
|
|
738
|
+
)
|
|
739
|
+
elif base_ref_text != current_ref_text:
|
|
740
|
+
logger.warning(
|
|
741
|
+
"Reference text mismatch for case index %d between base"
|
|
742
|
+
" dataset (0) and dataset %d. Using reference from base. "
|
|
743
|
+
" Base ref: '%s...', Current ref:"
|
|
744
|
+
" '%s...'",
|
|
745
|
+
case_idx,
|
|
746
|
+
dataset_idx,
|
|
747
|
+
str(base_ref_text)[:50],
|
|
748
|
+
str(current_ref_text)[:50],
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def merge_evaluation_datasets(
|
|
753
|
+
datasets: list[types.EvaluationDataset],
|
|
754
|
+
agent_info: Optional[types.evals.AgentInfo] = None,
|
|
755
|
+
) -> types.EvaluationDataset:
|
|
756
|
+
"""Merges multiple EvaluationDatasets into a single EvaluationDataset.
|
|
757
|
+
|
|
758
|
+
Assumes that each dataset has responses corresponding to the same set of
|
|
759
|
+
prompts, in the same order. The prompt, reference, system_instruction, and
|
|
760
|
+
conversation_history are taken from the first dataset.
|
|
761
|
+
"""
|
|
762
|
+
if not datasets:
|
|
763
|
+
raise ValueError("Input 'datasets' cannot be empty.")
|
|
764
|
+
|
|
765
|
+
num_expected_cases = 0
|
|
766
|
+
if datasets[0].eval_cases:
|
|
767
|
+
num_expected_cases = len(datasets[0].eval_cases)
|
|
768
|
+
|
|
769
|
+
if num_expected_cases == 0:
|
|
770
|
+
logger.warning(
|
|
771
|
+
"The first dataset has no evaluation cases. Result will be empty."
|
|
772
|
+
)
|
|
773
|
+
return types.EvaluationDataset(eval_cases=[])
|
|
774
|
+
|
|
775
|
+
for i, ds in enumerate(datasets):
|
|
776
|
+
current_len = len(ds.eval_cases) if ds.eval_cases else 0
|
|
777
|
+
if current_len != num_expected_cases:
|
|
778
|
+
raise ValueError(
|
|
779
|
+
"All datasets must have the same number of evaluation cases. "
|
|
780
|
+
"Base dataset (0) has %s, but dataset %s has %s."
|
|
781
|
+
% (num_expected_cases, i, current_len)
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
merged_eval_cases: list[types.EvalCase] = []
|
|
785
|
+
base_parsed_dataset = datasets[0]
|
|
786
|
+
|
|
787
|
+
for case_idx in range(num_expected_cases):
|
|
788
|
+
base_eval_case: types.EvalCase = (
|
|
789
|
+
base_parsed_dataset.eval_cases[case_idx]
|
|
790
|
+
if base_parsed_dataset.eval_cases
|
|
791
|
+
else types.EvalCase()
|
|
792
|
+
)
|
|
793
|
+
candidate_responses: list[types.ResponseCandidate] = []
|
|
794
|
+
|
|
795
|
+
if base_eval_case.responses:
|
|
796
|
+
candidate_responses.append(base_eval_case.responses[0])
|
|
797
|
+
elif base_eval_case.agent_data:
|
|
798
|
+
candidate_responses.append(_create_placeholder_response_candidate(""))
|
|
799
|
+
elif getattr(base_eval_case, "interactions_data_source", None):
|
|
800
|
+
# Interaction data will be resolved server-side for metric
|
|
801
|
+
# computation; add a placeholder without warning.
|
|
802
|
+
candidate_responses.append(_create_placeholder_response_candidate(""))
|
|
803
|
+
else:
|
|
804
|
+
logger.warning(
|
|
805
|
+
"No response or agent data found for base dataset (index 0) in case %s. "
|
|
806
|
+
"Adding placeholder.",
|
|
807
|
+
case_idx,
|
|
808
|
+
)
|
|
809
|
+
candidate_responses.append(
|
|
810
|
+
_create_placeholder_response_candidate(
|
|
811
|
+
"Missing response from base dataset (0) for case %s" % case_idx
|
|
812
|
+
)
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
eval_case_custom_columns = base_eval_case.model_dump(
|
|
816
|
+
exclude={
|
|
817
|
+
"eval_case_id",
|
|
818
|
+
"prompt",
|
|
819
|
+
"responses",
|
|
820
|
+
"reference",
|
|
821
|
+
"system_instruction",
|
|
822
|
+
"conversation_history",
|
|
823
|
+
"intermediate_events",
|
|
824
|
+
"agent_data",
|
|
825
|
+
"agent_info",
|
|
826
|
+
},
|
|
827
|
+
exclude_none=True,
|
|
828
|
+
)
|
|
829
|
+
for dataset_idx_offset, current_parsed_ds in enumerate(datasets[1:], start=1):
|
|
830
|
+
current_ds_eval_case: types.EvalCase = (
|
|
831
|
+
current_parsed_ds.eval_cases[case_idx]
|
|
832
|
+
if current_parsed_ds.eval_cases
|
|
833
|
+
else types.EvalCase()
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
_validate_case_consistency(
|
|
837
|
+
base_eval_case, current_ds_eval_case, case_idx, dataset_idx_offset
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
current_ds_extra_attrs = current_ds_eval_case.model_dump(
|
|
841
|
+
exclude={
|
|
842
|
+
"eval_case_id",
|
|
843
|
+
"prompt",
|
|
844
|
+
"responses",
|
|
845
|
+
"reference",
|
|
846
|
+
"system_instruction",
|
|
847
|
+
"conversation_history",
|
|
848
|
+
"intermediate_events",
|
|
849
|
+
"agent_data",
|
|
850
|
+
"agent_info",
|
|
851
|
+
},
|
|
852
|
+
exclude_none=True,
|
|
853
|
+
)
|
|
854
|
+
eval_case_custom_columns.update(current_ds_extra_attrs)
|
|
855
|
+
|
|
856
|
+
if current_ds_eval_case.responses:
|
|
857
|
+
candidate_responses.append(current_ds_eval_case.responses[0])
|
|
858
|
+
elif current_ds_eval_case.agent_data:
|
|
859
|
+
candidate_responses.append(_create_placeholder_response_candidate(""))
|
|
860
|
+
elif getattr(current_ds_eval_case, "interactions_data_source", None):
|
|
861
|
+
# Interaction data will be resolved server-side for metric
|
|
862
|
+
# computation; add a placeholder without warning.
|
|
863
|
+
candidate_responses.append(_create_placeholder_response_candidate(""))
|
|
864
|
+
else:
|
|
865
|
+
logger.warning(
|
|
866
|
+
"No response or agent data found for dataset %s in case %s. Adding"
|
|
867
|
+
" placeholder.",
|
|
868
|
+
dataset_idx_offset,
|
|
869
|
+
case_idx,
|
|
870
|
+
)
|
|
871
|
+
candidate_responses.append(
|
|
872
|
+
_create_placeholder_response_candidate(
|
|
873
|
+
"Missing response from dataset %s for case %s"
|
|
874
|
+
% (dataset_idx_offset, case_idx)
|
|
875
|
+
)
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
merged_case = types.EvalCase(
|
|
879
|
+
eval_case_id=base_eval_case.eval_case_id
|
|
880
|
+
or "merged_eval_case_%s" % case_idx,
|
|
881
|
+
prompt=base_eval_case.prompt,
|
|
882
|
+
responses=candidate_responses if candidate_responses else None,
|
|
883
|
+
reference=base_eval_case.reference,
|
|
884
|
+
system_instruction=base_eval_case.system_instruction,
|
|
885
|
+
conversation_history=base_eval_case.conversation_history,
|
|
886
|
+
agent_info=agent_info or base_eval_case.agent_info,
|
|
887
|
+
agent_data=base_eval_case.agent_data,
|
|
888
|
+
intermediate_events=base_eval_case.intermediate_events,
|
|
889
|
+
**eval_case_custom_columns,
|
|
890
|
+
)
|
|
891
|
+
merged_eval_cases.append(merged_case)
|
|
892
|
+
|
|
893
|
+
return types.EvaluationDataset(eval_cases=merged_eval_cases)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def merge_response_datasets_into_canonical_format(
|
|
897
|
+
raw_datasets: list[list[dict[str, Any]]],
|
|
898
|
+
schemas: list[str],
|
|
899
|
+
agent_info: Optional[types.evals.AgentInfo] = None,
|
|
900
|
+
) -> types.EvaluationDataset:
|
|
901
|
+
"""Merges multiple raw response datasets into a single EvaluationDataset.
|
|
902
|
+
|
|
903
|
+
Assumes that each dataset in raw_datasets has responses corresponding
|
|
904
|
+
to the same set of prompts, in the same order. The prompt, reference,
|
|
905
|
+
system_instruction, and conversation_history are taken from the first dataset.
|
|
906
|
+
"""
|
|
907
|
+
if not isinstance(raw_datasets, list):
|
|
908
|
+
raise TypeError(
|
|
909
|
+
"Input 'raw_datasets' must be a list, got %s." % type(raw_datasets)
|
|
910
|
+
)
|
|
911
|
+
if not raw_datasets or not all(isinstance(ds, list) for ds in raw_datasets):
|
|
912
|
+
raise ValueError(
|
|
913
|
+
"Input 'raw_datasets' cannot be empty and must be a list of lists."
|
|
914
|
+
)
|
|
915
|
+
if not schemas or len(schemas) != len(raw_datasets):
|
|
916
|
+
raise ValueError(
|
|
917
|
+
"A list of schemas must be provided, one for each raw dataset. "
|
|
918
|
+
"Got %s schemas for %s datasets." % (len(schemas), len(raw_datasets))
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
parsed_evaluation_datasets: list[types.EvaluationDataset] = []
|
|
922
|
+
for i, (raw_ds_entry, schema) in enumerate(zip(raw_datasets, schemas)):
|
|
923
|
+
converter = get_dataset_converter(schema)
|
|
924
|
+
parsed_evaluation_datasets.append(converter.convert(raw_ds_entry))
|
|
925
|
+
|
|
926
|
+
return merge_evaluation_datasets(parsed_evaluation_datasets, agent_info)
|