最小修复聊天摘要写回游标恢复、摘要元数据透传、webui反馈参数解析、embedding批次缓存索引、图存储清理与配置默认值回归,并补齐针对性回归测试,确保问题解决且不影响现有逻辑。
192 lines
6.5 KiB
Python
192 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from A_memorix.core.embedding import api_adapter as api_adapter_module
|
|
from A_memorix.core.embedding.api_adapter import EmbeddingAPIAdapter
|
|
from A_memorix.core.utils.runtime_self_check import run_embedding_runtime_self_check
|
|
|
|
|
|
class _FakeEmbeddingClient:
|
|
def __init__(self, *, natural_dimension: int = 12) -> None:
|
|
self.natural_dimension = int(natural_dimension)
|
|
self.requests = []
|
|
|
|
async def get_embedding(self, request):
|
|
self.requests.append(request)
|
|
requested_dimension = request.extra_params.get("dimensions")
|
|
if requested_dimension is None:
|
|
requested_dimension = request.extra_params.get("output_dimensionality")
|
|
dimension = int(requested_dimension or self.natural_dimension)
|
|
return SimpleNamespace(embedding=[1.0] * dimension)
|
|
|
|
|
|
def _build_adapter(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
*,
|
|
client_type: str,
|
|
configured_dimension: int = 1024,
|
|
effective_dimension: int | None = None,
|
|
model_extra_params: dict | None = None,
|
|
):
|
|
adapter = EmbeddingAPIAdapter(default_dimension=configured_dimension)
|
|
if effective_dimension is not None:
|
|
adapter._dimension = int(effective_dimension)
|
|
adapter._dimension_detected = True
|
|
|
|
fake_client = _FakeEmbeddingClient()
|
|
model_info = SimpleNamespace(
|
|
name="embedding-model",
|
|
api_provider="provider-1",
|
|
model_identifier="embedding-model-id",
|
|
extra_params=dict(model_extra_params or {}),
|
|
)
|
|
provider = SimpleNamespace(name="provider-1", client_type=client_type)
|
|
|
|
monkeypatch.setattr(adapter, "_resolve_candidate_model_names", lambda: ["embedding-model"])
|
|
monkeypatch.setattr(adapter, "_find_model_info", lambda model_name: model_info)
|
|
monkeypatch.setattr(adapter, "_find_provider", lambda provider_name: provider)
|
|
monkeypatch.setattr(
|
|
api_adapter_module.client_registry,
|
|
"get_client_class_instance",
|
|
lambda api_provider, force_new=True: fake_client,
|
|
)
|
|
return adapter, fake_client
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_encode_uses_canonical_dimension_for_openai_provider(monkeypatch):
|
|
adapter, fake_client = _build_adapter(
|
|
monkeypatch,
|
|
client_type="openai",
|
|
configured_dimension=1024,
|
|
effective_dimension=1024,
|
|
model_extra_params={"task_type": "SEMANTIC_SIMILARITY"},
|
|
)
|
|
|
|
embedding = await adapter.encode("北塔木梯")
|
|
|
|
request = fake_client.requests[-1]
|
|
assert request.extra_params["dimensions"] == 1024
|
|
assert "output_dimensionality" not in request.extra_params
|
|
assert request.extra_params["task_type"] == "SEMANTIC_SIMILARITY"
|
|
assert embedding.shape == (1024,)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_encode_explicit_dimension_override_wins(monkeypatch):
|
|
adapter, fake_client = _build_adapter(
|
|
monkeypatch,
|
|
client_type="openai",
|
|
configured_dimension=1024,
|
|
effective_dimension=1024,
|
|
)
|
|
|
|
embedding = await adapter.encode("海潮图", dimensions=256)
|
|
|
|
request = fake_client.requests[-1]
|
|
assert request.extra_params["dimensions"] == 256
|
|
assert "output_dimensionality" not in request.extra_params
|
|
assert embedding.shape == (256,)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_encode_maps_dimension_to_gemini_output_dimensionality(monkeypatch):
|
|
adapter, fake_client = _build_adapter(
|
|
monkeypatch,
|
|
client_type="gemini",
|
|
configured_dimension=1024,
|
|
effective_dimension=768,
|
|
)
|
|
|
|
embedding = await adapter.encode("广播站")
|
|
|
|
request = fake_client.requests[-1]
|
|
assert request.extra_params["output_dimensionality"] == 768
|
|
assert "dimensions" not in request.extra_params
|
|
assert embedding.shape == (768,)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_encode_does_not_force_dimension_for_unsupported_provider(monkeypatch):
|
|
adapter, fake_client = _build_adapter(
|
|
monkeypatch,
|
|
client_type="custom",
|
|
configured_dimension=1024,
|
|
effective_dimension=640,
|
|
model_extra_params={
|
|
"dimensions": 999,
|
|
"output_dimensionality": 888,
|
|
"custom_flag": "keep-me",
|
|
},
|
|
)
|
|
|
|
embedding = await adapter.encode("蓝漆铁盒")
|
|
|
|
request = fake_client.requests[-1]
|
|
assert "dimensions" not in request.extra_params
|
|
assert "output_dimensionality" not in request.extra_params
|
|
assert request.extra_params["custom_flag"] == "keep-me"
|
|
assert embedding.shape == (fake_client.natural_dimension,)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_runtime_self_check_reports_requested_dimension_without_explicit_override():
|
|
class _FakeEmbeddingManager:
|
|
def __init__(self) -> None:
|
|
self.detected_dimension = 384
|
|
self.encode_calls = []
|
|
|
|
async def _detect_dimension(self) -> int:
|
|
return self.detected_dimension
|
|
|
|
def get_requested_dimension(self) -> int:
|
|
return self.detected_dimension
|
|
|
|
async def encode(self, text):
|
|
self.encode_calls.append(text)
|
|
return np.ones(self.detected_dimension, dtype=np.float32)
|
|
|
|
manager = _FakeEmbeddingManager()
|
|
|
|
report = await run_embedding_runtime_self_check(
|
|
config={"embedding": {"dimension": 1024}},
|
|
vector_store=SimpleNamespace(dimension=384),
|
|
embedding_manager=manager,
|
|
)
|
|
|
|
assert report["ok"] is True
|
|
assert report["configured_dimension"] == 1024
|
|
assert report["requested_dimension"] == 384
|
|
assert report["detected_dimension"] == 384
|
|
assert report["encoded_dimension"] == 384
|
|
assert manager.encode_calls == ["A_Memorix runtime self check"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_encode_batch_keeps_batch_local_indexes_when_cache_hits_previous_batch(monkeypatch):
|
|
adapter = EmbeddingAPIAdapter(default_dimension=4, enable_cache=True)
|
|
adapter._dimension = 4
|
|
adapter._dimension_detected = True
|
|
|
|
async def fake_detect_dimension() -> int:
|
|
return 4
|
|
|
|
async def fake_get_embedding_direct(text: str, dimensions: int | None = None):
|
|
del dimensions
|
|
base = float(ord(str(text)[0]))
|
|
return [base, base + 1.0, base + 2.0, base + 3.0]
|
|
|
|
monkeypatch.setattr(adapter, "_detect_dimension", fake_detect_dimension)
|
|
monkeypatch.setattr(adapter, "_get_embedding_direct", fake_get_embedding_direct)
|
|
|
|
embeddings = await adapter.encode(["A", "B", "A", "C"], batch_size=2)
|
|
|
|
assert embeddings.shape == (4, 4)
|
|
assert np.array_equal(embeddings[0], embeddings[2])
|
|
assert embeddings[1][0] == float(ord("B"))
|
|
assert embeddings[3][0] == float(ord("C"))
|