File size: 8,380 Bytes
121fc9e
 
 
5558e3e
6050f7a
1e45ac8
 
37f3bb4
121fc9e
6050f7a
 
121fc9e
 
6050f7a
5558e3e
6050f7a
 
 
 
 
 
 
 
 
 
121fc9e
 
37f3bb4
1e45ac8
 
 
 
 
 
 
 
 
 
 
 
37f3bb4
 
1e45ac8
 
 
 
 
37f3bb4
1e45ac8
 
 
 
 
 
37f3bb4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1e45ac8
 
37f3bb4
 
1e45ac8
 
37f3bb4
 
1e45ac8
37f3bb4
1e45ac8
 
 
 
 
 
 
 
5558e3e
121fc9e
957e316
5558e3e
9962ed4
957e316
 
121fc9e
e0164c8
 
 
bf0034d
121fc9e
957e316
121fc9e
37f3bb4
6050f7a
5558e3e
 
 
 
 
 
 
121fc9e
e0164c8
6050f7a
121fc9e
6050f7a
 
957e316
6050f7a
 
121fc9e
 
6050f7a
 
e0164c8
121fc9e
 
5558e3e
9962ed4
e0164c8
6050f7a
e0164c8
121fc9e
 
e0164c8
6050f7a
 
 
121fc9e
 
957e316
1e45ac8
 
 
 
37f3bb4
1e45ac8
 
 
 
 
 
 
957e316
1e45ac8
 
 
 
 
 
 
957e316
 
1e45ac8
 
121fc9e
bf0034d
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
import gradio as gr
import torch
from sentence_transformers import SentenceTransformer, util
# Usaremos AutoTokenizer e AutoModelForCausalLM para o novo modelo
from transformers import AutoTokenizer, AutoModelForCausalLM
from pypdf import PdfReader
import os
import re # MÓDULO ADICIONADO para Expressões Regulares

# --- 1. Carregamento dos Modelos ---
# Modelo de recuperação não muda, ele é excelente para essa tarefa.
print("Carregando o modelo de recuperação (Sentence Transformer)...")
retriever_model = SentenceTransformer('all-MiniLM-L6-v2')

# Carregando o modelo de geração DeepSeek
print("Carregando o modelo de geração (DeepSeek)...")
# Nota: "trust_remote_code=True" é necessário para carregar a arquitetura do DeepSeek
generator_tokenizer = AutoTokenizer.from_pretrained(
    'deepseek-ai/deepseek-coder-1.3b-instruct', 
    trust_remote_code=True
)
generator_model = AutoModelForCausalLM.from_pretrained(
    'deepseek-ai/deepseek-coder-1.3b-instruct', 
    trust_remote_code=True
)
print("Modelos carregados com sucesso!")

# --- 2. Função para Processar Arquivos Enviados (COM CHUNKING ESTRUTURADO) ---
def process_files(files):
    if not files:
        return None, "Por favor, envie um ou mais arquivos."
    knowledge_text = ""
    for file in files:
        file_path = file.name
        if file_path.endswith(".pdf"):
            try:
                reader = PdfReader(file_path)
                for page in reader.pages:
                    page_text = page.extract_text()
                    if page_text:
                        # Adiciona um espaço extra entre as páginas para garantir a separação
                        knowledge_text += page_text + "\n\n"
            except Exception as e:
                return None, f"Erro ao ler o arquivo PDF {os.path.basename(file_path)}: {e}"
        elif file_path.endswith(".txt"):
            try:
                with open(file_path, 'r', encoding='utf-8') as f:
                    knowledge_text += f.read() + "\n\n"
            except Exception as e:
                return None, f"Erro ao ler o arquivo TXT {os.path.basename(file_path)}: {e}"

    if not knowledge_text.strip():
        return None, "Não foi possível extrair texto dos arquivos fornecidos."

    # MUDANÇA PRINCIPAL: Extrator Estruturado usando Regex
    # Este padrão de regex divide o texto ANTES de uma linha que parece um cabeçalho de seção
    # (Ex: "1. CLIENTE", "8. PADRÕES"). Isso mantém a seção inteira em um único chunk.
    # O `(?m)` ativa o modo multiline, fazendo `^` corresponder ao início de cada linha.
    chunk_pattern = r"(?m)(^\d+\..*)"
    
    # Divide o texto em chunks usando o padrão e remove os vazios
    text_chunks = [chunk.strip() for chunk in re.split(chunk_pattern, knowledge_text) if chunk.strip()]
    
    # Reagrupa o cabeçalho com seu conteúdo
    structured_chunks = []
    i = 0
    while i < len(text_chunks):
        if re.match(chunk_pattern, text_chunks[i]) and i + 1 < len(text_chunks):
            # Junta o cabeçalho (ex: "1. CLIENTE") com o conteúdo seguinte
            structured_chunks.append(text_chunks[i] + "\n" + text_chunks[i+1])
            i += 2
        else:
            # Adiciona conteúdo que não corresponde a um cabeçalho
            structured_chunks.append(text_chunks[i])
            i += 1

    if not structured_chunks:
        return None, "O texto extraído não continha blocos de texto válidos para processamento."

    print(f"Processando {len(structured_chunks)} chunks estruturados dos arquivos...")
    knowledge_base_embeddings = retriever_model.encode(structured_chunks, convert_to_tensor=True, show_progress_bar=True)
    print("Base de conhecimento criada a partir dos arquivos.")
    
    return (structured_chunks, knowledge_base_embeddings), f"✅ Sucesso! {len(files)} arquivo(s) processado(s), gerando {len(structured_chunks)} chunks estruturados."


# --- 3. A Função Principal do RAG (sem alterações) ---
def answer_question(question, knowledge_state):
    if not question:
        return "Por favor, insira uma pergunta."
    if not knowledge_state or not knowledge_state[0] or knowledge_state[1] is None:
        return "⚠️ A base de conhecimento está vazia. Por favor, processe alguns arquivos primeiro."

    knowledge_base, knowledge_base_embeddings = knowledge_state

    # Etapa de Recuperação
    question_embedding = retriever_model.encode(question, convert_to_tensor=True)
    cosine_scores = util.cos_sim(question_embedding, knowledge_base_embeddings)
    # Aumentado para 7 para mais contexto
    top_k = min(7, len(knowledge_base)) 
    top_results = torch.topk(cosine_scores, k=top_k, dim=-1)
    retrieved_context = "\n---\n".join([knowledge_base[i] for i in top_results.indices[0]])

    if not retrieved_context.strip():
        return "Não foi possível encontrar um contexto relevante nos documentos para responder a esta pergunta."

    print(f"\n--- Nova Pergunta de Auditoria ---")
    print(f"Pergunta: {question}")
    print(f"Contexto Recuperado (Top {top_k}):\n{retrieved_context}")

    # Prompt com regras explícitas de extração de entidades
    prompt = f"""### Instruction:
Você é um assistente de IA especialista em extrair informações de documentos técnicos. Analise o 'Contexto' para responder à 'Pergunta' seguindo estas regras rigorosamente:

**Regras de Extração:**
1.  **Use APENAS a informação do 'Contexto'.** Não adicione informações externas.
2.  **Para perguntas sobre 'cliente':** Procure por linhas que comecem com "Cliente:", "Contratante:", ou um nome de empresa claro. Ignore frases genéricas sobre escopo ou lotes.
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.
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.

**Contexto:**
{retrieved_context}

**Pergunta:**
{question}

### Response:
"""

    input_ids = generator_tokenizer(prompt, return_tensors="pt").input_ids
    input_length = input_ids.shape[1]

    # Ajuste nos parâmetros de geração
    outputs = generator_model.generate(
        input_ids,
        # Aumentado para permitir respostas mais detalhadas
        max_new_tokens=350,  
        do_sample=False,
        eos_token_id=generator_tokenizer.eos_token_id,
        pad_token_id=generator_tokenizer.eos_token_id
    )

    # Decodificação correta para modelos Causal LM
    generated_tokens = outputs[0, input_length:]
    answer = generator_tokenizer.decode(generated_tokens, skip_special_tokens=True)
    
    return answer

# --- 4. Interface Gráfica (sem alterações na estrutura) ---
with gr.Blocks(theme=gr.themes.Soft()) as interface:
    knowledge_state = gr.State()
    gr.Markdown(
        """
        # 🤖 RAG - Auditor de Documentos (v9 - Chunking Estruturado)
        **1. Carregue seus arquivos**: Envie um ou mais certificados ou documentos nos formatos `.pdf` ou `.txt`.
        **2. Processe os arquivos**: Clique no botão para criar a base de conhecimento.
        **3. Faça perguntas**: Após o processamento, faça perguntas sobre o conteúdo dos documentos.
        """
    )
    with gr.Row():
        with gr.Column(scale=1):
            file_uploader = gr.File(label="Carregar Certificados (.pdf, .txt)", file_count="multiple", file_types=[".pdf", ".txt"])
            process_button = gr.Button("Processar Arquivos", variant="primary")
            status_box = gr.Textbox(label="Status do Processamento", interactive=False)
        with gr.Column(scale=2):
            question_box = gr.Textbox(label="Faça sua pergunta aqui", placeholder="Ex: Qual o resultado da calibração do instrumento PI-101?")
            submit_button = gr.Button("Obter Resposta", variant="primary")
            answer_box = gr.Textbox(label="Resposta Baseada nos Documentos", interactive=False, lines=5)

    process_button.click(fn=process_files, inputs=[file_uploader], outputs=[knowledge_state, status_box])
    submit_button.click(fn=answer_question, inputs=[question_box, knowledge_state], outputs=[answer_box])

# --- 5. Lançamento do App ---
if __name__ == "__main__":
    interface.launch()