Romanes commited on
Commit
c0d875f
·
verified ·
1 Parent(s): 6506dd0

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +173 -0
app.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ # -*- coding: utf-8 -*-
3
+ import os
4
+ import re
5
+ import unicodedata
6
+ from pathlib import Path
7
+
8
+ import gradio as gr
9
+ import joblib
10
+ import pandas as pd
11
+ from scipy import sparse
12
+ from sklearn.metrics.pairwise import cosine_similarity
13
+
14
+ # ==========================
15
+ # Ubicación de artefactos
16
+ # ==========================
17
+ ART = Path("artifacts")
18
+ VEC_PATH = ART / "tfidf_vectorizer.joblib"
19
+ MAT_PATH = ART / "tfidf_matrix.npz"
20
+ IDX_PATH = ART / "doc_index.csv"
21
+
22
+ # ==========================
23
+ # Utilidades de limpieza
24
+ # ==========================
25
+ import nltk
26
+ from nltk.corpus import stopwords
27
+
28
+ def _ensure_nltk():
29
+ try:
30
+ nltk.data.find("corpora/stopwords")
31
+ except LookupError:
32
+ nltk.download("stopwords")
33
+ _ensure_nltk()
34
+
35
+ def strip_accents(s: str) -> str:
36
+ return "".join(c for c in unicodedata.normalize("NFKD", s) if not unicodedata.combining(c))
37
+
38
+ STOPWORDS = {strip_accents(w.lower()) for w in stopwords.words("spanish")} | {"aun"}
39
+
40
+ def limpiar_texto(s: str) -> str:
41
+ if not isinstance(s, str):
42
+ s = "" if s is None else str(s)
43
+ s = strip_accents(s.lower())
44
+ s = re.sub(r"[“”„‟‹›«»—–‐-‒–—―\-]", " ", s)
45
+ s = re.sub(r"[^\w\s]", " ", s)
46
+ s = re.sub(r"\s+", " ", s).strip()
47
+ toks = [t for t in s.split() if t not in STOPWORDS and not t.isdigit()]
48
+ return " ".join(toks)
49
+
50
+ # ==========================
51
+ # Reglas heurísticas (ejemplo OPS)
52
+ # ==========================
53
+ REGLAS = [
54
+ {
55
+ "keywords": ["ops", "orden de prestacion de servicios", "contrato ops"],
56
+ "respuesta": {
57
+ "CICP": ("2.1.2.02.02.008", "Servicios prestados a las empresas y servicios de producción"),
58
+ "CPC": ("8", "Servicios prestados a las empresas y servicios de producción"),
59
+ "UNSPSC":("80111600", "Servicios de personal temporal"),
60
+ },
61
+ "motivo": "Coincidencia con palabra clave OPS",
62
+ },
63
+ ]
64
+
65
+ def aplicar_reglas(consulta: str):
66
+ texto = limpiar_texto(consulta)
67
+ for regla in REGLAS:
68
+ if any(k in texto for k in regla["keywords"]):
69
+ rows = []
70
+ for cat, (cod, nom) in regla["respuesta"].items():
71
+ rows.append({"Catálogo": cat, "Código": cod, "Nombre": nom, "Similaridad": 1.0, "Origen": "Regla"})
72
+ return pd.DataFrame(rows)
73
+ return None
74
+
75
+ def catalog_tag(source_file: str) -> str:
76
+ s = (source_file or "").lower()
77
+ if "cicp" in s: return "CICP"
78
+ if "cpc" in s: return "CPC"
79
+ if "unspsc" in s: return "UNSPSC"
80
+ return "OTRO"
81
+
82
+ def parse_code_name(codes_raw: str, text_original: str):
83
+ codes_raw = str(codes_raw or "")
84
+ text_original = str(text_original or "")
85
+ m = re.search(r"CODIGO;NOMBRE:\s*([^;|]+)\s*;\s*([^|]+)", codes_raw, flags=re.I)
86
+ if not m:
87
+ m = re.search(r"CODIGO;NOMBRE:\s*([^;|]+)\s*;\s*([^|]+)", text_original, flags=re.I)
88
+ if m:
89
+ return m.group(1).strip(), m.group(2).strip()
90
+ code = None; name = None
91
+ m1 = re.search(r"CODIGO\s*:\s*([^|]+)", codes_raw, flags=re.I)
92
+ m2 = re.search(r"NOMBRE\s*:\s*([^|]+)", codes_raw, flags=re.I)
93
+ if m1: code = m1.group(1).strip()
94
+ if m2: name = m2.group(1).strip()
95
+ if code is None or name is None:
96
+ m1 = re.search(r"CODIGO\s*:\s*([^|]+)", text_original, flags=re.I)
97
+ m2 = re.search(r"NOMBRE\s*:\s*([^|]+)", text_original, flags=re.I)
98
+ if m1 and code is None: code = m1.group(1).strip()
99
+ if m2 and name is None: name = m2.group(1).strip()
100
+ return (code or "").strip(), (name or "").strip()
101
+
102
+ # ==========================
103
+ # Carga en startup
104
+ # ==========================
105
+ VEC = joblib.load(VEC_PATH)
106
+ MAT = sparse.load_npz(MAT_PATH)
107
+ IDX = pd.read_csv(IDX_PATH)
108
+ IDX["catalogo"] = IDX["source_file"].apply(catalog_tag)
109
+
110
+ # ==========================
111
+ # Endpoint de predicción
112
+ # ==========================
113
+ def predecir(consulta: str, top_por_catalogo: int = 1):
114
+ if not consulta or not consulta.strip():
115
+ return pd.DataFrame([{"Catálogo": "", "Código": "", "Nombre": "", "Similaridad": 0.0, "Origen": "—"}])
116
+
117
+ # 1) Reglas
118
+ out_regla = aplicar_reglas(consulta)
119
+ if out_regla is not None:
120
+ return out_regla.sort_values("Catálogo")
121
+
122
+ # 2) Modelo TF-IDF
123
+ q = limpiar_texto(consulta)
124
+ vec_q = VEC.transform([q])
125
+ sims = cosine_similarity(vec_q, MAT)[0]
126
+
127
+ df = IDX.copy()
128
+ df["Similaridad"] = sims
129
+
130
+ frames = []
131
+ for cat in ["CICP", "CPC", "UNSPSC"]:
132
+ sub = (
133
+ df[df["catalogo"] == cat]
134
+ .sort_values("Similaridad", ascending=False)
135
+ .head(top_por_catalogo)
136
+ .copy()
137
+ )
138
+ parsed = sub.apply(lambda r: parse_code_name(r.get("codes_raw",""), r.get("text_original","")), axis=1)
139
+ sub["Código"] = [c for c, _ in parsed]
140
+ sub["Nombre"] = [n for _, n in parsed]
141
+ sub["Catálogo"] = cat
142
+ sub["Origen"] = "TF-IDF"
143
+ frames.append(sub[["Catálogo","Código","Nombre","Similaridad","Origen"]])
144
+
145
+ res = pd.concat(frames, ignore_index=True)
146
+ res["Similaridad"] = res["Similaridad"].round(4)
147
+ return res.sort_values("Catálogo")
148
+
149
+ # ==========================
150
+ # Gradio UI
151
+ # ==========================
152
+ with gr.Blocks(title="Recomendador CICP / CPC / UNSPSC") as demo:
153
+ gr.Markdown("## Recomendador por texto (CICP / CPC / UNSPSC)\n*TF-IDF + reglas*")
154
+ with gr.Row():
155
+ consulta = gr.Textbox(label="Descripción técnica", lines=3, placeholder="Ej: Vinculación joven investigadora OPS ...")
156
+ topk = gr.Slider(1, 5, value=1, step=1, label="Top por catálogo")
157
+ btn = gr.Button("Buscar")
158
+ salida = gr.Dataframe(headers=["Catálogo","Código","Nombre","Similaridad","Origen"], interactive=False)
159
+
160
+ ejemplos = gr.Examples(
161
+ examples=[
162
+ ["Vinculación joven investigadora, OPS gastos de operación y servicios técnicos", 1],
163
+ ["contrato de personal temporal", 1],
164
+ ["reactivos de laboratorio para cromatografía hplc", 1],
165
+ ],
166
+ inputs=[consulta, topk],
167
+ label="Ejemplos",
168
+ )
169
+
170
+ btn.click(predecir, inputs=[consulta, topk], outputs=[salida])
171
+
172
+ if __name__ == "__main__":
173
+ demo.launch()