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
generate_embeddings.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# generate_embeddings.py (Atualizado para lidar com a estrutura e com Rate Limiting e incluir slug)
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import google.generativeai as genai
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
import time
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
# Carrega as variáveis de ambiente do arquivo .env
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
# --- Configuração da API do Google Gemini ---
|
|
14
|
+
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
|
15
|
+
if not GOOGLE_API_KEY:
|
|
16
|
+
raise ValueError("A variável de ambiente GOOGLE_API_KEY não está configurada.")
|
|
17
|
+
genai.configure(api_key=GOOGLE_API_KEY) # type: ignore
|
|
18
|
+
EMBEDDING_MODEL = "models/embedding-001"
|
|
19
|
+
|
|
20
|
+
# Limite de caracteres para o embedding, para evitar exceder o limite de tokens da API
|
|
21
|
+
EMBEDDING_TEXT_MAX_LENGTH = 1024
|
|
22
|
+
|
|
23
|
+
# --- Variáveis para controle de Rate Limiting ---
|
|
24
|
+
REQUEST_LIMIT_PER_MINUTE = 150
|
|
25
|
+
request_count = 0
|
|
26
|
+
last_request_time = time.time() # Inicializa com o tempo atual
|
|
27
|
+
|
|
28
|
+
def clean_text_for_embedding(text):
|
|
29
|
+
"""
|
|
30
|
+
Remove caracteres especiais e formatação markdown para texto que será EMBEDDADO.
|
|
31
|
+
Esta função foi aprimorada para lidar com mais casos de Markdown.
|
|
32
|
+
"""
|
|
33
|
+
# Remove links markdown (e.g., [texto](link))
|
|
34
|
+
text = re.sub(r'\[.*?\]\(.*?\)', '', text)
|
|
35
|
+
# Remove bold/italic (**, __, *, _)
|
|
36
|
+
text = re.sub(r'\*\*|__|\*|_', '', text)
|
|
37
|
+
# Remove cabeçalhos (#, ##, ### etc.)
|
|
38
|
+
text = re.sub(r'#+\s*', '', text)
|
|
39
|
+
# Remove blocos de código (``` ou `)
|
|
40
|
+
text = re.sub(r'```.*?```', '', text, flags=re.DOTALL) # Para blocos multilinhas
|
|
41
|
+
text = re.sub(r'`[^`]*`', '', text) # Para blocos de uma linha
|
|
42
|
+
# Remove blockquotes (>)
|
|
43
|
+
text = re.sub(r'^\s*>\s*', '', text, flags=re.MULTILINE)
|
|
44
|
+
# Remove linhas de lista (- + *)
|
|
45
|
+
text = re.sub(r'^\s*[-+*]\s*', '', text, flags=re.MULTILINE)
|
|
46
|
+
# Remove linhas horizontais (---, ***, ___)
|
|
47
|
+
text = re.sub(r'^-{3,}|^\*{3,}|^__{3,}', '', text, flags=re.MULTILINE)
|
|
48
|
+
# Remove múltiplos espaços e quebras de linha para um único espaço
|
|
49
|
+
text = re.sub(r'\s+', ' ', text).strip()
|
|
50
|
+
# Substitui múltiplas quebras de linha por um único espaço (se houver alguma que restou)
|
|
51
|
+
text = re.sub(r'\n+', ' ', text).strip()
|
|
52
|
+
return text
|
|
53
|
+
|
|
54
|
+
def split_content_into_semantic_chunks(document_content, doc_title, filepath, doc_slug): # MODIFICADO: adicionado doc_slug
|
|
55
|
+
"""
|
|
56
|
+
Divide o conteúdo de um único documento Markdown em chunks baseados em cabeçalhos (H2, H3, etc.).
|
|
57
|
+
Ignora seções de metadados se ainda estiverem presentes.
|
|
58
|
+
Inclui o slug do documento em cada chunk.
|
|
59
|
+
"""
|
|
60
|
+
chunks = []
|
|
61
|
+
|
|
62
|
+
# Remove o bloco de metadados se por acaso ainda estiver aqui
|
|
63
|
+
content_without_metadata = re.sub(r'## Metadata_Start.*?## Metadata_End', '', document_content, flags=re.DOTALL).strip()
|
|
64
|
+
|
|
65
|
+
# Divide por qualquer cabeçalho de nível 2 ou superior (##, ###, etc.)
|
|
66
|
+
sections = re.split(r'(^##+\s*.*$)', content_without_metadata, flags=re.MULTILINE)
|
|
67
|
+
|
|
68
|
+
current_chunk_title = doc_title
|
|
69
|
+
current_chunk_content_lines = []
|
|
70
|
+
|
|
71
|
+
for i, part in enumerate(sections):
|
|
72
|
+
if not part.strip():
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
if part.startswith("##"):
|
|
76
|
+
if current_chunk_content_lines:
|
|
77
|
+
chunks.append({
|
|
78
|
+
"document_title": doc_title,
|
|
79
|
+
"document_filepath": filepath,
|
|
80
|
+
"document_slug": doc_slug, # MODIFICADO: adicionado slug
|
|
81
|
+
"chunk_title": current_chunk_title.strip(),
|
|
82
|
+
"chunk_content": "\n".join(current_chunk_content_lines).strip()
|
|
83
|
+
})
|
|
84
|
+
current_chunk_content_lines = []
|
|
85
|
+
|
|
86
|
+
current_chunk_title = part.strip().lstrip('# ').strip()
|
|
87
|
+
else:
|
|
88
|
+
current_chunk_content_lines.append(part.strip())
|
|
89
|
+
|
|
90
|
+
if current_chunk_content_lines:
|
|
91
|
+
chunks.append({
|
|
92
|
+
"document_title": doc_title,
|
|
93
|
+
"document_filepath": filepath,
|
|
94
|
+
"document_slug": doc_slug, # MODIFICADO: adicionado slug
|
|
95
|
+
"chunk_title": current_chunk_title.strip(),
|
|
96
|
+
"chunk_content": "\n".join(current_chunk_content_lines).strip()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
if not chunks and content_without_metadata.strip():
|
|
100
|
+
chunks.append({
|
|
101
|
+
"document_title": doc_title,
|
|
102
|
+
"document_filepath": filepath,
|
|
103
|
+
"document_slug": doc_slug, # MODIFICADO: adicionado slug
|
|
104
|
+
"chunk_title": doc_title, # Usa o título do documento se não houver seções
|
|
105
|
+
"chunk_content": content_without_metadata.strip()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
return [chunk for chunk in chunks if chunk['chunk_content'].strip()]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def generate_embedding_with_retry(text_content):
|
|
112
|
+
"""
|
|
113
|
+
Gera um embedding para o conteúdo de texto, com mecanismo de retry e rate limiting.
|
|
114
|
+
"""
|
|
115
|
+
global request_count, last_request_time
|
|
116
|
+
|
|
117
|
+
current_time = time.time()
|
|
118
|
+
elapsed_time = current_time - last_request_time
|
|
119
|
+
|
|
120
|
+
if elapsed_time < 60 and request_count >= REQUEST_LIMIT_PER_MINUTE:
|
|
121
|
+
sleep_duration = 60 - elapsed_time
|
|
122
|
+
print(f" Atingido limite de requisições por minuto. Aguardando {sleep_duration:.2f} segundos...")
|
|
123
|
+
time.sleep(sleep_duration)
|
|
124
|
+
request_count = 0
|
|
125
|
+
last_request_time = time.time()
|
|
126
|
+
elif elapsed_time >= 60:
|
|
127
|
+
request_count = 0
|
|
128
|
+
last_request_time = time.time()
|
|
129
|
+
|
|
130
|
+
request_count += 1
|
|
131
|
+
|
|
132
|
+
retries = 3
|
|
133
|
+
for attempt in range(retries):
|
|
134
|
+
try:
|
|
135
|
+
response = genai.embed_content(model=EMBEDDING_MODEL, content=[text_content]) # type: ignore
|
|
136
|
+
return response['embedding'][0]
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"Erro ao gerar embedding (tentativa {attempt+1}/{retries}): {e}")
|
|
139
|
+
if attempt < retries - 1:
|
|
140
|
+
time.sleep(2 ** attempt)
|
|
141
|
+
else:
|
|
142
|
+
return None
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
def generate_embeddings_for_docs(input_json_path="raw_docs.json", output_json_path="embeddings.json"):
|
|
146
|
+
"""
|
|
147
|
+
Lê o JSON com dados de documentos (já separados), divide cada um em chunks,
|
|
148
|
+
gera embeddings para cada chunk, e salva o resultado final em um novo JSON.
|
|
149
|
+
"""
|
|
150
|
+
if not os.path.exists(input_json_path):
|
|
151
|
+
print(f"Erro: O arquivo '{input_json_path}' não foi encontrado. Por favor, execute o script de extração (ex: 'extract_consolidated_md_to_raw_json.py') primeiro.")
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
print(f"Gerando embeddings para documentos de '{input_json_path}'...")
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
with open(input_json_path, 'r', encoding='utf-8') as f:
|
|
158
|
+
raw_docs = json.load(f)
|
|
159
|
+
except json.JSONDecodeError as e:
|
|
160
|
+
print(f"Erro ao decodificar JSON de '{input_json_path}': {e}")
|
|
161
|
+
return False
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(f"Erro inesperado ao carregar '{input_json_path}': {e}")
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
all_processed_chunks = []
|
|
167
|
+
total_raw_docs = len(raw_docs)
|
|
168
|
+
|
|
169
|
+
for i, doc_data in enumerate(raw_docs):
|
|
170
|
+
doc_title = doc_data.get("title", "Título Desconhecido")
|
|
171
|
+
doc_content_full = doc_data.get("content", "")
|
|
172
|
+
file_path_relative = doc_data.get("filepath", "N/A")
|
|
173
|
+
doc_slug = doc_data.get("slug", "") # MODIFICADO: Obtém o slug
|
|
174
|
+
|
|
175
|
+
print(f"\n--- Processando documento {i + 1}/{total_raw_docs}: '{doc_title}' ({file_path_relative}) ---")
|
|
176
|
+
|
|
177
|
+
# MODIFICADO: Passa o doc_slug para a função de chunking
|
|
178
|
+
chunks_for_doc = split_content_into_semantic_chunks(doc_content_full, doc_title, file_path_relative, doc_slug)
|
|
179
|
+
|
|
180
|
+
if not chunks_for_doc:
|
|
181
|
+
print(f"Atenção: Nenhum chunk válido gerado para o documento '{doc_title}'. Pulando.")
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
for chunk_idx, chunk in enumerate(chunks_for_doc):
|
|
185
|
+
embedding_text_raw = f"Documento: {chunk['document_title']}. Seção: {chunk['chunk_title']}. Conteúdo: {chunk['chunk_content']}"
|
|
186
|
+
embedding_text = clean_text_for_embedding(embedding_text_raw)
|
|
187
|
+
|
|
188
|
+
if len(embedding_text) > EMBEDDING_TEXT_MAX_LENGTH:
|
|
189
|
+
embedding_text = embedding_text[:EMBEDDING_TEXT_MAX_LENGTH]
|
|
190
|
+
print(f" Truncando chunk {chunk_idx+1} de '{chunk['chunk_title']}' para {EMBEDDING_TEXT_MAX_LENGTH} caracteres para embedding.")
|
|
191
|
+
|
|
192
|
+
if not embedding_text.strip():
|
|
193
|
+
print(f" Atenção: Texto limpo para embedding vazio para chunk '{chunk['chunk_title']}' do documento '{doc_title}'. Pulando embedding.")
|
|
194
|
+
chunk["embedding"] = None
|
|
195
|
+
all_processed_chunks.append(chunk)
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
print(f" Gerando embedding para chunk {chunk_idx+1} de '{chunk['chunk_title']}'...")
|
|
199
|
+
chunk_embedding = generate_embedding_with_retry(embedding_text)
|
|
200
|
+
|
|
201
|
+
if chunk_embedding is not None:
|
|
202
|
+
chunk["embedding"] = chunk_embedding
|
|
203
|
+
all_processed_chunks.append(chunk)
|
|
204
|
+
else:
|
|
205
|
+
print(f" Atenção: Falha ao gerar embedding para chunk '{chunk['chunk_title']}' do documento '{doc_title}'. Chunk será incluído sem embedding.")
|
|
206
|
+
chunk["embedding"] = None
|
|
207
|
+
all_processed_chunks.append(chunk)
|
|
208
|
+
|
|
209
|
+
if not all_processed_chunks:
|
|
210
|
+
print("Nenhum chunk processado com sucesso (sem embeddings ou dados de entrada).")
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
with open(output_json_path, 'w', encoding='utf-8') as f:
|
|
215
|
+
json.dump(all_processed_chunks, f, ensure_ascii=False, indent=4)
|
|
216
|
+
print(f"\nGeração de embeddings concluída. Salvou {len(all_processed_chunks)} chunks com embeddings em '{output_json_path}'.")
|
|
217
|
+
return True
|
|
218
|
+
except Exception as e:
|
|
219
|
+
print(f"Erro ao salvar o arquivo JSON: {e}")
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
def cli_main():
|
|
223
|
+
import argparse
|
|
224
|
+
parser = argparse.ArgumentParser(description="Gera embeddings para documentos a partir de um JSON.")
|
|
225
|
+
parser.add_argument("input_json_path", help="Caminho para o arquivo JSON de entrada (ex: raw_docs.json).")
|
|
226
|
+
parser.add_argument("output_json_path", help="Caminho para o arquivo JSON de saída dos embeddings (ex: embeddings.json).")
|
|
227
|
+
args = parser.parse_args()
|
|
228
|
+
success = generate_embeddings_for_docs(args.input_json_path, args.output_json_path)
|
|
229
|
+
if not success:
|
|
230
|
+
print("A geração de embeddings falhou.")
|
|
231
|
+
sys.exit(1) # Garante que sys está importado se for usar aqui
|
|
232
|
+
else:
|
|
233
|
+
print("Geração de embeddings concluída com sucesso.")
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
import sys # Adicionado aqui para garantir que está disponível para cli_main
|
|
237
|
+
cli_main()
|
generate_report.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# generate_md_report.py
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
def generate_md_report(evaluation_json_path="evaluation_results.json", output_md_path="coverage_report.md", top_k_chunks=5):
|
|
8
|
+
"""
|
|
9
|
+
Gera um relatório Markdown a partir do JSON de avaliação.
|
|
10
|
+
"""
|
|
11
|
+
if not os.path.exists(evaluation_json_path):
|
|
12
|
+
print(f"Erro: O arquivo JSON de avaliação '{evaluation_json_path}' não foi encontrado. Execute 'evaluate_coverage.py' primeiro.")
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
print(f"Lendo dados de avaliação de '{evaluation_json_path}'...")
|
|
16
|
+
try:
|
|
17
|
+
with open(evaluation_json_path, 'r', encoding='utf-8') as f:
|
|
18
|
+
evaluation_results = json.load(f)
|
|
19
|
+
except json.JSONDecodeError as e:
|
|
20
|
+
print(f"Erro ao decodificar JSON de '{evaluation_json_path}': {e}")
|
|
21
|
+
return False
|
|
22
|
+
except Exception as e:
|
|
23
|
+
print(f"Erro inesperado ao carregar '{evaluation_json_path}': {e}")
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
if not evaluation_results:
|
|
27
|
+
print("Atenção: Nenhum resultado de avaliação válido encontrado no JSON.")
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
# Calcular resumo
|
|
31
|
+
total_questions = len(evaluation_results)
|
|
32
|
+
found_count = sum(1 for item in evaluation_results if "Encontrada" in item['status'])
|
|
33
|
+
coverage_percentage = (found_count / total_questions * 100) if total_questions > 0 else 0
|
|
34
|
+
|
|
35
|
+
report_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
36
|
+
|
|
37
|
+
md_content = f"""# Relatório de Cobertura da Documentação
|
|
38
|
+
|
|
39
|
+
**Gerado em:** {report_date}
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Resumo Geral
|
|
44
|
+
|
|
45
|
+
* **Total de Perguntas Avaliadas:** {total_questions}
|
|
46
|
+
* **Perguntas com Cobertura Suficiente:** {found_count}
|
|
47
|
+
* **Porcentagem de Cobertura Geral:** {coverage_percentage:.2f}%
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Resultados Detalhados por Pergunta
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
for i, item in enumerate(evaluation_results):
|
|
56
|
+
md_content += f"""
|
|
57
|
+
### {i + 1}. Pergunta: {item['pergunta']}
|
|
58
|
+
|
|
59
|
+
* **Status:** {item['status']}
|
|
60
|
+
* **Resposta Ideal:** {item['resposta_ideal']}
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
# Detalhes da Cobertura por Frase
|
|
64
|
+
if item.get('cobertura_detalhes'):
|
|
65
|
+
md_content += f"#### Cobertura por Frase da Resposta Ideal:\n\n"
|
|
66
|
+
for detail in item['cobertura_detalhes']:
|
|
67
|
+
status_icon = "✅" if "Coberta" in detail['status'] else "❌"
|
|
68
|
+
md_content += f"""* **{status_icon} Frase:** {detail['frase_ideal']}
|
|
69
|
+
* **Status da Frase:** {detail['status']}
|
|
70
|
+
* **Similaridade Máx. para Frase:** {detail['similaridade_max']}
|
|
71
|
+
* **Chunk Correspondente:** {detail['chunk_correspondente']}
|
|
72
|
+
"""
|
|
73
|
+
md_content += "\n" # Adiciona uma linha em branco para espaçamento
|
|
74
|
+
|
|
75
|
+
# Top K Chunks Relevantes
|
|
76
|
+
md_content += f"#### Top {top_k_chunks} Chunks Relevantes para a Pergunta:\n\n"
|
|
77
|
+
if item.get('top_k_chunks_relevantes'):
|
|
78
|
+
for chunk in item['top_k_chunks_relevantes']:
|
|
79
|
+
md_content += f"""* **Documento:** {chunk['document_title']}
|
|
80
|
+
* **Seção:** {chunk['chunk_title']}
|
|
81
|
+
* **Caminho do Arquivo:** `{chunk['filepath']}`
|
|
82
|
+
* **Similaridade com a Pergunta:** {chunk['similarity_to_query']}
|
|
83
|
+
* **Conteúdo (preview):** `{chunk['content_preview'].replace('`', '\\`')}`
|
|
84
|
+
"""
|
|
85
|
+
md_content += "\n" # Adiciona uma linha em branco para espaçamento
|
|
86
|
+
else:
|
|
87
|
+
md_content += "* Nenhum chunk relevante encontrado.\n\n"
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
with open(output_md_path, 'w', encoding='utf-8') as f:
|
|
91
|
+
f.write(md_content)
|
|
92
|
+
print(f"Relatório Markdown gerado com sucesso em '{output_md_path}'.")
|
|
93
|
+
return True
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"Erro ao salvar o relatório Markdown: {e}")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
import argparse # Adicione
|
|
100
|
+
import sys # Adicione
|
|
101
|
+
|
|
102
|
+
parser = argparse.ArgumentParser(description="Gera relatório Markdown da avaliação de cobertura.")
|
|
103
|
+
# Tornando-os posicionais para simplificar a chamada via subprocesso
|
|
104
|
+
parser.add_argument("evaluation_json_path", help="Caminho para o arquivo JSON de resultados da avaliação.")
|
|
105
|
+
parser.add_argument("output_md_path", help="Caminho para salvar o relatório Markdown.")
|
|
106
|
+
parser.add_argument("top_k_chunks", type=int, help="Valor de top_k_chunks usado na avaliação.")
|
|
107
|
+
args = parser.parse_args()
|
|
108
|
+
|
|
109
|
+
success = generate_md_report(
|
|
110
|
+
evaluation_json_path=args.evaluation_json_path,
|
|
111
|
+
output_md_path=args.output_md_path,
|
|
112
|
+
top_k_chunks=args.top_k_chunks
|
|
113
|
+
)
|
|
114
|
+
if not success:
|
|
115
|
+
print("A geração do relatório Markdown falhou.")
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
else:
|
|
118
|
+
print("Geração do relatório Markdown concluída com sucesso.")
|
generate_report_html.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# generate_report_html.py
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import argparse # Make sure argparse is imported
|
|
7
|
+
import sys # Make sure sys is imported
|
|
8
|
+
|
|
9
|
+
# ... (keep your generate_html_report function as is) ...
|
|
10
|
+
def generate_html_report(evaluation_json_path="evaluation_results.json", output_html_path="coverage_report.html", top_k_chunks=5):
|
|
11
|
+
# ... (your existing code for this function)
|
|
12
|
+
if not os.path.exists(evaluation_json_path):
|
|
13
|
+
print(f"Erro: O arquivo JSON de avaliação '{evaluation_json_path}' não foi encontrado. Execute 'evaluate_coverage.py' primeiro.")
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
print(f"Lendo dados de avaliação de '{evaluation_json_path}'...")
|
|
17
|
+
try:
|
|
18
|
+
with open(evaluation_json_path, 'r', encoding='utf-8') as f:
|
|
19
|
+
evaluation_results = json.load(f)
|
|
20
|
+
except json.JSONDecodeError as e:
|
|
21
|
+
print(f"Erro ao decodificar JSON de '{evaluation_json_path}': {e}")
|
|
22
|
+
return False
|
|
23
|
+
except Exception as e:
|
|
24
|
+
print(f"Erro inesperado ao carregar '{evaluation_json_path}': {e}")
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
if not evaluation_results:
|
|
28
|
+
print("Atenção: Nenhum resultado de avaliação válido encontrado no JSON.")
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
# Calcular resumo
|
|
32
|
+
total_questions = len(evaluation_results)
|
|
33
|
+
found_count = sum(1 for item in evaluation_results if "Encontrada" in item['status'])
|
|
34
|
+
coverage_percentage = (found_count / total_questions * 100) if total_questions > 0 else 0
|
|
35
|
+
|
|
36
|
+
report_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
37
|
+
|
|
38
|
+
html_content = f"""
|
|
39
|
+
<!DOCTYPE html>
|
|
40
|
+
<html lang="pt-BR">
|
|
41
|
+
<head>
|
|
42
|
+
<meta charset="UTF-8">
|
|
43
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
44
|
+
<title>Relatório de Cobertura da Documentação - {report_date}</title>
|
|
45
|
+
<style>
|
|
46
|
+
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f4f4f4; }}
|
|
47
|
+
.container {{ max-width: 1200px; margin: 20px auto; background: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
|
|
48
|
+
h1, h2, h3 {{ color: #0056b3; }}
|
|
49
|
+
h1 {{ text-align: center; margin-bottom: 30px; }}
|
|
50
|
+
.summary {{ background-color: #e9f5ff; border-left: 5px solid #0056b3; padding: 15px; margin-bottom: 30px; border-radius: 5px; }}
|
|
51
|
+
.summary p {{ margin: 5px 0; font-size: 1.1em; }}
|
|
52
|
+
.question-item {{ background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 5px; margin-bottom: 20px; padding: 15px; }}
|
|
53
|
+
.question-item.found {{ border-left: 5px solid #28a745; }}
|
|
54
|
+
.question-item.not-found {{ border-left: 5px solid #dc3545; }}
|
|
55
|
+
.question-item.partial-found {{ border-left: 5px solid #ffc107; }} /* Orange for partial/other */
|
|
56
|
+
.question-item h3 {{ margin-top: 0; color: #0056b3; }}
|
|
57
|
+
.question-item p {{ margin-bottom: 8px; }}
|
|
58
|
+
.question-item strong {{ color: #555; }}
|
|
59
|
+
.details ul {{ list-style-type: none; padding: 0; }}
|
|
60
|
+
.details li {{ margin-bottom: 5px; background-color: #eee; padding: 8px; border-radius: 3px; }}
|
|
61
|
+
.details li.covered {{ background-color: #d4edda; color: #155724; }}
|
|
62
|
+
.details li.not-covered {{ background-color: #f8d7da; color: #721c24; }}
|
|
63
|
+
.chunk-list {{ margin-top: 10px; border-top: 1px dashed #ccc; padding-top: 10px; }}
|
|
64
|
+
.chunk-list h4 {{ margin-bottom: 5px; color: #0056b3; }}
|
|
65
|
+
.chunk-item {{ background-color: #e2f0f7; border: 1px solid #cce5ff; padding: 10px; margin-bottom: 5px; border-radius: 3px; font-size: 0.9em; }}
|
|
66
|
+
.chunk-item strong {{ color: #333; }}
|
|
67
|
+
.collapsible {{ cursor: pointer; padding: 10px; margin-top: 10px; background-color: #007bff; color: white; border: none; text-align: left; border-radius: 4px; width: 100%; font-size: 1em; }}
|
|
68
|
+
.collapsible:hover {{ background-color: #0056b3; }}
|
|
69
|
+
.collapsible.active:after {{ content: "\\2212"; float: right; }} /* Minus sign */
|
|
70
|
+
.collapsible:not(.active):after {{ content: "\\002B"; float: right; }} /* Plus sign */
|
|
71
|
+
.content {{ padding: 0 18px; max-height: 0; overflow: hidden; transition: max-height 0.3s ease-out; background-color: #f9f9f9; border: 1px solid #ddd; border-top: none; border-radius: 0 0 5px 5px; }}
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body>
|
|
75
|
+
<div class="container">
|
|
76
|
+
<h1>Relatório de Cobertura da Documentação</h1>
|
|
77
|
+
<p style="text-align: center; font-size: 0.9em; color: #666;">Gerado em: {report_date}</p>
|
|
78
|
+
|
|
79
|
+
<div class="summary">
|
|
80
|
+
<h2>Resumo Geral</h2>
|
|
81
|
+
<p><strong>Total de Perguntas Avaliadas:</strong> {total_questions}</p>
|
|
82
|
+
<p><strong>Perguntas com Cobertura Suficiente:</strong> {found_count}</p>
|
|
83
|
+
<p><strong>Porcentagem de Cobertura Geral:</strong> {coverage_percentage:.2f}%</p>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<h2>Resultados Detalhados por Pergunta</h2>
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
for i, item in enumerate(evaluation_results): # Iterate with index for unique IDs
|
|
90
|
+
question_class = ""
|
|
91
|
+
if "Encontrada" in item['status']:
|
|
92
|
+
question_class = "found"
|
|
93
|
+
elif "Não Encontrada" in item['status']:
|
|
94
|
+
question_class = "not-found"
|
|
95
|
+
else:
|
|
96
|
+
question_class = "partial-found"
|
|
97
|
+
|
|
98
|
+
html_content += f"""
|
|
99
|
+
<div class="question-item {question_class}">
|
|
100
|
+
<h3>{i+1}. Pergunta: {item['pergunta']}</h3>
|
|
101
|
+
<p><strong>Status:</strong> {item['status']}</p>
|
|
102
|
+
<p><strong>Resposta Ideal:</strong> {item['resposta_ideal']}</p>
|
|
103
|
+
|
|
104
|
+
<button type="button" class="collapsible">Detalhes da Cobertura e Chunks Relevantes</button>
|
|
105
|
+
<div class="content">
|
|
106
|
+
<div class="details">
|
|
107
|
+
<h4>Cobertura por Frase da Resposta Ideal:</h4>
|
|
108
|
+
<ul>
|
|
109
|
+
"""
|
|
110
|
+
if item.get('cobertura_detalhes'):
|
|
111
|
+
for detail in item['cobertura_detalhes']:
|
|
112
|
+
detail_class = "covered" if "Coberta" in detail['status'] else "not-covered"
|
|
113
|
+
html_content += f"""
|
|
114
|
+
<li class="{detail_class}">
|
|
115
|
+
<strong>Frase:</strong> {detail['frase_ideal']}<br>
|
|
116
|
+
<strong>Status da Frase:</strong> {detail['status']}<br>
|
|
117
|
+
<strong>Similaridade Máx. para Frase:</strong> {detail['similaridade_max']}<br>
|
|
118
|
+
<strong>Chunk Correspondente:</strong> {detail['chunk_correspondente']}
|
|
119
|
+
</li>
|
|
120
|
+
"""
|
|
121
|
+
else:
|
|
122
|
+
html_content += "<li>Nenhum detalhe de cobertura de frase disponível.</li>"
|
|
123
|
+
|
|
124
|
+
html_content += f"""
|
|
125
|
+
</ul>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="chunk-list">
|
|
128
|
+
<h4>Top {top_k_chunks} Chunks Relevantes para a Pergunta:</h4>
|
|
129
|
+
""" # Removed <ul> here as chunk-item can be a direct child
|
|
130
|
+
if item.get('top_k_chunks_relevantes'):
|
|
131
|
+
for chunk in item['top_k_chunks_relevantes']:
|
|
132
|
+
html_content += f"""
|
|
133
|
+
<div class="chunk-item">
|
|
134
|
+
<p><strong>Documento:</strong> {chunk['document_title']}</p>
|
|
135
|
+
<p><strong>Seção:</strong> {chunk['chunk_title']}</p>
|
|
136
|
+
<p><strong>Caminho do Arquivo:</strong> <code>{chunk['filepath']}</code></p>
|
|
137
|
+
<p><strong>Similaridade com a Pergunta:</strong> {chunk['similarity_to_query']}</p>
|
|
138
|
+
<p><strong>Conteúdo (preview):</strong> <pre><code>{chunk['content_preview']}</code></pre></p>
|
|
139
|
+
</div>
|
|
140
|
+
"""
|
|
141
|
+
else:
|
|
142
|
+
html_content += "<p>Nenhum chunk relevante encontrado.</p>" # Use <p> for consistency
|
|
143
|
+
|
|
144
|
+
html_content += f"""
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
html_content += """
|
|
151
|
+
</div>
|
|
152
|
+
<script>
|
|
153
|
+
var coll = document.getElementsByClassName("collapsible");
|
|
154
|
+
var i;
|
|
155
|
+
|
|
156
|
+
for (i = 0; i < coll.length; i++) {
|
|
157
|
+
coll[i].addEventListener("click", function() {
|
|
158
|
+
this.classList.toggle("active");
|
|
159
|
+
var content = this.nextElementSibling;
|
|
160
|
+
if (content.style.maxHeight){
|
|
161
|
+
content.style.maxHeight = null;
|
|
162
|
+
} else {
|
|
163
|
+
content.style.maxHeight = content.scrollHeight + "px";
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
</script>
|
|
168
|
+
</body>
|
|
169
|
+
</html>
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
with open(output_html_path, 'w', encoding='utf-8') as f:
|
|
174
|
+
f.write(html_content)
|
|
175
|
+
print(f"Relatório HTML gerado com sucesso em '{output_html_path}'.")
|
|
176
|
+
return True
|
|
177
|
+
except Exception as e:
|
|
178
|
+
print(f"Erro ao salvar o relatório HTML: {e}")
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
# NEW cli_main function
|
|
182
|
+
def cli_main():
|
|
183
|
+
parser = argparse.ArgumentParser(description="Gera relatório HTML da avaliação de cobertura.")
|
|
184
|
+
parser.add_argument("input_file", # Changed from evaluation_json_path to match docs_tc.py call
|
|
185
|
+
help="Arquivo JSON de entrada com os resultados da avaliação (padrão: evaluation_results.json).")
|
|
186
|
+
parser.add_argument("output_file", # Changed from output_html_path to match docs_tc.py call
|
|
187
|
+
help="Arquivo HTML de saída para o relatório (padrão: coverage_report.html).")
|
|
188
|
+
parser.add_argument("top_k_chunks", type=int, # Changed from --top_k_chunks for positional
|
|
189
|
+
help="Valor de top_k_chunks usado na avaliação (para consistência do relatório).")
|
|
190
|
+
args = parser.parse_args()
|
|
191
|
+
|
|
192
|
+
success = generate_html_report(
|
|
193
|
+
evaluation_json_path=args.input_file, # Use new arg name
|
|
194
|
+
output_html_path=args.output_file, # Use new arg name
|
|
195
|
+
top_k_chunks=args.top_k_chunks
|
|
196
|
+
)
|
|
197
|
+
if not success:
|
|
198
|
+
print("A geração do relatório HTML falhou.")
|
|
199
|
+
sys.exit(1)
|
|
200
|
+
else:
|
|
201
|
+
print("Geração do relatório HTML concluída com sucesso.")
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
cli_main() # Call the new cli_main
|