feat: add asset manifest knowledge ingest
This commit is contained in:
@@ -6,8 +6,7 @@ import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
# ---------------- Health ----------------
|
||||
|
||||
@@ -39,6 +38,123 @@ class IngestFolderResponse(BaseModel):
|
||||
invalid_files: int
|
||||
|
||||
|
||||
AssetKind = Literal["document", "image", "audio", "video", "archive", "dataset", "other"]
|
||||
KnowledgeIngestStatus = Literal["accepted", "duplicate", "rejected"]
|
||||
|
||||
|
||||
def _looks_like_local_path(value: str) -> bool:
|
||||
stripped = value.strip()
|
||||
if not stripped:
|
||||
return False
|
||||
lower = stripped.lower()
|
||||
if lower.startswith("file://"):
|
||||
return True
|
||||
if stripped.startswith(("/", "\\")):
|
||||
return True
|
||||
if len(stripped) >= 3 and stripped[1] == ":" and stripped[2] in {"\\", "/"}:
|
||||
return stripped[0].isalpha()
|
||||
if "\\" in stripped:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _iter_string_values(value: Any, prefix: str = ""):
|
||||
if isinstance(value, str):
|
||||
yield prefix, value
|
||||
return
|
||||
if isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
child_prefix = f"{prefix}.{key}" if prefix else str(key)
|
||||
yield from _iter_string_values(child, child_prefix)
|
||||
return
|
||||
if isinstance(value, list):
|
||||
for idx, child in enumerate(value):
|
||||
yield from _iter_string_values(child, f"{prefix}[{idx}]")
|
||||
|
||||
|
||||
class AssetIdentity(BaseModel):
|
||||
asset_id: str = Field(..., min_length=1)
|
||||
owner_module: str = Field(..., min_length=1)
|
||||
owner_record_type: str = Field(..., min_length=1)
|
||||
owner_record_id: str = Field(..., min_length=1)
|
||||
asset_kind: AssetKind
|
||||
title: str | None = None
|
||||
original_filename: str | None = None
|
||||
content_type: str = Field(..., min_length=1)
|
||||
size_bytes: int = Field(..., ge=0)
|
||||
sha256: str = Field(..., min_length=64, max_length=64, pattern=r"^[0-9a-fA-F]{64}$")
|
||||
created_at: datetime | None = None
|
||||
created_by: str | None = None
|
||||
|
||||
|
||||
class AssetStorageRef(BaseModel):
|
||||
provider: str = Field(..., min_length=1)
|
||||
bucket: str = Field(..., min_length=1)
|
||||
object_key: str = Field(..., min_length=1)
|
||||
version_id: str | None = None
|
||||
region: str | None = None
|
||||
kms_key_id: str | None = None
|
||||
|
||||
|
||||
class AssetSecurityState(BaseModel):
|
||||
gate_status: str = Field(..., min_length=1)
|
||||
classification: str | None = None
|
||||
av_status: str | None = None
|
||||
content_scan_status: str | None = None
|
||||
pii_status: str | None = None
|
||||
approved_at: datetime | None = None
|
||||
approved_by: str | None = None
|
||||
quarantine_reason: str | None = None
|
||||
|
||||
|
||||
class AssetRetentionState(BaseModel):
|
||||
policy_id: str = Field(..., min_length=1)
|
||||
retain_until: datetime | None = None
|
||||
legal_hold: bool
|
||||
legal_hold_reason: str | None = None
|
||||
|
||||
|
||||
class AssetManifest(BaseModel):
|
||||
manifest_version: str = Field(..., min_length=1)
|
||||
asset: AssetIdentity
|
||||
storage: AssetStorageRef
|
||||
security: AssetSecurityState
|
||||
retention: AssetRetentionState
|
||||
derivatives: list[dict[str, Any]] = Field(default_factory=list)
|
||||
links: list[dict[str, Any]] = Field(default_factory=list)
|
||||
source: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def reject_local_filesystem_paths(self) -> AssetManifest:
|
||||
dumped = self.model_dump(mode="json")
|
||||
for path, value in _iter_string_values(dumped):
|
||||
if _looks_like_local_path(value):
|
||||
raise ValueError(
|
||||
f"AssetManifest MUST NOT contain local filesystem paths: {path}"
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
class KnowledgeIngestOptions(BaseModel):
|
||||
generate_derivatives: bool = True
|
||||
index_full_text: bool = True
|
||||
index_vectors: bool = True
|
||||
emit_events: bool = True
|
||||
|
||||
|
||||
class AssetManifestEnvelope(BaseModel):
|
||||
manifest: AssetManifest
|
||||
ingest_options: KnowledgeIngestOptions = Field(default_factory=KnowledgeIngestOptions)
|
||||
|
||||
|
||||
class KnowledgeIngestResponse(BaseModel):
|
||||
status: KnowledgeIngestStatus
|
||||
ingest_job_id: uuid.UUID | None = None
|
||||
asset_id: str
|
||||
idempotency_key: str | None = None
|
||||
reason_code: str | None = None
|
||||
|
||||
|
||||
class DocumentSummary(BaseModel):
|
||||
id: uuid.UUID
|
||||
original_file_name: str
|
||||
|
||||
Reference in New Issue
Block a user