mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
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
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user