fix(01): revise plans based on checker feedback

- Fix CSV-01 column count: 32 -> 33 (8 meta + 24 slide + 1 caption)
- Add TopicResult Pydantic model + topic_generator.txt prompt
- Make bulk generation async with background task + polling endpoint
- Add POST /api/export/{job_id}/csv for inline edit CSV download
- Split Plan 01-04 Task 2 into 2a/2b/2c (badges, slideviewer, pages)
- Update ProgressIndicator to use polling on /status endpoint
- Add --yes flag and frontend/ prerequisite note to Plan 01-01

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michele
2026-03-08 01:40:30 +01:00
parent 3f1dbbf396
commit 696b265e4d
5 changed files with 194 additions and 82 deletions

View File

@@ -23,8 +23,10 @@ must_haves:
- "CSVBuilder produce CSV con encoding utf-8-sig, header CANVA_FIELDS, e caratteri italiani intatti"
- "GenerationPipeline genera 13 post con per-item error isolation: un fallimento non blocca il batch"
- "API endpoint POST /api/calendar/generate ritorna CalendarResponse con 13 slot"
- "API endpoint POST /api/generate/bulk ritorna GenerateResponse con risultati per-item (success/failed)"
- "API endpoint GET /api/export/{job_id}/csv scarica file CSV con Content-Disposition attachment"
- "API endpoint POST /api/generate/bulk avvia generazione come background task e ritorna job_id immediatamente"
- "API endpoint GET /api/generate/job/{job_id}/status ritorna progresso in tempo reale (completed/total/current_post) per polling"
- "API endpoint GET /api/export/{job_id}/csv scarica file CSV originale con Content-Disposition attachment"
- "API endpoint POST /api/export/{job_id}/csv accetta dati modificati dall'utente e rigenera CSV con le modifiche inline"
- "API endpoint GET /api/settings ritorna configurazione corrente, PUT /api/settings salva"
artifacts:
- path: "backend/services/llm_service.py"
@@ -37,13 +39,13 @@ must_haves:
provides: "GenerationPipeline che orchestra calendario -> LLM -> CSV con per-item isolation"
contains: "class GenerationPipeline"
- path: "backend/routers/generate.py"
provides: "POST /api/generate/bulk e POST /api/generate/single endpoints"
provides: "POST /api/generate/bulk (async background task), POST /api/generate/single, GET /api/generate/job/{job_id}/status (polling)"
contains: "router = APIRouter"
- path: "backend/routers/calendar.py"
provides: "POST /api/calendar/generate endpoint"
contains: "router = APIRouter"
- path: "backend/routers/export.py"
provides: "GET /api/export/{job_id}/csv endpoint con FileResponse"
provides: "GET /api/export/{job_id}/csv (originale), POST /api/export/{job_id}/csv (con modifiche inline)"
contains: "router = APIRouter"
- path: "backend/routers/settings.py"
provides: "GET/PUT /api/settings endpoint per API key e configurazione"
@@ -53,6 +55,10 @@ must_haves:
to: "Claude API"
via: "anthropic.Anthropic client con retry loop"
pattern: "client\\.messages\\.create"
- from: "backend/services/llm_service.py"
to: "backend/schemas/generate.py"
via: "generate_topic() valida output con TopicResult(BaseModel)"
pattern: "TopicResult"
- from: "backend/services/csv_builder.py"
to: "backend/constants.py"
via: "Importa CANVA_FIELDS per header CSV"
@@ -120,8 +126,13 @@ Output: API backend completa che accetta una richiesta di generazione calendario
- Pydantic ValidationError: riprova UNA volta con istruzione correttiva appesa al prompt ("Il tuo output precedente non era JSON valido. Rispondi SOLO con JSON valido secondo lo schema.")
- Qualsiasi altra eccezione: non ritentare, solleva
f. Dopo ogni chiamata riuscita, applica inter_request_delay (time.sleep) per rispettare OTPM Tier 1
- Metodo generate_topic(system_prompt: str, obiettivo: str, tipo_contenuto: str, nicchia: str) -> str:
Genera un topic specifico per lo slot dato l'obiettivo campagna. Ritorna una stringa topic.
- Metodo generate_topic(system_prompt: str, obiettivo: str, tipo_contenuto: str, nicchia: str, fase_campagna: str) -> str:
Genera un topic specifico per lo slot dato l'obiettivo campagna.
Usa lo stesso pattern di validazione delle altre generazioni:
a. Carica prompt da topic_generator.txt via PromptService
b. Chiama generate() con response_schema=TopicResult (Pydantic model definito in schemas/generate.py)
c. Ritorna result.topic (stringa estratta dal model validato)
Questo garantisce che anche la generazione topic passi per il loop retry/validation JSON, coerente con LLM-02.
- Log strutturato: ogni chiamata logga model, tokens in/out, tempo risposta, tentativo N/max
2. Creare backend/services/csv_builder.py:
@@ -143,20 +154,32 @@ Output: API backend completa che accetta una richiesta di generazione calendario
3. Creare backend/services/generation_pipeline.py:
- class GenerationPipeline(__init__ riceve llm_service: LLMService, prompt_service: PromptService, calendar_service: CalendarService, format_selector: FormatSelector, csv_builder: CSVBuilder)
- Metodo generate_bulk(request: CalendarRequest, api_key: str) -> GenerateResponse:
a. Genera calendario via calendar_service.generate_calendar(request)
b. Per ogni slot del calendario:
- Genera topic via llm_service.generate_topic() se slot.topic e' None
- Dict in-memory _jobs: dict[str, JobStatus] per tracciare progresso dei job in corso
- Dataclass JobStatus: job_id (str), status (Literal["running", "completed", "failed"]), total (int), completed (int), current_post (int), results (list[PostResult]), calendar (Optional[CalendarResponse]), error (Optional[str])
- Metodo generate_bulk_async(request: CalendarRequest, api_key: str) -> str:
a. Genera job_id (UUID)
b. Genera calendario via calendar_service.generate_calendar(request)
c. Inizializza _jobs[job_id] con status="running", total=len(slots), completed=0
d. Lancia _run_generation(job_id, calendario, request) come asyncio.create_task (background)
e. Ritorna job_id immediatamente
- Metodo _run_generation(job_id, calendar, request) — async background:
a. Per ogni slot del calendario:
- Aggiorna _jobs[job_id].current_post = indice corrente
- Genera topic via llm_service.generate_topic(system_prompt, obiettivo, slot.tipo_contenuto, slot.target_nicchia, slot.fase_campagna) se slot.topic e' None
- Seleziona il prompt template corretto in base a formato_narrativo (es. "pas_valore" per PAS + valore)
- Compila il prompt con variabili (obiettivo, nicchia, livello, topic, brand)
- Chiama llm_service.generate(system_prompt, user_prompt, GeneratedPost)
- Se successo: PostResult(status="success", post=risultato)
- Se fallimento: PostResult(status="failed", error=str(e))
- CRITICO Pitfall 5: ogni slot in try/except INDIVIDUALE. Un fallimento NON blocca il loop.
c. Genera job_id (UUID)
d. Chiama csv_builder.build_csv() con i risultati
e. Salva job metadata in OUTPUTS_PATH / f"{job_id}.json" (per ricaricamento)
f. Ritorna GenerateResponse con risultati per-item
- Aggiorna _jobs[job_id].completed += 1 e appendi risultato
b. Chiama csv_builder.build_csv() con i risultati
c. Salva job metadata in OUTPUTS_PATH / f"{job_id}.json" (per ricaricamento e persistenza)
d. Aggiorna _jobs[job_id].status = "completed"
- Metodo get_job_status(job_id: str) -> JobStatus:
Ritorna lo stato corrente del job (per polling). Se non in memory, carica da disco ({job_id}.json).
- Metodo get_job_results(job_id: str) -> GenerateResponse:
Ritorna risultati completi. Carica da _jobs o da disco.
- Metodo generate_single(slot: CalendarSlot, obiettivo: str, api_key: str) -> PostResult:
Genera un singolo post. Utile per rigenerazione di post falliti.
- Metodo _select_prompt_template(formato: str, tipo: str) -> str:
@@ -166,12 +189,17 @@ Output: API backend completa che accetta una richiesta di generazione calendario
<verify>
- LLMService ha gestione specifica per RateLimitError con lettura retry-after
- LLMService ha inter_request_delay dopo ogni chiamata riuscita
- LLMService.generate_topic() chiama generate() con TopicResult come response_schema
- CSVBuilder importa CANVA_FIELDS e usa encoding='utf-8-sig'
- GenerationPipeline.generate_bulk_async() ritorna str (job_id), non GenerateResponse
- GenerationPipeline ha _run_generation come async background task con asyncio.create_task
- GenerationPipeline ha try/except dentro il loop per-slot (non attorno al loop intero)
- GenerationPipeline._jobs dict traccia progresso real-time per ogni job
- GenerationPipeline.get_job_status() ritorna JobStatus con completed/total/current_post
- GenerationPipeline salva job metadata JSON per ricaricamento
</verify>
<done>
LLMService chiama Claude con retry, backoff specifico per 429, e validation Pydantic. CSVBuilder produce CSV con encoding utf-8-sig e header CANVA_FIELDS locked. GenerationPipeline orchestra il flusso completo con per-item error isolation.
LLMService chiama Claude con retry, backoff specifico per 429, e validation Pydantic (incluso generate_topic con TopicResult). CSVBuilder produce CSV con encoding utf-8-sig e header CANVA_FIELDS locked. GenerationPipeline orchestra il flusso completo come background task async con progresso real-time tracciato in _jobs dict e per-item error isolation.
</done>
</task>
@@ -197,18 +225,28 @@ Output: API backend completa che accetta una richiesta di generazione calendario
3. Creare backend/routers/generate.py:
- router = APIRouter(prefix="/api/generate", tags=["generate"])
- POST /bulk: riceve CalendarRequest (+ eventuali topic overrides), usa GenerationPipeline.generate_bulk(), ritorna GenerateResponse
- POST /bulk: riceve CalendarRequest (+ eventuali topic overrides), usa GenerationPipeline.generate_bulk_async()
- Prima verifica che API key sia configurata (da settings), ritorna 400 se mancante
- Ritorna 200 anche con risultati parziali (alcuni failed) — il frontend gestisce lo stato per-item
- Ritorna IMMEDIATAMENTE 202 Accepted con {"job_id": "uuid"} — la generazione continua in background
- Il frontend usa polling su /job/{job_id}/status per seguire il progresso
- GET /job/{job_id}/status: ritorna lo stato corrente del job per polling
- Risposta: {"job_id", "status": "running|completed|failed", "total": 13, "completed": 5, "current_post": 6, "results": [...completed results...]}
- Frontend chiama ogni 2 secondi finche' status != "running"
- GET /job/{job_id}: ritorna i risultati completi di un job (carica da GenerationPipeline.get_job_results())
- POST /single: riceve GenerateRequest (singolo slot), usa GenerationPipeline.generate_single(), ritorna PostResult
- GET /job/{job_id}: ritorna i risultati salvati di un job precedente (carica da OUTPUTS_PATH/{job_id}.json)
4. Creare backend/routers/export.py:
- router = APIRouter(prefix="/api/export", tags=["export"])
- GET /{job_id}/csv: trova file CSV in OUTPUTS_PATH/{job_id}.csv
- GET /{job_id}/csv: trova file CSV originale in OUTPUTS_PATH/{job_id}.csv
- Ritorna FileResponse con media_type="text/csv; charset=utf-8"
- Headers: Content-Disposition: attachment; filename="postgenerator_{job_id}.csv"
- Ritorna 404 se file non esiste
- POST /{job_id}/csv: accetta body JSON con i post modificati dall'utente (inline edits)
- Riceve: {"results": list[PostResult]} con i dati aggiornati dal frontend
- Rigenera il CSV usando CSVBuilder.build_csv() con i dati modificati
- Salva come OUTPUTS_PATH/{job_id}_edited.csv
- Ritorna il file CSV rigenerato con Content-Disposition attachment
- Questo risolve il problema delle modifiche inline perse al download: il frontend invia lo stato locale modificato e riceve un CSV aggiornato
5. Creare backend/routers/settings.py:
- router = APIRouter(prefix="/api/settings", tags=["settings"])
@@ -229,13 +267,15 @@ Output: API backend completa che accetta una richiesta di generazione calendario
<verify>
- backend/main.py include tutti e 4 i router PRIMA del mount SPAStaticFiles
- POST /api/calendar/generate accetta CalendarRequest body
- POST /api/generate/bulk verifica API key prima di procedere
- GET /api/export/{job_id}/csv ha Content-Disposition header
- POST /api/generate/bulk verifica API key, ritorna 202 con job_id (non attende completamento)
- GET /api/generate/job/{job_id}/status ritorna status, total, completed, current_post per polling
- GET /api/export/{job_id}/csv ha Content-Disposition header (file originale)
- POST /api/export/{job_id}/csv accetta results modificati e rigenera CSV
- GET /api/settings/status ritorna api_key_configured boolean
- Nessun router contiene logica di business (solo validazione + chiamata service + return)
</verify>
<done>
4 routers API (calendar, generate, export, settings) creati e montati in main.py. Ogni endpoint ha schema request/response Pydantic. Generate verifica API key. Export serve CSV con header corretti. Settings gestisce configurazione persistente.
4 routers API (calendar, generate, export, settings) creati e montati in main.py. Ogni endpoint ha schema request/response Pydantic. Generate e' async (202 + polling via /status). Export serve CSV originale (GET) e CSV con modifiche inline (POST). Settings gestisce configurazione persistente.
</done>
</task>
@@ -244,17 +284,23 @@ Output: API backend completa che accetta una richiesta di generazione calendario
<verification>
1. `python -c "from backend.services.llm_service import LLMService; print('OK')"` — importa senza errori
2. `python -c "from backend.services.csv_builder import CSVBuilder; print('OK')"` — importa senza errori
3. `python -c "from backend.main import app; print(app.routes)"` — mostra tutti i routes registrati
3. `python -c "from backend.main import app; print(app.routes)"` — mostra tutti i routes registrati incluso /api/generate/job/{job_id}/status
4. CSVBuilder usa encoding='utf-8-sig' nel codice (grep)
5. GenerationPipeline ha try/except PER SINGOLO slot, non attorno al loop
6. LLMService gestisce RateLimitError separatamente dalle altre eccezioni
7. Nessun import circolare tra moduli
6. GenerationPipeline.generate_bulk_async() ritorna job_id (str), non GenerateResponse
7. LLMService gestisce RateLimitError separatamente dalle altre eccezioni
8. LLMService.generate_topic() usa TopicResult come response_schema (non ritorna raw string)
9. POST /api/export/{job_id}/csv endpoint esiste e accetta body con results
10. Nessun import circolare tra moduli
</verification>
<success_criteria>
- LLMService chiama Claude con retry specifico per 429 e validation Pydantic
- LLMService chiama Claude con retry specifico per 429 e validation Pydantic (incluso generate_topic con TopicResult)
- CSVBuilder produce CSV con utf-8-sig encoding e CANVA_FIELDS header
- GenerationPipeline ha per-item error isolation
- GenerationPipeline ha per-item error isolation e background task async
- POST /api/generate/bulk ritorna 202 con job_id, generazione continua in background
- GET /api/generate/job/{job_id}/status fornisce progresso real-time per polling
- POST /api/export/{job_id}/csv accetta dati modificati e rigenera CSV
- 4 API routers montati e funzionali
- Settings endpoint gestisce API key
- Job results salvati su disco per ricaricamento