opik-optimizer 1.0.6__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opik_optimizer/__init__.py +4 -0
- opik_optimizer/_throttle.py +2 -1
- opik_optimizer/base_optimizer.py +402 -28
- opik_optimizer/data/context7_eval.jsonl +3 -0
- opik_optimizer/datasets/context7_eval.py +90 -0
- opik_optimizer/datasets/tiny_test.py +33 -34
- opik_optimizer/datasets/truthful_qa.py +2 -2
- opik_optimizer/evolutionary_optimizer/crossover_ops.py +194 -0
- opik_optimizer/evolutionary_optimizer/evaluation_ops.py +136 -0
- opik_optimizer/evolutionary_optimizer/evolutionary_optimizer.py +289 -966
- opik_optimizer/evolutionary_optimizer/helpers.py +10 -0
- opik_optimizer/evolutionary_optimizer/llm_support.py +136 -0
- opik_optimizer/evolutionary_optimizer/mcp.py +249 -0
- opik_optimizer/evolutionary_optimizer/mutation_ops.py +306 -0
- opik_optimizer/evolutionary_optimizer/population_ops.py +228 -0
- opik_optimizer/evolutionary_optimizer/prompts.py +352 -0
- opik_optimizer/evolutionary_optimizer/reporting.py +28 -4
- opik_optimizer/evolutionary_optimizer/style_ops.py +86 -0
- opik_optimizer/few_shot_bayesian_optimizer/few_shot_bayesian_optimizer.py +90 -81
- opik_optimizer/few_shot_bayesian_optimizer/reporting.py +12 -5
- opik_optimizer/gepa_optimizer/__init__.py +3 -0
- opik_optimizer/gepa_optimizer/adapter.py +154 -0
- opik_optimizer/gepa_optimizer/gepa_optimizer.py +653 -0
- opik_optimizer/gepa_optimizer/reporting.py +181 -0
- opik_optimizer/logging_config.py +42 -7
- opik_optimizer/mcp_utils/__init__.py +22 -0
- opik_optimizer/mcp_utils/mcp.py +541 -0
- opik_optimizer/mcp_utils/mcp_second_pass.py +152 -0
- opik_optimizer/mcp_utils/mcp_simulator.py +116 -0
- opik_optimizer/mcp_utils/mcp_workflow.py +547 -0
- opik_optimizer/meta_prompt_optimizer/meta_prompt_optimizer.py +470 -134
- opik_optimizer/meta_prompt_optimizer/reporting.py +16 -2
- opik_optimizer/mipro_optimizer/_lm.py +30 -23
- opik_optimizer/mipro_optimizer/_mipro_optimizer_v2.py +52 -51
- opik_optimizer/mipro_optimizer/mipro_optimizer.py +126 -46
- opik_optimizer/mipro_optimizer/utils.py +2 -4
- opik_optimizer/optimizable_agent.py +21 -16
- opik_optimizer/optimization_config/chat_prompt.py +44 -23
- opik_optimizer/optimization_config/configs.py +3 -3
- opik_optimizer/optimization_config/mappers.py +9 -8
- opik_optimizer/optimization_result.py +22 -14
- opik_optimizer/reporting_utils.py +61 -10
- opik_optimizer/task_evaluator.py +9 -8
- opik_optimizer/utils/__init__.py +15 -0
- opik_optimizer/utils/colbert.py +236 -0
- opik_optimizer/{utils.py → utils/core.py} +160 -33
- opik_optimizer/utils/dataset_utils.py +49 -0
- opik_optimizer/utils/prompt_segments.py +186 -0
- opik_optimizer-2.0.0.dist-info/METADATA +345 -0
- opik_optimizer-2.0.0.dist-info/RECORD +74 -0
- opik_optimizer-2.0.0.dist-info/licenses/LICENSE +203 -0
- opik_optimizer-1.0.6.dist-info/METADATA +0 -181
- opik_optimizer-1.0.6.dist-info/RECORD +0 -50
- opik_optimizer-1.0.6.dist-info/licenses/LICENSE +0 -21
- {opik_optimizer-1.0.6.dist-info → opik_optimizer-2.0.0.dist-info}/WHEEL +0 -0
- {opik_optimizer-1.0.6.dist-info → opik_optimizer-2.0.0.dist-info}/top_level.txt +0 -0
@@ -1,42 +1,12 @@
|
|
1
1
|
import opik
|
2
2
|
|
3
|
-
TINY_TEST_ITEMS = [
|
4
|
-
{
|
5
|
-
"text": "What is the capital of France?",
|
6
|
-
"label": "Paris",
|
7
|
-
"metadata": {"context": "France is a country in Europe. Its capital is Paris."},
|
8
|
-
},
|
9
|
-
{
|
10
|
-
"text": "Who wrote Romeo and Juliet?",
|
11
|
-
"label": "William Shakespeare",
|
12
|
-
"metadata": {
|
13
|
-
"context": "Romeo and Juliet is a famous play written by William Shakespeare."
|
14
|
-
},
|
15
|
-
},
|
16
|
-
{
|
17
|
-
"text": "What is 2 + 2?",
|
18
|
-
"label": "4",
|
19
|
-
"metadata": {"context": "Basic arithmetic: 2 + 2 equals 4."},
|
20
|
-
},
|
21
|
-
{
|
22
|
-
"text": "What is the largest planet in our solar system?",
|
23
|
-
"label": "Jupiter",
|
24
|
-
"metadata": {"context": "Jupiter is the largest planet in our solar system."},
|
25
|
-
},
|
26
|
-
{
|
27
|
-
"text": "Who painted the Mona Lisa?",
|
28
|
-
"label": "Leonardo da Vinci",
|
29
|
-
"metadata": {"context": "The Mona Lisa was painted by Leonardo da Vinci."},
|
30
|
-
},
|
31
|
-
]
|
32
|
-
|
33
3
|
|
34
4
|
def tiny_test(test_mode: bool = False) -> opik.Dataset:
|
35
5
|
"""
|
36
|
-
|
6
|
+
Tiny QA benchmark (core_en subset from vincentkoc/tiny_qa_benchmark_pp).
|
37
7
|
"""
|
38
8
|
dataset_name = "tiny_test" if not test_mode else "tiny_test_test"
|
39
|
-
nb_items =
|
9
|
+
nb_items = 5 # keep tiny dataset size consistent with tests/docs
|
40
10
|
|
41
11
|
client = opik.Opik()
|
42
12
|
dataset = client.get_or_create_dataset(dataset_name)
|
@@ -49,5 +19,34 @@ def tiny_test(test_mode: bool = False) -> opik.Dataset:
|
|
49
19
|
f"Dataset {dataset_name} contains {len(items)} items, expected {nb_items}. We recommend deleting the dataset and re-creating it."
|
50
20
|
)
|
51
21
|
elif len(items) == 0:
|
52
|
-
|
53
|
-
|
22
|
+
import datasets as ds
|
23
|
+
|
24
|
+
download_config = ds.DownloadConfig(download_desc=False, disable_tqdm=True)
|
25
|
+
ds.disable_progress_bar()
|
26
|
+
try:
|
27
|
+
# Load only the core_en subset JSONL from the repo
|
28
|
+
# Use the generic JSON loader with streaming for efficiency
|
29
|
+
hf_dataset = ds.load_dataset(
|
30
|
+
"json",
|
31
|
+
data_files="hf://datasets/vincentkoc/tiny_qa_benchmark_pp/data/core_en/core_en.jsonl",
|
32
|
+
streaming=True,
|
33
|
+
download_config=download_config,
|
34
|
+
)["train"]
|
35
|
+
|
36
|
+
data = []
|
37
|
+
for i, item in enumerate(hf_dataset):
|
38
|
+
if i >= nb_items:
|
39
|
+
break
|
40
|
+
data.append(
|
41
|
+
{
|
42
|
+
"text": item.get("text", ""),
|
43
|
+
"label": item.get("label", ""),
|
44
|
+
# Preserve original tiny_test shape with metadata.context
|
45
|
+
"metadata": {"context": item.get("context", "")},
|
46
|
+
}
|
47
|
+
)
|
48
|
+
|
49
|
+
dataset.insert(data)
|
50
|
+
return dataset
|
51
|
+
finally:
|
52
|
+
ds.enable_progress_bar()
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import opik
|
2
|
-
from typing import Any
|
2
|
+
from typing import Any
|
3
3
|
|
4
4
|
|
5
5
|
def truthful_qa(test_mode: bool = False) -> opik.Dataset:
|
@@ -33,7 +33,7 @@ def truthful_qa(test_mode: bool = False) -> opik.Dataset:
|
|
33
33
|
"truthful_qa", "multiple_choice", download_config=download_config
|
34
34
|
)
|
35
35
|
|
36
|
-
data:
|
36
|
+
data: list[dict[str, Any]] = []
|
37
37
|
for gen_item, mc_item in zip(
|
38
38
|
gen_dataset["validation"], mc_dataset["validation"]
|
39
39
|
):
|
@@ -0,0 +1,194 @@
|
|
1
|
+
from typing import Any, TYPE_CHECKING
|
2
|
+
|
3
|
+
import logging
|
4
|
+
import random
|
5
|
+
import json
|
6
|
+
|
7
|
+
from deap import creator as _creator
|
8
|
+
|
9
|
+
from . import prompts as evo_prompts
|
10
|
+
from . import reporting
|
11
|
+
from .. import utils
|
12
|
+
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
creator = _creator # backward compt.
|
16
|
+
|
17
|
+
|
18
|
+
class CrossoverOps:
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
verbose: int
|
21
|
+
output_style_guidance: str
|
22
|
+
_call_model: Any
|
23
|
+
|
24
|
+
def _deap_crossover_chunking_strategy(
|
25
|
+
self, messages_1_str: str, messages_2_str: str
|
26
|
+
) -> tuple[str, str]:
|
27
|
+
chunks1 = [
|
28
|
+
chunk.strip() for chunk in messages_1_str.split(".") if chunk.strip()
|
29
|
+
]
|
30
|
+
chunks2 = [
|
31
|
+
chunk.strip() for chunk in messages_2_str.split(".") if chunk.strip()
|
32
|
+
]
|
33
|
+
|
34
|
+
if len(chunks1) >= 2 and len(chunks2) >= 2:
|
35
|
+
min_num_chunks = min(len(chunks1), len(chunks2))
|
36
|
+
point = random.randint(1, min_num_chunks - 1)
|
37
|
+
child1_chunks = chunks1[:point] + chunks2[point:]
|
38
|
+
child2_chunks = chunks2[:point] + chunks1[point:]
|
39
|
+
child1_str = ". ".join(child1_chunks) + ("." if child1_chunks else "")
|
40
|
+
child2_str = ". ".join(child2_chunks) + ("." if child2_chunks else "")
|
41
|
+
return child1_str, child2_str
|
42
|
+
else:
|
43
|
+
raise ValueError(
|
44
|
+
"Not enough chunks in either prompt for chunk-level crossover"
|
45
|
+
)
|
46
|
+
|
47
|
+
def _deap_crossover_word_level(
|
48
|
+
self, messages_1_str: str, messages_2_str: str
|
49
|
+
) -> tuple[str, str]:
|
50
|
+
words1 = messages_1_str.split()
|
51
|
+
words2 = messages_2_str.split()
|
52
|
+
if not words1 or not words2:
|
53
|
+
return messages_1_str, messages_2_str
|
54
|
+
min_word_len = min(len(words1), len(words2))
|
55
|
+
if min_word_len < 2:
|
56
|
+
return messages_1_str, messages_2_str
|
57
|
+
point = random.randint(1, min_word_len - 1)
|
58
|
+
child1_words = words1[:point] + words2[point:]
|
59
|
+
child2_words = words2[:point] + words1[point:]
|
60
|
+
return " ".join(child1_words), " ".join(child2_words)
|
61
|
+
|
62
|
+
def _deap_crossover(self, ind1: Any, ind2: Any) -> tuple[Any, Any]:
|
63
|
+
"""Crossover operation that preserves semantic meaning.
|
64
|
+
Attempts chunk-level crossover first, then falls back to word-level.
|
65
|
+
"""
|
66
|
+
reporting.display_message(
|
67
|
+
" Recombining prompts by mixing and matching words and sentences.",
|
68
|
+
verbose=self.verbose,
|
69
|
+
)
|
70
|
+
messages_1_orig: list[dict[str, str]] = ind1
|
71
|
+
messages_2_orig: list[dict[str, str]] = ind2
|
72
|
+
|
73
|
+
for i, message_1 in enumerate(messages_1_orig):
|
74
|
+
role: str = message_1["role"]
|
75
|
+
message_1_str: str = message_1["content"]
|
76
|
+
if (len(messages_2_orig) >= i + 1) and (messages_2_orig[i]["role"] == role):
|
77
|
+
message_2 = messages_2_orig[i]
|
78
|
+
message_2_str: str = message_2["content"]
|
79
|
+
try:
|
80
|
+
child1_str, child2_str = self._deap_crossover_chunking_strategy(
|
81
|
+
message_1_str, message_2_str
|
82
|
+
)
|
83
|
+
except ValueError:
|
84
|
+
child1_str, child2_str = self._deap_crossover_word_level(
|
85
|
+
message_1_str, message_2_str
|
86
|
+
)
|
87
|
+
messages_1_orig[i]["content"] = child1_str
|
88
|
+
messages_2_orig[i]["content"] = child2_str
|
89
|
+
else:
|
90
|
+
pass
|
91
|
+
|
92
|
+
return creator.Individual(messages_1_orig), creator.Individual(messages_2_orig)
|
93
|
+
|
94
|
+
def _llm_deap_crossover(self, ind1: Any, ind2: Any) -> tuple[Any, Any]:
|
95
|
+
"""Perform crossover by asking an LLM to blend two parent prompts."""
|
96
|
+
reporting.display_message(
|
97
|
+
" Recombining prompts using an LLM.", verbose=self.verbose
|
98
|
+
)
|
99
|
+
|
100
|
+
parent1_messages: list[dict[str, str]] = ind1
|
101
|
+
parent2_messages: list[dict[str, str]] = ind2
|
102
|
+
current_output_style_guidance = self.output_style_guidance
|
103
|
+
|
104
|
+
user_prompt_for_llm_crossover = evo_prompts.llm_crossover_user_prompt(
|
105
|
+
parent1_messages, parent2_messages, current_output_style_guidance
|
106
|
+
)
|
107
|
+
try:
|
108
|
+
logger.debug(
|
109
|
+
f"Attempting LLM-driven crossover between: '{parent1_messages[:50]}...' and '{parent2_messages[:50]}...' aiming for style: '{current_output_style_guidance[:30]}...'"
|
110
|
+
)
|
111
|
+
response_content = self._call_model(
|
112
|
+
messages=[
|
113
|
+
{
|
114
|
+
"role": "system",
|
115
|
+
"content": evo_prompts.llm_crossover_system_prompt(
|
116
|
+
current_output_style_guidance
|
117
|
+
),
|
118
|
+
},
|
119
|
+
{"role": "user", "content": user_prompt_for_llm_crossover},
|
120
|
+
],
|
121
|
+
is_reasoning=True,
|
122
|
+
)
|
123
|
+
logger.debug(f"Raw LLM response for crossover: {response_content}")
|
124
|
+
|
125
|
+
# First, try strict JSON parsing
|
126
|
+
json_response = None
|
127
|
+
try:
|
128
|
+
json_response = utils.json_to_dict(response_content)
|
129
|
+
except Exception:
|
130
|
+
# Continue with heuristic extraction below
|
131
|
+
json_response = None
|
132
|
+
children: list[list[dict[str, str]]] = []
|
133
|
+
if isinstance(json_response, list):
|
134
|
+
children = [c for c in json_response if isinstance(c, list)]
|
135
|
+
|
136
|
+
# If strict parse failed to yield children, try extracting arrays heuristically
|
137
|
+
if not children:
|
138
|
+
extracted = self._extract_json_arrays(response_content)
|
139
|
+
for arr in extracted:
|
140
|
+
try:
|
141
|
+
parsed = json.loads(arr)
|
142
|
+
if isinstance(parsed, list) and all(
|
143
|
+
isinstance(m, dict) and {"role", "content"} <= set(m.keys())
|
144
|
+
for m in parsed
|
145
|
+
):
|
146
|
+
children.append(parsed)
|
147
|
+
except Exception:
|
148
|
+
continue
|
149
|
+
|
150
|
+
if len(children) == 0:
|
151
|
+
raise ValueError("LLM response did not include any valid child prompts")
|
152
|
+
|
153
|
+
# We only need two children; if only one returned, duplicate pattern from DEAP
|
154
|
+
first_child = children[0]
|
155
|
+
second_child = children[1] if len(children) > 1 else children[0]
|
156
|
+
return creator.Individual(first_child), creator.Individual(second_child)
|
157
|
+
except Exception as e:
|
158
|
+
logger.warning(
|
159
|
+
f"LLM-driven crossover failed: {e}. Falling back to DEAP crossover."
|
160
|
+
)
|
161
|
+
return self._deap_crossover(ind1, ind2)
|
162
|
+
|
163
|
+
def _extract_json_arrays(self, text: str) -> list[str]:
|
164
|
+
"""Extract top-level JSON array substrings from arbitrary text.
|
165
|
+
This helps when models return multiple arrays like `[...],\n[...]`.
|
166
|
+
"""
|
167
|
+
arrays: list[str] = []
|
168
|
+
depth = 0
|
169
|
+
start: int | None = None
|
170
|
+
in_str = False
|
171
|
+
escape = False
|
172
|
+
for i, ch in enumerate(text):
|
173
|
+
if escape:
|
174
|
+
# current char is escaped; skip special handling
|
175
|
+
escape = False
|
176
|
+
continue
|
177
|
+
if ch == "\\":
|
178
|
+
escape = True
|
179
|
+
continue
|
180
|
+
if ch == '"':
|
181
|
+
in_str = not in_str
|
182
|
+
continue
|
183
|
+
if in_str:
|
184
|
+
continue
|
185
|
+
if ch == "[":
|
186
|
+
if depth == 0:
|
187
|
+
start = i
|
188
|
+
depth += 1
|
189
|
+
elif ch == "]" and depth > 0:
|
190
|
+
depth -= 1
|
191
|
+
if depth == 0 and start is not None:
|
192
|
+
arrays.append(text[start : i + 1])
|
193
|
+
start = None
|
194
|
+
return arrays
|
@@ -0,0 +1,136 @@
|
|
1
|
+
from typing import Any, TYPE_CHECKING, cast
|
2
|
+
from collections.abc import Callable
|
3
|
+
|
4
|
+
|
5
|
+
from .. import task_evaluator
|
6
|
+
from ..optimization_config import mappers, chat_prompt
|
7
|
+
from ..mcp_utils.mcp_workflow import MCPExecutionConfig
|
8
|
+
import opik
|
9
|
+
import copy
|
10
|
+
|
11
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
12
|
+
from ..base_optimizer import BaseOptimizer
|
13
|
+
|
14
|
+
|
15
|
+
class EvaluationOps:
|
16
|
+
if TYPE_CHECKING:
|
17
|
+
agent_class: type[Any]
|
18
|
+
num_threads: int
|
19
|
+
|
20
|
+
def _evaluate_prompt(
|
21
|
+
self,
|
22
|
+
prompt: chat_prompt.ChatPrompt,
|
23
|
+
messages: list[dict[str, str]],
|
24
|
+
dataset: opik.Dataset,
|
25
|
+
metric: Callable,
|
26
|
+
n_samples: int | None = None,
|
27
|
+
dataset_item_ids: list[str] | None = None,
|
28
|
+
experiment_config: dict | None = None,
|
29
|
+
optimization_id: str | None = None,
|
30
|
+
verbose: int = 0,
|
31
|
+
**kwargs: Any,
|
32
|
+
) -> float:
|
33
|
+
"""Evaluate a single prompt (individual) against the dataset and return the score."""
|
34
|
+
total_items = len(dataset.get_items())
|
35
|
+
|
36
|
+
new_prompt = prompt.copy()
|
37
|
+
new_prompt.set_messages(messages)
|
38
|
+
tools = getattr(messages, "tools", None)
|
39
|
+
if tools is not None:
|
40
|
+
new_prompt.tools = copy.deepcopy(tools)
|
41
|
+
|
42
|
+
optimizer = cast("BaseOptimizer", self)
|
43
|
+
|
44
|
+
configuration_updates = optimizer._drop_none(
|
45
|
+
{
|
46
|
+
"n_samples_for_eval": (
|
47
|
+
len(dataset_item_ids) if dataset_item_ids is not None else n_samples
|
48
|
+
),
|
49
|
+
"total_dataset_items": total_items,
|
50
|
+
}
|
51
|
+
)
|
52
|
+
evaluation_details = optimizer._drop_none(
|
53
|
+
{
|
54
|
+
"dataset_item_ids": dataset_item_ids,
|
55
|
+
"optimization_id": optimization_id,
|
56
|
+
}
|
57
|
+
)
|
58
|
+
additional_metadata = (
|
59
|
+
{"evaluation": evaluation_details} if evaluation_details else None
|
60
|
+
)
|
61
|
+
|
62
|
+
experiment_config = optimizer._prepare_experiment_config(
|
63
|
+
prompt=new_prompt,
|
64
|
+
dataset=dataset,
|
65
|
+
metric=metric,
|
66
|
+
experiment_config=experiment_config,
|
67
|
+
configuration_updates=configuration_updates,
|
68
|
+
additional_metadata=additional_metadata,
|
69
|
+
)
|
70
|
+
try:
|
71
|
+
agent = self.agent_class(new_prompt)
|
72
|
+
except Exception:
|
73
|
+
return 0.0
|
74
|
+
|
75
|
+
mcp_execution_config: MCPExecutionConfig | None = kwargs.get("mcp_config")
|
76
|
+
|
77
|
+
def llm_task(dataset_item: dict[str, Any]) -> dict[str, str]:
|
78
|
+
messages = new_prompt.get_messages(dataset_item)
|
79
|
+
|
80
|
+
if mcp_execution_config is None:
|
81
|
+
model_output = agent.invoke(messages)
|
82
|
+
return {mappers.EVALUATED_LLM_TASK_OUTPUT: model_output}
|
83
|
+
|
84
|
+
coordinator = mcp_execution_config.coordinator
|
85
|
+
coordinator.reset()
|
86
|
+
|
87
|
+
raw_model_output = agent.llm_invoke(
|
88
|
+
messages=messages,
|
89
|
+
seed=getattr(self, "seed", None),
|
90
|
+
allow_tool_use=True,
|
91
|
+
)
|
92
|
+
|
93
|
+
second_pass_messages = coordinator.build_second_pass_messages(
|
94
|
+
base_messages=messages,
|
95
|
+
dataset_item=dataset_item,
|
96
|
+
)
|
97
|
+
|
98
|
+
if (
|
99
|
+
second_pass_messages is None
|
100
|
+
and mcp_execution_config.fallback_invoker is not None
|
101
|
+
):
|
102
|
+
fallback_args = mcp_execution_config.fallback_arguments(dataset_item)
|
103
|
+
if fallback_args:
|
104
|
+
summary_override = mcp_execution_config.fallback_invoker(
|
105
|
+
fallback_args
|
106
|
+
)
|
107
|
+
second_pass_messages = coordinator.build_second_pass_messages(
|
108
|
+
base_messages=messages,
|
109
|
+
dataset_item=dataset_item,
|
110
|
+
summary_override=summary_override,
|
111
|
+
)
|
112
|
+
|
113
|
+
if second_pass_messages is not None:
|
114
|
+
final_response = agent.llm_invoke(
|
115
|
+
messages=second_pass_messages,
|
116
|
+
seed=getattr(self, "seed", None),
|
117
|
+
allow_tool_use=mcp_execution_config.allow_tool_use_on_second_pass,
|
118
|
+
)
|
119
|
+
else:
|
120
|
+
final_response = raw_model_output
|
121
|
+
|
122
|
+
return {mappers.EVALUATED_LLM_TASK_OUTPUT: final_response.strip()}
|
123
|
+
|
124
|
+
score = task_evaluator.evaluate(
|
125
|
+
dataset=dataset,
|
126
|
+
dataset_item_ids=dataset_item_ids,
|
127
|
+
metric=metric,
|
128
|
+
evaluated_task=llm_task,
|
129
|
+
num_threads=self.num_threads,
|
130
|
+
project_name=experiment_config.get("project_name"),
|
131
|
+
n_samples=n_samples if dataset_item_ids is None else None,
|
132
|
+
experiment_config=experiment_config,
|
133
|
+
optimization_id=optimization_id,
|
134
|
+
verbose=verbose,
|
135
|
+
)
|
136
|
+
return score
|