File size: 9,921 Bytes
8aff36c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
RAG Normativo - Modulo de Consulta a NT-01/2025 CBMGO
Usa sentence-transformers + FAISS para busca semantica
"""
import json
import os
import numpy as np

try:
    from sentence_transformers import SentenceTransformer
    import faiss
    HAS_FAISS = True
except ImportError:
    HAS_FAISS = False

from datasets import load_dataset


class RAGNormas:
    """
    Modulo RAG para consulta semantica a NT-01/2025 CBMGO.
    
    Uso:
        rag = RAGNormas()
        rag.build_from_jsonl("data/chunks.jsonl")
        resultado = rag.responder("Quais extintores sao obrigatorios para comercio?")
    """
    
    def __init__(self, model_name: str = "sentence-transformers/all-mpnet-base-v2"):
        self.model_name = model_name
        self.model = None
        self.index = None
        self.chunks = []
        self._initialized = False
    
    def _load_model(self):
        if not HAS_FAISS:
            raise ImportError("Instale: pip install sentence-transformers faiss-cpu")
        if self.model is None:
            print(f"Carregando modelo: {self.model_name}")
            self.model = SentenceTransformer(self.model_name)
    
    def build_from_jsonl(self, chunks_path: str):
        """Constroi indice FAISS a partir de arquivo .jsonl de chunks."""
        self._load_model()
        
        if not os.path.exists(chunks_path):
            raise FileNotFoundError(f"Arquivo nao encontrado: {chunks_path}")
        
        self.chunks = []
        with open(chunks_path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if line:
                    self.chunks.append(json.loads(line))
        
        print(f"Carregando {len(self.chunks)} chunks...")
        textos = [c["texto"] for c in self.chunks]
        embeddings = self.model.encode(textos, show_progress_bar=True,
                                        batch_size=32, normalize_embeddings=True)
        
        dim = embeddings.shape[1]
        self.index = faiss.IndexFlatIP(dim)  # Inner Product = cosine para vetores normalizados
        self.index.add(embeddings.astype(np.float32))
        self._initialized = True
        print(f"Indice FAISS construido: {self.index.ntotal} vetores, dim={dim}")
    
    def load_from_files(self, index_path: str, chunks_path: str):
        """Carrega indice FAISS pre-construido."""
        self._load_model()
        self.index = faiss.read_index(index_path)
        self.chunks = []
        with open(chunks_path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if line:
                    self.chunks.append(json.loads(line))
        self._initialized = True
        print(f"Indice carregado: {self.index.ntotal} vetores, {len(self.chunks)} chunks")
    
    def save_index(self, index_path: str):
        """Salva indice FAISS em disco."""
        if self.index is None:
            raise RuntimeError("Indice nao construido. Execute build_from_jsonl() primeiro.")
        faiss.write_index(self.index, index_path)
        print(f"Indice salvo em: {index_path}")
    
    def buscar(self, query: str, top_k: int = 5) -> list:
        """Busca os top_k chunks mais relevantes para a query."""
        if not self._initialized:
            raise RuntimeError("RAG nao inicializado. Execute build_from_jsonl() ou load_from_files().")
        
        emb = self.model.encode([query], normalize_embeddings=True)
        scores, indices = self.index.search(emb.astype(np.float32), top_k)
        
        resultados = []
        for score, idx in zip(scores[0], indices[0]):
            if idx < len(self.chunks):
                chunk = self.chunks[idx].copy()
                chunk["score"] = float(score)
                resultados.append(chunk)
        return resultados
    
    def responder(self, query: str, top_k: int = 5) -> str:
        """Retorna contexto formatado com referencias normativas."""
        resultados = self.buscar(query, top_k)
        if not resultados:
            return "Nenhum resultado encontrado para a consulta."
        
        linhas = [f"Consulta: {query}", "=" * 60]
        for i, r in enumerate(resultados, 1):
            ref = r.get("ref", "NT-01/2025")
            artigo = r.get("artigo", "")
            texto = r.get("texto", "")
            score = r.get("score", 0)
            linhas.append(f"[{i}] {ref}{' - ' + artigo if artigo else ''} (relevancia: {score:.3f})")
            linhas.append(texto[:500] + ("..." if len(texto) > 500 else ""))
            linhas.append("-" * 40)
        
        return "\n".join(linhas)
    
    def responder_fallback(self, query: str) -> str:
        """Fallback sem FAISS: busca por palavras-chave nos chunks carregados."""
        if not self.chunks:
            return self._responder_estatico(query)
        
        query_lower = query.lower()
        keywords = query_lower.split()
        
        scored = []
        for chunk in self.chunks:
            texto = chunk.get("texto", "").lower()
            score = sum(1 for kw in keywords if kw in texto)
            if score > 0:
                scored.append((score, chunk))
        
        scored.sort(key=lambda x: x[0], reverse=True)
        top = scored[:3]
        
        if not top:
            return self._responder_estatico(query)
        
        linhas = [f"Resultado para: {query}", "=" * 50]
        for score, chunk in top:
            ref = chunk.get("ref", "NT-01/2025")
            artigo = chunk.get("artigo", "")
            texto = chunk.get("texto", "")
            linhas.append(f"[{ref}{' - ' + artigo if artigo else ''}]")
            linhas.append(texto[:400])
            linhas.append("-" * 30)
        return "\n".join(linhas)
    
    def _responder_estatico(self, query: str) -> str:
        """Respostas estaticas para perguntas comuns sobre NT-01/2025."""
        q = query.lower()
        
        respostas = {
            "extintor": """NT-01/2025 CBMGO - Extintores Portateis (Anexo B)
Todas as edificacoes devem possuir extintores portateis.
- Grupo A (residencial): 1 extintor a cada 500m2, cap. 2-A:10-B:C
- Grupo D (comercial): 1 extintor a cada 400m2, cap. 2-A:20-B:C
- Grupo F (industrial): 1 extintor a cada 300m2, cap. 4-A:20-B:C
Distancia maxima de qualquer ponto ao extintor: 15 metros.
Ref: NT-01/2025 Tabela B-1 | ABNT NBR 12693""",
            
            "hidrante": """NT-01/2025 CBMGO - Sistema de Hidrantes (Art. 18-22)
Obrigatorio para edificacoes com area > 750m2 ou altura > 12m.
- Altura < 12m: Mangueira / Coluna Seca, vazao 300 L/min
- Altura 12-30m: Hidrante com Reservatorio, vazao 450 L/min
- Altura > 30m: Chuveiros Automaticos + Hidrante, vazao 600 L/min
Reserva tecnica de incendio minima: 18m3 (coluna seca) a 36m3 (alta pressao)
Ref: NT-01/2025 Art. 18 | ABNT NBR 13714""",
            
            "spda": """NT-01/2025 CBMGO - SPDA (Art. 25)
Sistema de Protecao contra Descargas Atmosfericas.
Obrigatorio para edificacoes com area total > 1000m2.
Tambem obrigatorio para: hospitais, escolas, locais de reuniao,
depositos de inflamaveis e edificacoes com altura > 20m.
Projeto conforme ABNT NBR 5419.
Ref: NT-01/2025 Art. 25""",
            
            "iluminacao": """NT-01/2025 CBMGO - Iluminacao de Emergencia (Art. 20)
Obrigatoria para edificacoes com altura > 12m.
Tambem obrigatoria em: saidas de emergencia, escadas, corredores,
locais de reuniao com lotacao > 50 pessoas.
Autonomia minima: 1 hora (ABNT NBR 10898).
Nivel minimo de iluminamento: 3 lux nas rotas de fuga.
Ref: NT-01/2025 Art. 20 | ABNT NBR 10898""",
            
            "sinalizacao": """NT-01/2025 CBMGO - Sinalizacao de Emergencia (Art. 19)
Obrigatoria em todas as edificacoes.
Itens obrigatorios:
- Placas de saida de emergencia (verde)
- Indicacao de extintores e hidrantes
- Planta de localizacao em locais de reuniao
- Instrucoes de emergencia
Ref: NT-01/2025 Art. 19 | ABNT NBR 13434""",
            
            "ocupacao": """NT-01/2025 CBMGO - Classificacao de Ocupacoes (Tabela 1)
Grupos de ocupacao:
A - Residencial (unifamiliar/multifamiliar)
B - Servicos de hospedagem (hoteis, pousadas)
C - Reuniao de publico (teatros, cinemas, igrejas)
D - Servico profissional, pessoal e tecnico (comercio, escritorios)
E - Educacional e cultura fisica
F - Locais de saude e institucional
G - Servico automotivo, transporte e pecas
H - Depositos, armazenamento geral
I - Industrial
Ref: NT-01/2025 Tabela 1""",
        }
        
        for chave, resp in respostas.items():
            if chave in q:
                return resp
        
        return (
            "Consulta: " + query + "\n"
            "\nBase de dados normativa NT-01/2025 CBMGO disponivel.\n"
            "Para consulta completa, construa o indice FAISS com build_from_jsonl().\n"
            "\nTopicos disponveis: extintores, hidrantes, SPDA, iluminacao, sinalizacao, ocupacao."
        )


# Instancia global (lazy loading)
_rag_instance = None

def get_rag() -> RAGNormas:
    global _rag_instance
    if _rag_instance is None:
        _rag_instance = RAGNormas()
        # Tentar carregar chunks se existirem
        if os.path.exists("data/chunks.jsonl"):
            try:
                if os.path.exists("data/embeds.faiss") and HAS_FAISS:
                    _rag_instance.load_from_files("data/embeds.faiss", "data/chunks.jsonl")
                else:
                    # Carregar apenas chunks para fallback por palavras-chave
                    with open("data/chunks.jsonl", "r", encoding="utf-8") as f:
                        _rag_instance.chunks = [json.loads(l) for l in f if l.strip()]
            except Exception as e:
                print(f"Aviso RAG: {e}")
    return _rag_instance


if __name__ == "__main__":
    rag = RAGNormas()
    print(rag._responder_estatico("extintores portateis"))
    print("\n---\n")
    print(rag._responder_estatico("sistema de hidrantes"))