"""BERTScore evaluation against reference resolutions. For 50 test queries (balanced across categories) we get the live agent response from the orchestrator, find the closest reference answer in the specialist conversation dataset (keyword-overlap match), and compute BERTScore. Reports mean precision/recall/F1. Run: ``python evaluation/bertscore_eval.py`` Writes: ``evaluation/bertscore_results.json`` """ from __future__ import annotations import json import re from pathlib import Path from common.config import PROCESSED_DIR, EVALUATION_DIR, TRIAGE_TEST_PATH, settings from common.logging_utils import get_logger from agents.orchestrator import handle_query logger = get_logger("bertscore_eval") _WORD_RE = re.compile(r"[a-z0-9]+") def _tokens(text: str) -> set[str]: return set(_WORD_RE.findall(text.lower())) def _load_references() -> list[tuple[str, str]]: """Return (category, reference_text) from specialist conversations.""" refs = [] for category in settings.categories: path = PROCESSED_DIR / f"specialist_conversations_{category}.jsonl" if not path.exists(): continue for line in path.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue convo = json.loads(line) agent_turns = [t["text"] for t in convo.get("turns", []) if t.get("role") == "agent"] ref = " ".join(agent_turns) or convo.get("resolution", "") if ref: refs.append((category, ref)) return refs def _best_reference(query: str, category: str, refs: list[tuple[str, str]]) -> str: q = _tokens(query) best, best_overlap = "", -1 for cat, ref in refs: if cat != category: continue overlap = len(q & _tokens(ref)) if overlap > best_overlap: best, best_overlap = ref, overlap return best def _load_test_queries(n_per_category: int = 5) -> list[tuple[str, str]]: if not TRIAGE_TEST_PATH.exists(): return [] buckets: dict[str, list[str]] = {c: [] for c in settings.categories} for line in TRIAGE_TEST_PATH.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue rec = json.loads(line) cat = json.loads(rec["output"])["category"] if len(buckets[cat]) < n_per_category: buckets[cat].append(rec["input"]) out = [] for cat, qs in buckets.items(): out.extend((q, cat) for q in qs) return out[:50] def main() -> None: refs = _load_references() queries = _load_test_queries() if not refs or not queries: logger.warning("Missing specialist references or test queries — generate Phase 1 data first.") return candidates, references = [], [] for query, category in queries: response = handle_query(query, run_hallucination_check=False).get("response", "") ref = _best_reference(query, category, refs) if response and ref: candidates.append(response) references.append(ref) from bert_score import score P, R, F1 = score(candidates, references, model_type="distilbert-base-uncased", verbose=False) results = { "n": len(candidates), "precision": round(P.mean().item(), 4), "recall": round(R.mean().item(), 4), "f1": round(F1.mean().item(), 4), } (EVALUATION_DIR / "bertscore_results.json").write_text( json.dumps(results, indent=2), encoding="utf-8" ) print("BERTScore:", results) if __name__ == "__main__": main()