opik-optimizer 2.1.2__py3-none-any.whl → 2.2.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.
Files changed (39) hide show
  1. opik_optimizer/__init__.py +2 -2
  2. opik_optimizer/base_optimizer.py +314 -145
  3. opik_optimizer/evolutionary_optimizer/crossover_ops.py +31 -4
  4. opik_optimizer/evolutionary_optimizer/evaluation_ops.py +23 -3
  5. opik_optimizer/evolutionary_optimizer/evolutionary_optimizer.py +122 -95
  6. opik_optimizer/evolutionary_optimizer/mcp.py +11 -6
  7. opik_optimizer/evolutionary_optimizer/mutation_ops.py +25 -5
  8. opik_optimizer/evolutionary_optimizer/population_ops.py +26 -10
  9. opik_optimizer/evolutionary_optimizer/reporting.py +5 -5
  10. opik_optimizer/few_shot_bayesian_optimizer/few_shot_bayesian_optimizer.py +53 -99
  11. opik_optimizer/few_shot_bayesian_optimizer/reporting.py +4 -4
  12. opik_optimizer/gepa_optimizer/gepa_optimizer.py +183 -172
  13. opik_optimizer/gepa_optimizer/reporting.py +164 -22
  14. opik_optimizer/hierarchical_reflective_optimizer/hierarchical_reflective_optimizer.py +221 -245
  15. opik_optimizer/hierarchical_reflective_optimizer/hierarchical_root_cause_analyzer.py +38 -14
  16. opik_optimizer/hierarchical_reflective_optimizer/prompts.py +7 -1
  17. opik_optimizer/hierarchical_reflective_optimizer/reporting.py +287 -132
  18. opik_optimizer/meta_prompt_optimizer/meta_prompt_optimizer.py +185 -205
  19. opik_optimizer/meta_prompt_optimizer/reporting.py +4 -4
  20. opik_optimizer/mipro_optimizer/__init__.py +2 -2
  21. opik_optimizer/mipro_optimizer/_lm.py +4 -4
  22. opik_optimizer/mipro_optimizer/{_mipro_optimizer_v2.py → mipro_optimizer_v2.py} +1 -7
  23. opik_optimizer/mipro_optimizer/utils.py +1 -0
  24. opik_optimizer/multi_metric_objective.py +33 -0
  25. opik_optimizer/optimizable_agent.py +7 -4
  26. opik_optimizer/optimization_config/chat_prompt.py +7 -10
  27. opik_optimizer/parameter_optimizer/parameter_optimizer.py +188 -40
  28. opik_optimizer/parameter_optimizer/reporting.py +148 -0
  29. opik_optimizer/reporting_utils.py +42 -15
  30. opik_optimizer/task_evaluator.py +26 -9
  31. opik_optimizer/utils/core.py +16 -2
  32. opik_optimizer/utils/prompt_segments.py +1 -2
  33. {opik_optimizer-2.1.2.dist-info → opik_optimizer-2.2.0.dist-info}/METADATA +2 -3
  34. {opik_optimizer-2.1.2.dist-info → opik_optimizer-2.2.0.dist-info}/RECORD +37 -37
  35. opik_optimizer/evolutionary_optimizer/llm_support.py +0 -136
  36. opik_optimizer/mipro_optimizer/mipro_optimizer.py +0 -680
  37. {opik_optimizer-2.1.2.dist-info → opik_optimizer-2.2.0.dist-info}/WHEEL +0 -0
  38. {opik_optimizer-2.1.2.dist-info → opik_optimizer-2.2.0.dist-info}/licenses/LICENSE +0 -0
  39. {opik_optimizer-2.1.2.dist-info → opik_optimizer-2.2.0.dist-info}/top_level.txt +0 -0
@@ -4,11 +4,19 @@ from typing import Any
4
4
  from rich.table import Table
5
5
  from rich.text import Text
6
6
  from rich.panel import Panel
7
+ from rich.progress import (
8
+ Progress,
9
+ SpinnerColumn,
10
+ TextColumn,
11
+ BarColumn,
12
+ TimeRemainingColumn,
13
+ MofNCompleteColumn,
14
+ )
7
15
 
8
- from ..reporting_utils import (
9
- display_configuration, # noqa: F401
10
- display_header, # noqa: F401
11
- display_result, # noqa: F401
16
+ from ..reporting_utils import ( # noqa: F401
17
+ display_configuration,
18
+ display_header,
19
+ display_result,
12
20
  get_console,
13
21
  convert_tqdm_to_rich,
14
22
  suppress_opik_logs,
@@ -18,16 +26,38 @@ console = get_console()
18
26
 
19
27
 
20
28
  class RichGEPAOptimizerLogger:
21
- """Adapter for GEPA's logger that provides concise Rich output."""
29
+ """Adapter for GEPA's logger that provides concise Rich output with progress tracking."""
22
30
 
23
31
  SUPPRESS_PREFIXES = (
24
32
  "Linear pareto front program index",
25
33
  "New program candidate index",
26
34
  )
27
35
 
28
- def __init__(self, optimizer: Any, verbose: int = 1) -> None:
36
+ # Additional messages to suppress (too technical for users)
37
+ SUPPRESS_KEYWORDS = (
38
+ "Individual valset scores for new program",
39
+ "New valset pareto front scores",
40
+ "Updated valset pareto front programs",
41
+ "Best program as per aggregate score on train_val",
42
+ "Best program as per aggregate score on valset",
43
+ "New program is on the linear pareto front",
44
+ "Full train_val score for new program",
45
+ )
46
+
47
+ def __init__(
48
+ self,
49
+ optimizer: Any,
50
+ verbose: int = 1,
51
+ progress: Progress | None = None,
52
+ task_id: Any | None = None,
53
+ max_trials: int = 10,
54
+ ) -> None:
29
55
  self.optimizer = optimizer
30
56
  self.verbose = verbose
57
+ self.progress = progress
58
+ self.task_id = task_id
59
+ self.max_trials = max_trials
60
+ self.current_iteration = 0
31
61
 
32
62
  def log(self, message: str) -> None:
33
63
  if self.verbose < 1:
@@ -43,30 +73,117 @@ class RichGEPAOptimizerLogger:
43
73
 
44
74
  first = lines[0]
45
75
 
76
+ # Track iteration changes and add separation
46
77
  if first.startswith("Iteration "):
47
78
  colon = first.find(":")
48
79
  head = first[:colon] if colon != -1 else first
49
80
  parts = head.split()
50
81
  if len(parts) >= 2 and parts[1].isdigit():
51
82
  try:
52
- self.optimizer._gepa_current_iteration = int(parts[1]) # type: ignore[attr-defined]
83
+ iteration = int(parts[1])
84
+
85
+ # Add separator when starting a new iteration (except iteration 0)
86
+ if iteration > 0 and iteration != self.current_iteration:
87
+ console.print("│")
88
+
89
+ self.optimizer._gepa_current_iteration = iteration # type: ignore[attr-defined]
90
+ self.current_iteration = iteration
91
+
92
+ # Update progress bar
93
+ if self.progress and self.task_id is not None:
94
+ self.progress.update(self.task_id, completed=iteration)
95
+
96
+ # Add explanatory text for iteration start
97
+ if "Base program full valset score" in first:
98
+ # Extract score
99
+ score_match = first.split(":")[-1].strip()
100
+ console.print(
101
+ f"│ Baseline evaluation: {score_match}", style="bold"
102
+ )
103
+ return
104
+ elif "Selected program" in first:
105
+ # Extract program number and score
106
+ parts_info = first.split(":")
107
+ if "Selected program" in parts_info[1]:
108
+ program_info = parts_info[1].strip()
109
+ score_info = (
110
+ parts_info[2].strip() if len(parts_info) > 2 else ""
111
+ )
112
+ console.print(
113
+ f"│ Trial {iteration}: {program_info}, score: {score_info}",
114
+ style="bold cyan",
115
+ )
116
+ else:
117
+ console.print(f"│ Trial {iteration}", style="bold cyan")
118
+ console.print("│ ├─ Testing new prompt variant...")
119
+ return
53
120
  except Exception:
54
121
  pass
55
122
 
56
- if "Proposed new text" in first and "system_prompt:" in first:
57
- _, _, rest = first.partition("system_prompt:")
58
- snippet = rest.strip()
59
- if len(snippet) > 120:
60
- snippet = snippet[:120] + "…"
61
- first = "Proposed new text · system_prompt: " + snippet
62
- elif len(first) > 160:
63
- first = first[:160] + "…"
123
+ # Check if this message should be suppressed
124
+ for keyword in self.SUPPRESS_KEYWORDS:
125
+ if keyword in first:
126
+ return
64
127
 
65
128
  for prefix in self.SUPPRESS_PREFIXES:
66
129
  if prefix in first:
67
130
  return
68
131
 
69
- console.print(f"│ {first}")
132
+ # Format proposed prompts
133
+ if "Proposed new text" in first and "system_prompt:" in first:
134
+ _, _, rest = first.partition("system_prompt:")
135
+ snippet = rest.strip()
136
+ if len(snippet) > 100:
137
+ snippet = snippet[:100] + "…"
138
+ console.print(f"│ │ Proposed: {snippet}", style="dim")
139
+ return
140
+
141
+ # Format subsample evaluation results
142
+ if "New subsample score" in first and "is not better than" in first:
143
+ console.print("│ └─ Rejected - no improvement", style="dim yellow")
144
+ console.print("│") # Add spacing after rejected trials
145
+ return
146
+
147
+ if "New subsample score" in first and "is better than" in first:
148
+ console.print("│ ├─ Promising! Running full validation...", style="green")
149
+ return
150
+
151
+ # Format final validation score
152
+ if "Full valset score for new program" in first:
153
+ # Extract score
154
+ parts = first.split(":")
155
+ if len(parts) >= 2:
156
+ score = parts[-1].strip()
157
+ console.print(f"│ ├─ Validation complete: {score}", style="bold green")
158
+ else:
159
+ console.print("│ ├─ Validation complete", style="green")
160
+ return
161
+
162
+ # Format best score updates
163
+ if (
164
+ "Best valset aggregate score so far" in first
165
+ or "Best score on valset" in first
166
+ ):
167
+ # Extract score
168
+ parts = first.split(":")
169
+ if len(parts) >= 2:
170
+ score = parts[-1].strip()
171
+ console.print(f"│ └─ New best: {score} ✓", style="bold green")
172
+ console.print("│") # Add spacing after successful trials
173
+ return
174
+
175
+ # Suppress redundant "Iteration X:" prefix from detailed messages
176
+ if first.startswith(f"Iteration {self.current_iteration}:"):
177
+ # Remove the iteration prefix for cleaner output
178
+ first = first.split(":", 1)[1].strip() if ":" in first else first
179
+
180
+ # Truncate very long messages
181
+ if len(first) > 160:
182
+ first = first[:160] + "…"
183
+
184
+ # Default: print with standard prefix only if not already handled
185
+ if first:
186
+ console.print(f"│ {first}", style="dim")
70
187
 
71
188
 
72
189
  @contextmanager
@@ -85,20 +202,45 @@ def baseline_evaluation(verbose: int = 1) -> Any:
85
202
 
86
203
 
87
204
  @contextmanager
88
- def start_gepa_optimization(verbose: int = 1) -> Any:
205
+ def start_gepa_optimization(verbose: int = 1, max_trials: int = 10) -> Any:
89
206
  if verbose >= 1:
90
207
  console.print("> Starting GEPA optimization")
91
208
 
92
209
  class Reporter:
210
+ progress: Progress | None = None
211
+ task_id: Any | None = None
212
+
93
213
  def info(self, message: str) -> None:
94
214
  if verbose >= 1:
95
215
  console.print(f"│ {message}")
96
216
 
97
- try:
98
- yield Reporter()
99
- finally:
100
- if verbose >= 1:
101
- console.print("")
217
+ with suppress_opik_logs():
218
+ try:
219
+ # Create Rich progress bar
220
+ if verbose >= 1:
221
+ Reporter.progress = Progress(
222
+ SpinnerColumn(),
223
+ TextColumn("[bold blue]{task.description}"),
224
+ BarColumn(),
225
+ MofNCompleteColumn(),
226
+ TextColumn("•"),
227
+ TimeRemainingColumn(),
228
+ console=console,
229
+ transient=True, # Make progress bar disappear when done
230
+ )
231
+ Reporter.progress.start()
232
+ Reporter.task_id = Reporter.progress.add_task(
233
+ "GEPA Optimization", total=max_trials
234
+ )
235
+
236
+ yield Reporter()
237
+ finally:
238
+ if verbose >= 1:
239
+ if Reporter.progress and Reporter.task_id is not None:
240
+ # Mark as complete before stopping
241
+ Reporter.progress.update(Reporter.task_id, completed=max_trials)
242
+ Reporter.progress.stop()
243
+ console.print("")
102
244
 
103
245
 
104
246
  def display_candidate_scores(