opik-optimizer 1.0.5__py3-none-any.whl → 1.1.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 (54) hide show
  1. opik_optimizer/__init__.py +2 -0
  2. opik_optimizer/_throttle.py +2 -1
  3. opik_optimizer/base_optimizer.py +28 -11
  4. opik_optimizer/colbert.py +236 -0
  5. opik_optimizer/data/context7_eval.jsonl +3 -0
  6. opik_optimizer/datasets/context7_eval.py +90 -0
  7. opik_optimizer/datasets/tiny_test.py +33 -34
  8. opik_optimizer/datasets/truthful_qa.py +2 -2
  9. opik_optimizer/evolutionary_optimizer/crossover_ops.py +194 -0
  10. opik_optimizer/evolutionary_optimizer/evaluation_ops.py +73 -0
  11. opik_optimizer/evolutionary_optimizer/evolutionary_optimizer.py +124 -941
  12. opik_optimizer/evolutionary_optimizer/helpers.py +10 -0
  13. opik_optimizer/evolutionary_optimizer/llm_support.py +134 -0
  14. opik_optimizer/evolutionary_optimizer/mutation_ops.py +292 -0
  15. opik_optimizer/evolutionary_optimizer/population_ops.py +223 -0
  16. opik_optimizer/evolutionary_optimizer/prompts.py +305 -0
  17. opik_optimizer/evolutionary_optimizer/reporting.py +16 -4
  18. opik_optimizer/evolutionary_optimizer/style_ops.py +86 -0
  19. opik_optimizer/few_shot_bayesian_optimizer/few_shot_bayesian_optimizer.py +26 -23
  20. opik_optimizer/few_shot_bayesian_optimizer/reporting.py +12 -5
  21. opik_optimizer/gepa_optimizer/__init__.py +3 -0
  22. opik_optimizer/gepa_optimizer/adapter.py +152 -0
  23. opik_optimizer/gepa_optimizer/gepa_optimizer.py +556 -0
  24. opik_optimizer/gepa_optimizer/reporting.py +181 -0
  25. opik_optimizer/logging_config.py +42 -7
  26. opik_optimizer/mcp_utils/__init__.py +22 -0
  27. opik_optimizer/mcp_utils/mcp.py +541 -0
  28. opik_optimizer/mcp_utils/mcp_second_pass.py +152 -0
  29. opik_optimizer/mcp_utils/mcp_simulator.py +116 -0
  30. opik_optimizer/mcp_utils/mcp_workflow.py +493 -0
  31. opik_optimizer/meta_prompt_optimizer/meta_prompt_optimizer.py +399 -69
  32. opik_optimizer/meta_prompt_optimizer/reporting.py +16 -2
  33. opik_optimizer/mipro_optimizer/_lm.py +20 -20
  34. opik_optimizer/mipro_optimizer/_mipro_optimizer_v2.py +51 -50
  35. opik_optimizer/mipro_optimizer/mipro_optimizer.py +33 -28
  36. opik_optimizer/mipro_optimizer/utils.py +2 -4
  37. opik_optimizer/optimizable_agent.py +18 -17
  38. opik_optimizer/optimization_config/chat_prompt.py +44 -23
  39. opik_optimizer/optimization_config/configs.py +3 -3
  40. opik_optimizer/optimization_config/mappers.py +9 -8
  41. opik_optimizer/optimization_result.py +21 -14
  42. opik_optimizer/reporting_utils.py +61 -10
  43. opik_optimizer/task_evaluator.py +9 -8
  44. opik_optimizer/utils/__init__.py +15 -0
  45. opik_optimizer/{utils.py → utils/core.py} +111 -26
  46. opik_optimizer/utils/dataset_utils.py +49 -0
  47. opik_optimizer/utils/prompt_segments.py +186 -0
  48. {opik_optimizer-1.0.5.dist-info → opik_optimizer-1.1.0.dist-info}/METADATA +93 -16
  49. opik_optimizer-1.1.0.dist-info/RECORD +73 -0
  50. opik_optimizer-1.1.0.dist-info/licenses/LICENSE +203 -0
  51. opik_optimizer-1.0.5.dist-info/RECORD +0 -50
  52. opik_optimizer-1.0.5.dist-info/licenses/LICENSE +0 -21
  53. {opik_optimizer-1.0.5.dist-info → opik_optimizer-1.1.0.dist-info}/WHEEL +0 -0
  54. {opik_optimizer-1.0.5.dist-info → opik_optimizer-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,181 @@
1
+ from contextlib import contextmanager
2
+ from typing import Any
3
+
4
+ from rich.table import Table
5
+ from rich.text import Text
6
+ from rich.panel import Panel
7
+
8
+ from ..reporting_utils import (
9
+ display_configuration, # noqa: F401
10
+ display_header, # noqa: F401
11
+ display_result, # noqa: F401
12
+ get_console,
13
+ convert_tqdm_to_rich,
14
+ suppress_opik_logs,
15
+ )
16
+
17
+ console = get_console()
18
+
19
+
20
+ class RichGEPAOptimizerLogger:
21
+ """Adapter for GEPA's logger that provides concise Rich output."""
22
+
23
+ SUPPRESS_PREFIXES = (
24
+ "Linear pareto front program index",
25
+ "New program candidate index",
26
+ )
27
+
28
+ def __init__(self, optimizer: Any, verbose: int = 1) -> None:
29
+ self.optimizer = optimizer
30
+ self.verbose = verbose
31
+
32
+ def log(self, message: str) -> None:
33
+ if self.verbose < 1:
34
+ return
35
+
36
+ msg = (message or "").strip()
37
+ if not msg:
38
+ return
39
+
40
+ lines = [ln.strip() for ln in msg.splitlines() if ln.strip()]
41
+ if not lines:
42
+ return
43
+
44
+ first = lines[0]
45
+
46
+ if first.startswith("Iteration "):
47
+ colon = first.find(":")
48
+ head = first[:colon] if colon != -1 else first
49
+ parts = head.split()
50
+ if len(parts) >= 2 and parts[1].isdigit():
51
+ try:
52
+ self.optimizer._gepa_current_iteration = int(parts[1]) # type: ignore[attr-defined]
53
+ except Exception:
54
+ pass
55
+
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] + "…"
64
+
65
+ for prefix in self.SUPPRESS_PREFIXES:
66
+ if prefix in first:
67
+ return
68
+
69
+ console.print(f"│ {first}")
70
+
71
+
72
+ @contextmanager
73
+ def baseline_evaluation(verbose: int = 1) -> Any:
74
+ if verbose >= 1:
75
+ console.print("> Establishing baseline performance (seed prompt)")
76
+
77
+ class Reporter:
78
+ def set_score(self, s: float) -> None:
79
+ if verbose >= 1:
80
+ console.print(f" Baseline score: {s:.4f}")
81
+
82
+ with suppress_opik_logs():
83
+ with convert_tqdm_to_rich(" Evaluation", verbose=verbose):
84
+ yield Reporter()
85
+
86
+
87
+ @contextmanager
88
+ def start_gepa_optimization(verbose: int = 1) -> Any:
89
+ if verbose >= 1:
90
+ console.print("> Starting GEPA optimization")
91
+
92
+ class Reporter:
93
+ def info(self, message: str) -> None:
94
+ if verbose >= 1:
95
+ console.print(f"│ {message}")
96
+
97
+ try:
98
+ yield Reporter()
99
+ finally:
100
+ if verbose >= 1:
101
+ console.print("")
102
+
103
+
104
+ def display_candidate_scores(
105
+ rows: list[dict[str, Any]],
106
+ *,
107
+ verbose: int = 1,
108
+ title: str = "GEPA Candidate Scores",
109
+ ) -> None:
110
+ """Render a summary table comparing GEPA's scores with Opik rescoring."""
111
+ if verbose < 1 or not rows:
112
+ return
113
+
114
+ table = Table(title=title, show_lines=False, expand=True)
115
+ table.add_column("#", justify="right", style="cyan")
116
+ table.add_column("Source", style="dim")
117
+ table.add_column("System Prompt", overflow="fold", ratio=2)
118
+ table.add_column("GEPA Score", justify="right")
119
+ table.add_column("Opik Score", justify="right", style="green")
120
+
121
+ for row in rows:
122
+ snippet = str(row.get("system_prompt", "")).replace("\n", " ")
123
+ snippet = snippet[:200] + ("…" if len(snippet) > 200 else "")
124
+ table.add_row(
125
+ str(row.get("iteration", "")),
126
+ str(row.get("source", "")),
127
+ snippet or "[dim]<empty>[/dim]",
128
+ _format_score(row.get("gepa_score")),
129
+ _format_score(row.get("opik_score")),
130
+ )
131
+
132
+ console.print(table)
133
+
134
+
135
+ def display_selected_candidate(
136
+ system_prompt: str,
137
+ score: float,
138
+ *,
139
+ verbose: int = 1,
140
+ title: str = "Selected Candidate",
141
+ ) -> None:
142
+ """Display the final selected candidate with its Opik score."""
143
+ if verbose < 1:
144
+ return
145
+
146
+ snippet = system_prompt.strip() or "<empty>"
147
+ text = Text(snippet)
148
+ panel = Panel(
149
+ text,
150
+ title=f"{title} — Opik score {score:.4f}",
151
+ border_style="green",
152
+ expand=True,
153
+ )
154
+ console.print(panel)
155
+
156
+
157
+ def _format_score(value: Any) -> str:
158
+ if value is None:
159
+ return "[dim]—[/dim]"
160
+ try:
161
+ return f"{float(value):.4f}"
162
+ except Exception:
163
+ return str(value)
164
+
165
+
166
+ def display_candidate_update(
167
+ iteration: int | None,
168
+ phase: str,
169
+ aggregate: float | None,
170
+ prompt_snippet: str,
171
+ *,
172
+ verbose: int = 1,
173
+ ) -> None:
174
+ if verbose < 1:
175
+ return
176
+ iter_label = f"Iter {iteration}" if iteration is not None else "Candidate"
177
+ agg_str = f"{aggregate:.4f}" if isinstance(aggregate, (int, float)) else "—"
178
+ snippet = prompt_snippet.replace("\n", " ")
179
+ if len(snippet) > 100:
180
+ snippet = snippet[:100] + "…"
181
+ console.print(f"│ {iter_label}: [{phase}] agg={agg_str} prompt={snippet}")
@@ -1,4 +1,6 @@
1
1
  import logging
2
+ import os
3
+
2
4
  from rich.logging import RichHandler
3
5
 
4
6
  DEFAULT_LOG_FORMAT = "%(message)s"
@@ -6,10 +8,31 @@ DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
6
8
 
7
9
  # Store configured state to prevent reconfiguration
8
10
  _logging_configured = False
11
+ _configured_level: int | None = None
12
+
13
+
14
+ def _coerce_level(level: int | str) -> int:
15
+ if isinstance(level, int):
16
+ return level
17
+
18
+ normalized = str(level).strip().upper()
19
+ if not normalized:
20
+ return logging.WARNING
21
+
22
+ if normalized.isdigit():
23
+ return int(normalized)
24
+
25
+ level_value = getattr(logging, normalized, None)
26
+ if isinstance(level_value, int):
27
+ return level_value
28
+
29
+ raise ValueError(
30
+ f"Unknown log level '{level}'. Expected standard logging level name or integer."
31
+ )
9
32
 
10
33
 
11
34
  def setup_logging(
12
- level: int = logging.WARNING,
35
+ level: int | str = logging.WARNING,
13
36
  format_string: str = DEFAULT_LOG_FORMAT,
14
37
  date_format: str = DEFAULT_DATE_FORMAT,
15
38
  force: bool = False,
@@ -23,8 +46,15 @@ def setup_logging(
23
46
  date_format: The format string for the date/time in log messages.
24
47
  force: If True, reconfigure logging even if already configured.
25
48
  """
26
- global _logging_configured
27
- if _logging_configured and not force:
49
+ env_level = os.getenv("OPIK_LOG_LEVEL")
50
+ target_level = _coerce_level(env_level if env_level is not None else level)
51
+
52
+ global _logging_configured, _configured_level
53
+ should_reconfigure = (
54
+ force or not _logging_configured or _configured_level != target_level
55
+ )
56
+
57
+ if _logging_configured and not should_reconfigure:
28
58
  # Use logger after getting it
29
59
  return
30
60
 
@@ -32,9 +62,9 @@ def setup_logging(
32
62
  package_logger = logging.getLogger("opik_optimizer")
33
63
 
34
64
  # Avoid adding handlers repeatedly if force=True replaces them
35
- if not package_logger.handlers or force:
65
+ if not package_logger.handlers or should_reconfigure:
36
66
  # Remove existing handlers if forcing re-configuration
37
- if force and package_logger.handlers:
67
+ if package_logger.handlers:
38
68
  for handler in package_logger.handlers[:]:
39
69
  package_logger.removeHandler(handler)
40
70
 
@@ -48,7 +78,11 @@ def setup_logging(
48
78
  # console_handler.setFormatter(formatter)
49
79
  package_logger.addHandler(console_handler)
50
80
 
51
- package_logger.setLevel(level)
81
+ if format_string:
82
+ formatter = logging.Formatter(format_string, datefmt=date_format)
83
+ console_handler.setFormatter(formatter)
84
+
85
+ package_logger.setLevel(target_level)
52
86
  package_logger.propagate = False # Don't duplicate messages in root logger
53
87
 
54
88
  # Set levels for noisy libraries like LiteLLM and httpx
@@ -62,10 +96,11 @@ def setup_logging(
62
96
  logging.getLogger("filelock").setLevel(logging.WARNING)
63
97
 
64
98
  _logging_configured = True
99
+ _configured_level = target_level
65
100
 
66
101
  # Use level name provided by rich handler by default
67
102
  package_logger.info(
68
- f"Opik Agent Optimizer logging configured to level: [bold cyan]{logging.getLevelName(level)}[/bold cyan]"
103
+ f"Opik Agent Optimizer logging configured to level: [bold cyan]{logging.getLevelName(target_level)}[/bold cyan]"
69
104
  )
70
105
 
71
106
 
@@ -0,0 +1,22 @@
1
+ """MCP utilities for opik_optimizer.
2
+
3
+ This module contains utilities for working with Model Context Protocol (MCP) tools
4
+ and workflows in optimization flows.
5
+ """
6
+
7
+ from .mcp import * # noqa: F401,F403
8
+ from .mcp_second_pass import * # noqa: F401,F403
9
+ from .mcp_simulator import * # noqa: F401,F403
10
+ from .mcp_workflow import * # noqa: F401,F403
11
+
12
+ from . import mcp as _mcp
13
+ from . import mcp_second_pass as _mcp_second_pass
14
+ from . import mcp_simulator as _mcp_simulator
15
+ from . import mcp_workflow as _mcp_workflow
16
+
17
+ __all__: list[str] = [
18
+ *getattr(_mcp, "__all__", []),
19
+ *getattr(_mcp_second_pass, "__all__", []),
20
+ *getattr(_mcp_simulator, "__all__", []),
21
+ *getattr(_mcp_workflow, "__all__", []),
22
+ ]