Piano in tasca: NON attivare finché Phase 2 (qwen3 + temporal) non raggiunge una delle condizioni di trigger documentate: - plateau max fitness < 0.01 per >= 4 gen consecutive - diversità prompt Levenshtein <= 0.15 - top genome quasi-fit (>= 0.10) con adversarial HIGH >= 2 Sei tipi di mutazione (tighten_threshold, swap_comparator, add/remove_condition, change_timeframe, add_temporal_gate) via mutator tier B (deepseek-v4-flash), weighted_random_mutate dispatcher con default 0.0 (opt-in), diversity guard Levenshtein, fallback random_mutate su validation fail. Costo stimato +$0.006/run. A/B con baseline B = phase2-qwen3-001 vs trattamento T = phase2-qwen3-prompt-mut-001 per attribuzione effetto. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
mutate_prompt_llm — Phase 2.5 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Status: Piano in tasca, NON attivare finché la condizione di trigger non è soddisfatta. Phase 2 (qwen3 + feature temporali) è la baseline.
Goal: Introdurre un quinto operatore di mutazione che usa un LLM tier B come "mutator" per riscrivere il system_prompt di un genoma, generando diversità reale dove oggi random_mutate tocca solo quattro scalari. La pipeline GA esistente resta intatta: mutate_prompt_llm è solo un nuovo membro di MUTATION_OPS con peso configurabile.
Architecture: Operatore puro come gli altri quattro (mutate_temperature, mutate_lookback, mutate_feature_access, mutate_cognitive_style). Riceve parent_genome, llm_client, rng e restituisce un child genome con system_prompt modificato. Il mutator LLM (tier B = deepseek/deepseek-v4-flash) riceve una mutation-instruction casuale tra sei tipi predefiniti (tighten_threshold, swap_comparator, add_condition, remove_condition, change_timeframe, add_temporal_gate) e produce un nuovo prompt vincolato a una mutazione "atomica". Il child viene validato (parser + adversarial dry-run); su fallimento si effettua fallback a random_mutate. Selezione probabilistica nel random_mutate dispatcher con peso configurabile (default 0.30) — i quattro operator scalari mantengono il 70% complessivo.
Tech Stack: Python 3.13, LLMClient esistente (OpenAI SDK via OpenRouter), pytest + pytest-mock. Niente nuove dipendenze.
Spec di riferimento: sezione "Meccanismo di mutazione" della conversazione 2026-05-11, valutazione mutate_prompt_llm (questa pagina contiene la sintesi).
Trigger condition (quando attivare)
Implementare e mergiare solo se uno dei seguenti è vero al termine di Phase 2:
- Plateau evolutivo: max fitness stagnante (Δ < 0.01) per ≥ 4 generazioni consecutive su
phase2-qwen3-001o successori. - Diversità prompt collassa: media Levenshtein normalizzata fra i prompt della popolazione finale ≤ 0.15 (= popolazione clonata).
- Top genome problematico ma quasi-fit: max fitness ≥ 0.10 ma adversarial finding HIGH ≥ 2 per il top, suggerendo che una mutazione mirata del prompt potrebbe "ripararlo".
Se Phase 2 raggiunge max fitness ≥ 0.30 senza plateau, non attivare (la diversità random basta).
File map
| File | Tipo | Responsabilità |
|---|---|---|
src/multi_swarm/genome/mutation_prompt_llm.py |
New | Operatore mutate_prompt_llm + helper MUTATION_INSTRUCTIONS + retry/fallback wrapper |
src/multi_swarm/genome/mutation.py |
Modify | Estendere MUTATION_OPS + introdurre dispatcher pesato weighted_random_mutate |
src/multi_swarm/ga/loop.py |
Modify | Sostituire random_mutate(parent, rng) con weighted_random_mutate(parent, rng, llm_client, weights) |
src/multi_swarm/orchestrator/run.py |
Modify | Aggiungere mutator_tier: ModelTier = ModelTier.B e prompt_mutation_weight: float = 0.30 a RunConfig, passare LLMClient al loop GA |
src/multi_swarm/llm/cost_tracker.py |
Modify (minimo) | Loggare mutation_call separatamente da hypothesis_call per attribuzione costo |
src/multi_swarm/metrics/diversity.py |
New | Funzione population_prompt_diversity (Levenshtein normalizzata) — usata in trigger check + telemetry |
tests/unit/test_mutation_prompt_llm.py |
New | Test operator con mock LLMClient (success + validation fail + retry/fallback) |
tests/unit/test_mutation_dispatcher.py |
New | Test weighted_random_mutate rispetta i pesi |
tests/unit/test_diversity.py |
New | Test population_prompt_diversity su prompt identici/diversi |
tests/integration/test_ga_loop_with_prompt_mutator.py |
New | Loop end-to-end di 2 gen × 5 genomi con mock LLM, verifica diversità prompt cresce |
Task 1: Mutator instructions + operator stub
Files:
-
New:
src/multi_swarm/genome/mutation_prompt_llm.py -
New:
tests/unit/test_mutation_prompt_llm.py -
Step 1.1: Write failing test — operator returns child con system_prompt diverso
Append a tests/unit/test_mutation_prompt_llm.py:
def test_mutate_prompt_llm_produces_different_prompt(mock_llm: LLMClient) -> None:
parent = make_genome(system_prompt="Strategia: compra quando RSI < 30")
mock_llm.respond_with("Strategia: compra quando RSI < 25 e ora >= 14")
child = mutate_prompt_llm(parent, mock_llm, rng=random.Random(0))
assert child.system_prompt != parent.system_prompt
assert child.parent_ids == [*parent.parent_ids, parent.id]
assert child.generation == parent.generation + 1
- Step 1.2: Implement
MUTATION_INSTRUCTIONSconstant
mutation_prompt_llm.py:
MUTATION_INSTRUCTIONS: dict[str, str] = {
"tighten_threshold": "Rendi una soglia numerica più restrittiva del 10–20%...",
"swap_comparator": "Inverti un comparator (gt ↔ lt, gte ↔ lte) mantenendo intent...",
"add_condition": "Aggiungi una condizione AND/OR alla rule più specifica...",
"remove_condition": "Rimuovi una condizione ridondante o debole...",
"change_timeframe": "Modifica una finestra rolling (lookback) di ±30%...",
"add_temporal_gate": "Aggiungi un gate temporale (hour, dow, is_weekend)...",
}
- Step 1.3: Implement
mutate_prompt_llm
Firma:
def mutate_prompt_llm(
g: HypothesisAgentGenome,
llm: LLMClient,
rng: random.Random,
mutator_tier: ModelTier = ModelTier.B,
) -> HypothesisAgentGenome:
Logica:
- Scegli
instruction_key = rng.choice(list(MUTATION_INSTRUCTIONS)). - Costruisci messaggio system + user con
MUTATION_INSTRUCTIONS[instruction_key]+g.system_prompt. - Crea genoma temporaneo
mutator_genomeconmodel_tier=mutator_tier. - Chiama
llm.complete(mutator_genome, system, user, max_tokens=2000). - Estrai nuovo prompt da risposta (cerca blocco
<prompt>...</prompt>o intero output). - Ritorna
_clone_with(g, system_prompt=new_prompt)(riusa helper dimutation.py).
- Step 1.4: Run test → green
uv run pytest tests/unit/test_mutation_prompt_llm.py::test_mutate_prompt_llm_produces_different_prompt -xvs
Task 2: Validation + fallback
Files:
-
Modify:
src/multi_swarm/genome/mutation_prompt_llm.py -
Append:
tests/unit/test_mutation_prompt_llm.py -
Step 2.1: Write failing test — fallback a random_mutate su prompt invalid
def test_mutate_prompt_llm_falls_back_on_invalid_prompt(mock_llm: LLMClient) -> None:
parent = make_genome()
mock_llm.respond_with("garbage that does not parse")
child = mutate_prompt_llm(parent, mock_llm, rng=random.Random(0))
# Garbage prompt deve fallback: child è prodotto da random_mutate, quindi
# system_prompt == parent.system_prompt (random_mutate tocca solo scalari)
assert child.system_prompt == parent.system_prompt
assert child.parent_ids == [*parent.parent_ids, parent.id]
- Step 2.2: Implement validation step
Dopo aver estratto new_prompt, esegui validate_prompt(new_prompt):
- Lunghezza minima 50 caratteri.
- Contiene almeno una keyword fra
{rsi, sma, ema, atr, momentum, breakout, mean reversion, gt, lt, ...}. - Non identico a
parent.system_prompt(Levenshtein > 0.05 normalizzata).
Su fail → log warning + ritorna random_mutate(g, rng).
- Step 2.3: Write failing test — diversity guard
Mock LLM ritorna prompt identico al parent → validate_prompt rifiuta → fallback.
- Step 2.4: Run test suite parziale
uv run pytest tests/unit/test_mutation_prompt_llm.py -xvs
Task 3: Weighted dispatcher
Files:
-
Modify:
src/multi_swarm/genome/mutation.py -
New:
tests/unit/test_mutation_dispatcher.py -
Step 3.1: Write failing test — weighted_random_mutate rispetta pesi
def test_weighted_random_mutate_picks_prompt_op_at_configured_rate() -> None:
rng = random.Random(0)
weights = {"prompt": 1.0, "scalar": 0.0} # 100% prompt
counter = Counter()
for _ in range(100):
op_name = _pick_op_name(weights, rng)
counter[op_name] += 1
assert counter["prompt"] == 100
- Step 3.2: Implement
weighted_random_mutate
def weighted_random_mutate(
g: HypothesisAgentGenome,
rng: random.Random,
llm: LLMClient | None = None,
prompt_mutation_weight: float = 0.30,
) -> HypothesisAgentGenome:
if llm is not None and rng.random() < prompt_mutation_weight:
return mutate_prompt_llm(g, llm, rng)
return random_mutate(g, rng)
-
Step 3.3: Test edge cases
-
llm=None→ sempre scalar mutation (backward compat). -
prompt_mutation_weight=0.0→ sempre scalar. -
prompt_mutation_weight=1.0→ sempre prompt (se llm presente).
Task 4: Integrazione GA loop
Files:
-
Modify:
src/multi_swarm/ga/loop.py -
Modify:
src/multi_swarm/orchestrator/run.py -
New:
tests/integration/test_ga_loop_with_prompt_mutator.py -
Step 4.1: Estendere
GAConfig
@dataclass(frozen=True)
class GAConfig:
population_size: int
elite_k: int
tournament_k: int
p_crossover: float
prompt_mutation_weight: float = 0.0 # default off → opt-in
- Step 4.2: Pass
LLMClientinnext_generation
def next_generation(
population: list[HypothesisAgentGenome],
fitnesses: dict[str, float],
cfg: GAConfig,
rng: random.Random,
llm: LLMClient | None = None,
) -> list[HypothesisAgentGenome]:
...
child = weighted_random_mutate(parent, rng, llm, cfg.prompt_mutation_weight)
- Step 4.3: Wire in orchestrator
RunConfig.prompt_mutation_weight: float = 0.0 (default off). Quando attivo via CLI --prompt-mutation-weight 0.30, passare a next_generation.
- Step 4.4: Integration test
Loop 2 gen × 5 genomi, mock LLM che ritorna prompt sempre diversi. Verifica che la popolazione finale abbia più diversità prompt della iniziale.
Task 5: Diversity metric
Files:
-
New:
src/multi_swarm/metrics/diversity.py -
New:
tests/unit/test_diversity.py -
Step 5.1: Implement
population_prompt_diversity
def population_prompt_diversity(prompts: list[str]) -> float:
"""Levenshtein normalizzata media su tutte le coppie. 0.0 = identici, 1.0 = totalmente diversi."""
- Step 5.2: Test
Tre prompt identici → 0.0. Tre prompt totalmente diversi → ~1.0.
- Step 5.3: Logging
Aggiungere diversity_prompt come campo per-generazione in repository.save_generation (richiede migration leggera).
Task 6: Cost attribution
Files:
-
Modify:
src/multi_swarm/llm/cost_tracker.py -
Modify: tests esistenti
-
Step 6.1: Aggiungere
call_kindaCostRecord
@dataclass
class CostRecord:
...
call_kind: str = "hypothesis" # "hypothesis" | "mutation"
- Step 6.2: Loggare separatamente in summary
summary()["by_call_kind"] con breakdown.
- Step 6.3: Test compatibilità con record esistenti
Backward compat: record senza call_kind interpretati come "hypothesis".
Verification end-to-end
uv run pytest -q→ 100% passa (157 + nuovi test).uv run python scripts/smoke_run.py→ completa con mock LLM.- Run baseline B: ripetere
phase2-qwen3-001con--prompt-mutation-weight 0.0per controllo. - Run trattamento T:
phase2-qwen3-prompt-mut-001con--prompt-mutation-weight 0.30. - Confronto: max fitness T > B + 20%, diversity_prompt(T) > diversity_prompt(B) + 30%.
- Costo aggiuntivo run T ≤ $0.10 (sanity check).
Risks & mitigations
| Rischio | Mitigazione |
|---|---|
| Mode collapse mutator LLM | mutation_instruction scelta random + diversity guard Levenshtein |
| Prompt LLM-output non parsabile dal compiler | Validation step + fallback random_mutate |
| Costo runaway (loop infinito retry) | max_tokens=2000, no retry su validation fail |
| Bias condiviso con generator tier C | Mutator tier B = deepseek-v4-flash, famiglia diversa da Qwen3 |
| Variabili confuse con Phase 2 | Attivare solo dopo Phase 2 baseline; A/B isolato |
Cost estimate
Pop = 20, gen = 10, mutation rate ~75% (5 elite + 15 children), prompt_mutation_weight = 0.30:
- ~45 chiamate LLM tier B aggiuntive per run.
- ~500 tok input + 200 tok output per call → 22.5k in + 9k out totali.
- 22.5k × $0.14/1M + 9k × $0.28/1M ≈ $0.0057/run.
Trascurabile rispetto al budget run base (~$0.10).