Spaces:
Paused
Paused
| # -*- coding: utf-8 -*- | |
| # pylint: disable=protected-access | |
| """Unit tests for GeminiEmbeddingModel.""" | |
| from dataclasses import asdict | |
| from typing import Any | |
| from unittest import IsolatedAsyncioTestCase | |
| from unittest.mock import AsyncMock | |
| from utils import AnyValue | |
| from agentscope.embedding import ( | |
| GeminiEmbeddingModel, | |
| EmbeddingResponse, | |
| EmbeddingUsage, | |
| ) | |
| from agentscope.message import DataBlock, Base64Source | |
| A = AnyValue() | |
| def _mock_resp(embeddings: list[list[float]]) -> EmbeddingResponse: | |
| """Create a mock EmbeddingResponse.""" | |
| return EmbeddingResponse( | |
| embeddings=embeddings, | |
| usage=EmbeddingUsage(tokens=len(embeddings), time=0.01), | |
| ) | |
| def _img() -> DataBlock: | |
| """Create a test image DataBlock.""" | |
| return DataBlock( | |
| source=Base64Source(data="aWltYWdl", media_type="image/png"), | |
| ) | |
| class GeminiListModelsTest(IsolatedAsyncioTestCase): | |
| """Test list_models for Gemini.""" | |
| async def test_list_models(self) -> None: | |
| """Should list 2 models.""" | |
| cards = GeminiEmbeddingModel.list_models() | |
| names = sorted(c.name for c in cards) | |
| self.assertEqual(names, ["gemini-embedding-001", "gemini-embedding-2"]) | |
| async def test_text_model_card(self) -> None: | |
| """gemini-embedding-001 is text-only with 2048 context.""" | |
| cards = GeminiEmbeddingModel.list_models() | |
| card = next(c for c in cards if c.name == "gemini-embedding-001") | |
| self.assertDictEqual( | |
| card.model_dump(), | |
| { | |
| "type": "embedding_model", | |
| "name": "gemini-embedding-001", | |
| "label": "Gemini Embedding 001", | |
| "status": "active", | |
| "input_types": ["text/plain"], | |
| "output_types": ["application/x-embedding"], | |
| "dimensions": 3072, | |
| "supported_dimensions": [3072, 1536, 768, 512, 256, 128], | |
| "context_size": 2048, | |
| "parameter_schema": { | |
| "type": "object", | |
| "properties": {}, | |
| "required": [], | |
| }, | |
| "parameter_overrides": {}, | |
| }, | |
| ) | |
| async def test_multimodal_model_card(self) -> None: | |
| """gemini-embedding-2 is multimodal with 8192 context.""" | |
| cards = GeminiEmbeddingModel.list_models() | |
| card = next(c for c in cards if c.name == "gemini-embedding-2") | |
| self.assertIn("image/png", card.input_types) | |
| self.assertIn("application/pdf", card.input_types) | |
| self.assertEqual(card.context_size, 8192) | |
| self.assertEqual(card.supported_dimensions, [3072, 1536, 768]) | |
| class GeminiTextCallTest(IsolatedAsyncioTestCase): | |
| """Test Gemini text embedding via mocked _call_text.""" | |
| def _make_text_model(self) -> GeminiEmbeddingModel: | |
| """Create a text-mode model bypassing __init__ (no genai).""" | |
| model = GeminiEmbeddingModel.__new__(GeminiEmbeddingModel) | |
| model.model = "gemini-embedding-001" | |
| model.dimensions = 2 | |
| model.context_size = 2048 | |
| model.batch_size = 100 | |
| model.max_retries = 3 | |
| model.retry_delay = 1.0 | |
| model._is_multimodal = False | |
| model.embedding_cache = None | |
| return model | |
| async def test_text_call(self) -> None: | |
| """Text mode delegates to _call_text.""" | |
| model = self._make_text_model() | |
| model._call_text = AsyncMock( | |
| return_value=_mock_resp([[0.1, 0.2], [0.3, 0.4]]), | |
| ) | |
| result = await model(["hello", "world"]) | |
| self.assertDictEqual( | |
| asdict(result), | |
| { | |
| "embeddings": [[0.1, 0.2], [0.3, 0.4]], | |
| "id": A, | |
| "created_at": A, | |
| "type": "embedding", | |
| "usage": {"tokens": 2, "time": 0.01, "type": "embedding"}, | |
| "source": "api", | |
| }, | |
| ) | |
| async def test_text_rejects_datablock(self) -> None: | |
| """Text mode rejects DataBlock inputs.""" | |
| model = self._make_text_model() | |
| with self.assertRaises(ValueError): | |
| await GeminiEmbeddingModel._call_text(model, [_img()]) | |
| class GeminiMultimodalCallTest(IsolatedAsyncioTestCase): | |
| """Test Gemini multimodal embedding via mocked _call_multimodal.""" | |
| def _make_multimodal_model(self) -> GeminiEmbeddingModel: | |
| """Create a multimodal-mode model bypassing __init__.""" | |
| model = GeminiEmbeddingModel.__new__(GeminiEmbeddingModel) | |
| model.model = "gemini-embedding-2" | |
| model.dimensions = 1 | |
| model.context_size = 8192 | |
| model.batch_size = 100 | |
| model.max_retries = 3 | |
| model.retry_delay = 1.0 | |
| model._is_multimodal = True | |
| model.embedding_cache = None | |
| from agentscope.embedding._gemini._model import _MultimodalLimits | |
| model._limits = _MultimodalLimits( | |
| max_elements=20, | |
| max_images=6, | |
| max_videos=1, | |
| max_audios=1, | |
| max_pdfs=1, | |
| ) | |
| return model | |
| async def test_multimodal_delegates(self) -> None: | |
| """Multimodal mode delegates to _call_multimodal.""" | |
| model = self._make_multimodal_model() | |
| model._call_multimodal = AsyncMock( | |
| return_value=_mock_resp([[0.1], [0.2]]), | |
| ) | |
| result = await model(["hello", "world"]) | |
| self.assertDictEqual( | |
| asdict(result), | |
| { | |
| "embeddings": [[0.1], [0.2]], | |
| "id": A, | |
| "created_at": A, | |
| "type": "embedding", | |
| "usage": {"tokens": 2, "time": 0.01, "type": "embedding"}, | |
| "source": "api", | |
| }, | |
| ) | |
| async def test_multimodal_batching_by_image_limit(self) -> None: | |
| """8 images with max_images=6 produces 2 batches (6+2).""" | |
| model = self._make_multimodal_model() | |
| call_count = 0 | |
| async def _mock(inputs: list, **_kw: Any) -> EmbeddingResponse: | |
| nonlocal call_count | |
| call_count += 1 | |
| return _mock_resp([[0.1]] * len(inputs)) | |
| model._call_multimodal = _mock # type: ignore[assignment] | |
| result = await model([_img() for _ in range(8)]) | |
| self.assertEqual(result["embeddings"], [[0.1]] * 8) | |
| self.assertEqual(call_count, 2) | |