carlosh10 commited on
Commit
8aff36c
·
verified ·
1 Parent(s): 03ff2c5

feat: Adiciona modulo RAG normativo NT-01/2025

Browse files
Files changed (1) hide show
  1. modules/rag_normas.py +250 -0
modules/rag_normas.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RAG Normativo - Modulo de Consulta a NT-01/2025 CBMGO
3
+ Usa sentence-transformers + FAISS para busca semantica
4
+ """
5
+ import json
6
+ import os
7
+ import numpy as np
8
+
9
+ try:
10
+ from sentence_transformers import SentenceTransformer
11
+ import faiss
12
+ HAS_FAISS = True
13
+ except ImportError:
14
+ HAS_FAISS = False
15
+
16
+ from datasets import load_dataset
17
+
18
+
19
+ class RAGNormas:
20
+ """
21
+ Modulo RAG para consulta semantica a NT-01/2025 CBMGO.
22
+
23
+ Uso:
24
+ rag = RAGNormas()
25
+ rag.build_from_jsonl("data/chunks.jsonl")
26
+ resultado = rag.responder("Quais extintores sao obrigatorios para comercio?")
27
+ """
28
+
29
+ def __init__(self, model_name: str = "sentence-transformers/all-mpnet-base-v2"):
30
+ self.model_name = model_name
31
+ self.model = None
32
+ self.index = None
33
+ self.chunks = []
34
+ self._initialized = False
35
+
36
+ def _load_model(self):
37
+ if not HAS_FAISS:
38
+ raise ImportError("Instale: pip install sentence-transformers faiss-cpu")
39
+ if self.model is None:
40
+ print(f"Carregando modelo: {self.model_name}")
41
+ self.model = SentenceTransformer(self.model_name)
42
+
43
+ def build_from_jsonl(self, chunks_path: str):
44
+ """Constroi indice FAISS a partir de arquivo .jsonl de chunks."""
45
+ self._load_model()
46
+
47
+ if not os.path.exists(chunks_path):
48
+ raise FileNotFoundError(f"Arquivo nao encontrado: {chunks_path}")
49
+
50
+ self.chunks = []
51
+ with open(chunks_path, "r", encoding="utf-8") as f:
52
+ for line in f:
53
+ line = line.strip()
54
+ if line:
55
+ self.chunks.append(json.loads(line))
56
+
57
+ print(f"Carregando {len(self.chunks)} chunks...")
58
+ textos = [c["texto"] for c in self.chunks]
59
+ embeddings = self.model.encode(textos, show_progress_bar=True,
60
+ batch_size=32, normalize_embeddings=True)
61
+
62
+ dim = embeddings.shape[1]
63
+ self.index = faiss.IndexFlatIP(dim) # Inner Product = cosine para vetores normalizados
64
+ self.index.add(embeddings.astype(np.float32))
65
+ self._initialized = True
66
+ print(f"Indice FAISS construido: {self.index.ntotal} vetores, dim={dim}")
67
+
68
+ def load_from_files(self, index_path: str, chunks_path: str):
69
+ """Carrega indice FAISS pre-construido."""
70
+ self._load_model()
71
+ self.index = faiss.read_index(index_path)
72
+ self.chunks = []
73
+ with open(chunks_path, "r", encoding="utf-8") as f:
74
+ for line in f:
75
+ line = line.strip()
76
+ if line:
77
+ self.chunks.append(json.loads(line))
78
+ self._initialized = True
79
+ print(f"Indice carregado: {self.index.ntotal} vetores, {len(self.chunks)} chunks")
80
+
81
+ def save_index(self, index_path: str):
82
+ """Salva indice FAISS em disco."""
83
+ if self.index is None:
84
+ raise RuntimeError("Indice nao construido. Execute build_from_jsonl() primeiro.")
85
+ faiss.write_index(self.index, index_path)
86
+ print(f"Indice salvo em: {index_path}")
87
+
88
+ def buscar(self, query: str, top_k: int = 5) -> list:
89
+ """Busca os top_k chunks mais relevantes para a query."""
90
+ if not self._initialized:
91
+ raise RuntimeError("RAG nao inicializado. Execute build_from_jsonl() ou load_from_files().")
92
+
93
+ emb = self.model.encode([query], normalize_embeddings=True)
94
+ scores, indices = self.index.search(emb.astype(np.float32), top_k)
95
+
96
+ resultados = []
97
+ for score, idx in zip(scores[0], indices[0]):
98
+ if idx < len(self.chunks):
99
+ chunk = self.chunks[idx].copy()
100
+ chunk["score"] = float(score)
101
+ resultados.append(chunk)
102
+ return resultados
103
+
104
+ def responder(self, query: str, top_k: int = 5) -> str:
105
+ """Retorna contexto formatado com referencias normativas."""
106
+ resultados = self.buscar(query, top_k)
107
+ if not resultados:
108
+ return "Nenhum resultado encontrado para a consulta."
109
+
110
+ linhas = [f"Consulta: {query}", "=" * 60]
111
+ for i, r in enumerate(resultados, 1):
112
+ ref = r.get("ref", "NT-01/2025")
113
+ artigo = r.get("artigo", "")
114
+ texto = r.get("texto", "")
115
+ score = r.get("score", 0)
116
+ linhas.append(f"[{i}] {ref}{' - ' + artigo if artigo else ''} (relevancia: {score:.3f})")
117
+ linhas.append(texto[:500] + ("..." if len(texto) > 500 else ""))
118
+ linhas.append("-" * 40)
119
+
120
+ return "\n".join(linhas)
121
+
122
+ def responder_fallback(self, query: str) -> str:
123
+ """Fallback sem FAISS: busca por palavras-chave nos chunks carregados."""
124
+ if not self.chunks:
125
+ return self._responder_estatico(query)
126
+
127
+ query_lower = query.lower()
128
+ keywords = query_lower.split()
129
+
130
+ scored = []
131
+ for chunk in self.chunks:
132
+ texto = chunk.get("texto", "").lower()
133
+ score = sum(1 for kw in keywords if kw in texto)
134
+ if score > 0:
135
+ scored.append((score, chunk))
136
+
137
+ scored.sort(key=lambda x: x[0], reverse=True)
138
+ top = scored[:3]
139
+
140
+ if not top:
141
+ return self._responder_estatico(query)
142
+
143
+ linhas = [f"Resultado para: {query}", "=" * 50]
144
+ for score, chunk in top:
145
+ ref = chunk.get("ref", "NT-01/2025")
146
+ artigo = chunk.get("artigo", "")
147
+ texto = chunk.get("texto", "")
148
+ linhas.append(f"[{ref}{' - ' + artigo if artigo else ''}]")
149
+ linhas.append(texto[:400])
150
+ linhas.append("-" * 30)
151
+ return "\n".join(linhas)
152
+
153
+ def _responder_estatico(self, query: str) -> str:
154
+ """Respostas estaticas para perguntas comuns sobre NT-01/2025."""
155
+ q = query.lower()
156
+
157
+ respostas = {
158
+ "extintor": """NT-01/2025 CBMGO - Extintores Portateis (Anexo B)
159
+ Todas as edificacoes devem possuir extintores portateis.
160
+ - Grupo A (residencial): 1 extintor a cada 500m2, cap. 2-A:10-B:C
161
+ - Grupo D (comercial): 1 extintor a cada 400m2, cap. 2-A:20-B:C
162
+ - Grupo F (industrial): 1 extintor a cada 300m2, cap. 4-A:20-B:C
163
+ Distancia maxima de qualquer ponto ao extintor: 15 metros.
164
+ Ref: NT-01/2025 Tabela B-1 | ABNT NBR 12693""",
165
+
166
+ "hidrante": """NT-01/2025 CBMGO - Sistema de Hidrantes (Art. 18-22)
167
+ Obrigatorio para edificacoes com area > 750m2 ou altura > 12m.
168
+ - Altura < 12m: Mangueira / Coluna Seca, vazao 300 L/min
169
+ - Altura 12-30m: Hidrante com Reservatorio, vazao 450 L/min
170
+ - Altura > 30m: Chuveiros Automaticos + Hidrante, vazao 600 L/min
171
+ Reserva tecnica de incendio minima: 18m3 (coluna seca) a 36m3 (alta pressao)
172
+ Ref: NT-01/2025 Art. 18 | ABNT NBR 13714""",
173
+
174
+ "spda": """NT-01/2025 CBMGO - SPDA (Art. 25)
175
+ Sistema de Protecao contra Descargas Atmosfericas.
176
+ Obrigatorio para edificacoes com area total > 1000m2.
177
+ Tambem obrigatorio para: hospitais, escolas, locais de reuniao,
178
+ depositos de inflamaveis e edificacoes com altura > 20m.
179
+ Projeto conforme ABNT NBR 5419.
180
+ Ref: NT-01/2025 Art. 25""",
181
+
182
+ "iluminacao": """NT-01/2025 CBMGO - Iluminacao de Emergencia (Art. 20)
183
+ Obrigatoria para edificacoes com altura > 12m.
184
+ Tambem obrigatoria em: saidas de emergencia, escadas, corredores,
185
+ locais de reuniao com lotacao > 50 pessoas.
186
+ Autonomia minima: 1 hora (ABNT NBR 10898).
187
+ Nivel minimo de iluminamento: 3 lux nas rotas de fuga.
188
+ Ref: NT-01/2025 Art. 20 | ABNT NBR 10898""",
189
+
190
+ "sinalizacao": """NT-01/2025 CBMGO - Sinalizacao de Emergencia (Art. 19)
191
+ Obrigatoria em todas as edificacoes.
192
+ Itens obrigatorios:
193
+ - Placas de saida de emergencia (verde)
194
+ - Indicacao de extintores e hidrantes
195
+ - Planta de localizacao em locais de reuniao
196
+ - Instrucoes de emergencia
197
+ Ref: NT-01/2025 Art. 19 | ABNT NBR 13434""",
198
+
199
+ "ocupacao": """NT-01/2025 CBMGO - Classificacao de Ocupacoes (Tabela 1)
200
+ Grupos de ocupacao:
201
+ A - Residencial (unifamiliar/multifamiliar)
202
+ B - Servicos de hospedagem (hoteis, pousadas)
203
+ C - Reuniao de publico (teatros, cinemas, igrejas)
204
+ D - Servico profissional, pessoal e tecnico (comercio, escritorios)
205
+ E - Educacional e cultura fisica
206
+ F - Locais de saude e institucional
207
+ G - Servico automotivo, transporte e pecas
208
+ H - Depositos, armazenamento geral
209
+ I - Industrial
210
+ Ref: NT-01/2025 Tabela 1""",
211
+ }
212
+
213
+ for chave, resp in respostas.items():
214
+ if chave in q:
215
+ return resp
216
+
217
+ return (
218
+ "Consulta: " + query + "\n"
219
+ "\nBase de dados normativa NT-01/2025 CBMGO disponivel.\n"
220
+ "Para consulta completa, construa o indice FAISS com build_from_jsonl().\n"
221
+ "\nTopicos disponveis: extintores, hidrantes, SPDA, iluminacao, sinalizacao, ocupacao."
222
+ )
223
+
224
+
225
+ # Instancia global (lazy loading)
226
+ _rag_instance = None
227
+
228
+ def get_rag() -> RAGNormas:
229
+ global _rag_instance
230
+ if _rag_instance is None:
231
+ _rag_instance = RAGNormas()
232
+ # Tentar carregar chunks se existirem
233
+ if os.path.exists("data/chunks.jsonl"):
234
+ try:
235
+ if os.path.exists("data/embeds.faiss") and HAS_FAISS:
236
+ _rag_instance.load_from_files("data/embeds.faiss", "data/chunks.jsonl")
237
+ else:
238
+ # Carregar apenas chunks para fallback por palavras-chave
239
+ with open("data/chunks.jsonl", "r", encoding="utf-8") as f:
240
+ _rag_instance.chunks = [json.loads(l) for l in f if l.strip()]
241
+ except Exception as e:
242
+ print(f"Aviso RAG: {e}")
243
+ return _rag_instance
244
+
245
+
246
+ if __name__ == "__main__":
247
+ rag = RAGNormas()
248
+ print(rag._responder_estatico("extintores portateis"))
249
+ print("\n---\n")
250
+ print(rag._responder_estatico("sistema de hidrantes"))