Coverage for agentos/prompts/optimizer.py: 58%
81 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 13:14 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 13:14 +0800
1"""
2Prompt Optimizer — DSPy-inspired automatic prompt improvement via
3iterative refinement, few-shot bootstrapping, and multi-strategy optimization.
4"""
6import random
7from collections.abc import Callable
8from dataclasses import dataclass, field
9from enum import StrEnum
10from typing import Any
13class OptimizationStrategy(StrEnum):
14 """Available optimization approaches."""
16 BOOTSTRAP_FEWSHOT = "bootstrap_fewshot"
17 MIPRO = "mipro" # Multi-prompt instruction proposal
18 GRADIENT_FREE = "gradient_free"
19 ENSEMBLE = "ensemble"
20 CHAIN_OF_THOUGHT = "chain_of_thought"
23@dataclass
24class OptimizerConfig:
25 """Configuration for prompt optimization runs."""
27 strategy: OptimizationStrategy = OptimizationStrategy.BOOTSTRAP_FEWSHOT
28 max_iterations: int = 10
29 candidates_per_iteration: int = 4
30 eval_samples: int = 20
31 target_metric: str = "accuracy"
32 target_threshold: float = 0.90
33 temperature_range: tuple[float, float] = (0.1, 0.9)
34 keep_top_k: int = 3
35 early_stop_patience: int = 3
36 seed: int = 42
39@dataclass
40class PromptCandidate:
41 """A single prompt variant under evaluation."""
43 id: str
44 text: str
45 score: float = 0.0
46 metrics: dict[str, float] = field(default_factory=dict)
47 generation: int = 0
48 parent_id: str = ""
51@dataclass
52class OptimizationResult:
53 """Final result after optimization converges or exhausts budget."""
55 best_prompt: str
56 best_score: float
57 iterations: int
58 candidates_evaluated: int
59 strategy: OptimizationStrategy
60 history: list[PromptCandidate] = field(default_factory=list)
61 metadata: dict[str, Any] = field(default_factory=dict)
64class PromptOptimizer:
65 """Iteratively refines prompts using a pluggable scoring function.
67 Usage::
69 def score(prompt: str) -> float:
70 # run your LLM eval and return metric
71 return measure(prompt)
73 opt = PromptOptimizer(config)
74 result = opt.optimize(base_prompt, score_fn=score)
75 print(result.best_prompt)
76 """
78 SEED_TEMPLATES = {
79 OptimizationStrategy.BOOTSTRAP_FEWSHOT: [
80 "{base}\n\nHere are some examples:\n{examples}",
81 "Task: {base}\n\nIllustrative examples:\n{examples}",
82 "{base}\n\nDemonstrations:\n{examples}",
83 ],
84 OptimizationStrategy.CHAIN_OF_THOUGHT: [
85 "{base}\n\nLet's think step by step.",
86 "{base}\n\nBreak this down logically:",
87 "Solve step-by-step:\n{base}",
88 ],
89 OptimizationStrategy.ENSEMBLE: [
90 "Consider multiple perspectives:\n{base}",
91 "Review from different angles:\n{base}",
92 "Analyze comprehensively:\n{base}",
93 ],
94 }
96 def __init__(self, config: OptimizerConfig | None = None):
97 self.config = config or OptimizerConfig()
98 random.seed(self.config.seed)
100 def optimize(
101 self,
102 base_prompt: str,
103 score_fn: Callable[[str], float],
104 few_shot_examples: list[str] | None = None,
105 ) -> OptimizationResult:
106 """Run optimization and return the best prompt found."""
107 best = PromptCandidate(
108 id="base",
109 text=base_prompt,
110 score=score_fn(base_prompt),
111 generation=0,
112 )
113 history = [best]
114 no_improve = 0
116 for iteration in range(1, self.config.max_iterations + 1):
117 candidates = self._generate_candidates(best.text, iteration, few_shot_examples)
118 for c in candidates:
119 c.score = score_fn(c.text)
120 history.append(c)
122 # Select best from this iteration
123 iteration_best = max(candidates, key=lambda c: c.score)
124 if iteration_best.score > best.score:
125 best = iteration_best
126 no_improve = 0
127 else:
128 no_improve += 1
130 # Keep top-K across all generations
131 history.sort(key=lambda c: c.score, reverse=True)
132 history = history[: self.config.keep_top_k * 2]
134 if best.score >= self.config.target_threshold:
135 break
136 if no_improve >= self.config.early_stop_patience:
137 break
139 return OptimizationResult(
140 best_prompt=best.text,
141 best_score=best.score,
142 iterations=iteration,
143 candidates_evaluated=len(history),
144 strategy=self.config.strategy,
145 history=history[: self.config.keep_top_k],
146 )
148 def _generate_candidates(
149 self,
150 base: str,
151 generation: int,
152 examples: list[str] | None,
153 ) -> list[PromptCandidate]:
154 templates = self.SEED_TEMPLATES.get(
155 self.config.strategy,
156 self.SEED_TEMPLATES[OptimizationStrategy.BOOTSTRAP_FEWSHOT],
157 )
158 candidates: list[PromptCandidate] = []
160 for i in range(self.config.candidates_per_iteration):
161 tmpl = random.choice(templates)
162 text = tmpl.format(
163 base=base,
164 examples=self._format_examples(examples) if examples else "",
165 )
166 # Add small perturbations
167 if random.random() < 0.3 and generation > 1:
168 text = self._perturb(text)
170 candidates.append(
171 PromptCandidate(
172 id=f"gen{generation}_{i}",
173 text=text,
174 generation=generation,
175 parent_id="base" if generation == 1 else f"gen{generation-1}_0",
176 )
177 )
179 return candidates
181 def _format_examples(self, examples: list[str]) -> str:
182 return "\n".join(f"- {e}" for e in examples[:5])
184 def _perturb(self, text: str) -> str:
185 """Apply minor random perturbations."""
186 perturbations = [
187 lambda t: t.replace(".", ". Please be thorough."),
188 lambda t: "Carefully: " + t,
189 lambda t: t + "\nBe precise and concise.",
190 lambda t: t.replace(":", ":\n"),
191 lambda t: t.replace("the ", "the relevant "),
192 ]
193 return random.choice(perturbations)(text)