Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,20 +1,16 @@
|
|
1 |
import gradio as gr
|
2 |
import torch
|
3 |
from sentence_transformers import SentenceTransformer, util
|
4 |
-
# Usaremos AutoTokenizer e AutoModelForCausalLM para o novo modelo
|
5 |
from transformers import AutoTokenizer, AutoModelForCausalLM
|
6 |
from pypdf import PdfReader
|
7 |
import os
|
8 |
-
import re
|
9 |
|
10 |
# --- 1. Carregamento dos Modelos ---
|
11 |
-
|
12 |
-
print("Carregando o modelo de recuperação (Sentence Transformer)...")
|
13 |
retriever_model = SentenceTransformer('all-MiniLM-L6-v2')
|
14 |
|
15 |
-
|
16 |
-
print("Carregando o modelo de geração (DeepSeek)...")
|
17 |
-
# Nota: "trust_remote_code=True" é necessário para carregar a arquitetura do DeepSeek
|
18 |
generator_tokenizer = AutoTokenizer.from_pretrained(
|
19 |
'deepseek-ai/deepseek-coder-1.3b-instruct',
|
20 |
trust_remote_code=True
|
@@ -25,12 +21,14 @@ generator_model = AutoModelForCausalLM.from_pretrained(
|
|
25 |
)
|
26 |
print("Modelos carregados com sucesso!")
|
27 |
|
28 |
-
# --- 2. Função
|
29 |
def process_files(files):
|
30 |
if not files:
|
31 |
-
return None, "Por favor, envie um ou mais
|
|
|
32 |
knowledge_text = ""
|
33 |
for file in files:
|
|
|
34 |
file_path = file.name
|
35 |
if file_path.endswith(".pdf"):
|
36 |
try:
|
@@ -38,138 +36,165 @@ def process_files(files):
|
|
38 |
for page in reader.pages:
|
39 |
page_text = page.extract_text()
|
40 |
if page_text:
|
41 |
-
# Adiciona um espaço extra entre as páginas para garantir a separação
|
42 |
knowledge_text += page_text + "\n\n"
|
43 |
except Exception as e:
|
44 |
-
return None, f"Erro ao ler o
|
45 |
elif file_path.endswith(".txt"):
|
46 |
try:
|
47 |
with open(file_path, 'r', encoding='utf-8') as f:
|
48 |
knowledge_text += f.read() + "\n\n"
|
49 |
except Exception as e:
|
50 |
-
return None, f"Erro ao ler o
|
51 |
|
52 |
if not knowledge_text.strip():
|
53 |
-
return None, "Não foi possível extrair texto dos
|
54 |
|
55 |
-
# MUDANÇA PRINCIPAL: Extrator Estruturado usando Regex
|
56 |
-
# Este padrão de regex divide o texto ANTES de uma linha que parece um cabeçalho de seção
|
57 |
-
# (Ex: "1. CLIENTE", "8. PADRÕES"). Isso mantém a seção inteira em um único chunk.
|
58 |
-
# O `(?m)` ativa o modo multiline, fazendo `^` corresponder ao início de cada linha.
|
59 |
chunk_pattern = r"(?m)(^\d+\..*)"
|
60 |
-
|
61 |
-
# Divide o texto em chunks usando o padrão e remove os vazios
|
62 |
text_chunks = [chunk.strip() for chunk in re.split(chunk_pattern, knowledge_text) if chunk.strip()]
|
63 |
|
64 |
-
# Reagrupa o cabeçalho com seu conteúdo
|
65 |
structured_chunks = []
|
66 |
i = 0
|
67 |
while i < len(text_chunks):
|
68 |
if re.match(chunk_pattern, text_chunks[i]) and i + 1 < len(text_chunks):
|
69 |
-
# Junta o cabeçalho (ex: "1. CLIENTE") com o conteúdo seguinte
|
70 |
structured_chunks.append(text_chunks[i] + "\n" + text_chunks[i+1])
|
71 |
i += 2
|
72 |
else:
|
73 |
-
# Adiciona conteúdo que não corresponde a um cabeçalho
|
74 |
structured_chunks.append(text_chunks[i])
|
75 |
i += 1
|
76 |
|
77 |
if not structured_chunks:
|
78 |
return None, "O texto extraído não continha blocos de texto válidos para processamento."
|
79 |
|
80 |
-
print(f"
|
81 |
knowledge_base_embeddings = retriever_model.encode(structured_chunks, convert_to_tensor=True, show_progress_bar=True)
|
82 |
-
print("Base de conhecimento criada a partir dos
|
83 |
|
84 |
-
return (structured_chunks, knowledge_base_embeddings), f"✅ Sucesso! {len(files)}
|
85 |
|
86 |
|
87 |
-
# --- 3. A
|
88 |
-
def
|
89 |
-
|
90 |
-
|
91 |
-
if not knowledge_state or not knowledge_state[0] or knowledge_state[1] is None:
|
92 |
-
return "⚠️ A base de conhecimento está vazia. Por favor, processe alguns arquivos primeiro."
|
93 |
-
|
94 |
knowledge_base, knowledge_base_embeddings = knowledge_state
|
95 |
-
|
96 |
-
# Etapa de Recuperação
|
97 |
question_embedding = retriever_model.encode(question, convert_to_tensor=True)
|
98 |
cosine_scores = util.cos_sim(question_embedding, knowledge_base_embeddings)
|
99 |
-
#
|
100 |
-
top_k = min(7, len(knowledge_base))
|
101 |
top_results = torch.topk(cosine_scores, k=top_k, dim=-1)
|
102 |
retrieved_context = "\n---\n".join([knowledge_base[i] for i in top_results.indices[0]])
|
103 |
|
104 |
-
|
105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
106 |
|
107 |
-
|
108 |
-
|
109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
|
111 |
-
|
112 |
-
|
113 |
-
Você é um assistente de IA especialista em extrair informações de documentos técnicos. Analise o 'Contexto' para responder à 'Pergunta' seguindo estas regras rigorosamente:
|
114 |
|
115 |
-
**
|
116 |
-
|
117 |
-
|
118 |
-
3. **Para perguntas sobre 'instrumento':** Procure por linhas que comecem com "Instrumento:", "Descrição:", "Objeto:", "TAG:", ou um modelo específico. Ignore descrições de métodos de calibração.
|
119 |
-
4. **Para perguntas sobre 'título' ou 'resumo':** Resuma os dados principais, como o tipo de documento (ex: Certificado de Calibração), o nome do cliente e o instrumento calibrado.
|
120 |
|
121 |
-
**
|
122 |
-
|
|
|
|
|
123 |
|
124 |
-
**
|
125 |
-
|
|
|
126 |
|
127 |
-
|
|
|
128 |
"""
|
129 |
|
130 |
-
input_ids = generator_tokenizer(
|
131 |
-
|
132 |
-
|
133 |
-
# Ajuste nos parâmetros de geração
|
134 |
-
outputs = generator_model.generate(
|
135 |
-
input_ids,
|
136 |
-
# Aumentado para permitir respostas mais detalhadas
|
137 |
-
max_new_tokens=350,
|
138 |
-
do_sample=False,
|
139 |
-
eos_token_id=generator_tokenizer.eos_token_id,
|
140 |
-
pad_token_id=generator_tokenizer.eos_token_id
|
141 |
-
)
|
142 |
|
143 |
-
|
144 |
-
|
145 |
-
answer = generator_tokenizer.decode(generated_tokens, skip_special_tokens=True)
|
146 |
-
|
147 |
-
return answer
|
148 |
|
149 |
-
# ---
|
150 |
with gr.Blocks(theme=gr.themes.Soft()) as interface:
|
151 |
knowledge_state = gr.State()
|
152 |
gr.Markdown(
|
153 |
"""
|
154 |
-
# 🤖
|
155 |
-
**1. Carregue
|
156 |
-
**2. Processe
|
157 |
-
**3.
|
158 |
"""
|
159 |
)
|
|
|
160 |
with gr.Row():
|
161 |
with gr.Column(scale=1):
|
162 |
-
file_uploader = gr.File(label="Carregar
|
163 |
-
process_button = gr.Button("Processar
|
164 |
status_box = gr.Textbox(label="Status do Processamento", interactive=False)
|
|
|
165 |
with gr.Column(scale=2):
|
166 |
-
|
167 |
-
submit_button = gr.Button("
|
168 |
-
|
|
|
|
|
|
|
|
|
|
|
169 |
|
170 |
process_button.click(fn=process_files, inputs=[file_uploader], outputs=[knowledge_state, status_box])
|
171 |
-
submit_button.click(fn=
|
172 |
|
173 |
-
# ---
|
174 |
if __name__ == "__main__":
|
175 |
interface.launch()
|
|
|
1 |
import gradio as gr
|
2 |
import torch
|
3 |
from sentence_transformers import SentenceTransformer, util
|
|
|
4 |
from transformers import AutoTokenizer, AutoModelForCausalLM
|
5 |
from pypdf import PdfReader
|
6 |
import os
|
7 |
+
import re
|
8 |
|
9 |
# --- 1. Carregamento dos Modelos ---
|
10 |
+
print("A carregar o modelo de recuperação (Sentence Transformer)...")
|
|
|
11 |
retriever_model = SentenceTransformer('all-MiniLM-L6-v2')
|
12 |
|
13 |
+
print("A carregar o modelo de geração (DeepSeek)...")
|
|
|
|
|
14 |
generator_tokenizer = AutoTokenizer.from_pretrained(
|
15 |
'deepseek-ai/deepseek-coder-1.3b-instruct',
|
16 |
trust_remote_code=True
|
|
|
21 |
)
|
22 |
print("Modelos carregados com sucesso!")
|
23 |
|
24 |
+
# --- 2. Função de Processamento de Ficheiros (Chunking Estruturado) ---
|
25 |
def process_files(files):
|
26 |
if not files:
|
27 |
+
return None, "Por favor, envie um ou mais ficheiros."
|
28 |
+
|
29 |
knowledge_text = ""
|
30 |
for file in files:
|
31 |
+
# ... (código de extração de texto de PDF/TXT permanece o mesmo) ...
|
32 |
file_path = file.name
|
33 |
if file_path.endswith(".pdf"):
|
34 |
try:
|
|
|
36 |
for page in reader.pages:
|
37 |
page_text = page.extract_text()
|
38 |
if page_text:
|
|
|
39 |
knowledge_text += page_text + "\n\n"
|
40 |
except Exception as e:
|
41 |
+
return None, f"Erro ao ler o ficheiro PDF {os.path.basename(file_path)}: {e}"
|
42 |
elif file_path.endswith(".txt"):
|
43 |
try:
|
44 |
with open(file_path, 'r', encoding='utf-8') as f:
|
45 |
knowledge_text += f.read() + "\n\n"
|
46 |
except Exception as e:
|
47 |
+
return None, f"Erro ao ler o ficheiro TXT {os.path.basename(file_path)}: {e}"
|
48 |
|
49 |
if not knowledge_text.strip():
|
50 |
+
return None, "Não foi possível extrair texto dos ficheiros fornecidos."
|
51 |
|
|
|
|
|
|
|
|
|
52 |
chunk_pattern = r"(?m)(^\d+\..*)"
|
|
|
|
|
53 |
text_chunks = [chunk.strip() for chunk in re.split(chunk_pattern, knowledge_text) if chunk.strip()]
|
54 |
|
|
|
55 |
structured_chunks = []
|
56 |
i = 0
|
57 |
while i < len(text_chunks):
|
58 |
if re.match(chunk_pattern, text_chunks[i]) and i + 1 < len(text_chunks):
|
|
|
59 |
structured_chunks.append(text_chunks[i] + "\n" + text_chunks[i+1])
|
60 |
i += 2
|
61 |
else:
|
|
|
62 |
structured_chunks.append(text_chunks[i])
|
63 |
i += 1
|
64 |
|
65 |
if not structured_chunks:
|
66 |
return None, "O texto extraído não continha blocos de texto válidos para processamento."
|
67 |
|
68 |
+
print(f"A processar {len(structured_chunks)} chunks estruturados dos ficheiros...")
|
69 |
knowledge_base_embeddings = retriever_model.encode(structured_chunks, convert_to_tensor=True, show_progress_bar=True)
|
70 |
+
print("Base de conhecimento criada a partir dos ficheiros.")
|
71 |
|
72 |
+
return (structured_chunks, knowledge_base_embeddings), f"✅ Sucesso! {len(files)} ficheiro(s) processado(s), gerando {len(structured_chunks)} chunks estruturados."
|
73 |
|
74 |
|
75 |
+
# --- 3. A FERRAMENTA PRINCIPAL DO AGENTE: find_info ---
|
76 |
+
def find_info(question, knowledge_state):
|
77 |
+
"""Esta função atua como a 'ferramenta de busca' do agente. Ela não dá a resposta final,
|
78 |
+
apenas extrai a informação bruta pedida."""
|
|
|
|
|
|
|
79 |
knowledge_base, knowledge_base_embeddings = knowledge_state
|
80 |
+
|
|
|
81 |
question_embedding = retriever_model.encode(question, convert_to_tensor=True)
|
82 |
cosine_scores = util.cos_sim(question_embedding, knowledge_base_embeddings)
|
83 |
+
top_k = min(5, len(knowledge_base)) # Reduzido para ser mais focado
|
|
|
84 |
top_results = torch.topk(cosine_scores, k=top_k, dim=-1)
|
85 |
retrieved_context = "\n---\n".join([knowledge_base[i] for i in top_results.indices[0]])
|
86 |
|
87 |
+
prompt = f"### Instruction:\nExtraia a informação exata para responder à pergunta com base no contexto. Seja direto.\n\nContexto:\n{retrieved_context}\n\nPergunta:\n{question}\n\n### Response:"
|
88 |
+
|
89 |
+
input_ids = generator_tokenizer(prompt, return_tensors="pt").input_ids
|
90 |
+
outputs = generator_model.generate(input_ids, max_new_tokens=100, do_sample=False, pad_token_id=generator_tokenizer.eos_token_id)
|
91 |
+
answer = generator_tokenizer.decode(outputs[0, input_ids.shape[1]:], skip_special_tokens=True)
|
92 |
+
return answer.strip()
|
93 |
+
|
94 |
+
|
95 |
+
# --- 4. O CÉREBRO DO AGENTE: evaluate_document ---
|
96 |
+
def evaluate_document(task, knowledge_state):
|
97 |
+
"""Esta é a função principal do agente. Ela orquestra as chamadas à ferramenta 'find_info'
|
98 |
+
para construir um relatório de avaliação completo."""
|
99 |
+
if not task:
|
100 |
+
return "Por favor, forneça uma tarefa de avaliação.", ""
|
101 |
+
if not knowledge_state or not knowledge_state[0] or knowledge_state[1] is None:
|
102 |
+
return "⚠️ A base de conhecimento está vazia. Por favor, processe alguns ficheiros primeiro.", ""
|
103 |
|
104 |
+
thought_process = "Iniciando a avaliação do documento...\n"
|
105 |
+
|
106 |
+
# Passo 1: Identificação
|
107 |
+
thought_process += "Passo 1: A identificar o cliente e o instrumento...\n"
|
108 |
+
client_info = find_info("Qual o nome do cliente ou contratante?", knowledge_state)
|
109 |
+
instrument_info = find_info("Qual é a descrição, TAG ou modelo do instrumento calibrado?", knowledge_state)
|
110 |
+
thought_process += f" - Cliente Encontrado: {client_info}\n"
|
111 |
+
thought_process += f" - Instrumento Encontrado: {instrument_info}\n\n"
|
112 |
+
|
113 |
+
# Passo 2: Resultado e Incerteza
|
114 |
+
thought_process += "Passo 2: A verificar o resultado da calibração e a incerteza...\n"
|
115 |
+
result_info = find_info("Qual foi o resultado final da calibração (ex: Aprovado, Reprovado)?", knowledge_state)
|
116 |
+
uncertainty_info = find_info("Qual é a incerteza de medição reportada?", knowledge_state)
|
117 |
+
thought_process += f" - Resultado: {result_info}\n"
|
118 |
+
thought_process += f" - Incerteza: {uncertainty_info}\n\n"
|
119 |
+
|
120 |
+
# Passo 3: Validade
|
121 |
+
thought_process += "Passo 3: A verificar as datas...\n"
|
122 |
+
calibration_date = find_info("Qual foi a data em que a calibração foi realizada?", knowledge_state)
|
123 |
+
due_date = find_info("Qual a data de vencimento ou próxima calibração?", knowledge_state)
|
124 |
+
thought_process += f" - Data da Calibração: {calibration_date}\n"
|
125 |
+
thought_process += f" - Data de Vencimento: {due_date}\n\n"
|
126 |
+
|
127 |
+
# Passo 4: Geração do Relatório Final
|
128 |
+
thought_process += "Passo 4: A compilar o relatório final...\n"
|
129 |
+
final_prompt = f"""### Instruction:
|
130 |
+
Você é um auditor de metrologia a escrever um relatório de avaliação. Com base nos 'Dados Recolhidos' abaixo, escreva um parecer técnico conciso e estruturado.
|
131 |
+
|
132 |
+
**Dados Recolhidos:**
|
133 |
+
- Cliente: {client_info}
|
134 |
+
- Instrumento: {instrument_info}
|
135 |
+
- Resultado da Calibração: {result_info}
|
136 |
+
- Incerteza de Medição: {uncertainty_info}
|
137 |
+
- Data da Execução: {calibration_date}
|
138 |
+
- Próxima Calibração: {due_date}
|
139 |
|
140 |
+
### Response:
|
141 |
+
**Relatório de Avaliação do Certificado**
|
|
|
142 |
|
143 |
+
**1. Identificação:**
|
144 |
+
- **Cliente:** [Preencha com o cliente]
|
145 |
+
- **Instrumento:** [Preencha com o instrumento]
|
|
|
|
|
146 |
|
147 |
+
**2. Análise Técnica:**
|
148 |
+
- **Resultado:** [Preencha com o resultado]
|
149 |
+
- **Incerteza:** [Preencha com a incerteza]
|
150 |
+
- **Conformidade:** [Comente brevemente se o resultado 'Aprovado' é consistente com os dados]
|
151 |
|
152 |
+
**3. Validade:**
|
153 |
+
- **Data da Calibração:** [Preencha com a data]
|
154 |
+
- **Validade:** [Preencha com a data de vencimento]
|
155 |
|
156 |
+
**4. Parecer Final:**
|
157 |
+
- [Escreva uma frase de conclusão sobre a validade e aceitabilidade do certificado com base nos dados.]
|
158 |
"""
|
159 |
|
160 |
+
input_ids = generator_tokenizer(final_prompt, return_tensors="pt").input_ids
|
161 |
+
outputs = generator_model.generate(input_ids, max_new_tokens=400, do_sample=False, pad_token_id=generator_tokenizer.eos_token_id)
|
162 |
+
final_report = generator_tokenizer.decode(outputs[0, input_ids.shape[1]:], skip_special_tokens=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
|
164 |
+
thought_process += "Avaliação concluída."
|
165 |
+
return thought_process, final_report
|
|
|
|
|
|
|
166 |
|
167 |
+
# --- 5. Interface Gráfica (Atualizada para o Agente) ---
|
168 |
with gr.Blocks(theme=gr.themes.Soft()) as interface:
|
169 |
knowledge_state = gr.State()
|
170 |
gr.Markdown(
|
171 |
"""
|
172 |
+
# 🤖 Agente de Avaliação de Documentos de Metrologia
|
173 |
+
**1. Carregue um documento**: Envie um certificado de calibração em `.pdf` ou `.txt`.
|
174 |
+
**2. Processe o documento**: Clique no botão para criar a base de conhecimento.
|
175 |
+
**3. Inicie a Avaliação**: Dê uma tarefa ao agente (ex: "Avalie este certificado") e clique em "Iniciar Avaliação".
|
176 |
"""
|
177 |
)
|
178 |
+
|
179 |
with gr.Row():
|
180 |
with gr.Column(scale=1):
|
181 |
+
file_uploader = gr.File(label="Carregar Documento (.pdf, .txt)", file_count="multiple", file_types=[".pdf", ".txt"])
|
182 |
+
process_button = gr.Button("Processar Documento", variant="primary")
|
183 |
status_box = gr.Textbox(label="Status do Processamento", interactive=False)
|
184 |
+
|
185 |
with gr.Column(scale=2):
|
186 |
+
task_box = gr.Textbox(label="Tarefa de Avaliação", placeholder='Ex: Avalie este certificado de calibração.')
|
187 |
+
submit_button = gr.Button("Iniciar Avaliação", variant="primary")
|
188 |
+
|
189 |
+
with gr.Row():
|
190 |
+
with gr.Column():
|
191 |
+
thought_box = gr.Textbox(label="Passos do Agente", interactive=False, lines=15)
|
192 |
+
with gr.Column():
|
193 |
+
report_box = gr.Textbox(label="Relatório Final de Avaliação", interactive=False, lines=15)
|
194 |
|
195 |
process_button.click(fn=process_files, inputs=[file_uploader], outputs=[knowledge_state, status_box])
|
196 |
+
submit_button.click(fn=evaluate_document, inputs=[task_box, knowledge_state], outputs=[thought_box, report_box])
|
197 |
|
198 |
+
# --- 6. Lançamento do App ---
|
199 |
if __name__ == "__main__":
|
200 |
interface.launch()
|