Livrare LOT 1 - Didi
This commit is contained in:
commit
5380c3fc63
990 changed files with 133308 additions and 0 deletions
1
ai_platform/modules/embeddings/tests/__init__.py
Normal file
1
ai_platform/modules/embeddings/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Tests for embeddings module."""
|
||||
82
ai_platform/modules/embeddings/tests/conftest.py
Normal file
82
ai_platform/modules/embeddings/tests/conftest.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"""Pytest fixtures for embeddings tests."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from embeddings.api.app import create_app
|
||||
from embeddings.api.dependencies import init_concurrency_limiter
|
||||
from embeddings.client import EmbeddingClient
|
||||
from embeddings.config import EmbeddingSettings, SettingsCache
|
||||
from embeddings.schemas import EmbeddingResponse
|
||||
from embeddings.types import BackendType, EmbeddingData, EmbeddingUsage
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_settings_cache() -> None:
|
||||
"""Clear settings cache before each test."""
|
||||
SettingsCache.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_settings() -> EmbeddingSettings:
|
||||
"""Create test settings with mocked values."""
|
||||
return EmbeddingSettings(
|
||||
default_backend="vllm",
|
||||
enable_vllm=True,
|
||||
enable_llamacpp=False,
|
||||
external_url="http://localhost:54100",
|
||||
host="127.0.0.1",
|
||||
port=54100,
|
||||
api_tokens=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_embedding_response() -> EmbeddingResponse:
|
||||
"""Create a mock embedding response."""
|
||||
return EmbeddingResponse(
|
||||
data=[
|
||||
EmbeddingData(
|
||||
index=0,
|
||||
embedding=[0.1, 0.2, 0.3, 0.4, 0.5],
|
||||
)
|
||||
],
|
||||
model="test-model",
|
||||
usage=EmbeddingUsage(prompt_tokens=5, total_tokens=5),
|
||||
backend="vllm",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client_with_mock_backend(
|
||||
test_settings: EmbeddingSettings, mock_embedding_response: EmbeddingResponse
|
||||
) -> EmbeddingClient:
|
||||
"""Create an EmbeddingClient with mocked backend."""
|
||||
SettingsCache.set(test_settings)
|
||||
|
||||
client = EmbeddingClient(settings=test_settings)
|
||||
|
||||
# Mock the backend's embed method
|
||||
backend = client.registry.get(BackendType.VLLM)
|
||||
embeddings = [d.embedding for d in mock_embedding_response.data]
|
||||
usage = mock_embedding_response.usage
|
||||
backend.embed = AsyncMock(return_value=(embeddings, usage))
|
||||
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_client(test_settings: EmbeddingSettings) -> TestClient:
|
||||
"""Create a FastAPI TestClient with test settings."""
|
||||
SettingsCache.set(test_settings)
|
||||
|
||||
init_concurrency_limiter(test_settings.max_concurrent_requests)
|
||||
|
||||
app = create_app()
|
||||
|
||||
app.state.settings = test_settings
|
||||
app.state.client = EmbeddingClient(settings=test_settings)
|
||||
|
||||
return TestClient(app)
|
||||
179
ai_platform/modules/embeddings/tests/test_config.py
Normal file
179
ai_platform/modules/embeddings/tests/test_config.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
"""Tests for configuration module."""
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from embeddings.config import EmbeddingSettings, SettingsCache
|
||||
|
||||
|
||||
class TestEmbeddingSettings:
|
||||
"""Tests for EmbeddingSettings."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
"""Test that required fields must be provided."""
|
||||
with pytest.raises(ValidationError):
|
||||
EmbeddingSettings()
|
||||
|
||||
def test_with_required_fields(self) -> None:
|
||||
"""Test settings with all required fields provided."""
|
||||
settings = EmbeddingSettings(
|
||||
default_backend="vllm",
|
||||
enable_vllm=True,
|
||||
enable_llamacpp=False,
|
||||
external_url="http://localhost:54100",
|
||||
)
|
||||
|
||||
assert settings.default_backend == "vllm"
|
||||
assert settings.enable_vllm is True
|
||||
assert settings.enable_llamacpp is False
|
||||
assert settings.external_url == "http://localhost:54100"
|
||||
|
||||
def test_optional_defaults(self) -> None:
|
||||
"""Test optional fields have sensible defaults."""
|
||||
settings = EmbeddingSettings(
|
||||
default_backend="vllm",
|
||||
enable_vllm=True,
|
||||
enable_llamacpp=False,
|
||||
external_url="http://localhost:54100",
|
||||
)
|
||||
|
||||
assert settings.host == "0.0.0.0"
|
||||
assert settings.port == 54100
|
||||
assert settings.request_timeout == 120.0
|
||||
assert settings.rate_limit_rps == 20.0
|
||||
assert settings.max_concurrent_requests == 20
|
||||
|
||||
def test_env_override(self) -> None:
|
||||
"""Test environment variable overrides."""
|
||||
env_vars = {
|
||||
"EMB_DEFAULT_BACKEND": "llamacpp",
|
||||
"EMB_ENABLE_VLLM": "false",
|
||||
"EMB_ENABLE_LLAMACPP": "true",
|
||||
"EMB_EXTERNAL_URL": "http://example.com",
|
||||
"EMB_PORT": "14100",
|
||||
}
|
||||
|
||||
with patch.dict(os.environ, env_vars, clear=False):
|
||||
settings = EmbeddingSettings()
|
||||
|
||||
assert settings.default_backend == "llamacpp"
|
||||
assert settings.enable_vllm is False
|
||||
assert settings.enable_llamacpp is True
|
||||
assert settings.port == 14100
|
||||
|
||||
def test_vllm_url_default(self) -> None:
|
||||
"""Test vLLM URL default value."""
|
||||
settings = EmbeddingSettings(
|
||||
default_backend="vllm",
|
||||
enable_vllm=True,
|
||||
enable_llamacpp=False,
|
||||
external_url="http://localhost:54100",
|
||||
)
|
||||
assert settings.vllm_base_url == "http://localhost:54101"
|
||||
|
||||
def test_llamacpp_url_default(self) -> None:
|
||||
"""Test llama.cpp URL default value."""
|
||||
settings = EmbeddingSettings(
|
||||
default_backend="vllm",
|
||||
enable_vllm=True,
|
||||
enable_llamacpp=False,
|
||||
external_url="http://localhost:54100",
|
||||
)
|
||||
assert settings.llamacpp_base_url == "http://localhost:54110"
|
||||
|
||||
def test_api_tokens_parsing(self) -> None:
|
||||
"""Test API tokens are parsed from comma-separated string."""
|
||||
settings = EmbeddingSettings(
|
||||
default_backend="vllm",
|
||||
enable_vllm=True,
|
||||
enable_llamacpp=False,
|
||||
external_url="http://localhost:54100",
|
||||
api_tokens="token1,token2,token3", # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
assert settings.api_tokens is not None
|
||||
assert "token1" in settings.api_tokens
|
||||
assert "token2" in settings.api_tokens
|
||||
assert "token3" in settings.api_tokens
|
||||
assert settings.auth_enabled is True
|
||||
|
||||
def test_auth_disabled_by_default(self) -> None:
|
||||
"""Test authentication is disabled when no tokens set."""
|
||||
settings = EmbeddingSettings(
|
||||
default_backend="vllm",
|
||||
enable_vllm=True,
|
||||
enable_llamacpp=False,
|
||||
external_url="http://localhost:54100",
|
||||
)
|
||||
|
||||
assert settings.api_tokens is None
|
||||
assert settings.auth_enabled is False
|
||||
|
||||
|
||||
class TestSettingsCache:
|
||||
"""Tests for SettingsCache class."""
|
||||
|
||||
def test_set_and_get(self) -> None:
|
||||
"""Test setting and getting cached settings."""
|
||||
SettingsCache.clear()
|
||||
|
||||
settings = EmbeddingSettings(
|
||||
default_backend="vllm",
|
||||
enable_vllm=True,
|
||||
enable_llamacpp=False,
|
||||
external_url="http://localhost:54100",
|
||||
)
|
||||
SettingsCache.set(settings)
|
||||
|
||||
retrieved = SettingsCache.get()
|
||||
assert retrieved is settings
|
||||
|
||||
def test_get_without_set_creates_instance(self) -> None:
|
||||
"""Test that get() creates instance from environment if not set."""
|
||||
SettingsCache.clear()
|
||||
|
||||
env_vars = {
|
||||
"EMB_DEFAULT_BACKEND": "vllm",
|
||||
"EMB_ENABLE_VLLM": "true",
|
||||
"EMB_ENABLE_LLAMACPP": "false",
|
||||
"EMB_EXTERNAL_URL": "http://localhost:54100",
|
||||
}
|
||||
|
||||
with patch.dict(os.environ, env_vars, clear=False):
|
||||
settings = SettingsCache.get()
|
||||
assert isinstance(settings, EmbeddingSettings)
|
||||
assert settings.default_backend == "vllm"
|
||||
|
||||
def test_cached_returns_same_instance(self) -> None:
|
||||
"""Test that SettingsCache returns cached instance."""
|
||||
SettingsCache.clear()
|
||||
|
||||
settings = EmbeddingSettings(
|
||||
default_backend="vllm",
|
||||
enable_vllm=True,
|
||||
enable_llamacpp=False,
|
||||
external_url="http://localhost:54100",
|
||||
)
|
||||
SettingsCache.set(settings)
|
||||
|
||||
settings1 = SettingsCache.get()
|
||||
settings2 = SettingsCache.get()
|
||||
|
||||
assert settings1 is settings2
|
||||
|
||||
def test_clear_removes_cached_instance(self) -> None:
|
||||
"""Test that clear() removes the cached instance."""
|
||||
settings = EmbeddingSettings(
|
||||
default_backend="vllm",
|
||||
enable_vllm=True,
|
||||
enable_llamacpp=False,
|
||||
external_url="http://localhost:54100",
|
||||
)
|
||||
SettingsCache.set(settings)
|
||||
|
||||
SettingsCache.clear()
|
||||
|
||||
assert SettingsCache._instance is None
|
||||
124
ai_platform/modules/embeddings/tests/test_schemas.py
Normal file
124
ai_platform/modules/embeddings/tests/test_schemas.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
"""Tests for schemas module."""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from embeddings.schemas import (
|
||||
EmbeddingRequest,
|
||||
EmbeddingResponse,
|
||||
encode_embedding_base64,
|
||||
)
|
||||
from embeddings.types import BackendType, EmbeddingData, EmbeddingUsage
|
||||
|
||||
|
||||
class TestEmbeddingRequest:
|
||||
"""Tests for EmbeddingRequest schema."""
|
||||
|
||||
def test_single_string_input(self) -> None:
|
||||
"""Test that single string input is converted to list."""
|
||||
request = EmbeddingRequest(input="hello", model="test-model")
|
||||
assert request.input == ["hello"]
|
||||
|
||||
def test_list_input(self) -> None:
|
||||
"""Test list input is preserved."""
|
||||
request = EmbeddingRequest(input=["hello", "world"], model="test-model")
|
||||
assert request.input == ["hello", "world"]
|
||||
|
||||
def test_empty_input_rejected(self) -> None:
|
||||
"""Test that empty input is rejected."""
|
||||
with pytest.raises(ValidationError):
|
||||
EmbeddingRequest(input=[], model="test-model")
|
||||
|
||||
def test_empty_string_in_list_rejected(self) -> None:
|
||||
"""Test that empty strings in list are rejected."""
|
||||
with pytest.raises(ValidationError):
|
||||
EmbeddingRequest(input=["hello", ""], model="test-model")
|
||||
|
||||
def test_default_encoding_format(self) -> None:
|
||||
"""Test default encoding format is float."""
|
||||
request = EmbeddingRequest(input="hello", model="test-model")
|
||||
assert request.encoding_format == "float"
|
||||
|
||||
def test_base64_encoding_format(self) -> None:
|
||||
"""Test base64 encoding format."""
|
||||
request = EmbeddingRequest(
|
||||
input="hello", model="test-model", encoding_format="base64"
|
||||
)
|
||||
assert request.encoding_format == "base64"
|
||||
|
||||
def test_backend_override(self) -> None:
|
||||
"""Test backend override."""
|
||||
request = EmbeddingRequest(
|
||||
input="hello", model="test-model", backend=BackendType.LLAMACPP
|
||||
)
|
||||
assert request.backend == BackendType.LLAMACPP
|
||||
|
||||
def test_dimensions_parameter(self) -> None:
|
||||
"""Test dimensions parameter."""
|
||||
request = EmbeddingRequest(input="hello", model="test-model", dimensions=256)
|
||||
assert request.dimensions == 256
|
||||
|
||||
def test_invalid_dimensions(self) -> None:
|
||||
"""Test that invalid dimensions are rejected."""
|
||||
with pytest.raises(ValidationError):
|
||||
EmbeddingRequest(input="hello", model="test-model", dimensions=0)
|
||||
|
||||
|
||||
class TestEmbeddingResponse:
|
||||
"""Tests for EmbeddingResponse schema."""
|
||||
|
||||
def test_creation(self) -> None:
|
||||
"""Test creating response."""
|
||||
response = EmbeddingResponse(
|
||||
data=[EmbeddingData(index=0, embedding=[0.1, 0.2, 0.3])],
|
||||
model="test-model",
|
||||
usage=EmbeddingUsage(prompt_tokens=5, total_tokens=5),
|
||||
backend="vllm",
|
||||
)
|
||||
assert response.object == "list"
|
||||
assert len(response.data) == 1
|
||||
assert response.model == "test-model"
|
||||
assert response.backend == "vllm"
|
||||
|
||||
def test_multiple_embeddings(self) -> None:
|
||||
"""Test response with multiple embeddings."""
|
||||
response = EmbeddingResponse(
|
||||
data=[
|
||||
EmbeddingData(index=0, embedding=[0.1, 0.2]),
|
||||
EmbeddingData(index=1, embedding=[0.3, 0.4]),
|
||||
],
|
||||
model="test-model",
|
||||
usage=EmbeddingUsage(prompt_tokens=10, total_tokens=10),
|
||||
backend="vllm",
|
||||
)
|
||||
assert len(response.data) == 2
|
||||
assert response.data[0].index == 0
|
||||
assert response.data[1].index == 1
|
||||
|
||||
|
||||
class TestEncodeEmbeddingBase64:
|
||||
"""Tests for base64 encoding function."""
|
||||
|
||||
def test_encode_simple(self) -> None:
|
||||
"""Test encoding simple embedding."""
|
||||
embedding = [1.0, 2.0, 3.0]
|
||||
encoded = encode_embedding_base64(embedding)
|
||||
assert isinstance(encoded, str)
|
||||
# Should be base64 encoded
|
||||
assert len(encoded) > 0
|
||||
|
||||
def test_encode_decode_roundtrip(self) -> None:
|
||||
"""Test that encoding can be reversed."""
|
||||
import base64
|
||||
import struct
|
||||
|
||||
embedding = [0.1, 0.2, 0.3, 0.4, 0.5]
|
||||
encoded = encode_embedding_base64(embedding)
|
||||
|
||||
# Decode
|
||||
decoded_bytes = base64.b64decode(encoded)
|
||||
decoded = list(struct.unpack(f"<{len(embedding)}f", decoded_bytes))
|
||||
|
||||
# Compare with tolerance for float precision
|
||||
for original, decoded_val in zip(embedding, decoded):
|
||||
assert abs(original - decoded_val) < 1e-6
|
||||
98
ai_platform/modules/embeddings/tests/test_types.py
Normal file
98
ai_platform/modules/embeddings/tests/test_types.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
"""Tests for types module."""
|
||||
|
||||
import pytest
|
||||
|
||||
from embeddings.types import BackendType, EmbeddingData, EmbeddingUsage, ModelInfo
|
||||
|
||||
|
||||
class TestBackendType:
|
||||
"""Tests for BackendType enum."""
|
||||
|
||||
def test_vllm_value(self) -> None:
|
||||
"""Test vLLM backend type value."""
|
||||
assert BackendType.VLLM.value == "vllm"
|
||||
|
||||
def test_llamacpp_value(self) -> None:
|
||||
"""Test llama.cpp backend type value."""
|
||||
assert BackendType.LLAMACPP.value == "llamacpp"
|
||||
|
||||
def test_from_string(self) -> None:
|
||||
"""Test creating backend type from string."""
|
||||
assert BackendType("vllm") == BackendType.VLLM
|
||||
assert BackendType("llamacpp") == BackendType.LLAMACPP
|
||||
|
||||
def test_invalid_backend(self) -> None:
|
||||
"""Test that invalid backend raises ValueError."""
|
||||
with pytest.raises(ValueError):
|
||||
BackendType("invalid")
|
||||
|
||||
|
||||
class TestEmbeddingUsage:
|
||||
"""Tests for EmbeddingUsage model."""
|
||||
|
||||
def test_default_values(self) -> None:
|
||||
"""Test default values."""
|
||||
usage = EmbeddingUsage()
|
||||
assert usage.prompt_tokens == 0
|
||||
assert usage.total_tokens == 0
|
||||
|
||||
def test_with_values(self) -> None:
|
||||
"""Test with provided values."""
|
||||
usage = EmbeddingUsage(prompt_tokens=10, total_tokens=10)
|
||||
assert usage.prompt_tokens == 10
|
||||
assert usage.total_tokens == 10
|
||||
|
||||
|
||||
class TestEmbeddingData:
|
||||
"""Tests for EmbeddingData model."""
|
||||
|
||||
def test_creation(self) -> None:
|
||||
"""Test creating embedding data."""
|
||||
data = EmbeddingData(
|
||||
index=0,
|
||||
embedding=[0.1, 0.2, 0.3],
|
||||
)
|
||||
assert data.object == "embedding"
|
||||
assert data.index == 0
|
||||
assert data.embedding == [0.1, 0.2, 0.3]
|
||||
|
||||
def test_forbids_extra_fields(self) -> None:
|
||||
"""Test that extra fields are forbidden."""
|
||||
from pydantic import ValidationError
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
EmbeddingData(
|
||||
index=0,
|
||||
embedding=[0.1],
|
||||
extra_field="not allowed", # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
|
||||
class TestModelInfo:
|
||||
"""Tests for ModelInfo model."""
|
||||
|
||||
def test_required_fields(self) -> None:
|
||||
"""Test required fields."""
|
||||
model = ModelInfo(id="test-model", backend="vllm")
|
||||
assert model.id == "test-model"
|
||||
assert model.backend == "vllm"
|
||||
|
||||
def test_optional_fields(self) -> None:
|
||||
"""Test optional fields with defaults."""
|
||||
model = ModelInfo(id="test-model", backend="vllm")
|
||||
assert model.loaded is True
|
||||
assert model.dimensions is None
|
||||
assert model.max_input_tokens is None
|
||||
|
||||
def test_with_all_fields(self) -> None:
|
||||
"""Test with all fields provided."""
|
||||
model = ModelInfo(
|
||||
id="test-model",
|
||||
backend="vllm",
|
||||
loaded=False,
|
||||
dimensions=768,
|
||||
max_input_tokens=8192,
|
||||
)
|
||||
assert model.loaded is False
|
||||
assert model.dimensions == 768
|
||||
assert model.max_input_tokens == 8192
|
||||
Loading…
Add table
Add a link
Reference in a new issue