docs(01-03): complete pipeline LLM + API routers plan
Tasks completed: 2/2 - Task 1: LLMService, CSVBuilder, GenerationPipeline - Task 2: API routers (calendar, generate, export, settings) e wiring main.py SUMMARY: .planning/phases/01-core-generation-pipeline/01-03-SUMMARY.md
This commit is contained in:
141
.planning/phases/01-core-generation-pipeline/01-03-SUMMARY.md
Normal file
141
.planning/phases/01-core-generation-pipeline/01-03-SUMMARY.md
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
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*
|
||||
Reference in New Issue
Block a user