File size: 4,496 Bytes
893a679
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""
Reconnaissance d'une reference d'article dans une requete.

« article 35 du code minier » doit ouvrir le Code Minier sur son article 35.
Cela suppose trois choses : reconnaitre le numero, reconnaitre le document, et
normaliser le numero sous la forme reellement stockee.

CE MODULE EXISTE PARCE QUE LA LOGIQUE ETAIT ECRITE HUIT FOIS. Un inventaire du
depot trouve des motifs « article N » dans rag_service (trois : detection dans
la question, CITATION_REGEX sur la reponse du modele, normalisation),
search_service, reranker, legal_domain_classifier, chunk_refiner et
text_chunker — tous legerement differents. Celui du RAG refuse « art.35 » faute
d'espace ; celui du reranker est le seul a accepter le prefixe L/R/D. Ces ecarts
ne se voient jamais a la lecture, seulement a l'usage.

Author: JuriX Team
"""

import re
from dataclasses import dataclass
from typing import Optional

# Ordinaux ecrits en toutes lettres. `text_chunker.normalize_article_number`
# ecrit "1" en base pour « Article premier » : sans cette table, une requete
# « article premier du code minier » produirait la cible "PREMIER", qui ne
# correspond a aucune ligne.
_ORDINAL_WORDS = {
    "PREMIER": "1", "PREMIERE": "1", "PREMIÈRE": "1", "1ER": "1", "1ÈRE": "1",
    "1ERE": "1", "FIRST": "1",
    "DEUXIEME": "2", "DEUXIÈME": "2", "SECOND": "2", "SECONDE": "2",
    "TROISIEME": "3", "TROISIÈME": "3", "THIRD": "3",
}

_LEADING = re.compile(r"^(ARTICLE|ART\.?|SECTION)\s*", re.IGNORECASE)

# Motifs essayes DANS L'ORDRE. Les deux premiers exigent une mention de
# document ; les deux derniers acceptent un numero seul. L'ordre compte : sans
# lui, « article 35 du code minier » serait capte par le motif « numero seul »,
# qui jetterait « du code minier ».
_PATTERNS = (
    # « article 35 du code minier », « art.35 de la constitution »
    re.compile(
        r"\bart(?:icle|\.)?\s*([LRD]\s*)?(\d+(?:[-.]\d+)*(?:\s*(?:bis|ter|quater))?)"
        r"\s+(?:de\s+la\s+|de\s+l['’]\s*|du\s+|de\s+|des\s+)(.+)",
        re.IGNORECASE,
    ),
    # « article premier de la constitution »
    re.compile(
        r"\bart(?:icle|\.)?\s+(premier|première|1er|1ère)"
        r"\s+(?:de\s+la\s+|de\s+l['’]\s*|du\s+|de\s+|des\s+)(.+)",
        re.IGNORECASE,
    ),
    # « article 35 » seul — aucune mention de document
    re.compile(
        r"\bart(?:icle|\.)?\s*([LRD]\s*)?(\d+(?:[-.]\d+)*)\s*$", re.IGNORECASE
    ),
    re.compile(r"\bart(?:icle|\.)?\s+(premier|première|1er|1ère)\s*$", re.IGNORECASE),
)


@dataclass(frozen=True)
class ArticleReference:
    """Numero demande, et mention du document si elle est presente."""

    number: str
    doc_hint: str = ""

    @property
    def has_hint(self) -> bool:
        return bool(self.doc_hint.strip())


def normalize_number(number: Optional[str]) -> str:
    """
    Ramene un numero d'article a la forme stockee dans `articles.number`.

    « Article 1er », « premier », « 1ER » et « 1 » designent le meme article.
    """
    if not number:
        return ""
    cleaned = _LEADING.sub("", str(number).strip().upper())
    cleaned = cleaned.strip(" .:-")
    cleaned = re.sub(r"\s+", " ", cleaned)
    return _ORDINAL_WORDS.get(cleaned, cleaned)


def parse_reference(query: str) -> Optional[ArticleReference]:
    """
    Extrait la reference d'article d'une requete, ou None.

    >>> parse_reference("article 35 du code minier")
    ArticleReference(number='35', doc_hint='code minier')
    >>> parse_reference("art.35")
    ArticleReference(number='35', doc_hint='')
    >>> parse_reference("le regime des permis de recherche") is None
    True
    """
    if not query:
        return None
    text = query.strip()

    for index, pattern in enumerate(_PATTERNS):
        match = pattern.search(text)
        if not match:
            continue
        groups = match.groups()
        if index == 0:
            prefix, number, hint = groups
            raw = f"{prefix.strip()} {number}".strip() if prefix else number
            return ArticleReference(normalize_number(raw), hint.strip(" .,;:"))
        if index == 1:
            number, hint = groups
            return ArticleReference(normalize_number(number), hint.strip(" .,;:"))
        if index == 2:
            prefix, number = groups
            raw = f"{prefix.strip()} {number}".strip() if prefix else number
            return ArticleReference(normalize_number(raw))
        return ArticleReference(normalize_number(groups[0]))

    return None