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:
Michele
2026-03-08 02:16:49 +01:00
parent e06edde4ef
commit 60b46cb5c1
3 changed files with 166 additions and 32 deletions

View File

@@ -93,7 +93,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
| Phase | Plans Complete | Status | Completed | | Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------| |-------|----------------|--------|-----------|
| 1. Core Generation Pipeline | 0/4 | Planned (3 waves) | - | | 1. Core Generation Pipeline | 3/4 | In progress | - |
| 2. Prompt Control + Output Review | 0/2 | Not started | - | | 2. Prompt Control + Output Review | 0/2 | Not started | - |
| 3. Organization Layer | 0/2 | Not started | - | | 3. Organization Layer | 0/2 | Not started | - |
| 4. Enrichment | 0/1 | Not started | - | | 4. Enrichment | 0/1 | Not started | - |

View File

@@ -10,28 +10,28 @@ See: .planning/PROJECT.md (updated 2026-03-07)
## Current Position ## Current Position
Phase: 1 of 4 (Core Generation Pipeline) Phase: 1 of 4 (Core Generation Pipeline)
Plan: 2 of 4 in current phase Plan: 3 of 4 in current phase
Status: In progress — Plan 02 completato (parallelo con Plan 01) Status: In progress — Plan 03 completato
Last activity: 2026-03-08 — Completato 01-02-PLAN.md (servizi dominio + prompt) Last activity: 2026-03-08 — Completato 01-03-PLAN.md (pipeline LLM + API routers)
Progress: [██░░░░░░░] 12% (2/16 piani totali stimati) Progress: [██░░░░░░░] 18% (3/16 piani totali stimati)
## Performance Metrics ## Performance Metrics
**Velocity:** **Velocity:**
- Total plans completed: 2 - Total plans completed: 3
- Average duration: ~7 min - Average duration: ~7 min
- Total execution time: 15 min - Total execution time: 23 min
**By Phase:** **By Phase:**
| Phase | Plans | Total | Avg/Plan | | Phase | Plans | Total | Avg/Plan |
|-------|-------|-------|----------| |-------|-------|-------|----------|
| 01-core-generation-pipeline | 2/4 | 15 min | 7 min | | 01-core-generation-pipeline | 3/4 | 23 min | 7 min |
**Recent Trend:** **Recent Trend:**
- Last 5 plans: 6 min, 9 min - Last 3 plans: 6 min, 9 min, 8 min
- Trend: baseline stabilita - Trend: baseline stabile ~7-8 min/piano
*Updated after each plan completion* *Updated after each plan completion*
@@ -42,33 +42,26 @@ Progress: [██░░░░░░░░] 12% (2/16 piani totali stimati)
Decisions are logged in PROJECT.md Key Decisions table. Decisions are logged in PROJECT.md Key Decisions table.
Recent decisions affecting current work: Recent decisions affecting current work:
- [Setup]: Tutti i 9 critical pitfalls identificati dalla research sono concentrati in Phase 1 — affrontarli subito e' la priorita' assoluta - [Setup]: FastAPI root_path SOLO via Uvicorn (--root-path), mai nel costruttore FastAPI()
- [Setup]: FastAPI root_path SOLO via Uvicorn (--root-path), mai nel costruttore FastAPI() — altrimenti doppio path bug - [Setup]: CSV encoding = utf-8-sig (BOM) sempre; CANVA_FIELDS locked come costante
- [Setup]: CSV encoding = utf-8-sig (BOM) sempre; CANVA_FIELDS locked come costante prima di qualsiasi codice di generazione - [Setup]: Prompt di sistema scritti IN italiano
- [Setup]: Prompt di sistema scritti IN italiano (non inglese + "scrivi in italiano") - [Setup]: Per-item error isolation dal primo loop di generazione
- [Setup]: Per-item error isolation dal primo loop di generazione — un fallimento non blocca il batch - [01-01]: root_path SOLO via Uvicorn --root-path nel Dockerfile CMD
- [01-01]: root_path SOLO via Uvicorn --root-path nel Dockerfile CMD, VERIFICATO e funzionante - [01-01]: API_BASE='/postgenerator/api' nel frontend — Pitfall #9 risolto
- [01-01]: API_BASE='/postgenerator/api' nel frontend — Pitfall #9 risolto nella configurazione base
- [01-01]: SPAStaticFiles montato come ultima operazione in main.py — pattern stabilito
- [01-01]: fastapi[standard]==0.135.1, anthropic==0.84.0 pinned in requirements.txt
- [01-01]: docker-compose NO porte esposte, named volume postgenerator-data per persistenza
- [01-02]: CANVA_FIELDS 33 colonne con _image_keyword (non URL) — URL Unsplash in Phase 4 - [01-02]: CANVA_FIELDS 33 colonne con _image_keyword (non URL) — URL Unsplash in Phase 4
- [01-02]: Distribuzione L3 split: valore-L3 in Cattura, riprova_sociale-L3 in Coinvolgi - [01-02]: PromptService usa ValueError per variabili mancanti
- [01-02]: PromptService usa ValueError per variabili mancanti — fail esplicito non silenzioso - [01-03]: LLMService._parse_retry_after() legge header HTTP per wait esatto (fallback 60s)
- [01-02]: Nicchie 50/50: slot pari=generico, slot dispari=verticale in rotazione - [01-03]: asyncio.to_thread wrappa LLM sync calls nel background task async
- [01-03]: Settings.api_key merge: PUT non sovrascrive con None se non inclusa nel body
### Pending Todos - [01-03]: Pipeline singleton in generate.py per mantenere _jobs tra request, con fallback da disco
None.
### Blockers/Concerns ### Blockers/Concerns
- [Phase 1 RISOLTO in 01-02]: CANVA_FIELDS locked con 33 colonne e assert a load-time - [Phase 1]: Validare token usage reale per batch 13 post con claude-sonnet-4-5 — inter_request_delay=2.0s configurato, da validare in produzione
- [Phase 1]: Validare token usage reale per batch 13 post con claude-sonnet-4-5 contro limite Tier 1 (8,000 OTPM) — necessario inter-request delay configurabile (2-3s) (da affrontare in 01-03) - [Phase 1]: Baseline qualita' prompt italiani da validare dopo prima generazione reale
- [Phase 1]: Baseline qualita' prompt italiani da validare dopo prima generazione reale (post 01-03)
## Session Continuity ## Session Continuity
Last session: 2026-03-08T01:00:34Z Last session: 2026-03-08T01:15:06Z
Stopped at: Completato 01-02-PLAN.md — Servizi dominio + prompt .txt italiani Stopped at: Completato 01-03-PLAN.md — Pipeline LLM + CSVBuilder + 4 router API
Resume file: None Resume file: None

View 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*