diff --git a/cookbook/introduction/17_Conflict_Detection_and_Resolution.ipynb b/cookbook/introduction/17_Conflict_Detection_and_Resolution.ipynb index ebdc6d41..e8487ca4 100644 --- a/cookbook/introduction/17_Conflict_Detection_and_Resolution.ipynb +++ b/cookbook/introduction/17_Conflict_Detection_and_Resolution.ipynb @@ -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": [ - "

🧠 Semantica - 📊 Current Progress

StatusActionModuleSubmoduleFileTime
Semantica is resolving⚠️ conflictsConflictDetector-0.01s
Semantica is resolving⚠️ conflictsConflictAnalyzer-0.01s
Semantica is resolving⚠️ conflictsInvestigationGuideGenerator-0.00s
" - ], - "text/plain": [ - "" - ] - }, - "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." ] } ], diff --git a/semantica/conflicts/conflict_detector.py b/semantica/conflicts/conflict_detector.py index 94554f3c..cc1eafbd 100644 --- a/semantica/conflicts/conflict_detector.py +++ b/semantica/conflicts/conflict_detector.py @@ -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) diff --git a/semantica/conflicts/conflict_resolver.py b/semantica/conflicts/conflict_resolver.py index 22fb7e7e..fd7d57e6 100644 --- a/semantica/conflicts/conflict_resolver.py +++ b/semantica/conflicts/conflict_resolver.py @@ -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: diff --git a/semantica/conflicts/investigation_guide.py b/semantica/conflicts/investigation_guide.py index ddfea40a..e7cdedf8 100644 --- a/semantica/conflicts/investigation_guide.py +++ b/semantica/conflicts/investigation_guide.py @@ -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: """ diff --git a/semantica/conflicts/methods.py b/semantica/conflicts/methods.py index 37339463..74b662fd 100644 --- a/semantica/conflicts/methods.py +++ b/semantica/conflicts/methods.py @@ -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: