feat(phase7): add asset classification workflow

This commit is contained in:
Xiaoxia AI
2026-06-17 18:29:48 +08:00
parent 8aeb46aded
commit db1ebc4e9d
12 changed files with 484 additions and 330 deletions
@@ -2,6 +2,7 @@
from .asset_library_repository import SQLAlchemyAssetLibraryRepository
from .asset_repository import SQLAlchemyAssetRepository
from .classification_job_repository import SQLAlchemyClassificationJobRepository
from .ingest_job_repository import SQLAlchemyIngestJobRepository
from .project_repository import SQLAlchemyProjectRepository
from .session import Base, build_engine, build_session_factory, ensure_database_exists, initialize_database
@@ -10,6 +11,7 @@ __all__ = [
"Base",
"SQLAlchemyAssetLibraryRepository",
"SQLAlchemyAssetRepository",
"SQLAlchemyClassificationJobRepository",
"SQLAlchemyIngestJobRepository",
"SQLAlchemyProjectRepository",
"build_engine",
@@ -11,34 +11,59 @@ class SQLAlchemyAssetRepository:
self.session = session
def list_by_library(self, library_id: str) -> list[Asset]:
models = self.session.query(AssetModel).filter(AssetModel.library_id == library_id).all()
return [
Asset(
id=model.id,
workspace_id=model.workspace_id,
project_id=model.project_id,
library_id=model.library_id,
name=model.name,
storage_key=model.storage_key,
mime_type=model.mime_type,
metadata=json.loads(model.metadata_json),
created_at=model.created_at,
)
for model in models
]
models = self.session.query(AssetModel).filter(AssetModel.asset_library_id == library_id).all()
return [self._to_domain(model) for model in models]
def get(self, asset_id: str) -> Asset | None:
model = self.session.query(AssetModel).filter(AssetModel.id == asset_id).first()
if model is None:
return None
return self._to_domain(model)
def create(self, asset: Asset) -> Asset:
model = AssetModel(
id=asset.id,
workspace_id=asset.workspace_id,
project_id=asset.project_id,
library_id=asset.library_id,
asset_library_id=asset.library_id,
name=asset.name,
storage_key=asset.storage_key,
mime_type=asset.mime_type,
metadata_json=json.dumps(asset.metadata),
file_type=asset.mime_type.split('/')[0] if '/' in asset.mime_type else asset.mime_type,
file_size=0,
file_url=asset.storage_key,
uploaded_by_user_id='system',
classification_result=json.dumps(asset.metadata),
created_at=asset.created_at,
updated_at=asset.created_at,
)
self.session.add(model)
self.session.commit()
return asset
def update(self, asset: Asset) -> Asset:
model = self.session.query(AssetModel).filter(AssetModel.id == asset.id).first()
if model is None:
raise ValueError(f"Asset {asset.id} not found")
model.name = asset.name
model.classification_result = json.dumps(asset.metadata)
model.updated_at = asset.created_at
self.session.commit()
return asset
def _to_domain(self, model: AssetModel) -> Asset:
metadata = {}
if model.classification_result:
try:
metadata = json.loads(model.classification_result)
except Exception:
metadata = {}
return Asset(
id=model.id,
workspace_id=model.workspace_id,
project_id=model.project_id,
library_id=model.asset_library_id,
name=model.name,
storage_key=model.file_url,
mime_type=model.file_type,
metadata=metadata,
created_at=model.created_at,
)
@@ -0,0 +1,55 @@
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.models import ClassificationJobModel
from packages.domain import ClassificationJob, ClassificationJobStatus
class SQLAlchemyClassificationJobRepository:
def __init__(self, session: Session):
self.session = session
def create(self, job: ClassificationJob) -> ClassificationJob:
model = ClassificationJobModel(
id=job.id,
workspace_id=job.workspace_id,
project_id=job.project_id,
asset_id=job.asset_id,
status=job.status.value,
classification=job.classification,
confidence=job.confidence,
error_message=job.error_message,
created_at=job.created_at,
updated_at=job.updated_at,
)
self.session.add(model)
self.session.commit()
return job
def get(self, job_id: str) -> ClassificationJob | None:
model = self.session.query(ClassificationJobModel).filter(ClassificationJobModel.id == job_id).first()
if model is None:
return None
return ClassificationJob(
id=model.id,
workspace_id=model.workspace_id,
project_id=model.project_id,
asset_id=model.asset_id,
status=ClassificationJobStatus(model.status),
classification=model.classification,
confidence=model.confidence,
error_message=model.error_message,
created_at=model.created_at,
updated_at=model.updated_at,
)
def update(self, job: ClassificationJob) -> ClassificationJob:
model = self.session.query(ClassificationJobModel).filter(ClassificationJobModel.id == job.id).first()
if model is None:
raise ValueError(f"ClassificationJob {job.id} not found")
model.status = job.status.value
model.classification = job.classification
model.confidence = job.confidence
model.error_message = job.error_message
model.updated_at = job.updated_at
self.session.commit()
return job
@@ -70,6 +70,21 @@ class IngestJobModel(Base):
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class ClassificationJobModel(Base):
__tablename__ = "classification_jobs"
id = Column(String(32), primary_key=True)
workspace_id = Column(String(32), nullable=False, index=True)
project_id = Column(String(32), nullable=False, index=True)
asset_id = Column(String(32), nullable=False, index=True)
status = Column(String(20), nullable=False, default="pending")
classification = Column(String(50), nullable=False, default="")
confidence = Column(Float, nullable=False, default=0.0)
error_message = Column(Text, nullable=False, default="")
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class TaskModel(Base):
__tablename__ = "tasks"