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>
This commit is contained in:
848
SPEC_taller_wox_fitlabs.md
Normal file
848
SPEC_taller_wox_fitlabs.md
Normal file
@@ -0,0 +1,848 @@
|
||||
# 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:
|
||||
```html
|
||||
<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
|
||||
|
||||
```python
|
||||
# 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.
|
||||
|
||||
```python
|
||||
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`):**
|
||||
```sql
|
||||
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):
|
||||
```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:**
|
||||
```python
|
||||
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):**
|
||||
```json
|
||||
{ "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.
|
||||
|
||||
```json
|
||||
{
|
||||
"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`.
|
||||
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```python
|
||||
[
|
||||
{"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):
|
||||
```csv
|
||||
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:**
|
||||
```python
|
||||
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):**
|
||||
```json
|
||||
{ "public_url": "https://taller-wox.fitlabs.dev/api/reports/output/abc123def456.html" }
|
||||
```
|
||||
|
||||
**Response (500) si algo falla:**
|
||||
```json
|
||||
{ "detail": "Error generating report: {mensaje}" }
|
||||
```
|
||||
|
||||
**Template `templates/report.html`** (wrapper del reporte generado):
|
||||
```html
|
||||
<!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
|
||||
|
||||
```bash
|
||||
# .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
|
||||
|
||||
```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:
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```bash
|
||||
# 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):**
|
||||
```html
|
||||
<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.**
|
||||
Reference in New Issue
Block a user