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.
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.")
@@ -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