import nest_asyncio
nest_asyncio.apply()
from llama_index.core import (
VectorStoreIndex,
ServiceContext,
SimpleDirectoryReader,
load_index_from_storage,
)
from llama_index.core.storage import StorageContext
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.prompts import PromptTemplate
from llama_index.core.response_synthesizers import TreeSummarize
from llama_index.core.query_pipeline import InputComponent
from llama_index.core.indices.knowledge_graph import KGTableRetriever
from llama_index.legacy.vector_stores.faiss import FaissVectorStore
from llama_index.llms.openai import OpenAI
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.core import Settings
import os
from datetime import datetime
import gradio as gr
import pandas as pd
# Context:
exec(os.environ.get('context'))
##### Graph start ##########
from pyvis.network import Network
from io import StringIO
import html
def draw_graph():
global kg_data
# Crear un grafo con opciones visuales básicas
net = Network(
notebook=True,
height="700px",
width="100%",
select_menu=True,
neighborhood_highlight=True,
cdn_resources="remote"
)
# Añadir nodos y relaciones desde los datos del grafo
for source, relation, target in kg_data:
# Agregar nodos
net.add_node(source, label=source, color="skyblue")
net.add_node(target, label=target, color="skyblue")
# Agregar aristas con la relación como etiqueta
net.add_edge(source, target, title=relation, label=relation, color="black")
# Generar el HTML como string
html_output = StringIO()
net.write_html(html_output)
html_output.seek(0)
html_contenido = html_output.getvalue()
# Generar el iframe embebido
return generar_iframe_embebido(html_contenido)
def generar_iframe_embebido(html_contenido):
# Escapar el contenido HTML para que sea seguro dentro del atributo srcdoc
html_escapado = html.escape(html_contenido)
# Generar el string del iframe con el contenido embebido
iframe = f"""
"""
return iframe
##### Graph end ##########
##### Refs start ##########
import re
def extraer_informacion_metadata(respuesta, max_results=10):
# Obtener source_nodes de la respuesta
source_nodes = respuesta.source_nodes
# Obtener page_labels, file_names y scores de source_nodes
page_file_info = [
f"Página {node.node.metadata.get('page_label', '')} del archivo {node.node.metadata.get('file_name', '')} (Relevance: {node.score:.6f} - Id: {node.node.id_})\n\n"
for node in source_nodes if node.score <= 1 # Excluir nodos con score > 1
]
# Limitar la cantidad de resultados
page_file_info = page_file_info[:max_results]
return page_file_info
import html
def extraer_textos_metadata(respuesta, max_results=10):
# Obtener source_nodes de la respuesta
source_nodes = respuesta.source_nodes
# Obtener información de página, archivo y texto de cada nodo
page_file_text_info = []
for node in source_nodes:
if node.score <= 1: # Excluir nodos con score > 1
page_label = node.node.metadata.get('page_label', '')
file_name = node.node.metadata.get('file_name', '')
text = node.node.text.strip()
# Escapar caracteres especiales en el texto
escaped_text = html.escape(text)
# Formatear con HTML
formatted_text = f"""
Página {page_label} del archivo {file_name}
{escaped_text}
"""
page_file_text_info.append(formatted_text.strip()) # Quitar espacios adicionales
# Limitar la cantidad de resultados
page_file_text_info = page_file_text_info[:max_results]
return ''.join(page_file_text_info) # Devolver como un string limpio
##### Refs end ##########
##### Logs start ##########
import pandas as pd
from datasets import load_dataset, Dataset, DatasetDict
from huggingface_hub import login, HfApi, file_exists, hf_hub_download, list_repo_files
# HuggingFace Token:
HF_TOKEN = os.environ.get('hf')
# Definiciones
repo_name = "pharma-IA"
project_id = "gmpcolombia"
def save_to_dataset(user_message, response_text, user):
current_month = datetime.now().strftime('%Y-%m')
filename = f"logs_{current_month}.csv"
repo_id = f"{repo_name}/logs-{project_id}"
if file_exists(repo_id=repo_id, filename=f"{filename}", repo_type="dataset", token=HF_TOKEN):
local_filepath = hf_hub_download(
repo_id=repo_id,
filename=f"{filename}",
repo_type="dataset",
token=HF_TOKEN
)
df = pd.read_csv(local_filepath)
else:
df = pd.DataFrame(columns=["timestamp", "user_message", "response_text", "flag", "user"])
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
new_data = pd.DataFrame([{
"timestamp": timestamp,
"user_message": user_message,
"response_text": response_text,
"flag": "",
"user": user
}])
df = pd.concat([df, new_data], ignore_index=True)
df.to_csv(filename, index=False)
api = HfApi()
api.upload_file(
path_or_fileobj=filename,
path_in_repo=f"{filename}",
repo_id=repo_id,
token=HF_TOKEN,
repo_type="dataset"
)
def normalize_text(text):
return text.strip().lower()
def print_like_dislike(x: gr.LikeData):
#print(f"Value: {x.value}")
#print(f"Liked: {x.liked}")
if x is not None:
text_value = x.value if isinstance(x.value, str) else x.value.get('value', '')
current_month = datetime.now().strftime('%Y-%m')
filename = f"logs_{current_month}.csv"
repo_id = f"{repo_name}/logs-{project_id}"
if file_exists(repo_id=repo_id, filename=f"{filename}", repo_type="dataset", token=HF_TOKEN):
local_filepath = hf_hub_download(
repo_id=repo_id,
filename=f"{filename}",
repo_type="dataset",
token=HF_TOKEN
)
df = pd.read_csv(local_filepath)
#print(df.head()) # Verifica el contenido del archivo CSV
normalized_value = normalize_text(text_value)
df['normalized_response_text'] = df['response_text'].apply(normalize_text)
response_indices = df.index[df['normalized_response_text'].str.contains(normalized_value, na=False, regex=False)].tolist()
print(f"Response Indices: {response_indices}")
if response_indices:
response_index = response_indices[-1]
print(f"Updating index: {response_index} with value: {x.liked}")
# Solo actualiza el valor de 'flag'
df['flag'] = df['flag'].astype(object)
df.at[response_index, 'flag'] = str(x.liked)
df = df.drop(columns=['normalized_response_text'])
df.to_csv(filename, index=False)
api = HfApi()
api.upload_file(
path_or_fileobj=filename,
path_in_repo=f"{filename}",
repo_id=repo_id,
token=HF_TOKEN,
repo_type="dataset"
)
else:
print("No matching response found to update.")
else:
print(f"File {filename} does not exist in the repository.")
else:
print("x is None.")
def save_evals_to_dataset(query, faithfulness_score, ans_relevancy_score, ctx_relevancy_score):
current_month = datetime.now().strftime('%Y-%m')
filename = f"logs_{current_month}.csv"
repo_id = f"{repo_name}/logs-{project_id}"
if file_exists(repo_id=repo_id, filename=f"{filename}", repo_type="dataset", token=HF_TOKEN):
local_filepath = hf_hub_download(
repo_id=repo_id,
filename=f"{filename}",
repo_type="dataset",
token=HF_TOKEN
)
df = pd.read_csv(local_filepath)
else:
print(f"File {filename} does not exist in the repository.")
return
# Normalizamos el query para la comparación
normalized_query = normalize_text(query).lower() # Convertimos a minúsculas
# Buscamos la última entrada que coincida con el query, convirtiendo a minúsculas en la comparación
matching_indices = df.index[df['user_message'].str.lower().str.contains(normalized_query, na=False, regex=False)].tolist()
if matching_indices:
last_index = matching_indices[-1] # Tomamos la última coincidencia
# Agregamos los puntajes a las columnas correspondientes
df.at[last_index, 'groundedness'] = faithfulness_score
df.at[last_index, 'answer_rel'] = ans_relevancy_score
df.at[last_index, 'context_rel'] = ctx_relevancy_score
df.to_csv(filename, index=False)
api = HfApi()
api.upload_file(
path_or_fileobj=filename,
path_in_repo=f"{filename}",
repo_id=repo_id,
token=HF_TOKEN,
repo_type="dataset"
)
else:
print("No matching query found in the dataset.")
# Función para verificar si un archivo existe en el repositorio
def file_exists(repo_id, filename, repo_type="dataset", token=None):
files = list_repo_files(repo_id=repo_id, repo_type=repo_type, token=token)
return filename in files
# Función para cargar las hojas de auditoría disponibles
def load_available_logs():
repo_id = f"{repo_name}/logs-{project_id}"
files = list_repo_files(repo_id=repo_id, repo_type="dataset", token=HF_TOKEN)
# Filtramos los archivos CSV con formato 'logs_YYYY-MM.csv'
available_months = [f.split('_')[1].replace('.csv', '') for f in files if f.startswith('logs_')]
return available_months
# Cargar los logs del mes seleccionado
def load_audit_trail(selected_month):
filename = f"logs_{selected_month}.csv"
repo_id = f"{repo_name}/logs-{project_id}"
if file_exists(repo_id=repo_id, filename=filename, repo_type="dataset", token=HF_TOKEN):
local_filepath = hf_hub_download(
repo_id=repo_id,
filename=filename,
repo_type="dataset",
token=HF_TOKEN
)
df = pd.read_csv(local_filepath)
# Convertir el campo 'timestamp' a una cadena con el formato UTC-0
df["timestamp"] = pd.to_datetime(df["timestamp"]).dt.strftime('%Y-%m-%d %H:%M:%S UTC-0')
# Ordenar por la columna timestamp de forma descendente
df = df.sort_values(by="timestamp", ascending=False)
# Renombrar las columnas para visualización
df = df.rename(columns={
"timestamp": "Marca de Tiempo",
"user_message": "Mensaje Usuario",
"response_text": "Respuesta",
"flag": "Etiqueta",
"user": "Usuario",
"groundedness": "Groundedness",
"answer_rel": "Answer Relev.",
"context_rel": "Context Relev."
})
return df
else:
return pd.DataFrame(columns=["Marca de Tiempo", "Mensaje Usuario", "Respuesta", "Etiqueta", "Usuario"])
##### Logs end ##########
##### Evaluate start ##########
from llama_index.core.evaluation import FaithfulnessEvaluator
from llama_index.core.evaluation import RelevancyEvaluator
from llama_index.core.evaluation import AnswerRelevancyEvaluator
from llama_index.core.evaluation import ContextRelevancyEvaluator
final_response = ""
query = ""
def ctx_relevancy_eval():
global final_response
global query
# Verificamos si 'final_response' tiene el atributo 'source_nodes'
if not hasattr(final_response, 'source_nodes'):
raise AttributeError("El objeto 'final_response' no tiene un atributo 'source_nodes'.")
# Obtener los source_nodes de la respuesta
source_nodes = final_response.source_nodes
# Extraer los textos de los source nodes
contexts = []
for node in source_nodes:
if node.score <= 1: # Excluir nodos con score > 1
text = node.node.text.strip()
contexts.append(text)
if not contexts:
raise ValueError("No se encontraron textos en los source nodes.")
evaluator = ContextRelevancyEvaluator(llm=gpt4omini)
evaluation_result = evaluator.evaluate(query=query, contexts=contexts)
# Extraer el puntaje de la evaluación
relevancy_score = evaluation_result.score
return relevancy_score
def faithfulness_eval():
global final_response
global query
# Verificamos si 'final_response' tiene el atributo 'source_nodes'
if not hasattr(final_response, 'source_nodes'):
raise AttributeError("El objeto 'final_response' no tiene un atributo 'source_nodes'.")
# Obtener los source_nodes de la respuesta
source_nodes = final_response.source_nodes
# Extraer los textos de los source nodes
contexts = []
for node in source_nodes:
if node.score <= 1: # Excluir nodos con score > 1
text = node.node.text.strip()
contexts.append(text)
if not contexts:
raise ValueError("No se encontraron textos en los source nodes.")
evaluator = FaithfulnessEvaluator(llm=llm)
eval_result = evaluator.evaluate(query=query, response=final_response.response, contexts=contexts)
print("Groundedness: " + str(eval_result.score) + " - " + str(eval_result.passing) + "\n")
print("Respuesta: " + str(final_response) + "\n\n----------")
print("Contexts: " + str(contexts) + "\n\n----------")
return float(eval_result.score)
def ans_relevancy_eval():
global final_response
global query
evaluator = AnswerRelevancyEvaluator(llm=gpt4omini)
eval_result = evaluator.evaluate(query=query, response=final_response.response)
return float(eval_result.score)
def evaluate():
global query
# Evaluaciones y escalado
faithfulness_score = round(faithfulness_eval() * 5, 1) # Redondear a un decimal
ans_relevancy_score = round(ans_relevancy_eval() * 5, 1) # Redondear a un decimal
ctx_relevancy_score = round(ctx_relevancy_eval() * 5, 1) # Redondear a un decimal
# Llamamos a save_evals_to_dataset
save_evals_to_dataset(query, faithfulness_score, ans_relevancy_score, ctx_relevancy_score)
def get_color(value):
if value <= 1.6667:
return '#f07b61' # Rojo
elif value <= 3.3334:
return '#f3e076' # Amarillo
else:
return '#84fa57' # Verde
color1 = get_color(faithfulness_score)
color2 = get_color(ans_relevancy_score)
color3 = get_color(ctx_relevancy_score)
html_output = f"""
{faithfulness_score}
Groundedness
{ans_relevancy_score}
Answer Relevance
{ctx_relevancy_score}
Context Relevance
"""
return html_output
##### Evaluate end ##########
chat_history_engine = []
result_metadata = ""
result_texts = ""
result_evals = ""
css = """
.block {
background: rgba(245, 247, 249, 0.7) !important;
}
#component-1 {
background: transparent !important;
}
.block.accordion > button:first-of-type span {
font-size: medium !important;
font-weight: bold !important;
}
.examples .block {
background: transparent !important;
}
table {
font-size: x-small !important;
}
#btn_select {
width:100px;
}
#select_list label {
width: 100%;
}
"""
choices_with_tools = [
("[1] Guía No Conformidades GLP", retriever_1_tool),
("[2] Guía No Conformidades GMP", retriever_2_tool),
("[3] MANUAL DE NORMAS TÉCNICAS DE CALIDAD", retriever_3_tool),
("[4] Resolución 1160 - GMP - 2016", retriever_4_tool),
# ("[5] Resolución 2266 - DECRETO FITOTERAPÉUTICOS - 2004", retriever_5_tool),
("[6] Resolución 3619 - GLP - 2013", retriever_6_tool),
# ("[7] Resolución 005107 - FITOTERAPEUTICOS - 2005", retriever_7_tool),
("[8] Resolución 3690 - 2016", retriever_8_tool),
("[9] Resolución 3157 - 2018", retriever_9_tool),
("[10] Preguntas y respuestas Res. 3157 - 2018", retriever_10_tool),
("General", retriever_all_tool),
]
# Solo extraer los nombres para mostrarlos en la interfaz
selected_choices = [label for label, _ in choices_with_tools]
choice_labels = [label for label, _ in choices_with_tools]
with gr.Blocks(theme='sudeepshouche/minimalist', css=css) as demo:
# Actualizar las choices seleccionadas
def update_selected_choices(choices):
global selected_choices
selected_choices = choices
# Alternar la selección
def toggle_all(selected):
if len(selected) == len(choice_labels):
return [] # Deseleccionar todos
else:
return choice_labels # Seleccionar todos
def get_ref():
return {mkdn: gr.Markdown(result_metadata), texts: gr.HTML(str(result_texts))}
def get_logs(selected_month):
df = load_audit_trail(selected_month)
return df
def get_evals():
global result_evals
global final_response
global query
# Verificar si 'final_response' está vacío
if not final_response:
# Si no hay final_response pero hay un resultado previo en result_evals, devolverlo
if result_evals:
return {evals: gr.HTML(f"""
{result_evals}
Esta evaluación corresponde a la consulta: {query}
""")}
# Si no hay final_response ni resultados previos, mostrar advertencia
gr.Info("Se necesita una respuesta completa para iniciar la evaluación.")
return {evals: gr.HTML(f"""
Se necesita una respuesta completa para iniciar la evaluación.
""")}
# Ejecuta la evaluación si final_response está disponible
result_evals = evaluate()
# Reiniciar 'final_response' después de la evaluación
final_response = ""
# Devolver el resultado de la evaluación
return {evals: gr.HTML(f"""{result_evals}"""), eval_accord: gr.Accordion(elem_classes="accordion", label="Evaluaciones", open=True)}
def refresh(chat_history):
global kg_data
global chat_history_engine
global result_metadata
kg_data = []
chat_history_engine = []
result_metadata = ""
chat_history = [[None, None]]
return chat_history
def summarize_assistant_messages(chat_history: List[ChatMessage]) -> List[ChatMessage]:
# Encontrar la anteúltima respuesta del asistente
assistant_messages = [msg for msg in chat_history if msg.role == MessageRole.ASSISTANT]
if len(assistant_messages) < 2:
return chat_history # No hay suficientes mensajes del asistente para resumir
anteultima_respuesta = assistant_messages[-2]
# Usar GPT-3.5 para generar un resumen de la anteúltima respuesta del asistente
prompt = Prompt(f"Responder SOLO con un resumen del siguiente texto: \n\n{anteultima_respuesta.content}")
response = gpt4omini.predict(prompt)
# Crear un nuevo ChatMessage con el resumen como contenido y el rol de asistente
summarized_message = ChatMessage(content=response, role=MessageRole.ASSISTANT)
# Reconstruir el historial de chat reemplazando la anteúltima respuesta del asistente con el resumen
new_chat_history = [msg if msg != anteultima_respuesta else summarized_message for msg in chat_history]
return new_chat_history
def respond(message, chat_history):
global chat_history_engine
global result_metadata
global result_texts
global final_response
global query
global selected_choices
global bm_status # Asegúrate de declarar bm_status como global
# Inicializar el historial de chat si está vacío con el mensaje del usuario actual
if not chat_history:
chat_history = [[message, ""]]
else:
# Agregar el mensaje actual al historial de chat
chat_history.append([message, ""])
# Resumir los mensajes previos en chat_history_engine
chat_history_engine = summarize_assistant_messages(chat_history_engine)
# Determina si 'retriever_all_tool' está en los retrievers seleccionados y ajusta 'bm_status'
bm_status = any(tool is retriever_all_tool for label, tool in choices_with_tools if label in selected_choices)
# Engine:
retriever_tools = [tool for choice, tool in choices_with_tools if choice in selected_choices]
print("Choice: " + str(retriever_tools))
# Configuración dinámica de RouterRetriever y engine
retriever = RouterRetriever(
selector=PydanticMultiSelector.from_defaults(
llm=llm, prompt_template_str=DEFAULT_MULTI_PYD_SELECT_PROMPT_TMPL, max_outputs=3
),
retriever_tools=retriever_tools,
)
custom_retriever = CustomRetriever(retriever, kg_retriever)
query_engine = RetrieverQueryEngine.from_args(
retriever=custom_retriever,
response_synthesizer=response_synthesizer,
streaming=True
)
memory = ChatMemoryBuffer.from_defaults(token_limit=20000)
chat_engine = ContextChatEngine.from_defaults(
retriever=custom_retriever,
system_prompt=system_prompt,
memory=memory,
node_postprocessors=[],
context_template=context_prompt,
llm=gpt4omini
)
# Generar la respuesta usando el motor de chat
response = chat_engine.stream_chat(message, chat_history=chat_history_engine)
# Extraer la información de los metadatos y textos de la respuesta
metadata_info = extraer_informacion_metadata(response, max_results=10)
texts_info = extraer_textos_metadata(response, max_results=10)
if metadata_info:
result_metadata = "\n".join(metadata_info)
if texts_info:
result_texts = texts_info
# Procesar la respuesta generada y agregarla al historial del chat
for text in response.response_gen:
chat_history[-1][1] += text
yield "", chat_history
# Guardar la conversación en el dataset
save_to_dataset(message, chat_history[-1][1], "no-ingresado")
final_response = response
query = message
gr.Markdown("""
# PharmaWise GMP Colombia Chat 4.7
Realiza preguntas a tus datos y obtén al final del texto las paginas y documentos utilizados generar tu responder.
""")
with gr.Row():
with gr.Column():
chatbot = gr.Chatbot(show_label=False, show_copy_button=True, ) #layout="panel"
pregunta = gr.Textbox(show_label=False, autofocus=True, placeholder="Realiza tu consulta...")
pregunta.submit(respond, [pregunta, chatbot], [pregunta, chatbot])
with gr.Row():
btn_send = gr.Button(value="Preguntar", variant="primary")
clear = gr.Button(value="Limpiar")
with gr.Row(elem_classes="examples"):
gr.Examples(label="Ejemplos", examples=["Implementación de la res. 3157 de 2018"], inputs=[pregunta])
with gr.Column():
with gr.Accordion(elem_classes="accordion", label="Bases de datos del conocimiento", open=False):
# CheckboxGroup
chx = gr.CheckboxGroup(choices=choice_labels, value=choice_labels, show_label=False, elem_id="select_list")
chx.select(fn=update_selected_choices, inputs=chx)
# Seleccionar/deseleccionar todos
toggle_button = gr.Button("Selección", elem_id="btn_select")
toggle_button.click(fn=toggle_all, inputs=chx, outputs=chx)
with gr.Accordion(elem_classes="accordion", label="Referencias", open=True):
mkdn = gr.Markdown()
with gr.Row():
btn_graph = gr.Button(value="Grafo")
btn_ref = gr.Button(value="Referencias")
btn_eval = gr.Button(value="Evaluar")
eval_accord = gr.Accordion(elem_classes="accordion", label="Evaluaciones", open=False)
with eval_accord:
evals = gr.HTML()
gr.Markdown("""| **Evaluador** | **Qué mide** | **Ejemplo de uso** | **Diferencias clave** |
|-----------------------|-------------------------------------------------------|-------------------------------------------------------|--------------------------------------------------------|
| **Groundedness** | Qué tan fundamentada está la respuesta en el contexto. | ¿La respuesta está respaldada por el contexto proporcionado? | Se enfoca en la relación entre la respuesta y el contexto. |
| **Answer Relevance** | Qué tan relevante es la respuesta para la consulta. | ¿La respuesta es pertinente a lo que el usuario preguntó? | Se centra en la relevancia de la respuesta ante la consulta. |
| **Context Relevance** | Qué tan relevante es el contexto recuperado para la consulta. | ¿El contexto obtenido es relevante para la consulta del usuario? | Se enfoca en la pertinencia del contexto en relación con la consulta. |
""")
with gr.Row():
grafo = gr.HTML(label="Grafo", show_share_button=False)
with gr.Accordion(elem_classes="accordion", label="Audit trail", open=False):
with gr.Row():
with gr.Column():
available_months = load_available_logs()
default_month = available_months[-1] if available_months else None
dropdown = gr.Dropdown(choices=available_months, label="Seleccionar mes", value=default_month)
btn_logs = gr.Button(value="Actualizar")
with gr.Column():
gr.Markdown()
with gr.Column():
gr.Markdown()
# Define un DataFrame con un ancho fijo para response_text
logs_df = gr.DataFrame(headers=["Marca de Tiempo", "Mensaje Usuario", "Respuesta", "Etiqueta", "Usuario"], wrap=True, line_breaks=True)
with gr.Accordion(elem_classes="accordion", label="Referencias ampliadas", open=False):
texts = gr.HTML()
btn_logs.click(fn=get_logs, inputs=[dropdown], outputs=[logs_df])
btn_ref.click(fn=get_ref, outputs=[mkdn, texts])
btn_eval.click(fn=get_evals, outputs=[evals, eval_accord])
btn_send.click(respond, [pregunta, chatbot], [pregunta, chatbot])
btn_graph.click(draw_graph, outputs=[grafo])
clear.click(refresh, inputs=[chatbot], outputs=[chatbot])
chatbot.like(print_like_dislike, None, None)
demo.queue(default_concurrency_limit=20)
demo.launch()