mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Consolidates the remaining #994 fixes into this PR so it can fully close the issue, per maintainer request. From #1005 (yzxcj797): - EmbeddingGeneratorWithProvenance.__getattr__ self-recursion guard: accessing self._generator via attribute syntax re-entered __getattr__ forever when _generator was absent (failed __init__, pickle/copy probes like __deepcopy__). Private-name lookups now raise AttributeError. - 4 regression tests in TestMethodDispatchRecursion: default dispatch no longer self-recurses for generation/text, a user-registered custom method still takes precedence, and a bare provenance wrapper raises AttributeError instead of RecursionError. (The methods.py identity guards from #1005 are already present here.) From #1006 (yzxcj797): - doctor gains two embedding backend checks, "Embeddings (sentence-transformers)" and "Embeddings (fastembed)". Default is a cheap import+version check (uninstalled backend now reports fail with a pip hint instead of invisible). --deep-embeddings (or SEMANTICA_DOCTOR_DEEP_EMBEDDINGS=1) instantiates via TextEmbedder and embeds a probe, catching backends that import cleanly but cannot load (the #994 failure mode) via the hash-fallback-active signal. _DeepEmbeddingFailure marks post-import runtime/model-load failures so they get a remediation hint instead of a misleading pip-install hint. - 7 tests in TestDoctorEmbeddings and TestDoctorEmbeddingHintsAndEnv. Validation: - tests/test_cli_commands.py: 237 passed (7 new) - tests/test_embedding_providers.py: 9 passed (4 new) - AST parse + import of all four modules OK
134 lines
5.4 KiB
Python
134 lines
5.4 KiB
Python
|
|
import sys
|
|
import os
|
|
import unittest
|
|
import numpy as np
|
|
|
|
# Add project root to path
|
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
|
|
from semantica.embeddings import TextEmbedder, EmbeddingGenerator
|
|
|
|
class TestEmbeddingProviders(unittest.TestCase):
|
|
def test_sentence_transformers_default(self):
|
|
print("\nTesting Sentence Transformers (Default)...")
|
|
embedder = TextEmbedder(method="sentence_transformers")
|
|
text = "This is a test sentence."
|
|
embedding = embedder.embed_text(text)
|
|
self.assertIsInstance(embedding, np.ndarray)
|
|
print(f"Embedding shape: {embedding.shape}")
|
|
# Default model is all-MiniLM-L6-v2 which is 384 dim
|
|
self.assertEqual(len(embedding), 384)
|
|
|
|
def test_sentence_transformers_custom_model(self):
|
|
print("\nTesting Sentence Transformers (Custom Model: all-mpnet-base-v2)...")
|
|
# all-mpnet-base-v2 produces 768 dim embeddings
|
|
try:
|
|
embedder = TextEmbedder(
|
|
method="sentence_transformers",
|
|
model_name="all-mpnet-base-v2"
|
|
)
|
|
text = "This is a test sentence."
|
|
embedding = embedder.embed_text(text)
|
|
self.assertIsInstance(embedding, np.ndarray)
|
|
print(f"Embedding shape: {embedding.shape}")
|
|
self.assertEqual(len(embedding), 768)
|
|
except Exception as e:
|
|
print(f"Skipping custom model test if download fails: {e}")
|
|
|
|
def test_fastembed_default(self):
|
|
print("\nTesting FastEmbed (Default)...")
|
|
try:
|
|
embedder = TextEmbedder(method="fastembed")
|
|
text = "This is a test sentence."
|
|
embedding = embedder.embed_text(text)
|
|
self.assertIsInstance(embedding, np.ndarray)
|
|
print(f"Embedding shape: {embedding.shape}")
|
|
# FastEmbed default is usually BAAI/bge-small-en-v1.5 (384 dim) or similar
|
|
self.assertTrue(len(embedding) > 0)
|
|
except ImportError:
|
|
print("FastEmbed not installed, skipping.")
|
|
|
|
def test_fastembed_custom_model(self):
|
|
print("\nTesting FastEmbed (Custom Model: BAAI/bge-small-en-v1.5)...")
|
|
try:
|
|
embedder = TextEmbedder(
|
|
method="fastembed",
|
|
model_name="BAAI/bge-small-en-v1.5"
|
|
)
|
|
text = "This is a test sentence."
|
|
embedding = embedder.embed_text(text)
|
|
self.assertIsInstance(embedding, np.ndarray)
|
|
print(f"Embedding shape: {embedding.shape}")
|
|
self.assertEqual(len(embedding), 384)
|
|
except ImportError:
|
|
print("FastEmbed not installed, skipping.")
|
|
except Exception as e:
|
|
print(f"FastEmbed custom model error: {e}")
|
|
|
|
def test_embedding_generator_config(self):
|
|
print("\nTesting EmbeddingGenerator with config...")
|
|
# Configure to use fastembed via EmbeddingGenerator
|
|
config = {
|
|
"text": {
|
|
"method": "fastembed",
|
|
"model_name": "BAAI/bge-small-en-v1.5"
|
|
}
|
|
}
|
|
generator = EmbeddingGenerator(config=config)
|
|
embeddings = generator.generate_embeddings(["Test text"], data_type="text")
|
|
self.assertEqual(embeddings.shape[1], 384)
|
|
print("EmbeddingGenerator config test passed.")
|
|
|
|
if __name__ == '__main__':
|
|
with open("test_results.txt", "w") as f:
|
|
runner = unittest.TextTestRunner(stream=f, verbosity=2)
|
|
unittest.main(testRunner=runner, exit=False)
|
|
|
|
|
|
class TestMethodDispatchRecursion(unittest.TestCase):
|
|
"""#994: built-in aliases are registered in the method registry onto the
|
|
wrapper functions themselves, so dispatching through the registry called a
|
|
wrapper back into itself with the same default method — a recursion storm
|
|
that surfaced as `maximum recursion depth exceeded` during model loading."""
|
|
|
|
def test_generate_embeddings_default_does_not_self_recurse(self):
|
|
from semantica.embeddings.methods import generate_embeddings
|
|
emb = generate_embeddings("recursion probe")
|
|
self.assertIsNotNone(emb)
|
|
|
|
def test_embed_text_default_does_not_self_recurse(self):
|
|
from semantica.embeddings.methods import embed_text
|
|
emb = embed_text("recursion probe", method="sentence_transformers")
|
|
self.assertIsNotNone(emb)
|
|
|
|
def test_custom_registered_method_still_wins(self):
|
|
from semantica.embeddings.methods import method_registry
|
|
calls = []
|
|
|
|
def spy(data, *a, **k):
|
|
calls.append(data)
|
|
return {"custom": True}
|
|
|
|
method_registry.register("generation", "my_custom_gen", spy)
|
|
try:
|
|
from semantica.embeddings.methods import generate_embeddings
|
|
out = generate_embeddings("payload", method="my_custom_gen")
|
|
self.assertEqual(out, {"custom": True})
|
|
self.assertEqual(calls, ["payload"])
|
|
finally:
|
|
method_registry.unregister("generation", "my_custom_gen")
|
|
|
|
def test_provenance_wrapper_missing_generator_raises_attribute_error(self):
|
|
# Partially-initialised wrappers (failed __init__, pickle/copy probes)
|
|
# must raise AttributeError, not RecursionError via __getattr__.
|
|
from semantica.embeddings.embeddings_provenance import (
|
|
EmbeddingGeneratorWithProvenance,
|
|
)
|
|
bare = EmbeddingGeneratorWithProvenance.__new__(
|
|
EmbeddingGeneratorWithProvenance
|
|
)
|
|
with self.assertRaises(AttributeError):
|
|
getattr(bare, "model")
|
|
|