task015 implement seo and language review flow
This commit is contained in:
@@ -13,6 +13,10 @@ from src.domain.contracts import (
|
||||
AgentJobStatus,
|
||||
AgentJobSummary,
|
||||
AgentJobType,
|
||||
ContentReviewKind,
|
||||
ContentReviewIssueSummary,
|
||||
ContentReviewReportSummary,
|
||||
ContentReviewSuggestionSummary,
|
||||
AssetRevisionSummary,
|
||||
AssetStatus,
|
||||
AssetSummary,
|
||||
@@ -33,6 +37,7 @@ from src.domain.contracts import (
|
||||
ResearchArtifactManifestSummary,
|
||||
ResearchArtifactSummary,
|
||||
ArticleWorkflowStatus,
|
||||
ReviewSuggestionStatus,
|
||||
WorkflowEventSummary,
|
||||
Role,
|
||||
ScriptConfigVersionStatus,
|
||||
@@ -59,6 +64,7 @@ class BackendRepository:
|
||||
self.boundary_questions = BoundaryQuestionsRepository(self)
|
||||
self.article_plans = ArticlePlansRepository(self)
|
||||
self.article_drafts = ArticleDraftsRepository(self)
|
||||
self.content_reviews = ContentReviewsRepository(self)
|
||||
self.assets = AssetsRepository(self)
|
||||
self.research_manifests = ResearchManifestsRepository(self)
|
||||
self.evidence_items = EvidenceItemsRepository(self)
|
||||
@@ -1197,6 +1203,418 @@ class ArticleDraftsRepository:
|
||||
"""
|
||||
|
||||
|
||||
class ContentReviewsRepository:
|
||||
def __init__(self, repository: BackendRepository) -> None:
|
||||
self._repository = repository
|
||||
|
||||
def create_report(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
review_kind: ContentReviewKind,
|
||||
draft_id: UUID,
|
||||
score: int,
|
||||
recommended_slug: str,
|
||||
recommended_title: str,
|
||||
schema_json: JsonObject,
|
||||
rules_snapshot: JsonObject,
|
||||
created_at: datetime,
|
||||
) -> ContentReviewReportSummary:
|
||||
report_id = uuid4()
|
||||
placeholder = self._repository.placeholder()
|
||||
json_cast = self._repository.json_cast()
|
||||
with self._repository.connection() as connection:
|
||||
connection.execute(
|
||||
f"""
|
||||
INSERT INTO article_review_reports (
|
||||
id,
|
||||
article_id,
|
||||
review_kind,
|
||||
draft_id,
|
||||
score,
|
||||
issues,
|
||||
recommended_slug,
|
||||
recommended_title,
|
||||
schema_json,
|
||||
rules_snapshot,
|
||||
created_at
|
||||
)
|
||||
VALUES (
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}
|
||||
)
|
||||
""",
|
||||
(
|
||||
str(report_id),
|
||||
str(article_id),
|
||||
review_kind.value,
|
||||
str(draft_id),
|
||||
score,
|
||||
_json_value([]),
|
||||
recommended_slug,
|
||||
recommended_title,
|
||||
_json_value(schema_json),
|
||||
_json_value(rules_snapshot),
|
||||
_datetime_value(created_at),
|
||||
),
|
||||
)
|
||||
return self.get_report(article_id=article_id, report_id=report_id)
|
||||
|
||||
def list_reports_for_article(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
review_kind: ContentReviewKind,
|
||||
) -> list[ContentReviewReportSummary]:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT
|
||||
id,
|
||||
article_id,
|
||||
review_kind,
|
||||
draft_id,
|
||||
score,
|
||||
recommended_slug,
|
||||
recommended_title,
|
||||
schema_json,
|
||||
rules_snapshot,
|
||||
created_at
|
||||
FROM article_review_reports
|
||||
WHERE article_id = {placeholder}
|
||||
AND review_kind = {placeholder}
|
||||
ORDER BY created_at DESC
|
||||
""",
|
||||
(str(article_id), review_kind.value),
|
||||
).fetchall()
|
||||
return [self.get_report(article_id=article_id, report_id=_row_value(row, "id")) for row in rows]
|
||||
|
||||
def latest_report(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
review_kind: ContentReviewKind,
|
||||
) -> ContentReviewReportSummary | None:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT id
|
||||
FROM article_review_reports
|
||||
WHERE article_id = {placeholder}
|
||||
AND review_kind = {placeholder}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(str(article_id), review_kind.value),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
return self.get_report(article_id=article_id, report_id=_row_value(row, "id"))
|
||||
|
||||
def get_report(self, *, article_id: UUID, report_id: UUID) -> ContentReviewReportSummary:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT
|
||||
id,
|
||||
article_id,
|
||||
review_kind,
|
||||
draft_id,
|
||||
score,
|
||||
recommended_slug,
|
||||
recommended_title,
|
||||
schema_json,
|
||||
rules_snapshot,
|
||||
created_at
|
||||
FROM article_review_reports
|
||||
WHERE article_id = {placeholder}
|
||||
AND id = {placeholder}
|
||||
""",
|
||||
(str(article_id), str(report_id)),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise LookupError(f"Content review report not found: {report_id}")
|
||||
suggestions = self.list_suggestions_for_report(
|
||||
article_id=article_id,
|
||||
report_id=report_id,
|
||||
)
|
||||
issues = [
|
||||
ContentReviewIssueSummary(
|
||||
id=item.suggestion_key,
|
||||
suggestion_id=item.id,
|
||||
severity=item.severity,
|
||||
location=item.location,
|
||||
message=item.message,
|
||||
suggested_fix=item.suggested_fix,
|
||||
suggested_rewrite=item.suggested_rewrite,
|
||||
status=item.status,
|
||||
)
|
||||
for item in suggestions
|
||||
]
|
||||
return _content_review_report_from_row(row, issues=issues)
|
||||
|
||||
def upsert_suggestion(
|
||||
self,
|
||||
*,
|
||||
suggestion_id: UUID,
|
||||
article_id: UUID,
|
||||
review_kind: ContentReviewKind,
|
||||
report_id: UUID,
|
||||
suggestion_key: str,
|
||||
severity: str,
|
||||
location: str,
|
||||
message: str,
|
||||
suggested_fix: str | None,
|
||||
suggested_rewrite: str | None,
|
||||
patch: JsonObject,
|
||||
created_at: datetime,
|
||||
updated_at: datetime,
|
||||
) -> ContentReviewSuggestionSummary:
|
||||
placeholder = self._repository.placeholder()
|
||||
json_cast = self._repository.json_cast()
|
||||
with self._repository.connection() as connection:
|
||||
connection.execute(
|
||||
f"""
|
||||
INSERT INTO article_review_suggestions (
|
||||
id,
|
||||
article_id,
|
||||
review_kind,
|
||||
report_id,
|
||||
suggestion_key,
|
||||
severity,
|
||||
location,
|
||||
message,
|
||||
suggested_fix,
|
||||
suggested_rewrite,
|
||||
patch,
|
||||
status,
|
||||
applied_text,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}
|
||||
)
|
||||
ON CONFLICT (article_id, review_kind, suggestion_key) DO UPDATE SET
|
||||
report_id = excluded.report_id,
|
||||
severity = excluded.severity,
|
||||
location = excluded.location,
|
||||
message = excluded.message,
|
||||
suggested_fix = excluded.suggested_fix,
|
||||
suggested_rewrite = excluded.suggested_rewrite,
|
||||
patch = excluded.patch,
|
||||
status = CASE
|
||||
WHEN article_review_suggestions.status = 'PENDING' THEN excluded.status
|
||||
ELSE article_review_suggestions.status
|
||||
END,
|
||||
applied_text = CASE
|
||||
WHEN article_review_suggestions.status = 'PENDING' THEN excluded.applied_text
|
||||
ELSE article_review_suggestions.applied_text
|
||||
END,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(
|
||||
str(suggestion_id),
|
||||
str(article_id),
|
||||
review_kind.value,
|
||||
str(report_id),
|
||||
suggestion_key,
|
||||
severity,
|
||||
location,
|
||||
message,
|
||||
suggested_fix,
|
||||
suggested_rewrite,
|
||||
_json_value(patch),
|
||||
ReviewSuggestionStatus.PENDING.value,
|
||||
None,
|
||||
_datetime_value(created_at),
|
||||
_datetime_value(updated_at),
|
||||
),
|
||||
)
|
||||
return self.get_suggestion(
|
||||
article_id=article_id,
|
||||
review_kind=review_kind,
|
||||
suggestion_id=suggestion_id,
|
||||
)
|
||||
|
||||
def get_suggestion(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
review_kind: ContentReviewKind,
|
||||
suggestion_id: UUID,
|
||||
) -> ContentReviewSuggestionSummary:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT
|
||||
id,
|
||||
article_id,
|
||||
review_kind,
|
||||
report_id,
|
||||
suggestion_key,
|
||||
severity,
|
||||
location,
|
||||
message,
|
||||
suggested_fix,
|
||||
suggested_rewrite,
|
||||
patch,
|
||||
status,
|
||||
applied_text,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM article_review_suggestions
|
||||
WHERE article_id = {placeholder}
|
||||
AND review_kind = {placeholder}
|
||||
AND id = {placeholder}
|
||||
""",
|
||||
(
|
||||
str(article_id),
|
||||
review_kind.value,
|
||||
str(suggestion_id),
|
||||
),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise LookupError(f"Content review suggestion not found: {suggestion_id}")
|
||||
return _content_review_suggestion_from_row(row)
|
||||
|
||||
def list_suggestions_for_report(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
report_id: UUID,
|
||||
) -> list[ContentReviewSuggestionSummary]:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT
|
||||
id,
|
||||
article_id,
|
||||
review_kind,
|
||||
report_id,
|
||||
suggestion_key,
|
||||
severity,
|
||||
location,
|
||||
message,
|
||||
suggested_fix,
|
||||
suggested_rewrite,
|
||||
patch,
|
||||
status,
|
||||
applied_text,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM article_review_suggestions
|
||||
WHERE article_id = {placeholder}
|
||||
AND report_id = {placeholder}
|
||||
ORDER BY suggestion_key
|
||||
""",
|
||||
(str(article_id), str(report_id)),
|
||||
).fetchall()
|
||||
return [_content_review_suggestion_from_row(row) for row in rows]
|
||||
|
||||
def list_unresolved_for_article(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
) -> list[ContentReviewSuggestionSummary]:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT
|
||||
id,
|
||||
article_id,
|
||||
review_kind,
|
||||
report_id,
|
||||
suggestion_key,
|
||||
severity,
|
||||
location,
|
||||
message,
|
||||
suggested_fix,
|
||||
suggested_rewrite,
|
||||
patch,
|
||||
status,
|
||||
applied_text,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM article_review_suggestions
|
||||
WHERE article_id = {placeholder}
|
||||
AND status = {placeholder}
|
||||
ORDER BY review_kind, suggestion_key
|
||||
""",
|
||||
(str(article_id), ReviewSuggestionStatus.PENDING.value),
|
||||
).fetchall()
|
||||
return [_content_review_suggestion_from_row(row) for row in rows]
|
||||
|
||||
def update_suggestion(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
review_kind: ContentReviewKind,
|
||||
suggestion_id: UUID,
|
||||
status: ReviewSuggestionStatus,
|
||||
applied_text: str | None,
|
||||
updated_at: datetime,
|
||||
) -> ContentReviewSuggestionSummary:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
cursor = connection.execute(
|
||||
f"""
|
||||
UPDATE article_review_suggestions
|
||||
SET
|
||||
status = {placeholder},
|
||||
applied_text = {placeholder},
|
||||
updated_at = {placeholder}
|
||||
WHERE article_id = {placeholder}
|
||||
AND review_kind = {placeholder}
|
||||
AND id = {placeholder}
|
||||
""",
|
||||
(
|
||||
status.value,
|
||||
applied_text,
|
||||
_datetime_value(updated_at),
|
||||
str(article_id),
|
||||
review_kind.value,
|
||||
str(suggestion_id),
|
||||
),
|
||||
)
|
||||
if int(getattr(cursor, "rowcount", 0)) <= 0:
|
||||
raise LookupError(f"Content review suggestion not found: {suggestion_id}")
|
||||
return self.get_suggestion(
|
||||
article_id=article_id,
|
||||
review_kind=review_kind,
|
||||
suggestion_id=suggestion_id,
|
||||
)
|
||||
|
||||
|
||||
class AssetsRepository:
|
||||
def __init__(self, repository: BackendRepository) -> None:
|
||||
self._repository = repository
|
||||
@@ -2707,6 +3125,46 @@ def _draft_summary_from_row(row: Any) -> DraftSummary:
|
||||
)
|
||||
|
||||
|
||||
def _content_review_report_from_row(
|
||||
row: Any,
|
||||
*,
|
||||
issues: list[ContentReviewIssueSummary],
|
||||
) -> ContentReviewReportSummary:
|
||||
return ContentReviewReportSummary(
|
||||
id=_row_value(row, "id"),
|
||||
article_id=_row_value(row, "article_id"),
|
||||
review_kind=_row_value(row, "review_kind"),
|
||||
draft_id=_row_value(row, "draft_id"),
|
||||
score=_row_value(row, "score"),
|
||||
issues=issues,
|
||||
recommended_slug=_row_value(row, "recommended_slug"),
|
||||
recommended_title=_row_value(row, "recommended_title"),
|
||||
schema_json=_json_from_row(row, "schema_json"),
|
||||
rules_snapshot=_json_from_row(row, "rules_snapshot"),
|
||||
created_at=_row_value(row, "created_at"),
|
||||
)
|
||||
|
||||
|
||||
def _content_review_suggestion_from_row(row: Any) -> ContentReviewSuggestionSummary:
|
||||
return ContentReviewSuggestionSummary(
|
||||
id=_row_value(row, "id"),
|
||||
article_id=_row_value(row, "article_id"),
|
||||
review_kind=_row_value(row, "review_kind"),
|
||||
report_id=_row_value(row, "report_id"),
|
||||
suggestion_key=_row_value(row, "suggestion_key"),
|
||||
severity=_row_value(row, "severity"),
|
||||
location=_row_value(row, "location"),
|
||||
message=_row_value(row, "message"),
|
||||
suggested_fix=_row_value(row, "suggested_fix"),
|
||||
suggested_rewrite=_row_value(row, "suggested_rewrite"),
|
||||
patch=_json_from_row(row, "patch"),
|
||||
status=_row_value(row, "status"),
|
||||
applied_text=_row_value(row, "applied_text"),
|
||||
created_at=_row_value(row, "created_at"),
|
||||
updated_at=_row_value(row, "updated_at"),
|
||||
)
|
||||
|
||||
|
||||
def _asset_revision_from_row(row: Any) -> AssetRevisionSummary:
|
||||
return AssetRevisionSummary(
|
||||
id=_row_value(row, "id"),
|
||||
|
||||
Reference in New Issue
Block a user