# Bot de catálogo con Python: kit de práctica

Este kit ya contiene el programa: el curso enseña a **entenderlo, adaptar sus datos y escribir pruebas**, no a crear un agente autónomo desde cero. Responde a comandos como `stock A-100` y `ficha A-100` usando un archivo JSON; sin el módulo opcional de modelo, no interpreta preguntas generales en lenguaje natural.

Un caso de práctica es una tienda ficticia que quiere responder consultas de precio y disponibilidad. Aprendes a separar **datos → función de consulta → canal de mensajes**. El catálogo de ejemplo no está conectado a una tienda ni se actualiza en tiempo real. No hay compras, reservas, pagos, escritura de inventario ni memoria de negocio.

La práctica local no requiere cuentas ni claves. Telegram y WhatsApp/Kapso son ampliaciones opcionales con adaptadores implementados y pruebas offline; **no se ha verificado una entrega real con estos adaptadores**. El modelo opcional también requiere una cuenta propia y no es necesario para ejecutar comandos exactos.

## Requisitos y prueba local

- Python **3.10 o superior**. El entorno usado para validar este kit fue Python 3.11.5.
- Solo biblioteca estándar: no hay que instalar dependencias ni usar `requirements.txt` para el flujo base.
- No se requieren cuentas, claves ni red para el núcleo y las pruebas.

Desde esta carpeta:

```bash
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -v
python3 -c 'from catalogo_core import respond; print(respond("stock A-100")["text"])'
```

La primera orden debe terminar en `OK`; la segunda imprime una respuesta local como `A-100: hay 23 unidades disponibles.`. El número exacto de tests puede cambiar. `catalogo.json` usa datos sintéticos versionados y dinero entero en centavos: no representa precios vigentes.

El núcleo acepta `ayuda`, `listar`, `buscar <término>`, `ficha <SKU>` y `stock <SKU>`. Devuelve `text`, `status`, `items` y `trace`. `stock C-300` es `out_of_stock`; `stock X-999` es `not_found`: un SKU desconocido nunca se convierte en stock cero.

## Archivos

- `catalogo_core.py`: contrato determinista y de solo lectura.
- `catalogo.json`: fixture sintético compartido por el kit.
- `modelo.py`: integración opcional con OpenAI Responses; no se usa sin configuración explícita.
- `telegram.py`: polling de desarrollo con allowlist de un chat privado, offset SQLite e intenciones de salida.
- `kapso.py`: receptor Kapso v2 con HMAC raw-body, persistencia SQLite, deduplicación e intenciones de salida.
- `cases.json` y `test_*.py`: casos y pruebas offline.
- `fixtures/`: payloads Kapso locales, incluido un batch y una plantilla de registro sin credenciales.
- `.env.example`: nombres de variables vacíos; no es un archivo de secretos.

Los datos operativos se guardan en SQLite local sin cifrar. Los valores por defecto viven bajo `~/.local/share/agente-catalogo/`, con directorio 700 y archivos 600. Evalúa privacidad, backups y control de acceso antes de usar este almacenamiento fuera de un entorno de aprendizaje. Las salidas rutinarias evitan imprimir cuerpos completos, pero SQLite conserva textos e identificadores necesarios para procesar mensajes; `inspect` puede mostrar respuestas preparadas. Trata la base, sus copias y la salida de inspección como datos sensibles, y no los publiques. Esta persistencia no es una memoria conversacional ni una política automática de borrado.

## Telegram: polling de desarrollo

1. En la conversación oficial **@BotFather**, usa `/newbot`, elige nombre y username y guarda el token en un gestor seguro.
2. Envía `/start` al bot desde el único chat privado que autorizarás.
3. Obtén el `chat_id` consultando el Bot API `getUpdates` sin imprimir el payload completo. El token no debe ir en una URL escrita en el shell, Git, logs o capturas; pásalo desde el entorno a un cliente HTTP.
4. Configura las variables sin valores reales en este README:

```bash
export TELEGRAM_BOT_TOKEN='cárgalo-desde-tu-gestor-seguro'
export TELEGRAM_ALLOWED_CHAT_ID='tu_chat_id'
export TELEGRAM_ALLOW_SEND=0
```

Lee updates y genera una vista previa sin enviar:

```bash
python3 telegram.py poll --once
python3 telegram.py inspect
```

Para autorizar conscientemente un envío, revisa el chat y la respuesta y exige ambas señales:

```bash
export TELEGRAM_ALLOW_SEND=1
python3 telegram.py poll --once --send
```

`--llm` es una decisión adicional y puede usar red/coste; no es necesario. El polling normal puede mantenerse abierto con `python3 telegram.py poll`, pero es una operación de desarrollo, no un webhook de producción ni una garantía exactly-once. Un timeout o resultado ambiguo queda `unknown` y no se reenvía a ciegas:

```bash
python3 telegram.py resolve <update_id> --status rejected
python3 telegram.py resolve <update_id> --status sent
```

Usa `sent` solo con confirmación externa. Estos comandos de resolución no hacen POST.

## WhatsApp con Kapso

El endpoint local solo autentica, filtra y persiste; responde `200 OK` después de guardar el evento. No procesa el modelo ni envía dentro del request. Configuración mínima:

```bash
export KAPSO_PHONE_NUMBER_ID='tu_phone_number_id'
export KAPSO_WEBHOOK_SECRET='secreto_largo_generado_por-ti'
export KAPSO_DB="$HOME/.local/share/agente-catalogo/kapso.sqlite3"
python3 kapso.py serve --host 127.0.0.1 --port 8080
```

Kapso requiere un destino HTTPS público para webhooks reales; este servidor solo escucha loopback y no termina TLS. Define `PUBLIC_WEBHOOK_URL` con el destino exacto que exponga el path `/webhook/kapso`, por ejemplo `https://tu-dominio.example/webhook/kapso`; no uses una URL HTTP ni otro path. La firma es HMAC-SHA256 hexadecimal desnuda en `X-Webhook-Signature`, calculada sobre los bytes exactos del body. No uses el prefijo `sha256=` de la firma Meta. Se valida `X-Webhook-Event: whatsapp.message.received`, payload v2 simple o batch, dirección entrante, número configurado y remitentes opcionalmente permitidos.

### Registrar un webhook sin exponer secretos

Define `PUBLIC_WEBHOOK_URL` como tu destino HTTPS público exacto, por ejemplo `https://tu-dominio.example/webhook/kapso`:

```bash
export PUBLIC_WEBHOOK_URL='https://tu-dominio.example/webhook/kapso'
```

Revisa primero `fixtures/kapso-webhook-registration.json`. Genera un archivo temporal aleatorio, modo 600, y bórralo siempre. El siguiente flujo evita una ruta predecible, no pone la API key en la URL y no deja el secreto al terminar:

```bash
registration_path="$(python3 - <<'PY'
import os, tempfile
fd, path = tempfile.mkstemp(prefix="kapso-registration-", suffix=".json")
os.chmod(path, 0o600)
os.close(fd)
print(path)
PY
)"
trap 'rm -f -- "$registration_path"' EXIT INT TERM
python3 - "$registration_path" <<'PY'
import json, os, sys
with open("fixtures/kapso-webhook-registration.json", encoding="utf-8") as source_file:
    payload = json.load(source_file)
payload["whatsapp_webhook"]["url"] = os.environ["PUBLIC_WEBHOOK_URL"]
payload["whatsapp_webhook"]["secret_key"] = os.environ["KAPSO_WEBHOOK_SECRET"]
with open(sys.argv[1], "w", encoding="utf-8") as output_file:
    json.dump(payload, output_file, ensure_ascii=False)
PY
curl --fail-with-body -X POST \
  "https://api.kapso.ai/platform/v1/whatsapp/phone_numbers/${KAPSO_PHONE_NUMBER_ID}/webhooks" \
  -H "X-API-Key: ${KAPSO_API_KEY}" \
  -H 'Content-Type: application/json' \
  --data-binary "@${registration_path}"
```

`KAPSO_API_KEY`, `PUBLIC_WEBHOOK_URL` y `KAPSO_WEBHOOK_SECRET` deben venir del entorno o gestor de secretos. El `trap` limpia el archivo al salir, incluso si `curl` falla; revisa también los permisos del directorio y los logs de tu sistema.

### Procesar la cola

En una segunda sesión inspecciona y procesa **un solo evento por invocación**:

```bash
python3 kapso.py inspect
python3 kapso.py process
```

Sin `--send`, `process` genera una vista previa local y libera el evento para que puedas revisarlo. Para un POST real se necesitan dos señales:

```bash
export KAPSO_ALLOW_SEND=1
export KAPSO_API_KEY='cárgala-desde-tu-gestor-seguro'
python3 kapso.py process --send
```

El programa no incluye un worker continuo. Si necesitas varios eventos, ejecuta una invocación separada por evento, con pausa, y observa cada resultado. `process` reclama como máximo un evento por invocación: sin `--send` libera el evento para volver a previsualizarlo, no drena la cola. Tras revisar cada vista previa y autorizar conscientemente el envío, repite de forma explícita `KAPSO_ALLOW_SEND=1 python3 kapso.py process --send` con la API key ya cargada; no uses un bucle desatendido ni lo presentes como garantía de procesamiento continuo, disponibilidad, orden o exactly-once. Ante `unknown`, inspecciona y reconcilia antes de actuar:

```bash
python3 kapso.py inspect
python3 kapso.py resolve <WAMID> --status failed
python3 kapso.py resolve <WAMID> --status sent --provider-id <ID-confirmado>
```

`resolve` nunca hace POST y solo debe marcar `sent` con evidencia externa. El kit no reintenta automáticamente un envío cuyo resultado es ambiguo.

## Modelo opcional

Sin `OPENAI_MODEL` y `OPENAI_API_KEY`, `modelo.respond_with_model` vuelve al núcleo local sin red. Con ambas variables, puede llamar al endpoint de Responses, con límites de herramienta y salida. Revisa privacidad, proveedor y condiciones vigentes antes de habilitarlo:

```bash
export OPENAI_MODEL='modelo-que-hayas-verificado'
export OPENAI_API_KEY='cárgala-desde-tu-gestor-seguro'
python3 telegram.py poll --once --llm
python3 kapso.py process --llm
```

El modelo solo selecciona la herramienta `consultar_catalogo`; el núcleo renderiza hechos. No se incluyen tarifas, latencias ni resultados de integración como evidencia.

## Fuentes y límites

La implementación se basa en documentación oficial de Telegram y Kapso. Consulta las versiones vigentes antes de una conexión real:

- [Telegram: BotFather](https://core.telegram.org/bots#6-botfather)
- [Telegram Bot API: `getUpdates`](https://core.telegram.org/bots/api#getupdates)
- [Telegram Bot API: `sendMessage`](https://core.telegram.org/bots/api#sendmessage)
- [Kapso: API y autenticación](https://docs.kapso.ai/api/introduction.md)
- [Kapso: crear webhook](https://docs.kapso.ai/api/platform/v1/webhooks/create-webhook.md)
- [Kapso: webhooks v2 y ACK](https://docs.kapso.ai/docs/platform/webhooks/overview.md)
- [Kapso: firma raw-body](https://docs.kapso.ai/docs/platform/webhooks/security.md)
- [Kapso: reintentos y entrega](https://docs.kapso.ai/docs/platform/webhooks/advanced.md)
- [Kapso: eventos de mensajes](https://docs.kapso.ai/docs/platform/webhooks/message-events.md)

No se afirma retención/TTL de `X-Idempotency-Key`, SLA, límites de concurrencia, SDK oficial Python, idempotencia del POST de salida, exactly-once, costes vigentes ni disponibilidad de cuentas. Es un ejemplo single-operator con una base SQLite y datos ficticios; no es una plataforma multi-tenant.
