govbridge-api / tests /test_knowledge_extractor.py
harshrawat18's picture
Upload folder using huggingface_hub
aa68ee9 verified
Raw
History Blame Contribute Delete
9.8 kB
"""
GovBridge India β€” Unit Tests for Knowledge Extraction Pipeline
Sprint 30: PROJECT INDRA Phase 1.5
Tests cover:
1. Prompt construction
2. Pydantic validation (valid predicates, invalid predicates)
3. Text chunking with overlap
4. Triplet deduplication
5. Entity normalization
Run: pytest tests/test_knowledge_extractor.py -v
"""
import sys
import os
import pytest
# Ensure gov_backend and ingestion are on sys.path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "ingestion"))
# ═══════════════════════════════════════════════════════════════
# TEST 1: Predicate Ontology
# ═══════════════════════════════════════════════════════════════
class TestGovPredicate:
def test_all_10_predicates_exist(self):
from prompts import VALID_PREDICATES
expected = {
"AMENDS", "SUPERSEDES", "REPEALS", "ENACTS",
"DELEGATES_TO", "FUNDS", "MANDATES", "APPLIES_TO",
"DEFINES", "PENALIZES"
}
assert VALID_PREDICATES == expected
def test_predicate_enum_values(self):
from prompts import GovPredicate
assert GovPredicate.AMENDS.value == "AMENDS"
assert GovPredicate.DELEGATES_TO.value == "DELEGATES_TO"
# ═══════════════════════════════════════════════════════════════
# TEST 2: Pydantic Validation
# ═══════════════════════════════════════════════════════════════
class TestKnowledgeTriplet:
def test_valid_triplet(self):
from knowledge_extractor import KnowledgeTriplet
t = KnowledgeTriplet(
subject="Finance Act 2024",
predicate="AMENDS",
object="Income Tax Act 1961"
)
assert t.predicate == "AMENDS"
assert t.subject == "Finance Act 2024"
def test_predicate_normalized_to_uppercase(self):
from knowledge_extractor import KnowledgeTriplet
t = KnowledgeTriplet(
subject="Act A",
predicate="amends", # lowercase
object="Act B"
)
assert t.predicate == "AMENDS"
def test_invalid_predicate_rejected(self):
from knowledge_extractor import KnowledgeTriplet
from pydantic import ValidationError
with pytest.raises(ValidationError):
KnowledgeTriplet(
subject="Act A",
predicate="MODIFIES", # Not in ontology
object="Act B"
)
def test_empty_subject_rejected(self):
from knowledge_extractor import KnowledgeTriplet
from pydantic import ValidationError
with pytest.raises(ValidationError):
KnowledgeTriplet(
subject="",
predicate="AMENDS",
object="Act B"
)
def test_whitespace_trimmed(self):
from knowledge_extractor import KnowledgeTriplet
t = KnowledgeTriplet(
subject=" Finance Act 2024 ",
predicate=" REPEALS ",
object=" Old Act "
)
assert t.subject == "Finance Act 2024"
assert t.object == "Old Act"
# ═══════════════════════════════════════════════════════════════
# TEST 3: Text Chunking
# ═══════════════════════════════════════════════════════════════
class TestTextChunking:
def test_short_text_single_chunk(self):
from knowledge_extractor import chunk_text_for_extraction
chunks = chunk_text_for_extraction("Short text", chunk_size=1000)
assert len(chunks) == 1
assert chunks[0] == "Short text"
def test_long_text_produces_multiple_chunks(self):
from knowledge_extractor import chunk_text_for_extraction
# Create text that's clearly > 1000 chars
text = "A" * 500 + "\n\n" + "B" * 500 + "\n\n" + "C" * 500
chunks = chunk_text_for_extraction(text, chunk_size=600, overlap=100)
assert len(chunks) >= 2
def test_overlap_is_applied(self):
from knowledge_extractor import chunk_text_for_extraction
# Create text with clear paragraph boundaries
text = ("Para one. " * 50) + "\n\n" + ("Para two. " * 50) + "\n\n" + ("Para three. " * 50)
chunks = chunk_text_for_extraction(text, chunk_size=300, overlap=50)
# Verify overlap: last N chars of chunk[0] should appear in chunk[1]
if len(chunks) > 1:
tail = chunks[0][-50:]
assert tail in chunks[1] or len(chunks[1]) > 50
# ═══════════════════════════════════════════════════════════════
# TEST 4: Deduplication
# ═══════════════════════════════════════════════════════════════
class TestDeduplication:
def test_exact_duplicates_removed(self):
from knowledge_extractor import KnowledgeTriplet, deduplicate_triplets
triplets = [
KnowledgeTriplet(subject="Act A", predicate="AMENDS", object="Act B"),
KnowledgeTriplet(subject="Act A", predicate="AMENDS", object="Act B"),
]
result = deduplicate_triplets(triplets)
assert len(result) == 1
def test_case_insensitive_dedup(self):
from knowledge_extractor import KnowledgeTriplet, deduplicate_triplets
triplets = [
KnowledgeTriplet(subject="Finance Act", predicate="AMENDS", object="IT Act"),
KnowledgeTriplet(subject="finance act", predicate="AMENDS", object="it act"),
]
result = deduplicate_triplets(triplets)
assert len(result) == 1
def test_different_predicates_kept(self):
from knowledge_extractor import KnowledgeTriplet, deduplicate_triplets
triplets = [
KnowledgeTriplet(subject="Act A", predicate="AMENDS", object="Act B"),
KnowledgeTriplet(subject="Act A", predicate="SUPERSEDES", object="Act B"),
]
result = deduplicate_triplets(triplets)
assert len(result) == 2
def test_article_noise_dedup(self):
from knowledge_extractor import KnowledgeTriplet, deduplicate_triplets
triplets = [
KnowledgeTriplet(subject="The Finance Act", predicate="AMENDS", object="The IT Act"),
KnowledgeTriplet(subject="Finance Act", predicate="AMENDS", object="IT Act"),
]
result = deduplicate_triplets(triplets)
assert len(result) == 1
# ═══════════════════════════════════════════════════════════════
# TEST 5: Entity Normalization
# ═══════════════════════════════════════════════════════════════
class TestEntityNormalization:
def test_normalize_strips_whitespace(self):
from knowledge_extractor import _normalize_entity
assert _normalize_entity(" Finance Act ") == "finance act"
def test_normalize_removes_articles(self):
from knowledge_extractor import _normalize_entity
assert _normalize_entity("The Finance Act") == "finance act"
def test_normalize_collapses_spaces(self):
from knowledge_extractor import _normalize_entity
assert _normalize_entity("Finance Act 2024") == "finance act 2024"
def test_normalize_removes_legal_filler(self):
from knowledge_extractor import _normalize_entity
assert _normalize_entity("said Ministry of Finance") == "ministry of finance"
# ═══════════════════════════════════════════════════════════════
# TEST 6: Prompt Construction
# ═══════════════════════════════════════════════════════════════
class TestPromptConstruction:
def test_build_chunk_user_prompt_includes_context(self):
from prompts import build_chunk_user_prompt
ctx = {
"document_title": "Finance Act 2024",
"primary_body": "Ministry of Finance",
"key_definitions": ["tax", "income"],
"document_date": "2024-01-15"
}
prompt = build_chunk_user_prompt(ctx, "Some chunk text here")
assert "Finance Act 2024" in prompt
assert "Ministry of Finance" in prompt
assert "Some chunk text here" in prompt
assert "tax, income" in prompt
def test_build_chunk_user_prompt_handles_empty_context(self):
from prompts import build_chunk_user_prompt
ctx = {}
prompt = build_chunk_user_prompt(ctx, "Chunk text")
assert "Unknown" in prompt
assert "Chunk text" in prompt