Files
Multi_Swarm_Coevolutive/docs/superpowers/plans/2026-05-11-mutate-prompt-llm-phase-2-5.md
T
Adriano ec80af9f26 feat(phase-2.5): population_prompt_diversity metric + piano aggiornato
Task 5 del piano Phase 2.5: nuovo modulo src/multi_swarm/metrics/diversity.py
con population_prompt_diversity(prompts) che ritorna la diversità media
1 - SequenceMatcher.ratio() su tutte le coppie distinte. 0.0 identici,
fino a ~0.9 totalmente diversi (SequenceMatcher considera spazi/lunghezza).

5 test: edge case empty/single, identici, diversi, intermediate, simmetria.

Piano aggiornato a stato "IMPLEMENTATO 2026-05-11": checkbox task 1-5
spuntate, task 6 (cost attribution per call_kind) deferito con motivazione.
Header preambolo aggiornato con trigger verificati e decisione collaterale
rollback tier C.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:52:09 +02:00

13 KiB
Raw Blame History

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 (- [x]) syntax for tracking.

Status: IMPLEMENTATO 2026-05-11. Task 1-5 completati e mergiati su main. Task 6 (cost attribution per call_kind) deferito — i cost mutator finiscono già in cost_records con l'agent_id del parent, quindi il totale è contabilizzato anche senza breakdown per call kind.

Trigger Phase 2.5 verificati con esito Phase 2 + run controllo:

  • Plateau max fitness ≥ 4 gen consecutive (Phase 2 qwen3-235b stuck 8 gen a 0.0238; run controllo qwen-2.5-72b stuck 9 gen a 0.0311).
  • Diversità prompt collapsed: top genomi del run controllo hanno fitness/Sharpe/DD identici (mutazioni scalari non producono varianti significative).
  • ✗ Top quasi-fit ≥ 0.10 non raggiunto, ma 2/3 trigger sufficienti.

Decisione collaterale: rollback tier C a qwen/qwen-2.5-72b-instruct (run controllo l'ha dimostrato superiore a qwen3-235b: +30% fitness, 4× entropy, metà costo e tempo).

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:

  1. Plateau evolutivo: max fitness stagnante (Δ < 0.01) per ≥ 4 generazioni consecutive su phase2-qwen3-001 o successori.
  2. Diversità prompt collassa: media Levenshtein normalizzata fra i prompt della popolazione finale ≤ 0.15 (= popolazione clonata).
  3. 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_INSTRUCTIONS constant

mutation_prompt_llm.py:

MUTATION_INSTRUCTIONS: dict[str, str] = {
    "tighten_threshold": "Rendi una soglia numerica più restrittiva del 1020%...",
    "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:

  1. Scegli instruction_key = rng.choice(list(MUTATION_INSTRUCTIONS)).
  2. Costruisci messaggio system + user con MUTATION_INSTRUCTIONS[instruction_key] + g.system_prompt.
  3. Crea genoma temporaneo mutator_genome con model_tier=mutator_tier.
  4. Chiama llm.complete(mutator_genome, system, user, max_tokens=2000).
  5. Estrai nuovo prompt da risposta (cerca blocco <prompt>...</prompt> o intero output).
  6. Ritorna _clone_with(g, system_prompt=new_prompt) (riusa helper di mutation.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 LLMClient in next_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_kind a CostRecord

@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-001 con --prompt-mutation-weight 0.0 per controllo.
  • Run trattamento T: phase2-qwen3-prompt-mut-001 con --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).