-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodelo_asistente_virtual_para_ciencia_abierta_version_2.py
More file actions
928 lines (751 loc) · 44.2 KB
/
Copy pathmodelo_asistente_virtual_para_ciencia_abierta_version_2.py
File metadata and controls
928 lines (751 loc) · 44.2 KB
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
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
# -*- coding: utf-8 -*-
"""modelo_asistente_virtual_para_ciencia_abierta_version_2.ipynb
Automatically generated by Colab.
Original file is located at
https://colab.research.google.com/drive/1Gbb71O5nbnWOL0PiklTR5Gt5BYbrEJtA
# 1. Librerías utilizadas.
"""
# # Lectura e ingesta de PDFs
# os
# pdfplumber==0.11.7
# # #Limpieza y Procesamiento de documentos
# # spacy==3.8.7
# nltk==3.9.1
# pandas==2.2.2
# regex==2024.11.6
# # Embeddings y vectorización
# sentence-transformers==4.1.0
# chromadb==1.0.15 (base de datos vectorial)
# faiss-cpu ==1.7.4
# # Framework RAG (Seleccionar langchain o llama-index no es necesario utilizar las dos)
# langchain==0.3.27
# langchain-community==0.3.27
# langchain-core==0.3.72
# langchain-text-splitters==0.3.9
# rank-bm25==0.2.2
# llama-index ==0.9.0.
# # APIs LLM (Seleccionar openai o anthropic de claude no es necesario utilizar las dos)
# huggingface-hub==0.34.3
# openai==1.98.0
# transformers==4.54.1
# torch @ https://download.pytorch.org/whl/cu124/torch-2.6.0%2Bcu124-cp311-cp311-linux_x86_64.whl
# # Interfaz y API (Para más adelante, elegir gradio o streamlit, no es necesario utilizar las dos)
# streamlit==1.28.0
# gradio==4.8.0 (utilizar esta por su sencillez, luego en la iteración agregar streamlit)
"""## 1.1 Instalación de librerías"""
!pip install pdfplumber spacy sentence_transformers -U langchain-community chromadb rank_bm25
!pip install -U :class:`~langchain-huggingface.HuggingFaceEmbeddings
"""## 1.2 Importación de librerías"""
# Librerías principales de python
import pdfplumber # Extracción de texto PDF
import spacy
import sentence_transformers # Embeddings semánticos
from langchain.text_splitter import RecursiveCharacterTextSplitter
from pathlib import Path
# import as `from :class:`~langchain_huggingface import HuggingFaceEmbeddings``.embedding_function = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-mpnet-base-v2") # Use model_name directly
"""# 2. Pipeline de Procesamiento de Documentos
## 2.1 Ingesta y preprocesamiento de los archivos PDFs
"""
import os
import pdfplumber
from langchain.text_splitter import RecursiveCharacterTextSplitter
def extract_text_from_pdf(pdf_path):
"""Extracts text from a PDF file."""
text = ""
try:
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
text += page.extract_text() + "\n"
except Exception as e:
print(f"Error extracting text from {pdf_path}: {e}")
return text
def split_text_into_chunks(text, chunk_size=500, chunk_overlap=100):
"""Splits text into smaller chunks."""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
length_function=len,
)
chunks = text_splitter.split_text(text)
return chunks
# Define the path to your PDF folder
input_folder = "pdfs"
folder_path = Path(input_folder)
# Check if the folder exists
if not folder_path.exists():
print(f"Error: Folder '{input_folder}' not found.")
# Create the folder if it doesn't exist and add a placeholder file
try:
folder_path.mkdir(parents=True, exist_ok=True)
print(f"Carpeta creada: '{input_folder}'. Por favor subí tus archivos pdfs aquí.")
# # Create a placeholder file to indicate where to put PDFs
# placeholder_file = folder_path / "upload_your_pdfs_here"
# with open(placeholder_file, "w") as f:
# f.write("Por favor subí tus archivos pdfs a esta carpeta.")
except Exception as e:
print(f"Error creating folder '{input_folder}': {e}")
pdf_files = [] # Set pdf_files to empty list if folder doesn't exist
else:
# Get a list of all PDF files in the folder
pdf_files = list(folder_path.glob("*.pdf"))
all_document_text = ""
if pdf_files:
print(f" {len(pdf_files)} PDF files in '{input_folder}'. Processing...")
for pdf_file in pdf_files:
print(f"Processing: {pdf_file}")
document_text = extract_text_from_pdf(pdf_file)
if document_text:
all_document_text += document_text + "\n"
print("Finished processing PDF files.")
# Split the combined text into chunks
if all_document_text:
text_chunks = split_text_into_chunks(all_document_text)
print(f"Successfully extracted and split {len(text_chunks)} chunks from all documents.")
else:
print("No text extracted from any PDF files.")
text_chunks = []
else:
print(f"No PDF files found in '{input_folder}'. Please upload your PDF files to this folder.")
text_chunks = [] # Initialize as empty list if no files are found
# Now text_chunks contains all chunks from all processed PDFs
"""## 2.2 Procesamiento de los archivos pdfs
Anteriormente los archivos pdfs fueron segmentados en chunks de 500 a 1000 tokens, en el siguiente chunk se procede a la limpieza y normalización del texto en español.
## 2.2.1 Descarga de recursos necesarios de Spacy
"""
# Cargar modelo de spaCy para español
try:
nlp = spacy.load("es_core_news_sm")
print("Modelo de spaCy cargado.")
except OSError:
print("Modelo de spaCy no encontrado. Se instalará a continuación.")
!python -m spacy download es_core_news_sm
nlp = spacy.load("es_core_news_sm")
# print("Modelo de spaCy no encontrado. Instalar con: python -m spacy download es_core_news_sm")
# nlp = None
"""## 2.2.2 Limpieza del texto"""
import re
import spacy
from typing import List, Dict, Tuple
import pandas as pd
from collections import defaultdict
class DocumentProcessor:
def __init__(self, chunk_size_tokens=500, chunk_overlap_tokens=100):
self.chunk_size_tokens = chunk_size_tokens
self.chunk_overlap_tokens = chunk_overlap_tokens
self.nlp = nlp
def clean_and_normalize_spanish(self, text: str) -> str: # podría agregar algunas reglas de limpieza del Código del CORPUS
"""
Limpieza y normalización de texto en español
"""
# Normalizar caracteres especiales del español
replacements = {
'á': 'á', 'é': 'é', 'í': 'í', 'ó': 'ó', 'ú': 'ú',
'ñ': 'ñ', 'ü': 'ü', 'Á': 'Á', 'É': 'É', 'Í': 'Í',
'Ó': 'Ó', 'Ú': 'Ú', 'Ñ': 'Ñ', 'Ü': 'Ü'
}
# Normalizar espacios y saltos de línea
text = re.sub(r'\s+', ' ', text)
text = re.sub(r'\n+', '\n', text)
# Eliminar caracteres de control
text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]', '', text)
# Normalizar puntuación
text = re.sub(r'["""]', '"', text)
text = re.sub(r"['']", "'", text) # Fixed the regex pattern for single quotes
text = re.sub(r'[–—]', '-', text)
# Eliminar espacios antes de puntuación
text = re.sub(r'\s+([,.;:!?])', r'\1', text)
# Normalizar números y fechas
text = re.sub(r'(\d+)\s*,\s*(\d+)', r'\1,\2', text)
text = re.sub(r'(\d+)\s*\.\s*(\d+)', r'\1.\2', text)
# Eliminar URLs y emails (preservar contexto)
text = re.sub(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-Z]))+', '[URL]', text)
text = re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL]', text)
text = re.sub(r'www\.(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-Z]))+', '', text)
# Limpiar espacios múltiples
text = re.sub(r'\s+', ' ', text)
#---------------------------------------------
# LIMPIEZA AGREGADA DESDE EL CODIGO DE CORPUS
#---------------------------------------------
# Eliminar números de página (patrones comunes)
# Elimina números solos en una línea o números con formato "Página X"
text = re.sub(r'^\d+\s*$', '', text, flags=re.MULTILINE)
text = re.sub(r'página\s*\d+', '', text, flags=re.IGNORECASE)
text = re.sub(r'pp' '\s*\d+', '', text, flags=re.IGNORECASE)
text = re.sub(r'p\.\s*\d+', '', text, flags=re.IGNORECASE)
text = re.sub(r'pág\.\s*\d+', '', text, flags=re.IGNORECASE)
text = re.sub(r'^-\s*\d+\s*-\s*$', '', text, flags=re.MULTILINE)
# También manejar casos donde hay espacios antes del salto de línea
# Como "nac- ieron" en la misma línea
text = re.sub(r'(\w+)-\s+(\w+)', r'\1\2', text)
# Eliminar patrones tipo peerj.preprints.27580v1 o similares
# Este patrón captura: palabra.palabra.número+letra+número
text = re.sub(r'\b\w+\.\w+\.\d+[a-zA-Z]+\d*\b', ' ', text)
# Eliminar patrones de identificadores de documentos científicos
# Como: arxiv.1234.5678, doi.10.1234/5678, pmid.12345678
text = re.sub(r'\b\w+\.\d+\.\d+\b', ' ', text)
text = re.sub(r'\bdoi\.\borg\.\S+', ' ', text)
text = re.sub(r'\bpmid\.\d+', ' ', text)
text = re.sub(r'\barxiv\.\d+\.\d+', ' ', text)
# Eliminar números con comas como 45, 56, 78
# Este patrón elimina secuencias de números separados por comas
text = re.sub(r'\b\d+(?:\s*,\s*\d+)+\b', '', text)
# Eliminar números solos (incluyendo decimales)
# Esto elimina números aislados como 123, 45.67, etc.
text = re.sub(r'\b\d+\.?\d*\b', '', text)
# Eliminar referencias tipo [1], [2,3], [45-47], etc.
text = re.sub(r'\[\d+(?:[-,]\d+)*\]', '', text)
# Eliminar años solos (4 dígitos)
text = re.sub(r'\b\d{4}\b', '', text)
# casos especiales de limpieza del texto de UNESCO
#Eliminar números romanos en minúscula
#roman_numerals_pattern = r'\b(?:i[vx]|v?i{0,3}|x)\b'
# elimina numeracion del tipo 45ª
text = re.sub(r'\b\d+ª\b', ' ', text)
#elimina url que solo terminan con '.com'
text = re.sub(r'\b(?!https:\/\/|www\.)[a-zA-Z0-9.-]+\.com\b', ' ', text)
# casos especiales de limpieza del texto de Conocimiento abierto
# elimina letras solas
text = re.sub(r'\b[a-z]\b', ' ', text)
# elimina el siguiente patrón: ccoolleecccciióónn ggrruuppooss ddee ttrraabbaajjoo ccoonnoocciimmiieennttoo aabbiieerrttoo eenn aamméérriiccaa llaattiinnaa
text = re.sub(r'(?<!acc)(?<!desarroll)([a-záéíóúñ])\1+(?!eso)(?!lo)',r'\1', text) # ver si conviene o no, tiene palabras en ingles como access(queda aces) o common(queda comon)
# filtra las palabras de 2 digitos: Ej:fs, ar, eb
text = re.sub(r'\b[a-z]{2}\b', ' ', text)
# filtra las palabras de 3 digitos sin vocales: Ej:pcb
text = re.sub(r'\b[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ]{3}\b', ' ', text)
# filtra las asociaciones de este tipo: n2o0s2, a0i
text = re.sub(r'\b(?=\w*\d)(?=\d*\w)\w+\b', ' ', text)
# Remove HTML tags
text = re.sub ( r'<.*?>' ,'', text)
# Eliminar correos electrónicos
text = re.sub(r'\S+@\S+', '', text)
# Eliminar caracteres especiales excesivos pero mantener puntuación básica
# Mantener: . , ; : ! ? ¿ ¡ ( ) - " '
# text = re.sub(r'[^\w\s\.\,\;\:\!\?\¿\¡\(\)\-\"\ª\'áéíóúñÁÉÍÓÚÑ]', '', text)
# decido eliminar todos los caracteres especiales por eso comento lo anterior
text = re.sub(r'[^\w\s]', '', text)
# Eliminar múltiples espacios
text = re.sub(r'\s+', ' ', text)
# Eliminar múltiples saltos de línea
text = re.sub(r'\n+', '\n', text)
# Eliminar líneas que solo contienen espacios
lines = text.split('\n')
lines = [line.strip() for line in lines if line.strip()]
text = '\n'.join(lines)
# Eliminar stopwords de spaCy
if self.nlp:
doc = self.nlp(text)
text = " ".join([token.text for token in doc if not token.is_stop])
print(f"Successfully cleaned and normalized {len(cleaned_text_chunks)} text chunks.")
return text.strip()
"""# 3. Arquitectura de Retrieval
## 3.1 Sistema de Búsqueda Multimodal
En sintesis, este código prepara los datos de texto para su recuperación convirtiéndolos a un formato (documentos con sus correspondientes embeddings almacenados en una base de datos vectorial) que permite buscar de manera eficiente información relevante basada en la similitud semántica. Este es un paso fundamental en un proceso RAG, ya que permite encontrar las partes más relevantes de los documentos para responder a las consultas de los usuarios. Los pasos que se utilizaron son:
* creación de objetos del tipo Document a partir de los fragmentos de texto.
* creación de un vector_store (utilizando Chroma) a partir de los objetos Document y sus correspondientes embeddings. Por último, se utilizarán los documentos y el vector_store para configurar el BM25Retriever, VectorStoreRetriever y EnsembleRetriever.
"""
from langchain.schema import Document
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
# Create Document objects from text_chunks
if text_chunks is not None:
documents = [Document(page_content=chunk) for chunk in text_chunks]
print(f"Created {len(documents)} Document objects from cleaned chunks.")
else:
documents = []
print("No cleaned text chunks available to create documents.")
# Assuming you have already loaded the model
# If not, you need to run the cell that loads the model (cell id: 4417745d)
if documents and model is not None: # Check if documents and the model are available
# Create a HuggingFaceEmbeddings object using your SentenceTransformer model
# Aplico embeddings del texto de los pdfs
embedding_function = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-mpnet-base-v2") # Use model_name directly
# Create a Chroma vector store, passing the embedding function
vector_store = Chroma.from_documents(documents, embedding_function)
print("Chroma vector store created from documents.")
else:
vector_store = None
print("Could not create Chroma vector store: no documents or embedding model available.")
#para aplicar lo siguiente antes debo cargar los objetos de tipo Documents, lo que se realiza en el paso anterior
from langchain.vectorstores import Chroma
from langchain.retrievers import BM25Retriever, EnsembleRetriever
# Retriever híbrido
bm25_retriever = BM25Retriever.from_documents(documents)
vector_retriever = vector_store.as_retriever(
search_type="similarity_score_threshold",
search_kwargs={"score_threshold": 0.7, "k": 5}
)
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.3, 0.7]
)
"""# 4. Integración con APIs de Grandes Modelos de Lenguaje
La clase LLMProvider permite cambiar fácilmente entre diferentes proveedores de un gran modelo del lenguaje, simplemente especificando el nombre del proveedor al crear una instancia de la clase, sin tener que modificar el código que utiliza el cliente. Esto facilita la experimentación con diferentes modelos.
## 4.1 Prompt Engineering Especializado
"""
SYSTEM_PROMPT = """
Eres un asistente especializado en Ciencia Abierta para investigadores y estudiantes de posgrado de América Latina.
Tu conocimiento se basa en:
- Recomendación UNESCO sobre Ciencia Abierta (2021)
- Recursos de Redalyc
- Documentos especializados en español
- Contexto regional latinoamericano
- Mejores prácticas académicas utilizadas en América Latina
Responde de manera:
- Precisa y basada en evidencia
- Adaptada al nivel del usuario: básico, intermedio, avanzado
- Con ejemplos del contexto latinoamericano
- Incluyendo fuentes y recursos adicionales
Contexto recuperado: {context}
Perfil del usuario: {user_profile}
"""
"""## 4.2 Integración y Evaluación de Modelos LLM
En el contexto del sistema RAG utilizado en esta sección:
* La clase LLMProvider es la encargada de la fase de "Generation".
Esta recibe la pregunta del usuario y el contexto relevante (obtenido de la fase de "Retrieval" que ya implementaste previamente con los EnsembleRetriever, BM25Retriever y VectorStoreRetriever).
* Luego, utiliza un ***system_promp***t especializado para guiar al LLM a responder como un experto en Ciencia Abierta, considerando el contexto recuperado.
* Por último, la clase LLMEvaluator te ayuda a medir la calidad de las respuestas generadas, permitiéndote iterar y mejorar tu sistema RAG
"""
import os
import time
import json
from openai import OpenAI
from anthropic import Anthropic
# Import the new MistralClient from the updated library
from mistralai.client import MistralClient
from mistralai.models.chat_completion import ChatMessage # Import ChatMessage if needed for future use
from google.colab import userdata
# from dotenv import load_dotenv
# Carga las variables de entorno desde un archivo .env
# load_dotenv() # Y EN LA SECCION DE LECTURA VA: return OpenAI(api_key=userdata.getload("OPENAI_API_KEY"))
# --- Este es el trozo de código que proporcionaste ---
class LLMProvider:
def __init__(self, provider="mistral"): # por defecto, está configurado con OpenAi pero lo cambio a Mistral por el límite que tiene el primer proveedor
#def __init__(self, provider="openai"):
self.provider = provider
# Se asume que las API keys están en las secrets, a futuro se debe pasar estos valores a variables del entorno
# KEY_OPENAI_API, ANTHROPIC_API_KEY, HUGGINGFACEHUB_API_TOKEN
self.client = self._initialize_client()
def _initialize_client(self):
if self.provider == "openai":
# Use userdata.get() to access the API key from Colab Secrets
return OpenAI(api_key=userdata.get("OPENAI_API_KEY"))
elif self.provider == "mistral":
# Updated initialization for MistralClient
return MistralClient(api_key=userdata.get("MISTRAL_API_KEY"))
elif self.provider == "anthropic":
return Anthropic(api_key=userdata.get("ANTHROPIC_API_KEY"))
elif self.provider == "huggingface":
# Esta implementación puede variar según la librería exacta que uses
# Aquí se asume un cliente compatible con un método 'invoke' o similar
print("Hugging Face client initialization needs specific implementation.")
return HuggingFaceHub(repo_id="microsoft/DialoGPT-large")
# return HuggingFaceHub(repo_id="google/flan-t5-xxl", ...)
return None
else:
raise ValueError("Proveedor no soportado")
# --- INICIO DEL CÓDIGO AÑADIDO ---
def generate_response(self, prompt, model="mistral-tiny", max_tokens=1500, temperature=0.4): # como está configurado con OpenAi, se utiliza el modelo "gpt-4o". Si queremos otro modelo debemos revisar el proveedor, es decir, saber si es Mistral, huggingFace, etc.
"""
Genera una respuesta utilizando el cliente del proveedor LLM configurado.
Args:
prompt (str): El prompt del usuario.
model (str): El modelo a utilizar (relevante para OpenAI/Anthropic/Mistral).
max_tokens (int): El número máximo de tokens en la respuesta.
temperature (float): La "creatividad" de la respuesta.
Returns:
dict: Un diccionario con la 'response' y la 'latency' en segundos.
"""
# system_prompt = "Eres un asistente experto en Ciencia Abierta. Tu propósito es proporcionar respuestas claras, precisas y bien estructuradas sobre sus principios, prácticas y beneficios."
system_prompt = """
Eres un asistente especializado en Ciencia Abierta para investigadores y estudiantes de posgrado de América Latina.
Tu conocimiento se basa en:
- Recomendación UNESCO sobre Ciencia Abierta (2021)
- Recursos de Redalyc
- Documentos especializados en español
- Contexto regional latinoamericano
- Mejores prácticas académicas utilizadas en América Latina
Responde de manera:
- Precisa y basada en evidencia
- Adaptada al nivel del usuario: básico, intermedio, avanzado
- Con ejemplos del contexto latinoamericano
- Incluyendo fuentes y recursos adicionales
Contexto recuperado: {context}
Perfil del usuario: {user_profile}
"""
start_time = time.time()
try:
response_text = ""
if self.provider == "openai":
completion = self.client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
max_tokens=max_tokens,
temperature=temperature
)
response_text = completion.choices[0].message.content
elif self.provider == "anthropic":
message = self.client.messages.create(
model= "claude-3-sonnet", # por ejemplo model="claude-3-opus-20240229", este último es más avanzado pero de pago
max_tokens=max_tokens,
temperature=temperature,
system=system_prompt,
messages=[
{"role": "user", "content": prompt}
]
)
response_text = message.content[0].text
elif self.provider == "mistral":
messages = [ChatMessage(role="user", content=prompt)] # Use ChatMessage for the new client
chat_response = self.client.chat(
model=model, # por ejemplo model= model "mistral-tiny" # También puedes usar "mistral-small" o "mistral-medium"
messages=messages,
max_tokens=max_tokens,
temperature=temperature
)
response_text = chat_response.choices[0].message.content
# elif self.provider == "huggingface":
# response_text = self.client.invoke(prompt)
else:
raise ValueError(f"Proveedor '{self.provider}' no implementado para generación.")
end_time = time.time()
latency = end_time - start_time
return {"response": response_text, "latency": latency}
except Exception as e:
print(f"Error al generar respuesta con {self.provider}: {e}")
return {"response": None, "latency": 0}
class LLMEvaluator:
"""
Evalúa la calidad de una respuesta de un LLM utilizando otro LLM (LLM-as-a-judge).
"""
def __init__(self, evaluator_provider="openai", evaluator_model="gpt-3.5-turbo"):
"""
Inicializa el evaluador con un LLM específico que actuará como juez.
"""
print(f"Inicializando evaluador con {evaluator_provider} ({evaluator_model})...")
# El evaluador usa su propia instancia de LLMProvider para ser el "juez"
self.judge_llm = LLMProvider(provider=evaluator_provider)
self.evaluator_model = evaluator_model
def evaluate(self, prompt, response, latency):
"""
Evalúa una respuesta basándose en criterios de calidad y rendimiento.
Args:
prompt (str): El prompt original.
response (str): La respuesta generada.
latency (float): La latencia de la generación.
Returns:
dict: Un diccionario con las métricas de rendimiento y calidad.
"""
if not response:
return {"error": "No se proporcionó una respuesta para evaluar."}
evaluation_prompt = f"""
Eres un sistema de evaluación experto. Tu tarea es analizar la calidad de una respuesta generada por un modelo de lenguaje sobre el tema de "Ciencia Abierta".
Evalúa la respuesta basándose en los siguientes criterios, asignando una puntuación del 1 (muy pobre) al 5 (excelente) para cada uno:
1. **Relevancia**: ¿La respuesta aborda directamente la pregunta del prompt?
2. **Precisión**: ¿La información proporcionada es correcta y fiable?
3. **Coherencia y Claridad**: ¿La respuesta está bien estructurada, es fácil de entender y no tiene contradicciones?
PROMPT ORIGINAL:
"{prompt}"
RESPUESTA GENERADA:
"{response}"
Por favor, devuelve tu evaluación únicamente en formato JSON, con la siguiente estructura:
{{
"puntuacion_relevancia": <int>,
"puntuacion_precision": <int>,
"puntuacion_coherencia": <int>,
"razonamiento_evaluacion": "<string con tu justificación>"
}}
"""
print("\n--- Solicitando evaluación de calidad al LLM juez... ---")
# Usamos el generador del juez para obtener la evaluación
evaluation_result = self.judge_llm.generate_response(
prompt=evaluation_prompt,
model=self.evaluator_model
)
try:
# Parseamos el JSON de la evaluación, handling potential non-JSON response
quality_metrics = json.loads(evaluation_result["response"])
except (json.JSONDecodeError, TypeError):
print("Error: La evaluación del juez no devolvió un JSON válido.")
# Provide the raw response if JSON parsing fails
quality_metrics = {"error": "Fallo al parsear la evaluación.", "raw_output": evaluation_result.get("response", "No response received.")}
# Combinamos todas las métricas en un resultado final
final_report = {
"metricas_rendimiento": {
"latencia_segundos": round(latency, 2)
},
"metricas_calidad": quality_metrics
}
return final_report
# --- Ejemplo de uso ---
if __name__ == "__main__":
# 1. Elige el proveedor para generar la respuesta principal
# Puedes cambiar "openai" por "anthropic" si tienes la API key configurada
provider_principal = LLMProvider(provider="mistral")
# 2. Define tu prompt sobre Ciencia Abierta
prompt_usuario = "¿Cuáles son los pilares fundamentales de la Ciencia Abierta y cómo benefician a la investigación colaborativa?"
print(f"Generando respuesta con '{provider_principal.provider}'...")
print(f"Prompt: {prompt_usuario}\n")
# 3. Genera la respuesta y obtén la latencia
# Para Anthropic, podrías usar un modelo como "claude-3-sonnet-20240229", este es pago
resultado_generacion = provider_principal.generate_response(
prompt_usuario,
model="mistral-tiny" # Changed model to a Mistral model as the provider is set to mistral
)
if resultado_generacion and resultado_generacion["response"]:
respuesta_generada = resultado_generacion["response"]
latencia_generacion = resultado_generacion["latency"]
print("✅ Respuesta Generada:")
print("--------------------")
print(respuesta_generada)
print("--------------------")
print(f"⏱️ Tiempo de generación: {latencia_generacion:.2f} segundos")
# 4. Evalúa la respuesta generada
# Se puede usar el mismo proveedor (ej. OpenAI con GPT-4o) como juez por su alta capacidad de razonamiento
evaluador = LLMEvaluator(evaluator_provider="mistral", evaluator_model="gpt-4o") # buscar configurar con Antropic q es el pago q tengo evaluador = LLMEvaluator(evaluator_provider="openai", evaluator_model="gpt-4o")
reporte_final = evaluador.evaluate(prompt_usuario, respuesta_generada, latencia_generacion)
# 5. Muestra el reporte de evaluación
print("\n\n📊 Reporte de Evaluación:")
print("========================")
# Imprime el reporte final de una forma legible
print(json.dumps(reporte_final, indent=4, ensure_ascii=False))
"""# 5. EXTRA: Fine-tuning para evaluar el modelo de Embeddings
Este código está diseñado para fine-tune (ajustar) un modelo de embeddings pre-entrenado (sentence-transformers/paraphrase-multilingual-mpnet-base-v2) utilizando ejemplos específicos de tu dominio (Ciencia Abierta en español). El objetivo es mejorar la capacidad del modelo para generar embeddings que capturen mejor la similitud semántica entre textos relacionados con este tema.
"""
from sentence_transformers import SentenceTransformer, InputExample, losses, evaluation
from torch.utils.data import DataLoader
import torch
import numpy as np
from sklearn.metrics import accuracy_score, precision_recall_fscore_support, classification_report
from sklearn.metrics.pairwise import cosine_similarity
import pandas as pd
# Ejemplos de dominio-específico
examples = [
InputExample(texts=['¿Qué es el acceso abierto?', 'El acceso abierto es una práctica que permite a los usuarios leer, descargar, copiar, distribuir, imprimir, buscar o enlazar los textos completos de artículos científicos y de investigación y utilizarlos para cualquier otro fin legítimo, sin barreras financieras, legales o técnicas, con la única salvedad de las que supone Internet misma.'], label=1.0),
InputExample(texts=['Datos FAIR', 'Los principios FAIR (Findable, Accessible, Interoperable, Reusable) son un conjunto de directrices para la gestión y administración de datos de investigación, asegurando que los datos sean localizables, accesibles, interoperables y reutilizables tanto por humanos como por máquinas.'], label=1.0),
InputExample(texts=['¿Cómo se relaciona la ciencia ciudadana con la ciencia abierta?', 'La ciencia ciudadana es una faceta de la ciencia abierta que permite la participación del público en general en proyectos de investigación científica.'], label=1.0),
InputExample(texts=['¿Qué es la evaluación abierta?', 'La evaluación abierta se refiere a la revisión por pares y la evaluación de la investigación que es transparente y participativa.'], label=1.0),
InputExample(texts=['Repositorios de acceso abierto', 'Los repositorios son plataformas donde los investigadores pueden depositar sus publicaciones y datos para que estén disponibles en acceso abierto.'], label=1.0),
InputExample(texts=['Licencias Creative Commons', 'Las licencias Creative Commons son un conjunto de herramientas legales que permiten a los creadores compartir su trabajo de forma abierta, definiendo cómo otros pueden usar, distribuir y modificar sus obras.'], label=1.0),
InputExample(texts=['Plan de Gestión de Datos (PGD)', 'Un PGD es un documento formal que describe cómo se gestionarán los datos de investigación durante y después de un proyecto, incluyendo su recopilación, organización, almacenamiento, acceso y preservación.'], label=1.0),
InputExample(texts=['Identificadores persistentes (PIDs)', 'Los PIDs son identificadores únicos y permanentes utilizados para referenciar objetos digitales (como datasets, publicaciones o investigadores) de manera consistente a lo largo del tiempo, facilitando su descubrimiento y citación.'], label=1.0),
InputExample(texts=['Software de código abierto en investigación', 'El software de código abierto es fundamental para la reproducibilidad y transparencia en la investigación, permitiendo a otros usuarios acceder, modificar y ejecutar el código utilizado en un estudio.'], label=1.0),
InputExample(texts=['Métricas alternativas (Altmetrics)', 'Las altmetrics son métricas complementarias a las citas tradicionales que miden el impacto de la investigación en línea, como menciones en redes sociales, blogs, noticias o repositorios.'], label=1.0),
InputExample(texts=['¿Cuál es la diferencia entre acceso abierto y datos abiertos?', 'El acceso abierto se centra principalmente en que las publicaciones de investigación estén libremente disponibles, mientras que los datos abiertos se refieren a hacer que los datos subyacentes a la investigación sean accesibles y reutilizables.'], label=0.9),
InputExample(texts=['Repositorio de datos vs Repositorio de publicaciones', 'Un repositorio de datos almacena datasets de investigación, mientras que un repositorio de publicaciones almacena artículos, preprints u otros textos académicos.'], label=0.9),
InputExample(texts=['Peer review tradicional vs evaluación abierta', 'La revisión por pares tradicional suele ser ciega o doblemente ciega, mientras que la evaluación abierta busca mayor transparencia en el proceso, a veces revelando la identidad de los revisores y los comentarios.'], label=0.9),
InputExample(texts=['Licencia CC BY vs CC BY-SA', 'Ambas son licencias Creative Commons que requieren atribución, pero CC BY-SA (ShareAlike) además exige que las obras derivadas se distribuyan bajo la misma licencia.'], label=0.9),
InputExample(texts=['¿Qué significa reproducible?', 'La reproducibilidad en ciencia implica que un investigador, usando los mismos datos y código que el investigador original, puede obtener los mismos resultados.'], label=1.0),
InputExample(texts=['Plataforma de ciencia ciudadana', 'Ejemplos de plataformas incluyen Zooniverse, donde los voluntarios ayudan a analizar grandes conjuntos de datos.'], label=0.8),
InputExample(texts=['Identificador ORCID', 'ORCID es un identificador persistente para investigadores, ayudando a distinguirlos y vincular su producción científica.'], label=0.8),
InputExample(texts=['Preprint', 'Un preprint es una versión de un artículo de investigación que se publica antes de ser revisado por pares en una revista formal.'], label=1.0),
InputExample(texts=['Ciencia abierta en América Latina', 'La ciencia abierta en América Latina enfrenta desafíos y oportunidades únicas relacionadas con la infraestructura, las políticas y la cultura académica regional.'], label=1.0),
InputExample(texts=['¿Cómo define la UNESCO la ciencia abierta?', 'La ciencia abierta se define como un constructo inclusivo que combina diversos movimientos y prácticas con el fin de que los conocimientos científicos multilingües estén abiertamente disponibles y sean accesibles para todos, así como reutilizables por todos, se incrementen las colaboraciones científicas y el intercambio de información en beneficio de la ciencia y la sociedad, y se abran los procesos de creación, evaluación y comunicación de los conocimientos científicos a los agentes sociales más allá de la comunidad científica tradicional.'], label=1.0),
InputExample(texts=['¿Cuáles son los pilares fundamentales de la ciencia abierta según la UNESCO?', 'La ciencia abierta se basa en los siguientes pilares clave: conocimiento científico abierto, infraestructuras de la ciencia abierta, comunicación científica, participación abierta de los agentes sociales y diálogo abierto con otros sistemas de conocimiento.'], label=1.0)
]
# Crear conjunto de evaluación (usando una porción de los datos de ejemplo) Utilizo 70/30
# En un escenario real, deberías tener un conjunto de evaluación separado
eval_examples = examples[-6:] # Últimos 6 ejemplos para evaluación, es decir utilizo casi el 30% del conjunto de ejemplos
train_examples = examples[:-6] # Resto para entrenamiento, es decir, el 70 %.
def evaluate_model_metrics(model, eval_examples, threshold=0.8):
"""
Evalúa el modelo calculando métricas de clasificación basadas en similitud coseno
"""
print("\n" + "="*60)
print("EVALUACIÓN DE MÉTRICAS DEL MODELO")
print("="*60)
true_labels = []
predicted_labels = []
similarities = []
for example in eval_examples:
# Obtener embeddings de ambos textos
embedding1 = model.encode([example.texts[0]])
embedding2 = model.encode([example.texts[1]])
# Calcular similitud coseno
similarity = cosine_similarity(embedding1, embedding2)[0][0]
similarities.append(similarity)
# Convertir a etiqueta binaria usando threshold
predicted_label = 1 if similarity >= threshold else 0
true_label = 1 if example.label >= threshold else 0
predicted_labels.append(predicted_label)
true_labels.append(true_label)
print(f"Texto 1: {example.texts[0][:50]}...")
print(f"Texto 2: {example.texts[1][:50]}...")
print(f"Similitud real: {example.label:.3f} | Similitud predicha: {similarity:.3f}")
print(f"Etiqueta real: {true_label} | Etiqueta predicha: {predicted_label}")
print("-" * 40)
# Calcular métricas
accuracy = accuracy_score(true_labels, predicted_labels)
precision, recall, f1, support = precision_recall_fscore_support(
true_labels, predicted_labels, average='binary', zero_division=0
)
# Mostrar métricas principales
print(f"\n📊 MÉTRICAS DE RENDIMIENTO:")
print(f"Accuracy: {accuracy:.4f} ({accuracy*100:.2f}%)")
print(f"Precision: {precision:.4f} ({precision*100:.2f}%)")
print(f"Recall: {recall:.4f} ({recall*100:.2f}%)")
print(f"F1-Score: {f1:.4f} ({f1*100:.2f}%)")
# Estadísticas de similitud
similarities = np.array(similarities)
print(f"\n📈 ESTADÍSTICAS DE SIMILITUD:")
print(f"Similitud promedio: {np.mean(similarities):.4f}")
print(f"Desviación estándar: {np.std(similarities):.4f}")
print(f"Similitud mínima: {np.min(similarities):.4f}")
print(f"Similitud máxima: {np.max(similarities):.4f}")
# Reporte de clasificación detallado
print(f"\n📋 REPORTE DE CLASIFICACIÓN DETALLADO:")
print(classification_report(true_labels, predicted_labels,
target_names=['Baja similitud', 'Alta similitud'],
zero_division=0))
return {
'accuracy': accuracy,
'precision': precision,
'recall': recall,
'f1_score': f1,
'similarities': similarities,
'true_labels': true_labels,
'predicted_labels': predicted_labels
}
def compare_models_performance(original_model, fine_tuned_model, eval_examples):
"""
Compara el rendimiento entre el modelo original y el fine-tuned
"""
print("\n" + "="*60)
print("COMPARACIÓN DE MODELOS")
print("="*60)
print("Evaluando modelo ORIGINAL...")
original_metrics = evaluate_model_metrics(original_model, eval_examples)
print("\nEvaluando modelo FINE-TUNED...")
finetuned_metrics = evaluate_model_metrics(fine_tuned_model, eval_examples)
# Crear tabla comparativa
comparison_data = {
'Métrica': ['Accuracy', 'Precision', 'Recall', 'F1-Score', 'Similitud Promedio'],
'Modelo Original': [
f"{original_metrics['accuracy']:.4f}",
f"{original_metrics['precision']:.4f}",
f"{original_metrics['recall']:.4f}",
f"{original_metrics['f1_score']:.4f}",
f"{np.mean(original_metrics['similarities']):.4f}"
],
'Modelo Fine-tuned': [
f"{finetuned_metrics['accuracy']:.4f}",
f"{finetuned_metrics['precision']:.4f}",
f"{finetuned_metrics['recall']:.4f}",
f"{finetuned_metrics['f1_score']:.4f}",
f"{np.mean(finetuned_metrics['similarities']):.4f}"
]
}
df_comparison = pd.DataFrame(comparison_data)
print("\n📊 TABLA COMPARATIVA:")
print(df_comparison.to_string(index=False))
# Calcular mejoras
accuracy_improvement = finetuned_metrics['accuracy'] - original_metrics['accuracy']
f1_improvement = finetuned_metrics['f1_score'] - original_metrics['f1_score']
print(f"\n🚀 MEJORAS OBTENIDAS:")
print(f"Mejora en Accuracy: {accuracy_improvement:+.4f} ({accuracy_improvement*100:+.2f}%)")
print(f"Mejora en F1-Score: {f1_improvement:+.4f} ({f1_improvement*100:+.2f}%)")
# Cargar el modelo original para comparación
print("Cargando modelo original...")
original_model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-mpnet-base-v2')
# Evaluar modelo original ANTES del fine-tuning
print("Evaluando modelo ANTES del fine-tuning...")
original_metrics = evaluate_model_metrics(original_model, eval_examples)
# Load the model para fine-tuning
modelFT = SentenceTransformer('sentence-transformers/paraphrase-multilingual-mpnet-base-v2')
# Create a DataLoader
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
# Define the loss function
train_loss = losses.CosineSimilarityLoss(model=modelFT)
# Define number of epochs
num_epochs = 2
# Crear evaluador para seguimiento durante el entrenamiento
evaluator = evaluation.EmbeddingSimilarityEvaluator.from_input_examples(
eval_examples, name='eval'
)
# Fine-tuning con evaluación
print(f"\n🔄 Iniciando fine-tuning del modelo de embeddings por {num_epochs} época(s)...")
print("="*60)
model.fit(
train_objectives=[(train_dataloader, train_loss)],
epochs=num_epochs,
warmup_steps=100,
evaluator=evaluator,
evaluation_steps=50, # Evaluar cada 50 pasos
output_path="./fine_tuned_model"
)
print("\n✅ Fine-tuning completado!")
# Evaluar modelo DESPUÉS del fine-tuning
print("\nEvaluando modelo DESPUÉS del fine-tuning...")
finetuned_metrics = evaluate_model_metrics(modelFT, eval_examples)
# Comparar rendimiento
compare_models_performance(original_model, modelFT, eval_examples)
# Guardar el modelo
modelFT.save("fine_tuned_model")
print(f"\n💾 Modelo guardado en: ./fine_tuned_model")
# Función adicional para evaluar en nuevos ejemplos
def test_model_on_new_examples(model, new_examples):
"""
Función para probar el modelo en nuevos ejemplos
"""
print("\n" + "="*60)
print("PRUEBA CON NUEVOS EJEMPLOS")
print("="*60)
for i, (text1, text2) in enumerate(new_examples, 1):
embedding1 = modelFT.encode([text1])
embedding2 = modelFT.encode([text2])
similarity = cosine_similarity(embedding1, embedding2)[0][0]
print(f"Ejemplo {i}:")
print(f"Texto 1: {text1}")
print(f"Texto 2: {text2}")
print(f"Similitud: {similarity:.4f}")
print("-" * 40)
# Ejemplo de uso con nuevos textos
nuevos_ejemplos = [
("¿Qué son los datos abiertos?", "Los datos abiertos son datos que pueden ser utilizados, reutilizados y redistribuidos libremente por cualquier persona."),
("Política de ciencia abierta", "Las políticas institucionales promueven prácticas de acceso abierto en las universidades."),
("Gatos y perros", "Los datos FAIR son importantes para la investigación reproducible.")
]
print(f"\nProbando modelo fine-tuned con nuevos ejemplos...")
test_model_on_new_examples(modelFT, nuevos_ejemplos)
"""# **A. EN ETAPA DE DESARROLLO: Base de Conocimiento Estructurada**
### Nota: Ver la notebook base_conocimiento_en_desarrollo dentro del mismo repositorio.
## A.1 Taxonomía de Pilares de Ciencia Abierta
Para la definición de la taxonomÍa utilizada en el entrenamiento se revisaron los siguientes documentos, recomendaciones de CA de la UNESCO (2021) y Taxonomia da Ciência Aberta: revisada e ampliada, además del material del su curso de OpenScience 101 desarrollado por por Nasa. En estos se identificaron distintas taxonomías como Pontika et al. (2015), Baumgartner (2019), Silveira et al. (2021), y una última ampliada. Finalmente, se decidió establecer una clasificación basada en las recomendaciones de la Unesco para la ciencia abierta y en los módulos definidos en el curso de OpenScience 101. La misma consiste en: acceso_abierto.datos abiertos, codigo_abierto, evaluacion_abierta y ciencia_ciudadana.
Fuente: Artículo: SciELO Brasil - Taxonomia da Ciência Aberta: revisada e ampliada Taxonomia da Ciência Aberta: revisada e ampliada https://share.google/C0r8ikdeLFwAGjR2u.
"""
PILARES_CA = {
"acceso_abierto": {
"subcategorias": ["publicaciones", "repositorios", "preprints"],
"entidades": ["revistas", "editoriales", "licencias"]
},
"datos_abiertos": {
"subcategorias": ["FAIR", "repositorios", "metadatos"],
"entidades": ["formatos", "identificadores", "planes_gestion"]
},
"codigo_abierto": {
"subcategorias": ["software", "notebooks", "reproducibilidad"],
"entidades": ["licencias", "plataformas", "versionado", "lenguaje"]
},
"evaluacion_abierta": {
"subcategorias": ["peer_review", "metricas", "transparencia"],
"entidades": ["altmetrics", "sistemas_evaluacion"]
},
"ciencia_ciudadana": {
"subcategorias": ["participacion", "metodologias", "plataformas"],
"entidades": ["proyectos", "herramientas", "comunidades"]
}
}
"""# A.2 Estructura de Pares Pregunta-Respuesta"""
class QAPair:
def __init__(self):
self.pregunta = ""
self.respuesta = ""
self.pilar = ""
self.nivel_complejidad = "" # básico, intermedio, avanzado
self.audiencia = "" # maestría, doctorado, investigador
self.contexto_regional = "" # América Latina específico
self.fuentes = []
self.entidades_relacionadas = []
"""## 4.2 Integración y Evaluación de Modelos LLM"""