Spaces:
Sleeping
Sleeping
File size: 9,235 Bytes
11840ab db69bb3 6998f76 49cd656 11840ab db69bb3 11840ab 773b423 db69bb3 49cd656 6998f76 30b2755 11840ab 49cd656 11840ab 49cd656 11840ab 49cd656 11840ab 49cd656 11840ab 49cd656 11840ab 30b2755 49cd656 11840ab 49cd656 11840ab 49cd656 11840ab 49cd656 11840ab 49cd656 11840ab 49cd656 db69bb3 49cd656 11840ab 49cd656 11840ab 49cd656 db69bb3 11840ab 49cd656 11840ab 49cd656 11840ab 49cd656 11840ab 49cd656 11840ab 49cd656 11840ab 49cd656 db69bb3 11840ab 49cd656 30b2755 49cd656 11840ab db69bb3 11840ab 49cd656 11840ab 49cd656 11840ab 49cd656 11840ab 6998f76 8e233d4 49cd656 11840ab 49cd656 11840ab 49cd656 11840ab 49cd656 11840ab 30b2755 | 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 | # app.py
import os
import gradio as gr
# LangChain + Google Gemini
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import PyPDFLoader, TextLoader, Docx2txtLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate
# -----------------------
# Utilidades
# -----------------------
def get_api_key():
# Lee secretos en runtime para evitar "faltan" claves si el env se inyectó después del import
return os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
def _get_path(file_obj):
if file_obj is None:
return None
if hasattr(file_obj, "name"):
return file_obj.name
if isinstance(file_obj, str):
return file_obj
if isinstance(file_obj, dict) and "name" in file_obj:
return file_obj["name"]
return None
def _load_docs(path):
ext = (path or "").lower().split(".")[-1]
if ext == "pdf":
return PyPDFLoader(path).load()
if ext == "docx":
return Docx2txtLoader(path).load()
if ext == "txt":
return TextLoader(path, encoding="utf-8").load()
raise ValueError(f"Formato no soportado: .{ext} (usa PDF, DOCX o TXT)")
def _split(docs):
splitter = RecursiveCharacterTextSplitter(
chunk_size=800, chunk_overlap=120, separators=["\n\n", "\n", ". ", " ", ""]
)
return splitter.split_documents(docs)
def build_chain(retriever, system_message, temperature, api_key):
if retriever is None:
raise RuntimeError("Primero sube e indexa un documento.")
llm = ChatGoogleGenerativeAI(
model="gemini-2.0-flash",
temperature=float(temperature),
google_api_key=api_key,
)
template = (
f"{system_message}\n\n"
"Responde SOLO usando el CONTEXTO recuperado. "
"Si no está en el contexto, di explícitamente que no aparece.\n\n"
"CONTEXTO:\n{{context}}\n\n"
"PREGUNTA:\n{{question}}\n\n"
"RESPUESTA:"
)
prompt = PromptTemplate(template=template, input_variables=["context", "question"])
qa = RetrievalQA.from_chain_type(
llm=llm,
retriever=retriever,
chain_type="stuff",
return_source_documents=True,
chain_type_kwargs={"prompt": prompt},
)
return qa
# -----------------------
# Callbacks (sin anotaciones de tipos para evitar schema issues)
# -----------------------
def on_upload(file, system_message, temperature, emb_state, retr_state, qa_state, last_sys_state, last_temp_state):
api_key = get_api_key()
if not api_key:
return "❌ Falta configurar GOOGLE_API_KEY o GEMINI_API_KEY en Settings → Secrets.", None, None, None, None, None
path = _get_path(file)
if not path:
return "No se ha subido ningún archivo.", emb_state, retr_state, qa_state, last_sys_state, last_temp_state
try:
docs = _load_docs(path)
splits = _split(docs)
# Embeddings (crear si no hay)
emb = emb_state
if emb is None:
emb = GoogleGenerativeAIEmbeddings(model="text-embedding-004", google_api_key=api_key)
# Vectorstore + retriever
vs = FAISS.from_documents(splits, emb)
retriever = vs.as_retriever(search_kwargs={"k": 4})
first = splits[0].page_content if splits else ""
preview = first[:500].replace("\n", " ") + ("..." if len(first) > 500 else "")
# Resetear QA y marcas
qa = None
last_sys = None
last_temp = None
msg = f"✅ Documento indexado. Chunks: {len(splits)}\n\nPreview:\n{preview}"
return msg, emb, retriever, qa, last_sys, last_temp
except Exception as e:
return f"❌ Error al procesar el archivo: {e}", emb_state, retr_state, qa_state, last_sys_state, last_temp_state
def on_user_send(message, history):
if not message:
return "", history
history = (history or []) + [{"role": "user", "content": message}]
return "", history
def on_bot_reply(history, system_message, max_tokens, temperature, emb_state, retr_state, qa_state, last_sys_state, last_temp_state):
api_key = get_api_key()
if not api_key:
return (history or []) + [{"role": "assistant", "content": "❌ Falta GOOGLE_API_KEY o GEMINI_API_KEY."}], qa_state, last_sys_state, last_temp_state
if not history:
return history, qa_state, last_sys_state, last_temp_state
last = history[-1]
if last.get("role") != "user":
return history, qa_state, last_sys_state, last_temp_state
retriever = retr_state
if retriever is None:
return history + [{"role": "assistant", "content": "Primero sube e indexa un documento."}], qa_state, last_sys_state, last_temp_state
# (Re)construir chain si cambió sistema o temperatura o no existe
need_build = qa_state is None or last_sys_state != system_message or last_temp_state != float(temperature)
if need_build:
qa_state = build_chain(retriever, system_message, float(temperature), api_key)
try:
res = qa_state.invoke({"query": last["content"]})
answer = res.get("result", "") or "No se obtuvo respuesta."
sources = res.get("source_documents", [])
if sources:
lines = []
for i, d in enumerate(sources, 1):
src = d.metadata.get("source", "doc")
page = d.metadata.get("page", "?")
preview = (d.page_content or "")[:140].replace("\n", " ")
lines.append(f"{i}. {src} (pág. {page}) → {preview} ...")
answer += "\n\n📚 Fuentes:\n" + "\n".join(lines)
history = history + [{"role": "assistant", "content": answer}]
last_sys_state = system_message
last_temp_state = float(temperature)
return history, qa_state, last_sys_state, last_temp_state
except Exception as e:
return history + [{"role": "assistant", "content": f"❌ Error: {e}"}], qa_state, last_sys_state, last_temp_state
# -----------------------
# UI
# -----------------------
with gr.Blocks(title="Chat RAG con LangChain + Gemini") as demo:
gr.Markdown("# 🤖 Chat RAG con LangChain + Gemini (Google)\nSube un documento (PDF, DOCX, TXT) y hazle preguntas.")
# Estados atómicos (evitamos dicts)
emb_state = gr.State(None) # GoogleGenerativeAIEmbeddings
retr_state = gr.State(None) # retriever
qa_state = gr.State(None) # RetrievalQA
last_sys_state = gr.State(None) # str
last_temp_state = gr.State(None) # float
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 📄 Subir e indexar documento")
file_upload = gr.File(label="Selecciona un archivo", file_types=[".pdf", ".docx", ".txt"])
file_status = gr.Textbox(label="Estado del archivo", interactive=False, max_lines=10)
gr.Markdown("### ⚙️ Configuración")
system_message = gr.Textbox(
value=(
"Eres un asistente que responde preguntas basándose EXCLUSIVAMENTE en el documento proporcionado. "
"Si la información no aparece en el contexto recuperado, indícalo explícitamente."
),
label="Mensaje del sistema",
max_lines=4
)
max_tokens = gr.Slider(50, 2048, value=512, step=50, label="Máximo de tokens (informativo)")
temperature = gr.Slider(0.0, 1.0, value=0.2, step=0.1, label="Temperatura")
with gr.Column(scale=2):
gr.Markdown("### 💬 Chat")
chatbot = gr.Chatbot(label="Conversación", height=520, type="messages")
msg = gr.Textbox(label="Tu pregunta", placeholder="Escribe tu pregunta sobre el documento...")
with gr.Row():
submit_btn = gr.Button("Enviar", variant="primary")
clear_btn = gr.Button("Limpiar chat")
# Eventos
file_upload.change(
fn=on_upload,
inputs=[file_upload, system_message, temperature, emb_state, retr_state, qa_state, last_sys_state, last_temp_state],
outputs=[file_status, emb_state, retr_state, qa_state, last_sys_state, last_temp_state],
)
msg.submit(
fn=on_user_send,
inputs=[msg, chatbot],
outputs=[msg, chatbot],
queue=False
).then(
fn=on_bot_reply,
inputs=[chatbot, system_message, max_tokens, temperature, emb_state, retr_state, qa_state, last_sys_state, last_temp_state],
outputs=[chatbot, qa_state, last_sys_state, last_temp_state],
)
submit_btn.click(
fn=on_user_send,
inputs=[msg, chatbot],
outputs=[msg, chatbot],
queue=False
).then(
fn=on_bot_reply,
inputs=[chatbot, system_message, max_tokens, temperature, emb_state, retr_state, qa_state, last_sys_state, last_temp_state],
outputs=[chatbot, qa_state, last_sys_state, last_temp_state],
)
clear_btn.click(lambda: [], None, chatbot, queue=False)
# Lanzar (evitamos API explorer)
demo.queue().launch(show_api=False)
|