Files
Michele 696b265e4d 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>
2026-03-08 01:40:30 +01:00

19 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
01-core-generation-pipeline 03 execute 2
01-01
01-02
backend/services/llm_service.py
backend/services/csv_builder.py
backend/services/generation_pipeline.py
backend/routers/calendar.py
backend/routers/generate.py
backend/routers/export.py
backend/routers/settings.py
backend/schemas/settings.py
backend/main.py
true
truths artifacts key_links
LLMService chiama Claude API con retry e backoff, gestisce 429 leggendo retry-after header
LLMService valida il JSON output con Pydantic GeneratedPost e rigetta output malformato
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 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
path provides contains
backend/services/llm_service.py LLMService con retry, backoff, rate limit, JSON validation via Pydantic class LLMService
path provides contains
backend/services/csv_builder.py CSVBuilder con CANVA_FIELDS header locked, utf-8-sig encoding, write to disk class CSVBuilder
path provides contains
backend/services/generation_pipeline.py GenerationPipeline che orchestra calendario -> LLM -> CSV con per-item isolation class GenerationPipeline
path provides contains
backend/routers/generate.py POST /api/generate/bulk (async background task), POST /api/generate/single, GET /api/generate/job/{job_id}/status (polling) router = APIRouter
path provides contains
backend/routers/calendar.py POST /api/calendar/generate endpoint router = APIRouter
path provides contains
backend/routers/export.py GET /api/export/{job_id}/csv (originale), POST /api/export/{job_id}/csv (con modifiche inline) router = APIRouter
path provides contains
backend/routers/settings.py GET/PUT /api/settings endpoint per API key e configurazione router = APIRouter
from to via pattern
backend/services/llm_service.py Claude API anthropic.Anthropic client con retry loop client.messages.create
from to via pattern
backend/services/llm_service.py backend/schemas/generate.py generate_topic() valida output con TopicResult(BaseModel) TopicResult
from to via pattern
backend/services/csv_builder.py backend/constants.py Importa CANVA_FIELDS per header CSV CANVA_FIELDS
from to via pattern
backend/services/generation_pipeline.py backend/services/llm_service.py Chiama generate() per ogni slot con try/except per-item llm_service.generate
from to via pattern
backend/routers/generate.py backend/services/generation_pipeline.py Chiama pipeline.generate_bulk() pipeline.generate
from to via pattern
backend/main.py backend/routers/ include_router per tutti i routers include_router
Creare la pipeline LLM completa: LLMService (Claude API con retry/backoff/rate limit), CSVBuilder (CSV Canva-compatibile con utf-8-sig), GenerationPipeline (orchestrazione con per-item error isolation), e tutti gli API routers (calendar, generate, export, settings).

Purpose: Connettere i servizi di dominio (Plan 02) alla Claude API e al CSV export, creando gli endpoint REST che il frontend (Plan 04) consumera'. Indirizzare i pitfall 1 (soft failures), 3 (CSV encoding), 5 (all-or-nothing batch), e 6 (rate limit).

Output: API backend completa che accetta una richiesta di generazione calendario, chiama Claude per ogni post con error isolation, produce un CSV scaricabile con encoding corretto.

<execution_context> @C:\Users\miche.claude/get-shit-done/workflows/execute-plan.md @C:\Users\miche.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/research/STACK.md @.planning/research/ARCHITECTURE.md @.planning/research/PITFALLS.md @.planning/phases/01-core-generation-pipeline/01-CONTEXT.md @.planning/phases/01-core-generation-pipeline/01-01-SUMMARY.md @.planning/phases/01-core-generation-pipeline/01-02-SUMMARY.md Task 1: LLMService, CSVBuilder, GenerationPipeline backend/services/llm_service.py backend/services/csv_builder.py backend/services/generation_pipeline.py 1. Creare backend/services/llm_service.py: - class LLMService(__init__ riceve api_key: str, model: str = "claude-sonnet-4-5", max_retries: int = 3, inter_request_delay: float = 2.0) - Usa anthropic.Anthropic(api_key=api_key) per il client - Metodo generate(system_prompt: str, user_prompt: str, response_schema: Type[BaseModel]) -> BaseModel: a. Loop retry con max_retries tentativi b. Chiama client.messages.create con model, max_tokens=4096, system=system_prompt, messages user c. Parse response.content[0].text come JSON d. Valida con response_schema.model_validate_json(raw_text) e. Gestione errori SPECIFICA: - anthropic.RateLimitError (429): leggi response header retry-after, attendi quel tempo esatto, poi riprova - anthropic.APIStatusError (5xx): exponential backoff con jitter (base_delay * 2^attempt + random 0-1s) - 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, 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:
   - class CSVBuilder
   - Importa CANVA_FIELDS da backend.constants
   - Metodo build_csv(posts: list[PostResult], calendar: CalendarResponse, job_id: str) -> Path:
     a. Filtra solo PostResult con status="success"
     b. Per ogni post success, mappa GeneratedPost + CalendarSlot -> dict con chiavi CANVA_FIELDS
        - Metadati: campagna, fase_campagna, tipo_contenuto, formato_narrativo, funzione, livello_schwartz, target_nicchia, data_pub_suggerita (da CalendarSlot)
        - Cover: cover_title, cover_subtitle, cover_image_keyword (da GeneratedPost)
        - Slide s2-s7: headline, body, image_keyword (da GeneratedPost.slides[0..5])
        - CTA: cta_text, cta_subtext, cta_image_keyword (da GeneratedPost)
        - caption_instagram (da GeneratedPost)
     c. Scrive CSV su disco in OUTPUTS_PATH / f"{job_id}.csv"
     d. Encoding: utf-8-sig (BOM) — CRITICO per Excel + caratteri italiani (Pitfall 3)
     e. Usa csv.DictWriter con fieldnames=CANVA_FIELDS
     f. Ritorna il Path del file scritto
   - Metodo build_csv_content(posts, calendar, job_id) -> str: come sopra ma ritorna stringa CSV (per preview)

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)
   - 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.
        - 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:
     Mappa formato_narrativo + tipo_contenuto al nome del file prompt (es. "PAS" + "valore" -> "pas_valore")
     Fallback a "pas_valore" se template specifico non esiste
- 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 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. Task 2: API routers e wiring in main.py backend/routers/calendar.py backend/routers/generate.py backend/routers/export.py backend/routers/settings.py backend/schemas/settings.py backend/main.py 1. Creare backend/schemas/settings.py: - class Settings(BaseModel): api_key (Optional[str]), llm_model (str, default "claude-sonnet-4-5"), nicchie_attive (list[str], default NICCHIE_DEFAULT), lingua (str, default "italiano"), frequenza_post (int, default 3), brand_name (Optional[str]), tono (Optional[str], default "diretto e concreto") - Settings salvate in CONFIG_PATH / "settings.json"
2. Creare backend/routers/calendar.py:
   - router = APIRouter(prefix="/api/calendar", tags=["calendar"])
   - POST /generate: riceve CalendarRequest, usa CalendarService, ritorna CalendarResponse
   - GET /formats: ritorna il mapping formati da FormatSelector

3. Creare backend/routers/generate.py:
   - router = APIRouter(prefix="/api/generate", tags=["generate"])
   - 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 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

4. Creare backend/routers/export.py:
   - router = APIRouter(prefix="/api/export", tags=["export"])
   - 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"])
   - GET /: carica settings da settings.json, ritorna Settings (con api_key mascherata: mostra solo ultimi 4 caratteri)
   - PUT /: riceve Settings, salva in settings.json, ritorna Settings aggiornate
   - GET /status: ritorna {"api_key_configured": bool, "llm_model": str} — usato dal frontend per abilitare/disabilitare pulsante genera

6. Aggiornare backend/main.py:
   - Importare tutti i router: calendar, generate, export, settings
   - app.include_router() per ciascuno, PRIMA del mount SPAStaticFiles
   - Aggiungere lifespan/startup che:
     a. Crea directory dati se non esistono
     b. Copia prompts default in DATA_PATH/prompts/ se la directory e' vuota (primo avvio)
   - Ordine mount: health -> routers -> SPAStaticFiles (ULTIMO)

NOTA: I routers sono thin — validano input, chiamano service, ritornano output. Nessuna logica di business nei routers.
- 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, 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) 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. 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 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. 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

<success_criteria>

  • 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 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 </success_criteria>
After completion, create `.planning/phases/01-core-generation-pipeline/01-03-SUMMARY.md`