Spaces:
Running
Running
File size: 8,190 Bytes
5583f46 6663ad4 5583f46 6663ad4 5583f46 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | import json
import math
from contextlib import contextmanager
from unittest.mock import MagicMock
import httpx
import pytest
from src.adapters.embeddings import EmbeddingsAdapter
from src.config import EmbeddingsConfig, OllamaConfig
from src.lib.embeddings import client_ollama
from src.lib.embeddings.profile import EmbeddingProfileError, index_profile
from src.lib.postgresql import vectors
@pytest.fixture
def gemma(monkeypatch):
monkeypatch.setenv('OLLAMA_EMBEDDING_MODEL', 'embeddinggemma-2:270m')
for kind in ('NODES', 'TRIPLETS', 'OBSERVATIONS', 'DATA', 'RELATIONSHIPS'):
monkeypatch.setenv(f'EMBEDDING_{kind}_DIMENSION', '512')
for name in ('EMBEDDING_PROFILE', 'EMBEDDING_NATIVE_DIMENSION', 'EMBEDDING_MRL_DIMENSION'):
monkeypatch.delenv(name, raising=False)
monkeypatch.setattr(client_ollama.config, 'embeddings_provider', 'ollama')
monkeypatch.setattr(client_ollama.config, 'ollama', OllamaConfig())
monkeypatch.setattr(client_ollama.config, 'embeddings', EmbeddingsConfig(
'local', provider='ollama', model='embeddinggemma-2:270m'))
client = client_ollama.OllamaEmbeddingsClient()
client.client.close()
yield client
client.client.close()
def respond(client, raw_vectors, requests=None):
def handler(request):
if requests is not None:
requests.append(json.loads(request.content))
return httpx.Response(200, content=json.dumps({'embeddings': raw_vectors}))
client.client = httpx.Client(transport=httpx.MockTransport(handler), base_url='http://127.0.0.1:11434')
def test_documents_and_queries_share_exact_mrl_contract_with_distinct_prefixes(gemma):
# The discarded tail must have no influence on the norm.
raw = [3.0, 4.0] + [0.0] * 510 + [999.0] * 256
requests = []
respond(gemma, [raw], requests)
adapter = EmbeddingsAdapter()
adapter.add_client(gemma)
document = adapter.embed_text('Orchid launch code: violet-791.').embeddings
query = adapter.embed_query('What is the Orchid launch code?').embeddings
assert document == query == [0.6, 0.8] + [0.0] * 510
assert len(document) == len(query) == 512
assert math.hypot(*document) == pytest.approx(1)
assert requests[0]['input'] == ['title: none | text: Orchid launch code: violet-791.']
assert requests[1]['input'] == ['task: search result | query: What is the Orchid launch code?']
for request in requests:
assert request['model'] == 'embeddinggemma-2:270m'
assert request['truncate'] is False
assert request['options']['num_ctx'] == 8192
assert 'dimensions' not in request # Native output must be transformed by BrainAPI.
def test_mrl_batch_order_and_normalization(gemma):
respond(gemma, [[1.0] + [0.0] * 767, [0.0, 2.0] + [0.0] * 766])
results = gemma.embed_texts(['first', 'second'])
assert [len(v) for v in results] == [512, 512]
assert results[0][:2] == [1.0, 0.0]
assert results[1][:2] == [0.0, 1.0]
assert gemma.embed_texts([]) == []
@pytest.mark.parametrize('invalid', [float('nan'), float('inf'), -float('inf'), True, '1'])
@pytest.mark.parametrize('index', [0, 767])
def test_invalid_native_values_including_discarded_tail_are_rejected(gemma, invalid, index):
raw = [1.0] * 768
raw[index] = invalid
respond(gemma, [raw])
with pytest.raises(ValueError, match='invalid embedding'):
gemma.embed_text('text')
@pytest.mark.parametrize('dimension', [512, 767, 769, 1024])
def test_unexpected_native_dimension_fails_closed(gemma, dimension):
respond(gemma, [[1.0] * dimension])
with pytest.raises(ValueError, match='native dimensions'):
gemma.embed_query('query')
def test_zero_mrl_vector_is_rejected(gemma):
respond(gemma, [[0.0] * 512 + [1.0] * 256])
with pytest.raises(ValueError, match='zero'):
gemma.embed_text('text')
@pytest.mark.parametrize('name,value', [
('EMBEDDING_NATIVE_DIMENSION', '512'), ('EMBEDDING_MRL_DIMENSION', '256'),
('EMBEDDING_DATA_DIMENSION', '1024'), ('EMBEDDING_PROFILE', 'other-model'),
])
def test_incompatible_gemma_configuration_fails_before_serving(gemma, monkeypatch, name, value):
monkeypatch.setenv(name, value)
with pytest.raises(ValueError, match='embeddinggemma2-text-v1 requires'):
EmbeddingsConfig('local', provider='ollama', model='embeddinggemma-2:270m')
@pytest.mark.parametrize('existing_dimension', [512, 1024])
def test_postgres_rejects_incompatible_index_without_dropping_vectors(gemma, monkeypatch, existing_dimension):
client = vectors.PostgreSQLVectorStoreClient()
expected = index_profile(client_ollama.config, 512)
previous = {**expected, 'model': 'qwen3-embedding:0.6b'}
connection = MagicMock()
cursor = connection.cursor.return_value.__enter__.return_value
cursor.fetchone.side_effect = [(existing_dimension,), (previous,)]
@contextmanager
def connect(brain):
yield connection
monkeypatch.setattr(client, '_connection', connect)
monkeypatch.setattr(vectors, 'require_registered_brain', lambda brain_id: None)
monkeypatch.setitem(vectors.EMBEDDING_STORES_SIZES, 'data', 512)
with pytest.raises(EmbeddingProfileError, match='new empty brain/index'):
client._ensure_store('data', 'qualification')
assert all('DROP' not in str(call) for call in cursor.execute.call_args_list)
assert not client._initialized_stores
def test_fresh_postgres_schema_records_profile_and_512_dimensions(gemma, monkeypatch):
client = vectors.PostgreSQLVectorStoreClient()
connection = MagicMock()
cursor = connection.cursor.return_value.__enter__.return_value
cursor.fetchone.side_effect = [None, None]
@contextmanager
def connect(brain):
yield connection
monkeypatch.setattr(client, '_connection', connect)
monkeypatch.setattr(vectors, 'require_registered_brain', lambda brain_id: None)
monkeypatch.setattr(vectors, 'EMBEDDING_STORES_SIZES', dict.fromkeys(
('nodes', 'triplets', 'observations', 'data', 'relationships'), 512))
for store in vectors.EMBEDDING_STORES_SIZES:
cursor.fetchone.side_effect = [None, None]
client._ensure_store(store, 'qualification')
executed = cursor.execute.call_args_list
creates = [str(call.args[0]) for call in executed if 'id BIGINT PRIMARY KEY' in str(call.args[0])]
assert len(creates) == 5
assert all('embeddings vector(512)' in ddl for ddl in creates)
metadata = [json.loads(call.args[1][1]) for call in executed if 'INSERT INTO embedding_profiles' in call.args[0]]
assert len(metadata) == 5
assert all(profile['model'] == 'embeddinggemma-2:270m' and profile['stored_dimension'] == 512 for profile in metadata)
def test_profile_mismatch_has_actionable_http_error():
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.services.api.errors import install_error_handlers
app = FastAPI()
install_error_handlers(app)
@app.get('/query')
def query():
raise EmbeddingProfileError('Existing Qwen index conflicts with configured Gemma profile')
response = TestClient(app).get('/query')
assert response.status_code == 503
assert response.json()['error']['code'] == 'EMBEDDING_PROFILE_MISMATCH'
assert 'Qwen' in response.json()['detail']
assert 're-index' in response.json()['error']['resolution']
def test_graph_only_search_does_not_hide_profile_mismatch(monkeypatch):
import asyncio
from src.constants.embeddings import Vector
from src.services.api.constants.requests import SearchRequestBody
from src.services.api.controllers import search
monkeypatch.setattr(search.config, 'search_enabled', True)
embeddings = MagicMock()
embeddings.embed_query.return_value = Vector(id='q', embeddings=[0.1], metadata={})
monkeypatch.setattr(search, 'embeddings_adapter', embeddings)
monkeypatch.setattr(search, '_collect_graph_hits', MagicMock(side_effect=EmbeddingProfileError('incompatible model')))
with pytest.raises(EmbeddingProfileError, match='incompatible model'):
asyncio.run(search.search(SearchRequestBody(query='launch code', channels=['entities'])))
|