| |
| """ |
| Offline test for ProductCatalog (no OpenAI / Qdrant required). |
| |
| Verifies the fix for the "answers from a different herbicide / keeps citing the |
| dominant product" bug: product resolution must be driven by what is actually in |
| the index, and must ABSTAIN (return None) for products we don't have. |
| |
| Run: python test_product_resolution.py |
| """ |
|
|
| from src.cdms.product_catalog import ( |
| ProductCatalog, |
| normalize_filename, |
| cross_product_abstention, |
| diversify_by_product, |
| ) |
|
|
|
|
| def main() -> bool: |
| cat = ProductCatalog() |
| catalog = cat.catalog() |
| available = cat.available_products() |
|
|
| print("=" * 70) |
| print("PRODUCT CATALOG (derived from data/cdms_metadata.db)") |
| print("=" * 70) |
| for product, n in sorted(catalog.items(), key=lambda kv: -kv[1]): |
| flag = "" if n > 0 else " <-- 0 chunks (unfindable / needs reprocessing)" |
| print(f" {n:5d} chunks {product}{flag}") |
|
|
| print(f"\nAvailable (has chunks): {sorted(available)}") |
|
|
| |
| passed, failed = 0, [] |
|
|
| def check(name, cond): |
| nonlocal passed |
| if cond: |
| passed += 1 |
| else: |
| failed.append(name) |
|
|
| |
| check("normalize roundup_hash", normalize_filename("roundup_bdc94bbee383.pdf") == "roundup") |
| check("normalize Brandt_Nema_Q", normalize_filename("Brandt_Nema_Q.pdf") == "brandt nema q") |
| check("normalize 24-d", normalize_filename("24-d_fa1e6bdacae6.pdf") == "24-d") |
|
|
| |
| check("resolve roundup", cat.resolve("what is the application rate for Roundup?") == "roundup") |
| check("resolve sevin", cat.resolve("Is Sevin safe for tomatoes?") == "sevin") |
| check("resolve 24-d", cat.resolve("mixing instructions for 24-d") == "24-d") |
|
|
| |
| |
| check("abstain on Trust", cat.resolve("Tell me about the Trust herbicide") is None) |
| check("abstain on Enlist", cat.resolve("What is the rate for Enlist One?") is None) |
| check("abstain on gibberish", cat.resolve("qzxwv nonsense product") is None) |
|
|
| |
| check("acquit not available", "acquit" not in available) |
|
|
| |
| |
| check( |
| "abstain when only wrong-product chunks returned", |
| cross_product_abstention("Is Sevin safe on tomatoes?", |
| ["roundup_bdc94bbee383.pdf", "roundup_75908642d433.pdf"], |
| catalog=cat) == "sevin", |
| ) |
| |
| check( |
| "proceed when correct-product chunk present", |
| cross_product_abstention("Is Sevin safe on tomatoes?", |
| ["sevin_0b73d6a0a2d4.pdf", "roundup_bdc94bbee383.pdf"], |
| catalog=cat) is None, |
| ) |
| |
| check( |
| "proceed on generic question", |
| cross_product_abstention("What is a pre-emergent herbicide?", |
| ["roundup_bdc94bbee383.pdf"], |
| catalog=cat) is None, |
| ) |
| |
| |
| check( |
| "no false abstain for un-indexed product", |
| cross_product_abstention("Tell me about Trust herbicide", |
| ["roundup_bdc94bbee383.pdf"], |
| catalog=cat) is None, |
| ) |
|
|
| |
| |
| |
| |
| hashes = ["bdc94bbee383", "75908642d433", "30b0dda04968", "424997b6293c", "744b26dd6d64"] |
| skewed = ( |
| [{"source_file": f"roundup_{hashes[i]}.pdf", "score": 0.9 - i * 0.01} for i in range(5)] |
| + [{"source_file": "sevin_0b73d6a0a2d4.pdf", "score": 0.62}] |
| + [{"source_file": "boron_b97d7d0cfb98.pdf", "score": 0.55}] |
| ) |
| div = diversify_by_product(skewed, limit=5, max_per_product=2) |
| div_products = [normalize_filename(r["source_file"]) for r in div] |
| |
| check("diversify caps per product", div_products.count("roundup") <= 2) |
| |
| check("diversify surfaces sevin", "sevin" in div_products) |
| check("diversify surfaces boron", "boron" in div_products) |
| |
| check("diversify keeps top hit", any(r["score"] == 0.9 for r in div)) |
| |
| |
| only = diversify_by_product( |
| [{"source_file": f"roundup_{hashes[i]}.pdf", "score": 0.9} for i in range(5)], |
| limit=5, max_per_product=2, |
| ) |
| check("single-product respects cap", len(only) == 2) |
|
|
| print("\n" + "=" * 70) |
| print(f"RESULT: {passed} passed, {len(failed)} failed") |
| if failed: |
| print("FAILED:", ", ".join(failed)) |
| print("=" * 70) |
| return not failed |
|
|
|
|
| if __name__ == "__main__": |
| import sys |
| sys.exit(0 if main() else 1) |
|
|