opik-optimizer 1.1.0__py3-none-any.whl → 2.0.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/base_optimizer.py +376 -19
- opik_optimizer/evolutionary_optimizer/evaluation_ops.py +80 -17
- opik_optimizer/evolutionary_optimizer/evolutionary_optimizer.py +179 -39
- opik_optimizer/evolutionary_optimizer/llm_support.py +3 -1
- opik_optimizer/evolutionary_optimizer/mcp.py +249 -0
- opik_optimizer/evolutionary_optimizer/mutation_ops.py +17 -3
- opik_optimizer/evolutionary_optimizer/population_ops.py +5 -0
- opik_optimizer/evolutionary_optimizer/prompts.py +47 -0
- opik_optimizer/evolutionary_optimizer/reporting.py +12 -0
- opik_optimizer/few_shot_bayesian_optimizer/few_shot_bayesian_optimizer.py +65 -59
- opik_optimizer/gepa_optimizer/adapter.py +5 -3
- opik_optimizer/gepa_optimizer/gepa_optimizer.py +163 -66
- opik_optimizer/mcp_utils/mcp_workflow.py +57 -3
- opik_optimizer/meta_prompt_optimizer/meta_prompt_optimizer.py +75 -69
- opik_optimizer/mipro_optimizer/_lm.py +10 -3
- opik_optimizer/mipro_optimizer/_mipro_optimizer_v2.py +1 -1
- opik_optimizer/mipro_optimizer/mipro_optimizer.py +96 -21
- opik_optimizer/optimizable_agent.py +5 -0
- opik_optimizer/optimization_result.py +1 -0
- opik_optimizer/utils/core.py +56 -14
- {opik_optimizer-1.1.0.dist-info → opik_optimizer-2.0.0.dist-info}/METADATA +96 -9
- {opik_optimizer-1.1.0.dist-info → opik_optimizer-2.0.0.dist-info}/RECORD +27 -26
- /opik_optimizer/{colbert.py → utils/colbert.py} +0 -0
- {opik_optimizer-1.1.0.dist-info → opik_optimizer-2.0.0.dist-info}/WHEEL +0 -0
- {opik_optimizer-1.1.0.dist-info → opik_optimizer-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {opik_optimizer-1.1.0.dist-info → opik_optimizer-2.0.0.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
|
|
1
|
+
import copy
|
1
2
|
import json
|
2
3
|
import logging
|
3
4
|
import random
|
@@ -13,15 +14,22 @@ import opik
|
|
13
14
|
# DEAP imports
|
14
15
|
from deap import base, tools
|
15
16
|
from deap import creator as _creator
|
16
|
-
from opik.api_objects import
|
17
|
+
from opik.api_objects import optimization
|
17
18
|
from opik.environment import get_tqdm_for_current_environment
|
18
19
|
|
19
20
|
from opik_optimizer.base_optimizer import BaseOptimizer, OptimizationRound
|
20
21
|
from opik_optimizer.optimization_config import chat_prompt
|
21
22
|
from opik_optimizer.optimization_result import OptimizationResult
|
22
23
|
from opik_optimizer.optimizable_agent import OptimizableAgent
|
24
|
+
from opik_optimizer.mcp_utils.mcp_second_pass import MCPSecondPassCoordinator
|
25
|
+
from opik_optimizer.mcp_utils.mcp_workflow import (
|
26
|
+
MCPExecutionConfig,
|
27
|
+
extract_tool_arguments,
|
28
|
+
)
|
29
|
+
from opik_optimizer.utils.prompt_segments import extract_prompt_segments
|
30
|
+
|
31
|
+
from .mcp import EvolutionaryMCPContext, finalize_mcp_result
|
23
32
|
|
24
|
-
from .. import utils
|
25
33
|
from . import reporting
|
26
34
|
from .llm_support import LlmSupport
|
27
35
|
from .mutation_ops import MutationOps
|
@@ -135,8 +143,11 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
135
143
|
RuntimeWarning,
|
136
144
|
)
|
137
145
|
if "project_name" in model_kwargs:
|
138
|
-
|
139
|
-
"
|
146
|
+
warnings.warn(
|
147
|
+
"The 'project_name' parameter in optimizer constructor is deprecated. "
|
148
|
+
"Set project_name in the ChatPrompt instead.",
|
149
|
+
DeprecationWarning,
|
150
|
+
stacklevel=2,
|
140
151
|
)
|
141
152
|
del model_kwargs["project_name"]
|
142
153
|
|
@@ -147,22 +158,25 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
147
158
|
self.crossover_rate = crossover_rate
|
148
159
|
self.tournament_size = tournament_size
|
149
160
|
if num_threads is not None:
|
150
|
-
|
161
|
+
warnings.warn(
|
162
|
+
"The 'num_threads' parameter is deprecated and will be removed in a future version. "
|
163
|
+
"Use 'n_threads' instead.",
|
164
|
+
DeprecationWarning,
|
165
|
+
stacklevel=2,
|
166
|
+
)
|
151
167
|
n_threads = num_threads
|
152
168
|
self.num_threads = n_threads
|
153
169
|
self.elitism_size = elitism_size
|
154
170
|
self.adaptive_mutation = adaptive_mutation
|
155
171
|
self.enable_moo = enable_moo
|
156
172
|
self.enable_llm_crossover = enable_llm_crossover
|
157
|
-
self.seed = seed
|
173
|
+
self.seed = seed if seed is not None else self.DEFAULT_SEED
|
158
174
|
self.output_style_guidance = (
|
159
175
|
output_style_guidance
|
160
176
|
if output_style_guidance is not None
|
161
177
|
else self.DEFAULT_OUTPUT_STYLE_GUIDANCE
|
162
178
|
)
|
163
179
|
self.infer_output_style = infer_output_style
|
164
|
-
self.llm_call_counter = 0
|
165
|
-
self._opik_client = opik_client.get_client_cached()
|
166
180
|
self._current_optimization_id: str | None = None
|
167
181
|
self._current_generation = 0
|
168
182
|
self._best_fitness_history: list[float] = []
|
@@ -202,13 +216,6 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
202
216
|
# Attach methods from helper mixin modules to this instance to avoid
|
203
217
|
# multiple inheritance while preserving behavior.
|
204
218
|
self._attach_helper_methods()
|
205
|
-
self.toolbox.register(
|
206
|
-
"default_individual", lambda: creator.Individual("placeholder")
|
207
|
-
)
|
208
|
-
self.toolbox.register(
|
209
|
-
"population", tools.initRepeat, list, self.toolbox.default_individual
|
210
|
-
)
|
211
|
-
|
212
219
|
if self.enable_llm_crossover:
|
213
220
|
self.toolbox.register("mate", self._llm_deap_crossover)
|
214
221
|
else:
|
@@ -232,6 +239,7 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
232
239
|
)
|
233
240
|
|
234
241
|
# (methods already attached above)
|
242
|
+
self._mcp_context: EvolutionaryMCPContext | None = None
|
235
243
|
|
236
244
|
def _attach_helper_methods(self) -> None:
|
237
245
|
"""Bind selected methods from mixin modules onto this instance."""
|
@@ -290,6 +298,35 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
290
298
|
# Style inference
|
291
299
|
bind(StyleOps, ["_infer_output_style_from_dataset"])
|
292
300
|
|
301
|
+
def get_optimizer_metadata(self) -> dict[str, Any]:
|
302
|
+
return {
|
303
|
+
"population_size": self.population_size,
|
304
|
+
"num_generations": self.num_generations,
|
305
|
+
"mutation_rate": self.mutation_rate,
|
306
|
+
"crossover_rate": self.crossover_rate,
|
307
|
+
"tournament_size": self.tournament_size,
|
308
|
+
"elitism_size": self.elitism_size,
|
309
|
+
"adaptive_mutation": self.adaptive_mutation,
|
310
|
+
"enable_moo": self.enable_moo,
|
311
|
+
"enable_llm_crossover": self.enable_llm_crossover,
|
312
|
+
"infer_output_style": self.infer_output_style,
|
313
|
+
"output_style_guidance": self.output_style_guidance,
|
314
|
+
}
|
315
|
+
|
316
|
+
def _create_individual_from_prompt(
|
317
|
+
self, prompt_candidate: chat_prompt.ChatPrompt
|
318
|
+
) -> Any:
|
319
|
+
individual = creator.Individual(prompt_candidate.get_messages())
|
320
|
+
setattr(individual, "tools", copy.deepcopy(prompt_candidate.tools))
|
321
|
+
return individual
|
322
|
+
|
323
|
+
def _update_individual_with_prompt(
|
324
|
+
self, individual: Any, prompt_candidate: chat_prompt.ChatPrompt
|
325
|
+
) -> Any:
|
326
|
+
individual[:] = prompt_candidate.get_messages()
|
327
|
+
setattr(individual, "tools", copy.deepcopy(prompt_candidate.tools))
|
328
|
+
return individual
|
329
|
+
|
293
330
|
def _get_adaptive_mutation_rate(self) -> float:
|
294
331
|
"""Calculate adaptive mutation rate based on population diversity and progress."""
|
295
332
|
if not self.adaptive_mutation or len(self._best_fitness_history) < 2:
|
@@ -387,7 +424,7 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
387
424
|
)
|
388
425
|
|
389
426
|
prompt_variants = self._initialize_population(seed_prompt)
|
390
|
-
new_pop = [
|
427
|
+
new_pop = [self._create_individual_from_prompt(p) for p in prompt_variants]
|
391
428
|
|
392
429
|
for ind, fit in zip(new_pop, map(self.toolbox.evaluate, new_pop)):
|
393
430
|
ind.fitness.values = fit
|
@@ -495,35 +532,27 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
495
532
|
experiment_config: Optional experiment configuration
|
496
533
|
n_samples: Optional number of samples to use
|
497
534
|
auto_continue: Whether to automatically continue optimization
|
498
|
-
|
535
|
+
agent_class: Optional agent class to use
|
536
|
+
**kwargs: Additional keyword arguments including:
|
537
|
+
mcp_config (MCPExecutionConfig | None): MCP tool calling configuration (default: None)
|
499
538
|
"""
|
500
|
-
|
501
|
-
|
539
|
+
# Use base class validation and setup methods
|
540
|
+
self.validate_optimization_inputs(prompt, dataset, metric)
|
541
|
+
self.configure_prompt_model(prompt)
|
542
|
+
self.agent_class = self.setup_agent_class(prompt, agent_class)
|
502
543
|
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
if not
|
507
|
-
|
508
|
-
"Metric must be a function that takes `dataset_item` and `llm_output` as arguments."
|
509
|
-
)
|
510
|
-
|
511
|
-
if prompt.model is None:
|
512
|
-
prompt.model = self.model
|
513
|
-
if prompt.model_kwargs is None:
|
514
|
-
prompt.model_kwargs = self.model_kwargs
|
515
|
-
|
516
|
-
if agent_class is None:
|
517
|
-
self.agent_class = utils.create_litellm_agent_class(prompt)
|
518
|
-
else:
|
519
|
-
self.agent_class = agent_class
|
544
|
+
# Extract MCP config from kwargs (for optional MCP workflows)
|
545
|
+
mcp_config = kwargs.pop("mcp_config", None)
|
546
|
+
evaluation_kwargs: dict[str, Any] = {}
|
547
|
+
if mcp_config is not None:
|
548
|
+
evaluation_kwargs["mcp_config"] = mcp_config
|
520
549
|
|
521
550
|
self.project_name = self.agent_class.project_name
|
522
551
|
|
523
552
|
# Step 0. Start Opik optimization run
|
524
553
|
opik_optimization_run: optimization.Optimization | None = None
|
525
554
|
try:
|
526
|
-
opik_optimization_run = self.
|
555
|
+
opik_optimization_run = self.opik_client.create_optimization(
|
527
556
|
dataset_name=dataset.name,
|
528
557
|
objective_name=metric.__name__,
|
529
558
|
metadata={"optimizer": self.__class__.__name__},
|
@@ -554,7 +583,7 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
554
583
|
)
|
555
584
|
|
556
585
|
# Step 1. Step variables and define fitness function
|
557
|
-
self.
|
586
|
+
self.reset_counters() # Reset counters for run
|
558
587
|
self._history: list[OptimizationRound] = []
|
559
588
|
self._current_generation = 0
|
560
589
|
self._best_fitness_history = []
|
@@ -576,6 +605,7 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
576
605
|
experiment_config=(experiment_config or {}).copy(),
|
577
606
|
optimization_id=self._current_optimization_id,
|
578
607
|
verbose=0,
|
608
|
+
**evaluation_kwargs,
|
579
609
|
)
|
580
610
|
prompt_length = float(len(str(json.dumps(messages))))
|
581
611
|
return (primary_fitness_score, prompt_length)
|
@@ -594,6 +624,7 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
594
624
|
experiment_config=(experiment_config or {}).copy(),
|
595
625
|
optimization_id=self._current_optimization_id,
|
596
626
|
verbose=0,
|
627
|
+
**evaluation_kwargs,
|
597
628
|
)
|
598
629
|
return (fitness_score, 0.0)
|
599
630
|
|
@@ -646,7 +677,7 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
646
677
|
)
|
647
678
|
|
648
679
|
deap_population = [
|
649
|
-
|
680
|
+
self._create_individual_from_prompt(p) for p in initial_prompts
|
650
681
|
]
|
651
682
|
deap_population = deap_population[: self.population_size]
|
652
683
|
|
@@ -939,6 +970,19 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
939
970
|
verbose=self.verbose,
|
940
971
|
tools=getattr(final_best_prompt, "tools", None),
|
941
972
|
)
|
973
|
+
|
974
|
+
final_tools = getattr(final_best_prompt, "tools", None)
|
975
|
+
if final_tools:
|
976
|
+
final_details["final_tools"] = final_tools
|
977
|
+
tool_prompts = {
|
978
|
+
(tool.get("function", {}).get("name") or f"tool_{idx}"): tool.get(
|
979
|
+
"function", {}
|
980
|
+
).get("description")
|
981
|
+
for idx, tool in enumerate(final_tools)
|
982
|
+
}
|
983
|
+
else:
|
984
|
+
tool_prompts = None
|
985
|
+
|
942
986
|
return OptimizationResult(
|
943
987
|
optimizer=self.__class__.__name__,
|
944
988
|
prompt=final_best_prompt.get_messages(),
|
@@ -949,10 +993,106 @@ class EvolutionaryOptimizer(BaseOptimizer):
|
|
949
993
|
details=final_details,
|
950
994
|
history=[x.model_dump() for x in self.get_history()],
|
951
995
|
llm_calls=self.llm_call_counter,
|
996
|
+
tool_calls=self.tool_call_counter,
|
952
997
|
dataset_id=dataset.id,
|
953
998
|
optimization_id=self._current_optimization_id,
|
999
|
+
tool_prompts=tool_prompts,
|
954
1000
|
)
|
955
1001
|
|
1002
|
+
def optimize_mcp(
|
1003
|
+
self,
|
1004
|
+
prompt: chat_prompt.ChatPrompt,
|
1005
|
+
dataset: opik.Dataset,
|
1006
|
+
metric: Callable,
|
1007
|
+
*,
|
1008
|
+
tool_name: str,
|
1009
|
+
second_pass: MCPSecondPassCoordinator,
|
1010
|
+
experiment_config: dict | None = None,
|
1011
|
+
n_samples: int | None = None,
|
1012
|
+
auto_continue: bool = False,
|
1013
|
+
agent_class: type[OptimizableAgent] | None = None,
|
1014
|
+
fallback_invoker: Callable[[dict[str, Any]], str] | None = None,
|
1015
|
+
fallback_arguments: Callable[[Any], dict[str, Any]] | None = None,
|
1016
|
+
allow_tool_use_on_second_pass: bool = False,
|
1017
|
+
**kwargs: Any,
|
1018
|
+
) -> OptimizationResult:
|
1019
|
+
if prompt.tools is None or not prompt.tools:
|
1020
|
+
raise ValueError("Prompt must include tools for MCP optimization")
|
1021
|
+
|
1022
|
+
panel_style = kwargs.pop("tool_panel_style", "bright_magenta")
|
1023
|
+
|
1024
|
+
segments = extract_prompt_segments(prompt)
|
1025
|
+
tool_segment_id = f"tool:{tool_name}"
|
1026
|
+
segment_lookup = {segment.segment_id: segment for segment in segments}
|
1027
|
+
if tool_segment_id not in segment_lookup:
|
1028
|
+
raise ValueError(f"Tool '{tool_name}' not present in prompt tools")
|
1029
|
+
|
1030
|
+
fallback_args_fn = fallback_arguments or extract_tool_arguments
|
1031
|
+
|
1032
|
+
if fallback_invoker is None:
|
1033
|
+
function_map = getattr(prompt, "function_map", {}) or {}
|
1034
|
+
default_invoker_candidate = function_map.get(tool_name)
|
1035
|
+
if default_invoker_candidate is not None:
|
1036
|
+
typed_invoker = cast(Callable[..., str], default_invoker_candidate)
|
1037
|
+
|
1038
|
+
def _fallback_invoker(args: dict[str, Any]) -> str:
|
1039
|
+
return typed_invoker(**args)
|
1040
|
+
|
1041
|
+
fallback_invoker = _fallback_invoker
|
1042
|
+
|
1043
|
+
tool_entry = None
|
1044
|
+
for entry in prompt.tools or []:
|
1045
|
+
function = entry.get("function", {})
|
1046
|
+
if (function.get("name") or entry.get("name")) == tool_name:
|
1047
|
+
tool_entry = entry
|
1048
|
+
break
|
1049
|
+
if tool_entry is None:
|
1050
|
+
raise ValueError(f"Tool '{tool_name}' not present in prompt.tools")
|
1051
|
+
|
1052
|
+
original_description = tool_entry.get("function", {}).get("description", "")
|
1053
|
+
tool_metadata = segment_lookup[tool_segment_id].metadata.get("raw_tool", {})
|
1054
|
+
|
1055
|
+
mcp_config = MCPExecutionConfig(
|
1056
|
+
coordinator=second_pass,
|
1057
|
+
tool_name=tool_name,
|
1058
|
+
fallback_arguments=fallback_args_fn,
|
1059
|
+
fallback_invoker=fallback_invoker,
|
1060
|
+
allow_tool_use_on_second_pass=allow_tool_use_on_second_pass,
|
1061
|
+
)
|
1062
|
+
|
1063
|
+
previous_context = getattr(self, "_mcp_context", None)
|
1064
|
+
previous_crossover = self.enable_llm_crossover
|
1065
|
+
|
1066
|
+
context = EvolutionaryMCPContext(
|
1067
|
+
tool_name=tool_name,
|
1068
|
+
tool_segment_id=tool_segment_id,
|
1069
|
+
original_description=original_description,
|
1070
|
+
tool_metadata=tool_metadata,
|
1071
|
+
panel_style=panel_style,
|
1072
|
+
)
|
1073
|
+
|
1074
|
+
self._mcp_context = context
|
1075
|
+
self.enable_llm_crossover = False
|
1076
|
+
|
1077
|
+
try:
|
1078
|
+
result = self.optimize_prompt(
|
1079
|
+
prompt=prompt,
|
1080
|
+
dataset=dataset,
|
1081
|
+
metric=metric,
|
1082
|
+
experiment_config=experiment_config,
|
1083
|
+
n_samples=n_samples,
|
1084
|
+
auto_continue=auto_continue,
|
1085
|
+
agent_class=agent_class,
|
1086
|
+
mcp_config=mcp_config,
|
1087
|
+
**kwargs,
|
1088
|
+
)
|
1089
|
+
finally:
|
1090
|
+
self._mcp_context = previous_context
|
1091
|
+
self.enable_llm_crossover = previous_crossover
|
1092
|
+
|
1093
|
+
finalize_mcp_result(result, context, panel_style)
|
1094
|
+
return result
|
1095
|
+
|
956
1096
|
# Evaluation is provided by EvaluationOps
|
957
1097
|
|
958
1098
|
# LLM crossover is provided by CrossoverOps
|
@@ -42,6 +42,8 @@ class LlmSupport:
|
|
42
42
|
frequency_penalty: float
|
43
43
|
presence_penalty: float
|
44
44
|
|
45
|
+
def increment_llm_counter(self) -> None: ...
|
46
|
+
|
45
47
|
@_throttle.rate_limited(_rate_limiter)
|
46
48
|
def _call_model(
|
47
49
|
self,
|
@@ -104,7 +106,7 @@ class LlmSupport:
|
|
104
106
|
response = litellm.completion(
|
105
107
|
model=self.model, messages=messages, **llm_config_params
|
106
108
|
)
|
107
|
-
self.
|
109
|
+
self.increment_llm_counter()
|
108
110
|
return response.choices[0].message.content
|
109
111
|
except (
|
110
112
|
litellm_exceptions.RateLimitError,
|
@@ -0,0 +1,249 @@
|
|
1
|
+
"""MCP helper utilities for the Evolutionary Optimizer."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
import re
|
8
|
+
import textwrap
|
9
|
+
from dataclasses import dataclass
|
10
|
+
from typing import Any
|
11
|
+
|
12
|
+
from opik_optimizer.optimization_config import chat_prompt
|
13
|
+
from opik_optimizer.utils.prompt_segments import (
|
14
|
+
apply_segment_updates,
|
15
|
+
extract_prompt_segments,
|
16
|
+
)
|
17
|
+
|
18
|
+
from . import prompts as evo_prompts
|
19
|
+
from . import reporting
|
20
|
+
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
23
|
+
|
24
|
+
@dataclass
|
25
|
+
class EvolutionaryMCPContext:
|
26
|
+
tool_name: str
|
27
|
+
tool_segment_id: str
|
28
|
+
original_description: str
|
29
|
+
tool_metadata: dict[str, Any]
|
30
|
+
panel_style: str
|
31
|
+
|
32
|
+
|
33
|
+
def _tool_metadata_json(metadata: dict[str, Any]) -> str:
|
34
|
+
try:
|
35
|
+
return json.dumps(metadata, indent=2)
|
36
|
+
except (
|
37
|
+
TypeError,
|
38
|
+
ValueError,
|
39
|
+
): # pragma: no cover - defensive, shouldn't happen under normal circumstances
|
40
|
+
return str(metadata)
|
41
|
+
|
42
|
+
|
43
|
+
def generate_tool_description_variations(
|
44
|
+
optimizer: Any,
|
45
|
+
base_prompt: chat_prompt.ChatPrompt,
|
46
|
+
context: EvolutionaryMCPContext,
|
47
|
+
num_variations: int,
|
48
|
+
) -> list[chat_prompt.ChatPrompt]:
|
49
|
+
if num_variations <= 0:
|
50
|
+
return []
|
51
|
+
|
52
|
+
instruction = textwrap.dedent(
|
53
|
+
evo_prompts.mcp_tool_rewrite_user_prompt(
|
54
|
+
tool_name=context.tool_name,
|
55
|
+
current_description=_current_tool_description(
|
56
|
+
base_prompt, context.tool_segment_id
|
57
|
+
)
|
58
|
+
or context.original_description,
|
59
|
+
tool_metadata_json=_tool_metadata_json(context.tool_metadata),
|
60
|
+
num_variations=num_variations,
|
61
|
+
)
|
62
|
+
).strip()
|
63
|
+
|
64
|
+
try:
|
65
|
+
response = optimizer._call_model( # type: ignore[attr-defined]
|
66
|
+
messages=[
|
67
|
+
{
|
68
|
+
"role": "system",
|
69
|
+
"content": evo_prompts.mcp_tool_rewrite_system_prompt(),
|
70
|
+
},
|
71
|
+
{"role": "user", "content": instruction},
|
72
|
+
],
|
73
|
+
is_reasoning=True,
|
74
|
+
optimization_id=getattr(optimizer, "_current_optimization_id", None),
|
75
|
+
)
|
76
|
+
|
77
|
+
payload = _extract_json_payload(response)
|
78
|
+
prompts_payload = payload.get("prompts")
|
79
|
+
if not isinstance(prompts_payload, list):
|
80
|
+
raise ValueError("LLM response missing 'prompts' list")
|
81
|
+
|
82
|
+
candidates: list[chat_prompt.ChatPrompt] = []
|
83
|
+
seen: set[str] = set()
|
84
|
+
for item in prompts_payload:
|
85
|
+
if not isinstance(item, dict):
|
86
|
+
continue
|
87
|
+
description = item.get("tool_description")
|
88
|
+
if not isinstance(description, str) or not description.strip():
|
89
|
+
continue
|
90
|
+
normalized = description.strip()
|
91
|
+
if normalized in seen:
|
92
|
+
continue
|
93
|
+
seen.add(normalized)
|
94
|
+
updated_prompt = _apply_description(base_prompt, context, normalized)
|
95
|
+
reporting.display_tool_description(
|
96
|
+
normalized,
|
97
|
+
f"Candidate tool description ({context.tool_name})",
|
98
|
+
context.panel_style,
|
99
|
+
)
|
100
|
+
candidates.append(updated_prompt)
|
101
|
+
if len(candidates) >= num_variations:
|
102
|
+
break
|
103
|
+
|
104
|
+
return candidates
|
105
|
+
except Exception as exc: # pragma: no cover - fallback path
|
106
|
+
logger.warning(f"Failed to generate MCP tool descriptions: {exc}")
|
107
|
+
return []
|
108
|
+
|
109
|
+
|
110
|
+
def initialize_population_mcp(
|
111
|
+
optimizer: Any,
|
112
|
+
prompt: chat_prompt.ChatPrompt,
|
113
|
+
context: EvolutionaryMCPContext,
|
114
|
+
) -> list[chat_prompt.ChatPrompt]:
|
115
|
+
population_size = getattr(optimizer, "population_size", 1)
|
116
|
+
with reporting.initializing_population(
|
117
|
+
verbose=getattr(optimizer, "verbose", 1)
|
118
|
+
) as init_pop_report:
|
119
|
+
init_pop_report.start(population_size)
|
120
|
+
|
121
|
+
population = [prompt]
|
122
|
+
num_to_generate = max(0, population_size - 1)
|
123
|
+
if num_to_generate > 0:
|
124
|
+
candidates = generate_tool_description_variations(
|
125
|
+
optimizer,
|
126
|
+
prompt,
|
127
|
+
context,
|
128
|
+
num_to_generate,
|
129
|
+
)
|
130
|
+
population.extend(candidates[:num_to_generate])
|
131
|
+
|
132
|
+
seen: set[str] = set()
|
133
|
+
final_population: list[chat_prompt.ChatPrompt] = []
|
134
|
+
for candidate in population:
|
135
|
+
key = json.dumps(candidate.get_messages())
|
136
|
+
if key in seen:
|
137
|
+
continue
|
138
|
+
seen.add(key)
|
139
|
+
final_population.append(candidate)
|
140
|
+
|
141
|
+
while len(final_population) < population_size:
|
142
|
+
final_population.append(prompt)
|
143
|
+
|
144
|
+
init_pop_report.end(final_population)
|
145
|
+
return final_population[:population_size]
|
146
|
+
|
147
|
+
|
148
|
+
def tool_description_mutation(
|
149
|
+
optimizer: Any,
|
150
|
+
prompt: chat_prompt.ChatPrompt,
|
151
|
+
context: EvolutionaryMCPContext,
|
152
|
+
) -> chat_prompt.ChatPrompt | None:
|
153
|
+
candidates = generate_tool_description_variations(optimizer, prompt, context, 1)
|
154
|
+
if not candidates:
|
155
|
+
return None
|
156
|
+
|
157
|
+
description = _current_tool_description(candidates[0], context.tool_segment_id)
|
158
|
+
if description:
|
159
|
+
reporting.display_tool_description(
|
160
|
+
description,
|
161
|
+
f"Updated tool description ({context.tool_name})",
|
162
|
+
context.panel_style,
|
163
|
+
)
|
164
|
+
return candidates[0]
|
165
|
+
|
166
|
+
|
167
|
+
def finalize_mcp_result(
|
168
|
+
result: Any,
|
169
|
+
context: EvolutionaryMCPContext,
|
170
|
+
panel_style: str,
|
171
|
+
) -> None:
|
172
|
+
final_tools = (
|
173
|
+
result.details.get("final_tools") if isinstance(result.details, dict) else None
|
174
|
+
)
|
175
|
+
tool_prompts = {
|
176
|
+
(tool.get("function", {}).get("name") or tool.get("name")): tool.get(
|
177
|
+
"function", {}
|
178
|
+
).get("description")
|
179
|
+
for tool in (final_tools or [])
|
180
|
+
}
|
181
|
+
if tool_prompts.get(context.tool_name):
|
182
|
+
reporting.display_tool_description(
|
183
|
+
tool_prompts[context.tool_name],
|
184
|
+
f"Final tool description ({context.tool_name})",
|
185
|
+
panel_style,
|
186
|
+
)
|
187
|
+
|
188
|
+
if not tool_prompts and context.original_description:
|
189
|
+
tool_prompts = {context.tool_name: context.original_description}
|
190
|
+
|
191
|
+
if tool_prompts:
|
192
|
+
result.tool_prompts = tool_prompts
|
193
|
+
|
194
|
+
|
195
|
+
# ---------------------------------------------------------------------------
|
196
|
+
# Internal helpers
|
197
|
+
# ---------------------------------------------------------------------------
|
198
|
+
|
199
|
+
|
200
|
+
def _current_tool_description(
|
201
|
+
prompt: chat_prompt.ChatPrompt,
|
202
|
+
tool_segment_id: str,
|
203
|
+
) -> str:
|
204
|
+
segments = {
|
205
|
+
segment.segment_id: segment for segment in extract_prompt_segments(prompt)
|
206
|
+
}
|
207
|
+
target = segments.get(tool_segment_id)
|
208
|
+
return target.content if target else ""
|
209
|
+
|
210
|
+
|
211
|
+
def _extract_json_payload(response: str) -> dict[str, Any]:
|
212
|
+
try:
|
213
|
+
return json.loads(response)
|
214
|
+
except json.JSONDecodeError:
|
215
|
+
match = re.search(r"\{.*\}", response, re.DOTALL)
|
216
|
+
if not match:
|
217
|
+
raise ValueError("No JSON object found in LLM response")
|
218
|
+
return json.loads(match.group())
|
219
|
+
|
220
|
+
|
221
|
+
def _apply_description(
|
222
|
+
prompt: chat_prompt.ChatPrompt,
|
223
|
+
context: EvolutionaryMCPContext,
|
224
|
+
description: str,
|
225
|
+
) -> chat_prompt.ChatPrompt:
|
226
|
+
updated_prompt = apply_segment_updates(
|
227
|
+
prompt,
|
228
|
+
{context.tool_segment_id: description},
|
229
|
+
)
|
230
|
+
_sync_system_description(updated_prompt, description)
|
231
|
+
return updated_prompt
|
232
|
+
|
233
|
+
|
234
|
+
def _sync_system_description(prompt: chat_prompt.ChatPrompt, description: str) -> None:
|
235
|
+
if not prompt.system:
|
236
|
+
return
|
237
|
+
|
238
|
+
marker_start = "<<TOOL_DESCRIPTION>>"
|
239
|
+
marker_end = "<<END_TOOL_DESCRIPTION>>"
|
240
|
+
|
241
|
+
start = prompt.system.find(marker_start)
|
242
|
+
end = prompt.system.find(marker_end)
|
243
|
+
if start == -1 or end == -1 or end <= start:
|
244
|
+
return
|
245
|
+
|
246
|
+
prefix = prompt.system[: start + len(marker_start)]
|
247
|
+
suffix = prompt.system[end:]
|
248
|
+
formatted_description = f"\n{description.strip()}\n"
|
249
|
+
prompt.system = f"{prefix}{formatted_description}{suffix}"
|
@@ -1,10 +1,12 @@
|
|
1
1
|
from typing import Any, TYPE_CHECKING
|
2
|
+
from collections.abc import Callable
|
2
3
|
|
3
4
|
import json
|
4
5
|
import logging
|
5
6
|
import random
|
6
7
|
|
7
8
|
from . import prompts as evo_prompts
|
9
|
+
from .mcp import EvolutionaryMCPContext, tool_description_mutation
|
8
10
|
from ..optimization_config import chat_prompt
|
9
11
|
from .. import utils
|
10
12
|
from . import reporting
|
@@ -21,6 +23,8 @@ class MutationOps:
|
|
21
23
|
output_style_guidance: str
|
22
24
|
_get_task_description_for_llm: Any
|
23
25
|
_call_model: Any
|
26
|
+
_mcp_context: EvolutionaryMCPContext | None
|
27
|
+
_update_individual_with_prompt: Callable[[Any, chat_prompt.ChatPrompt], Any]
|
24
28
|
|
25
29
|
def _deap_mutation(
|
26
30
|
self, individual: Any, initial_prompt: chat_prompt.ChatPrompt
|
@@ -28,6 +32,16 @@ class MutationOps:
|
|
28
32
|
"""Enhanced mutation operation with multiple strategies."""
|
29
33
|
prompt = chat_prompt.ChatPrompt(messages=individual)
|
30
34
|
|
35
|
+
mcp_context = getattr(self, "_mcp_context", None)
|
36
|
+
if mcp_context is not None:
|
37
|
+
mutated_prompt = tool_description_mutation(self, prompt, mcp_context)
|
38
|
+
if mutated_prompt is not None:
|
39
|
+
reporting.display_success(
|
40
|
+
" Mutation successful, tool description updated (MCP mutation).",
|
41
|
+
verbose=self.verbose,
|
42
|
+
)
|
43
|
+
return self._update_individual_with_prompt(individual, mutated_prompt)
|
44
|
+
|
31
45
|
# Choose mutation strategy based on current diversity
|
32
46
|
diversity = self._calculate_population_diversity()
|
33
47
|
|
@@ -49,21 +63,21 @@ class MutationOps:
|
|
49
63
|
" Mutation successful, prompt has been edited by randomizing words (word-level mutation).",
|
50
64
|
verbose=self.verbose,
|
51
65
|
)
|
52
|
-
return
|
66
|
+
return self._update_individual_with_prompt(individual, mutated_prompt)
|
53
67
|
elif mutation_choice > semantic_threshold:
|
54
68
|
mutated_prompt = self._structural_mutation(prompt)
|
55
69
|
reporting.display_success(
|
56
70
|
" Mutation successful, prompt has been edited by reordering, combining, or splitting sentences (structural mutation).",
|
57
71
|
verbose=self.verbose,
|
58
72
|
)
|
59
|
-
return
|
73
|
+
return self._update_individual_with_prompt(individual, mutated_prompt)
|
60
74
|
else:
|
61
75
|
mutated_prompt = self._semantic_mutation(prompt, initial_prompt)
|
62
76
|
reporting.display_success(
|
63
77
|
" Mutation successful, prompt has been edited using an LLM (semantic mutation).",
|
64
78
|
verbose=self.verbose,
|
65
79
|
)
|
66
|
-
return
|
80
|
+
return self._update_individual_with_prompt(individual, mutated_prompt)
|
67
81
|
|
68
82
|
def _semantic_mutation(
|
69
83
|
self, prompt: chat_prompt.ChatPrompt, initial_prompt: chat_prompt.ChatPrompt
|