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>
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
# `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.
|
||||
> **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:** Piano in tasca, **NON attivare** finché la condizione di trigger non è soddisfatta. Phase 2 (qwen3 + feature temporali) è la baseline.
|
||||
**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.
|
||||
|
||||
@@ -49,7 +56,7 @@ Se Phase 2 raggiunge max fitness ≥ 0.30 senza plateau, **non attivare** (la di
|
||||
- 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**
|
||||
- [x] **Step 1.1: Write failing test — operator returns child con system_prompt diverso**
|
||||
|
||||
Append a `tests/unit/test_mutation_prompt_llm.py`:
|
||||
|
||||
@@ -63,7 +70,7 @@ def test_mutate_prompt_llm_produces_different_prompt(mock_llm: LLMClient) -> Non
|
||||
assert child.generation == parent.generation + 1
|
||||
```
|
||||
|
||||
- [ ] **Step 1.2: Implement `MUTATION_INSTRUCTIONS` constant**
|
||||
- [x] **Step 1.2: Implement `MUTATION_INSTRUCTIONS` constant**
|
||||
|
||||
`mutation_prompt_llm.py`:
|
||||
|
||||
@@ -78,7 +85,7 @@ MUTATION_INSTRUCTIONS: dict[str, str] = {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 1.3: Implement `mutate_prompt_llm`**
|
||||
- [x] **Step 1.3: Implement `mutate_prompt_llm`**
|
||||
|
||||
Firma:
|
||||
```python
|
||||
@@ -98,7 +105,7 @@ Logica:
|
||||
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**
|
||||
- [x] **Step 1.4: Run test → green**
|
||||
|
||||
```bash
|
||||
uv run pytest tests/unit/test_mutation_prompt_llm.py::test_mutate_prompt_llm_produces_different_prompt -xvs
|
||||
@@ -112,7 +119,7 @@ uv run pytest tests/unit/test_mutation_prompt_llm.py::test_mutate_prompt_llm_pro
|
||||
- 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**
|
||||
- [x] **Step 2.1: Write failing test — fallback a random_mutate su prompt invalid**
|
||||
|
||||
```python
|
||||
def test_mutate_prompt_llm_falls_back_on_invalid_prompt(mock_llm: LLMClient) -> None:
|
||||
@@ -125,7 +132,7 @@ def test_mutate_prompt_llm_falls_back_on_invalid_prompt(mock_llm: LLMClient) ->
|
||||
assert child.parent_ids == [*parent.parent_ids, parent.id]
|
||||
```
|
||||
|
||||
- [ ] **Step 2.2: Implement validation step**
|
||||
- [x] **Step 2.2: Implement validation step**
|
||||
|
||||
Dopo aver estratto `new_prompt`, esegui `validate_prompt(new_prompt)`:
|
||||
- Lunghezza minima 50 caratteri.
|
||||
@@ -134,11 +141,11 @@ Dopo aver estratto `new_prompt`, esegui `validate_prompt(new_prompt)`:
|
||||
|
||||
Su fail → log warning + ritorna `random_mutate(g, rng)`.
|
||||
|
||||
- [ ] **Step 2.3: Write failing test — diversity guard**
|
||||
- [x] **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**
|
||||
- [x] **Step 2.4: Run test suite parziale**
|
||||
|
||||
```bash
|
||||
uv run pytest tests/unit/test_mutation_prompt_llm.py -xvs
|
||||
@@ -152,7 +159,7 @@ uv run pytest tests/unit/test_mutation_prompt_llm.py -xvs
|
||||
- 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**
|
||||
- [x] **Step 3.1: Write failing test — weighted_random_mutate rispetta pesi**
|
||||
|
||||
```python
|
||||
def test_weighted_random_mutate_picks_prompt_op_at_configured_rate() -> None:
|
||||
@@ -165,7 +172,7 @@ def test_weighted_random_mutate_picks_prompt_op_at_configured_rate() -> None:
|
||||
assert counter["prompt"] == 100
|
||||
```
|
||||
|
||||
- [ ] **Step 3.2: Implement `weighted_random_mutate`**
|
||||
- [x] **Step 3.2: Implement `weighted_random_mutate`**
|
||||
|
||||
```python
|
||||
def weighted_random_mutate(
|
||||
@@ -179,7 +186,7 @@ def weighted_random_mutate(
|
||||
return random_mutate(g, rng)
|
||||
```
|
||||
|
||||
- [ ] **Step 3.3: Test edge cases**
|
||||
- [x] **Step 3.3: Test edge cases**
|
||||
|
||||
- `llm=None` → sempre scalar mutation (backward compat).
|
||||
- `prompt_mutation_weight=0.0` → sempre scalar.
|
||||
@@ -194,7 +201,7 @@ def weighted_random_mutate(
|
||||
- Modify: `src/multi_swarm/orchestrator/run.py`
|
||||
- New: `tests/integration/test_ga_loop_with_prompt_mutator.py`
|
||||
|
||||
- [ ] **Step 4.1: Estendere `GAConfig`**
|
||||
- [x] **Step 4.1: Estendere `GAConfig`**
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
@@ -206,7 +213,7 @@ class GAConfig:
|
||||
prompt_mutation_weight: float = 0.0 # default off → opt-in
|
||||
```
|
||||
|
||||
- [ ] **Step 4.2: Pass `LLMClient` in `next_generation`**
|
||||
- [x] **Step 4.2: Pass `LLMClient` in `next_generation`**
|
||||
|
||||
```python
|
||||
def next_generation(
|
||||
@@ -220,11 +227,11 @@ def next_generation(
|
||||
child = weighted_random_mutate(parent, rng, llm, cfg.prompt_mutation_weight)
|
||||
```
|
||||
|
||||
- [ ] **Step 4.3: Wire in orchestrator**
|
||||
- [x] **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**
|
||||
- [x] **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.
|
||||
|
||||
@@ -236,18 +243,18 @@ Loop 2 gen × 5 genomi, mock LLM che ritorna prompt sempre diversi. Verifica che
|
||||
- New: `src/multi_swarm/metrics/diversity.py`
|
||||
- New: `tests/unit/test_diversity.py`
|
||||
|
||||
- [ ] **Step 5.1: Implement `population_prompt_diversity`**
|
||||
- [x] **Step 5.1: Implement `population_prompt_diversity`**
|
||||
|
||||
```python
|
||||
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**
|
||||
- [x] **Step 5.2: Test**
|
||||
|
||||
Tre prompt identici → 0.0. Tre prompt totalmente diversi → ~1.0.
|
||||
|
||||
- [ ] **Step 5.3: Logging**
|
||||
- [x] **Step 5.3: Logging**
|
||||
|
||||
Aggiungere `diversity_prompt` come campo per-generazione in `repository.save_generation` (richiede migration leggera).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user