opik-optimizer 2.0.1__py3-none-any.whl → 2.1.1__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.
@@ -0,0 +1,214 @@
1
+ """Parameter specification for defining tunable parameters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any, Literal
7
+ from collections.abc import Mapping, Sequence
8
+
9
+ from optuna.trial import Trial
10
+ from pydantic import BaseModel, Field, PrivateAttr, model_validator
11
+
12
+ from .search_space_types import ParameterType, ResolvedTarget
13
+
14
+
15
+ class ParameterSpec(BaseModel):
16
+ """Definition for a single tunable parameter."""
17
+
18
+ name: str
19
+ description: str | None = None
20
+ distribution: ParameterType = Field(alias="type")
21
+ low: float | None = Field(default=None, alias="min")
22
+ high: float | None = Field(default=None, alias="max")
23
+ step: float | None = None
24
+ scale: Literal["linear", "log"] = "linear"
25
+ choices: list[Any] | None = None
26
+ target: str | Sequence[str] | None = None
27
+ default: Any | None = None
28
+
29
+ model_config = {
30
+ "populate_by_name": True,
31
+ "extra": "forbid",
32
+ }
33
+
34
+ _resolved_target: ResolvedTarget | None = PrivateAttr(default=None)
35
+
36
+ @model_validator(mode="before")
37
+ @classmethod
38
+ def _coerce_aliases(cls, data: Any) -> Any:
39
+ if isinstance(data, Mapping):
40
+ data = dict(data)
41
+ if "values" in data and "choices" not in data:
42
+ data["choices"] = data.pop("values")
43
+ if "selection" in data and "choices" not in data:
44
+ data["choices"] = data.pop("selection")
45
+ if "path" in data and "target" not in data:
46
+ data["target"] = data.pop("path")
47
+ return data
48
+
49
+ @model_validator(mode="after")
50
+ def _validate(self) -> ParameterSpec:
51
+ if self.distribution in {ParameterType.FLOAT, ParameterType.INT}:
52
+ if self.low is None or self.high is None:
53
+ raise ValueError(
54
+ "'min' and 'max' must be provided for range parameters"
55
+ )
56
+ if self.low >= self.high:
57
+ raise ValueError("'min' must be less than 'max'")
58
+ if self.scale not in {"linear", "log"}:
59
+ raise ValueError("scale must be 'linear' or 'log'")
60
+ if self.scale == "log" and (self.low <= 0 or self.high <= 0):
61
+ raise ValueError("log-scaled parameters require positive bounds")
62
+ if self.step is not None and self.step <= 0:
63
+ raise ValueError("step must be positive when provided")
64
+
65
+ if self.distribution == ParameterType.INT:
66
+ object.__setattr__(self, "low", int(self.low))
67
+ object.__setattr__(self, "high", int(self.high))
68
+ if self.step is not None:
69
+ object.__setattr__(self, "step", int(self.step))
70
+ elif self.distribution == ParameterType.CATEGORICAL:
71
+ if not self.choices:
72
+ raise ValueError("categorical parameters require non-empty 'choices'")
73
+ elif self.distribution == ParameterType.BOOL:
74
+ if not self.choices:
75
+ object.__setattr__(self, "choices", [False, True])
76
+ else: # pragma: no cover - safety fallback
77
+ raise ValueError(f"Unsupported distribution: {self.distribution}")
78
+
79
+ object.__setattr__(self, "_resolved_target", self._resolve_target())
80
+ return self
81
+
82
+ @property
83
+ def target_path(self) -> ResolvedTarget:
84
+ if self._resolved_target is None:
85
+ self._resolved_target = self._resolve_target()
86
+ return self._resolved_target
87
+
88
+ def suggest(self, trial: Trial) -> Any:
89
+ """Return a sampled value for this parameter from Optuna."""
90
+ if self.distribution == ParameterType.FLOAT:
91
+ assert self.low is not None and self.high is not None # validated earlier
92
+ return trial.suggest_float(
93
+ self.name,
94
+ float(self.low),
95
+ float(self.high),
96
+ step=self.step,
97
+ log=self.scale == "log",
98
+ )
99
+ if self.distribution == ParameterType.INT:
100
+ assert self.low is not None and self.high is not None # validated earlier
101
+ return trial.suggest_int(
102
+ self.name,
103
+ int(self.low),
104
+ int(self.high),
105
+ step=int(self.step) if self.step is not None else 1,
106
+ log=self.scale == "log",
107
+ )
108
+ if self.distribution in {ParameterType.CATEGORICAL, ParameterType.BOOL}:
109
+ assert self.choices is not None # guarded in validators
110
+ return trial.suggest_categorical(self.name, list(self.choices))
111
+ raise RuntimeError(f"Unsupported distribution type: {self.distribution}")
112
+
113
+ def apply_to_prompt(
114
+ self,
115
+ prompt: Any,
116
+ value: Any, # ChatPrompt type
117
+ ) -> None:
118
+ """Apply a sampled value to the provided prompt instance."""
119
+ resolved = self.target_path
120
+ if resolved.root == "model":
121
+ if resolved.path:
122
+ raise ValueError("Nested paths under 'model' are not supported")
123
+ prompt.model = value
124
+ return
125
+
126
+ if prompt.model_kwargs is None:
127
+ prompt.model_kwargs = {}
128
+
129
+ self._assign_nested(prompt.model_kwargs, resolved.path, value)
130
+
131
+ def apply_to_model_kwargs(self, model_kwargs: dict[str, Any], value: Any) -> None:
132
+ """Apply a sampled value to a model_kwargs dictionary."""
133
+ resolved = self.target_path
134
+ if resolved.root != "model_kwargs":
135
+ return
136
+ self._assign_nested(model_kwargs, resolved.path, value)
137
+
138
+ def narrow(self, center: Any, scale: float) -> ParameterSpec:
139
+ """Return a narrowed version of the spec around the provided center."""
140
+
141
+ if center is None or scale <= 0:
142
+ return self
143
+
144
+ if self.distribution in {ParameterType.FLOAT, ParameterType.INT}:
145
+ if self.low is None or self.high is None:
146
+ return self
147
+
148
+ span = float(self.high) - float(self.low)
149
+ if span <= 0:
150
+ return self
151
+
152
+ half_window = span * float(scale) / 2
153
+ if half_window <= 0:
154
+ return self
155
+
156
+ center_val = float(center)
157
+ new_low = max(float(self.low), center_val - half_window)
158
+ new_high = min(float(self.high), center_val + half_window)
159
+
160
+ if self.distribution == ParameterType.INT:
161
+ new_low = math.floor(new_low)
162
+ new_high = math.ceil(new_high)
163
+ if new_low == new_high:
164
+ new_high = min(int(self.high), new_low + 1)
165
+ if new_low == new_high:
166
+ return self
167
+
168
+ if new_low >= new_high:
169
+ return self
170
+
171
+ spec_dict = self.model_dump(by_alias=True)
172
+ spec_dict["min"] = new_low
173
+ spec_dict["max"] = new_high
174
+ return ParameterSpec.model_validate(spec_dict)
175
+
176
+ # Non-numeric parameters remain unchanged
177
+ return self
178
+
179
+ def _assign_nested(
180
+ self, container: dict[str, Any], path: Sequence[str], value: Any
181
+ ) -> None:
182
+ if not path:
183
+ container[self.name] = value
184
+ return
185
+ current = container
186
+ for key in path[:-1]:
187
+ next_val = current.get(key)
188
+ if not isinstance(next_val, dict):
189
+ next_val = {}
190
+ current[key] = next_val
191
+ current = next_val
192
+ current[path[-1]] = value
193
+
194
+ def _resolve_target(self) -> ResolvedTarget:
195
+ target = self.target
196
+ if target is None:
197
+ return ResolvedTarget("model_kwargs", (self.name,))
198
+
199
+ if isinstance(target, str):
200
+ tokens = tuple(filter(None, (part.strip() for part in target.split("."))))
201
+ else:
202
+ tokens = tuple(target)
203
+
204
+ if not tokens:
205
+ return ResolvedTarget("model_kwargs", (self.name,))
206
+
207
+ root = tokens[0]
208
+ path = tokens[1:]
209
+
210
+ if root not in {"model", "model_kwargs"}:
211
+ root = "model_kwargs"
212
+ path = tokens
213
+
214
+ return ResolvedTarget(root, tuple(path)) # type: ignore[arg-type]
@@ -0,0 +1,24 @@
1
+ """Type definitions for parameter search space."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Literal
8
+
9
+
10
+ class ParameterType(str, Enum):
11
+ """Supported parameter distribution types."""
12
+
13
+ FLOAT = "float"
14
+ INT = "int"
15
+ CATEGORICAL = "categorical"
16
+ BOOL = "bool"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ResolvedTarget:
21
+ """Resolved target location for a parameter."""
22
+
23
+ root: Literal["model", "model_kwargs"]
24
+ path: tuple[str, ...]
@@ -0,0 +1,71 @@
1
+ """Sensitivity analysis utilities for parameter optimization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from optuna.trial import Trial
10
+
11
+ from .search_space import ParameterSpec
12
+
13
+
14
+ def compute_sensitivity_from_trials(
15
+ trials: list[Trial], specs: list[ParameterSpec]
16
+ ) -> dict[str, float]:
17
+ """
18
+ Compute parameter sensitivity from completed trials.
19
+
20
+ This function calculates a correlation-based sensitivity measure for each parameter
21
+ by analyzing how changes in parameter values correlate with changes in the objective
22
+ function values across trials.
23
+
24
+ Args:
25
+ trials: List of completed Optuna trials
26
+ specs: List of parameter specifications
27
+
28
+ Returns:
29
+ Dictionary mapping parameter names to sensitivity scores (0.0 to 1.0)
30
+ """
31
+ sensitivities: dict[str, float] = {}
32
+
33
+ for spec in specs:
34
+ param_name = spec.name
35
+ values: list[float] = []
36
+ scores: list[float] = []
37
+
38
+ for trial in trials:
39
+ if trial.value is None:
40
+ continue
41
+
42
+ raw_value = trial.params.get(param_name)
43
+ if isinstance(raw_value, bool):
44
+ processed = float(int(raw_value))
45
+ elif isinstance(raw_value, (int, float)):
46
+ processed = float(raw_value)
47
+ else:
48
+ continue
49
+
50
+ values.append(processed)
51
+ scores.append(float(trial.value))
52
+
53
+ if len(values) < 2 or len(set(values)) == 1:
54
+ sensitivities[param_name] = 0.0
55
+ continue
56
+
57
+ mean_val = sum(values) / len(values)
58
+ mean_score = sum(scores) / len(scores)
59
+
60
+ cov = sum((v - mean_val) * (s - mean_score) for v, s in zip(values, scores))
61
+ var_val = sum((v - mean_val) ** 2 for v in values)
62
+ var_score = sum((s - mean_score) ** 2 for s in scores)
63
+
64
+ if var_val <= 0 or var_score <= 0:
65
+ sensitivities[param_name] = 0.0
66
+ continue
67
+
68
+ corr = abs(cov) / math.sqrt(var_val * var_score)
69
+ sensitivities[param_name] = min(max(corr, 0.0), 1.0)
70
+
71
+ return sensitivities
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opik_optimizer
3
- Version: 2.0.1
3
+ Version: 2.1.1
4
4
  Summary: Agent optimization with Opik
5
5
  Home-page: https://github.com/comet-ml/opik
6
6
  Author: Comet ML
@@ -18,7 +18,7 @@ Requires-Dist: dspy<3
18
18
  Requires-Dist: gepa>=0.0.7
19
19
  Requires-Dist: ujson
20
20
  Requires-Dist: hf_xet
21
- Requires-Dist: litellm
21
+ Requires-Dist: litellm<=1.75.6
22
22
  Requires-Dist: mcp>=1.0.0
23
23
  Requires-Dist: opik>=1.7.17
24
24
  Requires-Dist: optuna
@@ -32,6 +32,7 @@ Requires-Dist: pytest; extra == "dev"
32
32
  Requires-Dist: pytest-cov; extra == "dev"
33
33
  Requires-Dist: langgraph; extra == "dev"
34
34
  Requires-Dist: pre-commit; extra == "dev"
35
+ Requires-Dist: scikit-learn; extra == "dev"
35
36
  Dynamic: author
36
37
  Dynamic: home-page
37
38
  Dynamic: license-file
@@ -51,6 +52,7 @@ The Opik Agent Optimizer refines your prompts to achieve better performance from
51
52
  * **MetaPromptOptimizer** - Employs meta-prompting techniques for optimization
52
53
  * **MiproOptimizer** - Implements MIPRO (Multi-Input Prompt Optimization) algorithm
53
54
  * **GepaOptimizer** - Leverages GEPA (Genetic-Pareto) optimization approach
55
+ * **ParameterOptimizer** - Optimizes LLM call parameters (temperature, top_p, etc.) using Bayesian optimization
54
56
 
55
57
  ## 🎯 Key Features
56
58
 
@@ -1,10 +1,10 @@
1
- opik_optimizer/__init__.py,sha256=bNNFoAJmORQ38vHPZsOb3m4Gb07-jjqPy-MbMIiTop8,1234
1
+ opik_optimizer/__init__.py,sha256=VwryQ5bSOmJSl4CiacCIv_UF_In8Zho54fQ3FUR8pyk,1573
2
2
  opik_optimizer/_throttle.py,sha256=1JXIhYlo0IaqCgwmNB0Hnh9CYhYPkwRFdVGIcE7pVNg,1362
3
- opik_optimizer/base_optimizer.py,sha256=QPGLOzdlniSCIGOPpsz6_1CrvC0NEXmVQqmRIOwIv7E,20021
3
+ opik_optimizer/base_optimizer.py,sha256=TKQknIvhJ1H5LOxhhkXIzjEepx3h0j0jyNsTGZ7EFLI,21410
4
4
  opik_optimizer/cache_config.py,sha256=Xd3NdUsL7bLQWoNe3pESqH4nHucU1iNTSGp-RqbwDog,599
5
5
  opik_optimizer/logging_config.py,sha256=TmxX0C1P20amxoXuiNQvlENOjdSNfWwvL8jFy206VWM,3837
6
6
  opik_optimizer/optimizable_agent.py,sha256=R0_BdwdHyZGWTw3oSvTg8FULDOYM8XaTiPNR3qV8DkQ,6344
7
- opik_optimizer/optimization_result.py,sha256=cscPGDNvkh88xAKhlU-nqaws-wXcYdJ7uwlCcshicwo,8112
7
+ opik_optimizer/optimization_result.py,sha256=sG-Yr-hOaH9zx_I5S6_W3v6j8nPUhwYdS333jVM4Gus,17218
8
8
  opik_optimizer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  opik_optimizer/reporting_utils.py,sha256=dcECFmzZ_J-DKoukMDEE_fm7X8sdQyl_ijTddvQtepE,8287
10
10
  opik_optimizer/task_evaluator.py,sha256=1hILYwJLtn7XpPX96JjubnlMasmudVTHMVK3pmd22bE,4312
@@ -45,6 +45,12 @@ opik_optimizer/gepa_optimizer/__init__.py,sha256=XcPah5t4mop7UCFo69E9l45Mem49-it
45
45
  opik_optimizer/gepa_optimizer/adapter.py,sha256=KzPa4koq7aJhALMAOKPxAO4yWuEy_YbW7tGnqny3Hfo,5139
46
46
  opik_optimizer/gepa_optimizer/gepa_optimizer.py,sha256=DqTjkDeR7WEMt-53MSzDbD_72FRiwCCGxkwEyrLaQ4A,26430
47
47
  opik_optimizer/gepa_optimizer/reporting.py,sha256=F0cxYSjRuFAszgi3rgqwH1A-KH26kZOLtENP7x1xrQs,5154
48
+ opik_optimizer/hierarchical_reflective_optimizer/__init__.py,sha256=9qM3kvfAaFy-Y6Tg19MXHJxpnF5DJQQwzr6oNsxaRBM,133
49
+ opik_optimizer/hierarchical_reflective_optimizer/hierarchical_reflective_optimizer.py,sha256=Fs83ztOuPS8mkFvJAVmYok15DaXTk4Jqpoa9ImRl2t4,27256
50
+ opik_optimizer/hierarchical_reflective_optimizer/hierarchical_root_cause_analyzer.py,sha256=GSIXUBxoS9LFnCXopS6B6wLSpmCYXA8Cv6ytELgEBoc,12709
51
+ opik_optimizer/hierarchical_reflective_optimizer/prompts.py,sha256=XcOEI9eeEbTgKFsFiRWxvHdaByQkiN02bH2gTl3HX-Y,3853
52
+ opik_optimizer/hierarchical_reflective_optimizer/reporting.py,sha256=LpHv_WBZCg2a0RhZaGwUmCch_-Dfk_rpuMxTckJMWTU,23234
53
+ opik_optimizer/hierarchical_reflective_optimizer/types.py,sha256=bS-JAheX2FpJ4XAxoZi5PfjloG8L-B1LGQA1iLXZhW4,1031
48
54
  opik_optimizer/mcp_utils/__init__.py,sha256=BsWQT8nAa6JV6zcOD__OvPMepUS2IpJD4J2rnAXhpuU,710
49
55
  opik_optimizer/mcp_utils/mcp.py,sha256=UylgpTJsybszS433_kuTAgKH-PPde-VHjHVelMardFs,18466
50
56
  opik_optimizer/mcp_utils/mcp_second_pass.py,sha256=p2Knlxg1CKIZVMBbdskdRDqw1BRrnjM4gkcwAZtggm8,4519
@@ -62,13 +68,19 @@ opik_optimizer/optimization_config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQe
62
68
  opik_optimizer/optimization_config/chat_prompt.py,sha256=d3jwM1UvUeRQOSsYHa5GD842VO3JWjVDmB3ROUGp57c,7089
63
69
  opik_optimizer/optimization_config/configs.py,sha256=EGacRNnl6TeWuf8RNsxpP6Nh5JhogjK-JxKllK8dQr0,413
64
70
  opik_optimizer/optimization_config/mappers.py,sha256=4uBoPaIvCo4bqt_w-4rJyVe2LMAP_W7p6xxnDmGT-Sk,1724
71
+ opik_optimizer/parameter_optimizer/__init__.py,sha256=Eg-LEFBJqnOFw7i2B_YH27CoIGDPb5y_q1ar-ZpjtYo,308
72
+ opik_optimizer/parameter_optimizer/parameter_optimizer.py,sha256=eDd9tFQinz2lKsEJtikCBVzSWMK4saI9bhUY2NtDEg0,14955
73
+ opik_optimizer/parameter_optimizer/parameter_search_space.py,sha256=rgTNK8HPbdDiVm4GVX2QESTmQPhPFj4UkxqZfAy9JAA,4659
74
+ opik_optimizer/parameter_optimizer/parameter_spec.py,sha256=HzYT_dHBTfZtx403mY-Epv_IEqn4kYuYBZ6QUdkFRiY,8064
75
+ opik_optimizer/parameter_optimizer/search_space_types.py,sha256=UajTA2QKikEWazokDNO7j141gc2WxxYYiDRnFFjXi6M,512
76
+ opik_optimizer/parameter_optimizer/sensitivity_analysis.py,sha256=8KEMVMHsmcoiK21Cq1-We6_Pw_6LX9qBX9Az4-tmj_w,2146
65
77
  opik_optimizer/utils/__init__.py,sha256=Ee0SnTPOcwRwp93M6Lh-X913lfSIwnvCiYYh5cpdRQE,486
66
78
  opik_optimizer/utils/colbert.py,sha256=qSrzKUUGw7P92mLy4Ofug5pBGeTsHBLMJXlXSJSfKuo,8147
67
79
  opik_optimizer/utils/core.py,sha256=5GT1vp6fW8ICO42LHMX14BjR-xEb6afAKjM7b1Evx5M,15298
68
80
  opik_optimizer/utils/dataset_utils.py,sha256=dqRUGOekjeNWL0J15R8xFwLyKJDJynJXzVyQmt8rhHA,1464
69
81
  opik_optimizer/utils/prompt_segments.py,sha256=1zUITSccJ82Njac1rmANzim4WWM6rVac61mfluS7lFE,5931
70
- opik_optimizer-2.0.1.dist-info/licenses/LICENSE,sha256=V-0VHJOBdcA_teT8VymvsBUQ1-CZU6yJRmMEjec_8tA,11372
71
- opik_optimizer-2.0.1.dist-info/METADATA,sha256=cboRfRa4vLcDFOog_nVkWaOLH3Xj6ukBElF94At9Q74,12665
72
- opik_optimizer-2.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
- opik_optimizer-2.0.1.dist-info/top_level.txt,sha256=ondOlpq6_yFckqpxoAHSfzZS2N-JfgmA-QQhOJfz7m0,15
74
- opik_optimizer-2.0.1.dist-info/RECORD,,
82
+ opik_optimizer-2.1.1.dist-info/licenses/LICENSE,sha256=V-0VHJOBdcA_teT8VymvsBUQ1-CZU6yJRmMEjec_8tA,11372
83
+ opik_optimizer-2.1.1.dist-info/METADATA,sha256=CEmFvO0lRClgIYAItfH7V1XAJNBKcRwp8uq673-AzPo,12829
84
+ opik_optimizer-2.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
85
+ opik_optimizer-2.1.1.dist-info/top_level.txt,sha256=ondOlpq6_yFckqpxoAHSfzZS2N-JfgmA-QQhOJfz7m0,15
86
+ opik_optimizer-2.1.1.dist-info/RECORD,,