danielspba commited on
Commit
37ecec6
·
verified ·
1 Parent(s): f989c07

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +311 -0
app.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sqlite3
3
+ import pandas as pd
4
+ from dotenv import load_dotenv
5
+ from bs4 import BeautifulSoup
6
+ import requests
7
+ import gradio as gr
8
+ import traceback # Para melhor formatação de erros
9
+ import tempfile # Para lidar com arquivos enviados
10
+
11
+ # Importações LangChain específicas
12
+ from langchain_community.document_loaders import WebBaseLoader, PyPDFLoader
13
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
14
+ from langchain_community.vectorstores import FAISS
15
+ from langchain_community.embeddings import HuggingFaceEmbeddings
16
+ from langchain_openai import ChatOpenAI
17
+ from langchain.chains import ConversationalRetrievalChain
18
+ from langchain.memory import ConversationBufferMemory
19
+
20
+ # --- Configuração Inicial ---
21
+ # Carrega chave da API (ajuste conforme sua necessidade)
22
+ load_dotenv()
23
+ # Certifique-se que as variáveis de ambiente estão corretas!
24
+ # Exemplo genérico, use as suas variáveis:
25
+ # os.environ["OPENAI_API_KEY"] = os.getenv("OPENROUTER_API_KEY") # Ou OPENAI_API_KEY
26
+ # os.environ["OPENAI_API_BASE"] = os.getenv("OPENROUTER_API_BASE") # Ou omita se usar OpenAI direto
27
+
28
+ # Verifique se a chave API está carregada (adicione um check)
29
+ api_key = os.getenv("OPENROUTER_API_KEY") or os.getenv("OPENAI_API_KEY")
30
+ if not api_key:
31
+ print("⚠️ Atenção: Nenhuma chave de API encontrada nas variáveis de ambiente (OPENROUTER_API_KEY ou OPENAI_API_KEY).")
32
+ # Você pode querer parar a execução aqui ou usar um modelo local se configurado.
33
+ # exit() # Descomente para parar se a API for essencial
34
+
35
+ # Use as variáveis corretas para seu endpoint (OpenRouter ou OpenAI)
36
+ openai_api_key = os.getenv("OPENROUTER_API_KEY") # Ou os.getenv("OPENAI_API_KEY")
37
+ openai_api_base = os.getenv("OPENROUTER_API_BASE") # Opcional, remova se usar OpenAI direto
38
+
39
+ # Embeddings (modelo local, não requer API)
40
+ print("Carregando modelo de embeddings (pode levar um tempo)...")
41
+ embeddings_model_name = "all-MiniLM-L6-v2"
42
+ try:
43
+ embeddings = HuggingFaceEmbeddings(model_name=embeddings_model_name)
44
+ print(f"Modelo de embeddings '{embeddings_model_name}' carregado.")
45
+ except Exception as e:
46
+ print(f"❌ Erro ao carregar embeddings: {e}")
47
+ print("Verifique sua conexão com a internet ou se o modelo está disponível.")
48
+ embeddings = None # Define como None para checagem posterior
49
+
50
+ # LLM (ajuste o modelo conforme disponibilidade/preferência)
51
+ # Use um modelo disponível no seu endpoint (OpenRouter ou OpenAI)
52
+ # Ex: "gpt-3.5-turbo", "deepseek/deepseek-r1:free", etc.
53
+ llm_model_name = "deepseek/deepseek-r1:free" # Exemplo OpenRouter - TROQUE SE NECESSÁRIO
54
+ try:
55
+ llm = ChatOpenAI(
56
+ model=llm_model_name,
57
+ temperature=0.5,
58
+ openai_api_key=openai_api_key,
59
+ base_url=openai_api_base # Passe None se estiver usando OpenAI diretamente
60
+ )
61
+ print(f"LLM '{llm_model_name}' configurado.")
62
+ except Exception as e:
63
+ print(f"❌ Erro ao configurar LLM: {e}")
64
+ llm = None # Define como None para checagem posterior
65
+
66
+ # Memória da conversa (pode ser global)
67
+ memoria = ConversationBufferMemory(memory_key="chat_history", return_messages=True, output_key="answer")
68
+
69
+ # --- Banco de Dados ---
70
+ DB_FILE = "historico_conversas_multidoc.db"
71
+
72
+ def inicializar_db():
73
+ conn = sqlite3.connect(DB_FILE)
74
+ cursor = conn.cursor()
75
+ cursor.execute('''
76
+ CREATE TABLE IF NOT EXISTS conversas (
77
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
78
+ aluno TEXT,
79
+ documento TEXT, -- Nova coluna
80
+ pergunta TEXT,
81
+ resposta TEXT,
82
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
83
+ )
84
+ ''')
85
+ conn.commit()
86
+ conn.close()
87
+ print(f"Banco de dados '{DB_FILE}' inicializado/verificado.")
88
+
89
+ inicializar_db() # Garante que a DB exista ao iniciar
90
+
91
+ def salvar_conversa(nome, documento, pergunta, resposta):
92
+ if not documento:
93
+ documento = "Nenhum Documento Carregado"
94
+ try:
95
+ conn = sqlite3.connect(DB_FILE)
96
+ cursor = conn.cursor()
97
+ cursor.execute("INSERT INTO conversas (aluno, documento, pergunta, resposta) VALUES (?, ?, ?, ?)",
98
+ (nome or "Anônimo", documento, pergunta, resposta))
99
+ conn.commit()
100
+ conn.close()
101
+ except Exception as e:
102
+ print(f"❌ Erro ao salvar conversa no DB: {e}")
103
+ # Não retorna o erro para a interface, apenas loga no console
104
+
105
+ # --- Funções Principais ---
106
+
107
+ def processar_documento(arquivo_pdf, url, progress=gr.Progress(track_tqdm=True)):
108
+ """Carrega, divide e cria o vector store para um PDF ou URL."""
109
+ if not embeddings or not llm:
110
+ return None, None, "❌ Erro: Embeddings ou LLM não foram carregados corretamente. Verifique o console.", ""
111
+
112
+ docs = []
113
+ documento_nome = None
114
+ temp_dir = None # Para limpar arquivos temporários
115
+
116
+ progress(0, desc="Iniciando...")
117
+ try:
118
+ if arquivo_pdf is not None:
119
+ documento_nome = os.path.basename(arquivo_pdf.name)
120
+ progress(0.1, desc=f"Carregando PDF: {documento_nome}")
121
+ # Gradio fornece um objeto de arquivo temporário.
122
+ # PyPDFLoader precisa do caminho do arquivo.
123
+ loader = PyPDFLoader(arquivo_pdf.name)
124
+ docs = loader.load()
125
+ print(f"PDF '{documento_nome}' carregado, {len(docs)} páginas.")
126
+
127
+ elif url and url.strip():
128
+ documento_nome = url.strip()
129
+ progress(0.1, desc=f"Carregando URL: {documento_nome}")
130
+ loader = WebBaseLoader(documento_nome)
131
+ docs = loader.load()
132
+ print(f"URL '{documento_nome}' carregada, {len(docs)} documentos (partes).")
133
+
134
+ else:
135
+ return None, None, "⚠️ Por favor, forneça um arquivo PDF ou uma URL.", ""
136
+
137
+ if not docs:
138
+ return None, None, f"❌ Erro: Não foi possível extrair conteúdo de '{documento_nome}'.", documento_nome
139
+
140
+ progress(0.4, desc="Dividindo documento...")
141
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
142
+ documents = text_splitter.split_documents(docs)
143
+ print(f"Documento dividido em {len(documents)} chunks.")
144
+
145
+ if not documents:
146
+ return None, None, "❌ Erro: Documento vazio após divisão.", documento_nome
147
+
148
+ progress(0.6, desc="Criando embeddings e vector store (pode levar tempo)...")
149
+ vectordb = FAISS.from_documents(documents, embeddings)
150
+ retriever = vectordb.as_retriever()
151
+ print("Vector store FAISS criado.")
152
+
153
+ progress(0.9, desc="Limpando memória da conversa anterior...")
154
+ memoria.clear() # Limpa o histórico ao carregar novo doc
155
+ print("Memória da conversa resetada.")
156
+
157
+ progress(1, desc="Documento processado!")
158
+ status = f"✅ Documento '{documento_nome}' carregado e pronto para consulta."
159
+ return retriever, documento_nome, status, "" # Limpa campo de pergunta
160
+
161
+ except Exception as e:
162
+ print(f"❌ Erro detalhado no processamento: {traceback.format_exc()}")
163
+ return None, None, f"❌ Erro ao processar o documento: {e}", ""
164
+ finally:
165
+ # Limpeza do arquivo temporário do Gradio (se aplicável)
166
+ # O Gradio geralmente cuida disso, mas podemos garantir
167
+ if arquivo_pdf is not None and hasattr(arquivo_pdf, 'name') and os.path.exists(arquivo_pdf.name):
168
+ # Não deletar aqui diretamente, Gradio pode precisar dele.
169
+ # Apenas certifique-se de que não há vazamento se o Gradio falhar.
170
+ pass
171
+
172
+
173
+ def responder(pergunta, nome_aluno, state_retriever, state_doc_nome):
174
+ """Responde a pergunta usando o RAG com o documento carregado."""
175
+ if not state_retriever:
176
+ return "⚠️ Por favor, carregue um documento (PDF ou URL) primeiro usando o botão 'Carregar Documento'."
177
+ if not pergunta or not pergunta.strip():
178
+ return "⚠️ Por favor, digite sua pergunta."
179
+ if not llm:
180
+ return "❌ Erro: LLM não está configurado corretamente."
181
+
182
+ print(f"\nRecebida pergunta sobre '{state_doc_nome}': {pergunta}")
183
+
184
+ try:
185
+ # Cria a cadeia DENTRO da função para usar o retriever do estado atual
186
+ qa_chain = ConversationalRetrievalChain.from_llm(
187
+ llm=llm,
188
+ retriever=state_retriever,
189
+ memory=memoria,
190
+ return_source_documents=True, # Pode ser útil para debug
191
+ output_key="answer" # Garante que a chave de saída seja 'answer'
192
+ )
193
+
194
+ # Invoca a cadeia
195
+ resultado = qa_chain.invoke({"question": pergunta})
196
+ resposta_bruta = resultado.get("answer", "Desculpe, não consegui gerar uma resposta.")
197
+ fontes = resultado.get("source_documents", []) # Pega as fontes se houver
198
+
199
+ # LangChain pode retornar objetos AIMessage, extrai o conteúdo se necessário
200
+ resposta = resposta_bruta.content if hasattr(resposta_bruta, "content") else str(resposta_bruta)
201
+
202
+ print(f"Resposta gerada: {resposta}")
203
+ if fontes:
204
+ print(f"Fontes encontradas: {len(fontes)} chunks.")
205
+ # print("Exemplo de fonte:", fontes[0].page_content[:200]) # Para debug
206
+
207
+ # Salva no banco de dados
208
+ salvar_conversa(nome_aluno, state_doc_nome, pergunta, resposta)
209
+
210
+ return resposta
211
+
212
+ except Exception as e:
213
+ print(f"❌ Erro detalhado ao responder: {traceback.format_exc()}")
214
+ # Retorna erro formatado para a interface
215
+ return f"❌ **Erro ao gerar resposta:**\n```\n{traceback.format_exc()}\n```"
216
+
217
+
218
+ def resetar_memoria_app():
219
+ """Reseta a memória da conversa."""
220
+ memoria.clear()
221
+ print("Memória resetada manualmente.")
222
+ return "✅ Memória da conversa atual resetada!"
223
+
224
+ def exportar_conversas():
225
+ """Exporta o histórico de conversas para CSV e Excel."""
226
+ try:
227
+ conn = sqlite3.connect(DB_FILE)
228
+ # Ordena pelas mais recentes primeiro e seleciona todas as colunas
229
+ df = pd.read_sql_query("SELECT id, timestamp, aluno, documento, pergunta, resposta FROM conversas ORDER BY timestamp DESC", conn)
230
+ csv_file = "conversas_exportadas.csv"
231
+ excel_file = "conversas_exportadas.xlsx"
232
+ df.to_csv(csv_file, index=False, encoding='utf-8') # Especifica encoding
233
+ df.to_excel(excel_file, index=False, engine="openpyxl")
234
+ conn.close()
235
+ print(f"Histórico exportado para '{csv_file}' e '{excel_file}'.")
236
+ return f"✅ Histórico exportado para '{csv_file}' e '{excel_file}'!"
237
+ except Exception as e:
238
+ print(f"❌ Erro ao exportar histórico: {e}")
239
+ return f"❌ Erro ao exportar histórico: {e}"
240
+
241
+ # --- Interface Gradio ---
242
+ with gr.Blocks(theme=gr.themes.Soft()) as app:
243
+ gr.Markdown("# 🧠 Tutor Multidisciplinar / Analista de Documentos Genérico 📄")
244
+ gr.Markdown("Faça upload de um PDF ou insira uma URL para começar a conversar sobre o conteúdo.")
245
+
246
+ # Estado para manter o retriever e o nome do documento atual
247
+ state_retriever = gr.State(None)
248
+ state_doc_nome = gr.State(None)
249
+
250
+ with gr.Row():
251
+ with gr.Column(scale=1):
252
+ pdf_upload = gr.File(label="Upload de PDF", file_types=[".pdf"])
253
+ url_input = gr.Textbox(label="Ou Insira a URL do Documento")
254
+ btn_carregar = gr.Button("🚀 Carregar Documento", variant="primary")
255
+ status_carregamento = gr.Markdown("") # Para mensagens de status do carregamento
256
+
257
+ with gr.Column(scale=2):
258
+ chatbot_display = gr.Textbox(label="Resposta do Assistente", lines=15, interactive=False) # Usar Textbox para formatar melhor erros
259
+ nome_aluno = gr.Textbox(label="Seu nome (opcional)", placeholder="Ex: Maria")
260
+ pergunta_input = gr.Textbox(label="Sua Pergunta sobre o Documento Carregado", placeholder="Faça sua pergunta aqui...")
261
+ with gr.Row():
262
+ btn_enviar = gr.Button("✉️ Enviar Pergunta", variant="primary")
263
+ btn_resetar = gr.Button("🔁 Resetar Memória")
264
+ btn_exportar = gr.Button("📤 Exportar Histórico")
265
+
266
+ # --- Conexões da Interface ---
267
+
268
+ # Botão Carregar Documento
269
+ btn_carregar.click(
270
+ fn=processar_documento,
271
+ inputs=[pdf_upload, url_input],
272
+ outputs=[state_retriever, state_doc_nome, status_carregamento, pergunta_input] # Limpa pergunta ao carregar
273
+ )
274
+
275
+ # Botão Enviar Pergunta
276
+ btn_enviar.click(
277
+ fn=responder,
278
+ inputs=[pergunta_input, nome_aluno, state_retriever, state_doc_nome],
279
+ outputs=chatbot_display
280
+ ).then(lambda: "", outputs=pergunta_input) # Limpa o campo de pergunta após enviar
281
+
282
+ # Botão Resetar Memória
283
+ btn_resetar.click(
284
+ fn=resetar_memoria_app,
285
+ outputs=chatbot_display # Mostra mensagem de reset na caixa de resposta
286
+ )
287
+
288
+ # Botão Exportar Histórico
289
+ btn_exportar.click(
290
+ fn=exportar_conversas,
291
+ outputs=chatbot_display # Mostra mensagem de exportação na caixa de resposta
292
+ )
293
+
294
+ # Limpar campos de input ao usar o outro (PDF vs URL)
295
+ def limpar_outro_input(input_data):
296
+ # Se o input veio do upload (não é None), retorna None para o textbox da URL
297
+ if input_data is not None:
298
+ return None
299
+ return gr.update() # Não muda nada se o input veio do textbox
300
+
301
+ pdf_upload.change(fn=limpar_outro_input, inputs=pdf_upload, outputs=url_input)
302
+ url_input.change(fn=limpar_outro_input, inputs=url_input, outputs=pdf_upload)
303
+
304
+
305
+ # --- Lançar a Aplicação ---
306
+ if __name__ == "__main__":
307
+ if embeddings and llm: # Só lança se componentes essenciais carregaram
308
+ print("Iniciando interface Gradio...")
309
+ app.launch(share=True, debug=True) # Share=True para link público, Debug=True para mais logs
310
+ else:
311
+ print("❌ Aplicação não iniciada devido a falha no carregamento de Embeddings ou LLM.")