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.")