Spaces:
Sleeping
Sleeping
Create metrology_rag.py
Browse files- metrology_rag.py +254 -0
metrology_rag.py
ADDED
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import google.generativeai as genai
|
2 |
+
from sentence_transformers import SentenceTransformer
|
3 |
+
import numpy as np
|
4 |
+
from typing import List, Dict, Optional
|
5 |
+
import faiss
|
6 |
+
import pickle
|
7 |
+
import os
|
8 |
+
from datetime import datetime
|
9 |
+
import pdfplumber
|
10 |
+
from pathlib import Path
|
11 |
+
|
12 |
+
# Configuração inicial do Gemini
|
13 |
+
genai.configure(api_key="AIzaSyClWplmEF8_sDgmSbhg0h6xkAoFQcLU4p4")
|
14 |
+
|
15 |
+
class MetrologyGlossary:
|
16 |
+
"""Glossário interno de termos metrológicos para melhorar recuperação e respostas."""
|
17 |
+
def __init__(self):
|
18 |
+
self.terms = {
|
19 |
+
"incerteza": "Medida da dispersão associada ao resultado de uma medição.",
|
20 |
+
"calibração": "Comparação de um instrumento com um padrão de referência.",
|
21 |
+
"traceability": "Propriedade de um resultado de medição que pode ser relacionado a um padrão nacional ou internacional.",
|
22 |
+
"iso/iec 17025": "Norma internacional para laboratórios de ensaio e calibração.",
|
23 |
+
# Adicione mais termos conforme necessário
|
24 |
+
}
|
25 |
+
|
26 |
+
def enhance_query(self, query: str) -> str:
|
27 |
+
"""Adiciona definições ou sinônimos à consulta para maior precisão."""
|
28 |
+
for term, definition in self.terms.items():
|
29 |
+
if term.lower() in query.lower():
|
30 |
+
query += f" ({definition})"
|
31 |
+
return query
|
32 |
+
|
33 |
+
class DocumentParser:
|
34 |
+
"""Extrai texto e tabelas de arquivos PDF, com foco em metrologia."""
|
35 |
+
def parse_pdf(self, file_path: str) -> Dict:
|
36 |
+
"""Extrai texto e tabelas de um único PDF."""
|
37 |
+
try:
|
38 |
+
with pdfplumber.open(file_path) as pdf:
|
39 |
+
text = ""
|
40 |
+
tables = []
|
41 |
+
for page in pdf.pages:
|
42 |
+
# Extrai texto
|
43 |
+
page_text = page.extract_text() or ""
|
44 |
+
text += page_text + "\n"
|
45 |
+
# Extrai tabelas
|
46 |
+
page_tables = page.extract_tables()
|
47 |
+
for table in page_tables:
|
48 |
+
tables.append(table)
|
49 |
+
|
50 |
+
# Converte tabelas em texto estruturado
|
51 |
+
table_text = ""
|
52 |
+
for idx, table in enumerate(tables):
|
53 |
+
table_text += f"Tabela {idx + 1}:\n"
|
54 |
+
for row in table:
|
55 |
+
table_text += " | ".join([str(cell) or "" for cell in row]) + "\n"
|
56 |
+
|
57 |
+
return {
|
58 |
+
"content": (text + "\n" + table_text).strip(),
|
59 |
+
"metadata": {
|
60 |
+
"file_name": os.path.basename(file_path),
|
61 |
+
"path": file_path,
|
62 |
+
"num_pages": len(pdf.pages),
|
63 |
+
"has_tables": len(tables) > 0
|
64 |
+
}
|
65 |
+
}
|
66 |
+
except Exception as e:
|
67 |
+
print(f"Erro ao processar {file_path}: {str(e)}")
|
68 |
+
return {"content": "", "metadata": {}}
|
69 |
+
|
70 |
+
def parse_multiple_pdfs(self, pdf_paths: List[str]) -> List[Dict]:
|
71 |
+
"""Extrai texto e tabelas de múltiplos PDFs."""
|
72 |
+
documents = []
|
73 |
+
for path in pdf_paths:
|
74 |
+
doc = self.parse_pdf(path)
|
75 |
+
if doc["content"]:
|
76 |
+
documents.append(doc)
|
77 |
+
return documents
|
78 |
+
|
79 |
+
class KnowledgeBase:
|
80 |
+
"""Gerencia a base de conhecimento metrológico."""
|
81 |
+
def __init__(self):
|
82 |
+
self.documents: List[Dict] = []
|
83 |
+
|
84 |
+
def add_document(self, content: str, metadata: Optional[Dict] = None):
|
85 |
+
doc = {"content": content, "metadata": metadata or {}, "id": len(self.documents)}
|
86 |
+
self.documents.append(doc)
|
87 |
+
|
88 |
+
def add_documents(self, documents: List[Dict]):
|
89 |
+
for doc in documents:
|
90 |
+
self.add_document(doc["content"], doc["metadata"])
|
91 |
+
|
92 |
+
def get_document(self, doc_id: int) -> Dict:
|
93 |
+
return self.documents[doc_id]
|
94 |
+
|
95 |
+
def get_all_contents(self) -> List[str]:
|
96 |
+
return [doc["content"] for doc in self.documents]
|
97 |
+
|
98 |
+
class EmbeddingGenerator:
|
99 |
+
"""Gera embeddings para textos."""
|
100 |
+
def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
|
101 |
+
self.model = SentenceTransformer(model_name)
|
102 |
+
|
103 |
+
def generate(self, texts: List[str]) -> np.ndarray:
|
104 |
+
return self.model.encode(texts, convert_to_numpy=True)
|
105 |
+
|
106 |
+
class VectorStore:
|
107 |
+
"""Armazena e busca embeddings usando FAISS."""
|
108 |
+
def __init__(self, dimension: int):
|
109 |
+
self.index = faiss.IndexFlatL2(dimension)
|
110 |
+
self.doc_ids = []
|
111 |
+
|
112 |
+
def add_vectors(self, embeddings: np.ndarray, doc_ids: List[int]):
|
113 |
+
self.index.add(embeddings)
|
114 |
+
self.doc_ids.extend(doc_ids)
|
115 |
+
|
116 |
+
def search(self, query_embedding: np.ndarray, k: int = 5) -> List[int]:
|
117 |
+
distances, indices = self.index.search(query_embedding, k)
|
118 |
+
return [self.doc_ids[idx] for idx in indices[0]]
|
119 |
+
|
120 |
+
class Retriever:
|
121 |
+
"""Recupera documentos relevantes para uma consulta."""
|
122 |
+
def __init__(self, knowledge_base: KnowledgeBase, vector_store: VectorStore, embedding_generator: EmbeddingGenerator):
|
123 |
+
self.knowledge_base = knowledge_base
|
124 |
+
self.vector_store = vector_store
|
125 |
+
self.embedding_generator = embedding_generator
|
126 |
+
|
127 |
+
def retrieve(self, query: str, k: int = 5) -> List[Dict]:
|
128 |
+
query_embedding = self.embedding_generator.generate([query])
|
129 |
+
doc_ids = self.vector_store.search(query_embedding, k)
|
130 |
+
return [self.knowledge_base.get_document(doc_id) for doc_id in doc_ids]
|
131 |
+
|
132 |
+
class ResponseGenerator:
|
133 |
+
"""Gera respostas técnicas para perguntas metrológicas."""
|
134 |
+
def __init__(self, model_name: str = "gemini-2.0-flash-thinking-exp-1219"):
|
135 |
+
self.model = genai.GenerativeModel(model_name)
|
136 |
+
|
137 |
+
def generate(self, query: str, retrieved_docs: List[Dict]) -> str:
|
138 |
+
context = "\n".join([doc["content"] for doc in retrieved_docs])
|
139 |
+
prompt = (
|
140 |
+
"Você é um especialista em metrologia, com conhecimento em normas como ISO/IEC 17025, incerteza de medição, "
|
141 |
+
"calibração e rastreabilidade. Com base no contexto fornecido, responda à pergunta de forma técnica, precisa e clara:\n\n"
|
142 |
+
f"Contexto:\n{context}\n\nPergunta: {query}\n\nResposta:"
|
143 |
+
)
|
144 |
+
try:
|
145 |
+
response = self.model.generate_content(prompt)
|
146 |
+
return response.text if response else "Desculpe, não consegui gerar uma resposta."
|
147 |
+
except Exception as e:
|
148 |
+
return f"Erro ao gerar resposta: {str(e)}"
|
149 |
+
|
150 |
+
class CacheManager:
|
151 |
+
"""Gerencia cache de respostas."""
|
152 |
+
def __init__(self, cache_file: str = "metrology_cache.pkl"):
|
153 |
+
self.cache_file = cache_file
|
154 |
+
self.cache = self._load_cache()
|
155 |
+
|
156 |
+
def _load_cache(self) -> Dict:
|
157 |
+
if os.path.exists(self.cache_file):
|
158 |
+
with open(self.cache_file, "rb") as f:
|
159 |
+
return pickle.load(f)
|
160 |
+
return {}
|
161 |
+
|
162 |
+
def _save_cache(self):
|
163 |
+
with open(self.cache_file, "wb") as f:
|
164 |
+
pickle.dump(self.cache, f)
|
165 |
+
|
166 |
+
def get(self, query: str) -> Optional[str]:
|
167 |
+
return self.cache.get(query)
|
168 |
+
|
169 |
+
def set(self, query: str, response: str):
|
170 |
+
self.cache[query] = {"response": response, "timestamp": datetime.now()}
|
171 |
+
self._save_cache()
|
172 |
+
|
173 |
+
class QueryProcessor:
|
174 |
+
"""Pré-processa consultas com foco em metrologia."""
|
175 |
+
def __init__(self):
|
176 |
+
self.glossary = MetrologyGlossary()
|
177 |
+
|
178 |
+
def process(self, query: str) -> str:
|
179 |
+
query = query.strip().lower()
|
180 |
+
return self.glossary.enhance_query(query)
|
181 |
+
|
182 |
+
class MetrologyRAGPipeline:
|
183 |
+
"""Orquestra o agente de metrologia avançado."""
|
184 |
+
def __init__(self):
|
185 |
+
self.knowledge_base = KnowledgeBase()
|
186 |
+
self.embedding_generator = EmbeddingGenerator()
|
187 |
+
self.vector_store = VectorStore(dimension=384)
|
188 |
+
self.retriever = Retriever(self.knowledge_base, self.vector_store, self.embedding_generator)
|
189 |
+
self.response_generator = ResponseGenerator()
|
190 |
+
self.cache_manager = CacheManager()
|
191 |
+
self.query_processor = QueryProcessor()
|
192 |
+
self.document_parser = DocumentParser()
|
193 |
+
|
194 |
+
def load_pdfs(self, pdf_paths: List[str] = None, pdf_folder: Optional[str] = None):
|
195 |
+
"""Carrega N arquivos PDF de uma lista de caminhos ou pasta."""
|
196 |
+
if pdf_paths and pdf_folder:
|
197 |
+
raise ValueError("Forneça apenas pdf_paths ou pdf_folder, não ambos.")
|
198 |
+
|
199 |
+
if pdf_folder:
|
200 |
+
pdf_paths = [str(p) for p in Path(pdf_folder).glob("*.pdf")]
|
201 |
+
|
202 |
+
if not pdf_paths:
|
203 |
+
print("Nenhum arquivo PDF fornecido ou encontrado.")
|
204 |
+
return
|
205 |
+
|
206 |
+
print(f"Carregando {len(pdf_paths)} arquivos PDF...")
|
207 |
+
documents = self.document_parser.parse_multiple_pdfs(pdf_paths)
|
208 |
+
if documents:
|
209 |
+
self.knowledge_base.add_documents(documents)
|
210 |
+
self._index_documents()
|
211 |
+
print(f"{len(documents)} documentos indexados com sucesso.")
|
212 |
+
else:
|
213 |
+
print("Nenhum documento válido foi extraído dos PDFs.")
|
214 |
+
|
215 |
+
def _index_documents(self):
|
216 |
+
contents = self.knowledge_base.get_all_contents()
|
217 |
+
if not contents:
|
218 |
+
return
|
219 |
+
embeddings = self.embedding_generator.generate(contents)
|
220 |
+
doc_ids = list(range(len(contents)))
|
221 |
+
self.vector_store.add_vectors(embeddings, doc_ids)
|
222 |
+
|
223 |
+
def query(self, query: str, k: int = 5) -> str:
|
224 |
+
processed_query = self.query_processor.process(query)
|
225 |
+
cached_response = self.cache_manager.get(processed_query)
|
226 |
+
if cached_response:
|
227 |
+
return f"[Resposta do cache] {cached_response}"
|
228 |
+
|
229 |
+
retrieved_docs = self.retriever.retrieve(processed_query, k)
|
230 |
+
response = self.response_generator.generate(processed_query, retrieved_docs)
|
231 |
+
self.cache_manager.set(processed_query, response)
|
232 |
+
return response
|
233 |
+
|
234 |
+
# # Exemplo de uso
|
235 |
+
if __name__ == "__main__":
|
236 |
+
# Inicializa o pipeline
|
237 |
+
rag = MetrologyRAGPipeline()
|
238 |
+
|
239 |
+
# Carrega N arquivos PDF de uma pasta
|
240 |
+
pdf_folder = "/content/" # Substitua pelo caminho real
|
241 |
+
rag.load_pdfs(pdf_folder=pdf_folder)
|
242 |
+
|
243 |
+
# Alternativamente, carrega PDFs específicos
|
244 |
+
pdf_paths = [
|
245 |
+
# "caminho/para/manual_calibrador.pdf",
|
246 |
+
# "caminho/para/iso_17025.pdf",
|
247 |
+
# Adicione mais caminhos
|
248 |
+
]
|
249 |
+
# rag.load_pdfs(pdf_paths=pdf_paths)
|
250 |
+
|
251 |
+
# Faz uma consulta técnica
|
252 |
+
pergunta = "faça uma avaliação sobre o documento CERTIFICADO DE CALIBRAÇÃO N RBC 25/0018"
|
253 |
+
resposta = rag.query(pergunta)
|
254 |
+
print("Agente de Metrologia:", resposta)
|