9.4 KiB
Prompt para Claude Code — Sprint 4: Features 3, 5 y 1
Lee CLAUDE.md antes de empezar. Las invariantes de seguridad aplican.
Implementar las tres features en orden: primero F5 (más simple), luego F3, luego F1.
Feature 5 — Frases libres → comandos (sin LLM)
Contexto
El bot hoy responde a texto libre con "no entendí eso, mirá /ayuda". Hay frases obvias que debería mapear a comandos existentes sin necesidad de LLM.
Implementación
En telegram_bot.py, modificar handle_texto_libre() para que antes de
responder "no entendí", intente mapear el texto a un comando conocido.
FRASES_COMANDOS = {
# Notificaciones
"novedades": "notif",
"actualizaciones": "notif",
"que hay nuevo": "notif",
"qué hay nuevo": "notif",
"hay algo nuevo": "notif",
"alguna novedad": "notif",
"notificaciones": "notif",
"notificacion": "notif",
"notificación": "notif",
"revisar": "notif",
"actualizar": "notif",
# Estado
"estado": "estado",
"como estas": "estado",
"cómo estás": "estado",
# Ayuda
"ayuda": "ayuda",
"help": "ayuda",
"que podes hacer": "ayuda",
"qué podés hacer": "ayuda",
"comandos": "ayuda",
}
Lógica de matching: normalizar el texto del usuario (lowercase, sin acentos, sin signos de puntuación) y buscar si alguna frase del diccionario está contenida en el texto normalizado.
Si hay match → ejecutar el handler correspondiente directamente. Si no hay match → respuesta actual ("no entendí eso...").
Casos especiales a detectar por regex:
- "expediente 198/2026" o "exp 198" → ejecutar cmd_exp con ese argumento
- "pdf 198/2026" → ejecutar cmd_pdf con ese argumento
Verificación
- "novedades" ejecuta lo mismo que /notif
- "qué hay nuevo?" ejecuta lo mismo que /notif
- "expediente 198/2026" ejecuta lo mismo que /exp 198/2026
- Texto completamente irrelevante ("hola qué tal") sigue respondiendo con el mensaje de ayuda
- No interfiere con el flujo de onboarding
Feature 3 — Extracto de PDF sin LLM
Contexto
Cuando llega una notificación con documento adjunto, el abogado quiere saber
qué dice sin tener que descargar y abrir el PDF. La solución es extraer el
texto del PDF con pypdf y mostrar las primeras líneas como extracto.
Sin LLM. Sin interpretación. Texto crudo del documento con disclaimer.
Nueva dependencia
Agregar a pyproject.toml:
"pypdf>=4.0",
Nuevo módulo pdf_extractor.py
"""
pdf_extractor.py — Extracción de texto de PDFs judiciales.
Sin LLM. Sin interpretación. Solo extracción de texto crudo.
El abogado lee el extracto y decide. Pedrito no interpreta.
Los PDFs nunca se guardan en disco — se procesan en memoria.
"""
from pypdf import PdfReader
import io
MAX_CHARS = 600 # máximo de caracteres a mostrar
def extraer_extracto(pdf_bytes: bytes) -> str | None:
"""
Extrae las primeras líneas de texto de un PDF judicial.
Retorna el extracto como string, o None si el PDF no tiene texto
extraíble (PDF escaneado, imagen, etc.)
Los PDFs del PJ son nativos (no escaneados), así que pypdf funciona.
"""
try:
reader = PdfReader(io.BytesIO(pdf_bytes))
texto = ""
for page in reader.pages:
texto += page.extract_text() or ""
if len(texto) >= MAX_CHARS:
break
texto = texto.strip()
if not texto:
return None
# Limpiar saltos de línea múltiples
import re
texto = re.sub(r'\n{3,}', '\n\n', texto)
if len(texto) > MAX_CHARS:
texto = texto[:MAX_CHARS] + "..."
return texto
except Exception:
return None
Integración en el flujo de notificaciones
En poller.py, método revisar_ahora(), cuando se detecta una notificación
nueva y se va a enviar al abogado:
- Intentar descargar el PDF (ya existe
descargar_documento_principal()) - Si descarga OK → extraer extracto con
extraer_extracto() - Enviar notificación con extracto incluido
- Si descarga falla o PDF sin texto → enviar notificación sin extracto (degradación elegante)
Formato del mensaje con extracto
📋 Nueva notificación judicial
{caratula}
Exp. {numero}/{anio} — {juzgado}
Tipo: {tipo}
Fecha: {fecha}
📄 Extracto del documento:
{extracto}
⚠️ Este es el texto crudo del documento. Verificá
el original antes de actuar.
[Ver PDF] [Ir al sistema del PJ]
Si no hay extracto disponible, el mensaje es igual pero sin la sección "📄 Extracto del documento".
En cmd_pdf() de telegram_bot.py
Cuando el abogado pide /pdf 198/2026, después de enviar el archivo también
enviar el extracto como mensaje de texto separado:
📄 Contenido del documento:
{extracto}
⚠️ Verificá el original antes de actuar.
Verificación
- Una notificación nueva llega con el extracto incluido
- Si el PDF no tiene texto extraíble, la notificación llega igual sin extracto
- El extracto no supera 600 caracteres
- El disclaimer siempre aparece cuando hay extracto
- Los bytes del PDF nunca se escriben a disco
scrub()en cualquier log que mencione el contenido del extracto- Agregar
pypdfapyproject.toml
Feature 1 — Backend de administración web (FastAPI + HTML puro)
Contexto
Panel de administración interno para gestionar tenants y ver actividad. Solo para el admin (Marcos). No es un producto — es una herramienta operativa. HTML puro servido por FastAPI. Sin framework frontend. Funcional sobre bonito.
Nueva dependencia
Agregar a pyproject.toml:
"fastapi>=0.115",
"uvicorn>=0.32",
"jinja2>=3.1",
Estructura de archivos nuevos
admin/
__init__.py
app.py ← FastAPI app del panel
auth.py ← autenticación básica (token en .env)
templates/
base.html ← layout común
tenants.html ← listado de tenants
tenant.html ← detalle de un tenant
audit.html ← audit log
Autenticación del panel
Token estático en .env:
ADMIN_TOKEN=genera-uno-con-secrets.token_hex(32)
Middleware simple: si el request no tiene el header X-Admin-Token con el
valor correcto → 403. No hace falta sesiones ni cookies para este sprint.
Agregar a Settings en config.py:
admin_token: str = ""
admin_port: int = 8080
Rutas del panel
GET / → redirect a /tenants
GET /tenants → listado de todos los tenants
GET /tenants/{id} → detalle de un tenant
POST /tenants/{id}/resetear → resetea el tenant (borra DB + snapshot)
GET /audit → audit log, últimas 200 entradas, filtrable por chat_id
GET /audit?chat_id=X → audit log filtrado por tenant
Página /tenants — tabla con columnas
| chat_id | nombre | estado | tono | timezone | cédula (primeros 3 dígitos + ***) | último chequeo | acciones |
|---|
La cédula se muestra parcialmente — nunca en texto plano.
"Acciones" tiene un botón "Resetear" que hace POST a /tenants/{id}/resetear.
Página /tenants/{id} — detalle
- Todos los campos del tenant
- Últimas 20 entradas del audit_log para ese chat_id
- Botón resetear
Página /audit — tabla con columnas
| timestamp (hora local) | chat_id | nombre tenant | evento | resultado | detalle (JSON colapsado) |
Filtro por chat_id con un input de texto simple. Paginación básica: 200 registros por página, botón "más antiguos".
Arrancar el panel junto con el bot
En main.py, arrancar el servidor de administración en un thread separado:
import uvicorn
import threading
def run_admin():
uvicorn.run("admin.app:app", host="127.0.0.1", port=settings.admin_port, log_level="warning")
admin_thread = threading.Thread(target=run_admin, daemon=True)
admin_thread.start()
El panel corre en 127.0.0.1:8080 — solo accesible via SSH tunnel, no expuesto públicamente.
Para acceder desde tu Mac:
ssh -L 8080:127.0.0.1:8080 pedrito@<IP_VPS>
# Luego abrir http://localhost:8080 en el browser
Seguridad del panel
- Solo escucha en 127.0.0.1 (no en 0.0.0.0) — inaccesible desde internet
- Requiere el token en cada request
- Las cédulas nunca se muestran completas
- Los bearer tokens del PJ nunca aparecen en el panel
- El panel es de solo lectura excepto la acción "resetear"
Verificación
GET /tenantsmuestra los tenants registrados con cédula parcialPOST /tenants/{id}/resetearborra el tenant y su snapshotGET /auditmuestra las últimas 200 entradasGET /audit?chat_id=Xfiltra correctamente- Sin
ADMIN_TOKENen el request → 403 - El panel NO es accesible desde internet (solo 127.0.0.1)
- El panel arranca junto con el bot sin errores
- Agregar
fastapi,uvicorn,jinja2apyproject.toml
Orden de implementación sugerido
- Feature 5 (frases libres) — 2-3 horas, sin dependencias nuevas
- Feature 3 (extracto PDF) — 3-4 horas, una dependencia nueva
- Feature 1 (panel admin) — 4-6 horas, más compleja
Lo que NO hacer en este sprint
- No usar LLM para nada — ni resumen, ni clasificación, ni mapeo de intenciones
- No exponer el panel en un puerto público
- No guardar PDFs en disco
- No mostrar cédulas completas en el panel
- No cambiar el onboarding
- No cambiar el flujo de notificaciones existente más allá de agregar el extracto