fix: prevent P2 from owning Qdrant lifecycle
This commit is contained in:
@@ -38,6 +38,7 @@ def build_vector_store(cfg: Config, *, require_write: bool = False) -> VectorSto
|
||||
workspace_id=cfg._workspace_id,
|
||||
workspace_revision=cfg._workspace_revision,
|
||||
expected_dimension=cfg.embeddings.dim if cfg.embeddings is not None else None,
|
||||
collection_lifecycle=resource.collection_lifecycle,
|
||||
)
|
||||
case other: # pragma: no cover - Pydantic's discriminator rejects this first.
|
||||
raise ConfigError(f"Adapter vector non supportato: {other}")
|
||||
|
||||
@@ -55,6 +55,7 @@ class QdrantVectorStore:
|
||||
workspace_id: str,
|
||||
workspace_revision: str | None = None,
|
||||
expected_dimension: int | None = None,
|
||||
collection_lifecycle: str = "create_if_missing",
|
||||
request: Callable[..., object] | None = None,
|
||||
connect_timeout: float = 2.0,
|
||||
read_timeout: float = 10.0,
|
||||
@@ -64,6 +65,9 @@ class QdrantVectorStore:
|
||||
self._workspace_id = workspace_id
|
||||
self._workspace_revision = workspace_revision
|
||||
self._expected_dimension = expected_dimension
|
||||
if collection_lifecycle not in ("create_if_missing", "require_existing"):
|
||||
raise ValueError("Unsupported Qdrant collection lifecycle")
|
||||
self._collection_lifecycle = collection_lifecycle
|
||||
self._request = request or requests.request
|
||||
self._timeout = (connect_timeout, read_timeout)
|
||||
|
||||
@@ -306,6 +310,8 @@ class QdrantVectorStore:
|
||||
if response is None:
|
||||
if not strict:
|
||||
raise VectorStoreError("Qdrant collection is missing")
|
||||
if self._collection_lifecycle == "require_existing":
|
||||
raise VectorStoreError("Qdrant collection configuration mismatch (semantic_index_incompatible)")
|
||||
self._call(
|
||||
"PUT",
|
||||
f"/collections/{self._collection}",
|
||||
@@ -328,11 +334,17 @@ class QdrantVectorStore:
|
||||
self._expected_dimension is not None
|
||||
and (size != self._expected_dimension or distance != "Cosine")
|
||||
):
|
||||
raise VectorStoreError("Qdrant collection configuration mismatch")
|
||||
raise VectorStoreError("Qdrant collection configuration mismatch (semantic_index_incompatible)")
|
||||
payload_schema = result.get("payload_schema")
|
||||
if not isinstance(payload_schema, dict):
|
||||
raise VectorStoreError("Qdrant returned malformed collection response")
|
||||
for field_name in _KEYWORD_INDEXES:
|
||||
if field_name not in result.get("payload_schema", {}):
|
||||
field = payload_schema.get(field_name)
|
||||
if not isinstance(field, dict) or field.get("data_type") != "keyword":
|
||||
if not strict:
|
||||
raise VectorStoreError("Qdrant collection payload indexes mismatch")
|
||||
if self._collection_lifecycle == "require_existing":
|
||||
raise VectorStoreError("Qdrant collection configuration mismatch (semantic_index_incompatible)")
|
||||
self._call(
|
||||
"PUT",
|
||||
f"/collections/{self._collection}/index",
|
||||
|
||||
+23
-1
@@ -222,6 +222,9 @@ class QdrantConfig(BaseModel):
|
||||
type: Literal["qdrant"]
|
||||
base_url: str
|
||||
collection: str = Field(min_length=1)
|
||||
# Internal runtime policy. Registry-rendered configs must not create or alter
|
||||
# the workspace-owned semantic collection; legacy configs retain compatibility.
|
||||
collection_lifecycle: Literal["create_if_missing", "require_existing"] = "create_if_missing"
|
||||
|
||||
|
||||
VectorResourceConfig = Annotated[
|
||||
@@ -547,6 +550,25 @@ def load_config(path: Path) -> Config:
|
||||
_validate_internal_embedding_contract(expanded, path)
|
||||
_validate_internal_vector_contract(expanded, path)
|
||||
translated, used_legacy = translate_legacy_config(expanded)
|
||||
vectors = translated.get("vectors")
|
||||
resource_vector = expanded.get("resources", {}).get("vector") if isinstance(
|
||||
expanded.get("resources"), dict
|
||||
) else None
|
||||
if (
|
||||
isinstance(vectors, dict)
|
||||
and vectors.get("type") == "qdrant"
|
||||
and isinstance(resource_vector, dict)
|
||||
and "collection_lifecycle" in resource_vector
|
||||
):
|
||||
vectors["collection_lifecycle"] = resource_vector["collection_lifecycle"]
|
||||
# runtime_identity is the registry marker. The lifecycle is an internal
|
||||
# runtime policy, never a descriptor-controlled option.
|
||||
if (
|
||||
isinstance(translated.get("runtime_identity"), dict)
|
||||
and isinstance(vectors, dict)
|
||||
and vectors.get("type") == "qdrant"
|
||||
):
|
||||
vectors["collection_lifecycle"] = "require_existing"
|
||||
_populate_legacy_views(translated)
|
||||
try:
|
||||
cfg = Config.model_validate(translated)
|
||||
@@ -662,7 +684,7 @@ def _validate_internal_vector_contract(raw: dict[str, Any], path: Path) -> None:
|
||||
engine = vector.get("engine")
|
||||
base_url = vector.get("base_url")
|
||||
collection = vector.get("collection")
|
||||
allowed = {"engine", "base_url", "collection"}
|
||||
allowed = {"engine", "base_url", "collection", "collection_lifecycle"}
|
||||
unexpected = sorted(set(vector) - allowed)
|
||||
if unexpected:
|
||||
raise ConfigError(
|
||||
|
||||
Reference in New Issue
Block a user