Files
taller-wox/SPEC_taller_wox_fitlabs.md
farentsen a062b45c51 feat: initial implementation taller-wox.fitlabs.dev
Portal FastAPI + 5 endpoints REST para Bootcamp Agentic AI con
watsonx Orchestrate (FactorIT). Single container, Coolify-ready.

- Landing brandeado FIT con formulario de registro (honeypot anti-bot)
- Tokens itsdangerous para descargas (24h expiry)
- 5 endpoints API: historical/available procedures, member-insights,
  schedule, generate-report (Jinja2 + Plotly)
- SQLite con upsert-on-email para leads + log de descargas
- Admin endpoints (HTTP Basic): leads.json, leads.csv, stats
- 23 tests pytest pasando
- Dockerfile listo para Coolify con volúmenes persistentes
  (/app/leads.db, /app/app/data/reports_output, /app/material)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 03:04:28 +00:00

32 KiB

SPEC — Portal taller-wox.fitlabs.dev

Proyecto: Sitio web + API para el "Bootcamp Agentic AI con watsonx Orchestrate" de FactorIT (FIT). Dominio: https://taller-wox.fitlabs.dev Audiencia técnica de este documento: un desarrollador (o Claude Code) que va a implementar TODO el sistema de cero.


0. Visión general

Construir un sitio web público + backend API que cumple dos funciones:

  1. Portal web brandeado FactorIT + watsonx Orchestrate donde los alumnos del taller se registran (nombre, email, empresa) y, tras el registro, descargan el material del bootcamp (manual PDF, deck PPT, archivos del lab).

  2. 5 endpoints REST que son consumidos durante el taller por los agentes que los alumnos construyen en IBM watsonx Orchestrate. Estos endpoints se exponen bajo https://taller-wox.fitlabs.dev/api/.

El sitio debe poder hostearse en cualquier proveedor (Vercel, Railway, Render, Fly.io, VPS propio). Recomendado: un solo servicio FastAPI que sirva tanto el frontend (HTML estático + endpoints de registro/descarga) como la API del taller.


1. Stack técnico recomendado

Capa Tecnología Justificación
Backend Python 3.11+ con FastAPI + Uvicorn Más rápido para los endpoints del taller (pandas para filtros)
Frontend HTML + CSS + JS vanilla (sin framework) Simple, una sola página, fácil de mantener
Persistencia SQLite (file-based) Solo se guardan registros de leads; no necesita Postgres
Almacenamiento de archivos Carpeta local ./material/ servida como static Los archivos pesan ~28 MB en total
HTML reports (endpoint 5) Jinja2 + Plotly Genera reportes y los hostea en /api/reports/output/{id}.html
Deploy Railway / Render / Fly.io (con Dockerfile) Cualquiera soporta FastAPI + volumen para SQLite

Dependencias Python:

fastapi
uvicorn[standard]
pandas
jinja2
plotly
python-multipart
pydantic[email]
itsdangerous            # firma tokens de descarga
aiosmtplib              # email opcional (descarga vía link)

2. Estructura de carpetas esperada

taller-wox/
├── app/
│   ├── main.py                    # FastAPI app + mounts
│   ├── frontend.py                # rutas que sirven HTML
│   ├── registration.py            # POST /register, GET /download
│   ├── benefits_api.py            # endpoints 1-4 (AskBenefits)
│   ├── reports_api.py             # endpoint 5 (AskReporting)
│   ├── db.py                      # SQLite (leads.db)
│   ├── data/
│   │   ├── historical_procedures.csv     # dataset endpoint 1
│   │   ├── available_procedures.csv      # dataset endpoint 2
│   │   ├── member_insights.json          # endpoint 3 (estático)
│   │   ├── schedule_response.txt         # endpoint 4 (estático)
│   │   ├── combined_email.txt            # endpoint 5
│   │   ├── provider_email.txt            # endpoint 5
│   │   ├── aetna_email.txt               # endpoint 5
│   │   └── aetna_claim_review_summary.csv# endpoint 5
│   └── templates/
│       ├── index.html             # landing + registro
│       ├── descargas.html         # página de material (post-registro)
│       └── report.html            # template del reporte (endpoint 5)
├── static/
│   ├── css/
│   │   └── styles.css             # estilos del sitio
│   ├── img/
│   │   ├── logo-factorit.svg      # logo FIT
│   │   ├── logo-watsonx.svg       # logo watsonx Orchestrate
│   │   └── hero-bg.jpg            # imagen de fondo (opcional)
│   └── js/
│       └── app.js
├── material/                       # archivos descargables
│   ├── Manual_Alumno_Bootcamp_FIT.docx
│   ├── Bootcamp_Agentic_AI_FIT.pptx
│   ├── openapi-tools-spec.json
│   ├── openapi-tools-spec.yaml
│   ├── dental_benefits_summary.pdf
│   ├── openapi-tools-report.json
│   ├── main-desk-concierge-action.json
│   └── bank-assistant-pack.zip
├── leads.db                        # SQLite (creado en runtime)
├── Dockerfile
├── requirements.txt
└── README.md

3. El sitio web (frontend)

3.1 Branding y look & feel

Paleta de colores (FIT corporativo):

  • Navy principal: #0A1F44
  • Azul: #1E4FA8
  • Cyan accent: #00B5D8
  • Naranja highlight: #FF7A00
  • Crema fondo claro: #F5F7FA
  • Texto oscuro: #1A1A1A
  • Gris secundario: #5A6473

Tipografía: Inter o system-ui (sans-serif). Cargar desde Google Fonts:

<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">

Logos a mostrar (en el header):

  • Logo FactorIT (izquierda) — ya está en static/img/logo-factorit.svg
  • Logo IBM watsonx Orchestrate (derecha, más pequeño, como "powered by") — bajar de https://www.ibm.com/brand/watsonx (o usar un placeholder hasta tener uno oficial)

3.2 Página principal GET /

Estructura visual (una sola página, scroll vertical):

Sección 1 — Hero (pantalla completa, fondo navy con elementos decorativos cyan/naranja):

  • Logo FactorIT arriba a la izquierda
  • Eyebrow: "FACTORIT · FIT"
  • Título grande blanco: "Bootcamp Agentic AI con watsonx Orchestrate"
  • Subtítulo crema italic: "Construye tu primer agente de IA en 4 horas"
  • Sello esquina derecha: "powered by IBM watsonx Orchestrate" (logo IBM pequeño)
  • Botón CTA naranja grande: "Acceder al material →" (scroll smooth a sección 4)

Sección 2 — ¿Qué vas a construir? (3 cards horizontales, fondo crema):

  • Card 1: "Tu primer agente" — icono engranaje navy, descripción 2 líneas
  • Card 2: "Multi-agente con RAG" — icono libro, descripción
  • Card 3: "Reportes y APIs" — icono gráfico, descripción

Sección 3 — El taller en números (4 stats con números grandes naranja):

  • "4h" Duración
  • "6" Módulos
  • "0" Líneas de código (en el módulo 1-4)
  • "100%" Hands-on

Sección 4 — Material del taller (la sección donde está el formulario de registro):

  • Título: "Descarga todo el material"
  • Texto: "Registra tus datos para acceder al kit completo del bootcamp."
  • Formulario con 3 campos (ver 3.3)
  • Tras enviar, redirige a /descargas?token=XXX

Sección 5 — Footer (fondo navy):

  • Logos FIT y watsonx
  • "Material del bootcamp · taller-wox.fitlabs.dev"
  • Link "github.com/leozangulo/agentic-bootcamp"
  • "© 2026 FactorIT — Todos los derechos reservados"

3.3 Formulario de registro

Campos:

Campo Tipo Validación Required
nombre text min 2 chars, max 80
email email regex email válido
empresa text min 2 chars, max 100
consentimiento checkbox debe estar marcado

Texto del checkbox: "Acepto que FactorIT use mis datos para enviarme información del bootcamp y comunicaciones futuras. No spam — solo lo importante."

Validación cliente: HTML5 + JS antes de submit.

Action: POST /register (ver 4.1).

3.4 Página de descargas GET /descargas?token=XXX

Validar el token (firma con itsdangerous, ver 4.1). Si es inválido o expirado, redirigir a /?error=token-invalido.

Si es válido, mostrar:

  • Saludo: "¡Hola {nombre}! Acá tienes todo el material."
  • Grilla 2x4 (o 3x3 según ancho) con cards de descarga, cada una con:
    • Icono del tipo de archivo (PDF, Word, PPT, JSON, ZIP)
    • Nombre del archivo
    • Descripción breve (1 línea)
    • Tamaño en MB
    • Botón "Descargar" naranja

Cards a mostrar:

# Archivo Descripción
1 Manual_Alumno_Bootcamp_FIT.docx Manual paso a paso del taller (26 páginas)
2 Bootcamp_Agentic_AI_FIT.pptx Slides del bootcamp (46 slides)
3 openapi-tools-spec.json API principal de AskBenefits
4 openapi-tools-spec.yaml Misma API en YAML (backup)
5 dental_benefits_summary.pdf PDF para Knowledge Base de AskDental
6 openapi-tools-report.json API de AskReporting
7 main-desk-concierge-action.json Acción watsonx Assistant (opcional)
8 bank-assistant-pack.zip YAMLs + tools Python del ADK

Bonus card (opcional, si quieres ofrecer todo en un solo download):

  • 9 | taller-wox-archivos.zip | Todo el material en un solo zip (28 MB)

Cada botón "Descargar" hace GET /download/{filename}?token=XXX.

3.5 Estilos críticos de CSS

  • Mobile-first (breakpoint a 768px)
  • Hero ocupa 100vh en desktop, 80vh en mobile
  • Cards: border-radius 12px, sombra suave, hover: lift 4px + sombra más fuerte
  • Botones primarios: fondo naranja #FF7A00, hover: #E66A00, border-radius 8px
  • Botones secundarios: outline cyan
  • Animaciones: solo fade-in al scroll (intersection observer), nada elaborado

4. Backend (FastAPI)

4.1 Rutas del frontend y registro

# rutas que sirven HTML
GET  /                       renderiza templates/index.html
GET  /descargas              valida token, renderiza templates/descargas.html
GET  /static/*               archivos estáticos (CSS, JS, imgs, logos)

# registro y descarga
POST /register               recibe form, guarda lead en SQLite, devuelve {token, redirect_url}
GET  /download/{filename}    valida token query param, devuelve archivo de ./material/

Lógica del token:

  • Usar itsdangerous.URLSafeTimedSerializer con un SECRET_KEY desde env.
  • El token contiene el email del lead.
  • Expira a las 24 horas (configurable).
  • Validar en cada descarga.
from itsdangerous import URLSafeTimedSerializer
serializer = URLSafeTimedSerializer(SECRET_KEY)
token = serializer.dumps({"email": email})
# validar:
data = serializer.loads(token, max_age=86400)  # 24h

Esquema SQLite (leads.db):

CREATE TABLE IF NOT EXISTS leads (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    nombre      TEXT NOT NULL,
    email       TEXT NOT NULL,
    empresa     TEXT NOT NULL,
    ip          TEXT,
    user_agent  TEXT,
    consent     INTEGER NOT NULL DEFAULT 0,
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS downloads (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    lead_email  TEXT NOT NULL,
    filename    TEXT NOT NULL,
    ip          TEXT,
    downloaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Registrar cada descarga en downloads para tener métricas.

4.2 Endpoint admin (opcional, protegido con HTTP Basic)

GET  /admin/leads      → lista todos los leads (JSON, requiere basic auth con ADMIN_USER/ADMIN_PASS desde env)
GET  /admin/stats      → conteo de registros, descargas por archivo, top empresas
GET  /admin/leads.csv  → exporta leads en CSV

Útil para que el equipo comercial pueda hacer follow-up.


5. Los 5 endpoints del taller

Los agentes que el alumno construye en watsonx Orchestrate llamarán estos endpoints. Los specs OpenAPI están en material/openapi-tools-spec.json y material/openapi-tools-report.json.

5.1 Endpoint 1 — POST /api/historical-procedures

Para qué sirve: responde "¿qué procedimientos he tenido?", "¿cuánto pagué por X?". Es un dataset histórico filtrable.

Request body (JSON):

{
  "filters": "[{\"column\": \"member_name\", \"operator\": \"equals\", \"value\": \"Alice\"}]",
  "group_by": "[\"relationship\"]"
}

⚠️ Importante: filters y group_by llegan como strings JSON-encoded, no como objetos directos. Decodificar con json.loads() dentro.

Operadores soportados: equals, ne, contains, gt, lt, ge, le.

Dataset a servir (CSV en app/data/historical_procedures.csv, ~50 filas inventadas):

Columnas exactas (todas requeridas):

Columna Tipo Ejemplos
member_name string Alice, Bob, Charlie, Diana, Ethan
relationship string Self, Spouse, Mother, Father, Son, Daughter
age int 12, 25, 42, 65
gender string Female, Male
procedure string Annual Physical Exam, Appendectomy, CT Scan, X Ray, Dental Cleaning, Vision Exam, MRI, Blood Test
procedure_type string preventive, surgery, diagnostic
location string City Hospital, Green Valley Clinic, Sunrise Health, Regional Medical Center
date YYYY-MM-DD 2024-04-28, 2023-05-02
in_network bool true, false
member_plan string Gold PPO, Family Plan - Silver EPO, Bronze HDHP
accepted_plans string "Gold PPO, Medicare Advantage"
cost_facility float 29.4, 48.02
cost_physician float 199.09, 128.9
cost_anesthesia float 0.0, 2024.21
cost_medication float 4.19, 5.53
total_cost float 232.68, 243.3
facility_rating float 4.7, 4.5
notes string "Annual Physical Exam performed at City Hospital."

Lógica del handler:

import pandas as pd, json

df = pd.read_csv("app/data/historical_procedures.csv")

@app.post("/api/historical-procedures")
def historical_procedures(payload: dict):
    filters = json.loads(payload.get("filters", "[]"))
    group_by = json.loads(payload.get("group_by", "[]"))

    result = df.copy()
    for f in filters:
        col, op, val = f["column"], f["operator"], f["value"]
        if op == "equals":   result = result[result[col] == val]
        elif op == "ne":     result = result[result[col] != val]
        elif op == "contains": result = result[result[col].astype(str).str.contains(str(val), case=False, na=False)]
        elif op == "gt":     result = result[result[col] > val]
        elif op == "lt":     result = result[result[col] < val]
        elif op == "ge":     result = result[result[col] >= val]
        elif op == "le":     result = result[result[col] <= val]

    if group_by:
        # agregaciones por columna numérica, conteo por las demás
        numeric_cols = result.select_dtypes(include='number').columns.tolist()
        result = result.groupby(group_by)[numeric_cols].mean().reset_index()

    return {"result": result.to_dict(orient="records")}

Response (200):

{ "result": [ { "member_name": "Alice", "age": 42, ... }, ... ] }

5.2 Endpoint 2 — POST /api/available-procedures

Para qué sirve: responde "¿cuánto cuesta una radiografía?", "¿dónde puedo hacerme una resonancia magnética?". Es el catálogo de procedimientos disponibles con costos por proveedor.

Request body: idéntico al endpoint 1 (filters + group_by).

Dataset (CSV en app/data/available_procedures.csv, ~25-30 filas):

Columna Tipo Ejemplos
procedure string Angioplasty, Annual Physical Exam, Appendectomy, X Ray, MRI, CT Scan
location string City Hospital, Regional Medical Center, Green Valley Clinic
facility_rating float 4.7, 4.3, 4.5
distance_miles float 5.2, 12.6, 1.2
gold_ppo_plan_accepted bool true, false
silver_epo_plan_accepted bool true, false
accepted_plans string "Gold PPO, Medicare Advantage"
cost_facility float 9432.8
cost_physician float 4774.57
cost_anesthesia float 1894.37
cost_medication float 834.8
total_cost float 16936.54

Lógica: misma función que el endpoint 1, solo cambia el CSV de entrada.

Response (200): { "result": [ ... ] }

5.3 Endpoint 3 — GET /api/member-insights

Para qué sirve: responde "¿qué chequeos tengo atrasados?", "¿cuál es mi plan?", "¿cómo accedo a mi formulario 1095?". Devuelve el perfil del afiliado de demo.

Sin parámetros.

Response (200): SIEMPRE el mismo objeto. Guardarlo como app/data/member_insights.json y servirlo tal cual.

{
  "result": {
    "member": {
      "name": "Charlie Smith",
      "date_of_birth": "2013-03-04",
      "plan": "Gold PPO",
      "member_id": "CS-001-2024"
    },
    "medical_plan": {
      "name": "Gold PPO",
      "deductible": 1500,
      "deductible_met": 850,
      "out_of_pocket_max": 6000,
      "out_of_pocket_met": 1200,
      "coinsurance": "20%",
      "primary_care_copay": 25,
      "specialist_copay": 50,
      "emergency_room_copay": 250
    },
    "pharmacy_plan": {
      "tier_1_copay": 10,
      "tier_2_copay": 30,
      "tier_3_copay": 60,
      "tier_4_coinsurance": "30%",
      "mail_order_available": true
    },
    "mental_health": {
      "covered_visits_per_year": 20,
      "visits_used": 4,
      "telehealth": true,
      "in_network_providers": "https://taller-wox.fitlabs.dev/docs/mental-health-providers"
    },
    "wellness": {
      "gym_reimbursement_annual": 300,
      "gym_reimbursement_used": 150,
      "annual_checkup_covered": true,
      "preventive_care_100_percent": true,
      "flu_shot_covered": true
    },
    "tax_documents": {
      "form_1095_available": true,
      "form_1095_url": "https://taller-wox.fitlabs.dev/docs/1095-2024.pdf",
      "instructions": "Tu formulario 1095 está disponible en el portal del afiliado bajo Documentos > Impuestos. Si no lo puedes acceder, llama al 1-800-FIT-CARE."
    },
    "overdue_procedures": [
      {
        "procedure": "Annual Physical Exam",
        "last_date": "2022-06-15",
        "recommended_frequency_months": 12,
        "due_since_months": 22,
        "priority": "high"
      },
      {
        "procedure": "Dental Cleaning",
        "last_date": "2023-01-10",
        "recommended_frequency_months": 6,
        "due_since_months": 15,
        "priority": "medium"
      },
      {
        "procedure": "Vision Exam",
        "last_date": "2021-08-22",
        "recommended_frequency_months": 24,
        "due_since_months": 32,
        "priority": "medium"
      },
      {
        "procedure": "Blood Test - Cholesterol Panel",
        "last_date": "2023-03-04",
        "recommended_frequency_months": 12,
        "due_since_months": 13,
        "priority": "low"
      }
    ]
  }
}

5.4 Endpoint 4 — GET /api/schedule

Para qué sirve: el agente lo invoca cuando el usuario dice "agéndame una cita". No agenda nada real — devuelve una guía/instrucciones que el agente lee y reformula al usuario.

Sin parámetros.

Response (200): SIEMPRE el mismo string. Guardar el texto en app/data/schedule_response.txt.

{
  "result": "Para agendar una cita médica, sigue estos pasos: 1) Confirma con el afiliado el día y hora preferidos, y el tipo de procedimiento. 2) Verifica que el procedimiento esté cubierto por su plan (Gold PPO en este caso). 3) Llama al sistema de agendamiento de FIT Care al 1-800-FIT-CARE o entra al portal en https://taller-wox.fitlabs.dev/agenda. 4) Indica el procedimiento, el proveedor preferido (City Hospital, Green Valley Clinic, Sunrise Health o Regional Medical Center) y la fecha. 5) Confirma la cita y registra el número de confirmación. 6) Envía recordatorios automáticos al afiliado 24h y 1h antes. Recuerda: si el procedimiento requiere autorización previa, gestiónala antes de confirmar la cita."
}

5.5 Endpoint 5 — POST /api/reports/generate-report

Para qué sirve: el agente AskReporting lo llama cuando el usuario dice "create a care report". Genera un HTML compuesto, lo guarda en disco y devuelve la URL pública.

Request body:

{
  "layout_config": "[\"care_report\", {\"element_type\": \"header\", \"parameters\": {\"title\": \"End of Report\"}}]"
}

layout_config es un string JSON-encoded que contiene una lista de elementos. Decodificar con json.loads().

Cada elemento puede ser:

(a) Un string preset:

  • "care_report" — equivale a este layout completo:
    [
      {"element_type": "header", "parameters": {"title": "Care Report"}},
      {"element_type": "overview", "parameters": {
        "prompt": "Summarize this email exchange in 3 bullet points: ",
        "text_file": "./data/combined_email.txt",
        "title": "Customer Overview"
      }},
      {"element_type": "claim_review_chart", "parameters": {}},
      {"element_type": "table", "parameters": {
        "csv_file": "./data/aetna_claim_review_summary.csv",
        "title": "Claim Review Summary"
      }}
    ]
    

(b) Un objeto con element_type y parameters:

element_type Parámetros Hace
header title (str) Renderiza <h2>{title}</h2> centrado
overview prompt, text_file, title Lee el .txt, opcionalmente le aplica un prompt simple (puedes hardcodear un resumen pre-armado por archivo para evitar dependencia de LLM), renderiza como markdown convertido a HTML
claim_review_chart (ninguno) Renderiza un gráfico de barras Plotly con datos sintéticos de CPT codes vs charged/allowed/patient_responsibility
table csv_file, title Lee el CSV y lo renderiza como tabla HTML con styling Plotly

Archivos en ./app/data/ que tienes que crear:

  • combined_email.txt (~10 líneas): email mezclado proveedor + paciente. Ej:

    From: dr.martinez@cityhospital.com
    Subject: Follow-up after appointment
    
    Hi Charlie,
    
    Following our visit last week, I'm recommending a follow-up CT scan
    to confirm the diagnosis. Please schedule within the next 2 weeks.
    
    ---
    
    From: charlie.smith@gmail.com
    Subject: Re: Follow-up
    
    Thanks Dr. Martinez. Will the CT scan be covered by my Gold PPO plan?
    Also, can I get a copy of the lab results from last visit?
    
  • provider_email.txt (~8 líneas): email solo del proveedor con jerga médica.

    Patient presented with bilateral lower quadrant tenderness, WBC 14k,
    CRP elevated. Differential includes appendicitis vs diverticulitis.
    Recommending CT abdomen/pelvis with contrast STAT.
    CPT 74177 ordered. Pre-auth obtained: AUTH-2024-8821.
    
  • aetna_email.txt (~8 líneas): email de aseguradora con EOB.

    Claim #AET-2024-9912 has been processed.
    Service date: 2024-04-15
    Provider: City Hospital
    Total billed: $1,847.50
    Plan allowance: $1,200.00
    Plan paid: $960.00
    Patient responsibility (20% coinsurance after deductible): $240.00
    
  • aetna_claim_review_summary.csv (~10 filas):

    Date,CPT_Code,Description,Charged_Amount,Allowed_Amount,Plan_Paid,Patient_Responsibility
    2024-04-15,74177,CT Abdomen/Pelvis w/contrast,1847.50,1200.00,960.00,240.00
    2024-03-22,99213,Office visit established patient,185.00,120.00,96.00,24.00
    2024-02-10,80061,Lipid panel,82.00,55.00,44.00,11.00
    ...
    

Lógica del handler:

import json, uuid
from pathlib import Path

OUTPUT_DIR = Path("app/data/reports_output")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

@app.post("/api/reports/generate-report")
def generate_report(payload: dict):
    layout = json.loads(payload["layout_config"])

    # expandir presets
    expanded = []
    for item in layout:
        if item == "care_report":
            expanded.extend(CARE_REPORT_PRESET)  # constante definida arriba
        else:
            expanded.append(item)

    # renderizar cada elemento
    html_parts = []
    for el in expanded:
        kind = el["element_type"]
        params = el.get("parameters", {})
        if kind == "header":
            html_parts.append(f"<h2 style='text-align:center;'>{params['title']}</h2>")
        elif kind == "overview":
            html_parts.append(render_overview(params))
        elif kind == "claim_review_chart":
            html_parts.append(render_claim_chart())
        elif kind == "table":
            html_parts.append(render_table(params))

    # combinar en un HTML completo (usar templates/report.html como wrapper)
    report_id = uuid.uuid4().hex[:12]
    output_path = OUTPUT_DIR / f"{report_id}.html"
    output_path.write_text(wrap_in_template("\n".join(html_parts)))

    public_url = f"https://taller-wox.fitlabs.dev/api/reports/output/{report_id}.html"
    return {"public_url": public_url}

# servir los HTMLs generados como estáticos
app.mount("/api/reports/output", StaticFiles(directory="app/data/reports_output"), name="reports")

Response (200):

{ "public_url": "https://taller-wox.fitlabs.dev/api/reports/output/abc123def456.html" }

Response (500) si algo falla:

{ "detail": "Error generating report: {mensaje}" }

Template templates/report.html (wrapper del reporte generado):

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Care Report</title>
  <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
  <style>
    body { font-family: Inter, sans-serif; max-width: 960px; margin: 40px auto; padding: 20px; color: #1A1A1A; }
    h2 { color: #0A1F44; border-bottom: 3px solid #FF7A00; padding-bottom: 8px; }
    table { width: 100%; border-collapse: collapse; margin: 16px 0; }
    th { background: #0A1F44; color: white; padding: 10px; }
    td { padding: 8px; border-bottom: 1px solid #D8DEE5; }
  </style>
</head>
<body>
  <header>
    <p style="color:#00B5D8; font-weight:700;">FACTORIT · FIT</p>
  </header>
  {{ content | safe }}
  <footer style="margin-top:40px; color:#5A6473; font-size:13px;">
    Generado por AskReporting · taller-wox.fitlabs.dev
  </footer>
</body>
</html>

6. Configuración por variables de entorno

# .env.example
SECRET_KEY=cambiar-esto-en-prod-string-largo-aleatorio
ADMIN_USER=admin
ADMIN_PASS=cambiar-esto
TOKEN_EXPIRY_HOURS=24
BASE_URL=https://taller-wox.fitlabs.dev
# Opcional, para mandar email de confirmación al registrarse
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
SMTP_FROM=bootcamp@fitlabs.dev

7. Deployment

7.1 Dockerfile

FROM python:3.11-slim
WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app/ ./app/
COPY static/ ./static/
COPY material/ ./material/

# crear directorios runtime
RUN mkdir -p app/data/reports_output

EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

7.2 DNS

Apuntar taller-wox.fitlabs.dev (registro A o CNAME) al hosting elegido (Railway/Render/etc.).

7.3 HTTPS

Obligatorio. Usar el SSL automático del proveedor o Let's Encrypt si es VPS propio. watsonx Orchestrate rechaza llamar HTTPS con certificados inválidos.

7.4 CORS

Los endpoints /api/* deben permitir requests desde https://*.ibm.com (donde corre watsonx Orchestrate). Configurar:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # o más restrictivo: ["https://us-south.dl.watson-orchestrate.cloud.ibm.com"]
    allow_credentials=True,
    allow_methods=["GET", "POST"],
    allow_headers=["*"],
)

8. Testing manual antes de publicar

Una vez todo desplegado, verificar:

# Frontend
curl https://taller-wox.fitlabs.dev/                  # debe devolver HTML del landing
curl -X POST https://taller-wox.fitlabs.dev/register \
  -d "nombre=Test&email=test@test.com&empresa=ACME&consentimiento=on"
# debería devolver {"token":"...","redirect_url":"/descargas?token=..."}

# Descargas (con token válido)
curl -I "https://taller-wox.fitlabs.dev/download/Manual_Alumno_Bootcamp_FIT.docx?token=XXX"
# debe devolver 200 con Content-Type docx

# Endpoints del taller
curl -X POST https://taller-wox.fitlabs.dev/api/historical-procedures \
  -H "Content-Type: application/json" \
  -d '{"filters":"[]","group_by":"[]"}'
# debe devolver {"result":[...]}

curl https://taller-wox.fitlabs.dev/api/member-insights
# debe devolver el objeto con member, plan, overdue_procedures

curl https://taller-wox.fitlabs.dev/api/schedule
# debe devolver el texto de scheduling

curl -X POST https://taller-wox.fitlabs.dev/api/reports/generate-report \
  -H "Content-Type: application/json" \
  -d '{"layout_config":"[\"care_report\"]"}'
# debe devolver {"public_url":"..."}

Test end-to-end del taller:

  1. Crear cuenta IBM Cloud trial.
  2. Importar openapi-tools-spec.json en un agente nuevo en watsonx Orchestrate.
  3. Hacer prompt: "¿Estoy atrasado en algún chequeo?" — debe llamar /api/member-insights y devolver los 4 procedimientos overdue.
  4. Hacer prompt: "¿Cuánto cuesta una resonancia magnética?" — debe llamar /api/available-procedures con filtro procedure contains MRI.

9. Datos sintéticos — orientación

Los datasets CSV los inventas pero tienen que ser coherentes entre sí:

  • En historical_procedures.csv, el afiliado principal (member_name = Charlie Smith) debería tener filas de Annual Physical Exam, Dental Cleaning, Vision Exam y Blood Test con date consistente con los last_date de member_insights.json (los procedimientos overdue).
  • En available_procedures.csv, los procedure deben incluir al menos los procedimientos preguntables: X Ray, MRI, CT Scan, Annual Physical Exam, Appendectomy, Dental Cleaning, Vision Exam.
  • Los costos en ambos datasets pueden diferir (uno es histórico, otro es catálogo de oferta).

Importante: los costos deben verse realistas. Una radiografía cuesta ~$80-300, una resonancia ~$1500-3000, una apendicectomía ~$10000-30000.


10. Logos y branding

Logos a conseguir (Felipe los provee al desarrollador):

  1. static/img/logo-factorit.svg — logo oficial FactorIT (Felipe ya lo tiene)
  2. static/img/logo-watsonx.svg — logo "IBM watsonx Orchestrate" oficial (bajar de https://www.ibm.com/brand/ o usar texto estilizado mientras tanto)

Favicon:

  • static/img/favicon.ico y favicon-32x32.png — versión simplificada del logo FIT

Meta tags para social sharing (OpenGraph):

<meta property="og:title" content="Bootcamp Agentic AI — watsonx Orchestrate | FactorIT">
<meta property="og:description" content="Construye tu primer agente de IA en 4 horas con IBM watsonx Orchestrate. Material completo del bootcamp para descarga.">
<meta property="og:image" content="https://taller-wox.fitlabs.dev/static/img/og-image.png">
<meta property="og:url" content="https://taller-wox.fitlabs.dev/">

11. Checklist de entrega para Claude Code

Cuando Claude Code termine, debe poder confirmar:

  • El comando uvicorn app.main:app --reload levanta el server local en http://localhost:8000
  • GET / muestra el landing brandeado FIT con formulario
  • El formulario valida los 3 campos + checkbox y guarda en leads.db
  • Tras submit redirige a /descargas?token=...
  • La página de descargas muestra las 8-9 cards de archivos
  • Cada botón "Descargar" baja el archivo correcto desde material/
  • Los 5 endpoints responden correctamente con curl (ver sección 8)
  • GET /admin/leads requiere basic auth y lista los registros
  • El Dockerfile builda sin errores: docker build -t taller-wox .
  • El container corre: docker run -p 8000:8000 -v $(pwd)/leads.db:/app/leads.db taller-wox

12. Lo que NO está en el alcance (para evitar scope creep)

  • Editor visual de leads — solo lectura JSON/CSV.
  • Recuperación de password — no hay password, solo email para descarga.
  • Email transaccional obligatorio — opcional (SMTP en env).
  • Analytics avanzados — solo conteos simples en /admin/stats.
  • Multi-idioma — solo español.
  • Re-captcha — basta con honeypot field invisible en el formulario.

13. Resumen ejecutivo para arrancar

Lo primero que Claude Code debe hacer:

  1. Crear la estructura de carpetas (sección 2).
  2. Implementar app/main.py con FastAPI básico, mount de static y templates Jinja2.
  3. Implementar registro + descarga (sección 4) — eso es el corazón del producto.
  4. Implementar los 5 endpoints (sección 5) — con datasets sintéticos pequeños primero.
  5. Implementar el landing HTML con branding (sección 3).
  6. Implementar la página de descargas (sección 3.4).
  7. Dockerizar (sección 7.1).
  8. Smoke tests (sección 8).
  9. Deploy a un proveedor (a definir por Felipe).
  10. Apuntar el DNS taller-wox.fitlabs.dev al deployment.

Tiempo estimado total para un desarrollador senior: 1-1.5 días de trabajo enfocado.


Fin del SPEC. Cualquier ambigüedad → preguntar a Felipe antes de implementar.