Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

181 lines
6.1 KiB
Python

"""
Base Embedding Adapter
=======================
Abstract base class for all embedding adapters.
Defines the contract that all embedding providers must implement.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
def looks_like_multimodal_embedding_model(model_name: Optional[str]) -> bool:
"""Best-effort guard for OpenAI-compatible multimodal embedding models."""
if not model_name:
return False
normalized = model_name.lower().replace("_", "-")
return any(
marker in normalized
for marker in (
"qwen3-vl-embedding",
"multimodal-embedding",
"vision-embedding",
"vl-embedding",
"image-embedding",
)
)
@dataclass
class EmbeddingRequest:
"""
Standard embedding request structure.
Provider-agnostic request format. Different providers interpret fields differently:
Args:
texts: List of texts to embed
model: Model name to use
dimensions: Embedding vector dimensions (optional)
input_type: Input type hint for task-aware embeddings (optional)
- Cohere: Maps to 'input_type' ("search_document", "search_query", "classification", "clustering")
- Jina: Maps to 'task' ("retrieval.passage", "retrieval.query", etc.)
- OpenAI/Ollama: Ignored
encoding_format: Output format ("float" or "base64", default: "float")
truncate: Whether to truncate texts that exceed max tokens (default: True)
normalized: Whether to return L2-normalized embeddings (Jina/Ollama only)
late_chunking: Enable late chunking for long context (Jina v3 only)
contents: Multimodal content list of dicts like
``[{"text": "..."}, {"image": "url|data: URI"}, {"video": "..."}]``.
Adapters that support multimodal (DashScope, SiliconFlow Qwen3-VL,
Cohere v4) consume this directly; text-only adapters MUST raise
``ValueError`` if it is set so the caller can route differently.
When ``contents`` is set, ``texts`` is ignored.
enable_fusion: DashScope-specific. ``True`` fuses all multimodal items
into one vector; ``False`` (or None) returns one vector per item.
"""
texts: List[str]
model: str
dimensions: Optional[int] = None
input_type: Optional[str] = None
encoding_format: Optional[str] = "float"
truncate: Optional[bool] = True
normalized: Optional[bool] = True
late_chunking: Optional[bool] = False
contents: Optional[List[Dict[str, Any]]] = None
enable_fusion: Optional[bool] = None
@dataclass
class EmbeddingResponse:
"""Standard embedding response structure."""
embeddings: List[List[float]]
model: str
dimensions: int
usage: Dict[str, Any]
class EmbeddingProviderError(RuntimeError):
"""Structured error raised by embedding adapters on provider failures.
Carries the HTTP status, response body excerpt, model name, and request
URL so downstream callers (task log streams, UI surfaces) can show
actionable diagnostics instead of a bare exception string.
"""
def __init__(
self,
message: str,
*,
status: Optional[int] = None,
body: Optional[str] = None,
model: Optional[str] = None,
url: Optional[str] = None,
provider: Optional[str] = None,
) -> None:
super().__init__(message)
self.status = status
self.body = body
self.model = model
self.url = url
self.provider = provider
def __str__(self) -> str: # noqa: D401 - succinct
parts = [super().__str__()]
if self.provider:
parts.append(f"provider={self.provider}")
if self.model:
parts.append(f"model={self.model}")
if self.status is not None:
parts.append(f"status={self.status}")
if self.url:
parts.append(f"url={self.url}")
if self.body:
snippet = self.body if len(self.body) <= 500 else self.body[:500] + "...(truncated)"
parts.append(f"body={snippet}")
return " | ".join(parts)
class BaseEmbeddingAdapter(ABC):
"""
Base class for all embedding adapters.
Each adapter implements the specific API interface for a provider
(OpenAI, Cohere, Ollama, etc.) while exposing a unified interface.
"""
def __init__(self, config: Dict[str, Any]):
"""
Initialize the adapter with configuration.
Args:
config: Dictionary containing:
- api_key: API authentication key (optional for local)
- base_url: API endpoint URL
- model: Model name to use
- dimensions: Embedding vector dimensions
- send_dimensions: Tri-state opt-in for the `dimensions`
request param. ``True`` always sends, ``False`` never
sends, ``None`` lets the adapter decide based on the
model family (default).
- request_timeout: Request timeout in seconds
"""
self.api_key = config.get("api_key")
self.base_url = config.get("base_url")
self.api_version = config.get("api_version")
self.model = config.get("model")
self.dimensions = config.get("dimensions")
self.send_dimensions: Optional[bool] = config.get("send_dimensions")
self.request_timeout = config.get("request_timeout", 60)
self.extra_headers = config.get("extra_headers") or {}
@abstractmethod
async def embed(self, request: EmbeddingRequest) -> EmbeddingResponse:
"""
Generate embeddings for a list of texts.
Args:
request: EmbeddingRequest with texts and parameters
Returns:
EmbeddingResponse with embeddings and metadata
Raises:
httpx.HTTPError: If the API request fails
"""
pass
@abstractmethod
def get_model_info(self) -> Dict[str, Any]:
"""
Return information about the configured model.
Returns:
Dictionary with model metadata (name, dimensions, etc.)
"""
pass