diff --git a/cayceops/components/maild/classify.py b/cayceops/components/maild/classify.py index 3cfa658..8787cf2 100644 --- a/cayceops/components/maild/classify.py +++ b/cayceops/components/maild/classify.py @@ -19,10 +19,17 @@ from cayceops.models import MailClassification _APPROVAL_WORDS = re.compile(r"\b(approv|accept|welcome)", re.I) -# any of these tokens anywhere in the mail vetoes an `approval` result +# any of these tokens anywhere in the mail vetoes an `approval` result. +# English and Spanish (merchants write in both; mixed mails are common). _NEGATIONS = re.compile( r"\b(not|no|never|cannot|can'?t|won'?t|hasn'?t|isn'?t|" - r"no\s+longer|unable|decline\w*|regret\w*|unfortunately)\b", + r"no\s+longer|unable|decline\w*|regret\w*|unfortunately|" + # spanish: standalone negation words + r"nunca|jam[aá]s|tampoco|ning[uú]n(?:a|o)?|nadie|nada|" + # spanish: multi-word negations and rejection stems + r"no\s+podemos|no\s+es\s+posible|no\s+acept(?:amos|ar[aá])|" + r"rechaz\w*|lamentablemente|descartad\w*|" + r"no\s+ha\s+sido\s+aprobad\w*|no\s+procede)\b", re.I, ) @@ -34,11 +41,24 @@ _RULES: list[tuple[MailClassification, re.Pattern[str], float]] = [ re.compile(r"\b(declin|reject|not able to|unable to|we regret|won.t be able)\w*", re.I), # noqa: E501 0.9, ), + ( + MailClassification.REJECTION, + # spanish rejections (strong stems, not just negation words) + re.compile(r"\b(rechaz\w*|no\s+vamos\s+a\s+pod(?:er|emos)|su\s+solicitud\s+ha\s+sido\s+denegada|no\s+cumple\s+los\s+requisitos)\b", re.I), # noqa: E501 + 0.9, + ), ( MailClassification.APPROVAL, re.compile(r"\b(approv|accept|welcome to the|you.re (in|approved)|live|activated|glad to)\w*", re.I), # noqa: E501 0.85, ), + ( + MailClassification.APPROVAL, + # spanish approvals (standalone positive stems; the veto still + # downgrades if ANY negation appears elsewhere in the mail) + re.compile(r"\b(aprob\w*|aceptad\w*|bienvenid\w*|ha\s+sido\s+aceptada|encantados?\s+de\s+(trabajar|colaborar))\b", re.I), # noqa: E501 + 0.85, + ), ( MailClassification.QUESTION, re.compile(r"\b(could you|can you|please (provide|clarify|confirm)|what (is|are)|how (do|does|can)|need more (info|details))\b", re.I), # noqa: E501 diff --git a/tests/test_spanish_negations.py b/tests/test_spanish_negations.py new file mode 100644 index 0000000..efaf1a7 --- /dev/null +++ b/tests/test_spanish_negations.py @@ -0,0 +1,80 @@ +"""Spanish negation tests (round-3 point 4): merchants write in Spanish; +negations like 'nunca', 'lamentablemente' or 'no podemos' must veto +`approval` exactly like their English counterparts. Veto rule unchanged: +when in doubt -> needs_review.""" + +from __future__ import annotations + +import pytest + +from cayceops.components.maild import classify as clf +from cayceops.models import MailClassification + +APPROVAL_ONLY = MailClassification.APPROVAL + + +def _never_approval(cfg_factory, text: str) -> None: + kind, _ = clf.classify(text, cfg_factory()) + assert kind is not APPROVAL_ONLY, f"{text!r} classified as APPROVAL" + + +def test_english_baseline_still_works(cfg): + config = cfg() + kind, _ = clf.classify("Your application was approved, welcome to the program", config) + assert kind is APPROVAL_ONLY + + +@pytest.mark.parametrize( + "text", + [ + # 1. rejection stem + "Lamentamos informarle que su solicitud fue rechazada", + # 2. nunca + "Nunca aceptamos negocios de criptomonedas", + # 3. jamás + "Jamás hemos trabajado con affiliates externos", + # 4. tampoco + "No podemos aceptar su sitio, tampoco podemos ofrecerle otro acuerdo", + # 5. ningún/ninguna + "En este momento no tenemos ningún hueco para nuevos partners", + # 6. no podemos (multi-word) + "Su propuesta es interesante pero no podemos proceder", + # 7. no es posible (multi-word) + "Gracias por escribir, pero no es posible colaborar este trimestre", + # 8. lamentablemente + "Lamentablemente su aplicación no ha sido aprobada", + # 9. no aceptamos + "Por política interna no aceptamos sitios de viajes", + # 10. mixed spanish/english: negation in spanish, approval word in english + "Thanks for reaching out! Lamentablemente we cannot approve your site", + # 11. mixed: approval in spanish, negation in english + "Apreciamos su interés, but we are not accepting new partners", + # 12. nadie / nada + "Aquí nadie decidiría eso, así que no hay nada que aprobar", + ], +) +def test_spanish_and_mixed_negations_veto_approval(cfg, text): + _never_approval(cfg, text) + + +def test_spanish_pure_approval_still_approves(cfg): + """A clean Spanish approval WITHOUT any negation must still classify + approval — the veto must not over-fire.""" + config = cfg() + kind, confidence = clf.classify( + "Su solicitud fue aprobada. Bienvenido al programa, estamos encantados " + "de trabajar con ustedes.", + config, + ) + assert kind is APPROVAL_ONLY + assert confidence >= 0.5 + + +def test_spanish_rejection_classifies_rejection(cfg): + config = cfg() + kind, conf = clf.classify( + "Lamentamos informarle que su solicitud fue rechazada por el comité", + config, + ) + assert kind is MailClassification.REJECTION + assert conf >= 0.9