docs(01): create phase plan
Phase 01: Core Generation Pipeline - 4 plan(s) in 3 wave(s) - Wave 1: 01-01 (infra) + 01-02 (core services) parallel - Wave 2: 01-03 (LLM pipeline + API routers) - Wave 3: 01-04 (Web UI) with human-verify checkpoint - Ready for execution
This commit is contained in:
265
.planning/phases/01-core-generation-pipeline/01-03-PLAN.md
Normal file
265
.planning/phases/01-core-generation-pipeline/01-03-PLAN.md
Normal file
@@ -0,0 +1,265 @@
|
||||
---
|
||||
phase: 01-core-generation-pipeline
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["01-01", "01-02"]
|
||||
files_modified:
|
||||
- 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
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "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 ritorna GenerateResponse con risultati per-item (success/failed)"
|
||||
- "API endpoint GET /api/export/{job_id}/csv scarica file CSV con Content-Disposition attachment"
|
||||
- "API endpoint GET /api/settings ritorna configurazione corrente, PUT /api/settings salva"
|
||||
artifacts:
|
||||
- path: "backend/services/llm_service.py"
|
||||
provides: "LLMService con retry, backoff, rate limit, JSON validation via Pydantic"
|
||||
contains: "class LLMService"
|
||||
- path: "backend/services/csv_builder.py"
|
||||
provides: "CSVBuilder con CANVA_FIELDS header locked, utf-8-sig encoding, write to disk"
|
||||
contains: "class CSVBuilder"
|
||||
- path: "backend/services/generation_pipeline.py"
|
||||
provides: "GenerationPipeline che orchestra calendario -> LLM -> CSV con per-item isolation"
|
||||
contains: "class GenerationPipeline"
|
||||
- path: "backend/routers/generate.py"
|
||||
provides: "POST /api/generate/bulk e POST /api/generate/single endpoints"
|
||||
contains: "router = APIRouter"
|
||||
- path: "backend/routers/calendar.py"
|
||||
provides: "POST /api/calendar/generate endpoint"
|
||||
contains: "router = APIRouter"
|
||||
- path: "backend/routers/export.py"
|
||||
provides: "GET /api/export/{job_id}/csv endpoint con FileResponse"
|
||||
contains: "router = APIRouter"
|
||||
- path: "backend/routers/settings.py"
|
||||
provides: "GET/PUT /api/settings endpoint per API key e configurazione"
|
||||
contains: "router = APIRouter"
|
||||
key_links:
|
||||
- from: "backend/services/llm_service.py"
|
||||
to: "Claude API"
|
||||
via: "anthropic.Anthropic client con retry loop"
|
||||
pattern: "client\\.messages\\.create"
|
||||
- from: "backend/services/csv_builder.py"
|
||||
to: "backend/constants.py"
|
||||
via: "Importa CANVA_FIELDS per header CSV"
|
||||
pattern: "CANVA_FIELDS"
|
||||
- from: "backend/services/generation_pipeline.py"
|
||||
to: "backend/services/llm_service.py"
|
||||
via: "Chiama generate() per ogni slot con try/except per-item"
|
||||
pattern: "llm_service\\.generate"
|
||||
- from: "backend/routers/generate.py"
|
||||
to: "backend/services/generation_pipeline.py"
|
||||
via: "Chiama pipeline.generate_bulk()"
|
||||
pattern: "pipeline\\.generate"
|
||||
- from: "backend/main.py"
|
||||
to: "backend/routers/"
|
||||
via: "include_router per tutti i routers"
|
||||
pattern: "include_router"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<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>
|
||||
|
||||
<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
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: LLMService, CSVBuilder, GenerationPipeline</name>
|
||||
<files>
|
||||
backend/services/llm_service.py
|
||||
backend/services/csv_builder.py
|
||||
backend/services/generation_pipeline.py
|
||||
</files>
|
||||
<action>
|
||||
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) -> str:
|
||||
Genera un topic specifico per lo slot dato l'obiettivo campagna. Ritorna una stringa topic.
|
||||
- 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)
|
||||
- Metodo generate_bulk(request: CalendarRequest, api_key: str) -> GenerateResponse:
|
||||
a. Genera calendario via calendar_service.generate_calendar(request)
|
||||
b. Per ogni slot del calendario:
|
||||
- Genera topic via llm_service.generate_topic() 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.
|
||||
c. Genera job_id (UUID)
|
||||
d. Chiama csv_builder.build_csv() con i risultati
|
||||
e. Salva job metadata in OUTPUTS_PATH / f"{job_id}.json" (per ricaricamento)
|
||||
f. Ritorna GenerateResponse con risultati per-item
|
||||
- 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
|
||||
</action>
|
||||
<verify>
|
||||
- LLMService ha gestione specifica per RateLimitError con lettura retry-after
|
||||
- LLMService ha inter_request_delay dopo ogni chiamata riuscita
|
||||
- CSVBuilder importa CANVA_FIELDS e usa encoding='utf-8-sig'
|
||||
- GenerationPipeline ha try/except dentro il loop per-slot (non attorno al loop intero)
|
||||
- GenerationPipeline salva job metadata JSON per ricaricamento
|
||||
</verify>
|
||||
<done>
|
||||
LLMService chiama Claude con retry, backoff specifico per 429, e validation Pydantic. CSVBuilder produce CSV con encoding utf-8-sig e header CANVA_FIELDS locked. GenerationPipeline orchestra il flusso completo con per-item error isolation.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: API routers e wiring in main.py</name>
|
||||
<files>
|
||||
backend/routers/calendar.py
|
||||
backend/routers/generate.py
|
||||
backend/routers/export.py
|
||||
backend/routers/settings.py
|
||||
backend/schemas/settings.py
|
||||
backend/main.py
|
||||
</files>
|
||||
<action>
|
||||
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(), ritorna GenerateResponse
|
||||
- Prima verifica che API key sia configurata (da settings), ritorna 400 se mancante
|
||||
- Ritorna 200 anche con risultati parziali (alcuni failed) — il frontend gestisce lo stato per-item
|
||||
- POST /single: riceve GenerateRequest (singolo slot), usa GenerationPipeline.generate_single(), ritorna PostResult
|
||||
- GET /job/{job_id}: ritorna i risultati salvati di un job precedente (carica da OUTPUTS_PATH/{job_id}.json)
|
||||
|
||||
4. Creare backend/routers/export.py:
|
||||
- router = APIRouter(prefix="/api/export", tags=["export"])
|
||||
- GET /{job_id}/csv: trova file CSV 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
|
||||
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
- 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 prima di procedere
|
||||
- GET /api/export/{job_id}/csv ha Content-Disposition header
|
||||
- GET /api/settings/status ritorna api_key_configured boolean
|
||||
- Nessun router contiene logica di business (solo validazione + chiamata service + return)
|
||||
</verify>
|
||||
<done>
|
||||
4 routers API (calendar, generate, export, settings) creati e montati in main.py. Ogni endpoint ha schema request/response Pydantic. Generate verifica API key. Export serve CSV con header corretti. Settings gestisce configurazione persistente.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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
|
||||
4. CSVBuilder usa encoding='utf-8-sig' nel codice (grep)
|
||||
5. GenerationPipeline ha try/except PER SINGOLO slot, non attorno al loop
|
||||
6. LLMService gestisce RateLimitError separatamente dalle altre eccezioni
|
||||
7. Nessun import circolare tra moduli
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- LLMService chiama Claude con retry specifico per 429 e validation Pydantic
|
||||
- CSVBuilder produce CSV con utf-8-sig encoding e CANVA_FIELDS header
|
||||
- GenerationPipeline ha per-item error isolation
|
||||
- 4 API routers montati e funzionali
|
||||
- Settings endpoint gestisce API key
|
||||
- Job results salvati su disco per ricaricamento
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-core-generation-pipeline/01-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user