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()