mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
- Robust ID extraction in CentralityCalculator, CommunityDetector, and ConnectivityAnalyzer - Support for direct Entity objects and dictionaries as node identifiers - Improved Entity hashability in utils/types.py - Added integration test to verify fix and prevent regression
236 lines
9.7 KiB
Python
236 lines
9.7 KiB
Python
import unittest
|
|
import sys
|
|
import os
|
|
from unittest.mock import MagicMock, patch
|
|
from dataclasses import dataclass
|
|
|
|
import pytest
|
|
|
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
|
|
from semantica.semantic_extract.named_entity_recognizer import (
|
|
NamedEntityRecognizer, EntityClassifier, EntityConfidenceScorer, CustomEntityDetector
|
|
)
|
|
from semantica.semantic_extract.ner_extractor import NERExtractor, Entity
|
|
from semantica.semantic_extract.relation_extractor import RelationExtractor, Relation
|
|
from semantica.semantic_extract.triplet_extractor import TripletExtractor, Triplet
|
|
from semantica.semantic_extract.event_detector import EventDetector, Event
|
|
from semantica.semantic_extract.semantic_analyzer import SemanticAnalyzer, SemanticRole
|
|
from semantica.semantic_extract.methods import (
|
|
extract_entities_regex, extract_entities_rules,
|
|
extract_relations_regex, extract_relations_dependency,
|
|
extract_triplets_rules
|
|
)
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
class TestSemanticExtractDeepDivePart2(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.text = "Steve Jobs founded Apple Inc. in 1976."
|
|
self.entities = [
|
|
Entity(text="Steve Jobs", label="PERSON", start_char=0, end_char=10),
|
|
Entity(text="Apple Inc.", label="ORG", start_char=19, end_char=29),
|
|
Entity(text="1976", label="DATE", start_char=33, end_char=37)
|
|
]
|
|
|
|
# --- Entity Classifier Tests ---
|
|
|
|
def test_entity_classifier(self):
|
|
"""Test EntityClassifier type classification"""
|
|
classifier = EntityClassifier()
|
|
|
|
# Test type normalization
|
|
e1 = Entity(text="Steve", label="PER", start_char=0, end_char=5)
|
|
type1 = classifier.classify_entity_type(e1)
|
|
self.assertEqual(type1, "PERSON")
|
|
|
|
e2 = Entity(text="Apple", label="ORGANIZATION", start_char=0, end_char=5)
|
|
type2 = classifier.classify_entity_type(e2)
|
|
self.assertEqual(type2, "ORG")
|
|
|
|
e3 = Entity(text="Unknown", label="CUSTOM", start_char=0, end_char=7)
|
|
type3 = classifier.classify_entity_type(e3)
|
|
self.assertEqual(type3, "CUSTOM")
|
|
|
|
def test_entity_classifier_disambiguation(self):
|
|
"""Test EntityClassifier disambiguation"""
|
|
classifier = EntityClassifier()
|
|
|
|
target = Entity(text="Apple", label="ORG", start_char=0, end_char=5)
|
|
candidates = [
|
|
Entity(text="Apple", label="FRUIT", start_char=0, end_char=5, confidence=0.6),
|
|
Entity(text="Apple", label="ORG", start_char=0, end_char=5, confidence=0.9),
|
|
Entity(text="Apple", label="ORG", start_char=0, end_char=5, confidence=0.5)
|
|
]
|
|
|
|
best = classifier.disambiguate_entity(target, candidates)
|
|
self.assertIsNotNone(best)
|
|
self.assertEqual(best.label, "ORG")
|
|
self.assertEqual(best.confidence, 0.9)
|
|
|
|
# --- Entity Confidence Scorer Tests ---
|
|
|
|
def test_entity_confidence_scorer(self):
|
|
"""Test EntityConfidenceScorer"""
|
|
scorer = EntityConfidenceScorer()
|
|
|
|
# Test scoring adjustments
|
|
e1 = Entity(text="s", label="PERSON", start_char=0, end_char=1) # Too short
|
|
scored_e1 = scorer.score_entities([e1])[0]
|
|
self.assertLess(scored_e1.confidence, 1.0)
|
|
|
|
e2 = Entity(text="steve jobs", label="PERSON", start_char=0, end_char=10) # Lowercase person
|
|
scored_e2 = scorer.score_entities([e2])[0]
|
|
self.assertLess(scored_e2.confidence, 1.0)
|
|
|
|
e3 = Entity(text="1999", label="DATE", start_char=0, end_char=4) # Digit date
|
|
# Should be boosted (capped at 1.0)
|
|
scored_e3 = scorer.score_entities([e3])[0]
|
|
self.assertLessEqual(scored_e3.confidence, 1.0)
|
|
|
|
# --- Custom Entity Detector Tests ---
|
|
|
|
def test_custom_entity_detector(self):
|
|
"""Test CustomEntityDetector"""
|
|
config = {
|
|
"patterns": {
|
|
"PROJECT": r"Project\s+[A-Z]\w+"
|
|
}
|
|
}
|
|
detector = CustomEntityDetector(**config)
|
|
text = "We are working on Project Apollo and Project Gemini."
|
|
|
|
entities = detector.detect_custom_entities(text, "PROJECT")
|
|
self.assertEqual(len(entities), 2)
|
|
self.assertEqual(entities[0].text, "Project Apollo")
|
|
self.assertEqual(entities[0].label, "PROJECT")
|
|
self.assertEqual(entities[1].text, "Project Gemini")
|
|
|
|
# --- Method Implementation Tests ---
|
|
|
|
def test_extract_entities_regex(self):
|
|
"""Test regex-based entity extraction"""
|
|
text = "Contact support@example.com or admin@test.org"
|
|
patterns = {"EMAIL": r"[\w\.-]+@[\w\.-]+"}
|
|
|
|
entities = extract_entities_regex(text, patterns=patterns)
|
|
self.assertEqual(len(entities), 2)
|
|
self.assertEqual(entities[0].label, "EMAIL")
|
|
self.assertEqual(entities[0].text, "support@example.com")
|
|
|
|
def test_extract_entities_rules(self):
|
|
"""Test rule-based entity extraction (sentence start rule)"""
|
|
text = "Alice went to the park. Bob stayed home."
|
|
# Assuming rule: Capitalized word at start of sentence is PERSON
|
|
entities = extract_entities_rules(text)
|
|
|
|
# This depends on exact implementation details in methods.py
|
|
# Current impl: Checks first word of sentence
|
|
names = [e.text for e in entities]
|
|
self.assertIn("Alice", names)
|
|
self.assertIn("Bob", names)
|
|
|
|
def test_extract_relations_regex(self):
|
|
"""Test regex-based relation extraction"""
|
|
text = "London is located in UK"
|
|
entities = [
|
|
Entity(text="London", label="GPE", start_char=0, end_char=6),
|
|
Entity(text="UK", label="GPE", start_char=21, end_char=23)
|
|
]
|
|
|
|
relations = extract_relations_regex(text, entities)
|
|
self.assertTrue(len(relations) > 0)
|
|
self.assertEqual(relations[0].predicate, "located_in")
|
|
|
|
@patch("semantica.semantic_extract.methods.SPACY_AVAILABLE", False)
|
|
@patch("semantica.semantic_extract.methods.extract_relations_pattern")
|
|
def test_extract_relations_dependency_fallback(self, mock_pattern):
|
|
"""Test dependency extraction fallback when spaCy is missing"""
|
|
mock_pattern.return_value = []
|
|
extract_relations_dependency("text", [])
|
|
mock_pattern.assert_called_once()
|
|
|
|
def test_extract_triplets_rules(self):
|
|
"""Test rule-based triplet extraction"""
|
|
text = "Steve founded Apple"
|
|
entities = [
|
|
Entity(text="Steve", label="PERSON", start_char=0, end_char=5),
|
|
Entity(text="Apple", label="ORG", start_char=14, end_char=19)
|
|
]
|
|
|
|
triplets = extract_triplets_rules(text, entities)
|
|
self.assertTrue(len(triplets) > 0)
|
|
self.assertEqual(triplets[0].predicate, "founded")
|
|
self.assertEqual(triplets[0].subject, "Steve")
|
|
self.assertEqual(triplets[0].object, "Apple")
|
|
|
|
# --- Event Detector Tests ---
|
|
|
|
def test_event_detector_basic(self):
|
|
"""Test EventDetector basic flow"""
|
|
# EventDetector uses internal patterns, so we test with text matching those patterns
|
|
# Patterns include: founded, acquired, launched, etc.
|
|
text = "Apple was founded by Steve Jobs in 1976."
|
|
|
|
# Mock _extract_participants to avoid complex logic and potential flake
|
|
# or just let it run if it's simple. It looks simple in the code.
|
|
# But we must be careful.
|
|
|
|
detector = EventDetector()
|
|
events = detector.detect_events(text)
|
|
|
|
self.assertTrue(len(events) > 0)
|
|
self.assertEqual(events[0].event_type, "founded")
|
|
# Check if participants were extracted (simple capitalization rule)
|
|
# "Steve" and "Jobs" should be captured.
|
|
# The logic captures capitalized words > 2 chars.
|
|
# "Apple" (if in context), "Steve", "Jobs" might be captured.
|
|
|
|
# We'll check if "Steve" or "Jobs" is in participants list
|
|
participants = events[0].participants
|
|
self.assertTrue(any("Steve" in p for p in participants) or any("Jobs" in p for p in participants))
|
|
|
|
# --- Semantic Analyzer Tests ---
|
|
|
|
def test_semantic_analyzer_similarity(self):
|
|
"""Test SemanticAnalyzer similarity"""
|
|
analyzer = SemanticAnalyzer()
|
|
# Jaccard similarity
|
|
s1 = "apple banana"
|
|
s2 = "apple orange"
|
|
score = analyzer.calculate_similarity(s1, s2, method="jaccard")
|
|
# intersection: apple (1), union: apple, banana, orange (3) -> 1/3 ~ 0.33
|
|
self.assertAlmostEqual(score, 1/3)
|
|
|
|
# --- Coreference Resolver Tests ---
|
|
|
|
def test_coreference_resolver_pronouns(self):
|
|
"""Test CoreferenceResolver pronoun resolution"""
|
|
from semantica.semantic_extract.coreference_resolver import CoreferenceResolver, Mention
|
|
|
|
resolver = CoreferenceResolver()
|
|
|
|
# "Steve Jobs founded Apple. He was the CEO."
|
|
# We need to manually construct mentions because we are testing the resolver logic
|
|
# independent of the entity extractor for this unit test
|
|
|
|
mentions = [
|
|
Mention(text="Steve Jobs", start_char=0, end_char=10, mention_type="entity", entity_id="e1"),
|
|
Mention(text="Apple", start_char=19, end_char=24, mention_type="entity", entity_id="e2"),
|
|
Mention(text="He", start_char=26, end_char=28, mention_type="pronoun")
|
|
]
|
|
|
|
text = "Steve Jobs founded Apple. He was the CEO."
|
|
|
|
# Use the pronoun resolver directly or via main resolver
|
|
resolutions = resolver.pronoun_resolver.resolve_pronouns(text, mentions)
|
|
|
|
self.assertTrue(len(resolutions) > 0)
|
|
# Should resolve "He" to "Steve Jobs" (closest preceding entity)
|
|
self.assertEqual(resolutions[0][0], "He")
|
|
self.assertEqual(resolutions[0][1], "Steve Jobs")
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|