feat: F5 — frases libres mapeadas a comandos sin LLM
Antes: texto libre de tenants activos → siempre "no entendí eso". Ahora: normalización (lowercase, sin acentos, sin puntuación) + lookup en diccionario de frases y regex para exp/pdf con número. "novedades", "hay algo nuevo?" → /notif "dame el estado" → /estado "necesito ayuda" → /ayuda "expediente 198/2026", "exp 198" → /exp <arg> "pdf 198/2026" → /pdf <arg> Texto irrelevante → respuesta "no entendí" sin cambios. Onboarding en curso: sin interferencia. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ Responsabilidades:
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING
|
||||
from zoneinfo import ZoneInfo
|
||||
@@ -47,6 +48,41 @@ logger = structlog.get_logger(__name__)
|
||||
|
||||
PJ_URL = "https://apps.csj.gov.py/gestion-partes/notificaciones-electronicas"
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Mapeo de frases libres a comandos (sin LLM)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
_FRASES_COMANDOS: dict[str, str] = {
|
||||
"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",
|
||||
"como estas": "estado",
|
||||
"cómo estás": "estado",
|
||||
"ayuda": "ayuda",
|
||||
"help": "ayuda",
|
||||
"que podes hacer": "ayuda",
|
||||
"qué podés hacer": "ayuda",
|
||||
"comandos": "ayuda",
|
||||
}
|
||||
|
||||
|
||||
def _normalizar(texto: str) -> str:
|
||||
"""Lowercase, sin acentos, sin puntuación — para comparación de frases."""
|
||||
texto = texto.lower().strip()
|
||||
texto = unicodedata.normalize("NFD", texto)
|
||||
texto = "".join(c for c in texto if unicodedata.category(c) != "Mn")
|
||||
texto = re.sub(r"[^\w\s]", " ", texto)
|
||||
return re.sub(r"\s+", " ", texto).strip()
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Helpers de formato
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -626,6 +662,45 @@ class PedritoBot:
|
||||
if self._onboarding.is_in_onboarding(chat_id):
|
||||
await self._onboarding.handle_mensaje(update, context)
|
||||
|
||||
async def _intentar_mapear_comando(
|
||||
self,
|
||||
update: Update,
|
||||
context: ContextTypes.DEFAULT_TYPE,
|
||||
texto: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Intenta mapear texto libre a un comando conocido sin LLM.
|
||||
Retorna True si encontró match y ejecutó el handler correspondiente.
|
||||
"""
|
||||
texto_norm = _normalizar(texto)
|
||||
texto_lower = texto.lower()
|
||||
|
||||
# Regex especiales sobre texto original (preserva la "/")
|
||||
m_exp = re.search(r"\bexp(?:ediente)?\b\s+(\d+(?:/\d+)?)", texto_lower)
|
||||
if m_exp:
|
||||
context.args = [m_exp.group(1)] # type: ignore[assignment]
|
||||
await self.cmd_exp(update, context)
|
||||
return True
|
||||
|
||||
m_pdf = re.search(r"\bpdf\b\s+(\d+/\d+)", texto_lower)
|
||||
if m_pdf:
|
||||
context.args = [m_pdf.group(1)] # type: ignore[assignment]
|
||||
await self.cmd_pdf(update, context)
|
||||
return True
|
||||
|
||||
# Matching por frases del diccionario
|
||||
for frase, comando in _FRASES_COMANDOS.items():
|
||||
if _normalizar(frase) in texto_norm:
|
||||
if comando == "notif":
|
||||
await self.cmd_notif(update, context)
|
||||
elif comando == "estado":
|
||||
await self.cmd_estado(update, context)
|
||||
elif comando == "ayuda":
|
||||
await self.cmd_ayuda(update, context)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def handle_texto_libre(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Responde a texto que no es comando ni está en un flujo de onboarding."""
|
||||
chat_id = update.effective_chat.id # type: ignore[union-attr]
|
||||
@@ -635,11 +710,16 @@ class PedritoBot:
|
||||
return # el onboarding lo maneja en handle_mensaje, no responder acá
|
||||
|
||||
tenant = await get_tenant(chat_id)
|
||||
if tenant and tenant.get("estado") == "activo":
|
||||
if not (tenant and tenant.get("estado") == "activo"):
|
||||
# Usuarios sin registro: silencio total.
|
||||
return
|
||||
|
||||
texto_original = (update.effective_message.text or "") # type: ignore[union-attr]
|
||||
if await self._intentar_mapear_comando(update, context, texto_original):
|
||||
return
|
||||
|
||||
texto = _escape_md(_texto_tono("texto_no_reconocido", tenant))
|
||||
await update.effective_message.reply_text(texto, parse_mode=ParseMode.MARKDOWN_V2) # type: ignore[union-attr]
|
||||
# Usuarios sin registro: silencio total.
|
||||
# No confirmar que el bot existe ni qué hace — quien lo debe usar ya sabe cómo.
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Handlers de callbacks
|
||||
|
||||
Reference in New Issue
Block a user