Implement task 014 assets and media library workflow

This commit is contained in:
2026-05-21 23:55:15 +03:00
parent c3914bf8be
commit 8243f20b0e
23 changed files with 2423 additions and 22 deletions
@@ -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"),