From b169ce62538e82aa354affa121e2c30b5d3cf476 Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Wed, 10 Dec 2025 13:36:05 +0530 Subject: [PATCH] fix(conflicts): fix recursion bug in methods.py and add comprehensive tests - Fix infinite recursion in semantica/conflicts/methods.py by removing redundant registration - Update 04_Conflict_Resolution_Strategies.ipynb to use correct API - Add unit tests for conflicts module in tests/conflicts/test_conflicts.py - Add __init__.py files to tests/ and tests/conflicts/ for package structure --- .../04_Conflict_Resolution_Strategies.ipynb | 252 ++++++------------ semantica/conflicts/methods.py | 15 -- tests/__init__.py | 0 tests/conflicts/__init__.py | 0 tests/conflicts/test_conflicts.py | 193 ++++++++++++++ 5 files changed, 280 insertions(+), 180 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/conflicts/__init__.py create mode 100644 tests/conflicts/test_conflicts.py diff --git a/cookbook/advanced/04_Conflict_Resolution_Strategies.ipynb b/cookbook/advanced/04_Conflict_Resolution_Strategies.ipynb index ad9353ca..e63733ee 100644 --- a/cookbook/advanced/04_Conflict_Resolution_Strategies.ipynb +++ b/cookbook/advanced/04_Conflict_Resolution_Strategies.ipynb @@ -41,17 +41,17 @@ "metadata": {}, "outputs": [], "source": [ - "from semantica.kg import GraphBuilder\n", - "from datetime import datetime\n", - "import json\n" + "import json\n", + "from semantica.conflicts import ConflictDetector, ConflictResolver, SourceTracker\n", + "from semantica.conflicts.conflict_resolver import ResolutionStrategy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Step 1: Create Knowledge Graph with Conflicting Data\n" + "## Step 1: Define Entities with Conflicting Data\n" ] }, { @@ -60,49 +60,48 @@ "metadata": {}, "outputs": [], "source": [ - "builder = GraphBuilder()\n", - "\n", "entities = [\n", " {\n", " \"id\": \"e1\",\n", " \"type\": \"Person\",\n", " \"name\": \"John Doe\",\n", - " \"properties\": {\"age\": 30, \"location\": \"New York\"},\n", + " \"age\": 30,\n", + " \"location\": \"New York\",\n", " \"source\": \"source1\",\n", - " \"timestamp\": datetime(2023, 1, 1)\n", + " \"confidence\": 0.8,\n", + " \"metadata\": {\"timestamp\": datetime(2023, 1, 1)}\n", " },\n", " {\n", " \"id\": \"e1\",\n", " \"type\": \"Person\",\n", " \"name\": \"John Doe\",\n", - " \"properties\": {\"age\": 32, \"location\": \"Boston\"},\n", + " \"age\": 32,\n", + " \"location\": \"Boston\",\n", " \"source\": \"source2\",\n", - " \"timestamp\": datetime(2023, 6, 1)\n", + " \"confidence\": 0.9,\n", + " \"metadata\": {\"timestamp\": datetime(2023, 6, 1)}\n", " },\n", " {\n", " \"id\": \"e2\",\n", " \"type\": \"Organization\",\n", " \"name\": \"Tech Corp\",\n", - " \"properties\": {\"founded\": 2010, \"employees\": 100},\n", + " \"founded\": 2010,\n", + " \"employees\": 100,\n", " \"source\": \"source1\",\n", - " \"timestamp\": datetime(2023, 1, 1)\n", + " \"confidence\": 0.9,\n", + " \"metadata\": {\"timestamp\": datetime(2023, 1, 1)}\n", " },\n", " {\n", " \"id\": \"e2\",\n", " \"type\": \"Organization\",\n", " \"name\": \"Tech Corp\",\n", - " \"properties\": {\"founded\": 2012, \"employees\": 150},\n", + " \"founded\": 2012,\n", + " \"employees\": 150,\n", " \"source\": \"source2\",\n", - " \"timestamp\": datetime(2023, 3, 1)\n", - " },\n", - "]\n", - "\n", - "relationships = [\n", - " {\"source\": \"e1\", \"target\": \"e2\", \"type\": \"works_for\", \"source\": \"source1\"},\n", - " {\"source\": \"e1\", \"target\": \"e2\", \"type\": \"founder_of\", \"source\": \"source2\"},\n", - "]\n", - "\n", - "knowledge_graph = builder.build(entities, relationships)\n" + " \"confidence\": 0.7,\n", + " \"metadata\": {\"timestamp\": datetime(2023, 3, 1)}\n", + " }\n", + "]" ] }, { @@ -118,22 +117,33 @@ "metadata": {}, "outputs": [], "source": [ - "consistency_checker = ConsistencyChecker()\n", - "conflicts = consistency_checker.check_conflicts(knowledge_graph)\n", + "# Initialize detector\n", + "detector = ConflictDetector(track_provenance=True)\n", "\n", + "# Detect conflicts across all properties\n", + "conflicts = detector.detect_entity_conflicts(entities)\n", + "\n", + "print(f\"Detected {len(conflicts)} conflicts:\")\n", "for i, conflict in enumerate(conflicts, 1):\n", - " print(f\"Conflict {i}:\")\n", - " print(f\" Entity/Relationship: {conflict.get('entity_id', conflict.get('relationship_id'))}\")\n", - " print(f\" Type: {conflict.get('type')}\")\n", - " print(f\" Conflicting values: {conflict.get('values')}\")\n", - " print(f\" Sources: {conflict.get('sources')}\")\n" + " print(f\"\\nConflict {i}:\")\n", + " print(f\" ID: {conflict.conflict_id}\")\n", + " print(f\" Type: {conflict.conflict_type.value}\")\n", + " print(f\" Entity: {conflict.entity_id}\")\n", + " print(f\" Property: {conflict.property_name}\")\n", + " print(f\" Values: {conflict.conflicting_values}\")\n", + " print(f\" Severity: {conflict.severity}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Step 3: Multiple Resolution Strategies\n" + "## Step 3: Resolve Conflicts\n", + "\n", + "We can apply different strategies to resolve the conflicts:\n", + "- **Voting**: Selects the most frequent value\n", + "- **Most Recent**: Selects the value with the latest timestamp\n", + "- **Highest Confidence**: Selects the value from the source with highest confidence\n" ] }, { @@ -142,86 +152,38 @@ "metadata": {}, "outputs": [], "source": [ - "class ConflictResolver:\n", - " def __init__(self):\n", - " self.audit_trail = []\n", - " \n", - " def resolve(self, conflicts, strategy=\"most_recent\"):\n", - " resolved = []\n", - " \n", - " for conflict in conflicts:\n", - " if strategy == \"most_recent\":\n", - " values = conflict.get('values', [])\n", - " timestamps = conflict.get('timestamps', [])\n", - " if timestamps:\n", - " most_recent_idx = timestamps.index(max(timestamps))\n", - " resolved_value = values[most_recent_idx]\n", - " else:\n", - " resolved_value = values[-1] if values else None\n", - " \n", - " elif strategy == \"authoritative\":\n", - " sources = conflict.get('sources', [])\n", - " authoritative_sources = [\"source1\", \"official_db\", \"verified\"]\n", - " resolved_value = None\n", - " for auth_source in authoritative_sources:\n", - " if auth_source in sources:\n", - " idx = sources.index(auth_source)\n", - " resolved_value = conflict.get('values', [])[idx]\n", - " break\n", - " if resolved_value is None:\n", - " resolved_value = conflict.get('values', [])[0] if conflict.get('values') else None\n", - " \n", - " elif strategy == \"merge\":\n", - " values = conflict.get('values', [])\n", - " if isinstance(values[0], dict):\n", - " merged = {}\n", - " for val in values:\n", - " merged.update(val)\n", - " resolved_value = merged\n", - " elif isinstance(values[0], (int, float)):\n", - " resolved_value = sum(values) / len(values)\n", - " else:\n", - " resolved_value = \", \".join(set(str(v) for v in values))\n", - " else:\n", - " resolved_value = conflict.get('values', [])[0] if conflict.get('values') else None\n", - " \n", - " resolved.append({\n", - " 'conflict_id': conflict.get('entity_id', conflict.get('relationship_id')),\n", - " 'resolved_value': resolved_value,\n", - " 'strategy': strategy,\n", - " 'timestamp': datetime.now()\n", - " })\n", - " \n", - " self.audit_trail.append({\n", - " 'conflict': conflict,\n", - " 'resolution': resolved[-1],\n", - " 'resolved_at': datetime.now()\n", - " })\n", - " \n", - " return resolved\n", - "\n", + "# Initialize resolver\n", "resolver = ConflictResolver()\n", "\n", - "resolved_1 = resolver.resolve(conflicts, strategy=\"most_recent\")\n", - "for r in resolved_1:\n", - " print(f\" Resolved: {r['conflict_id']} = {r['resolved_value']}\")\n", + "# Strategy 1: Voting\n", + "print(\"--- Strategy: Voting ---\")\n", + "results_voting = resolver.resolve_conflicts(conflicts, strategy=\"voting\")\n", + "for r in results_voting:\n", + " if r.resolved:\n", + " print(f\"Resolved {r.conflict_id}: {r.resolved_value} (Confidence: {r.confidence:.2f})\")\n", "\n", - "resolver2 = ConflictResolver()\n", - "resolved_2 = resolver2.resolve(conflicts, strategy=\"authoritative\")\n", - "for r in resolved_2:\n", - " print(f\" Resolved: {r['conflict_id']} = {r['resolved_value']}\")\n", + "# Strategy 2: Most Recent\n", + "print(\"\\n--- Strategy: Most Recent ---\")\n", + "results_recent = resolver.resolve_conflicts(conflicts, strategy=\"most_recent\")\n", + "for r in results_recent:\n", + " if r.resolved:\n", + " print(f\"Resolved {r.conflict_id}: {r.resolved_value}\")\n", "\n", - "resolver3 = ConflictResolver()\n", - "resolved_3 = resolver3.resolve(conflicts, strategy=\"merge\")\n", - "for r in resolved_3:\n", - " print(f\" Resolved: {r['conflict_id']} = {r['resolved_value']}\")\n" + "# Strategy 3: Highest Confidence\n", + "print(\"\\n--- Strategy: Highest Confidence ---\")\n", + "results_confidence = resolver.resolve_conflicts(conflicts, strategy=\"highest_confidence\")\n", + "for r in results_confidence:\n", + " if r.resolved:\n", + " print(f\"Resolved {r.conflict_id}: {r.resolved_value} (Confidence: {r.confidence:.2f})\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Step 4: Track Sources\n" + "## Step 4: Track Sources\n", + "\n", + "The `ConflictDetector` tracks source provenance when `track_provenance=True`." ] }, { @@ -230,43 +192,26 @@ "metadata": {}, "outputs": [], "source": [ - "class SourceTracker:\n", - " def __init__(self):\n", - " self.source_map = {}\n", - " \n", - " def track_sources(self, conflicts):\n", - " for conflict in conflicts:\n", - " conflict_id = conflict.get('entity_id', conflict.get('relationship_id'))\n", - " sources = conflict.get('sources', [])\n", - " timestamps = conflict.get('timestamps', [])\n", - " \n", - " self.source_map[conflict_id] = {\n", - " 'sources': sources,\n", - " 'timestamps': timestamps,\n", - " 'values': conflict.get('values', [])\n", - " }\n", - " \n", - " def get_sources(self, conflict):\n", - " conflict_id = conflict.get('entity_id', conflict.get('relationship_id'))\n", - " return self.source_map.get(conflict_id, {})\n", - "\n", - "tracker = SourceTracker()\n", - "tracker.track_sources(conflicts)\n", + "tracker = detector.source_tracker\n", "\n", "for conflict in conflicts:\n", - " sources = tracker.get_sources(conflict)\n", - " conflict_id = conflict.get('entity_id', conflict.get('relationship_id'))\n", - " print(f\"Conflict: {conflict_id}\")\n", - " print(f\" Sources: {sources.get('sources', [])}\")\n", - " print(f\" Timestamps: {sources.get('timestamps', [])}\")\n", - " print(f\" Values: {sources.get('values', [])}\")\n" + " print(f\"\\nConflict: {conflict.conflict_id}\")\n", + " # Get detailed source info for the property\n", + " sources = tracker.get_property_sources(conflict.entity_id, conflict.property_name)\n", + " if sources:\n", + " print(f\" Entity: {conflict.entity_id}, Property: {conflict.property_name}\")\n", + " print(f\" Sources found: {len(sources.sources)}\")\n", + " for src in sources.sources:\n", + " print(f\" - {src.document} (Confidence: {src.confidence})\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Step 5: Audit Trail\n" + "## Step 5: Audit Trail\n", + "\n", + "The `ConflictResolver` maintains a history of all resolutions." ] }, { @@ -275,26 +220,14 @@ "metadata": {}, "outputs": [], "source": [ - "audit_log = resolver.get_audit_trail() if hasattr(resolver, 'get_audit_trail') else resolver.audit_trail\n", + "history = resolver.get_resolution_history()\n", "\n", - "for i, entry in enumerate(audit_log, 1):\n", - " print(f\"Entry {i}:\")\n", - " print(f\" Conflict ID: {entry['conflict'].get('entity_id', entry['conflict'].get('relationship_id'))}\")\n", - " print(f\" Resolution Strategy: {entry['resolution']['strategy']}\")\n", - " print(f\" Resolved Value: {entry['resolution']['resolved_value']}\")\n", - " print(f\" Resolved At: {entry['resolved_at']}\")\n", - "\n", - "audit_export = []\n", - "for entry in audit_log:\n", - " audit_export.append({\n", - " 'conflict_id': entry['conflict'].get('entity_id', entry['conflict'].get('relationship_id')),\n", - " 'conflict_type': entry['conflict'].get('type'),\n", - " 'original_values': entry['conflict'].get('values'),\n", - " 'sources': entry['conflict'].get('sources'),\n", - " 'resolution_strategy': entry['resolution']['strategy'],\n", - " 'resolved_value': str(entry['resolution']['resolved_value']),\n", - " 'resolved_at': entry['resolved_at'].isoformat()\n", - " })\n" + "print(f\"Resolution History ({len(history)} entries):\")\n", + "for entry in history:\n", + " print(f\"\\nConflict: {entry.conflict_id}\")\n", + " print(f\" Strategy: {entry.resolution_strategy}\")\n", + " print(f\" Resolved Value: {entry.resolved_value}\")\n", + " print(f\" Notes: {entry.resolution_notes}\")" ] }, { @@ -304,21 +237,10 @@ "## Summary\n", "\n", "Conflict resolution workflow:\n", - "- Conflict Detection\n", - "- Multiple Resolution Strategies (Most Recent, Authoritative, Merge)\n", - "- Source Tracking\n", - "- Complete Audit Trail\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f\"Detected {len(conflicts)} conflicts\")\n", - "print(f\"Applied 3 resolution strategies\")\n", - "print(f\"Maintained audit trail with {len(audit_log)} entries\")\n" + "- Conflict Detection using `ConflictDetector`\n", + "- Multiple Resolution Strategies (Voting, Most Recent, Highest Confidence)\n", + "- Source Tracking with `SourceTracker`\n", + "- Complete Audit Trail via `ConflictResolver`" ] } ], @@ -329,4 +251,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/semantica/conflicts/methods.py b/semantica/conflicts/methods.py index 4486485a..37339463 100644 --- a/semantica/conflicts/methods.py +++ b/semantica/conflicts/methods.py @@ -625,19 +625,4 @@ def list_available_methods(task: Optional[str] = None) -> Dict[str, List[str]]: return result -# Register default methods with registry -def _value_detection(entities, **kw): - return detect_conflicts(entities, method="value", **kw) - -def _voting_resolution(conflicts, **kw): - return resolve_conflicts(conflicts, method="voting", **kw) - - -def _pattern_analysis(conflicts, **kw): - return analyze_conflicts(conflicts, method="pattern", **kw) - - -method_registry.register("detection", "value", _value_detection) -method_registry.register("resolution", "voting", _voting_resolution) -method_registry.register("analysis", "pattern", _pattern_analysis) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/conflicts/__init__.py b/tests/conflicts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/conflicts/test_conflicts.py b/tests/conflicts/test_conflicts.py new file mode 100644 index 00000000..7f443863 --- /dev/null +++ b/tests/conflicts/test_conflicts.py @@ -0,0 +1,193 @@ +import unittest +from datetime import datetime +from semantica.conflicts.source_tracker import SourceTracker, SourceReference +from semantica.conflicts.conflict_detector import ConflictDetector, ConflictType, Conflict +from semantica.conflicts.conflict_resolver import ConflictResolver, ResolutionStrategy +from semantica.conflicts.conflict_analyzer import ConflictAnalyzer +from semantica.conflicts.investigation_guide import InvestigationGuideGenerator + +class TestConflictsModule(unittest.TestCase): + + def setUp(self): + # Setup common data for tests + self.entities = [ + { + "id": "e1", + "type": "Person", + "name": "John Doe", + "properties": {"age": 30, "location": "New York"}, + "source": "source1", + "page": 1, + "confidence": 0.9, + "metadata": {"timestamp": "2023-01-01T10:00:00"} + }, + { + "id": "e1", + "type": "Person", + "name": "John Doe", + "properties": {"age": 32, "location": "Boston"}, + "source": "source2", + "page": 5, + "confidence": 0.8, + "metadata": {"timestamp": "2023-06-01T10:00:00"} + } + ] + + self.source1 = SourceReference( + document="doc1", + page=1, + confidence=0.9, + timestamp=datetime(2023, 1, 1) + ) + self.source2 = SourceReference( + document="doc2", + page=2, + confidence=0.8, + timestamp=datetime(2023, 6, 1) + ) + + def test_source_tracker(self): + tracker = SourceTracker() + + # Test tracking property source + tracker.track_property_source("e1", "age", 30, self.source1) + tracker.track_property_source("e1", "age", 32, self.source2) + + # Test getting property sources + prop_source = tracker.get_property_sources("e1", "age") + self.assertIsNotNone(prop_source) + self.assertEqual(len(prop_source.sources), 2) + self.assertEqual(prop_source.value, 32) # Should store latest value + + # Test finding disagreements + disagreements = tracker.find_source_disagreements("e1", "age") + # Since we tracked different sources for the same property, there might be disagreements + # based on how find_source_disagreements works (it checks for diff document or confidence) + self.assertTrue(len(disagreements) > 0) + + # Test tracking entity source + tracker.track_entity_source("e1", self.source1) + sources = tracker.get_entity_sources("e1") + self.assertTrue(len(sources) >= 1) + + def test_conflict_detector(self): + detector = ConflictDetector() + + # We need to flatten the entities structure for detect_value_conflicts as it expects + # list of entity dicts where properties are at top level or we need to adjust input + # Looking at code: value = entity[property_name] + + flat_entities = [ + {"id": "e1", "age": 30, "source": "source1", "confidence": 0.9}, + {"id": "e1", "age": 32, "source": "source2", "confidence": 0.8} + ] + + conflicts = detector.detect_value_conflicts(flat_entities, "age") + + self.assertEqual(len(conflicts), 1) + conflict = conflicts[0] + self.assertEqual(conflict.entity_id, "e1") + self.assertEqual(conflict.property_name, "age") + self.assertEqual(conflict.conflict_type, ConflictType.VALUE_CONFLICT) + self.assertEqual(len(conflict.conflicting_values), 2) + self.assertIn(30, conflict.conflicting_values) + self.assertIn(32, conflict.conflicting_values) + + # Test type conflicts + type_entities = [ + {"id": "e2", "type": "Person", "source": "s1"}, + {"id": "e2", "type": "Organization", "source": "s2"} + ] + type_conflicts = detector.detect_type_conflicts(type_entities) + self.assertEqual(len(type_conflicts), 1) + self.assertEqual(type_conflicts[0].conflict_type, ConflictType.TYPE_CONFLICT) + + def test_conflict_resolver(self): + resolver = ConflictResolver() + + conflict = Conflict( + conflict_id="c1", + conflict_type=ConflictType.VALUE_CONFLICT, + entity_id="e1", + property_name="age", + conflicting_values=[30, 30, 32], + sources=[ + {"document": "doc1", "confidence": 0.9, "metadata": {"timestamp": datetime(2023, 1, 1)}}, + {"document": "doc3", "confidence": 0.9, "metadata": {"timestamp": datetime(2023, 1, 2)}}, + {"document": "doc2", "confidence": 0.8, "metadata": {"timestamp": datetime(2023, 6, 1)}} + ] + ) + + # Test Voting + result_voting = resolver.resolve_conflict(conflict, strategy="voting") + self.assertTrue(result_voting.resolved) + self.assertEqual(result_voting.resolved_value, 30) # 30 appears twice + + # Test Most Recent + result_recent = resolver.resolve_conflict(conflict, strategy="most_recent") + self.assertTrue(result_recent.resolved) + self.assertEqual(result_recent.resolved_value, 32) # doc2 is most recent (June) + + # Test Highest Confidence + # doc1 and doc3 have 0.9, doc2 has 0.8. Should pick 30 (first max confidence) + result_conf = resolver.resolve_conflict(conflict, strategy="highest_confidence") + self.assertTrue(result_conf.resolved) + self.assertEqual(result_conf.resolved_value, 30) + + def test_conflict_analyzer(self): + analyzer = ConflictAnalyzer() + + conflicts = [ + Conflict( + conflict_id="c1", + conflict_type=ConflictType.VALUE_CONFLICT, + entity_id="e1", + property_name="age", + conflicting_values=[30, 32], + sources=[{"document": "doc1"}, {"document": "doc2"}], + severity="medium" + ), + Conflict( + conflict_id="c2", + conflict_type=ConflictType.TYPE_CONFLICT, + entity_id="e2", + property_name="type", + conflicting_values=["Person", "Org"], + sources=[{"document": "doc1"}, {"document": "doc3"}], + severity="critical" + ) + ] + + analysis = analyzer.analyze_conflicts(conflicts) + + self.assertEqual(analysis["total_conflicts"], 2) + self.assertEqual(analysis["by_severity"]["counts"]["critical"], 1) + self.assertEqual(analysis["by_severity"]["counts"]["medium"], 1) + self.assertIn("recommendations", analysis) + + def test_investigation_guide(self): + generator = InvestigationGuideGenerator() + + conflict = Conflict( + conflict_id="c1", + conflict_type=ConflictType.VALUE_CONFLICT, + entity_id="e1", + property_name="age", + conflicting_values=[30, 32], + sources=[{"document": "doc1"}, {"document": "doc2"}], + severity="medium" + ) + + guide = generator.generate_guide(conflict) + + self.assertEqual(guide.conflict_id, "c1") + self.assertEqual(guide.severity, "medium") + self.assertTrue(len(guide.investigation_steps) > 0) + self.assertTrue(len(guide.recommended_actions) > 0) + + # Test checklist export + checklist = generator.export_investigation_checklist(guide, format="text") + self.assertIn("INVESTIGATION GUIDE: c1", checklist) + +if __name__ == '__main__': + unittest.main()