docs-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docs_cli-0.1.0.dist-info/METADATA +223 -0
- docs_cli-0.1.0.dist-info/RECORD +13 -0
- docs_cli-0.1.0.dist-info/WHEEL +5 -0
- docs_cli-0.1.0.dist-info/entry_points.txt +9 -0
- docs_cli-0.1.0.dist-info/top_level.txt +8 -0
- docs_tc.py +258 -0
- evaluate_coverage.py +362 -0
- extract_data_from_markdown.py +128 -0
- generate_embeddings.py +237 -0
- generate_report.py +118 -0
- generate_report_html.py +204 -0
- limpa_csv.py +170 -0
- merge_markdown.py +91 -0
evaluate_coverage.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# evaluate_coverage.py (Versão com avaliação de cobertura por similaridade de frases e CLI)
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import csv
|
|
5
|
+
import os
|
|
6
|
+
import google.generativeai as genai
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
import time
|
|
9
|
+
import numpy as np # Para cálculo de similaridade de cosseno
|
|
10
|
+
import re # Para manipulação de texto e divisão de frases
|
|
11
|
+
import argparse # Adicionado para parsing de argumentos CLI
|
|
12
|
+
import sys # Adicionado para sys.exit
|
|
13
|
+
|
|
14
|
+
# Carrega as variáveis de ambiente do arquivo .env
|
|
15
|
+
load_dotenv()
|
|
16
|
+
|
|
17
|
+
# --- Configuração da API do Google Gemini (igual ao generate_embeddings.py) ---
|
|
18
|
+
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
|
19
|
+
if not GOOGLE_API_KEY:
|
|
20
|
+
raise ValueError("A variável de ambiente GOOGLE_API_KEY não está configurada.")
|
|
21
|
+
# genai.configure(api_key=GOOGLE_API_KEY) # Removido porque não existe em google.generativeai
|
|
22
|
+
os.environ["GOOGLE_API_KEY"] = GOOGLE_API_KEY # Garante que a variável de ambiente está definida
|
|
23
|
+
EMBEDDING_MODEL = "models/embedding-001"
|
|
24
|
+
|
|
25
|
+
# Limite de caracteres para o embedding, para evitar exceder o limite de tokens da API
|
|
26
|
+
EMBEDDING_TEXT_MAX_LENGTH = 1024
|
|
27
|
+
|
|
28
|
+
# --- Variáveis para controle de Rate Limiting (igual ao generate_embeddings.py) ---
|
|
29
|
+
REQUEST_LIMIT_PER_MINUTE = 150
|
|
30
|
+
request_count = 0
|
|
31
|
+
last_request_time = time.time()
|
|
32
|
+
|
|
33
|
+
# Funções auxiliares (copiadas de generate_embeddings.py e adaptadas)
|
|
34
|
+
|
|
35
|
+
def clean_text_for_embedding(text):
|
|
36
|
+
"""
|
|
37
|
+
Remove caracteres especiais e formatação markdown para texto que será EMBEDDADO.
|
|
38
|
+
Esta função foi aprimorada para lidar com mais casos de Markdown.
|
|
39
|
+
"""
|
|
40
|
+
if not isinstance(text, str):
|
|
41
|
+
return ""
|
|
42
|
+
|
|
43
|
+
# Remove links markdown (e.g., [texto](link))
|
|
44
|
+
text = re.sub(r'\[.*?\]\(.*?\)', '', text)
|
|
45
|
+
# Remove bold/italic (**, __, *, _)
|
|
46
|
+
text = re.sub(r'\*\*|__|\*|_', '', text)
|
|
47
|
+
# Remove cabeçalhos (#, ##, ### etc.)
|
|
48
|
+
text = re.sub(r'#+\s*', '', text)
|
|
49
|
+
# Remove blocos de código (``` ou `)
|
|
50
|
+
text = re.sub(r'```.*?```', '', text, flags=re.DOTALL) # Para blocos multilinhas
|
|
51
|
+
text = re.sub(r'`[^`]*`', '', text) # Para blocos de uma linha
|
|
52
|
+
# Remove blockquotes (>)
|
|
53
|
+
text = re.sub(r'^\s*>\s*', '', text, flags=re.MULTILINE)
|
|
54
|
+
# Remove linhas de lista (- + *)
|
|
55
|
+
text = re.sub(r'^\s*[-+*]\s*', '', text, flags=re.MULTILINE)
|
|
56
|
+
# Remove linhas horizontais (---, ***, ___)
|
|
57
|
+
text = re.sub(r'^-{3,}|^\*{3,}|^__{3,}', '', text, flags=re.MULTILINE)
|
|
58
|
+
# Remove múltiplos espaços e quebras de linha para um único espaço
|
|
59
|
+
text = re.sub(r'\s+', ' ', text).strip()
|
|
60
|
+
# Substitui múltiplas quebras de linha por um único espaço (se houver alguma que restou)
|
|
61
|
+
text = re.sub(r'\n+', ' ', text).strip()
|
|
62
|
+
return text
|
|
63
|
+
|
|
64
|
+
def generate_embedding_with_retry(text_content):
|
|
65
|
+
"""
|
|
66
|
+
Gera um embedding para o conteúdo de texto, com mecanismo de retry e rate limiting.
|
|
67
|
+
"""
|
|
68
|
+
global request_count, last_request_time
|
|
69
|
+
|
|
70
|
+
current_time = time.time()
|
|
71
|
+
elapsed_time = current_time - last_request_time
|
|
72
|
+
|
|
73
|
+
if elapsed_time < 60 and request_count >= REQUEST_LIMIT_PER_MINUTE:
|
|
74
|
+
sleep_duration = 60 - elapsed_time
|
|
75
|
+
print(f" Atingido limite de requisições por minuto. Aguardando {sleep_duration:.2f} segundos...")
|
|
76
|
+
time.sleep(sleep_duration)
|
|
77
|
+
request_count = 0
|
|
78
|
+
last_request_time = time.time()
|
|
79
|
+
elif elapsed_time >= 60:
|
|
80
|
+
request_count = 0
|
|
81
|
+
last_request_time = time.time()
|
|
82
|
+
|
|
83
|
+
request_count += 1
|
|
84
|
+
|
|
85
|
+
retries = 3
|
|
86
|
+
for attempt in range(retries):
|
|
87
|
+
try:
|
|
88
|
+
# É importante limpar o texto antes de gerar o embedding, assim como no generate_embeddings.py
|
|
89
|
+
cleaned_text = clean_text_for_embedding(text_content)
|
|
90
|
+
if not cleaned_text: # Se o texto ficar vazio após a limpeza, não tentar gerar embedding
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
if len(cleaned_text) > EMBEDDING_TEXT_MAX_LENGTH:
|
|
94
|
+
cleaned_text = cleaned_text[:EMBEDDING_TEXT_MAX_LENGTH]
|
|
95
|
+
# print(f" Truncando texto para embedding para {EMBEDDING_TEXT_MAX_LENGTH} caracteres.")
|
|
96
|
+
|
|
97
|
+
response = genai.generate_embeddings(model=EMBEDDING_MODEL, content=cleaned_text) # type: ignore
|
|
98
|
+
return response['embeddings'][0]['values']
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print(f"Erro ao gerar embedding (tentativa {attempt+1}/{retries}): {e}")
|
|
101
|
+
if attempt < retries - 1:
|
|
102
|
+
time.sleep(2 ** attempt) # Espera exponencial
|
|
103
|
+
else:
|
|
104
|
+
return None # Retorna None se todas as retries falharem
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def cosine_similarity(vecA, vecB):
|
|
108
|
+
"""
|
|
109
|
+
Calcula a similaridade de cosseno entre dois vetores.
|
|
110
|
+
"""
|
|
111
|
+
# Certifica-se de que os vetores são arrays numpy
|
|
112
|
+
vecA = np.array(vecA)
|
|
113
|
+
vecB = np.array(vecB)
|
|
114
|
+
|
|
115
|
+
# Verifica se os vetores não estão vazios
|
|
116
|
+
if vecA.size == 0 or vecB.size == 0:
|
|
117
|
+
return 0.0
|
|
118
|
+
|
|
119
|
+
dot_product = np.dot(vecA, vecB)
|
|
120
|
+
norm_A = np.linalg.norm(vecA)
|
|
121
|
+
norm_B = np.linalg.norm(vecB)
|
|
122
|
+
|
|
123
|
+
if norm_A == 0 or norm_B == 0:
|
|
124
|
+
return 0.0
|
|
125
|
+
return dot_product / (norm_A * norm_B)
|
|
126
|
+
|
|
127
|
+
def get_relevant_chunks(query_embedding, processed_chunks, top_k=5):
|
|
128
|
+
"""
|
|
129
|
+
Encontra os chunks mais relevantes com base na similaridade de cosseno.
|
|
130
|
+
"""
|
|
131
|
+
similarities = []
|
|
132
|
+
for chunk_info in processed_chunks:
|
|
133
|
+
chunk_embedding = chunk_info.get('embedding')
|
|
134
|
+
if chunk_embedding and len(chunk_embedding) > 0 and len(query_embedding) == len(chunk_embedding):
|
|
135
|
+
try:
|
|
136
|
+
similarity = cosine_similarity(query_embedding, chunk_embedding)
|
|
137
|
+
similarities.append({'similarity': similarity, 'chunk': chunk_info})
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(f"Aviso: Erro ao calcular similaridade para chunk '{chunk_info.get('chunk_title', 'N/A')}' do documento '{chunk_info.get('document_title', 'N/A')}': {e}")
|
|
140
|
+
else:
|
|
141
|
+
pass # Silenciar avisos excessivos para embeddings inválidos/incompatíveis
|
|
142
|
+
|
|
143
|
+
similarities.sort(key=lambda x: x['similarity'], reverse=True)
|
|
144
|
+
return similarities[:top_k]
|
|
145
|
+
|
|
146
|
+
# MODIFICADO: Adicionado output_json_path como parâmetro
|
|
147
|
+
def evaluate_coverage(qa_filepath="gartner_filtrado_processed.csv",
|
|
148
|
+
chunks_filepath="processed_chunks_with_embeddings.json",
|
|
149
|
+
top_k_chunks=5,
|
|
150
|
+
output_json_path="evaluation_results.json"): # MODIFICADO: parâmetro para nome do arquivo de saída
|
|
151
|
+
"""
|
|
152
|
+
Avalia a cobertura da documentação usando um arquivo CSV de perguntas e respostas ideais.
|
|
153
|
+
A avaliação considera a similaridade de frases da resposta ideal com os chunks relevantes.
|
|
154
|
+
Salva os resultados no caminho especificado por output_json_path.
|
|
155
|
+
"""
|
|
156
|
+
if not os.path.exists(qa_filepath):
|
|
157
|
+
print(f"Erro: O arquivo de perguntas e respostas '{qa_filepath}' não foi encontrado.")
|
|
158
|
+
return False
|
|
159
|
+
if not os.path.exists(chunks_filepath):
|
|
160
|
+
print(f"Erro: O arquivo de chunks processados '{chunks_filepath}' não foi encontrado. Execute 'generate_embeddings.py' primeiro.")
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
print(f"Carregando chunks de '{chunks_filepath}'...")
|
|
164
|
+
try:
|
|
165
|
+
with open(chunks_filepath, 'r', encoding='utf-8') as f:
|
|
166
|
+
processed_chunks = json.load(f)
|
|
167
|
+
except json.JSONDecodeError as e:
|
|
168
|
+
print(f"Erro ao decodificar JSON de '{chunks_filepath}': {e}")
|
|
169
|
+
return False
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print(f"Erro inesperado ao carregar '{chunks_filepath}': {e}")
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
# Filtrar chunks que não têm embedding válido (se houver algum)
|
|
175
|
+
processed_chunks = [c for c in processed_chunks if c.get('embedding') is not None]
|
|
176
|
+
if not processed_chunks:
|
|
177
|
+
print("Erro: Nenhum chunk com embedding válido encontrado após o carregamento. Verifique o arquivo de chunks com embeddings.")
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
print(f"Carregando perguntas e respostas de '{qa_filepath}'...")
|
|
182
|
+
qa_pairs = []
|
|
183
|
+
try:
|
|
184
|
+
with open(qa_filepath, 'r', encoding='utf-8') as f:
|
|
185
|
+
reader = csv.DictReader(f)
|
|
186
|
+
for row in reader:
|
|
187
|
+
# Adaptação para as colunas do seu CSV: 'question' e 'response'
|
|
188
|
+
if 'question' in row and 'response' in row:
|
|
189
|
+
qa_pairs.append({'pergunta': row['question'], 'resposta_ideal': row['response']})
|
|
190
|
+
else:
|
|
191
|
+
print(f"Aviso: Linha ignorada no CSV. Esperava 'question' e 'response': {row}")
|
|
192
|
+
except Exception as e:
|
|
193
|
+
print(f"Erro ao carregar ou ler o arquivo CSV '{qa_filepath}': {e}")
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
if not qa_pairs:
|
|
197
|
+
print("Atenção: Nenhum par de pergunta-resposta válido encontrado no CSV.")
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
evaluation_results = []
|
|
201
|
+
total_questions = len(qa_pairs)
|
|
202
|
+
found_in_top_k_count = 0
|
|
203
|
+
|
|
204
|
+
# Limiares para a avaliação de frases
|
|
205
|
+
MIN_SENTENCE_SIMILARITY_THRESHOLD = 0.65 # Similaridade mínima para uma frase ser considerada "coberta"
|
|
206
|
+
MIN_PHRASES_COVERED_PERCENTAGE = 0.70 # % mínima de frases da resposta ideal que devem ser cobertas
|
|
207
|
+
|
|
208
|
+
print(f"\nIniciando avaliação de cobertura para {total_questions} perguntas...")
|
|
209
|
+
print(f"Configuração de avaliação: Considerar 'Encontrada' se {MIN_PHRASES_COVERED_PERCENTAGE*100:.0f}% das frases da resposta ideal tiverem similaridade >= {MIN_SENTENCE_SIMILARITY_THRESHOLD:.2f} com os top {top_k_chunks} chunks.")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
for i, qa in enumerate(qa_pairs):
|
|
213
|
+
question = qa['pergunta']
|
|
214
|
+
ideal_answer = qa['resposta_ideal']
|
|
215
|
+
|
|
216
|
+
print(f"\n--- Avaliando Pergunta {i + 1}/{total_questions}: '{question[:100]}...' ---") # Mostra o começo da pergunta
|
|
217
|
+
|
|
218
|
+
# 1. Gerar embedding da pergunta
|
|
219
|
+
query_embedding = generate_embedding_with_retry(question)
|
|
220
|
+
if query_embedding is None:
|
|
221
|
+
print(f" Falha ao gerar embedding para a pergunta. Pulando.")
|
|
222
|
+
evaluation_results.append({
|
|
223
|
+
"pergunta": question,
|
|
224
|
+
"resposta_ideal": ideal_answer,
|
|
225
|
+
"status": "Falha no Embedding da Pergunta",
|
|
226
|
+
"cobertura_detalhes": [],
|
|
227
|
+
"top_k_chunks_relevantes": []
|
|
228
|
+
})
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
# 2. Encontrar chunks relevantes
|
|
232
|
+
relevant_chunks_with_similarity = get_relevant_chunks(query_embedding, processed_chunks, top_k=top_k_chunks)
|
|
233
|
+
|
|
234
|
+
# Preparar detalhes dos chunks relevantes para o relatório
|
|
235
|
+
top_chunks_report = []
|
|
236
|
+
for item in relevant_chunks_with_similarity:
|
|
237
|
+
chunk = item['chunk']
|
|
238
|
+
top_chunks_report.append({
|
|
239
|
+
"document_title": chunk.get('document_title', 'N/A'),
|
|
240
|
+
"chunk_title": chunk.get('chunk_title', 'N/A'),
|
|
241
|
+
"filepath": chunk.get('document_filepath', 'N/A'),
|
|
242
|
+
"similarity_to_query": f"{item['similarity']:.4f}",
|
|
243
|
+
"content_preview": chunk.get('chunk_content', '')[:200] + "..." if len(chunk.get('chunk_content', '')) > 200 else chunk.get('chunk_content', '')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
# 3. Avaliar cobertura da resposta ideal pelas frases
|
|
247
|
+
answer_covered = False
|
|
248
|
+
covered_sentences_count = 0
|
|
249
|
+
coverage_details = []
|
|
250
|
+
coverage_percentage = 0.0 # Inicializa coverage_percentage
|
|
251
|
+
|
|
252
|
+
# Divide a resposta ideal em frases e as limpa
|
|
253
|
+
# Usa um regex mais robusto para split de frases, considerando múltiplos delimitadores
|
|
254
|
+
ideal_answer_sentences_raw = re.split(r'(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?|\!)\s', ideal_answer)
|
|
255
|
+
ideal_answer_sentences = [clean_text_for_embedding(s).strip() for s in ideal_answer_sentences_raw if clean_text_for_embedding(s).strip()]
|
|
256
|
+
|
|
257
|
+
if not ideal_answer_sentences:
|
|
258
|
+
print(f" Aviso: Resposta ideal vazia ou não divisível em frases após limpeza para '{question}'.")
|
|
259
|
+
status = "Resposta Ideal Vazia/Inválida"
|
|
260
|
+
else:
|
|
261
|
+
for ideal_sentence in ideal_answer_sentences:
|
|
262
|
+
sentence_embedding = generate_embedding_with_retry(ideal_sentence)
|
|
263
|
+
|
|
264
|
+
sentence_covered_by_chunk = False
|
|
265
|
+
best_similarity_for_sentence = 0.0
|
|
266
|
+
covered_by_chunk_info = "N/A"
|
|
267
|
+
|
|
268
|
+
if sentence_embedding is None:
|
|
269
|
+
coverage_details.append({
|
|
270
|
+
"frase_ideal": ideal_sentence,
|
|
271
|
+
"status": "Falha no Embedding da Frase",
|
|
272
|
+
"similaridade_max": 0.0,
|
|
273
|
+
"chunk_correspondente": "N/A"
|
|
274
|
+
})
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
for item in relevant_chunks_with_similarity:
|
|
278
|
+
chunk_content_for_embedding = item['chunk'].get('embedding') # Usa o embedding do chunk diretamente
|
|
279
|
+
if chunk_content_for_embedding: # Verifica se o embedding do chunk é válido
|
|
280
|
+
current_similarity = cosine_similarity(sentence_embedding, chunk_content_for_embedding)
|
|
281
|
+
if current_similarity > best_similarity_for_sentence:
|
|
282
|
+
best_similarity_for_sentence = current_similarity
|
|
283
|
+
covered_by_chunk_info = f"Doc: {item['chunk'].get('document_title', 'N/A')} | Sec: {item['chunk'].get('chunk_title', 'N/A')}"
|
|
284
|
+
|
|
285
|
+
if current_similarity >= MIN_SENTENCE_SIMILARITY_THRESHOLD:
|
|
286
|
+
sentence_covered_by_chunk = True
|
|
287
|
+
break # Já encontrou um chunk relevante para esta frase
|
|
288
|
+
|
|
289
|
+
if sentence_covered_by_chunk:
|
|
290
|
+
covered_sentences_count += 1
|
|
291
|
+
|
|
292
|
+
coverage_details.append({
|
|
293
|
+
"frase_ideal": ideal_sentence,
|
|
294
|
+
"status": "Coberta" if sentence_covered_by_chunk else "Não Coberta",
|
|
295
|
+
"similaridade_max": f"{best_similarity_for_sentence:.4f}",
|
|
296
|
+
"chunk_correspondente": covered_by_chunk_info
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
# Determinar o status geral da cobertura
|
|
300
|
+
total_sentences = len(ideal_answer_sentences)
|
|
301
|
+
if total_sentences > 0:
|
|
302
|
+
coverage_percentage = covered_sentences_count / total_sentences
|
|
303
|
+
if coverage_percentage >= MIN_PHRASES_COVERED_PERCENTAGE:
|
|
304
|
+
answer_covered = True
|
|
305
|
+
|
|
306
|
+
status = "Encontrada (Cobertura Suficiente)" if answer_covered else "Não Encontrada (Cobertura Insuficiente)"
|
|
307
|
+
print(f" Status: {status}. Frases cobertas: {covered_sentences_count}/{total_sentences} ({coverage_percentage*100:.2f}%)")
|
|
308
|
+
|
|
309
|
+
if answer_covered:
|
|
310
|
+
found_in_top_k_count += 1
|
|
311
|
+
|
|
312
|
+
evaluation_results.append({
|
|
313
|
+
"pergunta": question,
|
|
314
|
+
"resposta_ideal": ideal_answer,
|
|
315
|
+
"status": status,
|
|
316
|
+
"cobertura_detalhes": coverage_details,
|
|
317
|
+
"top_k_chunks_relevantes": top_chunks_report # Adiciona os chunks mais relevantes para a pergunta
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
# Salvar resultados da avaliação
|
|
321
|
+
# MODIFICADO: usa output_json_path
|
|
322
|
+
try:
|
|
323
|
+
with open(output_json_path, 'w', encoding='utf-8') as f:
|
|
324
|
+
json.dump(evaluation_results, f, ensure_ascii=False, indent=4)
|
|
325
|
+
print(f"\nResultados da avaliação salvos em '{output_json_path}'.")
|
|
326
|
+
except Exception as e:
|
|
327
|
+
print(f"Erro ao salvar os resultados da avaliação: {e}")
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
print(f"\n--- Resumo da Avaliação ---")
|
|
331
|
+
print(f"Total de perguntas avaliadas: {total_questions}")
|
|
332
|
+
if total_questions > 0:
|
|
333
|
+
print(f"Perguntas consideradas 'Encontradas (Cobertura Suficiente)': {found_in_top_k_count}")
|
|
334
|
+
print(f"Porcentagem de cobertura geral: {(found_in_top_k_count / total_questions * 100):.2f}%")
|
|
335
|
+
else:
|
|
336
|
+
print("Nenhuma pergunta foi avaliada.")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
return True
|
|
340
|
+
|
|
341
|
+
def cli_main():
|
|
342
|
+
parser = argparse.ArgumentParser(description="Avalia a cobertura da documentação usando embeddings.")
|
|
343
|
+
parser.add_argument("qa_filepath", help="Caminho para o arquivo CSV de perguntas e respostas ideais.")
|
|
344
|
+
parser.add_argument("embeddings_filepath", help="Caminho para o arquivo JSON de chunks processados com embeddings.")
|
|
345
|
+
parser.add_argument("-k", "--top_k_chunks", type=int, default=5, help="Número de chunks mais relevantes a considerar (padrão: 5).")
|
|
346
|
+
parser.add_argument("-o", "--output", default="evaluation_results.json", help="Arquivo de saída para os resultados da avaliação (padrão: evaluation_results.json).")
|
|
347
|
+
args = parser.parse_args()
|
|
348
|
+
|
|
349
|
+
success = evaluate_coverage(
|
|
350
|
+
qa_filepath=args.qa_filepath,
|
|
351
|
+
chunks_filepath=args.embeddings_filepath,
|
|
352
|
+
top_k_chunks=args.top_k_chunks,
|
|
353
|
+
output_json_path=args.output
|
|
354
|
+
)
|
|
355
|
+
if not success:
|
|
356
|
+
print("\nA avaliação de cobertura da documentação falhou.")
|
|
357
|
+
sys.exit(1)
|
|
358
|
+
else:
|
|
359
|
+
print("\nA avaliação de cobertura da documentação foi concluída com sucesso.")
|
|
360
|
+
|
|
361
|
+
if __name__ == "__main__":
|
|
362
|
+
cli_main() # Now it calls the cli_main function
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Novo ou modificado: extract_consolidated_md_to_raw_json.py
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
def extract_docs_from_consolidated_md(input_md_path="corpus_consolidated.md", output_json_path="raw_docs.json"):
|
|
7
|
+
"""
|
|
8
|
+
Lê o arquivo MD consolidado, divide-o em documentos individuais
|
|
9
|
+
e extrai seu conteúdo e metadados para raw_docs.json.
|
|
10
|
+
"""
|
|
11
|
+
if not os.path.exists(input_md_path):
|
|
12
|
+
print(f"Erro: O arquivo consolidado '{input_md_path}' não foi encontrado.")
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
print(f"Extraindo documentos de '{input_md_path}'...")
|
|
16
|
+
|
|
17
|
+
with open(input_md_path, 'r', encoding='utf-8') as f:
|
|
18
|
+
full_content = f.read()
|
|
19
|
+
|
|
20
|
+
# Padrão para identificar o início de um novo documento e capturar o caminho do arquivo
|
|
21
|
+
# E também o conteúdo de metadados
|
|
22
|
+
doc_sections = re.split(r'(^## Arquivo: (.*?)\.md$)', full_content, flags=re.MULTILINE)
|
|
23
|
+
|
|
24
|
+
extracted_docs = []
|
|
25
|
+
current_doc_filepath = None
|
|
26
|
+
current_doc_content = []
|
|
27
|
+
|
|
28
|
+
# Ignora o primeiro elemento de doc_sections se estiver vazio (antes do primeiro cabeçalho)
|
|
29
|
+
# ou se for a primeira quebra
|
|
30
|
+
for i, section_part in enumerate(doc_sections):
|
|
31
|
+
if i == 0:
|
|
32
|
+
continue # Ignora o que vem antes do primeiro "## Arquivo:"
|
|
33
|
+
|
|
34
|
+
if section_part.startswith("## Arquivo:"): # É o cabeçalho de um novo documento
|
|
35
|
+
# Se já temos conteúdo do documento anterior, salve-o
|
|
36
|
+
if current_doc_filepath and current_doc_content:
|
|
37
|
+
# Extrair metadados e conteúdo
|
|
38
|
+
full_doc_content_str = "\n".join(current_doc_content).strip()
|
|
39
|
+
title, slug, content_for_doc = extract_metadata_and_content(full_doc_content_str, current_doc_filepath)
|
|
40
|
+
extracted_docs.append({
|
|
41
|
+
"title": title,
|
|
42
|
+
"slug": slug,
|
|
43
|
+
"content": content_for_doc,
|
|
44
|
+
"filepath": current_doc_filepath
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
# Iniciar um novo documento
|
|
48
|
+
match = re.match(r'^## Arquivo: (.*?)\.md$', section_part.strip())
|
|
49
|
+
if match:
|
|
50
|
+
current_doc_filepath = match.group(1) + ".md" # Recompõe o filepath
|
|
51
|
+
current_doc_content = [] # Reseta o conteúdo
|
|
52
|
+
elif i % 2 != 0: # Isso pega o conteúdo que segue o cabeçalho '## Arquivo:'
|
|
53
|
+
# Este é o conteúdo completo do documento, incluindo o Metadata_Start/End
|
|
54
|
+
current_doc_content.append(section_part.strip())
|
|
55
|
+
# else: este é o grupo de captura do regex, ignoramos aqui
|
|
56
|
+
|
|
57
|
+
# Salva o último documento após o loop
|
|
58
|
+
if current_doc_filepath and current_doc_content:
|
|
59
|
+
full_doc_content_str = "\n".join(current_doc_content).strip()
|
|
60
|
+
title, slug, content_for_doc = extract_metadata_and_content(full_doc_content_str, current_doc_filepath)
|
|
61
|
+
extracted_docs.append({
|
|
62
|
+
"title": title,
|
|
63
|
+
"slug": slug,
|
|
64
|
+
"content": content_for_doc,
|
|
65
|
+
"filepath": current_doc_filepath
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
# Verifica se algum documento foi realmente extraído
|
|
69
|
+
if not extracted_docs:
|
|
70
|
+
print("Atenção: Nenhum documento válido foi extraído do arquivo consolidado.")
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
with open(output_json_path, 'w', encoding='utf-8') as f:
|
|
75
|
+
json.dump(extracted_docs, f, ensure_ascii=False, indent=4)
|
|
76
|
+
print(f"Extração concluída. Salvou {len(extracted_docs)} documentos em '{output_json_path}'.")
|
|
77
|
+
return True
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f"Erro ao salvar o arquivo JSON de documentos brutos: {e}")
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def extract_metadata_and_content(doc_full_text, default_filepath):
|
|
83
|
+
"""
|
|
84
|
+
Extrai metadados (title, slug) e o conteúdo principal de um bloco de documento,
|
|
85
|
+
ignorando as seções Metadata_Start/End.
|
|
86
|
+
"""
|
|
87
|
+
title = "Título Desconhecido"
|
|
88
|
+
slug = ""
|
|
89
|
+
content = doc_full_text
|
|
90
|
+
|
|
91
|
+
# Expressão regular para encontrar o bloco Metadata_Start/End
|
|
92
|
+
metadata_match = re.search(r'## Metadata_Start(.*?)## Metadata_End', doc_full_text, re.DOTALL)
|
|
93
|
+
|
|
94
|
+
if metadata_match:
|
|
95
|
+
metadata_block = metadata_match.group(1)
|
|
96
|
+
# Extrai title e slug do bloco de metadados
|
|
97
|
+
title_match = re.search(r'## title: (.*)', metadata_block)
|
|
98
|
+
if title_match:
|
|
99
|
+
title = title_match.group(1).strip()
|
|
100
|
+
|
|
101
|
+
slug_match = re.search(r'## slug: (.*)', metadata_block)
|
|
102
|
+
if slug_match:
|
|
103
|
+
slug = slug_match.group(1).strip()
|
|
104
|
+
|
|
105
|
+
# Remove o bloco de metadados do conteúdo principal
|
|
106
|
+
content = re.sub(r'## Metadata_Start.*?## Metadata_End', '', doc_full_text, flags=re.DOTALL).strip()
|
|
107
|
+
|
|
108
|
+
# Remove o cabeçalho "## Arquivo: ..." que foi usado para dividir
|
|
109
|
+
content = re.sub(r'^## Arquivo: .*$', '', content, flags=re.MULTILINE).strip()
|
|
110
|
+
|
|
111
|
+
return title, slug, content
|
|
112
|
+
|
|
113
|
+
def cli_main():
|
|
114
|
+
import argparse
|
|
115
|
+
parser = argparse.ArgumentParser(description="Extrai dados do arquivo MD consolidado para JSON.")
|
|
116
|
+
parser.add_argument("input_md_path", help="Caminho para o arquivo MD consolidado de entrada.")
|
|
117
|
+
parser.add_argument("output_json_path", help="Caminho para o arquivo JSON de saída (ex: raw_docs.json).")
|
|
118
|
+
args = parser.parse_args()
|
|
119
|
+
success = extract_docs_from_consolidated_md(args.input_md_path, args.output_json_path)
|
|
120
|
+
if not success:
|
|
121
|
+
print("A extração de documentos do MD consolidado falhou.")
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
else:
|
|
124
|
+
print("Extração de documentos do MD consolidado concluída com sucesso.")
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
import sys # Adicione se não estiver importado
|
|
128
|
+
cli_main()
|