PedroM2626 commited on
Commit
5e680ad
·
verified ·
1 Parent(s): c528658

Upload 4 files

Browse files
Files changed (4) hide show
  1. .dockerignore +16 -0
  2. Dockerfile +32 -0
  3. app.py +345 -0
  4. requirements.txt +6 -0
.dockerignore ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .env
5
+ .venv
6
+ venv/
7
+ ENV/
8
+ .git/
9
+ .gitignore
10
+ tests/
11
+ .pytest_cache/
12
+ *.pdf
13
+ *.docx
14
+ *.txt
15
+ !requirements.txt
16
+ README.md
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Usar uma imagem base leve de Python
2
+ FROM python:3.9-slim
3
+
4
+ # Definir variáveis de ambiente para o Python não gerar arquivos .pyc e não usar buffer para logs
5
+ ENV PYTHONDONTWRITEBYTECODE=1
6
+ ENV PYTHONUNBUFFERED=1
7
+
8
+ # Definir o diretório de trabalho dentro do container
9
+ WORKDIR /app
10
+
11
+ # Instalar dependências do sistema necessárias para algumas bibliotecas Python
12
+ RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ build-essential \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Copiar o arquivo de dependências
17
+ COPY requirements.txt .
18
+
19
+ # Instalar as dependências do Python
20
+ RUN pip install --no-cache-dir -r requirements.txt
21
+
22
+ # Copiar o restante do código do projeto
23
+ COPY . .
24
+
25
+ # Expor a porta que o Gradio usa por padrão
26
+ EXPOSE 7860
27
+
28
+ # Definir variáveis de ambiente para o Gradio (necessário para Hugging Face Spaces)
29
+ ENV GRADIO_SERVER_NAME="0.0.0.0"
30
+
31
+ # Comando para rodar a aplicação
32
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from ibm_watson import NaturalLanguageUnderstandingV1
3
+ from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
4
+ from docx import Document
5
+ from PyPDF2 import PdfReader
6
+ import os
7
+ from dotenv import load_dotenv
8
+ import json
9
+ import re
10
+ import unicodedata
11
+ import requests
12
+
13
+ def normalizar_texto(texto):
14
+ """Remove acentos, caracteres especiais e converte para minúsculas."""
15
+ if not texto:
16
+ return ""
17
+ # Converte para minúsculas e remove espaços extras
18
+ texto = texto.lower().strip()
19
+ # Remove acentos
20
+ texto = "".join(c for c in unicodedata.normalize('NFD', texto) if unicodedata.category(c) != 'Mn')
21
+ # Remove pontuação básica para busca (mantém letras e números)
22
+ texto = re.sub(r'[^a-z0-9\s]', '', texto)
23
+ return texto
24
+
25
+ # Carregar variáveis de ambiente
26
+ load_dotenv()
27
+
28
+ # Inicializar o Natural Language Understanding
29
+ API_KEY = os.getenv('IBM_WATSON_API_KEY', 'SUA_CHAVE_API')
30
+ SERVICE_URL = os.getenv('IBM_WATSON_URL', 'SUA_URL_SERVICO')
31
+ PROJECT_ID = os.getenv('IBM_WATSONX_PROJECT_ID', 'SEU_PROJECT_ID')
32
+ WATSONX_API_KEY = os.getenv('IBM_WATSONX_API_KEY', API_KEY) # Usa a chave específica ou a geral como fallback
33
+
34
+ authenticator = IAMAuthenticator(API_KEY)
35
+ nlu = NaturalLanguageUnderstandingV1(
36
+ version='2024-05-10',
37
+ authenticator=authenticator
38
+ )
39
+ nlu.set_service_url(SERVICE_URL)
40
+
41
+ # Função para extrair texto de um documento
42
+ def extrair_texto(arquivo):
43
+ if not arquivo:
44
+ return "Nenhum arquivo enviado."
45
+
46
+ try:
47
+ # Se arquivo for um objeto gr.File, ele tem o atributo .name (caminho temporário)
48
+ nome_arquivo = arquivo.name if hasattr(arquivo, 'name') else arquivo
49
+
50
+ if nome_arquivo.endswith('.pdf'):
51
+ reader = PdfReader(nome_arquivo)
52
+ texto = ''
53
+ for page in reader.pages:
54
+ page_text = page.extract_text()
55
+ if page_text:
56
+ texto += page_text
57
+ return texto
58
+ elif nome_arquivo.endswith('.docx'):
59
+ doc = Document(nome_arquivo)
60
+ texto = ''
61
+ for para in doc.paragraphs:
62
+ texto += para.text + '\n'
63
+ return texto
64
+ elif nome_arquivo.endswith('.txt'):
65
+ with open(nome_arquivo, 'r', encoding='utf-8') as f:
66
+ return f.read()
67
+ else:
68
+ return "Formato de arquivo não suportado. Use PDF, DOCX ou TXT."
69
+ except Exception as e:
70
+ return f"Erro ao extrair texto: {str(e)}"
71
+
72
+ # Função para processar o texto (Resumo, Tópicos, Classificação)
73
+ def processar_texto(texto):
74
+ if not texto or len(texto.strip()) < 10:
75
+ return "Texto insuficiente para processamento.", "", ""
76
+
77
+ try:
78
+ # Tenta o resumo automático (pode não estar disponível em todos os planos/regiões)
79
+ try:
80
+ resumo_res = nlu.analyze(
81
+ text=texto,
82
+ features={'summarization': {'limit': 1}}
83
+ ).get_result()
84
+ resumo = resumo_res.get('summarization', {}).get('text', 'Resumo não disponível.')
85
+ except Exception:
86
+ resumo = "Resumo automático não disponível no seu plano Watson NLU. Exibindo principais conceitos..."
87
+
88
+ # Extração de tópicos-chave (keywords)
89
+ topicos_res = nlu.analyze(
90
+ text=texto,
91
+ features={'keywords': {'limit': 10}}
92
+ ).get_result()
93
+ topicos_lista = [k['text'] for k in topicos_res.get('keywords', [])]
94
+ topicos = ", ".join(topicos_lista[:5])
95
+
96
+ # Se o resumo falhou, tentamos usar os tópicos para criar uma descrição simples
97
+ if "não disponível" in resumo:
98
+ resumo = f"O documento aborda temas como: {', '.join(topicos_lista[:3])}."
99
+
100
+ # Classificação temática (categories)
101
+ classificacao_res = nlu.analyze(
102
+ text=texto,
103
+ features={'categories': {'limit': 5}}
104
+ ).get_result()
105
+ classificacao = ", ".join([c['label'] for c in classificacao_res.get('categories', [])])
106
+
107
+ return resumo, topicos, classificacao
108
+ except Exception as e:
109
+ return f"Erro no processamento: {str(e)}", "", ""
110
+
111
+ # Função para responder a perguntas sobre o documento (Q&A)
112
+ def responder_pergunta(pergunta, texto):
113
+ if not pergunta or not texto:
114
+ return "Por favor, forneça uma pergunta e garanta que o documento foi analisado primeiro."
115
+
116
+ try:
117
+ # 1. Extração de termos importantes da pergunta usando NLU (Keywords e Concepts)
118
+ termos_busca = []
119
+ try:
120
+ analise_pergunta = nlu.analyze(
121
+ text=pergunta,
122
+ features={'keywords': {}, 'concepts': {}}
123
+ ).get_result()
124
+
125
+ for k in analise_pergunta.get('keywords', []):
126
+ termos_busca.append(normalizar_texto(k['text']))
127
+ for c in analise_pergunta.get('concepts', []):
128
+ termos_busca.append(normalizar_texto(c['text']))
129
+ except:
130
+ pass # Fallback para extração manual se o NLU falhar na pergunta curta
131
+
132
+ # Se o Watson não retornar termos ou falhar, usamos split manual com normalização
133
+ if not termos_busca:
134
+ termos_busca = normalizar_texto(pergunta).split()
135
+
136
+ if not termos_busca:
137
+ # Última tentativa: se tudo falhar, usa a pergunta normalizada inteira
138
+ termos_busca = [normalizar_texto(pergunta)]
139
+
140
+ # 2. Processamento do texto do documento
141
+ # Normalizamos o texto completo para a busca
142
+ texto_normalizado = normalizar_texto(texto)
143
+
144
+ # Dividimos o documento em blocos menores (parágrafos)
145
+ blocos_brutos = re.split(r'\n\s*\n', texto)
146
+ if len(blocos_brutos) < 2:
147
+ blocos_brutos = texto.split('\n')
148
+
149
+ paragrafos_validos = []
150
+ for bloco in blocos_brutos:
151
+ limpo = bloco.strip()
152
+ if len(limpo) > 20: # Mantém blocos com conteúdo mínimo
153
+ paragrafos_validos.append({
154
+ 'original': limpo,
155
+ 'normalizado': normalizar_texto(limpo)
156
+ })
157
+
158
+ # Se ainda houver poucos blocos, tentamos dividir por sentenças
159
+ if len(paragrafos_validos) < 3:
160
+ sentencas = re.split(r'\.\s+', texto)
161
+ paragrafos_validos = []
162
+ for s in sentencas:
163
+ limpo = s.strip()
164
+ if len(limpo) > 20:
165
+ paragrafos_validos.append({
166
+ 'original': limpo,
167
+ 'normalizado': normalizar_texto(limpo)
168
+ })
169
+
170
+ # 3. Cálculo de relevância (Ranking)
171
+ melhor_paragrafo = ""
172
+ maior_score = 0
173
+
174
+ for item in paragrafos_validos:
175
+ p_norm = item['normalizado']
176
+ score = 0
177
+
178
+ for termo in termos_busca:
179
+ if not termo: continue
180
+ # Se o termo exato (normalizado) está no parágrafo
181
+ if termo in p_norm:
182
+ score += 1
183
+ # Bônus por palavra inteira para evitar falso-positivos em substrings
184
+ if re.search(rf'\b{re.escape(termo)}\b', p_norm):
185
+ score += 2
186
+
187
+ # Se o score for igual, preferimos o parágrafo mais curto (mais específico)
188
+ if score > maior_score:
189
+ maior_score = score
190
+ melhor_paragrafo = item['original']
191
+ elif score == maior_score and score > 0:
192
+ if len(item['original']) < len(melhor_paragrafo):
193
+ melhor_paragrafo = item['original']
194
+
195
+ # 4. Retorno do resultado
196
+ if melhor_paragrafo and maior_score > 0:
197
+ return f"Com base no documento, encontrei este trecho relevante:\n\n\"{melhor_paragrafo}\""
198
+ else:
199
+ return "Infelizmente não encontrei uma resposta direta no documento. Tente reformular sua pergunta com outros termos."
200
+
201
+ except Exception as e:
202
+ return f"Erro ao processar busca inteligente: {str(e)}"
203
+
204
+ # --- Funções de Chat Inteligente (RAG com Watsonx AI) ---
205
+
206
+ def obter_iam_token():
207
+ """Gera um token de acesso IAM usando a API Key do Watsonx."""
208
+ url = "https://iam.cloud.ibm.com/identity/token"
209
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
210
+ data = f"grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey={WATSONX_API_KEY}"
211
+
212
+ try:
213
+ response = requests.post(url, headers=headers, data=data)
214
+ if response.status_code == 200:
215
+ return response.json().get("access_token")
216
+ elif response.status_code == 400:
217
+ return f"Erro de Autenticação (400): A API Key fornecida é inválida ou não foi encontrada. Verifique seu arquivo .env."
218
+ else:
219
+ return f"Erro ao gerar token ({response.status_code}): {response.text}"
220
+ except Exception as e:
221
+ return f"Erro de conexão ao gerar token: {str(e)}"
222
+
223
+ def chat_inteligente(pergunta, texto_documento):
224
+ """Realiza um chat inteligente (RAG) usando o modelo Llama-3 no Watsonx AI."""
225
+ if not pergunta or not texto_documento:
226
+ return "Por favor, analise um documento primeiro e digite uma pergunta."
227
+
228
+ token = obter_iam_token()
229
+ if token.startswith("Erro"):
230
+ return token
231
+
232
+ url = "https://us-south.ml.cloud.ibm.com/ml/v1/text/chat?version=2023-05-29"
233
+
234
+ # Limitamos o texto do documento para não exceder o limite de tokens do modelo
235
+ contexto = texto_documento[:10000] # Aproximadamente 2500 tokens
236
+
237
+ body = {
238
+ "messages": [
239
+ {
240
+ "role": "system",
241
+ "content": (
242
+ "Você é um assistente de IA prestativo e honesto. "
243
+ "Sua tarefa é responder perguntas baseando-se EXCLUSIVAMENTE no conteúdo do documento fornecido abaixo. "
244
+ "Se a resposta não estiver no texto, diga que não encontrou a informação no documento. "
245
+ "Responda sempre em português brasileiro e use formatação Markdown.\n\n"
246
+ f"CONTEÚDO DO DOCUMENTO:\n{contexto}"
247
+ )
248
+ },
249
+ {
250
+ "role": "user",
251
+ "content": pergunta
252
+ }
253
+ ],
254
+ "project_id": PROJECT_ID,
255
+ "model_id": "meta-llama/llama-3-3-70b-instruct",
256
+ "frequency_penalty": 0,
257
+ "max_tokens": 2000,
258
+ "presence_penalty": 0,
259
+ "temperature": 0,
260
+ "top_p": 1
261
+ }
262
+
263
+ headers = {
264
+ "Accept": "application/json",
265
+ "Content-Type": "application/json",
266
+ "Authorization": f"Bearer {token}"
267
+ }
268
+
269
+ try:
270
+ response = requests.post(url, headers=headers, json=body)
271
+ if response.status_code != 200:
272
+ return f"Erro na API Watsonx: {response.text}"
273
+
274
+ data = response.json()
275
+ return data['choices'][0]['message']['content']
276
+ except Exception as e:
277
+ return f"Erro no processamento do chat: {str(e)}"
278
+
279
+ # --- Interface Gradio usando Blocks ---
280
+ def criar_interface():
281
+ with gr.Blocks(title="Análise Inteligente de Documentos") as demo:
282
+ gr.Markdown("# 📑 Watsonx AI - Análise Inteligente de Documentos")
283
+ gr.Markdown("Extraia informações, resumos e faça perguntas sobre seus documentos PDF, DOCX ou TXT.")
284
+
285
+ with gr.Tab("1. Extração e Análise"):
286
+ with gr.Row():
287
+ with gr.Column():
288
+ arquivo_input = gr.File(label="Upload de Documento")
289
+ botao_analisar = gr.Button("Analisar Documento", variant="primary")
290
+
291
+ with gr.Column():
292
+ texto_extraido = gr.Textbox(label="Texto Extraído", lines=10, interactive=False)
293
+
294
+ with gr.Row():
295
+ resumo_output = gr.Textbox(label="Resumo Automático")
296
+ topicos_output = gr.Textbox(label="Tópicos-Chave")
297
+ classificacao_output = gr.Textbox(label="Classificação Temática")
298
+
299
+ with gr.Tab("2. Localizador de Trechos (Busca Semântica)"):
300
+ gr.Markdown("### 🔍 Encontre trechos específicos no documento")
301
+ gr.Markdown("Esta ferramenta localiza os parágrafos mais relevantes que contêm os termos da sua pergunta.")
302
+ with gr.Row():
303
+ pergunta_input = gr.Textbox(label="O que você procura no texto?", placeholder="Ex: Metas de faturamento")
304
+ botao_perguntar = gr.Button("Localizar Trecho", variant="secondary")
305
+
306
+ resposta_output = gr.Textbox(label="Trecho mais relevante encontrado", lines=10)
307
+
308
+ with gr.Tab("3. Chat Inteligente (RAG)"):
309
+ gr.Markdown("### 🤖 Pergunte à Inteligência Artificial")
310
+ gr.Markdown("O modelo Llama-3 analisará todo o documento para responder suas perguntas com raciocínio e síntese.")
311
+ with gr.Row():
312
+ chat_input = gr.Textbox(label="Sua Pergunta para a IA", placeholder="Ex: Qual o tema principal do documento?")
313
+ botao_chat = gr.Button("Gerar Resposta com IA", variant="primary")
314
+
315
+ chat_output = gr.Markdown(label="Resposta da IA (Markdown)")
316
+
317
+ # Definição dos eventos
318
+ def executar_fluxo_analise(arquivo):
319
+ texto = extrair_texto(arquivo)
320
+ resumo, topicos, classificacao = processar_texto(texto)
321
+ return texto, resumo, topicos, classificacao
322
+
323
+ botao_analisar.click(
324
+ fn=executar_fluxo_analise,
325
+ inputs=[arquivo_input],
326
+ outputs=[texto_extraido, resumo_output, topicos_output, classificacao_output]
327
+ )
328
+
329
+ botao_perguntar.click(
330
+ fn=responder_pergunta,
331
+ inputs=[pergunta_input, texto_extraido],
332
+ outputs=[resposta_output]
333
+ )
334
+
335
+ botao_chat.click(
336
+ fn=chat_inteligente,
337
+ inputs=[chat_input, texto_extraido],
338
+ outputs=[chat_output]
339
+ )
340
+
341
+ return demo
342
+
343
+ if __name__ == "__main__":
344
+ app = criar_interface()
345
+ app.launch()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio
2
+ ibm-watson
3
+ python-docx
4
+ PyPDF2
5
+ python-dotenv
6
+ pytest