File size: 9,058 Bytes
9abe007
 
 
 
 
 
 
2c07e6b
9abe007
 
 
 
 
 
 
 
 
 
2c07e6b
9abe007
 
 
 
 
 
 
 
2c07e6b
 
9abe007
 
 
 
 
 
 
 
 
 
 
 
 
 
2c07e6b
 
 
 
9abe007
 
 
 
 
2c07e6b
 
 
 
 
9abe007
 
c64e5be
9abe007
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c64e5be
9abe007
 
 
 
 
 
 
c64e5be
9abe007
 
 
 
 
2c07e6b
9abe007
 
2c07e6b
 
 
 
 
 
9abe007
 
 
 
 
 
 
 
2c07e6b
 
9abe007
 
 
 
 
 
 
2c07e6b
 
 
9abe007
 
 
 
 
2c07e6b
9abe007
 
 
 
 
 
2c07e6b
9abe007
 
 
 
 
 
 
 
 
 
2c07e6b
9abe007
2c07e6b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9abe007
 
2c07e6b
9abe007
 
 
 
 
 
2c07e6b
9abe007
 
 
 
 
2c07e6b
9abe007
 
 
 
 
 
c64e5be
9abe007
2c07e6b
 
9abe007
 
 
 
 
 
 
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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
"""
rag_corrector.py
────────────────
NΓΊcleo del sistema RAG.
  1. Detecta posibles errores HTR y grafΓ­as modernas en el texto de entrada.
  2. Recupera ejemplos similares del vector store (few-shot dinΓ‘mico).
  3. Construye el prompt con reglas, ejemplos y alertas.
  4. Llama al LLM y devuelve el texto corregido + trazabilidad.

Uso:
    from rag_corrector import RAGCorrector
    corrector = RAGCorrector(vector_store)
    result = corrector.correct("texto htr aqui")
    print(result["corrected"])
    print(result["prompt"])          # para depuraciΓ³n
"""

import os
import re
from typing import List, Dict, Tuple
from openai import OpenAI
from dotenv import load_dotenv

from knowledge_base import HTR_ERROR_PATTERNS, GRAFIA_PATTERNS

load_dotenv()

MODEL = os.getenv("OPENAI_MODEL", "gpt-4o")
TOP_K = int(os.getenv("TOP_K", 5))

SYSTEM_PROMPT = """Eres un corrector especializado en documentos notariales y judiciales \
espaΓ±oles del siglo XVI (castellano antiguo).
Tu ÚNICA tarea es corregir los errores introducidos por el proceso automÑtico de \
reconocimiento de texto manuscrito (HTR). NO debes modernizar el texto bajo ninguna \
circunstancia.
REGLAS ABSOLUTAS β€” incumplirlas invalida la correcciΓ³n:
1. Conserva SIEMPRE las grafΓ­as propias del s.XVI:
   fizo, fazer, hazer, merΓ§ed, vezino, mesmo, Γ§ibdad, escriuano,
   dho (=dicho), q̃ (=que), nro (=nuestro), vn/vna, etc.
2. NO conviertas f→h inicial latina (fizo ≠ hizo, fazer ≠ hacer).
3. Conserva abreviaturas y tildes voladas (q̃, nro, dho, sr).
4. Corrige SOLO lo que claramente sea un error HTR (grafema confundido visualmente).
5. Si no estΓ‘s seguro de si algo es error HTR o forma s.XVI vΓ‘lida β†’ conserva el original.
6. Presta especial atenciΓ³n a los errores marcados como [CRÍTICO] β€” son los de mayor \
impacto en la comprensiΓ³n del texto.
7. NO corrijas ningΓΊn patrΓ³n marcado con ⚠ NO_CORREGIR β€” esas formas son vΓ‘lidas en s.XVI.
8. Responde ÚNICAMENTE con el texto corregido. Sin explicaciones, sin comillas."""


class RAGCorrector:
    def __init__(self, vector_store):
        self.vs = vector_store
        self.client = OpenAI(
            api_key=os.getenv("OPENAI_API_KEY"),
            base_url=os.getenv("OPENAI_BASE_URL", "https://api.x.ai/v1"),
        )

    # ── API pΓΊblica ──────────────────────────────────────────────────────────

    def correct(self, htr_text: str, top_k: int = TOP_K, model: str = None) -> Dict:
        """
        Corrige un texto HTR usando RAG.
        Retorna dict con:
          corrected     : str   β€” texto corregido
          prompt        : str   β€” prompt completo enviado al LLM
          retrieved     : list  β€” documentos recuperados del vector store
          htr_errors    : list  β€” patrones HTR detectados
          grafia_warns  : list  β€” grafΓ­as modernas detectadas (alertas)
          model         : str   β€” modelo usado
        """
        retrieved    = self.vs.retrieve(htr_text, k=top_k)
        htr_errors   = self._detect_htr_errors(htr_text)
        grafia_warns = self._detect_grafias(htr_text)

        prompt = self._build_prompt(htr_text, retrieved, htr_errors, grafia_warns)
        corrected = self._call_llm(prompt, model=model or MODEL)

        return {
            "corrected":    corrected,
            "prompt":       prompt,
            "retrieved":    retrieved,
            "htr_errors":   htr_errors,
            "grafia_warns": grafia_warns,
            "model":        model or MODEL,
        }

    # ── DetecciΓ³n de patrones ────────────────────────────────────────────────

    def _detect_htr_errors(self, text: str) -> List[Dict]:
        """Detecta patrones de error HTR en el texto usando word boundaries."""
        found = []
        for p in HTR_ERROR_PATTERNS:
            htr_val = p.get("htr", "")
            # Ignorar patrones descriptivos (fusion_palabras, omision_*, etc.)
            if "_" in htr_val or len(htr_val) > 10:
                continue
            pattern = r'\b' + re.escape(htr_val) + r'\b'
            if re.search(pattern, text, re.IGNORECASE):
                found.append(p)
        return found

    def _detect_grafias(self, text: str) -> List[Dict]:
        """Detecta formas modernas que NO deberΓ­an modernizarse."""
        found = []
        lower = text.lower()
        for p in GRAFIA_PATTERNS:
            pattern = r'\b' + re.escape(p["modern"].lower()) + r'\b'
            if re.search(pattern, lower):
                found.append(p)
        return found

    # ── Constructor de prompt ────────────────────────────────────────────────

    def _build_prompt(
        self,
        htr_text:     str,
        retrieved:    List[Dict],
        htr_errors:   List[Dict],
        grafia_warns: List[Dict],
    ) -> str:

        sections = []

        # ── Bloque 1: Few-shot dinΓ‘mico (ejemplos recuperados) ────────────────
        if retrieved:
            examples = []
            for i, doc in enumerate(retrieved, 1):
                corr = "; ".join(doc["corrections"]) if doc["corrections"] else "β€”"
                examples.append(
                    f"Ejemplo {i} [{doc['type']}, {doc['region']}, {doc['date']}]"
                    f" caligrafia: {doc.get('caligrafia', '?')}"
                    f" (similitud={doc['score']}):\n"
                    f"  HTR:  \"{doc['htr']}\"\n"
                    f"  GT:   \"{doc['gt']}\"\n"
                    f"  Correcciones aplicadas: {corr}"
                )
            sections.append(
                "EJEMPLOS DEL CORPUS (similares al texto a corregir):\n"
                + "\n\n".join(examples)
            )

        # ── Bloque 2: Alertas de errores HTR detectados ───────────────────────
        if htr_errors:
            # Ordenar: crΓ­ticos primero
            SEV_ORDER = {"alta": 0, "media": 1, "baja": 2}
            sorted_errors = sorted(
                htr_errors,
                key=lambda p: SEV_ORDER.get(p.get("severity", "media"), 1)
            )

            hints = []
            for p in sorted_errors:
                sev      = p.get("severity", "media").upper()
                htr_c    = p.get("htr", "")
                gt_c     = p.get("gt", "")
                context  = p.get("context", "")
                tendencia= p.get("tendencia", "")
                warning  = p.get("warning")

                # Ejemplos: hasta 3 de la lista completa
                ejemplos_raw = p.get("examples", p.get("example", ""))
                if isinstance(ejemplos_raw, list):
                    ejemplos_str = "; ".join(ejemplos_raw[:3])
                else:
                    ejemplos_str = ejemplos_raw

                linea = (
                    f"  β€’ [{sev}] '{htr_c}' puede ser '{gt_c}'\n"
                    f"    Causa    : {context}"
                )
                if tendencia:
                    linea += f"\n    Tendencia: {tendencia}"
                if ejemplos_str:
                    linea += f"\n    Ejemplos : {ejemplos_str}"
                if warning:
                    linea += f"\n    ⚠ {warning}"

                hints.append(linea)

            sections.append(
                "POSIBLES ERRORES HTR DETECTADOS EN ESTE TEXTO:\n"
                + "\n\n".join(hints)
            )

        # ── Bloque 3: Alertas de grafΓ­as modernas ─────────────────────────────
        if grafia_warns:
            warns = "\n".join(
                f"  β€’ '{p['modern']}' β†’ mantener como '{p['ancient']}': {p['rule']}"
                for p in grafia_warns
            )
            sections.append(
                "ALERTA β€” GRAFÍAS QUE NO DEBEN MODERNIZARSE:\n" + warns
            )

        context_block = "\n\n".join(sections)

        return (
            f"{context_block}\n\nTEXTO HTR A CORREGIR:\n\"{htr_text}\""
            if context_block
            else f"TEXTO HTR A CORREGIR:\n\"{htr_text}\""
        )

    # ── Llamada al LLM ───────────────────────────────────────────────────────

    def _call_llm(self, user_prompt: str, model: str = MODEL) -> str:
        response = self.client.chat.completions.create(
            model=model,
            temperature=0.1,
            max_tokens=1024,
            messages=[
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user",   "content": user_prompt},
            ],
        )
        return response.choices[0].message.content.strip().strip('"').strip("'")