File size: 12,818 Bytes
ad7703f
 
 
 
57290d8
 
 
 
 
 
ad7703f
 
 
 
 
 
 
 
 
 
57290d8
 
ad7703f
 
 
 
 
57290d8
 
 
ad7703f
 
 
 
 
 
 
 
 
 
 
 
57290d8
ad7703f
57290d8
 
ad7703f
57290d8
 
 
ad7703f
57290d8
 
 
ad7703f
57290d8
 
 
ad7703f
 
57290d8
ad7703f
57290d8
 
 
 
 
ad7703f
 
 
57290d8
 
 
ad7703f
57290d8
 
ad7703f
 
 
 
 
 
 
 
 
 
 
 
 
57290d8
 
 
ad7703f
57290d8
 
 
ad7703f
 
 
 
 
 
 
 
 
 
 
57290d8
ad7703f
57290d8
ad7703f
57290d8
ad7703f
57290d8
ad7703f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57290d8
ad7703f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57290d8
 
ad7703f
57290d8
 
 
ad7703f
 
57290d8
 
 
ad7703f
57290d8
 
ad7703f
57290d8
ad7703f
 
 
 
57290d8
 
ad7703f
 
57290d8
ad7703f
 
 
 
 
 
 
 
 
 
 
 
57290d8
ad7703f
57290d8
ad7703f
57290d8
ad7703f
57290d8
ad7703f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57290d8
ad7703f
 
 
 
57290d8
ad7703f
57290d8
ad7703f
57290d8
ad7703f
 
 
57290d8
ad7703f
57290d8
ad7703f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57290d8
ad7703f
 
 
57290d8
ad7703f
 
57290d8
ad7703f
 
 
 
 
 
57290d8
 
ad7703f
 
 
 
57290d8
 
ad7703f
57290d8
ad7703f
 
57290d8
 
ad7703f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57290d8
ad7703f
 
 
 
 
57290d8
 
ad7703f
 
 
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
#***************************************************************************
#Importing Libraries
#***************************************************************************
import os, sys, torch, json, csv, warnings, joblib, uuid
os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
import streamlit as st
from pathlib import Path
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
from unidecode import unidecode
from datetime import datetime
from huggingface_hub import hf_hub_download
#***************************************************************************
#Defining default paths for the model to work
#***************************************************************************

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

#***************************************************************************
#Setting up variables
#***************************************************************************


#***************************************************************************
#Functions
#***************************************************************************

# Function to clean the question field
def limpiar_input():
    st.session_state["entrada"] = ""
    
# Function to save user interaction
def saving_interaction(question, response, context, user_id):

    '''
    inputs:
    
    question --> User input question
    response --> Mori response to the user question
    context --> Context related to the user input, found by the trained classifier
    user_id --> ID for the current user (Unique ID per session)

    '''    

    # Getting the current time for the saving log
    timestamp = datetime.now().isoformat()

    # Defining the path to save the current interaction
    stats_dir = Path("Statistics")
    stats_dir.mkdir(parents=True, exist_ok=True)

    # Setting the file to save the interactions
    archivo_csv = stats_dir / "conversaciones_log.csv"
    existe_csv = archivo_csv.exists()

    # Saving statistics as a csv
    with open(archivo_csv, mode="a", encoding="utf-8", newline="") as f_csv:
        writer = csv.writer(f_csv)
        if not existe_csv:
            writer.writerow(["timestamp", "user_id", "contexto", "pregunta", "respuesta"])
        writer.writerow([timestamp, user_id, context, question, response])

    # Saving statiistics as a json file
    archivo_jsonl = stats_dir / "conversaciones_log.jsonl"
    with open(archivo_jsonl, mode="a", encoding="utf-8") as f_jsonl:
        registro = {
            "timestamp": timestamp,
            "user_id": user_id,
            "context": context,
            "pregunta": question,
            "respuesta": response}
        f_jsonl.write(json.dumps(registro, ensure_ascii=False) + "\n")
        

# Function to load models within the huggingface respositories space
@st.cache_resource
def load_model(path_str):

    '''
    inputs:
    
    path_str --> Path for loading models and tokenizers

    outsputs:

    model --> Loaded Model
    tokenizer --> Loaded tokenizer

    '''
    
    path = Path(path_str).resolve()
    tokenizer = AutoTokenizer.from_pretrained(path, local_files_only=True)
    model = AutoModelForSeq2SeqLM.from_pretrained(path, local_files_only=True)
    
    return model, tokenizer


# Funcion para clasificar las preguntas del usuario definiendo el contexto de las mismas
def classify_context(question, label_classes, model, tokenizer, device):

    '''
    inputs:
    
    question --> Pregunta formulada por el usuario
    label_classes --> Clases del label encoder para decodificar inferencias
    model --> Clasificador para determinar el contexto de las pregutnas
    tokenizer --> Tokenizer usada para clasificar contextos    
    device --> Usar el GPU o el CPU dependiendo de su disponibilidad    

    outsputs:

    predicted_label --> Clasificacion de la pregunta en diversos contextos (clases)

    '''

    # Moviendo el modelo al device disponible
    model = model.to(device)
    
    # Procesando la entrada del usuario
    inputs = tokenizer(question, return_tensors="pt", padding=True, truncation=True, max_length=128)
    inputs = {key: val.to(device) for key, val in inputs.items()}
    
    # Clasificacion de la pregunta del usuario en contextos
    with torch.no_grad():
        outputs = model(**inputs)
        logits = outputs.logits
    
    # Inferencia del clasificador
    pred_intent = torch.argmax(logits, dim=1).item()
    predicted_label = label_classes[pred_intent]
    
    return predicted_label



# Funcion para generar respuestas tecnicas de Mori
def technical_asnwer(question, context, model, tokenizer, device):

    '''
    inputs:
    
    question --> Pregunta formulada por el usuario
    context --> Contexto de la preguntadel usario definido por el clasificador
    model --> Modelo de Mori para responder preguntas tecnicas
    tokenizer --> Tokenizer usado para procesar entradas y decoodificar respuestas
    device --> Usar el GPU o el CPU dependiendo de su disponibilidad

    outsputs:

    response --> Respues de Mori tecnico (Modelo tecnico)

    '''
    
    # Moviendo el modelo al device disponible
    model = model.to(device)    
    
    # Promp Engineering para ayudar a Mori a encontrar la mejor respuesta
    input_text = f"Context: {context} [SEP] Question: {question}"
    
    # Tokenizando el texto de entrada
    inputs = tokenizer(input_text, return_tensors="pt").to(device)
    
    # Generando la respuesta
    summary_ids = model.generate(inputs['input_ids'], max_length=150, num_beams=5, early_stopping=True)
    
    # Decodificando la respuesta
    response = tokenizer.decode(summary_ids[0], skip_special_tokens=True)
    
    return "🧠 [Mori Técnico] " + response.strip()


# Funcion para generar respuestas sociales de Mori
def social_asnwer(question, model, tokenizer, device):

    '''
    inputs:
    
    question --> Pregunta formulada por el usuario
    model --> Modelo de Mori para responder preguntas sociales
    tokenizer --> Tokenizer usado para procesar entradas y decoodificar respuestas    
    device --> Usar el GPU o el CPU dependiendo de su disponibilidad    

    outsputs:

    response --> Respues de Mori social (Modelo social)

    '''

    # Moviendo el modelo al device disponible
    model = model.to(device)

    # Tokenizando la entrada del usuario sin agregar <eos> explícitamente    
    inputs = tokenizer(
        question,  # ✅ sin agregar eos_token
        return_tensors="pt",
        padding=True,
        truncation=True,
        max_length=128  # ✅ especificado para evitar warning
    ).to(device)

    # Generando respuesta usando muestreo
    output_ids = model.generate(
        input_ids=inputs["input_ids"],
        attention_mask=inputs["attention_mask"],  # ✅ FIX agregado
        max_length=50,
        pad_token_id= tokenizer.eos_token_id,
        do_sample=True,
        top_p=0.95,
        top_k=50)

    # Decodificando y limpiando la respuesta
    response = tokenizer.decode(output_ids[0], skip_special_tokens=True)
    
    return "🤝 [Mori Social] " + response.strip()


# Funcion para generar respuesta general de Mori
def contextual_asnwer(question, label_classes, context_model, cont_tok, tec_model, tec_tok, soc_model, soc_tok, device):

    '''
    inputs:
    
    question --> Pregunta formulada por el usuario
    label_classes --> Clases del label encoder para decodificar inferencias
    context_model --> Clasificador para determinar el contexto de las pregutnas
    cont_tok --> Tokenizer usada para clasificar contextos
    tec_model --> Modelo de Mori para responder preguntas tecnicas
    tec_tok -->  Tokenizer usado por Mori Tenico
    soc_model --> Modelo de Mori para responder preguntas sociales
    soc_tok --> Tokenizer usado por Mori Social
    device --> Usar el GPU o el CPU dependiendo de su disponibilidad    

    outsputs:

    response --> Respues de Mori General (Respues con Prompt Engineering)

    '''
    
    # Detectar contexto usando el clasificador
    context = classify_context(question, label_classes, context_model, cont_tok, device)

    context_icons = {"social": "💬",
                     "modelos": "🔧",
                     "evaluación": "📏",
                     "optimización": "⚙️",
                     "visualización": "📈",
                     "aprendizaje": "🧠",
                     "vida digital" : "🧑‍💻",
                     "estadística": "📊",
                     "infraestructura": "🖥",
                     "datos": "📂",
                     "transformación digital": "🌀"}
        
    icon = context_icons.get(context, "🧠")
    #print(f"{icon} Contexto detectado: {context}") # (opcional para debug)
    st.markdown(f"**{icon} Contexto detectado:** `{context}`")

    if context == 'social':
        
        # Generar respuesta contextual usando el modelo social
        response = social_asnwer(question, soc_model,soc_tok, device)

    else:      

        # Generar respuesta contextual usando el modelo tecnico
        response = technical_asnwer(question, context, tec_model, tec_tok, device)
    
    return response, context

    

#***************************************************************************
#MAIN
#***************************************************************************

if __name__ == '__main__':

    # Setting historial for the current user
    if "historial" not in st.session_state:
        st.session_state.historial = []
        
    # Addigning a new ID to the current user
    if "user_id" not in st.session_state:
        st.session_state["user_id"] = str(uuid.uuid4())[:8]  # Ej: "f6a9b3e2"

    # Loading classifier encoder classes:
    labels_path = hf_hub_download(repo_id="tecuhtli/mori-context-model", filename="context_labels.pkl")
    label_classes = joblib.load(labels_path)

    # Loading Saved Models  
    # Modelo Contexto
    cont_tok = AutoTokenizer.from_pretrained("tecuhtli/mori-context-model")
    context_model = AutoModelForSeq2SeqLM.from_pretrained("tecuhtli/mori-context-model")
    
    # Modelo Técnico
    tec_tok = AutoTokenizer.from_pretrained("tecuhtli/mori-tecnico-model")
    tec_model = AutoModelForSeq2SeqLM.from_pretrained("tecuhtli/mori-tecnico-model")

    # Modelo Social
    soc_tok = AutoTokenizer.from_pretrained("tecuhtli/mori-social-model")
    soc_model = AutoModelForSeq2SeqLM.from_pretrained("tecuhtli/mori-social-model")

    # Available Device
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

    # Defining Moris Presetation
    st.title("🤖 Mori - Tu Asistente Personal 🐈")
    st.caption("💬 Puedes preguntarme conceptos técnicos como visualización, limpieza, BI, etc.")
    st.caption("😅 Por el momento, solo puedo contestar preguntas como: ")
    st.caption("🤓  ¿Como estas? ¿Que son?, Explícame algo, Define algo, ¿Para que sirven?")
    st.caption("✏️ Escribe 'salir' para terminar.\n")        


    #entrada_usuario = st.text_area("📝 Escribe tu pregunta aquí")
    with st.form("formulario_mori"):
        user_question = st.text_area("📝 Escribe tu pregunta aquí", key="entrada", height=100)
        submitted = st.form_submit_button("Responder")


    if submitted:

        if not user_question:
            print("Mori: ¿Podrías repetir eso? No entendí bien 😅")        

        else:
            
            #if st.button("Responder") and entrada_usuario:
            response, context = contextual_asnwer(user_question, label_classes, context_model, cont_tok, tec_model, tec_tok, soc_model, soc_tok, device)
            st.success(response)

            # Guarda en historial
            st.session_state.historial.append(("Mori", response))
            st.session_state.historial.append(("Tú", user_question))

            # 💾 Guarda en archivo para stats/dataset
            saving_interaction(user_question, response, context, st.session_state["user_id"])


    # 🔁 Muestra historial
    if st.session_state.historial:
        st.markdown("---")
        for autor, texto in reversed(st.session_state.historial):
            if autor == "Tú":
                st.markdown(f"🧍‍♂️ **{autor}**: {texto}")
            else:
                st.markdown(f"🤖 **{autor}**: {texto}")    


    if st.session_state.historial:
        texto_chat = ""
        for autor, texto in st.session_state.historial:
            texto_chat += f"{autor}: {texto}\n\n"

        st.download_button(
            label="💾 Descargar conversación como .txt",
            data=texto_chat,
            file_name="conversacion_mori.txt",
            mime="text/plain")


#***************************************************************************
#FIN
#***************************************************************************