Implement task 014 assets and media library workflow
This commit is contained in:
@@ -13,6 +13,10 @@ from src.domain.contracts import (
|
||||
AgentJobStatus,
|
||||
AgentJobSummary,
|
||||
AgentJobType,
|
||||
AssetRevisionSummary,
|
||||
AssetStatus,
|
||||
AssetSummary,
|
||||
AssetType,
|
||||
ArticleSummary,
|
||||
BoundaryQuestionSummary,
|
||||
ClaimRiskLevel,
|
||||
@@ -55,6 +59,7 @@ class BackendRepository:
|
||||
self.boundary_questions = BoundaryQuestionsRepository(self)
|
||||
self.article_plans = ArticlePlansRepository(self)
|
||||
self.article_drafts = ArticleDraftsRepository(self)
|
||||
self.assets = AssetsRepository(self)
|
||||
self.research_manifests = ResearchManifestsRepository(self)
|
||||
self.evidence_items = EvidenceItemsRepository(self)
|
||||
self.claims = ClaimsRepository(self)
|
||||
@@ -1192,6 +1197,325 @@ class ArticleDraftsRepository:
|
||||
"""
|
||||
|
||||
|
||||
class AssetsRepository:
|
||||
def __init__(self, repository: BackendRepository) -> None:
|
||||
self._repository = repository
|
||||
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
section_id: UUID | None,
|
||||
asset_type: AssetType,
|
||||
title: str,
|
||||
prompt: str | None,
|
||||
object_key: str | None,
|
||||
file_url: str | None,
|
||||
alt_text: str | None,
|
||||
caption: str | None,
|
||||
status: AssetStatus,
|
||||
created_at: datetime,
|
||||
updated_at: datetime,
|
||||
) -> AssetSummary:
|
||||
asset_id = uuid4()
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
connection.execute(
|
||||
f"""
|
||||
INSERT INTO assets (
|
||||
id,
|
||||
article_id,
|
||||
section_id,
|
||||
asset_type,
|
||||
title,
|
||||
prompt,
|
||||
object_key,
|
||||
file_url,
|
||||
alt_text,
|
||||
caption,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}
|
||||
)
|
||||
""",
|
||||
(
|
||||
str(asset_id),
|
||||
str(article_id),
|
||||
_uuid_value(section_id),
|
||||
asset_type.value,
|
||||
title,
|
||||
prompt,
|
||||
object_key,
|
||||
file_url,
|
||||
alt_text,
|
||||
caption,
|
||||
status.value,
|
||||
_datetime_value(created_at),
|
||||
_datetime_value(updated_at),
|
||||
),
|
||||
)
|
||||
|
||||
return self.get(article_id=article_id, asset_id=asset_id)
|
||||
|
||||
def list_for_article(self, article_id: UUID) -> list[AssetSummary]:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT
|
||||
id,
|
||||
article_id,
|
||||
section_id,
|
||||
asset_type,
|
||||
title,
|
||||
prompt,
|
||||
object_key,
|
||||
file_url,
|
||||
alt_text,
|
||||
caption,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM assets
|
||||
WHERE article_id = {placeholder}
|
||||
ORDER BY created_at, id
|
||||
""",
|
||||
(str(article_id),),
|
||||
).fetchall()
|
||||
|
||||
history_by_asset = self._history_by_asset(article_id)
|
||||
assets: list[AssetSummary] = []
|
||||
for row in rows:
|
||||
asset = _asset_summary_from_row(
|
||||
row,
|
||||
history=history_by_asset.get(str(_row_value(row, "id")), []),
|
||||
)
|
||||
assets.append(asset)
|
||||
return assets
|
||||
|
||||
def get(self, *, article_id: UUID, asset_id: UUID) -> AssetSummary:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT
|
||||
id,
|
||||
article_id,
|
||||
section_id,
|
||||
asset_type,
|
||||
title,
|
||||
prompt,
|
||||
object_key,
|
||||
file_url,
|
||||
alt_text,
|
||||
caption,
|
||||
status,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM assets
|
||||
WHERE article_id = {placeholder} AND id = {placeholder}
|
||||
""",
|
||||
(str(article_id), str(asset_id)),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise LookupError(f"Asset not found: {asset_id}")
|
||||
history_by_asset = self._history_by_asset(article_id)
|
||||
return _asset_summary_from_row(
|
||||
row,
|
||||
history=history_by_asset.get(str(asset_id), []),
|
||||
)
|
||||
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
asset_id: UUID,
|
||||
section_id: UUID | None,
|
||||
title: str,
|
||||
prompt: str | None,
|
||||
object_key: str | None,
|
||||
file_url: str | None,
|
||||
alt_text: str | None,
|
||||
caption: str | None,
|
||||
status: AssetStatus,
|
||||
updated_at: datetime,
|
||||
) -> AssetSummary:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
connection.execute(
|
||||
f"""
|
||||
UPDATE assets
|
||||
SET
|
||||
section_id = {placeholder},
|
||||
title = {placeholder},
|
||||
prompt = {placeholder},
|
||||
object_key = {placeholder},
|
||||
file_url = {placeholder},
|
||||
alt_text = {placeholder},
|
||||
caption = {placeholder},
|
||||
status = {placeholder},
|
||||
updated_at = {placeholder}
|
||||
WHERE article_id = {placeholder} AND id = {placeholder}
|
||||
""",
|
||||
(
|
||||
_uuid_value(section_id),
|
||||
title,
|
||||
prompt,
|
||||
object_key,
|
||||
file_url,
|
||||
alt_text,
|
||||
caption,
|
||||
status.value,
|
||||
_datetime_value(updated_at),
|
||||
str(article_id),
|
||||
str(asset_id),
|
||||
),
|
||||
)
|
||||
|
||||
return self.get(article_id=article_id, asset_id=asset_id)
|
||||
|
||||
def next_revision_index(self, *, article_id: UUID, asset_id: UUID) -> int:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT COALESCE(MAX(revision_index), 0) + 1 AS next_revision
|
||||
FROM asset_revisions
|
||||
WHERE article_id = {placeholder} AND asset_id = {placeholder}
|
||||
""",
|
||||
(str(article_id), str(asset_id)),
|
||||
).fetchone()
|
||||
return int(_row_value(row, "next_revision"))
|
||||
|
||||
def create_revision(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
asset_id: UUID,
|
||||
revision_index: int,
|
||||
action: str,
|
||||
actor_user_id: UUID | None,
|
||||
payload: JsonObject,
|
||||
created_at: datetime,
|
||||
) -> AssetRevisionSummary:
|
||||
revision_id = uuid4()
|
||||
placeholder = self._repository.placeholder()
|
||||
json_cast = self._repository.json_cast()
|
||||
with self._repository.connection() as connection:
|
||||
connection.execute(
|
||||
f"""
|
||||
INSERT INTO asset_revisions (
|
||||
id,
|
||||
article_id,
|
||||
asset_id,
|
||||
revision_index,
|
||||
action,
|
||||
actor_user_id,
|
||||
payload,
|
||||
created_at
|
||||
)
|
||||
VALUES (
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}
|
||||
)
|
||||
""",
|
||||
(
|
||||
str(revision_id),
|
||||
str(article_id),
|
||||
str(asset_id),
|
||||
revision_index,
|
||||
action,
|
||||
_uuid_value(actor_user_id),
|
||||
_json_value(payload),
|
||||
_datetime_value(created_at),
|
||||
),
|
||||
)
|
||||
|
||||
return self.get_revision(
|
||||
article_id=article_id,
|
||||
asset_id=asset_id,
|
||||
revision_id=revision_id,
|
||||
)
|
||||
|
||||
def get_revision(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
asset_id: UUID,
|
||||
revision_id: UUID,
|
||||
) -> AssetRevisionSummary:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT
|
||||
id,
|
||||
article_id,
|
||||
asset_id,
|
||||
revision_index,
|
||||
action,
|
||||
actor_user_id,
|
||||
payload,
|
||||
created_at
|
||||
FROM asset_revisions
|
||||
WHERE article_id = {placeholder}
|
||||
AND asset_id = {placeholder}
|
||||
AND id = {placeholder}
|
||||
""",
|
||||
(str(article_id), str(asset_id), str(revision_id)),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise LookupError(f"Asset revision not found: {revision_id}")
|
||||
return _asset_revision_from_row(row)
|
||||
|
||||
def _history_by_asset(self, article_id: UUID) -> dict[str, list[AssetRevisionSummary]]:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT
|
||||
id,
|
||||
article_id,
|
||||
asset_id,
|
||||
revision_index,
|
||||
action,
|
||||
actor_user_id,
|
||||
payload,
|
||||
created_at
|
||||
FROM asset_revisions
|
||||
WHERE article_id = {placeholder}
|
||||
ORDER BY revision_index, created_at, id
|
||||
""",
|
||||
(str(article_id),),
|
||||
).fetchall()
|
||||
history: dict[str, list[AssetRevisionSummary]] = {}
|
||||
for row in rows:
|
||||
asset_id = str(_row_value(row, "asset_id"))
|
||||
history.setdefault(asset_id, []).append(_asset_revision_from_row(row))
|
||||
return history
|
||||
|
||||
|
||||
class ResearchManifestsRepository:
|
||||
def __init__(self, repository: BackendRepository) -> None:
|
||||
self._repository = repository
|
||||
@@ -2383,6 +2707,42 @@ def _draft_summary_from_row(row: Any) -> DraftSummary:
|
||||
)
|
||||
|
||||
|
||||
def _asset_revision_from_row(row: Any) -> AssetRevisionSummary:
|
||||
return AssetRevisionSummary(
|
||||
id=_row_value(row, "id"),
|
||||
article_id=_row_value(row, "article_id"),
|
||||
asset_id=_row_value(row, "asset_id"),
|
||||
revision_index=_row_value(row, "revision_index"),
|
||||
action=_row_value(row, "action"),
|
||||
actor_user_id=_row_value(row, "actor_user_id"),
|
||||
payload=_json_from_row(row, "payload"),
|
||||
created_at=_row_value(row, "created_at"),
|
||||
)
|
||||
|
||||
|
||||
def _asset_summary_from_row(
|
||||
row: Any,
|
||||
*,
|
||||
history: list[AssetRevisionSummary] | None = None,
|
||||
) -> AssetSummary:
|
||||
return AssetSummary(
|
||||
id=_row_value(row, "id"),
|
||||
article_id=_row_value(row, "article_id"),
|
||||
section_id=_row_value(row, "section_id"),
|
||||
asset_type=_row_value(row, "asset_type"),
|
||||
title=_row_value(row, "title"),
|
||||
prompt=_row_value(row, "prompt"),
|
||||
object_key=_row_value(row, "object_key"),
|
||||
file_url=_row_value(row, "file_url"),
|
||||
alt_text=_row_value(row, "alt_text"),
|
||||
caption=_row_value(row, "caption"),
|
||||
status=_row_value(row, "status"),
|
||||
history=history or [],
|
||||
created_at=_row_value(row, "created_at"),
|
||||
updated_at=_row_value(row, "updated_at"),
|
||||
)
|
||||
|
||||
|
||||
def _agent_job_summary_from_row(row: Any) -> AgentJobSummary:
|
||||
return AgentJobSummary(
|
||||
id=_row_value(row, "id"),
|
||||
|
||||
Reference in New Issue
Block a user