Files
AI/ontology_platform/tests/unit/test_phase7_hybrid_extraction.py
2026-05-22 00:22:03 +09:00

145 lines
5.3 KiB
Python

from crawler_platform.app.config.loader import ProjectConfig
from crawler_platform.app.core.extractor.base import ExtractedClaim, ExtractionBundle, ExtractionPageContext
from crawler_platform.app.core.extractor.hybrid import HybridExtractor, mark_bundle, merge_bundles
from crawler_platform.app.core.extractor.validation import validate_extraction_bundle
def project_config() -> ProjectConfig:
return ProjectConfig(
project_name="phase7-test",
domain="generic",
target_entities=["Thing"],
fields=[],
sources=[],
ontology={
"entity_types": ["Thing"],
"predicates": ["hasPart", "color", "size"],
"relation_types": {
"hasPart": {
"allowed_subject_types": ["Thing"],
"allowed_object_types": ["Thing"],
"confidence_rules": {"min_confidence": 0.1},
},
"color": {
"allowed_subject_types": ["Thing"],
"literal_value": True,
"confidence_rules": {"min_confidence": 0.1},
},
"size": {
"allowed_subject_types": ["Thing"],
"literal_value": True,
"confidence_rules": {"min_confidence": 0.1},
},
},
},
)
def test_hybrid_merge_marks_agreement_conflict_and_counts() -> None:
rule = ExtractionBundle(
claims=[
ExtractedClaim("A", "Thing", "hasPart", "B", "Thing", confidence=0.7),
ExtractedClaim("A", "Thing", "color", object_value="red", confidence=0.6),
]
)
llm = ExtractionBundle(
claims=[
ExtractedClaim("A", "Thing", "hasPart", "B", "Thing", confidence=0.8),
ExtractedClaim("A", "Thing", "color", object_value="blue", confidence=0.65),
ExtractedClaim("A", "Thing", "size", object_value="large", confidence=0.5),
]
)
mark_bundle(rule, source="rule", mode="compare")
mark_bundle(llm, source="llm", mode="compare")
merged = merge_bundles(rule, llm, HybridExtractor("generic", mode="compare"))
assert merged.extractor_name == "compare_rule_llm_extractor"
assert merged.raw_output["comparison"] == {
"both_agree": 1,
"rule_only": 0,
"llm_only": 1,
"conflict": 1,
"rejected_by_validation": 0,
}
by_predicate = {(claim.predicate, claim.object_name or claim.object_value): claim for claim in merged.claims}
assert by_predicate[("hasPart", "B")].metadata["agreement"] == "rule_and_llm"
assert by_predicate[("color", "red")].metadata["review_required"] is True
assert by_predicate[("color", "blue")].metadata["conflict_status"] == "rule_llm_conflict"
assert by_predicate[("size", "large")].metadata["agreement"] == "llm_only"
def test_validation_keeps_evidence_missing_llm_only_claim_as_candidate() -> None:
bundle = ExtractionBundle(
claims=[
ExtractedClaim(
"A",
"Thing",
"color",
object_value="blue",
confidence=0.8,
metadata={
"agreement": "llm_only",
"extraction_source": "llm",
"llm_confidence": 0.8,
},
)
],
extractor_name="hybrid_rule_llm_extractor",
provider="lm_studio",
raw_output={"extraction_mode": "hybrid"},
)
result = validate_extraction_bundle(bundle, project_config())
assert len(result.bundle.claims) == 1
claim = result.bundle.claims[0]
assert claim.metadata["validation_status"] == "candidate_claim"
assert claim.metadata["review_required"] is True
assert claim.metadata["review_reason"] == "claim has no evidence"
def test_validation_marks_rule_only_claim_as_rule_candidate() -> None:
bundle = ExtractionBundle(
claims=[
ExtractedClaim(
"Alpha",
"Thing",
"color",
object_value="red",
evidence_text="Alpha color red",
confidence=0.7,
metadata={
"agreement": "rule_only",
"extraction_source": "rule",
"rule_confidence": 0.7,
},
)
],
extractor_name="hybrid_rule_llm_extractor",
provider="lm_studio",
raw_output={"extraction_mode": "hybrid"},
)
result = validate_extraction_bundle(bundle, project_config())
assert result.bundle.claims[0].metadata["validation_status"] == "rule_candidate"
assert result.bundle.claims[0].metadata["claim_kind"] == "rule_candidate"
def test_hybrid_smart_routing_skips_llm_for_category_page() -> None:
context = ExtractionPageContext(
url="https://example.test/category",
final_url=None,
title="Category",
page_type="CategoryPage",
clean_text="Category listing " * 40,
)
bundle = HybridExtractor("generic", mode="hybrid").extract_from_context(context, project_config())
assert bundle.extractor_name == "hybrid_rule_only_routed"
assert bundle.raw_output["llm_skipped"] is True
assert bundle.raw_output["effective_extraction_mode"] == "rule_only"
assert "CategoryPage" in bundle.raw_output["llm_skip_reason"]