opik-optimizer 2.0.0__tar.gz → 2.1.0__tar.gz

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 (88) hide show
  1. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/PKG-INFO +4 -2
  2. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/README.md +1 -0
  3. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/pyproject.toml +3 -2
  4. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/__init__.py +10 -0
  5. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/base_optimizer.py +33 -0
  6. opik_optimizer-2.1.0/src/opik_optimizer/optimization_result.py +437 -0
  7. opik_optimizer-2.1.0/src/opik_optimizer/parameter_optimizer/__init__.py +11 -0
  8. opik_optimizer-2.1.0/src/opik_optimizer/parameter_optimizer/parameter_optimizer.py +382 -0
  9. opik_optimizer-2.1.0/src/opik_optimizer/parameter_optimizer/parameter_search_space.py +125 -0
  10. opik_optimizer-2.1.0/src/opik_optimizer/parameter_optimizer/parameter_spec.py +214 -0
  11. opik_optimizer-2.1.0/src/opik_optimizer/parameter_optimizer/search_space_types.py +24 -0
  12. opik_optimizer-2.1.0/src/opik_optimizer/parameter_optimizer/sensitivity_analysis.py +71 -0
  13. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer.egg-info/PKG-INFO +4 -2
  14. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer.egg-info/SOURCES.txt +6 -0
  15. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer.egg-info/requires.txt +2 -1
  16. opik_optimizer-2.0.0/src/opik_optimizer/optimization_result.py +0 -216
  17. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/LICENSE +0 -0
  18. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/setup.cfg +0 -0
  19. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/setup.py +0 -0
  20. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/_throttle.py +0 -0
  21. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/cache_config.py +0 -0
  22. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/data/context7_eval.jsonl +0 -0
  23. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/data/hotpot-500.json +0 -0
  24. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/__init__.py +0 -0
  25. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/ai2_arc.py +0 -0
  26. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/cnn_dailymail.py +0 -0
  27. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/context7_eval.py +0 -0
  28. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/election_questions.py +0 -0
  29. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/gsm8k.py +0 -0
  30. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/halu_eval.py +0 -0
  31. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/hotpot_qa.py +0 -0
  32. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/medhallu.py +0 -0
  33. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/rag_hallucinations.py +0 -0
  34. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/ragbench.py +0 -0
  35. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/tiny_test.py +0 -0
  36. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/datasets/truthful_qa.py +0 -0
  37. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/demo/__init__.py +0 -0
  38. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/demo/cache.py +0 -0
  39. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/demo/datasets.py +0 -0
  40. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/evolutionary_optimizer/__init__.py +0 -0
  41. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/evolutionary_optimizer/crossover_ops.py +0 -0
  42. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/evolutionary_optimizer/evaluation_ops.py +0 -0
  43. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/evolutionary_optimizer/evolutionary_optimizer.py +0 -0
  44. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/evolutionary_optimizer/helpers.py +0 -0
  45. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/evolutionary_optimizer/llm_support.py +0 -0
  46. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/evolutionary_optimizer/mcp.py +0 -0
  47. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/evolutionary_optimizer/mutation_ops.py +0 -0
  48. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/evolutionary_optimizer/population_ops.py +0 -0
  49. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/evolutionary_optimizer/prompts.py +0 -0
  50. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/evolutionary_optimizer/reporting.py +0 -0
  51. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/evolutionary_optimizer/style_ops.py +0 -0
  52. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/few_shot_bayesian_optimizer/__init__.py +0 -0
  53. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/few_shot_bayesian_optimizer/few_shot_bayesian_optimizer.py +0 -0
  54. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/few_shot_bayesian_optimizer/reporting.py +0 -0
  55. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/gepa_optimizer/__init__.py +0 -0
  56. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/gepa_optimizer/adapter.py +0 -0
  57. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/gepa_optimizer/gepa_optimizer.py +0 -0
  58. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/gepa_optimizer/reporting.py +0 -0
  59. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/logging_config.py +0 -0
  60. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/mcp_utils/__init__.py +0 -0
  61. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/mcp_utils/mcp.py +0 -0
  62. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/mcp_utils/mcp_second_pass.py +0 -0
  63. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/mcp_utils/mcp_simulator.py +0 -0
  64. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/mcp_utils/mcp_workflow.py +0 -0
  65. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/meta_prompt_optimizer/__init__.py +0 -0
  66. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/meta_prompt_optimizer/meta_prompt_optimizer.py +0 -0
  67. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/meta_prompt_optimizer/reporting.py +0 -0
  68. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/mipro_optimizer/__init__.py +0 -0
  69. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/mipro_optimizer/_lm.py +0 -0
  70. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/mipro_optimizer/_mipro_optimizer_v2.py +0 -0
  71. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/mipro_optimizer/mipro_optimizer.py +0 -0
  72. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/mipro_optimizer/utils.py +0 -0
  73. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/optimizable_agent.py +0 -0
  74. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/optimization_config/__init__.py +0 -0
  75. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/optimization_config/chat_prompt.py +0 -0
  76. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/optimization_config/configs.py +0 -0
  77. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/optimization_config/mappers.py +0 -0
  78. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/py.typed +0 -0
  79. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/reporting_utils.py +0 -0
  80. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/task_evaluator.py +0 -0
  81. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/utils/__init__.py +0 -0
  82. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/utils/colbert.py +0 -0
  83. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/utils/core.py +0 -0
  84. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/utils/dataset_utils.py +0 -0
  85. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer/utils/prompt_segments.py +0 -0
  86. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer.egg-info/dependency_links.txt +0 -0
  87. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/src/opik_optimizer.egg-info/top_level.txt +0 -0
  88. {opik_optimizer-2.0.0 → opik_optimizer-2.1.0}/tests/test_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opik_optimizer
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Agent optimization with Opik
5
5
  Home-page: https://github.com/comet-ml/opik
6
6
  Author: Comet ML
@@ -15,6 +15,7 @@ Requires-Dist: datasets
15
15
  Requires-Dist: deap>=1.4.3
16
16
  Requires-Dist: diskcache
17
17
  Requires-Dist: dspy<3
18
+ Requires-Dist: gepa>=0.0.7
18
19
  Requires-Dist: ujson
19
20
  Requires-Dist: hf_xet
20
21
  Requires-Dist: litellm
@@ -30,8 +31,8 @@ Provides-Extra: dev
30
31
  Requires-Dist: pytest; extra == "dev"
31
32
  Requires-Dist: pytest-cov; extra == "dev"
32
33
  Requires-Dist: langgraph; extra == "dev"
33
- Requires-Dist: gepa>=0.0.7; 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
 
@@ -12,6 +12,7 @@ The Opik Agent Optimizer refines your prompts to achieve better performance from
12
12
  * **MetaPromptOptimizer** - Employs meta-prompting techniques for optimization
13
13
  * **MiproOptimizer** - Implements MIPRO (Multi-Input Prompt Optimization) algorithm
14
14
  * **GepaOptimizer** - Leverages GEPA (Genetic-Pareto) optimization approach
15
+ * **ParameterOptimizer** - Optimizes LLM call parameters (temperature, top_p, etc.) using Bayesian optimization
15
16
 
16
17
  ## 🎯 Key Features
17
18
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "opik_optimizer"
3
- version = "2.0.0"
3
+ version = "2.1.0"
4
4
  description = "Agent optimization with Opik"
5
5
  authors = [
6
6
  {name = "Comet ML", email = "support@comet.com"}
@@ -13,6 +13,7 @@ dependencies = [
13
13
  "deap>=1.4.3",
14
14
  "diskcache",
15
15
  "dspy<3",
16
+ "gepa>=0.0.7",
16
17
  "ujson",
17
18
  "hf_xet",
18
19
  "litellm",
@@ -32,8 +33,8 @@ dev = [
32
33
  "pytest-cov",
33
34
  # "google-adk",
34
35
  "langgraph",
35
- "gepa>=0.0.7",
36
36
  "pre-commit",
37
+ "scikit-learn",
37
38
  ]
38
39
 
39
40
  [tool.setuptools.packages.find]
@@ -18,6 +18,12 @@ from .meta_prompt_optimizer import MetaPromptOptimizer
18
18
  from .mipro_optimizer import MiproOptimizer
19
19
  from .optimization_config.configs import TaskConfig
20
20
  from .optimization_result import OptimizationResult
21
+ from .parameter_optimizer import (
22
+ ParameterOptimizer,
23
+ ParameterSearchSpace,
24
+ ParameterSpec,
25
+ ParameterType,
26
+ )
21
27
 
22
28
  __version__ = importlib.metadata.version("opik_optimizer")
23
29
 
@@ -34,9 +40,13 @@ __all__ = [
34
40
  "MetaPromptOptimizer",
35
41
  "MiproOptimizer",
36
42
  "EvolutionaryOptimizer",
43
+ "ParameterOptimizer",
37
44
  "OptimizationResult",
38
45
  "OptimizableAgent",
39
46
  "setup_logging",
40
47
  "datasets",
41
48
  "TaskConfig",
49
+ "ParameterSearchSpace",
50
+ "ParameterSpec",
51
+ "ParameterType",
42
52
  ]
@@ -470,6 +470,39 @@ class BaseOptimizer(ABC):
470
470
  f"{self.__class__.__name__} does not implement optimize_mcp yet."
471
471
  )
472
472
 
473
+ def optimize_parameter(
474
+ self,
475
+ prompt: "chat_prompt.ChatPrompt",
476
+ dataset: Dataset,
477
+ metric: Callable,
478
+ parameter_space: Any,
479
+ experiment_config: dict | None = None,
480
+ n_trials: int | None = None,
481
+ n_samples: int | None = None,
482
+ agent_class: type[OptimizableAgent] | None = None,
483
+ **kwargs: Any,
484
+ ) -> optimization_result.OptimizationResult:
485
+ """
486
+ Optimize LLM call parameters such as temperature or top_k.
487
+
488
+ Args:
489
+ prompt: The chat prompt to evaluate with tuned parameters
490
+ dataset: Dataset providing evaluation examples
491
+ metric: Objective function to maximize
492
+ parameter_space: Definition of the search space for tunable parameters
493
+ experiment_config: Optional experiment metadata
494
+ n_trials: Number of trials to run (optimizer specific default if None)
495
+ n_samples: Number of dataset samples to evaluate per trial (None for all)
496
+ agent_class: Optional custom agent class to execute evaluations
497
+ **kwargs: Additional optimizer specific settings
498
+
499
+ Returns:
500
+ OptimizationResult: Structured result describing the best parameters found
501
+ """
502
+ raise NotImplementedError(
503
+ f"{self.__class__.__name__} does not implement optimize_parameter yet."
504
+ )
505
+
473
506
  def get_history(self) -> list[OptimizationRound]:
474
507
  """
475
508
  Get the optimization history.
@@ -0,0 +1,437 @@
1
+ """Module containing the OptimizationResult class."""
2
+
3
+ from typing import Any
4
+
5
+ import pydantic
6
+ import rich
7
+
8
+ from .reporting_utils import get_console, get_link_text, get_optimization_run_url_by_id
9
+
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
+
18
+ class OptimizationResult(pydantic.BaseModel):
19
+ """Result oan optimization run."""
20
+
21
+ optimizer: str = "Optimizer"
22
+
23
+ prompt: list[dict[str, str]]
24
+ score: float
25
+ metric_name: str
26
+
27
+ optimization_id: str | None = None
28
+ dataset_id: str | None = None
29
+
30
+ # Initial score
31
+ initial_prompt: list[dict[str, str]] | None = None
32
+ initial_score: float | None = None
33
+
34
+ details: dict[str, Any] = pydantic.Field(default_factory=dict)
35
+ history: list[dict[str, Any]] = []
36
+ llm_calls: int | None = None
37
+ tool_calls: int | None = None
38
+
39
+ # MIPRO specific
40
+ demonstrations: list[dict[str, Any]] | None = None
41
+ mipro_prompt: str | None = None
42
+ tool_prompts: dict[str, str] | None = None
43
+
44
+ model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
45
+
46
+ def get_run_link(self) -> str:
47
+ return get_optimization_run_url_by_id(
48
+ optimization_id=self.optimization_id, dataset_id=self.dataset_id
49
+ )
50
+
51
+ def model_dump(self, *kargs: Any, **kwargs: Any) -> dict[str, Any]:
52
+ return super().model_dump(*kargs, **kwargs)
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
+
98
+ def _calculate_improvement_str(self) -> str:
99
+ """Helper to calculate improvement percentage string."""
100
+ initial_s = self.initial_score
101
+ final_s = self.score
102
+
103
+ # Check if initial score exists and is a number
104
+ if not isinstance(initial_s, (int, float)):
105
+ return "[dim]N/A (no initial score)[/dim]"
106
+
107
+ # Proceed with calculation only if initial_s is valid
108
+ if initial_s != 0:
109
+ improvement_pct = (final_s - initial_s) / abs(initial_s)
110
+ # Basic coloring for rich, plain for str
111
+ color_start = ""
112
+ color_end = ""
113
+ if improvement_pct > 0:
114
+ color_start, color_end = "[bold green]", "[/bold green]"
115
+ elif improvement_pct < 0:
116
+ color_start, color_end = "[bold red]", "[/bold red]"
117
+ return f"{color_start}{improvement_pct:.2%}{color_end}"
118
+ elif final_s > 0:
119
+ return "[bold green]infinite[/bold green] (initial score was 0)"
120
+ else:
121
+ return "0.00% (no improvement from 0)"
122
+
123
+ def __str__(self) -> str:
124
+ """Provides a clean, well-formatted plain-text summary."""
125
+ separator = "=" * 80
126
+ rounds_ran = len(self.details.get("rounds", []))
127
+ initial_score = self.initial_score
128
+ initial_score_str = (
129
+ f"{initial_score:.4f}" if isinstance(initial_score, (int, float)) else "N/A"
130
+ )
131
+ final_score_str = f"{self.score:.4f}"
132
+ improvement_str = (
133
+ self._calculate_improvement_str()
134
+ .replace("[bold green]", "")
135
+ .replace("[/bold green]", "")
136
+ .replace("[bold red]", "")
137
+ .replace("[/bold red]", "")
138
+ .replace("[dim]", "")
139
+ .replace("[/dim]", "")
140
+ )
141
+
142
+ model_name = self.details.get("model", "N/A")
143
+ temp = self.details.get("temperature")
144
+ temp_str = f"{temp:.1f}" if isinstance(temp, (int, float)) else "N/A"
145
+
146
+ try:
147
+ final_prompt_display = "\n".join(
148
+ [
149
+ f" {msg.get('role', 'unknown')}: {str(msg.get('content', ''))[:150]}..."
150
+ for msg in self.prompt
151
+ ]
152
+ )
153
+ except Exception:
154
+ final_prompt_display = str(self.prompt)
155
+
156
+ output = [
157
+ f"\n{separator}",
158
+ "OPTIMIZATION COMPLETE",
159
+ f"{separator}",
160
+ f"Optimizer: {self.optimizer}",
161
+ f"Model Used: {model_name} (Temp: {temp_str})",
162
+ f"Metric Evaluated: {self.metric_name}",
163
+ f"Initial Score: {initial_score_str}",
164
+ f"Final Best Score: {final_score_str}",
165
+ f"Total Improvement:{improvement_str.rjust(max(0, 18 - len('Total Improvement:')))}",
166
+ f"Rounds Completed: {rounds_ran}",
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
+ )
258
+ return "\n".join(output)
259
+
260
+ def __rich__(self) -> rich.panel.Panel:
261
+ """Provides a rich, formatted output for terminals supporting Rich."""
262
+ improvement_str = self._calculate_improvement_str()
263
+ rounds_ran = len(self.details.get("rounds", []))
264
+ initial_score = self.initial_score
265
+ initial_score_str = (
266
+ f"{initial_score:.4f}"
267
+ if isinstance(initial_score, (int, float))
268
+ else "[dim]N/A[/dim]"
269
+ )
270
+ final_score_str = f"{self.score:.4f}"
271
+
272
+ model_name = self.details.get("model", "[dim]N/A[/dim]")
273
+
274
+ table = rich.table.Table.grid(padding=(0, 1))
275
+ table.add_column(style="dim")
276
+ table.add_column()
277
+
278
+ table.add_row(
279
+ "Optimizer:",
280
+ f"[bold]{self.optimizer}[/bold]",
281
+ )
282
+ table.add_row("Model Used:", f"{model_name}")
283
+ table.add_row("Metric Evaluated:", f"[bold]{self.metric_name}[/bold]")
284
+ table.add_row("Initial Score:", initial_score_str)
285
+ table.add_row("Final Best Score:", f"[bold cyan]{final_score_str}[/bold cyan]")
286
+ table.add_row("Total Improvement:", improvement_str)
287
+ table.add_row("Rounds Completed:", str(rounds_ran))
288
+ table.add_row(
289
+ "Optimization run link:",
290
+ get_link_text(
291
+ pre_text="",
292
+ link_text="Open in Opik Dashboard",
293
+ dataset_id=self.dataset_id,
294
+ optimization_id=self.optimization_id,
295
+ ),
296
+ )
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
+
303
+ # Display Chat Structure if available
304
+ panel_title = "[bold]Final Optimized Prompt[/bold]"
305
+ try:
306
+ chat_group_items = []
307
+ for msg in self.prompt:
308
+ role = msg.get("role", "unknown")
309
+ content = str(msg.get("content", ""))
310
+ role_style = (
311
+ "bold green"
312
+ if role == "user"
313
+ else (
314
+ "bold blue"
315
+ if role == "assistant"
316
+ else ("bold magenta" if role == "system" else "")
317
+ )
318
+ )
319
+ chat_group_items.append(
320
+ f"[{role_style}]{role.capitalize()}:[/] {content}"
321
+ )
322
+ chat_group_items.append("---") # Separator
323
+ prompt_renderable = rich.console.Group(*chat_group_items)
324
+
325
+ except Exception:
326
+ # Fallback to simple text prompt
327
+ prompt_renderable = rich.text.Text(str(self.prompt or ""), overflow="fold")
328
+ panel_title = "[bold]Final Optimized Prompt (Instruction - fallback)[/bold]"
329
+
330
+ prompt_panel = rich.panel.Panel(
331
+ prompt_renderable, title=panel_title, border_style="blue", padding=(1, 2)
332
+ )
333
+
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)
415
+
416
+ return rich.panel.Panel(
417
+ content_group,
418
+ title="[bold yellow]Optimization Complete[/bold yellow]",
419
+ border_style="yellow",
420
+ box=rich.box.DOUBLE_EDGE,
421
+ padding=1,
422
+ )
423
+
424
+ def display(self) -> None:
425
+ """
426
+ Displays the OptimizationResult using rich formatting
427
+ """
428
+ console = get_console()
429
+ console.print(self)
430
+ # Gracefully handle cases where optimization tracking isn't available
431
+ if self.dataset_id and self.optimization_id:
432
+ try:
433
+ print("Optimization run link:", self.get_run_link())
434
+ except Exception:
435
+ print("Optimization run link: No optimization run link available")
436
+ else:
437
+ print("Optimization run link: No optimization run link available")
@@ -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
+ ]