183 lines
6.1 KiB
Python
183 lines
6.1 KiB
Python
# Copyright 2025-present the zvec project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import ClassVar, Optional
|
|
|
|
from ..common.constants import TEXT
|
|
from ..tool import require_module
|
|
|
|
|
|
class JinaFunctionBase:
|
|
"""Base class for Jina AI functions.
|
|
|
|
This base class provides common functionality for calling Jina AI APIs
|
|
and handling responses. It supports embeddings (dense) operations via
|
|
the OpenAI-compatible Jina Embeddings API.
|
|
|
|
This class is not meant to be used directly. Use concrete implementations:
|
|
- ``JinaDenseEmbedding`` for dense embeddings
|
|
|
|
Args:
|
|
model (str): Jina embedding model identifier.
|
|
api_key (Optional[str]): Jina API authentication key.
|
|
task (Optional[str]): Task type for the embedding model.
|
|
|
|
Note:
|
|
- This is an internal base class for code reuse across Jina features
|
|
- Subclasses should inherit from appropriate Protocol
|
|
- Provides unified API connection and response handling
|
|
- Jina API is OpenAI-compatible, using the ``openai`` Python client
|
|
"""
|
|
|
|
_BASE_URL: ClassVar[str] = "https://api.jina.ai/v1"
|
|
|
|
# Model default dimensions
|
|
_MODEL_DIMENSIONS: ClassVar[dict[str, int]] = {
|
|
"jina-embeddings-v5-text-nano": 768,
|
|
"jina-embeddings-v5-text-small": 1024,
|
|
}
|
|
|
|
# Model max tokens
|
|
_MODEL_MAX_TOKENS: ClassVar[dict[str, int]] = {
|
|
"jina-embeddings-v5-text-nano": 8192,
|
|
"jina-embeddings-v5-text-small": 32768,
|
|
}
|
|
|
|
# Valid task types
|
|
_VALID_TASKS: ClassVar[tuple[str, ...]] = (
|
|
"retrieval.query",
|
|
"retrieval.passage",
|
|
"text-matching",
|
|
"classification",
|
|
"separation",
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
model: str,
|
|
api_key: Optional[str] = None,
|
|
task: Optional[str] = None,
|
|
):
|
|
"""Initialize the base Jina functionality.
|
|
|
|
Args:
|
|
model (str): Jina model name.
|
|
api_key (Optional[str]): API key or None to use environment variable.
|
|
task (Optional[str]): Task type for the embedding model.
|
|
Valid values: "retrieval.query", "retrieval.passage",
|
|
"text-matching", "classification", "separation".
|
|
|
|
Raises:
|
|
ValueError: If API key is not provided and not in environment,
|
|
or if task is not a valid task type.
|
|
"""
|
|
self._model = model
|
|
self._api_key = api_key or os.environ.get("JINA_API_KEY")
|
|
self._task = task
|
|
|
|
if not self._api_key:
|
|
raise ValueError(
|
|
"Jina API key is required. Please provide 'api_key' parameter "
|
|
"or set the 'JINA_API_KEY' environment variable. "
|
|
"Get your key from: https://jina.ai/api-dashboard"
|
|
)
|
|
|
|
if task is not None and task not in self._VALID_TASKS:
|
|
raise ValueError(
|
|
f"Invalid task '{task}'. Valid tasks: {', '.join(self._VALID_TASKS)}"
|
|
)
|
|
|
|
@property
|
|
def model(self) -> str:
|
|
"""str: The Jina model name currently in use."""
|
|
return self._model
|
|
|
|
@property
|
|
def task(self) -> Optional[str]:
|
|
"""Optional[str]: The task type for the embedding model."""
|
|
return self._task
|
|
|
|
def _get_client(self):
|
|
"""Get OpenAI-compatible client instance configured for Jina API.
|
|
|
|
Returns:
|
|
OpenAI: Configured OpenAI client pointing to Jina API.
|
|
|
|
Raises:
|
|
ImportError: If openai package is not installed.
|
|
"""
|
|
openai = require_module("openai")
|
|
return openai.OpenAI(api_key=self._api_key, base_url=self._BASE_URL)
|
|
|
|
def _call_text_embedding_api(
|
|
self,
|
|
input: TEXT,
|
|
dimension: Optional[int] = None,
|
|
) -> list:
|
|
"""Call Jina Embeddings API.
|
|
|
|
Args:
|
|
input (TEXT): Input text to embed.
|
|
dimension (Optional[int]): Target dimension for Matryoshka embeddings.
|
|
|
|
Returns:
|
|
list: Embedding vector as list of floats.
|
|
|
|
Raises:
|
|
RuntimeError: If API call fails.
|
|
ValueError: If API returns error response.
|
|
"""
|
|
try:
|
|
client = self._get_client()
|
|
|
|
# Prepare embedding parameters
|
|
params = {"model": self.model, "input": input}
|
|
|
|
# Add dimension parameter for Matryoshka support
|
|
if dimension is not None:
|
|
params["dimensions"] = dimension
|
|
|
|
# Add task parameter via extra_body
|
|
if self._task is not None:
|
|
params["extra_body"] = {"task": self._task}
|
|
|
|
# Call Jina API (OpenAI-compatible)
|
|
response = client.embeddings.create(**params)
|
|
|
|
except Exception as e:
|
|
# Check if it's an OpenAI API error
|
|
openai = require_module("openai")
|
|
if isinstance(e, (openai.APIError, openai.APIConnectionError)):
|
|
raise RuntimeError(f"Failed to call Jina API: {e!s}") from e
|
|
raise RuntimeError(f"Unexpected error during API call: {e!s}") from e
|
|
|
|
# Extract embedding from response
|
|
try:
|
|
if not response.data:
|
|
raise ValueError("Invalid API response: no embedding data returned")
|
|
|
|
embedding_vector = response.data[0].embedding
|
|
|
|
if not isinstance(embedding_vector, list):
|
|
raise ValueError(
|
|
"Invalid API response: embedding is not a list of numbers"
|
|
)
|
|
|
|
return embedding_vector
|
|
|
|
except (AttributeError, IndexError, TypeError) as e:
|
|
raise ValueError(f"Failed to parse API response: {e!s}") from e
|