--- phase: 01-core-generation-pipeline plan: 03 subsystem: api tags: [python, fastapi, anthropic, pydantic, csv, asyncio, retry, backoff, rate-limit] # Dependency graph requires: - phase: 01-core-generation-pipeline (plan 01) provides: backend/config.py con OUTPUTS_PATH, CONFIG_PATH, PROMPTS_PATH - phase: 01-core-generation-pipeline (plan 02) provides: CalendarService, PromptService, CANVA_FIELDS, GeneratedPost schema, TopicResult schema provides: - LLMService con retry 3x, RateLimitError con lettura retry-after header, backoff esponenziale 5xx, validazione Pydantic con correzione automatica - CSVBuilder con encoding utf-8-sig, header CANVA_FIELDS locked, mapping GeneratedPost+CalendarSlot -> 33 colonne - GenerationPipeline con background task asyncio, _jobs dict real-time, per-item error isolation, persistenza JSON - API router calendar: POST /api/calendar/generate, GET /api/calendar/formats - API router generate: POST /api/generate/bulk (202+job_id), GET /job/{id}/status (polling), GET /job/{id}, POST /single - API router export: GET /api/export/{id}/csv (originale), POST /api/export/{id}/csv (modifiche inline) - API router settings: GET /api/settings/status, GET /api/settings, PUT /api/settings affects: - 01-04 (frontend usa tutti questi endpoint API) - fase deploy (tutti gli endpoint da testare via HTTP) # Tech tracking tech-stack: added: - anthropic.Anthropic client (già in requirements.txt) - asyncio.create_task per background generazione patterns: - "Retry pattern: RateLimitError con retry-after header esatto, 5xx con backoff esponenziale + jitter" - "Per-item isolation: try/except individuale per slot dentro il loop, non attorno al loop" - "Background task pattern: generate_bulk_async ritorna job_id, _run_generation gira in background" - "Job polling pattern: _jobs dict in-memory + file JSON su disco per resume post-restart" - "Settings masking: api_key mai inviata al frontend, solo ultimi 4 caratteri mostrati" - "CSV BOM: encoding utf-8-sig garantisce compatibilità Excel con caratteri italiani" key-files: created: - 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 modified: - backend/main.py (aggiunto include_router x4, copia prompt default al primo avvio) key-decisions: - "LLMService._parse_retry_after() legge l'header 'retry-after' dalla response HTTP per wait esatto (non hardcoded)" - "GenerationPipeline usa asyncio.to_thread per wrap dei metodi LLM sincroni (time.sleep non blocca event loop)" - "Settings.api_key non sovrascritta con None se non inviata nel PUT body (merge con esistente)" - "POST /export/{job_id}/csv usa build_csv_content() che produce stringa, GET usa FileResponse da disco" - "Pipeline singleton in-memory in generate.py per mantenere _jobs tra request, con fallback da disco" patterns-established: - "Thin routers: nessuna logica di business nei router, solo validazione + chiamata service + return" - "API key loading: legge da settings.json con fallback a env var ANTHROPIC_API_KEY" - "Job lifecycle: running -> completed (con CSV su disco) | running -> failed (con error)" # Metrics duration: 8min completed: 2026-03-08 --- # Phase 1 Plan 03: Pipeline LLM, CSVBuilder e API routers completi **LLMService con retry specifico per 429/5xx/ValidationError, CSVBuilder utf-8-sig con CANVA_FIELDS locked, GenerationPipeline async con per-item isolation, e 4 router FastAPI (calendar, generate, export, settings) cablati in main.py** ## Performance - **Duration:** 8 min - **Started:** 2026-03-08T01:06:19Z - **Completed:** 2026-03-08T01:15:06Z - **Tasks:** 2/2 - **Files modified:** 9 ## Accomplishments - LLMService: retry loop 3x con gestione specifica RateLimitError (legge retry-after header), backoff esponenziale+jitter per 5xx, ValidationError riprova con istruzione correttiva, inter_request_delay post-call, logging strutturato tokens/elapsed - CSVBuilder: encoding utf-8-sig (BOM), header CANVA_FIELDS locked, mappa GeneratedPost+CalendarSlot -> 33 colonne, build_csv() scrive su disco, build_csv_content() ritorna stringa per export inline - GenerationPipeline: generate_bulk_async ritorna job_id subito (asyncio.create_task), _run_generation in background con try/except PER SLOT (non attorno al loop), _jobs dict real-time, persistenza JSON su disco per resume - 4 router API completi e thin (nessuna logica di business inline): calendar, generate, export, settings - main.py aggiornato: include_router x4 prima di SPAStaticFiles, copia prompt default al primo avvio ## Task Commits Ogni task committato atomicamente: 1. **Task 1: LLMService, CSVBuilder, GenerationPipeline** - `083621a` (feat) 2. **Task 2: API routers e wiring main.py** - `e06edde` (feat) **Plan metadata:** (questo commit) (docs) ## Files Created/Modified - `backend/services/llm_service.py` — LLMService: retry, backoff RateLimitError, validation Pydantic, generate_topic con TopicResult - `backend/services/csv_builder.py` — CSVBuilder: utf-8-sig, CANVA_FIELDS, mapping 33 colonne - `backend/services/generation_pipeline.py` — GenerationPipeline: background task, _jobs dict, per-item isolation, persistenza JSON - `backend/routers/calendar.py` — POST /api/calendar/generate, GET /api/calendar/formats - `backend/routers/generate.py` — POST /api/generate/bulk (202), GET /job/{id}/status, GET /job/{id}, POST /single - `backend/routers/export.py` — GET /api/export/{id}/csv, POST /api/export/{id}/csv (con modifiche inline) - `backend/routers/settings.py` — GET /status, GET /, PUT / (api_key mascherata) - `backend/schemas/settings.py` — Settings pydantic model - `backend/main.py` — aggiunto include_router x4, copia prompt default al primo avvio ## Decisions Made - **LLMService._parse_retry_after()** legge header 'retry-after' dalla response HTTP per wait esatto invece di hardcodare 60s. Se l'header non è disponibile, fallback a 60s. - **asyncio.to_thread** wrappa le chiamate LLM sincrone (che usano time.sleep per inter_request_delay) per non bloccare l'event loop FastAPI durante la generazione background. - **Settings merge**: PUT /api/settings non sovrascrive api_key con None se il body non la include — evita perdita accidentale della chiave quando si aggiornano altri parametri. - **Pipeline singleton**: la GenerationPipeline è un singleton in-memory nel router generate.py per mantenere il _jobs dict tra request HTTP diverse. I job vengono anche salvati su disco per resume dopo restart. - **POST export con stringa**: POST /api/export/{id}/csv usa build_csv_content() che produce una stringa con BOM manuale (\ufeff), poi risponde con Response(content=..., encoding="utf-8") per non raddoppiare il BOM. ## Deviations from Plan Nessuna — piano eseguito esattamente come scritto. Unica nota tecnica: asyncio.to_thread è stato usato per wrappare le chiamate LLM (sync con time.sleep) nel background task async — questo è un dettaglio implementativo necessario per non bloccare l'event loop, non una deviazione dal piano. ## Issues Encountered Nessun problema durante l'implementazione. Tutti i moduli importano senza errori e tutte le 10 verifiche del piano passano. ## User Setup Required Nessuno per questo piano — la pipeline è pronta ma richiede ANTHROPIC_API_KEY per funzionare. La key può essere configurata via PUT /api/settings o via variabile d'ambiente ANTHROPIC_API_KEY. ## Next Phase Readiness - LLMService, CSVBuilder, GenerationPipeline pronti per Plan 04 (frontend) - Tutti gli endpoint API documentati e verificati — il frontend può iniziare a integrarli - Pitfall risolti: #1 (soft failures via per-item isolation), #3 (CSV encoding utf-8-sig), #5 (all-or-nothing via per-item try/except), #6 (rate limit via retry-after header) - Job polling pattern definito: POST /bulk -> job_id -> GET /job/{id}/status ogni 2s finché completed/failed - Nessun blocco: Plan 04 (frontend UI) può proseguire con questi endpoint --- *Phase: 01-core-generation-pipeline* *Completed: 2026-03-08*