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.
- opik_optimizer/__init__.py +12 -0
- opik_optimizer/base_optimizer.py +33 -0
- opik_optimizer/hierarchical_reflective_optimizer/__init__.py +5 -0
- opik_optimizer/hierarchical_reflective_optimizer/hierarchical_reflective_optimizer.py +718 -0
- opik_optimizer/hierarchical_reflective_optimizer/hierarchical_root_cause_analyzer.py +355 -0
- opik_optimizer/hierarchical_reflective_optimizer/prompts.py +91 -0
- opik_optimizer/hierarchical_reflective_optimizer/reporting.py +679 -0
- opik_optimizer/hierarchical_reflective_optimizer/types.py +49 -0
- opik_optimizer/optimization_result.py +227 -6
- opik_optimizer/parameter_optimizer/__init__.py +11 -0
- opik_optimizer/parameter_optimizer/parameter_optimizer.py +382 -0
- opik_optimizer/parameter_optimizer/parameter_search_space.py +125 -0
- opik_optimizer/parameter_optimizer/parameter_spec.py +214 -0
- opik_optimizer/parameter_optimizer/search_space_types.py +24 -0
- opik_optimizer/parameter_optimizer/sensitivity_analysis.py +71 -0
- {opik_optimizer-2.0.1.dist-info → opik_optimizer-2.1.1.dist-info}/METADATA +4 -2
- {opik_optimizer-2.0.1.dist-info → opik_optimizer-2.1.1.dist-info}/RECORD +20 -8
- {opik_optimizer-2.0.1.dist-info → opik_optimizer-2.1.1.dist-info}/WHEEL +0 -0
- {opik_optimizer-2.0.1.dist-info → opik_optimizer-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {opik_optimizer-2.0.1.dist-info → opik_optimizer-2.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
"""Type definitions for the Reflective Optimizer."""
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
|
6
|
+
class FailureMode(BaseModel):
|
7
|
+
"""Model for a single failure mode identified in evaluation."""
|
8
|
+
|
9
|
+
name: str
|
10
|
+
description: str
|
11
|
+
root_cause: str
|
12
|
+
|
13
|
+
|
14
|
+
class RootCauseAnalysis(BaseModel):
|
15
|
+
"""Model for root cause analysis response."""
|
16
|
+
|
17
|
+
failure_modes: list[FailureMode]
|
18
|
+
|
19
|
+
|
20
|
+
class BatchAnalysis(BaseModel):
|
21
|
+
"""Model for a single batch analysis result."""
|
22
|
+
|
23
|
+
batch_number: int
|
24
|
+
start_index: int
|
25
|
+
end_index: int
|
26
|
+
failure_modes: list[FailureMode]
|
27
|
+
|
28
|
+
|
29
|
+
class HierarchicalRootCauseAnalysis(BaseModel):
|
30
|
+
"""Model for the final hierarchical root cause analysis."""
|
31
|
+
|
32
|
+
total_test_cases: int
|
33
|
+
num_batches: int
|
34
|
+
unified_failure_modes: list[FailureMode]
|
35
|
+
synthesis_notes: str
|
36
|
+
|
37
|
+
|
38
|
+
class PromptMessage(BaseModel):
|
39
|
+
"""Model for a single prompt message."""
|
40
|
+
|
41
|
+
role: str
|
42
|
+
content: str
|
43
|
+
|
44
|
+
|
45
|
+
class ImprovedPrompt(BaseModel):
|
46
|
+
"""Model for improved prompt response."""
|
47
|
+
|
48
|
+
reasoning: str
|
49
|
+
messages: list[PromptMessage]
|
@@ -8,6 +8,13 @@ import rich
|
|
8
8
|
from .reporting_utils import get_console, get_link_text, get_optimization_run_url_by_id
|
9
9
|
|
10
10
|
|
11
|
+
def _format_float(value: Any, digits: int = 6) -> str:
|
12
|
+
"""Format float values with specified precision."""
|
13
|
+
if isinstance(value, float):
|
14
|
+
return f"{value:.{digits}f}"
|
15
|
+
return str(value)
|
16
|
+
|
17
|
+
|
11
18
|
class OptimizationResult(pydantic.BaseModel):
|
12
19
|
"""Result oan optimization run."""
|
13
20
|
|
@@ -44,6 +51,50 @@ class OptimizationResult(pydantic.BaseModel):
|
|
44
51
|
def model_dump(self, *kargs: Any, **kwargs: Any) -> dict[str, Any]:
|
45
52
|
return super().model_dump(*kargs, **kwargs)
|
46
53
|
|
54
|
+
def get_optimized_model_kwargs(self) -> dict[str, Any]:
|
55
|
+
"""
|
56
|
+
Extract optimized model_kwargs for use in other optimizers.
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
Dictionary of optimized model kwargs, empty dict if not available
|
60
|
+
"""
|
61
|
+
return self.details.get("optimized_model_kwargs", {})
|
62
|
+
|
63
|
+
def get_optimized_model(self) -> str | None:
|
64
|
+
"""
|
65
|
+
Extract optimized model name.
|
66
|
+
|
67
|
+
Returns:
|
68
|
+
Model name string if available, None otherwise
|
69
|
+
"""
|
70
|
+
return self.details.get("optimized_model")
|
71
|
+
|
72
|
+
def get_optimized_parameters(self) -> dict[str, Any]:
|
73
|
+
"""
|
74
|
+
Extract optimized parameter values.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
Dictionary of optimized parameters, empty dict if not available
|
78
|
+
"""
|
79
|
+
return self.details.get("optimized_parameters", {})
|
80
|
+
|
81
|
+
def apply_to_prompt(self, prompt: Any) -> Any:
|
82
|
+
"""
|
83
|
+
Apply optimized parameters to a prompt.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
prompt: ChatPrompt instance to apply optimizations to
|
87
|
+
|
88
|
+
Returns:
|
89
|
+
New ChatPrompt instance with optimized parameters applied
|
90
|
+
"""
|
91
|
+
prompt_copy = prompt.copy()
|
92
|
+
if "optimized_model_kwargs" in self.details:
|
93
|
+
prompt_copy.model_kwargs = self.details["optimized_model_kwargs"]
|
94
|
+
if "optimized_model" in self.details:
|
95
|
+
prompt_copy.model = self.details["optimized_model"]
|
96
|
+
return prompt_copy
|
97
|
+
|
47
98
|
def _calculate_improvement_str(self) -> str:
|
48
99
|
"""Helper to calculate improvement percentage string."""
|
49
100
|
initial_s = self.initial_score
|
@@ -113,12 +164,97 @@ class OptimizationResult(pydantic.BaseModel):
|
|
113
164
|
f"Final Best Score: {final_score_str}",
|
114
165
|
f"Total Improvement:{improvement_str.rjust(max(0, 18 - len('Total Improvement:')))}",
|
115
166
|
f"Rounds Completed: {rounds_ran}",
|
116
|
-
"\nFINAL OPTIMIZED PROMPT / STRUCTURE:",
|
117
|
-
"--------------------------------------------------------------------------------",
|
118
|
-
f"{final_prompt_display}",
|
119
|
-
"--------------------------------------------------------------------------------",
|
120
|
-
f"{separator}",
|
121
167
|
]
|
168
|
+
|
169
|
+
optimized_params = self.details.get("optimized_parameters") or {}
|
170
|
+
parameter_importance = self.details.get("parameter_importance") or {}
|
171
|
+
search_ranges = self.details.get("search_ranges") or {}
|
172
|
+
precision = self.details.get("parameter_precision", 6)
|
173
|
+
|
174
|
+
if optimized_params:
|
175
|
+
|
176
|
+
def _format_range(desc: dict[str, Any]) -> str:
|
177
|
+
if "min" in desc and "max" in desc:
|
178
|
+
step_str = (
|
179
|
+
f", step={_format_float(desc['step'], precision)}"
|
180
|
+
if desc.get("step") is not None
|
181
|
+
else ""
|
182
|
+
)
|
183
|
+
return f"[{_format_float(desc['min'], precision)}, {_format_float(desc['max'], precision)}{step_str}]"
|
184
|
+
if desc.get("choices"):
|
185
|
+
return f"choices={desc['choices']}"
|
186
|
+
return str(desc)
|
187
|
+
|
188
|
+
rows = []
|
189
|
+
stage_order = [
|
190
|
+
record.get("stage")
|
191
|
+
for record in self.details.get("search_stages", [])
|
192
|
+
if record.get("stage") in search_ranges
|
193
|
+
]
|
194
|
+
if not stage_order:
|
195
|
+
stage_order = sorted(search_ranges)
|
196
|
+
|
197
|
+
for name in sorted(optimized_params):
|
198
|
+
contribution = parameter_importance.get(name)
|
199
|
+
stage_ranges = []
|
200
|
+
for stage in stage_order:
|
201
|
+
params = search_ranges.get(stage) or {}
|
202
|
+
if name in params:
|
203
|
+
stage_ranges.append(f"{stage}: {_format_range(params[name])}")
|
204
|
+
if not stage_ranges:
|
205
|
+
for stage, params in search_ranges.items():
|
206
|
+
if name in params:
|
207
|
+
stage_ranges.append(
|
208
|
+
f"{stage}: {_format_range(params[name])}"
|
209
|
+
)
|
210
|
+
joined_ranges = "\n".join(stage_ranges) if stage_ranges else "N/A"
|
211
|
+
rows.append(
|
212
|
+
{
|
213
|
+
"parameter": name,
|
214
|
+
"value": optimized_params[name],
|
215
|
+
"contribution": contribution,
|
216
|
+
"ranges": joined_ranges,
|
217
|
+
}
|
218
|
+
)
|
219
|
+
|
220
|
+
if rows:
|
221
|
+
output.append("Parameter Summary:")
|
222
|
+
# Compute overall improvement fraction for gain calculation
|
223
|
+
total_improvement = None
|
224
|
+
if isinstance(self.initial_score, (int, float)) and isinstance(
|
225
|
+
self.score, (int, float)
|
226
|
+
):
|
227
|
+
if self.initial_score != 0:
|
228
|
+
total_improvement = (self.score - self.initial_score) / abs(
|
229
|
+
self.initial_score
|
230
|
+
)
|
231
|
+
else:
|
232
|
+
total_improvement = self.score
|
233
|
+
for row in rows:
|
234
|
+
value_str = _format_float(row["value"], precision)
|
235
|
+
contrib_val = row["contribution"]
|
236
|
+
if contrib_val is not None:
|
237
|
+
contrib_percent = contrib_val * 100
|
238
|
+
gain_str = ""
|
239
|
+
if total_improvement is not None:
|
240
|
+
gain_value = contrib_val * total_improvement * 100
|
241
|
+
gain_str = f" ({gain_value:+.2f}%)"
|
242
|
+
contrib_str = f"{contrib_percent:.1f}%{gain_str}"
|
243
|
+
else:
|
244
|
+
contrib_str = "N/A"
|
245
|
+
output.append(
|
246
|
+
f"- {row['parameter']}: value={value_str}, contribution={contrib_str}, ranges=\n {row['ranges']}"
|
247
|
+
)
|
248
|
+
|
249
|
+
output.extend(
|
250
|
+
[
|
251
|
+
"\nFINAL OPTIMIZED PROMPT / STRUCTURE:",
|
252
|
+
"--------------------------------------------------------------------------------",
|
253
|
+
f"{final_prompt_display}",
|
254
|
+
"--------------------------------------------------------------------------------",
|
255
|
+
f"{separator}",
|
256
|
+
]
|
257
|
+
)
|
122
258
|
return "\n".join(output)
|
123
259
|
|
124
260
|
def __rich__(self) -> rich.panel.Panel:
|
@@ -159,6 +295,11 @@ class OptimizationResult(pydantic.BaseModel):
|
|
159
295
|
),
|
160
296
|
)
|
161
297
|
|
298
|
+
optimized_params = self.details.get("optimized_parameters") or {}
|
299
|
+
parameter_importance = self.details.get("parameter_importance") or {}
|
300
|
+
search_ranges = self.details.get("search_ranges") or {}
|
301
|
+
precision = self.details.get("parameter_precision", 6)
|
302
|
+
|
162
303
|
# Display Chat Structure if available
|
163
304
|
panel_title = "[bold]Final Optimized Prompt[/bold]"
|
164
305
|
try:
|
@@ -190,7 +331,87 @@ class OptimizationResult(pydantic.BaseModel):
|
|
190
331
|
prompt_renderable, title=panel_title, border_style="blue", padding=(1, 2)
|
191
332
|
)
|
192
333
|
|
193
|
-
|
334
|
+
renderables: list[rich.console.RenderableType] = [table, "\n"]
|
335
|
+
|
336
|
+
if optimized_params:
|
337
|
+
summary_table = rich.table.Table(
|
338
|
+
title="Parameter Summary", show_header=True, title_style="bold"
|
339
|
+
)
|
340
|
+
summary_table.add_column("Parameter", justify="left", style="cyan")
|
341
|
+
summary_table.add_column("Value", justify="left")
|
342
|
+
summary_table.add_column("Importance", justify="left", style="magenta")
|
343
|
+
summary_table.add_column("Gain", justify="left", style="dim")
|
344
|
+
summary_table.add_column("Ranges", justify="left")
|
345
|
+
|
346
|
+
stage_order = [
|
347
|
+
record.get("stage")
|
348
|
+
for record in self.details.get("search_stages", [])
|
349
|
+
if record.get("stage") in search_ranges
|
350
|
+
]
|
351
|
+
if not stage_order:
|
352
|
+
stage_order = sorted(search_ranges)
|
353
|
+
|
354
|
+
def _format_range(desc: dict[str, Any]) -> str:
|
355
|
+
if "min" in desc and "max" in desc:
|
356
|
+
step_str = (
|
357
|
+
f", step={_format_float(desc['step'], precision)}"
|
358
|
+
if desc.get("step") is not None
|
359
|
+
else ""
|
360
|
+
)
|
361
|
+
return f"[{_format_float(desc['min'], precision)}, {_format_float(desc['max'], precision)}{step_str}]"
|
362
|
+
if desc.get("choices"):
|
363
|
+
return ",".join(map(str, desc["choices"]))
|
364
|
+
return str(desc)
|
365
|
+
|
366
|
+
total_improvement = None
|
367
|
+
if isinstance(self.initial_score, (int, float)) and isinstance(
|
368
|
+
self.score, (int, float)
|
369
|
+
):
|
370
|
+
if self.initial_score != 0:
|
371
|
+
total_improvement = (self.score - self.initial_score) / abs(
|
372
|
+
self.initial_score
|
373
|
+
)
|
374
|
+
else:
|
375
|
+
total_improvement = self.score
|
376
|
+
|
377
|
+
for name in sorted(optimized_params):
|
378
|
+
value_str = _format_float(optimized_params[name], precision)
|
379
|
+
contrib_val = parameter_importance.get(name)
|
380
|
+
if contrib_val is not None:
|
381
|
+
contrib_str = f"{contrib_val:.1%}"
|
382
|
+
gain_str = (
|
383
|
+
f"{contrib_val * total_improvement:+.2%}"
|
384
|
+
if total_improvement is not None
|
385
|
+
else "N/A"
|
386
|
+
)
|
387
|
+
else:
|
388
|
+
contrib_str = "N/A"
|
389
|
+
gain_str = "N/A"
|
390
|
+
ranges_parts = []
|
391
|
+
for stage in stage_order:
|
392
|
+
params = search_ranges.get(stage) or {}
|
393
|
+
if name in params:
|
394
|
+
ranges_parts.append(f"{stage}: {_format_range(params[name])}")
|
395
|
+
if not ranges_parts:
|
396
|
+
for stage, params in search_ranges.items():
|
397
|
+
if name in params:
|
398
|
+
ranges_parts.append(
|
399
|
+
f"{stage}: {_format_range(params[name])}"
|
400
|
+
)
|
401
|
+
|
402
|
+
summary_table.add_row(
|
403
|
+
name,
|
404
|
+
value_str,
|
405
|
+
contrib_str,
|
406
|
+
gain_str,
|
407
|
+
"\n".join(ranges_parts) if ranges_parts else "N/A",
|
408
|
+
)
|
409
|
+
|
410
|
+
renderables.extend([summary_table, "\n"])
|
411
|
+
|
412
|
+
renderables.append(prompt_panel)
|
413
|
+
|
414
|
+
content_group = rich.console.Group(*renderables)
|
194
415
|
|
195
416
|
return rich.panel.Panel(
|
196
417
|
content_group,
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from .parameter_optimizer import ParameterOptimizer
|
2
|
+
from .parameter_search_space import ParameterSearchSpace
|
3
|
+
from .parameter_spec import ParameterSpec
|
4
|
+
from .search_space_types import ParameterType
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"ParameterOptimizer",
|
8
|
+
"ParameterSearchSpace",
|
9
|
+
"ParameterSpec",
|
10
|
+
"ParameterType",
|
11
|
+
]
|