# -*- coding: utf-8 -*- """The embedding model card class.""" from __future__ import annotations import copy from typing import Literal, Self, Type import yaml from pydantic import BaseModel, Field class EmbeddingModelCard(BaseModel): """A card describing an embedding model's capabilities. Mirrors :class:`~agentscope.model.ModelCard` but tailored for embedding models. Uses ``input_types`` / ``output_types`` to describe model capabilities, and ``parameter_schema`` (built from the embedding class's ``Parameters`` + YAML ``parameter_overrides``) to tell the frontend which knobs the user can adjust. The output type ``application/x-embedding`` indicates that the model produces dense vector embeddings. """ type: Literal["embedding_model"] = "embedding_model" """The card type, always ``"embedding_model"``.""" name: str = Field(description="The model name used in API calls.") """The model name (e.g. ``"text-embedding-3-small"``).""" label: str = Field(description="Human-readable label for the frontend.") """Display label (e.g. ``"Text Embedding 3 Small"``).""" status: Literal["active", "deprecated", "sunset"] = Field( default="active", description="The model lifecycle status.", ) """The model status.""" input_types: list[str] = Field( default=["text/plain"], description="Supported input media types.", ) """Supported input types (e.g. ``["text/plain"]``, ``["text/plain", "image/jpeg", "image/png"]``).""" output_types: list[str] = Field( default=["application/x-embedding"], description="Supported output media types.", ) """Output types. ``application/x-embedding`` for vector output.""" dimensions: int = Field( ..., description="Default output vector dimensions for this model.", gt=0, ) """The default output dimensions for this model. First-class top-level field — kept outside of :attr:`parameter_schema` so that callers can rely on a strongly typed ``int`` rather than the soft ``parameter_schema['properties'] ['dimensions']['default']`` lookup. """ supported_dimensions: list[int] | None = Field( default=None, description=( "If set, the only dimensions this model can produce. " "``None`` means dimensions are fixed at " ":attr:`dimensions` and cannot be overridden." ), ) """Optional set of allowed output dimensions. Set for Matryoshka-style models (e.g. OpenAI's ``text-embedding-3-*``) that can be truncated to a smaller size. ``None`` indicates a fixed-dimension model. """ context_size: int | None = Field( default=None, description="Maximum input length (in tokens) per request.", gt=0, ) """Maximum input context size, if known.""" parameter_schema: dict = Field( default_factory=dict, description=( "JSON Schema for user-configurable parameters " "(built from the Parameters class + YAML overrides)." ), ) """The parameter schema sent to the frontend for form rendering. Empty ``properties`` means nothing to configure (e.g. fixed dimensions).""" parameter_overrides: dict[str, dict] = Field( default_factory=dict, description="Raw parameter overrides from the YAML file.", ) """The raw parameter overrides, preserved for reference.""" @classmethod def from_yaml( cls, yaml_path: str, parameter_class: Type[BaseModel], ) -> Self: """Load an embedding model card from a YAML file. Merges the base ``parameter_class`` JSON Schema with ``parameter_overrides`` from the YAML — identical to the approach used by :meth:`~agentscope.model.ModelCard.from_yaml`. Args: yaml_path (`str`): Path to the YAML file. parameter_class (`Type[BaseModel]`): The ``Parameters`` class from the embedding model subclass. Returns: `EmbeddingModelCard`: The loaded model card. """ with open(yaml_path, "r", encoding="utf-8") as f: config = yaml.safe_load(f) if "dimensions" not in config: raise ValueError( f"Embedding model card {yaml_path!r} is missing the " f"required top-level 'dimensions' field.", ) # Build parameter schema from the Parameters class base_schema = parameter_class.model_json_schema() properties = copy.deepcopy(base_schema.get("properties", {})) # Apply parameter_overrides (same logic as ModelCard.from_yaml) overrides = config.get("parameter_overrides", {}) for param_name, override in overrides.items(): if override is None: # null means remove properties.pop(param_name, None) continue if isinstance(override, dict): if override.get("hidden"): properties.pop(param_name, None) continue # Simple dict merge if param_name in properties: properties[param_name] = { **properties[param_name], **override, } final_schema = { "type": "object", "properties": properties, "required": base_schema.get("required", []), } return cls( name=config["name"], label=config["label"], status=config.get("status", "active"), input_types=config.get("input_types", ["text/plain"]), output_types=config.get( "output_types", ["application/x-embedding"], ), dimensions=config["dimensions"], supported_dimensions=config.get("supported_dimensions"), context_size=config.get("context_size"), parameter_schema=final_schema, parameter_overrides=overrides, )