Update conflict resolution notebook and fix module errors

This commit is contained in:
KaifAhmad1
2025-12-17 14:33:09 +05:30
parent 00322d81c6
commit 999c490ba9
5 changed files with 144 additions and 142 deletions
@@ -4,23 +4,38 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"# Conflict Detection and Resolution\n",
"# Conflict Detection and Resolution in Semantica\n",
"\n",
"## Overview\n",
"\n",
"In real-world Knowledge Graph construction, data often comes from multiple heterogeneous sources (databases, APIs, files, streams). These sources may provide conflicting information about the same entities or relationships. \n",
"In modern data pipelines, especially those building Knowledge Graphs, data is often ingested from multiple heterogeneous sources (e.g., internal databases, third-party APIs, web scrapes). Discrepancies are inevitable. \n",
"\n",
"The **Semantica Conflict Detection and Resolution** module (`semantica.conflicts`) provides a comprehensive suite of tools to identifying, analyzing, and resolving these discrepancies to ensure high data quality and trust.\n",
"The **Semantica Conflict Resolution Module** (`semantica.conflicts`) provides a robust framework for managing these data inconsistencies. It is designed to ensure that your downstream applications consume only high-quality, reconciled data.\n",
"\n",
"**Key Capabilities:**\n",
"- **Conflict Detection**: Identify value mismatches, type inconsistencies, and temporal contradictions.\n",
"- **Source Tracking**: Trace every piece of data back to its origin with credibility scoring.\n",
"- **Resolution Strategies**: Apply automated strategies like voting, credibility weighting, or recency.\n",
"- **Investigation Guides**: Generate human-readable guides for complex conflicts requiring manual review.\n",
"### Key Capabilities\n",
"\n",
"1. **Multi-Dimensional Conflict Detection**\n",
" * **Value Conflicts**: Different values for the same property (e.g., `\"Google\"` vs `\"Google Inc.\"`).\n",
" * **Type Conflicts**: Data type mismatches (e.g., string vs integer).\n",
" * **Temporal Conflicts**: Chronological inconsistencies (e.g., a `start_date` after an `end_date`).\n",
"\n",
"2. **Provenance & Source Tracking**\n",
" * **Granular Tracking**: Trace every property value back to its specific source document, page, or API call.\n",
" * **Credibility Scoring**: Assign trust scores to sources (e.g., `0.95` for internal HR DB vs `0.60` for web scrapes).\n",
"\n",
"3. **Automated Resolution Strategies**\n",
" * **Voting**: Majority rules (useful for multiple equal-weight sources).\n",
" * **Credibility Weighted**: Values from higher-trust sources override others.\n",
" * **Recency**: The most recent data point wins.\n",
" * **Expert Review**: Flag complex conflicts for human intervention.\n",
"\n",
"4. **Investigation & Auditing**\n",
" * **Investigation Guides**: Auto-generate step-by-step guides for human analysts to resolve sticky conflicts.\n",
" * **Audit Trails**: Keep a record of how every conflict was resolved for compliance.\n",
"\n",
"## Installation\n",
"\n",
"Ensure Semantica is installed:\n",
"Ensure Semantica is installed in your environment:\n",
"\n",
"```bash\n",
"pip install semantica\n",
@@ -51,7 +66,17 @@
"source": [
"## Step 1: Simulating Multi-Source Data\n",
"\n",
"Let's simulate a scenario where we receive data about the same person from three different sources: an HR database, a LinkedIn scrape, and a public directory. Note the discrepancies in `birth_date` and `department`."
"To demonstrate the framework, we will simulate a realistic scenario involving employee data.\n",
"\n",
"**The Scenario:**\n",
"We have received records for **Employee 001** from three distinct sources:\n",
"1. **HR Database**: Highly trusted internal source.\n",
"2. **LinkedIn Scrape**: Less reliable external source.\n",
"3. **Public Directory**: Outdated public API.\n",
"\n",
"**The Conflicts:**\n",
"* **`birth_date`**: The Public Directory lists a different year.\n",
"* **`department`**: LinkedIn uses a more specific name (\"Software Engineering\") vs the generic \"Engineering\" in the HR DB."
]
},
{
@@ -68,14 +93,14 @@
}
],
"source": [
"# Define simulated data sources\n",
"sources = {\n",
"# 1. Define source metadata\n",
"sources_metadata = {\n",
" \"hr_db\": {\"credibility\": 0.95, \"type\": \"internal_database\"},\n",
" \"linkedin_scrape\": {\"credibility\": 0.60, \"type\": \"web_scrape\"},\n",
" \"public_dir\": {\"credibility\": 0.40, \"type\": \"public_api\"}\n",
"}\n",
"\n",
"# Define entities from these sources\n",
"# 2. Define entity records from these sources\n",
"entity_records = [\n",
" {\n",
" \"id\": \"emp_001\",\n",
@@ -96,7 +121,7 @@
" {\n",
" \"id\": \"emp_001\",\n",
" \"name\": \"John Doe\",\n",
" \"birth_date\": \"1982-05-15\", # Conflict!\n",
" \"birth_date\": \"1982-05-15\", # Conflict: Different year\n",
" \"department\": \"Engineering\",\n",
" \"source\": \"public_dir\",\n",
" \"timestamp\": \"2022-12-01T09:00:00\"\n",
@@ -110,40 +135,33 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 2: Tracking Sources\n",
"## Step 2: Registering and Tracking Sources\n",
"\n",
"Before detecting conflicts, we register our sources with the `SourceTracker`. This allows the system to factor in source credibility during resolution."
"Before we can effectively resolve conflicts based on trust, we must register our sources with the `SourceTracker`.\n",
"\n",
"The `SourceTracker` acts as a central registry for:\n",
"* **Credibility Scores**: How much we trust the source.\n",
"* **Metadata**: Source type, location, and update frequency.\n",
"\n",
"We iterate through our simulated sources and register them."
]
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 3,
"metadata": {},
"outputs": [
{
"ename": "AttributeError",
"evalue": "'SourceTracker' object has no attribute 'register_source'",
"output_type": "error",
"traceback": [
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)",
"Cell \u001b[1;32mIn[8], line 7\u001b[0m\n\u001b[0;32m 4\u001b[0m source_tracker \u001b[38;5;241m=\u001b[39m SourceTracker()\n\u001b[0;32m 6\u001b[0m \u001b[38;5;66;03m# 4. Now this will work\u001b[39;00m\n\u001b[1;32m----> 7\u001b[0m \u001b[43msource_tracker\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mregister_source\u001b[49m(\n\u001b[0;32m 8\u001b[0m source_id\u001b[38;5;241m=\u001b[39msource_id,\n\u001b[0;32m 9\u001b[0m source_type\u001b[38;5;241m=\u001b[39mmetadata[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtype\u001b[39m\u001b[38;5;124m\"\u001b[39m],\n\u001b[0;32m 10\u001b[0m credibility_score\u001b[38;5;241m=\u001b[39mmetadata[\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcredibility\u001b[39m\u001b[38;5;124m\"\u001b[39m]\n\u001b[0;32m 11\u001b[0m )\n",
"\u001b[1;31mAttributeError\u001b[0m: 'SourceTracker' object has no attribute 'register_source'"
]
}
],
"outputs": [],
"source": [
"from semantica.conflicts import SourceTracker\n",
"\n",
"# 3. Create a new instance\n",
"source_tracker = SourceTracker()\n",
"\n",
"# 4. Now this will work\n",
"source_tracker.register_source(\n",
" source_id=source_id,\n",
" source_type=metadata[\"type\"],\n",
" credibility_score=metadata[\"credibility\"]\n",
")"
"print(\"Registering sources...\")\n",
"for source_id, metadata in sources_metadata.items():\n",
" source_tracker.register_source(\n",
" source_id=source_id,\n",
" source_type=metadata[\"type\"],\n",
" credibility_score=metadata[\"credibility\"]\n",
" )\n",
" print(f\" - Registered '{source_id}' with credibility {metadata['credibility']}\")"
]
},
{
@@ -152,36 +170,20 @@
"source": [
"## Step 3: Detecting Conflicts\n",
"\n",
"Now we use `ConflictDetector` to identify discrepancies. We'll check for value conflicts in `birth_date` and `department`.\n",
"We use the `ConflictDetector` to scan our records for discrepancies. \n",
"\n",
"The detector compares values across all records for the same entity ID."
"The detector is flexible and can be configured to check:\n",
"* **Specific Properties**: Check only critical fields like `birth_date`.\n",
"* **Entire Entities**: Scan all properties for an entity.\n",
"\n",
"Here, we explicitly check `birth_date` and `department`."
]
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div style='font-family: monospace;'><h4>🧠 Semantica - 📊 Current Progress</h4><table style='width: 100%; border-collapse: collapse;'><tr><th>Status</th><th>Action</th><th>Module</th><th>Submodule</th><th>File</th><th>Time</th></tr><tr><td>✅</td><td>Semantica is resolving</td><td>⚠️ conflicts</td><td>ConflictDetector</td><td>-</td><td>0.01s</td></tr><tr><td>✅</td><td>Semantica is resolving</td><td>⚠️ conflicts</td><td>ConflictAnalyzer</td><td>-</td><td>0.01s</td></tr><tr><td>✅</td><td>Semantica is resolving</td><td>⚠️ conflicts</td><td>InvestigationGuideGenerator</td><td>-</td><td>0.00s</td></tr></table></div>"
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"Value conflict detected: emp_001.birth_date has conflicting values: ['1980-05-15', '1982-05-15']\n",
"Value conflict detected: emp_001.department has conflicting values: ['Software Engineering', 'Engineering']\n"
]
},
{
"name": "stdout",
"output_type": "stream",
@@ -199,16 +201,16 @@
}
],
"source": [
"detector = ConflictDetector()\n",
"# Initialize detector with our populated source tracker\n",
"detector = ConflictDetector(source_tracker=source_tracker)\n",
"\n",
"# Detect conflicts for specific properties\n",
"conflicts = []\n",
"\n",
"# Check birth_date\n",
"# 1. Check birth_date\n",
"dob_conflicts = detector.detect_value_conflicts(entity_records, \"birth_date\")\n",
"conflicts.extend(dob_conflicts)\n",
"\n",
"# Check department\n",
"# 2. Check department\n",
"dept_conflicts = detector.detect_value_conflicts(entity_records, \"department\")\n",
"conflicts.extend(dept_conflicts)\n",
"\n",
@@ -224,14 +226,17 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"## Step 4: Analyzing Patterns\n",
"## Step 4: Analyzing Conflict Patterns\n",
"\n",
"The `ConflictAnalyzer` can help identify systemic issues, such as a specific source consistently contradicting others."
"When dealing with large datasets, individual conflicts are less important than systemic patterns. The `ConflictAnalyzer` helps answer questions like:\n",
"* \"Is one specific source responsible for most conflicts?\"\n",
"* \"Are conflicts concentrated in a specific entity type?\"\n",
"* \"What is the distribution of conflict severity?\""
]
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 5,
"metadata": {},
"outputs": [
{
@@ -251,8 +256,8 @@
"\n",
"print(\"Conflict Analysis Summary:\")\n",
"print(f\"Total Conflicts: {analysis['total_conflicts']}\")\n",
"print(f\"By Type: {analysis['by_type']}\")\n",
"print(f\"By Severity: {analysis['by_severity']}\")"
"print(f\"By Type: {analysis.get('by_type', {}).get('counts')}\")\n",
"print(f\"By Severity: {analysis.get('by_severity', {}).get('counts')}\")"
]
},
{
@@ -261,52 +266,43 @@
"source": [
"## Step 5: Resolving Conflicts\n",
"\n",
"We can resolve conflicts using different strategies. \n",
"This is the critical step where we decide which value to trust. Semantica offers flexible `ResolutionStrategies`.\n",
"\n",
"### Strategy A: Voting\n",
"Uses the most frequent value. Useful when you have many sources of equal standing.\n",
"### Strategy A: Voting (Majority Rules)\n",
"This strategy selects the value that appears most frequently. It is simple but treats all sources as equal.\n",
"\n",
"### Strategy B: Credibility Weighted\n",
"Prefers values from trusted sources (like our HR DB) over lower-trust sources (public directory)."
"This strategy calculates a weighted score for each value based on the `credibility` of its source. \n",
"\n",
"**Example:**\n",
"* `hr_db` (0.95) says \"1980-05-15\"\n",
"* `public_dir` (0.40) says \"1982-05-15\"\n",
"\n",
"Even if multiple low-quality sources agreed on the wrong date, the high-credibility source would likely win."
]
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": 6,
"metadata": {},
"outputs": [
{
"ename": "AttributeError",
"evalue": "'ConflictResolver' object has no attribute 'set_source_tracker'",
"output_type": "error",
"traceback": [
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)",
"Cell \u001b[1;32mIn[10], line 4\u001b[0m\n\u001b[0;32m 1\u001b[0m resolver \u001b[38;5;241m=\u001b[39m ConflictResolver()\n\u001b[0;32m 3\u001b[0m \u001b[38;5;66;03m# Need to link the source tracker to the resolver for credibility strategies\u001b[39;00m\n\u001b[1;32m----> 4\u001b[0m \u001b[43mresolver\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mset_source_tracker\u001b[49m(source_tracker)\n\u001b[0;32m 6\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m--- Resolution: Voting ---\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 7\u001b[0m voting_results \u001b[38;5;241m=\u001b[39m resolver\u001b[38;5;241m.\u001b[39mresolve_conflicts(conflicts, strategy\u001b[38;5;241m=\u001b[39mResolutionStrategy\u001b[38;5;241m.\u001b[39mVOTING)\n",
"\u001b[1;31mAttributeError\u001b[0m: 'ConflictResolver' object has no attribute 'set_source_tracker'"
]
}
],
"outputs": [],
"source": [
"resolver = ConflictResolver()\n",
"\n",
"# Need to link the source tracker to the resolver for credibility strategies\n",
"# CRITICAL: Link the source tracker to the resolver.\n",
"# This allows the resolver to look up the credibility scores we registered in Step 2.\n",
"resolver.set_source_tracker(source_tracker)\n",
"\n",
"print(\"--- Resolution: Voting ---\")\n",
"voting_results = resolver.resolve_conflicts(conflicts, strategy=ResolutionStrategy.VOTING)\n",
"for res in voting_results:\n",
" print(f\"Property: {res.metadata.get('property_name')}\")\n",
" print(f\"Resolved Value: {res.resolved_value}\")\n",
" print(f\"Confidence: {res.confidence:.2f}\")\n",
" print(f\"Property: {res.metadata.get('property_name'):<15} | Resolved Value: {res.resolved_value}\")\n",
"\n",
"print(\"\\n--- Resolution: Credibility Weighted ---\")\n",
"# This should favor the HR DB value for birth_date\n",
"# Notice how the HR DB's value is preferred due to higher credibility\n",
"credibility_results = resolver.resolve_conflicts(conflicts, strategy=ResolutionStrategy.CREDIBILITY_WEIGHTED)\n",
"for res in credibility_results:\n",
" print(f\"Property: {res.metadata.get('property_name')}\")\n",
" print(f\"Resolved Value: {res.resolved_value}\")\n",
" print(f\"Confidence: {res.confidence:.2f}\")"
" print(f\"Property: {res.metadata.get('property_name'):<15} | Resolved Value: {res.resolved_value} (Confidence: {res.confidence:.2f})\")"
]
},
{
@@ -315,48 +311,36 @@
"source": [
"## Step 6: Generating Investigation Guides\n",
"\n",
"For critical conflicts or those with low resolution confidence, manual intervention is needed. The `InvestigationGuideGenerator` creates a structured guide for human analysts."
"Not all conflicts can be resolved automatically. High-stakes or low-confidence resolutions require human review.\n",
"\n",
"The `InvestigationGuideGenerator` produces a structured \"flight plan\" for an analyst, detailing:\n",
"1. **What** is in conflict.\n",
"2. **Who** (which sources) are involved.\n",
"3. **How** to verify the correct data (actionable steps)."
]
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Investigation Guide for emp_001_birth_date_conflict:\n"
]
},
{
"ename": "AttributeError",
"evalue": "'InvestigationGuide' object has no attribute 'title'",
"output_type": "error",
"traceback": [
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[1;31mAttributeError\u001b[0m Traceback (most recent call last)",
"Cell \u001b[1;32mIn[11], line 7\u001b[0m\n\u001b[0;32m 4\u001b[0m guide \u001b[38;5;241m=\u001b[39m guide_generator\u001b[38;5;241m.\u001b[39mgenerate_guide(conflicts[\u001b[38;5;241m0\u001b[39m])\n\u001b[0;32m 6\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mInvestigation Guide for \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mguide\u001b[38;5;241m.\u001b[39mconflict_id\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m:\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m----> 7\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mTitle: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[43mguide\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mtitle\u001b[49m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 8\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mSteps:\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[0;32m 9\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m i, step \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(guide\u001b[38;5;241m.\u001b[39msteps, \u001b[38;5;241m1\u001b[39m):\n",
"\u001b[1;31mAttributeError\u001b[0m: 'InvestigationGuide' object has no attribute 'title'"
]
}
],
"outputs": [],
"source": [
"guide_generator = InvestigationGuideGenerator()\n",
"\n",
"# Generate a guide for the first conflict (e.g., birth_date)\n",
"# Generate a guide for the first conflict (birth_date)\n",
"guide = guide_generator.generate_guide(conflicts[0])\n",
"\n",
"print(f\"Investigation Guide for {guide.conflict_id}:\")\n",
"print(f\"Title: {guide.title}\")\n",
"print(\"Steps:\")\n",
"for i, step in enumerate(guide.steps, 1):\n",
" print(f\"{i}. {step.description} (Action: {step.action_type})\")\n",
"print(f\"=== {guide.title} ===\")\n",
"print(f\"Summary: {guide.conflict_summary}\\n\")\n",
"\n",
"print(\"\\nRecommended Checks:\")\n",
"for check in guide.checklist:\n",
" print(f\"[ ] {check}\")"
"print(\"Investigation Steps:\")\n",
"for i, step in enumerate(guide.investigation_steps, 1):\n",
" print(f\"{i}. {step.description}\")\n",
" print(f" Action: {step.action}\")\n",
"\n",
"print(\"\\nRecommended Actions:\")\n",
"for action in guide.recommended_actions:\n",
" print(f"[ ] {action}\")"
]
},
{
@@ -365,13 +349,14 @@
"source": [
"## Conclusion\n",
"\n",
"In this notebook, we explored how to:\n",
"1. **Detect** conflicts in multi-source data.\n",
"2. **Track** data provenance and source credibility.\n",
"3. **Resolve** conflicts using automated strategies tailored to your data governance needs.\n",
"4. **Investigate** complex issues with generated guides.\n",
"You have successfully built a conflict resolution pipeline using Semantica! \n",
"\n",
"By integrating these steps into your pipeline, you ensure your Knowledge Graph remains accurate, consistent, and trustworthy."
"**Recap of what we achieved:**\n",
"1. **Ingested** multi-source data with potential quality issues.\n",
"2. **Registered** sources with credibility scores to establish a \"hierarchy of trust.\"\n",
"3. **Detected** conflicts automatically across the dataset.\n",
"4. **Resolved** conflicts using a credibility-weighted strategy, prioritizing trusted internal data.\n",
"5. **Generated** actionable guides for human-in-the-loop review of difficult cases."
]
}
],
+1 -1
View File
@@ -115,7 +115,7 @@ class ConflictDetector:
self.config = config or {}
self.config.update(kwargs)
self.source_tracker = SourceTracker()
self.source_tracker = self.config.get("source_tracker") or SourceTracker()
self.track_provenance = self.config.get("track_provenance", True)
self.conflict_fields = self.config.get("conflict_fields", {})
self.confidence_threshold = self.config.get("confidence_threshold", 0.7)
+9
View File
@@ -133,6 +133,15 @@ class ConflictResolver:
self.resolution_history: List[ResolutionResult] = []
def set_source_tracker(self, source_tracker: SourceTracker) -> None:
"""
Set the source tracker instance.
Args:
source_tracker: Source tracker instance
"""
self.source_tracker = source_tracker
def _normalize_strategy(
self, strategy: Union[str, ResolutionStrategy, None]
) -> ResolutionStrategy:
@@ -94,6 +94,11 @@ class InvestigationGuide:
context: Dict[str, Any] = field(default_factory=dict)
generated_at: str = field(default_factory=lambda: datetime.now().isoformat())
@property
def title(self) -> str:
"""Get guide title."""
return f"Investigation: {self.conflict_id}"
class InvestigationGuideGenerator:
"""
+10 -7
View File
@@ -317,6 +317,7 @@ def track_sources(
value: Optional[Any] = None,
source: Optional[SourceReference] = None,
method: str = "property",
tracker: Optional[SourceTracker] = None,
**kwargs,
) -> bool:
"""
@@ -333,6 +334,7 @@ def track_sources(
- "property": Track sources for property values
- "entity": Track sources for entities
- "relationship": Track sources for relationships
tracker: Optional SourceTracker instance to use (otherwise creates new one)
**kwargs: Additional options passed to SourceTracker
Returns:
@@ -348,11 +350,12 @@ def track_sources(
custom_method = method_registry.get("tracking", method)
if custom_method:
return custom_method(
entity_id, property_name=property_name, value=value, source=source, **kwargs
entity_id, property_name=property_name, value=value, source=source, tracker=tracker, **kwargs
)
# Use default SourceTracker
tracker = SourceTracker(**kwargs)
# Use provided tracker or create new one
# Also check kwargs for 'source_tracker' for compatibility
actual_tracker = tracker or kwargs.get("source_tracker") or SourceTracker(**kwargs)
# Map method to tracker method
if method == "property":
@@ -360,24 +363,24 @@ def track_sources(
raise ValueError(
"property_name, value, and source are required for property tracking"
)
return tracker.track_property_source(
return actual_tracker.track_property_source(
entity_id, property_name, value, source, **kwargs
)
elif method == "entity":
if not source:
raise ValueError("source is required for entity tracking")
return tracker.track_entity_source(entity_id, source, **kwargs)
return actual_tracker.track_entity_source(entity_id, source, **kwargs)
elif method == "relationship":
relationship_id = kwargs.get("relationship_id")
if not relationship_id or not source:
raise ValueError(
"relationship_id and source are required for relationship tracking"
)
return tracker.track_relationship_source(relationship_id, source, **kwargs)
return actual_tracker.track_relationship_source(relationship_id, source, **kwargs)
else:
# Default to property tracking
if property_name and value and source:
return tracker.track_property_source(
return actual_tracker.track_property_source(
entity_id, property_name, value, source, **kwargs
)
else: