danielspba's picture
Update app.py
e00b0c8 verified
import os
import sqlite3
import pandas as pd
from dotenv import load_dotenv
from bs4 import BeautifulSoup
import requests
import gradio as gr
import traceback # Para melhor formatação de erros
import tempfile # Para lidar com arquivos enviados
# Importações LangChain específicas
from langchain_community.document_loaders import WebBaseLoader, PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
# --- Configuração Inicial ---
# Carrega chave da API (ajuste conforme sua necessidade)
load_dotenv()
# Certifique-se que as variáveis de ambiente estão corretas!
# Exemplo genérico, use as suas variáveis:
# os.environ["OPENAI_API_KEY"] = os.getenv("OPENROUTER_API_KEY") # Ou OPENAI_API_KEY
# os.environ["OPENAI_API_BASE"] = os.getenv("OPENROUTER_API_BASE") # Ou omita se usar OpenAI direto
# Verifique se a chave API está carregada (adicione um check)
api_key = os.getenv("OPENROUTER_API_KEY") or os.getenv("OPENAI_API_KEY")
if not api_key:
print("⚠️ Atenção: Nenhuma chave de API encontrada nas variáveis de ambiente (OPENROUTER_API_KEY ou OPENAI_API_KEY).")
# Você pode querer parar a execução aqui ou usar um modelo local se configurado.
# exit() # Descomente para parar se a API for essencial
# Use as variáveis corretas para seu endpoint (OpenRouter ou OpenAI)
openai_api_key = os.getenv("OPENROUTER_API_KEY") # Ou os.getenv("OPENAI_API_KEY")
openai_api_base = os.getenv("OPENROUTER_API_BASE") # Opcional, remova se usar OpenAI direto
# Embeddings (modelo local, não requer API)
print("Carregando modelo de embeddings (pode levar um tempo)...")
embeddings_model_name = "all-MiniLM-L6-v2"
try:
embeddings = HuggingFaceEmbeddings(model_name=embeddings_model_name)
print(f"Modelo de embeddings '{embeddings_model_name}' carregado.")
except Exception as e:
print(f"❌ Erro ao carregar embeddings: {e}")
print("Verifique sua conexão com a internet ou se o modelo está disponível.")
embeddings = None # Define como None para checagem posterior
# LLM (ajuste o modelo conforme disponibilidade/preferência)
# Use um modelo disponível no seu endpoint (OpenRouter ou OpenAI)
# Ex: "gpt-3.5-turbo", "deepseek/deepseek-r1:free", etc.
llm_model_name = "deepseek/deepseek-r1:free" # Exemplo OpenRouter - TROQUE SE NECESSÁRIO
try:
llm = ChatOpenAI(
model=llm_model_name,
temperature=0.5,
openai_api_key=openai_api_key,
base_url=openai_api_base # Passe None se estiver usando OpenAI diretamente
)
print(f"LLM '{llm_model_name}' configurado.")
except Exception as e:
print(f"❌ Erro ao configurar LLM: {e}")
llm = None # Define como None para checagem posterior
# Memória da conversa (pode ser global)
memoria = ConversationBufferMemory(memory_key="chat_history", return_messages=True, output_key="answer")
# --- Banco de Dados ---
DB_FILE = "historico_conversas_multidoc.db"
def inicializar_db():
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS conversas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
aluno TEXT,
documento TEXT, -- Nova coluna
pergunta TEXT,
resposta TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
print(f"Banco de dados '{DB_FILE}' inicializado/verificado.")
inicializar_db() # Garante que a DB exista ao iniciar
def salvar_conversa(nome, documento, pergunta, resposta):
if not documento:
documento = "Nenhum Documento Carregado"
try:
conn = sqlite3.connect(DB_FILE)
cursor = conn.cursor()
cursor.execute("INSERT INTO conversas (aluno, documento, pergunta, resposta) VALUES (?, ?, ?, ?)",
(nome or "Anônimo", documento, pergunta, resposta))
conn.commit()
conn.close()
except Exception as e:
print(f"❌ Erro ao salvar conversa no DB: {e}")
# Não retorna o erro para a interface, apenas loga no console
# --- Funções Principais ---
def processar_documento(arquivo_pdf, url, progress=gr.Progress(track_tqdm=True)):
print("Arquivo recebido:", arquivo_pdf)
if arquivo_pdf is not None:
print("Nome do arquivo temporário:", getattr(arquivo_pdf, 'name', None))
def processar_documento(arquivo_pdf, url, progress=gr.Progress(track_tqdm=True)):
"""Carrega, divide e cria o vector store para um PDF ou URL."""
if not embeddings or not llm:
return None, None, "❌ Erro: Embeddings ou LLM não foram carregados corretamente. Verifique o console.", ""
docs = []
documento_nome = None
temp_dir = None # Para limpar arquivos temporários
progress(0, desc="Iniciando...")
try:
if arquivo_pdf is not None:
documento_nome = os.path.basename(arquivo_pdf.name)
progress(0.1, desc=f"Carregando PDF: {documento_nome}")
# Gradio fornece um objeto de arquivo temporário.
# PyPDFLoader precisa do caminho do arquivo.
loader = PyPDFLoader(arquivo_pdf.name)
docs = loader.load()
print(f"PDF '{documento_nome}' carregado, {len(docs)} páginas.")
elif url and url.strip():
documento_nome = url.strip()
progress(0.1, desc=f"Carregando URL: {documento_nome}")
loader = WebBaseLoader(documento_nome)
docs = loader.load()
print(f"URL '{documento_nome}' carregada, {len(docs)} documentos (partes).")
else:
return None, None, "⚠️ Por favor, forneça um arquivo PDF ou uma URL.", ""
if not docs:
return None, None, f"❌ Erro: Não foi possível extrair conteúdo de '{documento_nome}'.", documento_nome
progress(0.4, desc="Dividindo documento...")
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
documents = text_splitter.split_documents(docs)
print(f"Documento dividido em {len(documents)} chunks.")
if not documents:
return None, None, "❌ Erro: Documento vazio após divisão.", documento_nome
progress(0.6, desc="Criando embeddings e vector store (pode levar tempo)...")
vectordb = FAISS.from_documents(documents, embeddings)
retriever = vectordb.as_retriever()
print("Vector store FAISS criado.")
progress(0.9, desc="Limpando memória da conversa anterior...")
memoria.clear() # Limpa o histórico ao carregar novo doc
print("Memória da conversa resetada.")
progress(1, desc="Documento processado!")
status = f"✅ Documento '{documento_nome}' carregado e pronto para consulta."
return retriever, documento_nome, status, "" # Limpa campo de pergunta
except Exception as e:
print(f"❌ Erro detalhado no processamento: {traceback.format_exc()}")
return None, None, f"❌ Erro ao processar o documento: {e}", ""
finally:
# Limpeza do arquivo temporário do Gradio (se aplicável)
# O Gradio geralmente cuida disso, mas podemos garantir
if arquivo_pdf is not None and hasattr(arquivo_pdf, 'name') and os.path.exists(arquivo_pdf.name):
# Não deletar aqui diretamente, Gradio pode precisar dele.
# Apenas certifique-se de que não há vazamento se o Gradio falhar.
pass
def responder(pergunta, nome_aluno, state_retriever, state_doc_nome):
"""Responde a pergunta usando o RAG com o documento carregado."""
if not state_retriever:
return "⚠️ Por favor, carregue um documento (PDF ou URL) primeiro usando o botão 'Carregar Documento'."
if not pergunta or not pergunta.strip():
return "⚠️ Por favor, digite sua pergunta."
if not llm:
return "❌ Erro: LLM não está configurado corretamente."
print(f"\nRecebida pergunta sobre '{state_doc_nome}': {pergunta}")
try:
# Cria a cadeia DENTRO da função para usar o retriever do estado atual
qa_chain = ConversationalRetrievalChain.from_llm(
llm=llm,
retriever=state_retriever,
memory=memoria,
return_source_documents=True, # Pode ser útil para debug
output_key="answer" # Garante que a chave de saída seja 'answer'
)
# Invoca a cadeia
resultado = qa_chain.invoke({"question": pergunta})
resposta_bruta = resultado.get("answer", "Desculpe, não consegui gerar uma resposta.")
fontes = resultado.get("source_documents", []) # Pega as fontes se houver
# LangChain pode retornar objetos AIMessage, extrai o conteúdo se necessário
resposta = resposta_bruta.content if hasattr(resposta_bruta, "content") else str(resposta_bruta)
print(f"Resposta gerada: {resposta}")
if fontes:
print(f"Fontes encontradas: {len(fontes)} chunks.")
# print("Exemplo de fonte:", fontes[0].page_content[:200]) # Para debug
# Salva no banco de dados
salvar_conversa(nome_aluno, state_doc_nome, pergunta, resposta)
return resposta
except Exception as e:
print(f"❌ Erro detalhado ao responder: {traceback.format_exc()}")
# Retorna erro formatado para a interface
return f"❌ **Erro ao gerar resposta:**\n```\n{traceback.format_exc()}\n```"
def resetar_memoria_app():
"""Reseta a memória da conversa."""
memoria.clear()
print("Memória resetada manualmente.")
return "✅ Memória da conversa atual resetada!"
def exportar_conversas():
"""Exporta o histórico de conversas para CSV e Excel."""
try:
conn = sqlite3.connect(DB_FILE)
# Ordena pelas mais recentes primeiro e seleciona todas as colunas
df = pd.read_sql_query("SELECT id, timestamp, aluno, documento, pergunta, resposta FROM conversas ORDER BY timestamp DESC", conn)
csv_file = "conversas_exportadas.csv"
excel_file = "conversas_exportadas.xlsx"
df.to_csv(csv_file, index=False, encoding='utf-8') # Especifica encoding
df.to_excel(excel_file, index=False, engine="openpyxl")
conn.close()
print(f"Histórico exportado para '{csv_file}' e '{excel_file}'.")
return f"✅ Histórico exportado para '{csv_file}' e '{excel_file}'!"
except Exception as e:
print(f"❌ Erro ao exportar histórico: {e}")
return f"❌ Erro ao exportar histórico: {e}"
# --- Interface Gradio ---
with gr.Blocks(theme=gr.themes.Soft()) as app:
gr.Markdown("# 🧠 Tutor Multidisciplinar / Analista de Documentos Genérico 📄")
gr.Markdown("Faça upload de um PDF ou insira uma URL para começar a conversar sobre o conteúdo.")
# Estado para manter o retriever e o nome do documento atual
state_retriever = gr.State(None)
state_doc_nome = gr.State(None)
with gr.Row():
with gr.Column(scale=1):
pdf_upload = gr.File(label="Upload de PDF", file_types=[".pdf"])
url_input = gr.Textbox(label="Ou Insira a URL do Documento")
btn_carregar = gr.Button("🚀 Carregar Documento", variant="primary")
status_carregamento = gr.Markdown("") # Para mensagens de status do carregamento
with gr.Column(scale=2):
chatbot_display = gr.Textbox(label="Resposta do Assistente", lines=15, interactive=False) # Usar Textbox para formatar melhor erros
nome_aluno = gr.Textbox(label="Seu nome (opcional)", placeholder="Ex: Maria")
pergunta_input = gr.Textbox(label="Sua Pergunta sobre o Documento Carregado", placeholder="Faça sua pergunta aqui...")
with gr.Row():
btn_enviar = gr.Button("✉️ Enviar Pergunta", variant="primary")
btn_resetar = gr.Button("🔁 Resetar Memória")
btn_exportar = gr.Button("📤 Exportar Histórico")
# --- Conexões da Interface ---
# Botão Carregar Documento
btn_carregar.click(
fn=processar_documento,
inputs=[pdf_upload, url_input],
outputs=[state_retriever, state_doc_nome, status_carregamento, pergunta_input] # Limpa pergunta ao carregar
)
# Botão Enviar Pergunta
btn_enviar.click(
fn=responder,
inputs=[pergunta_input, nome_aluno, state_retriever, state_doc_nome],
outputs=chatbot_display
).then(lambda: "", outputs=pergunta_input) # Limpa o campo de pergunta após enviar
# Botão Resetar Memória
btn_resetar.click(
fn=resetar_memoria_app,
outputs=chatbot_display # Mostra mensagem de reset na caixa de resposta
)
# Botão Exportar Histórico
btn_exportar.click(
fn=exportar_conversas,
outputs=chatbot_display # Mostra mensagem de exportação na caixa de resposta
)
# Limpar campos de input ao usar o outro (PDF vs URL)
def limpar_outro_input(input_data):
# Se o input veio do upload (não é None), retorna None para o textbox da URL
if input_data is not None:
return None
return gr.update() # Não muda nada se o input veio do textbox
pdf_upload.change(fn=limpar_outro_input, inputs=pdf_upload, outputs=url_input)
url_input.change(fn=limpar_outro_input, inputs=url_input, outputs=pdf_upload)
# --- Lançar a Aplicação ---
if __name__ == "__main__":
if embeddings and llm: # Só lança se componentes essenciais carregaram
print("Iniciando interface Gradio...")
app.launch(share=True, debug=True) # Share=True para link público, Debug=True para mais logs
else:
print("❌ Aplicação não iniciada devido a falha no carregamento de Embeddings ou LLM.")