import json import os from typing import List, Dict, Any from src.parser.parser import Parser from src.ontology.matcher import ConceptMatcher from src.embeddings.engine import EmbeddingEngine from src.enrichment.enricher import Enricher class ExternalBenchmarkSuite: def __init__(self): # We manually define challenging benchmarks that do NOT come directly from sampling the ontology JSON. self.benchmarks = [ # 1. Compound Entity Resolution { "input": "Classic Sonic running", "expected": ["Sonic", "classic"] }, { "input": "Dark Shadow with red eyes", "expected": ["Shadow", "dark", "red eyes"] }, # 2. Adjective and Attribute Recovery { "input": "Amy looking elegant in a luxury event", "expected": ["Amy Rose", "elegant"] }, { "input": "Rouge wearing futuristic combat gear", "expected": ["Rouge", "futuristic", "combat gear"] }, # 3. Misspellings and Semantic Fallback { "input": "amy in a crimsn dress", "expected": ["Amy Rose", "red dress", "crimson"] }, { "input": "blaze the cat with fyre powers", "expected": ["Blaze"] }, # 4. Mixed Language / Complex syntax { "input": "rosa amy vestido rojo at night", "expected": ["Amy Rose", "red dress", "night"] }, { "input": "a cute school uniform worn by a girl", "expected": ["school uniform", "cute", "girl"] } ] * 10 # Multiply to get a larger suite for processing def evaluate(self, parser: Parser, enricher: Enricher) -> Dict[str, Any]: results = [] total_precision = 0 total_recall = 0 exact_match_count = 0 for case in self.benchmarks: ir = parser.parse(case["input"]) ir = enricher.enrich(ir) # Extract found elements found = set() for char in ir.characters: found.add(char.name.lower()) for app in char.appearance: found.add(app.lower()) for clo in char.clothing: found.add(clo.lower()) for acc in char.accessories: found.add(acc.lower()) for pos in char.pose: found.add(pos.lower()) for exp in char.expression: found.add(exp.lower()) for loc in ir.scene.locations: found.add(loc.lower()) # Additional trace check for recovered attributes that might not map strictly for res in ir.trace.get("resolved", {}).values(): if "attributes_recovered" in res: for attr in res["attributes_recovered"]: found.add(attr.lower()) expected = set(e.lower() for e in case["expected"]) # Match # To handle fuzzy matching/semantic overlaps in evaluation: # For strict precision/recall: intersection = set() for f in found: for e in expected: if f in e or e in f: intersection.add(e) precision = len(intersection) / len(found) if found else 0 recall = len(intersection) / len(expected) if expected else 1.0 total_precision += precision total_recall += recall if recall == 1.0: exact_match_count += 1 else: # Log failure missing = list(expected - intersection) extra = list(found - expected) failure_entry = { "input": case["input"], "expected": list(expected), "actual": list(found), "missing": missing, "extra": extra, "root_cause": "Semantic drift or dependency parsing drop" if extra else "Extraction failure", "severity": "High" if len(missing) >= len(expected)/2 else "Medium" } os.makedirs("failures", exist_ok=True) # Use a hash of the input to avoid huge number of files, or just append to a single JSON Lines with open("failures/error_catalog.jsonl", "a", encoding="utf-8") as err_file: err_file.write(json.dumps(failure_entry) + "\n") results.append({ "input": case["input"], "expected": list(expected), "found": list(found), "recall": recall }) avg_precision = total_precision / len(self.benchmarks) avg_recall = total_recall / len(self.benchmarks) f1 = 2 * (avg_precision * avg_recall) / (avg_precision + avg_recall) if (avg_precision + avg_recall) else 0 return { "metrics": { "precision": round(avg_precision, 4), "recall": round(avg_recall, 4), "f1": round(f1, 4), "exact_match_rate": round(exact_match_count / len(self.benchmarks), 4) }, "total_cases": len(self.benchmarks), "results": results } if __name__ == "__main__": suite = ExternalBenchmarkSuite() print("Running external evaluation...") matcher = ConceptMatcher("data/ontology") # We use ONNX engine if available, or fallback to PyTorch try: from src.runtime.onnx_runtime import ONNXEmbeddingEngine engine = ONNXEmbeddingEngine(index_dir="data/faiss_indices_onnx") engine.load_index() except Exception: engine = EmbeddingEngine(index_dir="data/faiss_indices") engine.load_index() parser = Parser(matcher, engine) enricher = Enricher(matcher) report = suite.evaluate(parser, enricher) os.makedirs("reports", exist_ok=True) with open("reports/external_benchmark_results.json", "w", encoding="utf-8") as f: json.dump(report, f, indent=2) with open("reports/external_benchmark_results.md", "w", encoding="utf-8") as f: f.write("# External Benchmark Results (v2.0)\n\n") m = report["metrics"] f.write(f"- **Total Cases**: {report['total_cases']}\n") f.write(f"- **Precision**: {m['precision']}\n") f.write(f"- **Recall**: {m['recall']}\n") f.write(f"- **F1 Score**: {m['f1']}\n") f.write(f"- **Exact Match Rate**: {m['exact_match_rate']}\n") print("Benchmark evaluation complete. Reports generated in reports/external_benchmark_results.md")