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.
- opik_optimizer/__init__.py +2 -0
- opik_optimizer/_throttle.py +2 -1
- opik_optimizer/base_optimizer.py +28 -11
- opik_optimizer/colbert.py +236 -0
- 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 +73 -0
- opik_optimizer/evolutionary_optimizer/evolutionary_optimizer.py +124 -941
- opik_optimizer/evolutionary_optimizer/helpers.py +10 -0
- opik_optimizer/evolutionary_optimizer/llm_support.py +134 -0
- opik_optimizer/evolutionary_optimizer/mutation_ops.py +292 -0
- opik_optimizer/evolutionary_optimizer/population_ops.py +223 -0
- opik_optimizer/evolutionary_optimizer/prompts.py +305 -0
- opik_optimizer/evolutionary_optimizer/reporting.py +16 -4
- opik_optimizer/evolutionary_optimizer/style_ops.py +86 -0
- opik_optimizer/few_shot_bayesian_optimizer/few_shot_bayesian_optimizer.py +26 -23
- opik_optimizer/few_shot_bayesian_optimizer/reporting.py +12 -5
- opik_optimizer/gepa_optimizer/__init__.py +3 -0
- opik_optimizer/gepa_optimizer/adapter.py +152 -0
- opik_optimizer/gepa_optimizer/gepa_optimizer.py +556 -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 +493 -0
- opik_optimizer/meta_prompt_optimizer/meta_prompt_optimizer.py +399 -69
- opik_optimizer/meta_prompt_optimizer/reporting.py +16 -2
- opik_optimizer/mipro_optimizer/_lm.py +20 -20
- opik_optimizer/mipro_optimizer/_mipro_optimizer_v2.py +51 -50
- opik_optimizer/mipro_optimizer/mipro_optimizer.py +33 -28
- opik_optimizer/mipro_optimizer/utils.py +2 -4
- opik_optimizer/optimizable_agent.py +18 -17
- 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 +21 -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.py → utils/core.py} +111 -26
- opik_optimizer/utils/dataset_utils.py +49 -0
- opik_optimizer/utils/prompt_segments.py +186 -0
- {opik_optimizer-1.0.5.dist-info → opik_optimizer-1.1.0.dist-info}/METADATA +93 -16
- opik_optimizer-1.1.0.dist-info/RECORD +73 -0
- opik_optimizer-1.1.0.dist-info/licenses/LICENSE +203 -0
- opik_optimizer-1.0.5.dist-info/RECORD +0 -50
- opik_optimizer-1.0.5.dist-info/licenses/LICENSE +0 -21
- {opik_optimizer-1.0.5.dist-info → opik_optimizer-1.1.0.dist-info}/WHEEL +0 -0
- {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}")
|
opik_optimizer/logging_config.py
CHANGED
@@ -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
|
-
|
27
|
-
if
|
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
|
65
|
+
if not package_logger.handlers or should_reconfigure:
|
36
66
|
# Remove existing handlers if forcing re-configuration
|
37
|
-
if
|
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
|
-
|
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(
|
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
|
+
]
|