vibe/vibe/core/config/schema.py
Mathias Gesbert 3b8f65b306
v2.9.2 (#645)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai>
Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-04-29 19:32:15 +02:00

141 lines
4.5 KiB
Python

from __future__ import annotations
from dataclasses import dataclass, field
from pydantic import BaseModel
from pydantic.fields import FieldInfo
from vibe.core.utils.merge import MergeStrategy
class DuplicateMergeMetadataError(TypeError):
"""Raised when a field declares more than one MergeFieldMetadata marker."""
class ConfigDefinitionError(TypeError):
"""Raised when a config schema or fragment is declared with invalid fields."""
class ConfigSchema(BaseModel):
"""Base for composite config schemas composed of fragments and merge-aware fields."""
@classmethod
def __pydantic_on_complete__(cls) -> None:
super().__pydantic_on_complete__()
if cls.__name__ == "ConfigSchema" and cls.__module__ == __name__:
return
for field_name, field_info in cls.model_fields.items():
has_merge_metadata = MergeFieldMetadata.from_field(field_info) is not None
is_fragment = isinstance(field_info.annotation, type) and issubclass(
field_info.annotation, ConfigFragment
)
if is_fragment and has_merge_metadata:
raise ConfigDefinitionError(
f"{cls.__name__}.{field_name} is a ConfigFragment field and "
"must not declare merge metadata"
)
if is_fragment or has_merge_metadata:
continue
raise ConfigDefinitionError(
f"{cls.__name__}.{field_name} must declare merge metadata or use a "
"ConfigFragment subclass"
)
class ConfigFragment(BaseModel):
"""Base for domain config groups with merge-aware top-level fields."""
@classmethod
def __pydantic_on_complete__(cls) -> None:
super().__pydantic_on_complete__()
if cls.__name__ == "ConfigFragment" and cls.__module__ == __name__:
return
for field_name, field_info in cls.model_fields.items():
if MergeFieldMetadata.from_field(field_info) is not None:
continue
raise ConfigDefinitionError(
f"{cls.__name__}.{field_name} must declare merge metadata via "
"MergeFieldMetadata"
)
@dataclass(frozen=True)
class MergeFieldMetadata:
"""Base Pydantic Annotated marker that declares how a config field merges across layers.
Usage::
active_model: Annotated[str, WithReplaceMerge()] = "devstral-2"
models: Annotated[list[M], WithUnionMerge(merge_key="alias")]
Use the concrete subclasses (``WithReplaceMerge``, ``WithConcatMerge``, etc.)
rather than instantiating this base class directly.
"""
merge_strategy: MergeStrategy
merge_key: str | None = None
@classmethod
def from_field(cls, field_info: FieldInfo) -> MergeFieldMetadata | None:
"""Extract MergeFieldMetadata from a FieldInfo's metadata, if present.
Raises DuplicateMergeMetadataError if more than one marker is found.
"""
result: MergeFieldMetadata | None = None
for item in field_info.metadata:
if isinstance(item, cls):
if result is not None:
raise DuplicateMergeMetadataError(
f"Field has multiple MergeFieldMetadata markers: "
f"{result} and {item}"
)
result = item
return result
@dataclass(frozen=True)
class WithReplaceMerge(MergeFieldMetadata):
"""Higher layer wins outright."""
merge_strategy: MergeStrategy = field(default=MergeStrategy.REPLACE, init=False)
@dataclass(frozen=True)
class WithConcatMerge(MergeFieldMetadata):
"""Lists appended in layer order."""
merge_strategy: MergeStrategy = field(default=MergeStrategy.CONCAT, init=False)
@dataclass(frozen=True)
class WithUnionMerge(MergeFieldMetadata):
"""Lists merged by key, higher layer wins per-key."""
merge_strategy: MergeStrategy = field(default=MergeStrategy.UNION, init=False)
merge_key: str = ""
def __post_init__(self) -> None:
if not self.merge_key:
raise TypeError("WithUnionMerge requires merge_key")
@dataclass(frozen=True)
class WithShallowMerge(MergeFieldMetadata):
"""Dicts shallow-merged, absent keys preserved."""
merge_strategy: MergeStrategy = field(default=MergeStrategy.MERGE, init=False)
@dataclass(frozen=True)
class WithConflictMerge(MergeFieldMetadata):
"""Raises error if more than one layer provides a value."""
merge_strategy: MergeStrategy = field(default=MergeStrategy.CONFLICT, init=False)