mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
fix(embeddings): stop the registry dispatch from calling wrappers back into themselves (#1005)
Co-authored-by: Sameer Kadam <sskadam6305@gmail.com>
This commit is contained in:
@@ -147,9 +147,12 @@ def calculate_similarity(
|
||||
>>> result = calculate_similarity(entity1, entity2, method="levenshtein")
|
||||
>>> print(f"Similarity: {result.score:.2f}")
|
||||
"""
|
||||
# Check for custom method in registry
|
||||
# Check for custom method in registry, skip self-referential wrappers.
|
||||
# _multi_factor_similarity is registered under "multi_factor" and calls back
|
||||
# into calculate_similarity(method="multi_factor"), creating indirect
|
||||
# infinite recursion. The identity guard short-circuits that loop.
|
||||
custom_method = method_registry.get("similarity", method)
|
||||
if custom_method:
|
||||
if custom_method and custom_method is not calculate_similarity:
|
||||
return custom_method(entity1, entity2, **kwargs)
|
||||
|
||||
# Use default SimilarityCalculator
|
||||
@@ -235,9 +238,11 @@ def detect_duplicates(
|
||||
>>> duplicates = detect_duplicates(entities, method="pairwise", similarity_threshold=0.8)
|
||||
>>> print(f"Found {len(duplicates)} duplicate candidates")
|
||||
"""
|
||||
# Check for custom method in registry
|
||||
# Check for custom method in registry, skip self-referential wrappers.
|
||||
# _pairwise_detection is registered under "pairwise" and calls back into
|
||||
# detect_duplicates(method="pairwise"), creating indirect infinite recursion.
|
||||
custom_method = method_registry.get("detection", method)
|
||||
if custom_method:
|
||||
if custom_method and custom_method is not detect_duplicates:
|
||||
return custom_method(
|
||||
entities, similarity_threshold=similarity_threshold, **kwargs
|
||||
)
|
||||
@@ -282,9 +287,10 @@ def dedup_triplets(
|
||||
List of duplicate relationship piars (rel1, rel2).
|
||||
"""
|
||||
|
||||
# Check for custom method in registry (but not ourself)
|
||||
# Check for custom method in registry (but not ourself — identity guard
|
||||
# consistent with the other dispatch functions in this module).
|
||||
custom_method = method_registry.get("detection", "triplets")
|
||||
if custom_method and custom_method.__name__ != "dedup_triplets":
|
||||
if custom_method and custom_method is not dedup_triplets:
|
||||
return custom_method(relationships, mode=mode, threshold=threshold, **kwargs)
|
||||
|
||||
detector = DuplicateDetector(**kwargs)
|
||||
@@ -328,9 +334,11 @@ def merge_entities(
|
||||
>>> operations = merge_entities(duplicate_entities, method="keep_most_complete")
|
||||
>>> print(f"Performed {len(operations)} merge operations")
|
||||
"""
|
||||
# Check for custom method in registry
|
||||
# Check for custom method in registry, skip self-referential registration.
|
||||
# merge_entities is now registered directly under its default method name;
|
||||
# the identity guard prevents a direct recursion loop.
|
||||
custom_method = method_registry.get("merging", method)
|
||||
if custom_method:
|
||||
if custom_method and custom_method is not merge_entities:
|
||||
return custom_method(
|
||||
entities, preserve_provenance=preserve_provenance, **kwargs
|
||||
)
|
||||
@@ -374,9 +382,12 @@ def build_clusters(
|
||||
>>> result = build_clusters(entities, method="graph_based", similarity_threshold=0.8)
|
||||
>>> print(f"Found {len(result.clusters)} clusters")
|
||||
"""
|
||||
# Check for custom method in registry
|
||||
# Check for custom method in registry, skip self-referential wrappers.
|
||||
# _graph_based_clustering is registered under "graph_based" and calls back
|
||||
# into build_clusters(method="graph_based"), creating indirect infinite
|
||||
# recursion.
|
||||
custom_method = method_registry.get("clustering", method)
|
||||
if custom_method:
|
||||
if custom_method and custom_method is not build_clusters:
|
||||
return custom_method(
|
||||
entities, similarity_threshold=similarity_threshold, **kwargs
|
||||
)
|
||||
@@ -546,25 +557,12 @@ def list_available_methods(task: Optional[str] = None) -> Dict[str, List[str]]:
|
||||
return result
|
||||
|
||||
|
||||
# Register default methods with registry
|
||||
def _multi_factor_similarity(e1, e2, **kw):
|
||||
return calculate_similarity(e1, e2, method="multi_factor", **kw)
|
||||
|
||||
|
||||
def _pairwise_detection(entities, **kw):
|
||||
return detect_duplicates(entities, method="pairwise", **kw)
|
||||
|
||||
|
||||
def _keep_most_complete_merging(entities, **kw):
|
||||
return merge_entities(entities, method="keep_most_complete", **kw)
|
||||
|
||||
|
||||
def _graph_based_clustering(entities, **kw):
|
||||
return build_clusters(entities, method="graph_based", **kw)
|
||||
|
||||
|
||||
method_registry.register("similarity", "multi_factor", _multi_factor_similarity)
|
||||
method_registry.register("detection", "pairwise", _pairwise_detection)
|
||||
method_registry.register("merging", "keep_most_complete", _keep_most_complete_merging)
|
||||
method_registry.register("clustering", "graph_based", _graph_based_clustering)
|
||||
# Register default methods with registry.
|
||||
# The public dispatch functions are registered directly so the identity guard
|
||||
# in each function short-circuits the self-reference rather than going through
|
||||
# an intermediate wrapper that re-enters the same dispatch path.
|
||||
method_registry.register("similarity", "multi_factor", calculate_similarity)
|
||||
method_registry.register("detection", "pairwise", detect_duplicates)
|
||||
method_registry.register("merging", "keep_most_complete", merge_entities)
|
||||
method_registry.register("clustering", "graph_based", build_clusters)
|
||||
method_registry.register("detection", "triplets", dedup_triplets)
|
||||
|
||||
@@ -98,8 +98,11 @@ class TestMethodDispatchRecursion(unittest.TestCase):
|
||||
self.assertIsNotNone(emb)
|
||||
|
||||
def test_embed_text_default_does_not_self_recurse(self):
|
||||
# Use the deterministic hash fallback to avoid model download;
|
||||
# "fallback" is registered as embed_text itself, so the identity
|
||||
# guard is the thing being tested — no sentence-transformers needed.
|
||||
from semantica.embeddings.methods import embed_text
|
||||
emb = embed_text("recursion probe", method="sentence_transformers")
|
||||
emb = embed_text("recursion probe", method="fallback")
|
||||
self.assertIsNotNone(emb)
|
||||
|
||||
def test_custom_registered_method_still_wins(self):
|
||||
@@ -131,3 +134,85 @@ class TestMethodDispatchRecursion(unittest.TestCase):
|
||||
with self.assertRaises(AttributeError):
|
||||
getattr(bare, "model")
|
||||
|
||||
def test_calculate_similarity_cosine_does_not_self_recurse(self):
|
||||
"""calculate_similarity is registered under "cosine"/"euclidean" — the
|
||||
identity guard must prevent infinite recursion when those aliases fire."""
|
||||
import numpy as np
|
||||
from semantica.embeddings.methods import calculate_similarity
|
||||
e1 = np.array([1.0, 0.0, 0.0])
|
||||
e2 = np.array([0.0, 1.0, 0.0])
|
||||
result = calculate_similarity(e1, e2, method="cosine")
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_pool_embeddings_mean_does_not_self_recurse(self):
|
||||
"""pool_embeddings is registered under all pooling aliases — the identity
|
||||
guard must prevent infinite recursion for every built-in pooling method."""
|
||||
import numpy as np
|
||||
from semantica.embeddings.methods import pool_embeddings
|
||||
embs = np.array([[1.0, 2.0], [3.0, 4.0]])
|
||||
result = pool_embeddings(embs, method="mean")
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
|
||||
class TestDeduplicationDispatchRecursion(unittest.TestCase):
|
||||
"""Indirect recursion in deduplication/methods.py: the private wrapper
|
||||
functions (_multi_factor_similarity, _pairwise_detection, _graph_based_clustering)
|
||||
are registered as handlers under their respective default method names and
|
||||
call back into the public dispatch functions with the same method, creating
|
||||
an indirect infinite recursion loop without an identity guard."""
|
||||
|
||||
def test_calculate_similarity_multi_factor_does_not_recurse(self):
|
||||
"""_multi_factor_similarity is registered under 'similarity/multi_factor'
|
||||
and calls calculate_similarity(method='multi_factor'), which without a
|
||||
guard would re-enter _multi_factor_similarity infinitely."""
|
||||
from semantica.deduplication.methods import calculate_similarity
|
||||
e1 = {"name": "Apple Inc.", "type": "Company"}
|
||||
e2 = {"name": "Apple", "type": "Company"}
|
||||
result = calculate_similarity(e1, e2, method="multi_factor")
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_detect_duplicates_pairwise_does_not_recurse(self):
|
||||
"""_pairwise_detection is registered under 'detection/pairwise' and
|
||||
calls detect_duplicates(method='pairwise') — indirect loop without guard."""
|
||||
from semantica.deduplication.methods import detect_duplicates
|
||||
entities = [
|
||||
{"id": "1", "name": "Alice"},
|
||||
{"id": "2", "name": "Bob"},
|
||||
]
|
||||
result = detect_duplicates(entities, method="pairwise")
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_build_clusters_graph_based_does_not_recurse(self):
|
||||
"""_graph_based_clustering is registered under 'clustering/graph_based'
|
||||
and calls build_clusters(method='graph_based') — indirect loop without guard."""
|
||||
from semantica.deduplication.methods import build_clusters
|
||||
entities = [
|
||||
{"id": "1", "name": "Alice"},
|
||||
{"id": "2", "name": "Bob"},
|
||||
]
|
||||
result = build_clusters(entities, method="graph_based")
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_custom_deduplication_method_still_wins(self):
|
||||
"""A genuinely user-registered custom method must still take precedence
|
||||
over the built-in implementation after the guard is added."""
|
||||
from semantica.deduplication.methods import (
|
||||
calculate_similarity,
|
||||
)
|
||||
from semantica.deduplication.registry import method_registry
|
||||
calls = []
|
||||
|
||||
def spy(e1, e2, **kw):
|
||||
calls.append((e1, e2))
|
||||
from semantica.deduplication.similarity_calculator import SimilarityResult
|
||||
return SimilarityResult(score=0.99, method="spy")
|
||||
|
||||
method_registry.register("similarity", "spy_method", spy)
|
||||
try:
|
||||
e1 = {"name": "Alice"}
|
||||
e2 = {"name": "Alice"}
|
||||
result = calculate_similarity(e1, e2, method="spy_method")
|
||||
self.assertEqual(result.score, 0.99)
|
||||
self.assertEqual(len(calls), 1)
|
||||
finally:
|
||||
method_registry.unregister("similarity", "spy_method")
|
||||
|
||||
Reference in New Issue
Block a user