Update app.py
Browse files
app.py
CHANGED
|
@@ -1,18 +1,18 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
| 5 |
import numpy as np
|
| 6 |
-
os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
|
| 7 |
import streamlit as st
|
| 8 |
import datetime as dt
|
| 9 |
from pathlib import Path
|
| 10 |
-
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
| 11 |
-
from
|
| 12 |
-
|
| 13 |
from huggingface_hub import hf_hub_download, login
|
| 14 |
from sentence_transformers import SentenceTransformer # RAG embeddings
|
| 15 |
-
|
| 16 |
#***************************************************************************
|
| 17 |
#Setting up variables
|
| 18 |
#***************************************************************************
|
|
@@ -24,25 +24,20 @@ HF_TOKEN = os.environ.get("HF_TOKEN")
|
|
| 24 |
#***************************************************************************
|
| 25 |
REPO_ID = "tecuhtli/Mori_FAISS_Full"
|
| 26 |
|
|
|
|
| 27 |
#***************************************************************************
|
| 28 |
# Sidebar controls for generation params
|
| 29 |
#***************************************************************************
|
| 30 |
|
| 31 |
def sidebar_params():
|
| 32 |
-
|
| 33 |
with st.sidebar:
|
| 34 |
st.title("🎮 Personalidad (FLAN-T5)")
|
| 35 |
|
| 36 |
ss = st.session_state
|
| 37 |
-
# Defaults (solo 1ª vez)
|
| 38 |
-
|
| 39 |
-
# Estado inicial: ocultar ajustes avanzados
|
| 40 |
-
ss = st.session_state
|
| 41 |
-
if "show_llm_controls" not in ss:
|
| 42 |
-
ss.show_llm_controls = False
|
| 43 |
|
| 44 |
-
|
| 45 |
-
ss.setdefault("
|
|
|
|
| 46 |
ss.setdefault("mode", "beam") # 'beam' | 'sampling'
|
| 47 |
ss.setdefault("max_new", 128)
|
| 48 |
ss.setdefault("min_tok", 16)
|
|
@@ -52,7 +47,6 @@ def sidebar_params():
|
|
| 52 |
ss.setdefault("temperature", 0.7)
|
| 53 |
ss.setdefault("top_p", 0.9)
|
| 54 |
ss.setdefault("repetition_penalty", 1.0)
|
| 55 |
-
ss.setdefault("show_llm_controls", True) # Toggle principal
|
| 56 |
|
| 57 |
# ----------------------------
|
| 58 |
# Personalidad (presets)
|
|
@@ -61,133 +55,40 @@ def sidebar_params():
|
|
| 61 |
c1, c2 = st.columns(2)
|
| 62 |
|
| 63 |
with c1:
|
| 64 |
-
if st.button("
|
| 65 |
-
ss.update({
|
| 66 |
-
"persona": "Mori Normal",
|
| 67 |
-
"mode": "beam",
|
| 68 |
-
"num_beams": 4,
|
| 69 |
-
"max_new": 128,
|
| 70 |
-
"min_tok": 16,
|
| 71 |
-
"no_repeat": 3,
|
| 72 |
-
"length_penalty": 1.0,
|
| 73 |
-
"temperature": 0.7,
|
| 74 |
-
"top_p": 0.9,
|
| 75 |
-
"repetition_penalty": 1.0,
|
| 76 |
-
})
|
| 77 |
st.rerun()
|
| 78 |
|
| 79 |
with c2:
|
| 80 |
-
if st.button("
|
| 81 |
-
ss.update({
|
| 82 |
-
"persona": "Mori Entusiasta", # <- corregido
|
| 83 |
-
"mode": "sampling",
|
| 84 |
-
"max_new": 192,
|
| 85 |
-
"min_tok": 48,
|
| 86 |
-
"no_repeat": 3,
|
| 87 |
-
"temperature": 1.25,
|
| 88 |
-
"top_p": 0.95,
|
| 89 |
-
"repetition_penalty": 1.0,
|
| 90 |
-
})
|
| 91 |
st.rerun()
|
| 92 |
|
| 93 |
st.caption(f"Personalidad actual: **{ss.persona}**")
|
| 94 |
|
| 95 |
-
# ----------------------------
|
| 96 |
-
# Botón para mostrar/ocultar parámetros
|
| 97 |
-
# ----------------------------
|
| 98 |
-
if st.button(("🔼 Ocultar" if ss.show_llm_controls else "🔽 Mostrar") + " ajustes avanzados"):
|
| 99 |
-
ss.show_llm_controls = not ss.show_llm_controls
|
| 100 |
-
st.rerun()
|
| 101 |
-
|
| 102 |
-
# ----------------------------
|
| 103 |
-
# Controles del modelo (sliders, estrategia, etc.)
|
| 104 |
-
# ----------------------------
|
| 105 |
-
if ss.show_llm_controls:
|
| 106 |
-
st.header("⚙️ Ajustes manuales")
|
| 107 |
-
st.subheader("📝 Generación de texto")
|
| 108 |
-
picked = st.radio(
|
| 109 |
-
"Estrategia",
|
| 110 |
-
["Beam search (estable)", "Sampling (creativo)"],
|
| 111 |
-
index=0 if ss.mode == "beam" else 1,
|
| 112 |
-
help="https://huggingface.co/docs/transformers/generation_strategies"
|
| 113 |
-
)
|
| 114 |
-
ss.mode = "beam" if picked.startswith("Beam") else "sampling"
|
| 115 |
-
|
| 116 |
-
st.subheader("🔧 Parámetros del modelo LLM")
|
| 117 |
-
ss.max_new = st.slider(
|
| 118 |
-
"max_new_tokens", 16, 256, int(ss.max_new), step=8,
|
| 119 |
-
help="https://huggingface.co/docs/transformers/main_classes/text_generation"
|
| 120 |
-
)
|
| 121 |
-
ss.min_tok = st.slider(
|
| 122 |
-
"min_tokens", 0, int(ss.max_new), int(ss.min_tok),
|
| 123 |
-
help="https://huggingface.co/docs/transformers/main_classes/text_generation"
|
| 124 |
-
)
|
| 125 |
-
ss.no_repeat = st.slider(
|
| 126 |
-
"no_repeat_ngram_size", 0, 6, int(ss.no_repeat),
|
| 127 |
-
help="https://huggingface.co/docs/transformers/main_classes/text_generation"
|
| 128 |
-
)
|
| 129 |
-
|
| 130 |
-
# Subcontroles según modo
|
| 131 |
-
if ss.mode == "beam":
|
| 132 |
-
ss.num_beams = st.slider(
|
| 133 |
-
"num_beams", 2, 8, int(ss.num_beams),
|
| 134 |
-
help="https://huggingface.co/docs/transformers/main_classes/text_generation"
|
| 135 |
-
)
|
| 136 |
-
ss.length_penalty = st.slider(
|
| 137 |
-
"length_penalty", 0.0, 2.0, float(ss.length_penalty),
|
| 138 |
-
step=0.1, help="https://huggingface.co/docs/transformers/main_classes/text_generation"
|
| 139 |
-
)
|
| 140 |
-
else:
|
| 141 |
-
ss.temperature = st.slider(
|
| 142 |
-
"temperature", 0.1, 1.5, float(ss.temperature),
|
| 143 |
-
step=0.05, help="https://huggingface.co/docs/transformers/main_classes/text_generation"
|
| 144 |
-
)
|
| 145 |
-
ss.top_p = st.slider(
|
| 146 |
-
"top_p", 0.5, 1.0, float(ss.top_p),
|
| 147 |
-
step=0.01, help="https://huggingface.co/docs/transformers/main_classes/text_generation"
|
| 148 |
-
)
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
# 🔹 Nueva sección que quieres mostrar siempre
|
| 152 |
-
st.markdown("---")
|
| 153 |
-
st.title("✏️🛠 Prompt Engineering")
|
| 154 |
-
|
| 155 |
-
prompt_type = st.radio(
|
| 156 |
-
"🧑🏽🔬 Modo de prompting",
|
| 157 |
-
options=["Zero-shot", "One-shot", "Few-shot (3)"],
|
| 158 |
-
index=0,
|
| 159 |
-
help=("https://huggingface.co/docs/transformers/en/tasks/prompting"))
|
| 160 |
-
st.session_state.prompt_type = prompt_type
|
| 161 |
-
|
| 162 |
-
# Aquí podrías agregar el preview del prompt, o el comparador si lo usas
|
| 163 |
-
# En session_state:
|
| 164 |
-
if "PROMPT_CASES" not in st.session_state:
|
| 165 |
-
st.session_state.PROMPT_CASES = load_prompt_cases()
|
| 166 |
-
|
| 167 |
-
|
| 168 |
st.markdown("---")
|
| 169 |
-
st.title("👀 RAG
|
| 170 |
ss.setdefault("use_rag", True)
|
| 171 |
-
ss.setdefault("rag_k",
|
| 172 |
-
ss.use_rag = st.checkbox(
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
st.markdown("---")
|
| 179 |
st.title("🧾 Vista previa del Prompt")
|
| 180 |
-
|
| 181 |
-
if "last_prompt" in
|
| 182 |
with st.expander("Mostrar prompt generado"):
|
| 183 |
st.text_area(
|
| 184 |
"Prompt actual:",
|
| 185 |
-
|
| 186 |
height=200,
|
| 187 |
disabled=True
|
| 188 |
)
|
| 189 |
else:
|
| 190 |
-
st.caption("👉 Aún no se ha generado ningún prompt.")
|
| 191 |
|
| 192 |
# ----------------------------
|
| 193 |
# Construir diccionario de parámetros
|
|
@@ -200,55 +101,14 @@ def sidebar_params():
|
|
| 200 |
"no_repeat_ngram_size": int(ss.no_repeat),
|
| 201 |
"repetition_penalty": float(ss.repetition_penalty),
|
| 202 |
}
|
| 203 |
-
if ss.mode == "beam":
|
| 204 |
-
params.update({
|
| 205 |
-
"num_beams": int(ss.num_beams),
|
| 206 |
-
"length_penalty": float(ss.length_penalty),
|
| 207 |
-
})
|
| 208 |
-
else:
|
| 209 |
-
params.update({
|
| 210 |
-
"temperature": float(ss.temperature),
|
| 211 |
-
"top_p": float(ss.top_p),
|
| 212 |
-
})
|
| 213 |
|
| 214 |
return params
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
#***************************************************************************
|
| 220 |
# Functions
|
| 221 |
#***************************************************************************
|
| 222 |
|
| 223 |
-
|
| 224 |
-
def truncate_sentences(text: str, max_sentences: int = 4) -> str:
|
| 225 |
-
_SENT_SPLIT = re.compile(r'(?<=[\.\!\?…])\s+')
|
| 226 |
-
s = text.strip()
|
| 227 |
-
if not s: return s
|
| 228 |
-
parts = _SENT_SPLIT.split(s)
|
| 229 |
-
cut = " ".join(parts[:max_sentences]).strip()
|
| 230 |
-
if cut and cut[-1] not in ".!?…": cut += "."
|
| 231 |
-
return cut
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
def _load_json_safe(path: Path, fallback: dict) -> dict:
|
| 235 |
-
try:
|
| 236 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 237 |
-
return json.load(f)
|
| 238 |
-
except Exception:
|
| 239 |
-
return fallback
|
| 240 |
-
|
| 241 |
-
def load_prompt_cases():
|
| 242 |
-
base = Path("Prompts")
|
| 243 |
-
tech = _load_json_safe(base / "prompts_technical.json", {"modes": {}})
|
| 244 |
-
social = _load_json_safe(base / "prompts_social.json", {"modes": {}})
|
| 245 |
-
return {"technical": tech, "social": social}
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
# Function to clean the question field
|
| 252 |
def limpiar_input():
|
| 253 |
st.session_state["entrada"] = ""
|
| 254 |
|
|
@@ -257,14 +117,10 @@ def get_model_path(folder_name):
|
|
| 257 |
return Path("Models") / folder_name
|
| 258 |
|
| 259 |
# Function to save user interaction
|
| 260 |
-
def saving_interaction(question, response,
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
response --> Mori response to the user question
|
| 265 |
-
context --> Context related to the user input, found by the trained classifier
|
| 266 |
-
user_id --> ID for the current user (Unique ID per session)
|
| 267 |
-
'''
|
| 268 |
timestamp = dt.datetime.now().isoformat()
|
| 269 |
stats_dir = Path("Statistics")
|
| 270 |
stats_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -272,765 +128,52 @@ def saving_interaction(question, response, context, user_id):
|
|
| 272 |
archivo_csv = stats_dir / "conversaciones_log.csv"
|
| 273 |
existe_csv = archivo_csv.exists()
|
| 274 |
|
|
|
|
| 275 |
with open(archivo_csv, mode="a", encoding="utf-8", newline="") as f_csv:
|
| 276 |
writer = csv.writer(f_csv)
|
| 277 |
if not existe_csv:
|
| 278 |
-
writer.writerow(["timestamp", "user_id", "
|
| 279 |
-
writer.writerow([timestamp, user_id,
|
| 280 |
|
|
|
|
| 281 |
archivo_jsonl = stats_dir / "conversaciones_log.jsonl"
|
| 282 |
with open(archivo_jsonl, mode="a", encoding="utf-8") as f_jsonl:
|
| 283 |
registro = {
|
| 284 |
"timestamp": timestamp,
|
| 285 |
"user_id": user_id,
|
| 286 |
-
"context": context,
|
| 287 |
"pregunta": question,
|
| 288 |
-
"respuesta": response
|
|
|
|
|
|
|
|
|
|
| 289 |
f_jsonl.write(json.dumps(registro, ensure_ascii=False) + "\n")
|
| 290 |
|
| 291 |
# Function to load models within the huggingface repositories space
|
| 292 |
@st.cache_resource
|
| 293 |
-
def
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
model = AutoModelForSeq2SeqLM.from_pretrained(path, local_files_only=True)
|
| 297 |
return model, tokenizer
|
| 298 |
-
|
| 299 |
#-------------------------------------------------------------------------
|
| 300 |
-
#
|
| 301 |
-
#-------------------------------------------------------------------------
|
| 302 |
-
|
| 303 |
-
def polish_spanish(s: str) -> str:
|
| 304 |
-
s = unicodedata.normalize("NFC", s).strip()
|
| 305 |
-
s = re.sub(r'\s*[\[\(]\s*Mori\s+(?:Social|T[eé]nico|T[eé]cnico)\s*[\]\)]\s*', '', s, flags=re.I)
|
| 306 |
-
fixes = [
|
| 307 |
-
(r'(?i)(^|\W)T\s+puedes(?P<p>[^\w]|$)', r'\1Tú puedes\g<p>'),
|
| 308 |
-
(r'(?i)(^|\W)T\s+(ya|eres|estas|estás|tienes|puedes)\b', r'\1Tú \2'),
|
| 309 |
-
(r'(?i)\bclaro que s(?:i|í)?\b(?P<p>[,.\!?…])?', r'Claro que sí\g<p>'),
|
| 310 |
-
(r'(?i)(^|\s)si,', r'\1Sí,'),
|
| 311 |
-
(r'(?i)(\beso\s+)s(\s+est[áa]\b)', r'\1sí\2'),
|
| 312 |
-
(r'(?i)(^|[\s,;:])s(\s+es\b)', r'\1sí\2'),
|
| 313 |
-
(r'(?i)\btiles\b', 'útiles'),
|
| 314 |
-
(r'(?i)\butiles\b', 'útiles'),
|
| 315 |
-
(r'(?i)\butil\b', 'útil'),
|
| 316 |
-
(r'(?i)\baqui\b', 'aquí'),
|
| 317 |
-
(r'(?i)\baqu\b(?=\s+estoy\b)', 'aquí'),
|
| 318 |
-
(r'(?i)\balgn\b', 'algún'),
|
| 319 |
-
(r'(?i)\balgun\b', 'algún'),
|
| 320 |
-
(r'(?i)\bAnimo\b', 'Ánimo'),
|
| 321 |
-
(r'(?i)\bcario\b', 'cariño'),
|
| 322 |
-
(r'(?i)\baprendisaje\b', 'aprendizaje'),
|
| 323 |
-
(r'(?i)\bmanana\b', 'mañana'),
|
| 324 |
-
(r'(?i)\bmaana\b', 'mañana'),
|
| 325 |
-
(r'(?i)\benergia\b', 'energía'),
|
| 326 |
-
(r'(?i)\benerga\b', 'energía'),
|
| 327 |
-
(r'(?i)\bextrano\b', 'extraño'),
|
| 328 |
-
(r'(?i)\bextrana\b', 'extraña'),
|
| 329 |
-
(r'(?i)\bextranar\b', 'extrañar'),
|
| 330 |
-
(r'(?i)\bextranarte\b', 'extrañarte'),
|
| 331 |
-
(r'(?i)\bextranas\b', 'extrañas'),
|
| 332 |
-
(r'(?i)\bextranos\b', 'extraños'),
|
| 333 |
-
(r'(?i)\baqu\b', 'aquí'),
|
| 334 |
-
(r'(?i)\baqui\b', 'aquí'),
|
| 335 |
-
(r'(?i)\bestare\b', 'estaré'),
|
| 336 |
-
(r'(?i)\bclarn\b', 'clarín'),
|
| 337 |
-
(r'(?i)\bclarin\b', 'clarín'),
|
| 338 |
-
(r'(?i)\bclar[íi]n\s+cornetas\b', 'clarín cornetas'),
|
| 339 |
-
(r'(?i)(^|\s)s([,.;:!?])', r'\1Sí\2'),
|
| 340 |
-
(r'(?i)\bfutbol\b', 'fútbol'),
|
| 341 |
-
(r'(?i)(^|\s)as(\s+se\b)', r'\1Así\2'),
|
| 342 |
-
(r'(?i)(^|\s)s(\s+orientarte\b)', r'\1sí\2'),
|
| 343 |
-
(r'(?i)\bbuen dia\b', 'buen día'),
|
| 344 |
-
(r'(?i)\bgran dia\b', 'gran día'),
|
| 345 |
-
(r'(?i)\bdias\b', 'días'),
|
| 346 |
-
(r'(?i)\bdia\b', 'día'),
|
| 347 |
-
(r'(?i)\bgran da\b', 'gran día'),
|
| 348 |
-
(r'(?i)\bacompa?a(r|rte|do|da|dos|das)?\b', r'acompaña\1'),
|
| 349 |
-
(r'(?i)(^|\s)as([,.;:!?]|\s|$)', r'\1así\2'),
|
| 350 |
-
(r'(?i)(^|\s)S lo se\b', r'\1Sí lo sé'),
|
| 351 |
-
(r'(?i)(^|\s)S lo sé\b', r'\1Sí lo sé'),
|
| 352 |
-
(r'(?i)\bcudese\b', 'cuídese'),
|
| 353 |
-
(r'(?i)\bpequeo\b', 'pequeño'),
|
| 354 |
-
(r'(?i)\bpequea\b', 'pequeña'),
|
| 355 |
-
(r'(?i)\bpequeos\b', 'pequeños'),
|
| 356 |
-
(r'(?i)\bpequeas\b', 'pequeñas'),
|
| 357 |
-
(r'(?i)\bunico\b', 'único'),
|
| 358 |
-
(r'(?i)\bunica\b', 'única'),
|
| 359 |
-
(r'(?i)\bunicos\b', 'únicos'),
|
| 360 |
-
(r'(?i)\bunicas\b', 'únicas'),
|
| 361 |
-
(r'(?i)\bnico\b', 'único'),
|
| 362 |
-
(r'(?i)\bnica\b', 'única'),
|
| 363 |
-
(r'(?i)\bnicos\b', 'únicos'),
|
| 364 |
-
(r'(?i)\bnicas\b', 'únicas'),
|
| 365 |
-
(r'(?i)\bestadstico\b', 'estadístico'),
|
| 366 |
-
(r'(?i)\bestadstica\b', 'estadística'),
|
| 367 |
-
(r'(?i)\bestadsticos\b', 'estadísticos'),
|
| 368 |
-
(r'(?i)\bestadsticas\b', 'estadísticas'),
|
| 369 |
-
(r'(?i)\bcudate\b', 'cuídate'),
|
| 370 |
-
(r'(?i)\bcuidate\b', 'cuídate'),
|
| 371 |
-
(r'(?i)\bcuidese\b', 'cuídese'),
|
| 372 |
-
(r'(?i)\bcudese\b', 'cuídese'),
|
| 373 |
-
(r'(?i)\bcuidense\b', 'cuídense'),
|
| 374 |
-
(r'(?i)\bcudense\b', 'cuídense'),
|
| 375 |
-
(r'(?i)\bgracias por confiar en m\b', 'gracias por confiar en mí'),
|
| 376 |
-
(r'(?i)\bcada dia\b', 'cada día'),
|
| 377 |
-
(r'(?i)\bcada da\b', 'cada día'),
|
| 378 |
-
(r'(?i)\bsegun\b', 'según'),
|
| 379 |
-
(r'(?i)\bcaracteristica(s)?\b', r'característica\1'),
|
| 380 |
-
(r'(?i)\bcaracterstica(s)?\b', r'característica\1'),
|
| 381 |
-
(r'(?i)\b([a-záéíóúñ]+)cion\b', r'\1ción'),
|
| 382 |
-
(r'(?i)\bdeterminacio\b', 'determinación'),
|
| 383 |
-
]
|
| 384 |
-
for pat, rep in fixes:
|
| 385 |
-
s = re.sub(pat, rep, s)
|
| 386 |
-
|
| 387 |
-
s = re.sub(r'(?i)^eso es todo!(?P<r>(\s|$).*)', r'¡Eso es todo!\g<r>', s)
|
| 388 |
-
|
| 389 |
-
def add_opening_q(m):
|
| 390 |
-
cuerpo = m.group('qbody')
|
| 391 |
-
if '¿' in cuerpo:
|
| 392 |
-
return m.group(0)
|
| 393 |
-
return f"{m.group('pre')}¿{cuerpo}"
|
| 394 |
-
s = re.sub(r'(?P<pre>(^|[\.!\…]\s+))(?P<qbody>[^?]*\?)', add_opening_q, s)
|
| 395 |
-
|
| 396 |
-
def _open_exclam(m):
|
| 397 |
-
palabra = m.group('w')
|
| 398 |
-
resto = m.group('r') or ''
|
| 399 |
-
return f'¡{palabra}!{resto}'
|
| 400 |
-
s = re.sub(r'(?i)^(?P<w>(hola|gracias|genial|perfecto|claro|por supuesto|con gusto|listo|vaya|wow|tu puedes|tú puedes|clarín|clarin|clarín cornetas))!(?P<r>(\s|$).*)',_open_exclam, s)
|
| 401 |
-
|
| 402 |
-
s = re.sub(r'\s+', ' ', s).strip()
|
| 403 |
-
if s and s[-1] not in ".!?…":
|
| 404 |
-
s += "."
|
| 405 |
-
return s
|
| 406 |
-
|
| 407 |
-
#-------------------------------------------------------------------------
|
| 408 |
-
# Function to remove repeated input in the Model answer
|
| 409 |
-
#-------------------------------------------------------------------------
|
| 410 |
-
|
| 411 |
-
def anti_echo(response: str, user_text: str) -> str:
|
| 412 |
-
rn = normalize_for_route(response)
|
| 413 |
-
un = normalize_for_route(user_text)
|
| 414 |
-
def _clean_leading(s: str) -> str:
|
| 415 |
-
s = re.sub(r'^\s*[,;:\-–—]\s*', '', s)
|
| 416 |
-
s = re.sub(r'^\s+', '', s)
|
| 417 |
-
return s
|
| 418 |
-
if len(un) >= 4 and rn.startswith(un):
|
| 419 |
-
cut = re.sub(r'^\s*[^,;:\.\!\?]{0,120}[,;:\-]\s*', '', response).lstrip()
|
| 420 |
-
if cut and cut != response:
|
| 421 |
-
return _clean_leading(cut)
|
| 422 |
-
return _clean_leading(response[len(user_text):])
|
| 423 |
-
return response
|
| 424 |
-
|
| 425 |
-
#-------------------------------------------------------------------------
|
| 426 |
-
# Normalization helpers
|
| 427 |
-
#-------------------------------------------------------------------------
|
| 428 |
-
|
| 429 |
-
def normalize_for_route(s: str) -> str:
|
| 430 |
-
s = unicodedata.normalize("NFKD", s)
|
| 431 |
-
s = "".join(ch for ch in s if not unicodedata.combining(ch))
|
| 432 |
-
s = re.sub(r"[^\w\s-]", " ", s, flags=re.UNICODE)
|
| 433 |
-
s = re.sub(r"\s+", " ", s).strip().lower()
|
| 434 |
-
return s
|
| 435 |
-
|
| 436 |
-
_Q_STARTERS = {
|
| 437 |
-
"como","que","quien","quienes","cuando","donde","por que","para que",
|
| 438 |
-
"cual","cuales","cuanto","cuantos","cuanta","cuantas"
|
| 439 |
-
}
|
| 440 |
-
_EXC_TRIGGERS = {"motiva","motivame","animate","animame","animo","ayudame","ayudame porfa", "clarin", "clarín", "clarinete", "clarin cornetas"}
|
| 441 |
-
SPECIAL_NOPUNCT = {"kiubo", "quiubo", "que chido", "qué chido", "que buena onda"}
|
| 442 |
-
_Q_VERB_STARTERS = {"eres","estas","estás","puedes","sabes","tienes","quieres","conoces",
|
| 443 |
-
"crees","piensas","dirias","dirías","podrias","podrías","podras","podrás"}
|
| 444 |
-
|
| 445 |
-
#-------------------------------------------------------------------------
|
| 446 |
-
# Punctuation helpers
|
| 447 |
-
#-------------------------------------------------------------------------
|
| 448 |
-
|
| 449 |
-
def needs_question_marks(norm: str) -> bool:
|
| 450 |
-
if "?" in norm: return False
|
| 451 |
-
for w in _Q_STARTERS:
|
| 452 |
-
if norm.startswith(w + " ") or norm == w:
|
| 453 |
-
return True
|
| 454 |
-
return False
|
| 455 |
-
|
| 456 |
-
def needs_exclam(norm: str) -> bool:
|
| 457 |
-
if "!" in norm: return False
|
| 458 |
-
return any(t in norm for t in _EXC_TRIGGERS)
|
| 459 |
-
|
| 460 |
-
#-------------------------------------------------------------------------
|
| 461 |
-
# Greetings detection
|
| 462 |
-
#-------------------------------------------------------------------------
|
| 463 |
-
|
| 464 |
-
def is_slang_greeting(norm: str) -> bool:
|
| 465 |
-
SHORT = {
|
| 466 |
-
"que pex", "que onda", "ke pex", "k pex", "q onda",
|
| 467 |
-
"kiubo", "quiubo", "quiubole", "quiúbole", "kionda", "q onda", "k onda",
|
| 468 |
-
"que rollo", "ke onda", "que show", "que tranza"
|
| 469 |
-
}
|
| 470 |
-
if norm in SHORT: return True
|
| 471 |
-
if re.match(r"^(q|k|ke|que)\s+(pex|onda|rollo|show|tranza)\b", norm): return True
|
| 472 |
-
if re.match(r"^(kiubo|quiubo|quiubole|quiúbole|quiubol[e]?)\b", norm): return True
|
| 473 |
-
return False
|
| 474 |
-
|
| 475 |
-
#-------------------------------------------------------------------------
|
| 476 |
-
# Capitalization & autopunct
|
| 477 |
-
#-------------------------------------------------------------------------
|
| 478 |
-
|
| 479 |
-
def capitalize_spanish(s: str) -> str:
|
| 480 |
-
s = s.strip()
|
| 481 |
-
i = 0
|
| 482 |
-
while i < len(s) and not s[i].isalpha():
|
| 483 |
-
i += 1
|
| 484 |
-
if i < len(s):
|
| 485 |
-
s = s[:i] + s[i].upper() + s[i+1:]
|
| 486 |
-
return s
|
| 487 |
-
|
| 488 |
-
def smart_autopunct(user_text: str) -> str:
|
| 489 |
-
s = user_text.strip()
|
| 490 |
-
if len(s) > 20:
|
| 491 |
-
return capitalize_spanish(s)
|
| 492 |
-
norm = normalize_for_route(s)
|
| 493 |
-
if norm in SPECIAL_NOPUNCT:
|
| 494 |
-
s = re.sub(r'[¿?!¡]+', '', s).strip()
|
| 495 |
-
return capitalize_spanish(s)
|
| 496 |
-
if norm.startswith("y si "):
|
| 497 |
-
s = f"¿{s}?"
|
| 498 |
-
return capitalize_spanish(s)
|
| 499 |
-
if "?" in s and "¿" not in s:
|
| 500 |
-
s = "¿" + s
|
| 501 |
-
return capitalize_spanish(s)
|
| 502 |
-
if "!" in s and "¡" not in s:
|
| 503 |
-
s = "¡" + s
|
| 504 |
-
return capitalize_spanish(s)
|
| 505 |
-
if is_slang_greeting(norm):
|
| 506 |
-
s = f"¡{s}!"
|
| 507 |
-
return capitalize_spanish(s)
|
| 508 |
-
if needs_question_marks(norm):
|
| 509 |
-
s = f"¿{s}?"
|
| 510 |
-
return capitalize_spanish(s)
|
| 511 |
-
toks = norm.split()
|
| 512 |
-
if toks and toks[0] in _Q_VERB_STARTERS:
|
| 513 |
-
s = f"¿{s}?"
|
| 514 |
-
return capitalize_spanish(s)
|
| 515 |
-
if re.match(r"^(me\s+ayudas?|me\s+puedes|podrias?|podras?)\b", norm):
|
| 516 |
-
s = f"¿{s}?"
|
| 517 |
-
return capitalize_spanish(s)
|
| 518 |
-
if needs_exclam(norm):
|
| 519 |
-
s = f"¡{s}!"
|
| 520 |
-
return capitalize_spanish(s)
|
| 521 |
-
return capitalize_spanish(s)
|
| 522 |
-
|
| 523 |
-
#-------------------------------------------------------------------------
|
| 524 |
-
# Prompt builders
|
| 525 |
-
#-------------------------------------------------------------------------
|
| 526 |
-
|
| 527 |
-
def build_prompt_from_cases(domain: str, # "technical" | "social"
|
| 528 |
-
prompt_type: str, # "Zero-shot" | "One-shot" | "Few-shot (3)"
|
| 529 |
-
persona: str, # "Mori Normal" | "Mori Entusiasta"
|
| 530 |
-
question: str,
|
| 531 |
-
context: str | None = None) -> str:
|
| 532 |
-
# Map UI → keys de JSON
|
| 533 |
-
key_map = {
|
| 534 |
-
"Zero-shot": "zero_shot",
|
| 535 |
-
"One-shot": "one_shot",
|
| 536 |
-
"Few-shot (3)": "few_shot_3"
|
| 537 |
-
}
|
| 538 |
-
mode_key = key_map.get(prompt_type, "zero_shot")
|
| 539 |
-
|
| 540 |
-
data = st.session_state.PROMPT_CASES.get(domain, {}).get("modes", {}).get(mode_key)
|
| 541 |
-
|
| 542 |
-
# Ajuste de tono por personalidad (opcional, simple)
|
| 543 |
-
tone = data.get("tone", "")
|
| 544 |
-
|
| 545 |
-
out_fmt = data.get("output_format", "")
|
| 546 |
-
rules = "\n- ".join(data.get("rules", []))
|
| 547 |
-
ctx_line = f"\n- Contexto: {context}" if (context and domain == "technical") else "social"
|
| 548 |
-
|
| 549 |
-
# Ensamblar ejemplos si aplica
|
| 550 |
-
examples = data.get("examples", [])
|
| 551 |
-
ex_str = ""
|
| 552 |
-
if examples:
|
| 553 |
-
parts = []
|
| 554 |
-
for i, ex in enumerate(examples, 1):
|
| 555 |
-
parts.append(f"Ejemplo {i} →\nPregunta: {ex.get('input','')}\nRespuesta: {ex.get('output','')}")
|
| 556 |
-
ex_str = "\n\n" + "\n\n".join(parts) + "\n\nAhora responde:"
|
| 557 |
-
|
| 558 |
-
if 'social' in ctx_line:
|
| 559 |
-
question = smart_autopunct(question)
|
| 560 |
-
question = f"pregunta social:{question}"
|
| 561 |
-
else:
|
| 562 |
-
question = f"pregunta={question}"
|
| 563 |
-
|
| 564 |
-
# Prompt final
|
| 565 |
-
prompt = (
|
| 566 |
-
f"Tarea: {data.get('instruction','')}\n"
|
| 567 |
-
f"Reglas:\n- {rules}{ctx_line}\n"
|
| 568 |
-
f"Estilo: {tone}\n"
|
| 569 |
-
f"Formato de salida: {out_fmt}\n"
|
| 570 |
-
f"{ex_str}\n"
|
| 571 |
-
f"{question}\n"
|
| 572 |
-
)
|
| 573 |
-
|
| 574 |
-
return prompt.strip()
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
#-------------------------------------------------------------------------
|
| 578 |
-
# Seeds & helpers
|
| 579 |
#-------------------------------------------------------------------------
|
| 580 |
|
| 581 |
def set_seeds(seed: int = 42):
|
| 582 |
-
random.seed(seed)
|
| 583 |
-
|
|
|
|
|
|
|
|
|
|
| 584 |
torch.backends.cudnn.deterministic = True
|
| 585 |
torch.backends.cudnn.benchmark = False
|
| 586 |
|
| 587 |
-
# --- Personalidades (solo estilo en prompt; parámetros ya vienen del sidebar) ---
|
| 588 |
-
|
| 589 |
-
def persona_style_prompt(persona: str, domain: str) -> str:
|
| 590 |
-
"""Instrucción breve de estilo según personalidad y dominio (technical/social)."""
|
| 591 |
-
if persona == "Mori Entusiasta":
|
| 592 |
-
return (
|
| 593 |
-
"Responde de forma natural y amistosa, con un toque reflexivo; agrega ejemplos sencillos o analogías cortas. "
|
| 594 |
-
"Evita sonar formal y permite algo de color humano."
|
| 595 |
-
)
|
| 596 |
-
if persona == "Mori Objetivo": # ya no se usa, pero por compatibilidad
|
| 597 |
-
return "Responde en pocas frases, directo y técnico, sin adornos; solo lo esencial."
|
| 598 |
-
return "" # Mori Normal
|
| 599 |
-
|
| 600 |
-
#-------------------------------------------------------------------------
|
| 601 |
-
# Classifier
|
| 602 |
-
#-------------------------------------------------------------------------
|
| 603 |
-
|
| 604 |
-
def classify_context(question, label_classes, model, tokenizer, device):
|
| 605 |
-
model = model.to(device)
|
| 606 |
-
inputs = tokenizer(question, return_tensors="pt", padding=True, truncation=True, max_length=128)
|
| 607 |
-
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 608 |
-
with torch.no_grad():
|
| 609 |
-
outputs = model(**inputs)
|
| 610 |
-
logits = outputs.logits
|
| 611 |
-
pred_intent = torch.argmax(logits, dim=1).item()
|
| 612 |
-
predicted_label = label_classes[pred_intent]
|
| 613 |
-
return predicted_label
|
| 614 |
-
|
| 615 |
-
#-------------------------------------------------------------------------
|
| 616 |
-
# Chatbot response for technical contexts using a Hugging Face model
|
| 617 |
-
#-------------------------------------------------------------------------
|
| 618 |
-
|
| 619 |
-
def technical_asnwer(question, context, model, tokenizer, device, gen_params=None):
|
| 620 |
-
model = model.to(device).eval()
|
| 621 |
-
#persona_name = (gen_params or {}).get("persona", st.session_state.get("persona", "Mori Normal"))
|
| 622 |
-
#style = persona_style_prompt(persona_name, "technical")
|
| 623 |
-
#style_tag = f" Estilo:{style}" if style else ""
|
| 624 |
-
#input_text = f"definir: responde con la definición canónica exacta. Contexto={context} ; Pregunta={question}. {style_tag}"
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
#persona_name = GEN_PARAMS.get("persona", st.session_state.persona)
|
| 628 |
-
#prompt_type = st.session_state.get("prompt_type", "Zero-shot")
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
persona_name = (gen_params or {}).get("persona", st.session_state.get("persona", "Mori Normal"))
|
| 632 |
-
prompt_type = st.session_state.get("prompt_type", "Zero-shot")
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
input_text = build_prompt_from_cases(
|
| 636 |
-
domain="technical",
|
| 637 |
-
prompt_type=prompt_type,
|
| 638 |
-
persona=persona_name,
|
| 639 |
-
question=question,
|
| 640 |
-
context=context # el contexto detectado que ya pasas
|
| 641 |
-
)
|
| 642 |
-
|
| 643 |
-
st.session_state["last_prompt"] = input_text # o prompt
|
| 644 |
-
st.session_state["just_generated"] = True
|
| 645 |
-
#st.rerun()
|
| 646 |
-
enc = tokenizer(input_text, return_tensors="pt", padding=True, truncation=True, max_length=256).to(device)
|
| 647 |
-
|
| 648 |
-
bad_words = ["["]
|
| 649 |
-
bad_ids = [tokenizer(bw, add_special_tokens=False).input_ids for bw in bad_words]
|
| 650 |
-
|
| 651 |
-
# --- construir kwargs de generación, SIN tocar nada por personalidad ---
|
| 652 |
-
max_new = int((gen_params).get("max_new_tokens"))
|
| 653 |
-
min_new = int((gen_params).get("min_tokens")) # <- ahora SIEMPRE min_new_tokens
|
| 654 |
-
no_repeat = int((gen_params).get("no_repeat_ngram_size"))
|
| 655 |
-
rep_pen = float((gen_params).get("repetition_penalty"))
|
| 656 |
-
mode = (gen_params or {}).get("mode", "beam")
|
| 657 |
-
|
| 658 |
-
if mode == "sampling":
|
| 659 |
-
temperature = float((gen_params or {}).get("temperature", 0.7))
|
| 660 |
-
top_p = float((gen_params or {}).get("top_p", 0.9))
|
| 661 |
-
kwargs = dict(
|
| 662 |
-
do_sample=True,
|
| 663 |
-
num_beams=1,
|
| 664 |
-
temperature=max(0.1, temperature),
|
| 665 |
-
top_p=min(1.0, max(0.5, top_p)),
|
| 666 |
-
max_new_tokens=max_new,
|
| 667 |
-
min_new_tokens=max(0, min_new), # 👈 consistente
|
| 668 |
-
no_repeat_ngram_size=no_repeat,
|
| 669 |
-
repetition_penalty=max(1.0, rep_pen),
|
| 670 |
-
bad_words_ids=bad_ids,
|
| 671 |
-
eos_token_id=tokenizer.eos_token_id,
|
| 672 |
-
pad_token_id=tokenizer.pad_token_id,
|
| 673 |
-
)
|
| 674 |
-
else:
|
| 675 |
-
num_beams = max(2, int((gen_params or {}).get("num_beams", 4)))
|
| 676 |
-
length_penalty = float((gen_params or {}).get("length_penalty", 1.0))
|
| 677 |
-
kwargs = dict(
|
| 678 |
-
do_sample=False,
|
| 679 |
-
num_beams=num_beams,
|
| 680 |
-
length_penalty=length_penalty,
|
| 681 |
-
max_new_tokens=max_new,
|
| 682 |
-
min_new_tokens=max(0, min_new), # 👈 también aquí (no min_length)
|
| 683 |
-
no_repeat_ngram_size=no_repeat,
|
| 684 |
-
repetition_penalty=max(1.0, rep_pen),
|
| 685 |
-
bad_words_ids=bad_ids,
|
| 686 |
-
eos_token_id=tokenizer.eos_token_id,
|
| 687 |
-
pad_token_id=tokenizer.pad_token_id,
|
| 688 |
-
)
|
| 689 |
-
|
| 690 |
-
out_ids = model.generate(
|
| 691 |
-
input_ids=enc["input_ids"], attention_mask=enc["attention_mask"], **kwargs
|
| 692 |
-
)
|
| 693 |
-
text = tokenizer.decode(out_ids[0], skip_special_tokens=True)
|
| 694 |
-
|
| 695 |
-
if persona_name == "Mori Normal":
|
| 696 |
-
text = truncate_sentences(text, max_sentences=1)
|
| 697 |
-
|
| 698 |
-
st.session_state["last_response"] = text
|
| 699 |
-
#st.rerun()
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
return polish_spanish(text)
|
| 703 |
-
|
| 704 |
-
#-------------------------------------------------------------------------
|
| 705 |
-
# Chatbot response for social contexts using a Hugging Face model
|
| 706 |
-
#-------------------------------------------------------------------------
|
| 707 |
-
|
| 708 |
-
def social_asnwer(question, model, tokenizer, device, gen_params=None, block_web=True):
|
| 709 |
-
|
| 710 |
-
model = model.to(device).eval()
|
| 711 |
-
persona_name = (gen_params or {}).get("persona", st.session_state.get("persona", "Mori Normal"))
|
| 712 |
-
prompt_type = st.session_state.get("prompt_type", "Zero-shot")
|
| 713 |
-
prompt = build_prompt_from_cases(
|
| 714 |
-
domain="social",
|
| 715 |
-
prompt_type=prompt_type,
|
| 716 |
-
persona=persona_name,
|
| 717 |
-
question=question)
|
| 718 |
-
|
| 719 |
-
st.session_state["last_prompt"] = prompt # o prompt
|
| 720 |
-
st.session_state["just_generated"] = True
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
enc = tokenizer(prompt, return_tensors="pt", padding=True, truncation=True, max_length=192).to(device)
|
| 724 |
-
|
| 725 |
-
bad_words = ["[", "Thanks", "thank you"]
|
| 726 |
-
if block_web:
|
| 727 |
-
bad_words += ["website", "http", "www", ".com"]
|
| 728 |
-
bad_ids = [tokenizer(bw, add_special_tokens=False).input_ids for bw in bad_words]
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
max_new = int((gen_params).get("max_new_tokens"))
|
| 732 |
-
min_tokens = int((gen_params).get("min_tokens"))
|
| 733 |
-
min_length = int(enc["input_ids"].shape[1]) + max(0, min_tokens)
|
| 734 |
-
no_repeat = int((gen_params).get("no_repeat_ngram_size"))
|
| 735 |
-
rep_pen = float((gen_params).get("repetition_penalty"))
|
| 736 |
-
mode = (gen_params or {}).get("mode", "beam")
|
| 737 |
-
|
| 738 |
-
if mode == "sampling":
|
| 739 |
-
temperature = float((gen_params or {}).get("temperature", 0.7))
|
| 740 |
-
top_p = float((gen_params or {}).get("top_p", 0.9))
|
| 741 |
-
kwargs = dict(
|
| 742 |
-
do_sample=True, num_beams=1,
|
| 743 |
-
temperature=max(0.1, temperature),
|
| 744 |
-
top_p=min(1.0, max(0.5, top_p)),
|
| 745 |
-
max_new_tokens=max_new,
|
| 746 |
-
#min_length=min_length,
|
| 747 |
-
min_new_tokens=max(0, min_tokens),
|
| 748 |
-
no_repeat_ngram_size=no_repeat,
|
| 749 |
-
repetition_penalty=max(1.0, rep_pen),
|
| 750 |
-
bad_words_ids=bad_ids,
|
| 751 |
-
eos_token_id=tokenizer.eos_token_id,
|
| 752 |
-
pad_token_id=tokenizer.pad_token_id,
|
| 753 |
-
)
|
| 754 |
-
else:
|
| 755 |
-
num_beams = max(2, int((gen_params or {}).get("num_beams", 4)))
|
| 756 |
-
length_penalty = float((gen_params or {}).get("length_penalty", 1.0))
|
| 757 |
-
kwargs = dict(
|
| 758 |
-
do_sample=False, num_beams=num_beams, length_penalty=length_penalty,
|
| 759 |
-
max_new_tokens=max_new,
|
| 760 |
-
#min_length=min_length,
|
| 761 |
-
min_new_tokens=max(0, min_tokens), # <- usar min_new_tokens
|
| 762 |
-
no_repeat_ngram_size=no_repeat,
|
| 763 |
-
repetition_penalty=max(1.0, rep_pen),
|
| 764 |
-
bad_words_ids=bad_ids,
|
| 765 |
-
eos_token_id=tokenizer.eos_token_id,
|
| 766 |
-
pad_token_id=tokenizer.pad_token_id,
|
| 767 |
-
|
| 768 |
-
)
|
| 769 |
-
|
| 770 |
-
out_ids = model.generate(
|
| 771 |
-
input_ids=enc["input_ids"], attention_mask=enc["attention_mask"], **kwargs
|
| 772 |
-
)
|
| 773 |
-
text = tokenizer.decode(out_ids[0], skip_special_tokens=True)
|
| 774 |
-
if persona_name == "Mori Normal":
|
| 775 |
-
text = truncate_sentences(text, max_sentences=2)
|
| 776 |
-
#text = anti_echo(text, question)
|
| 777 |
-
text = polish_spanish(text)
|
| 778 |
-
text = capitalize_spanish(text)
|
| 779 |
-
|
| 780 |
-
st.session_state["last_response"] = text
|
| 781 |
-
#st.rerun()
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
return text
|
| 785 |
-
|
| 786 |
-
#-------------------------------------------------------------------------
|
| 787 |
-
# Rule overrides
|
| 788 |
-
#-------------------------------------------------------------------------
|
| 789 |
-
|
| 790 |
-
def rule_intent_override(user_text: str, predicted_label: str) -> str:
|
| 791 |
-
n = normalize_for_route(user_text)
|
| 792 |
-
if re.fullmatch(r"(motivame|motiva|animame|animo|ayudame|que tranza|qué tranza|que tranza mori|qué tranza mori)", n):
|
| 793 |
-
return "social"
|
| 794 |
-
return predicted_label
|
| 795 |
-
|
| 796 |
-
#-------------------------------------------------------------------------
|
| 797 |
-
# Router
|
| 798 |
-
#-------------------------------------------------------------------------
|
| 799 |
-
|
| 800 |
-
def contextual_asnwer(question, label_classes, context_model, cont_tok,
|
| 801 |
-
tec_model, tec_tok, soc_model, soc_tok, device, gen_params=None, block_web=True):
|
| 802 |
-
context = classify_context(question, label_classes, context_model, cont_tok, device)
|
| 803 |
-
context = rule_intent_override(question, context)
|
| 804 |
-
|
| 805 |
-
context_icons = {
|
| 806 |
-
"social": "💬", "modelos": "🔧", "evaluación": "📏", "optimización": "⚙️",
|
| 807 |
-
"visualización": "📈", "aprendizaje": "🧠", "vida digital": "🧑💻",
|
| 808 |
-
"estadística": "📊", "infraestructura": "🖥", "datos": "📂", "transformación digital": "🌀"}
|
| 809 |
-
icon = context_icons.get(context, "🧠")
|
| 810 |
-
|
| 811 |
-
if gen_params and "seed" in gen_params:
|
| 812 |
-
set_seeds(gen_params["seed"])
|
| 813 |
-
|
| 814 |
-
if context == "social":
|
| 815 |
-
# Nota: por resultados del análisis, RAG social no aporta (dataset muy redundante).
|
| 816 |
-
# Puedes activarlo en el futuro si amplías la diversidad.
|
| 817 |
-
return social_asnwer(question, soc_model, soc_tok, device, gen_params=gen_params, block_web=block_web), context
|
| 818 |
-
else:
|
| 819 |
-
# Técnico: si el usuario activó RAG, lo usamos
|
| 820 |
-
use_rag = st.session_state.get("use_rag", False)
|
| 821 |
-
if use_rag:
|
| 822 |
-
# Carga única de E5+FAISS (cache_resource)
|
| 823 |
-
dev_str = "cuda" if torch.cuda.is_available() else "cpu"
|
| 824 |
-
e5, index, metas = load_rag_assets(dev_str)
|
| 825 |
-
if e5 is None:
|
| 826 |
-
# Fallback si no se encuentra la base RAG
|
| 827 |
-
return technical_asnwer(question, context, tec_model, tec_tok, device, gen_params=gen_params), context
|
| 828 |
-
resp = technical_answer_rag(
|
| 829 |
-
question, tec_model, tec_tok, device, gen_params,
|
| 830 |
-
e5=e5, index=index, metas=metas,
|
| 831 |
-
k=st.session_state.get("rag_k", 5), sim_threshold=0.40
|
| 832 |
-
)
|
| 833 |
-
return resp, context
|
| 834 |
-
else:
|
| 835 |
-
return technical_asnwer(question, context, tec_model, tec_tok, device, gen_params=gen_params), context
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
# ============================
|
| 840 |
-
# RAG assets (carga única)
|
| 841 |
-
# ============================
|
| 842 |
-
@st.cache_resource
|
| 843 |
-
def load_rag_assets(device_str: str = "cpu"):
|
| 844 |
-
"""
|
| 845 |
-
Carga todo el stack RAG: modelo E5 + FAISS + metadatos desde Hugging Face.
|
| 846 |
-
Se ejecuta una sola vez gracias a st.cache_resource.
|
| 847 |
-
"""
|
| 848 |
-
REPO_ID = "tecuhtli/Mori_FAISS_Full" # Ajusta si cambia el repo
|
| 849 |
-
TOKEN = os.getenv("HF_TOKEN") # Guardado en Settings → Repository secrets
|
| 850 |
-
if not TOKEN:
|
| 851 |
-
st.error("❌ No se encontró HF_TOKEN en los Secrets del Space.")
|
| 852 |
-
st.stop()
|
| 853 |
-
|
| 854 |
-
st.info("📥 Cargando base RAG desde Hugging Face...")
|
| 855 |
-
try:
|
| 856 |
-
# Descarga los archivos privados del dataset
|
| 857 |
-
faiss_path = hf_hub_download(repo_id=REPO_ID, filename="mori.faiss", repo_type="dataset", token=TOKEN)
|
| 858 |
-
ids_path = hf_hub_download(repo_id=REPO_ID, filename="mori_ids.npy", repo_type="dataset", token=TOKEN)
|
| 859 |
-
meta_path = hf_hub_download(repo_id=REPO_ID, filename="mori_metas.json", repo_type="dataset", token=TOKEN)
|
| 860 |
-
|
| 861 |
-
# Carga los datos locales (ya descargados)
|
| 862 |
-
index = faiss.read_index(faiss_path)
|
| 863 |
-
ids = np.load(ids_path, allow_pickle=True)
|
| 864 |
-
with open(meta_path, "r", encoding="utf-8") as f:
|
| 865 |
-
metas = json.load(f)
|
| 866 |
-
|
| 867 |
-
# Carga el modelo de embeddings
|
| 868 |
-
e5 = SentenceTransformer("intfloat/multilingual-e5-base", device=device_str)
|
| 869 |
-
|
| 870 |
-
st.success(f"✅ RAG cargado: {index.ntotal} vectores listos.")
|
| 871 |
-
return e5, index, metas
|
| 872 |
-
|
| 873 |
-
except Exception as e:
|
| 874 |
-
st.error(f"⚠️ Error al cargar el índice RAG: {e}")
|
| 875 |
-
return None, None, None
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
def rag_retrieve(e5, index, metas, user_text: str, k: int = 5):
|
| 879 |
-
"""Top-k por similitud coseno (IP + embeddings normalizados)."""
|
| 880 |
-
if e5 is None or index is None or metas is None or index.ntotal == 0:
|
| 881 |
-
return []
|
| 882 |
-
qv = e5.encode([f"query: {user_text}"], normalize_embeddings=True,
|
| 883 |
-
convert_to_numpy=True).astype("float32")
|
| 884 |
-
k = max(1, min(int(k), index.ntotal))
|
| 885 |
-
scores, idxs = index.search(qv, k)
|
| 886 |
-
out = []
|
| 887 |
-
for rank, (s, i) in enumerate(zip(scores[0], idxs[0]), 1):
|
| 888 |
-
if i == -1:
|
| 889 |
-
continue
|
| 890 |
-
m = metas[i]
|
| 891 |
-
out.append({
|
| 892 |
-
"rank": rank, "score": float(s),
|
| 893 |
-
"id": m.get("id",""),
|
| 894 |
-
"canonical_term": m.get("canonical_term",""),
|
| 895 |
-
"context": m.get("context",""),
|
| 896 |
-
"input": m.get("input",""),
|
| 897 |
-
"output": m.get("output",""),
|
| 898 |
-
})
|
| 899 |
-
return out
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
def _format_evidence(passages):
|
| 903 |
-
lines = []
|
| 904 |
-
for p in passages:
|
| 905 |
-
lines.append(
|
| 906 |
-
f"[{p['rank']}] term='{p['canonical_term']}' ctx='{p['context']}'\n"
|
| 907 |
-
f" Q: {p['input']}\n"
|
| 908 |
-
f" A: {p['output']}"
|
| 909 |
-
)
|
| 910 |
-
return "\n".join(lines)
|
| 911 |
-
|
| 912 |
-
|
| 913 |
-
def build_rag_prompt_technical(base_prompt: str, user_text: str, passages):
|
| 914 |
-
ev_lines = []
|
| 915 |
-
for p in passages:
|
| 916 |
-
ev_lines.append(
|
| 917 |
-
f"[{p['rank']}] term='{p.get('canonical_term','')}' ctx='{p.get('context','')}'\n"
|
| 918 |
-
f"input: {p.get('input','')}\n"
|
| 919 |
-
f"output: {p.get('output','')}"
|
| 920 |
-
)
|
| 921 |
-
|
| 922 |
-
ev_block = "\n".join(ev_lines)
|
| 923 |
-
rag_rules = (
|
| 924 |
-
"\n\n[ Modo RAG ]\n"
|
| 925 |
-
"- Usa EXCLUSIVAMENTE la información relevante de las evidencias.\n"
|
| 926 |
-
"- Si algo no aparece en las evidencias, dilo explícitamente.\n"
|
| 927 |
-
"- Cita las evidencias con [n] (ej. [1], [3]).\n"
|
| 928 |
-
)
|
| 929 |
-
return f"{base_prompt.strip()}\n{rag_rules}\nEVIDENCIAS:\n{ev_block}\n"
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
def get_bad_words_ids(tok):
|
| 933 |
-
bad = []
|
| 934 |
-
for sym in ["[", "]"]:
|
| 935 |
-
ids = tok.encode(sym, add_special_tokens=False) # p.ej. [784]
|
| 936 |
-
if ids and all(isinstance(t, int) and t >= 0 for t in ids):
|
| 937 |
-
bad.append(ids) # [[784]]
|
| 938 |
-
return bad
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
# --- FUNCIÓN ACTUALIZADA: Prompt Engineering + RAG en capas separadas ---
|
| 942 |
-
def technical_answer_rag(
|
| 943 |
-
question, tec_model, tec_tok, device, gen_params,
|
| 944 |
-
e5, index, metas, k=5, sim_threshold=0.40
|
| 945 |
-
):
|
| 946 |
-
"""Orquesta retrieval + (base_prompt de Prompt Engineering) + inyección RAG + generación."""
|
| 947 |
-
passages = rag_retrieve(e5, index, metas, question, k=k)
|
| 948 |
-
if not passages:
|
| 949 |
-
return "No encontré evidencias relevantes para responder con certeza. ¿Puedes dar más contexto?"
|
| 950 |
-
|
| 951 |
-
# 1) Prompt Engineering (ESTILO/ROL/PERSONA) → base_prompt
|
| 952 |
-
persona_name = (gen_params or {}).get("persona", st.session_state.get("persona", "Mori Normal"))
|
| 953 |
-
prompt_type = st.session_state.get("prompt_type", "Zero-shot")
|
| 954 |
-
base_prompt = build_prompt_from_cases( # <<-- tu función existente de Prompt Engineering
|
| 955 |
-
domain="technical",
|
| 956 |
-
prompt_type="Zero-shot",
|
| 957 |
-
persona=persona_name,
|
| 958 |
-
question=question,
|
| 959 |
-
context="RAG" # etiqueta informativa
|
| 960 |
-
)
|
| 961 |
-
|
| 962 |
-
# 2) RAG (CONTENIDO/EVIDENCIAS) → se inyecta SOBRE el base_prompt
|
| 963 |
-
prompt = build_rag_prompt_technical("", question, passages)
|
| 964 |
-
|
| 965 |
-
# 3) UI: guardar prompt y marcar baja similitud si aplica
|
| 966 |
-
max_sim = passages[0]["score"]
|
| 967 |
-
if max_sim < sim_threshold:
|
| 968 |
-
prompt = "⚠️ Baja similitud con la base; podría faltar contexto.\n\n" + prompt
|
| 969 |
-
st.session_state["last_prompt"] = prompt
|
| 970 |
-
st.session_state["just_generated"] = True
|
| 971 |
-
|
| 972 |
-
# 4) Generación
|
| 973 |
-
enc = tec_tok(prompt, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device)
|
| 974 |
-
|
| 975 |
-
bad_ids = get_bad_words_ids(tec_tok) # opcional; puedes quitarlo si quieres permitir corchetes libres
|
| 976 |
-
|
| 977 |
-
max_new = int(gen_params.get("max_new_tokens"))
|
| 978 |
-
min_new = int(gen_params.get("min_tokens"))
|
| 979 |
-
no_repeat = int(gen_params.get("no_repeat_ngram_size"))
|
| 980 |
-
rep_pen = float(gen_params.get("repetition_penalty"))
|
| 981 |
-
mode = gen_params.get("mode", "beam")
|
| 982 |
-
|
| 983 |
-
# IDs de control (por si el tokenizer no los trae definidos)
|
| 984 |
-
eos_id = tec_tok.eos_token_id or tec_tok.convert_tokens_to_ids("</s>")
|
| 985 |
-
pad_id = tec_tok.pad_token_id or eos_id
|
| 986 |
-
|
| 987 |
-
if mode == "sampling":
|
| 988 |
-
temperature = float(gen_params.get("temperature", 0.7))
|
| 989 |
-
top_p = float(gen_params.get("top_p", 0.9))
|
| 990 |
-
kwargs = dict(
|
| 991 |
-
do_sample=True, num_beams=1,
|
| 992 |
-
temperature=max(0.1, temperature),
|
| 993 |
-
top_p=min(1.0, max(0.5, top_p)),
|
| 994 |
-
max_new_tokens=max_new,
|
| 995 |
-
min_new_tokens=max(0, min_new),
|
| 996 |
-
no_repeat_ngram_size=no_repeat,
|
| 997 |
-
repetition_penalty=max(1.0, rep_pen),
|
| 998 |
-
eos_token_id=eos_id,
|
| 999 |
-
pad_token_id=pad_id,
|
| 1000 |
-
)
|
| 1001 |
-
else:
|
| 1002 |
-
num_beams = max(2, int(gen_params.get("num_beams", 4)))
|
| 1003 |
-
length_penalty = float(gen_params.get("length_penalty", 1.0))
|
| 1004 |
-
kwargs = dict(
|
| 1005 |
-
do_sample=False, num_beams=num_beams, length_penalty=length_penalty,
|
| 1006 |
-
max_new_tokens=max_new,
|
| 1007 |
-
min_new_tokens=max(0, min_new),
|
| 1008 |
-
no_repeat_ngram_size=no_repeat,
|
| 1009 |
-
repetition_penalty=max(1.0, rep_pen),
|
| 1010 |
-
eos_token_id=eos_id,
|
| 1011 |
-
pad_token_id=pad_id,
|
| 1012 |
-
)
|
| 1013 |
-
|
| 1014 |
-
if bad_ids: # solo si existen; evita [[[...]]] y errores de validación
|
| 1015 |
-
kwargs["bad_words_ids"] = bad_ids
|
| 1016 |
-
|
| 1017 |
-
out_ids = tec_model.generate(**enc, **kwargs)
|
| 1018 |
-
text = tec_tok.decode(out_ids[0], skip_special_tokens=True)
|
| 1019 |
-
|
| 1020 |
-
if persona_name == "Mori Normal":
|
| 1021 |
-
text = truncate_sentences(text, max_sentences=1)
|
| 1022 |
-
text = polish_spanish(text)
|
| 1023 |
-
|
| 1024 |
-
st.session_state["last_response"] = text
|
| 1025 |
-
return text
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 1029 |
#***************************************************************************
|
| 1030 |
# MAIN
|
| 1031 |
#***************************************************************************
|
| 1032 |
|
| 1033 |
-
if __name__ ==
|
|
|
|
|
|
|
| 1034 |
|
| 1035 |
# --- Estado que debe persistir en todos los reruns ---
|
| 1036 |
ss = st.session_state
|
|
@@ -1038,57 +181,48 @@ if __name__ == '__main__':
|
|
| 1038 |
ss.setdefault("last_prompt", "")
|
| 1039 |
ss.setdefault("last_response", "")
|
| 1040 |
ss.setdefault("just_generated", False)
|
| 1041 |
-
|
| 1042 |
# Sidebar (control total)
|
| 1043 |
GEN_PARAMS = sidebar_params()
|
| 1044 |
-
GEN_PARAMS["persona"] =
|
| 1045 |
-
|
| 1046 |
-
# Setting historial for the current user
|
| 1047 |
-
#if "historial" not in st.session_state:
|
| 1048 |
-
# st.session_state.historial = []
|
| 1049 |
|
| 1050 |
# Assigning a new ID to the current user
|
| 1051 |
-
if "user_id" not in
|
| 1052 |
-
|
| 1053 |
|
| 1054 |
-
# Loading classifier encoder classes:
|
| 1055 |
-
labels_path = hf_hub_download(repo_id="tecuhtli/mori-context-model", filename="context_labels.pkl", use_auth_token=HF_TOKEN)
|
| 1056 |
-
label_classes = joblib.load(labels_path)
|
| 1057 |
|
| 1058 |
-
# Loading Saved Models
|
| 1059 |
-
# Modelo Contexto
|
| 1060 |
-
context_model = AutoModelForSequenceClassification.from_pretrained("tecuhtli/mori-context-model", use_auth_token=HF_TOKEN)
|
| 1061 |
-
cont_tok = AutoTokenizer.from_pretrained("tecuhtli/mori-context-model", use_auth_token=HF_TOKEN)
|
| 1062 |
-
|
| 1063 |
# Modelo Técnico
|
| 1064 |
-
|
| 1065 |
-
|
|
|
|
|
|
|
|
|
|
| 1066 |
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
soc_model = AutoModelForSeq2SeqLM.from_pretrained("tecuhtli/mori-social-model", use_auth_token=HF_TOKEN)
|
| 1070 |
|
| 1071 |
-
|
| 1072 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1073 |
|
| 1074 |
-
# Defining Mori's Presentation
|
| 1075 |
-
st.title("🤖 Mori - Tu Asistente Personal 🎓")
|
| 1076 |
-
st.caption("🙋🏽 Puedes preguntarme conceptos técnicos como visualización, limpieza, BI, etc.")
|
| 1077 |
-
st.caption("🙇🏽 Por el momento, solo puedo contestar preguntas simples como:")
|
| 1078 |
-
st.caption("➡️ ¿Cómo estás? ¿Qué son?, Explícame algo, Define algo, ¿Para qué sirven?")
|
| 1079 |
-
st.caption("🦾 Me siguen mejorando, más sobre mí en: [hazutecuhtli.github.io](https://github.com/hazutecuhtli/Mori_Development)")
|
| 1080 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 1081 |
-
|
|
|
|
| 1082 |
|
| 1083 |
# 🔁 Limpieza segura antes del formulario
|
| 1084 |
-
if
|
| 1085 |
-
if "entrada" in
|
| 1086 |
-
del
|
| 1087 |
|
| 1088 |
# 🧠 Flash de respuesta (la guardamos, pero la mostraremos después del form)
|
| 1089 |
-
_flash =
|
| 1090 |
-
|
| 1091 |
|
|
|
|
| 1092 |
with st.form("formulario_mori"):
|
| 1093 |
user_question = st.text_area("📝 Escribe tu pregunta aquí", key="entrada", height=100)
|
| 1094 |
submitted = st.form_submit_button("Responder")
|
|
@@ -1097,27 +231,42 @@ if __name__ == '__main__':
|
|
| 1097 |
if not user_question:
|
| 1098 |
st.info("Mori: ¿Podrías repetir eso? No entendí bien 😅")
|
| 1099 |
else:
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1105 |
|
| 1106 |
# 🧠 Guarda historial
|
| 1107 |
hora_actual = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 1108 |
-
|
| 1109 |
|
| 1110 |
hora_actual = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 1111 |
-
|
| 1112 |
|
| 1113 |
# 💾 Guarda conversación
|
| 1114 |
-
saving_interaction(user_question, response,
|
| 1115 |
|
| 1116 |
# 🟩 Guarda respuesta para mostrar después del rerun
|
| 1117 |
-
|
| 1118 |
|
| 1119 |
# 🧼 Limpieza del textarea en el próximo ciclo
|
| 1120 |
-
|
| 1121 |
|
| 1122 |
# ♻️ Forzar refresh (sidebar verá el nuevo prompt)
|
| 1123 |
st.rerun()
|
|
@@ -1128,27 +277,19 @@ if __name__ == '__main__':
|
|
| 1128 |
if _flash:
|
| 1129 |
st.success(_flash)
|
| 1130 |
|
| 1131 |
-
# Mostrar último mensaje (opcional, arriba de todo)
|
| 1132 |
-
#if st.session_state.get("just_generated"):
|
| 1133 |
-
# if st.session_state["last_response"]:
|
| 1134 |
-
# st.success(st.session_state["last_response"])
|
| 1135 |
-
# st.session_state["just_generated"] = False
|
| 1136 |
-
|
| 1137 |
-
# ... formulario y lógica de respuesta ...
|
| 1138 |
-
|
| 1139 |
# 🔁 Historial con estilo chat y contenedor con scroll
|
| 1140 |
-
if
|
| 1141 |
st.markdown("---")
|
| 1142 |
|
| 1143 |
# 💾 Botón de descarga arriba del historial
|
| 1144 |
lineas = []
|
| 1145 |
-
for msg in reversed(
|
| 1146 |
-
if len(msg) ==
|
|
|
|
|
|
|
|
|
|
| 1147 |
autor, texto, hora = msg
|
| 1148 |
lineas.append(f"[{hora}] {autor}: {texto}")
|
| 1149 |
-
else:
|
| 1150 |
-
autor, texto = msg
|
| 1151 |
-
lineas.append(f"{autor}: {texto}")
|
| 1152 |
texto_chat = "\n\n".join(lineas)
|
| 1153 |
|
| 1154 |
st.download_button(
|
|
@@ -1175,11 +316,11 @@ if __name__ == '__main__':
|
|
| 1175 |
unsafe_allow_html=True
|
| 1176 |
)
|
| 1177 |
|
| 1178 |
-
for msg in reversed(
|
| 1179 |
-
if len(msg) ==
|
| 1180 |
-
autor, texto,
|
| 1181 |
else:
|
| 1182 |
-
autor, texto = msg
|
| 1183 |
|
| 1184 |
if autor == "Tú":
|
| 1185 |
st.markdown(
|
|
@@ -1225,7 +366,3 @@ if __name__ == '__main__':
|
|
| 1225 |
)
|
| 1226 |
|
| 1227 |
st.markdown("</div>", unsafe_allow_html=True)
|
| 1228 |
-
|
| 1229 |
-
#***************************************************************************
|
| 1230 |
-
# FIN
|
| 1231 |
-
#***************************************************************************
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""Mori – Inferencia Técnica (estable, UTF-8, con opción RAG ON/OFF)"""
|
| 3 |
+
#=====================================================================================
|
| 4 |
+
# Importing Libraries ===============================================================
|
| 5 |
+
#=====================================================================================
|
| 6 |
+
import os, warnings, json, random, uuid, csv
|
| 7 |
import numpy as np
|
|
|
|
| 8 |
import streamlit as st
|
| 9 |
import datetime as dt
|
| 10 |
from pathlib import Path
|
| 11 |
+
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
| 12 |
+
from Mori_TechnicalPrompts import answer_with_mori_rag, answer_with_mori_plain
|
| 13 |
+
import torch
|
| 14 |
from huggingface_hub import hf_hub_download, login
|
| 15 |
from sentence_transformers import SentenceTransformer # RAG embeddings
|
|
|
|
| 16 |
#***************************************************************************
|
| 17 |
#Setting up variables
|
| 18 |
#***************************************************************************
|
|
|
|
| 24 |
#***************************************************************************
|
| 25 |
REPO_ID = "tecuhtli/Mori_FAISS_Full"
|
| 26 |
|
| 27 |
+
|
| 28 |
#***************************************************************************
|
| 29 |
# Sidebar controls for generation params
|
| 30 |
#***************************************************************************
|
| 31 |
|
| 32 |
def sidebar_params():
|
|
|
|
| 33 |
with st.sidebar:
|
| 34 |
st.title("🎮 Personalidad (FLAN-T5)")
|
| 35 |
|
| 36 |
ss = st.session_state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
# Estado inicial
|
| 39 |
+
ss.setdefault("show_llm_controls", False)
|
| 40 |
+
ss.setdefault("persona", "Mori Exacto")
|
| 41 |
ss.setdefault("mode", "beam") # 'beam' | 'sampling'
|
| 42 |
ss.setdefault("max_new", 128)
|
| 43 |
ss.setdefault("min_tok", 16)
|
|
|
|
| 47 |
ss.setdefault("temperature", 0.7)
|
| 48 |
ss.setdefault("top_p", 0.9)
|
| 49 |
ss.setdefault("repetition_penalty", 1.0)
|
|
|
|
| 50 |
|
| 51 |
# ----------------------------
|
| 52 |
# Personalidad (presets)
|
|
|
|
| 55 |
c1, c2 = st.columns(2)
|
| 56 |
|
| 57 |
with c1:
|
| 58 |
+
if st.button("Exacto 🧐", use_container_width=True):
|
| 59 |
+
ss.update({"persona": "exacto"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
st.rerun()
|
| 61 |
|
| 62 |
with c2:
|
| 63 |
+
if st.button("Creativo 😃", use_container_width=True):
|
| 64 |
+
ss.update({"persona": "creativo"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
st.rerun()
|
| 66 |
|
| 67 |
st.caption(f"Personalidad actual: **{ss.persona}**")
|
| 68 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
st.markdown("---")
|
| 70 |
+
st.title("👀 RAG")
|
| 71 |
ss.setdefault("use_rag", True)
|
| 72 |
+
ss.setdefault("rag_k", 1)
|
| 73 |
+
ss.use_rag = st.checkbox(
|
| 74 |
+
"Usar RAG (FAISS + One-Shot)",
|
| 75 |
+
value=ss.use_rag,
|
| 76 |
+
help="Recupera evidencias de la base FAISS de Mori en Hugging Face y las usa en el prompt."
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
st.markdown("---")
|
| 80 |
st.title("🧾 Vista previa del Prompt")
|
| 81 |
+
|
| 82 |
+
if "last_prompt" in ss and ss["last_prompt"]:
|
| 83 |
with st.expander("Mostrar prompt generado"):
|
| 84 |
st.text_area(
|
| 85 |
"Prompt actual:",
|
| 86 |
+
ss["last_prompt"],
|
| 87 |
height=200,
|
| 88 |
disabled=True
|
| 89 |
)
|
| 90 |
else:
|
| 91 |
+
st.caption("👉 Aún no se ha generado ningún prompt.")
|
| 92 |
|
| 93 |
# ----------------------------
|
| 94 |
# Construir diccionario de parámetros
|
|
|
|
| 101 |
"no_repeat_ngram_size": int(ss.no_repeat),
|
| 102 |
"repetition_penalty": float(ss.repetition_penalty),
|
| 103 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
return params
|
| 106 |
|
|
|
|
|
|
|
|
|
|
| 107 |
#***************************************************************************
|
| 108 |
# Functions
|
| 109 |
#***************************************************************************
|
| 110 |
|
| 111 |
+
# Function to clean the question field (por si luego lo quieres usar en un botón)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
def limpiar_input():
|
| 113 |
st.session_state["entrada"] = ""
|
| 114 |
|
|
|
|
| 117 |
return Path("Models") / folder_name
|
| 118 |
|
| 119 |
# Function to save user interaction
|
| 120 |
+
def saving_interaction(question, response, user_id, use_of_rag, bot_personality):
|
| 121 |
+
"""
|
| 122 |
+
Guarda la interacción en CSV y JSONL para análisis posterior.
|
| 123 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
timestamp = dt.datetime.now().isoformat()
|
| 125 |
stats_dir = Path("Statistics")
|
| 126 |
stats_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 128 |
archivo_csv = stats_dir / "conversaciones_log.csv"
|
| 129 |
existe_csv = archivo_csv.exists()
|
| 130 |
|
| 131 |
+
# CSV
|
| 132 |
with open(archivo_csv, mode="a", encoding="utf-8", newline="") as f_csv:
|
| 133 |
writer = csv.writer(f_csv)
|
| 134 |
if not existe_csv:
|
| 135 |
+
writer.writerow(["timestamp", "user_id", "pregunta", "respuesta", "rag", "personality"])
|
| 136 |
+
writer.writerow([timestamp, user_id, question, response, use_of_rag, bot_personality])
|
| 137 |
|
| 138 |
+
# JSONL
|
| 139 |
archivo_jsonl = stats_dir / "conversaciones_log.jsonl"
|
| 140 |
with open(archivo_jsonl, mode="a", encoding="utf-8") as f_jsonl:
|
| 141 |
registro = {
|
| 142 |
"timestamp": timestamp,
|
| 143 |
"user_id": user_id,
|
|
|
|
| 144 |
"pregunta": question,
|
| 145 |
+
"respuesta": response,
|
| 146 |
+
"uso_rag": use_of_rag,
|
| 147 |
+
"personality": bot_personality
|
| 148 |
+
}
|
| 149 |
f_jsonl.write(json.dumps(registro, ensure_ascii=False) + "\n")
|
| 150 |
|
| 151 |
# Function to load models within the huggingface repositories space
|
| 152 |
@st.cache_resource
|
| 153 |
+
def load_remote_model(repo_id: str, token: str = None):
|
| 154 |
+
tokenizer = AutoTokenizer.from_pretrained(repo_id, token=token)
|
| 155 |
+
model = AutoModelForSeq2SeqLM.from_pretrained(repo_id, token=token)
|
|
|
|
| 156 |
return model, tokenizer
|
|
|
|
| 157 |
#-------------------------------------------------------------------------
|
| 158 |
+
# Seeds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
#-------------------------------------------------------------------------
|
| 160 |
|
| 161 |
def set_seeds(seed: int = 42):
|
| 162 |
+
random.seed(seed)
|
| 163 |
+
np.random.seed(seed)
|
| 164 |
+
torch.manual_seed(seed)
|
| 165 |
+
if torch.cuda.is_available():
|
| 166 |
+
torch.cuda.manual_seed_all(seed)
|
| 167 |
torch.backends.cudnn.deterministic = True
|
| 168 |
torch.backends.cudnn.benchmark = False
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
#***************************************************************************
|
| 171 |
# MAIN
|
| 172 |
#***************************************************************************
|
| 173 |
|
| 174 |
+
if __name__ == "__main__":
|
| 175 |
+
# Semilla para reproducibilidad
|
| 176 |
+
set_seeds(42)
|
| 177 |
|
| 178 |
# --- Estado que debe persistir en todos los reruns ---
|
| 179 |
ss = st.session_state
|
|
|
|
| 181 |
ss.setdefault("last_prompt", "")
|
| 182 |
ss.setdefault("last_response", "")
|
| 183 |
ss.setdefault("just_generated", False)
|
| 184 |
+
|
| 185 |
# Sidebar (control total)
|
| 186 |
GEN_PARAMS = sidebar_params()
|
| 187 |
+
GEN_PARAMS["persona"] = ss.persona # por si acaso
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
# Assigning a new ID to the current user
|
| 190 |
+
if "user_id" not in ss:
|
| 191 |
+
ss["user_id"] = str(uuid.uuid4())[:8]
|
| 192 |
|
|
|
|
|
|
|
|
|
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
# Modelo Técnico
|
| 195 |
+
MODEL_REPO_ID = "tecuhtli/mori-tecnico-model"
|
| 196 |
+
model, tokenizer = load_remote_model(MODEL_REPO_ID, HF_TOKEN)
|
| 197 |
+
|
| 198 |
+
# Presentación de Mori
|
| 199 |
+
st.title("🤖 Mori - Tu Asistente Personal ⌨️")
|
| 200 |
|
| 201 |
+
st.caption("🙋🏽 Puedes preguntarme conceptos sobre machine learning, estadística, visualización, BI, limpieza de datos y más.")
|
| 202 |
+
st.caption("🙇🏽 Por el momento, solo puedo contestar preguntas simples como:")
|
|
|
|
| 203 |
|
| 204 |
+
st.caption(" 🔹 **Definiciones** — Ejemplo: *¿Qué es machine learning?*")
|
| 205 |
+
st.caption(" 🔹 **Procedimientos** — Ejemplo: *¿Cómo limpiar datos?*")
|
| 206 |
+
st.caption(" 🔹 **Funcionalidad** — Ejemplo: *¿Para qué sirve un autoencoder?*")
|
| 207 |
+
|
| 208 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 209 |
+
|
| 210 |
+
st.caption("🦾 Aún estoy aprendiendo. Puedes ver mi desarrollo aquí:")
|
| 211 |
+
st.caption("[hazutecuhtli.github.io](https://github.com/hazutecuhtli/Mori_Development)")
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
st.markdown("<br>", unsafe_allow_html=True)
|
| 214 |
+
|
| 215 |
+
st.caption("✏️ Escribe tu pregunta abajo.")
|
| 216 |
|
| 217 |
# 🔁 Limpieza segura antes del formulario
|
| 218 |
+
if ss.pop("_clear_entrada", False):
|
| 219 |
+
if "entrada" in ss:
|
| 220 |
+
del ss["entrada"]
|
| 221 |
|
| 222 |
# 🧠 Flash de respuesta (la guardamos, pero la mostraremos después del form)
|
| 223 |
+
_flash = ss.pop("_flash_response", None)
|
|
|
|
| 224 |
|
| 225 |
+
# Formulario principal
|
| 226 |
with st.form("formulario_mori"):
|
| 227 |
user_question = st.text_area("📝 Escribe tu pregunta aquí", key="entrada", height=100)
|
| 228 |
submitted = st.form_submit_button("Responder")
|
|
|
|
| 231 |
if not user_question:
|
| 232 |
st.info("Mori: ¿Podrías repetir eso? No entendí bien 😅")
|
| 233 |
else:
|
| 234 |
+
use_rag = st.session_state.get("use_rag", False)
|
| 235 |
+
|
| 236 |
+
persona = GEN_PARAMS.get("persona", ss.persona)
|
| 237 |
+
|
| 238 |
+
if use_rag:
|
| 239 |
+
use_of_rag = 'Con RAG'
|
| 240 |
+
response, prompt = answer_with_mori_rag(
|
| 241 |
+
tokenizer, model, user_question,
|
| 242 |
+
modo=persona,
|
| 243 |
+
verbose=False
|
| 244 |
+
)
|
| 245 |
+
else:
|
| 246 |
+
use_of_rag = 'Sin RAG'
|
| 247 |
+
response, prompt = answer_with_mori_plain(
|
| 248 |
+
tokenizer, model, user_question,
|
| 249 |
+
modo=persona
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
ss["last_prompt"] = prompt
|
| 253 |
+
ss["just_generated"] = True
|
| 254 |
|
| 255 |
# 🧠 Guarda historial
|
| 256 |
hora_actual = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 257 |
+
ss.historial.append(("Tú", user_question, hora_actual))
|
| 258 |
|
| 259 |
hora_actual = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 260 |
+
ss.historial.append(("Mori", response, hora_actual, use_of_rag, persona))
|
| 261 |
|
| 262 |
# 💾 Guarda conversación
|
| 263 |
+
saving_interaction(user_question, response, ss["user_id"], use_of_rag, persona)
|
| 264 |
|
| 265 |
# 🟩 Guarda respuesta para mostrar después del rerun
|
| 266 |
+
ss["_flash_response"] = response
|
| 267 |
|
| 268 |
# 🧼 Limpieza del textarea en el próximo ciclo
|
| 269 |
+
ss["_clear_entrada"] = True
|
| 270 |
|
| 271 |
# ♻️ Forzar refresh (sidebar verá el nuevo prompt)
|
| 272 |
st.rerun()
|
|
|
|
| 277 |
if _flash:
|
| 278 |
st.success(_flash)
|
| 279 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
# 🔁 Historial con estilo chat y contenedor con scroll
|
| 281 |
+
if ss.historial:
|
| 282 |
st.markdown("---")
|
| 283 |
|
| 284 |
# 💾 Botón de descarga arriba del historial
|
| 285 |
lineas = []
|
| 286 |
+
for msg in reversed(ss.historial):
|
| 287 |
+
if len(msg) == 5:
|
| 288 |
+
autor, texto, hora, rag, bot_per = msg
|
| 289 |
+
lineas.append(f"[{hora}] {autor}: {texto} RAG:{rag} Persoality:{bot_per}")
|
| 290 |
+
else:
|
| 291 |
autor, texto, hora = msg
|
| 292 |
lineas.append(f"[{hora}] {autor}: {texto}")
|
|
|
|
|
|
|
|
|
|
| 293 |
texto_chat = "\n\n".join(lineas)
|
| 294 |
|
| 295 |
st.download_button(
|
|
|
|
| 316 |
unsafe_allow_html=True
|
| 317 |
)
|
| 318 |
|
| 319 |
+
for msg in reversed(ss.historial):
|
| 320 |
+
if len(msg) == 5:
|
| 321 |
+
autor, texto, hora, rag, bot_per = msg
|
| 322 |
else:
|
| 323 |
+
autor, texto, hora = msg
|
| 324 |
|
| 325 |
if autor == "Tú":
|
| 326 |
st.markdown(
|
|
|
|
| 366 |
)
|
| 367 |
|
| 368 |
st.markdown("</div>", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|