tuts-rag-service / scripts /dataset_auditor.py
Gil Almeida
fix: resolve TypeError in normalized_uc, add Intent Router for inventory queries, and fix payload integrity
5bfaada
Raw
History Blame Contribute Delete
7.84 kB
#!/usr/bin/env python3
import json
import csv
import re
import argparse
from pathlib import Path
import unicodedata
KNOWN_RANGES = {
"tacs": {
"default": (4, 133),
"variáveis e scope": (7, 9),
"assincronia: promises": (119, 121),
},
"semio": {
"default": (1, 60),
"resumo sc": (17, 28),
"semiótica resumos": (1, 30),
}
}
def clean_text(text):
text = (text or "").lower()
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode("ascii")
return re.sub(r'\s+', ' ', re.sub(r'[^a-z0-9_]+', ' ', text)).strip()
def get_words(text):
return set(clean_text(text).split())
def calculate_jaccard(text1, text2):
w1 = get_words(text1)
w2 = get_words(text2)
if not w1 or not w2:
return 0.0
return len(w1.intersection(w2)) / len(w1.union(w2))
def parse_expected_sources(src_list):
# Returns list of (file, page)
parsed = []
for src in src_list:
# e.g., "Variáveis e Scope, pp. 6-8" or "Resumo SC, p. 18"
parts = src.split(",")
file_name = parts[0].strip() if parts else "Desconhecido"
page = parts[1].strip() if len(parts) > 1 else "N/A"
parsed.append((file_name, page))
return parsed
def audit_dataset(dataset_path: Path, old_dataset_path: Path | None, subject: str, out_csv: Path) -> None:
print(f"\n================ AUDITING DATASET: {dataset_path.name} ================")
with open(dataset_path, "r", encoding="utf-8") as f:
data = json.load(f)
old_data = []
if old_dataset_path and old_dataset_path.exists():
with open(old_dataset_path, "r", encoding="utf-8") as f:
old_data = json.load(f)
# 1. Structural Integrity Check
structural_ok = True
reasons = []
if len(data) != 50:
structural_ok = False
reasons.append(f"Número de itens incorreto: {len(data)} (esperado 50)")
ids = [item.get("id") for item in data]
if sorted(ids) != list(range(1, 51)):
structural_ok = False
reasons.append(f"IDs não são consecutivos de 1 a 50: {sorted(ids)[:5]}...")
categories = [item.get("categoria") for item in data]
from collections import Counter
counts = Counter(categories)
expected_counts = {
"definicao": 10,
"conceptual": 10,
"procedimento": 10,
"debug": 8,
"aplicacao": 5,
"fora_scope": 5,
"ambigua": 2
}
for cat, expected in expected_counts.items():
if counts[cat] != expected:
structural_ok = False
reasons.append(f"Categoria '{cat}' com contagem {counts[cat]} (esperado {expected})")
# 2. Reference Validity Check
reference_ok = True
# 3. Internal Consistency Check (must_include in expected_context)
consistency_ok = True
# Generate CSV Rows
csv_rows = []
for item in data:
item_id = item.get("id")
cat = item.get("categoria")
pergunta = item.get("pergunta")
gt = item.get("contexto_esperado")
must_inc = item.get("must_include", [])
must_not = item.get("must_not_include", [])
sources = item.get("expected_sources", [])
notes = item.get("evaluation_notes", "")
parsed_src = parse_expected_sources(sources)
file_names = " | ".join(p[0] for p in parsed_src) if parsed_src else "N/A"
pages = " | ".join(p[1] for p in parsed_src) if parsed_src else "N/A"
# Validar páginas
subject_ranges = KNOWN_RANGES.get(subject, {})
for src in sources:
matched_key = "default"
for key in subject_ranges:
if key != "default" and key in src.lower():
matched_key = key
break
min_p, max_p = subject_ranges[matched_key]
page_matches = re.findall(r'\b(?:p\.?|pp\.?|pag\.?|p[aá]gina|powerpoint|slide)\s*[:.]?\s*(\d+)', src, re.IGNORECASE)
for p_str in page_matches:
p_num = int(p_str)
if p_num < min_p or p_num > max_p:
reference_ok = False
reasons.append(f"ID {item_id}: página {p_num} em '{src}' fora do intervalo ({min_p}-{max_p})")
# Validar consistência interna
for must in must_inc:
must_clean = clean_text(must)
must_words = [w for w in must_clean.split() if len(w) >= 4]
if must_words:
matches = sum(1 for w in must_words if w in clean_text(gt))
if (matches / len(must_words)) < 0.4:
consistency_ok = False
reasons.append(f"ID {item_id}: must_include '{must}' tem fraca consistência com o ground truth")
csv_rows.append({
"ID": item_id,
"categoria": cat,
"pergunta": pergunta,
"resposta_de_referencia": gt,
"ficheiro": file_names,
"pagina": pages,
"excerto_da_fonte": "[Preencher na auditoria manual]",
"must_include": "; ".join(must_inc),
"notas": notes,
"manual_audit_status": "pending"
})
# Write CSV
fields = ["ID", "categoria", "pergunta", "resposta_de_referencia", "ficheiro", "pagina", "excerto_da_fonte", "must_include", "notas", "manual_audit_status"]
with open(out_csv, "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
writer.writerows(csv_rows)
print(f"Audit CSV written to: {out_csv.name}")
# 4. Lexical Similarity Report against Previous Dataset
similarities = []
if old_data:
for item in data:
q_new = item.get("pergunta")
id_new = item.get("id")
for old_item in old_data:
q_old = old_item.get("pergunta")
id_old = old_item.get("id")
jac = calculate_jaccard(q_new, q_old)
similarities.append((id_new, id_old, jac, q_new, q_old))
# Sort and get top 5 highest similarities
similarities.sort(key=lambda x: x[2], reverse=True)
print("\nTop 5 Pares com Maior Semelhança com Dataset Anterior:")
for idx, (id_n, id_o, jac, q_n, q_o) in enumerate(similarities[:5]):
print(f" {idx+1}. Jaccard Similarity = {jac:.3f}")
print(f" Novo ID {id_n}: {q_n}")
print(f" Antigo ID {id_o}: {q_o}")
# Audit summary
print("\n---------------- AUDIT SUMMARY ----------------")
print(f"1. Integridade Estrutural: {'✅ APROVADO' if structural_ok else '❌ FALHOU'}")
print(f"2. Validade das Referências: {'✅ APROVADO' if reference_ok else '❌ FALHOU'}")
print(f"3. Consistência Interna: {'✅ APROVADO' if consistency_ok else '⚠️ AVISOS EMITIDOS'}")
print(f"4. Factualidade Científica: 🔎 PENDENTE DE AUDITORIA HUMANA (manual_audit_status=pending)")
print("-----------------------------------------------")
if reasons:
print("Detalhes encontrados:")
for r in reasons[:10]:
print(f" - {r}")
if len(reasons) > 10:
print(f" ... e mais {len(reasons)-10} ocorrências.")
if __name__ == "__main__":
audit_dataset(
Path("dataset_tacs_mis4tel_v5_heldout_50.json"),
Path("dataset_tacs_mis4tel_v4_50.json"),
"tacs",
Path("dataset_tacs_mis4tel_v5_heldout_audit.csv")
)
audit_dataset(
Path("dataset_semio_mis4tel_v2_heldout_50.json"),
Path("dataset_semio_mis4tel_v1.json"),
"semio",
Path("dataset_semio_mis4tel_v2_heldout_audit.csv")
)