diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 4d036f5..e036d58 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -93,7 +93,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 | 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 | - | | 3. Organization Layer | 0/2 | Not started | - | | 4. Enrichment | 0/1 | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 2a4d23b..4460bf6 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -10,28 +10,28 @@ See: .planning/PROJECT.md (updated 2026-03-07) ## Current Position Phase: 1 of 4 (Core Generation Pipeline) -Plan: 2 of 4 in current phase -Status: In progress — Plan 02 completato (parallelo con Plan 01) -Last activity: 2026-03-08 — Completato 01-02-PLAN.md (servizi dominio + prompt) +Plan: 3 of 4 in current phase +Status: In progress — Plan 03 completato +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 **Velocity:** -- Total plans completed: 2 +- Total plans completed: 3 - Average duration: ~7 min -- Total execution time: 15 min +- Total execution time: 23 min **By Phase:** | 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:** -- Last 5 plans: 6 min, 9 min -- Trend: baseline stabilita +- Last 3 plans: 6 min, 9 min, 8 min +- Trend: baseline stabile ~7-8 min/piano *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. 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() — altrimenti doppio path bug -- [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 (non inglese + "scrivi in italiano") -- [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, VERIFICATO e funzionante -- [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 +- [Setup]: FastAPI root_path SOLO via Uvicorn (--root-path), mai nel costruttore FastAPI() +- [Setup]: CSV encoding = utf-8-sig (BOM) sempre; CANVA_FIELDS locked come costante +- [Setup]: Prompt di sistema scritti IN italiano +- [Setup]: Per-item error isolation dal primo loop di generazione +- [01-01]: root_path SOLO via Uvicorn --root-path nel Dockerfile CMD +- [01-01]: API_BASE='/postgenerator/api' nel frontend — Pitfall #9 risolto - [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 — fail esplicito non silenzioso -- [01-02]: Nicchie 50/50: slot pari=generico, slot dispari=verticale in rotazione - -### Pending Todos - -None. +- [01-02]: PromptService usa ValueError per variabili mancanti +- [01-03]: LLMService._parse_retry_after() legge header HTTP per wait esatto (fallback 60s) +- [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 +- [01-03]: Pipeline singleton in generate.py per mantenere _jobs tra request, con fallback da disco ### 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 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 (post 01-03) +- [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]: Baseline qualita' prompt italiani da validare dopo prima generazione reale ## Session Continuity -Last session: 2026-03-08T01:00:34Z -Stopped at: Completato 01-02-PLAN.md — Servizi dominio + prompt .txt italiani +Last session: 2026-03-08T01:15:06Z +Stopped at: Completato 01-03-PLAN.md — Pipeline LLM + CSVBuilder + 4 router API Resume file: None diff --git a/.planning/phases/01-core-generation-pipeline/01-03-SUMMARY.md b/.planning/phases/01-core-generation-pipeline/01-03-SUMMARY.md new file mode 100644 index 0000000..d2da2f3 --- /dev/null +++ b/.planning/phases/01-core-generation-pipeline/01-03-SUMMARY.md @@ -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*