File size: 13,967 Bytes
37ecec6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e00b0c8
 
 
 
 
37ecec6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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.")