Task 009 implement plan generation review gate
This commit is contained in:
@@ -15,6 +15,9 @@ from src.domain.contracts import (
|
||||
AgentJobType,
|
||||
ArticleSummary,
|
||||
BoundaryQuestionSummary,
|
||||
PlanReviewStatus,
|
||||
PlanSectionSummary,
|
||||
PlanSummary,
|
||||
PublishingRules,
|
||||
PublishingStatus,
|
||||
ArticleWorkflowStatus,
|
||||
@@ -42,6 +45,7 @@ class BackendRepository:
|
||||
self.target_sites = TargetSitesRepository(self)
|
||||
self.articles = ArticlesRepository(self)
|
||||
self.boundary_questions = BoundaryQuestionsRepository(self)
|
||||
self.article_plans = ArticlePlansRepository(self)
|
||||
self.agent_jobs = AgentJobsRepository(self)
|
||||
self.script_config_versions = ScriptConfigVersionsRepository(self)
|
||||
self.script_config_version_events = ScriptConfigVersionAuditEventsRepository(self)
|
||||
@@ -750,6 +754,257 @@ class BoundaryQuestionsRepository:
|
||||
return self.get(article_id=article_id, question_id=question_id)
|
||||
|
||||
|
||||
class ArticlePlansRepository:
|
||||
def __init__(self, repository: BackendRepository) -> None:
|
||||
self._repository = repository
|
||||
|
||||
def create_version(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
version: int,
|
||||
status: PlanReviewStatus,
|
||||
title_options: list[str],
|
||||
recommended_title: str | None,
|
||||
reader_persona: str | None,
|
||||
search_intent: str | None,
|
||||
thesis: str | None,
|
||||
claims_to_prove: list[str],
|
||||
evidence_needs: list[str],
|
||||
visual_needs: list[str],
|
||||
seo_notes: list[str],
|
||||
source_requirements: list[str],
|
||||
excluded_sources: list[str],
|
||||
tone: str | None,
|
||||
audience: str | None,
|
||||
risks: list[str],
|
||||
sections: list[JsonObject],
|
||||
created_at: datetime,
|
||||
) -> PlanSummary:
|
||||
plan_id = uuid4()
|
||||
placeholder = self._repository.placeholder()
|
||||
json_cast = self._repository.json_cast()
|
||||
with self._repository.connection() as connection:
|
||||
connection.execute(
|
||||
f"""
|
||||
INSERT INTO article_plans (
|
||||
id,
|
||||
article_id,
|
||||
version,
|
||||
status,
|
||||
title_options,
|
||||
recommended_title,
|
||||
reader_persona,
|
||||
search_intent,
|
||||
thesis,
|
||||
claims_to_prove,
|
||||
evidence_needs,
|
||||
visual_needs,
|
||||
seo_notes,
|
||||
source_requirements,
|
||||
excluded_sources,
|
||||
tone,
|
||||
audience,
|
||||
risks,
|
||||
created_at
|
||||
)
|
||||
VALUES (
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}
|
||||
)
|
||||
""",
|
||||
(
|
||||
str(plan_id),
|
||||
str(article_id),
|
||||
version,
|
||||
status.value,
|
||||
_json_value(title_options),
|
||||
recommended_title,
|
||||
reader_persona,
|
||||
search_intent,
|
||||
thesis,
|
||||
_json_value(claims_to_prove),
|
||||
_json_value(evidence_needs),
|
||||
_json_value(visual_needs),
|
||||
_json_value(seo_notes),
|
||||
_json_value(source_requirements),
|
||||
_json_value(excluded_sources),
|
||||
tone,
|
||||
audience,
|
||||
_json_value(risks),
|
||||
_datetime_value(created_at),
|
||||
),
|
||||
)
|
||||
for index, section in enumerate(sections, start=1):
|
||||
connection.execute(
|
||||
f"""
|
||||
INSERT INTO plan_sections (
|
||||
id,
|
||||
article_plan_id,
|
||||
sort_order,
|
||||
heading,
|
||||
purpose,
|
||||
key_points,
|
||||
evidence_needs,
|
||||
claims_to_support,
|
||||
target_word_count
|
||||
)
|
||||
VALUES (
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}
|
||||
)
|
||||
""",
|
||||
(
|
||||
str(uuid4()),
|
||||
str(plan_id),
|
||||
index,
|
||||
section["heading"],
|
||||
section.get("purpose"),
|
||||
_json_value(section.get("key_points", [])),
|
||||
_json_value(section.get("evidence_needs", [])),
|
||||
_json_value(section.get("claims_to_support", [])),
|
||||
section.get("target_word_count"),
|
||||
),
|
||||
)
|
||||
|
||||
return self.get(article_id=article_id, plan_id=plan_id)
|
||||
|
||||
def list_for_article(self, article_id: UUID) -> list[PlanSummary]:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT {self._select_columns()}
|
||||
FROM article_plans
|
||||
WHERE article_id = {placeholder}
|
||||
ORDER BY version
|
||||
""",
|
||||
(str(article_id),),
|
||||
).fetchall()
|
||||
|
||||
return [self._with_sections(row) for row in rows]
|
||||
|
||||
def get(self, *, article_id: UUID, plan_id: UUID) -> PlanSummary:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT {self._select_columns()}
|
||||
FROM article_plans
|
||||
WHERE article_id = {placeholder} AND id = {placeholder}
|
||||
""",
|
||||
(str(article_id), str(plan_id)),
|
||||
).fetchone()
|
||||
|
||||
if row is None:
|
||||
raise LookupError(f"Plan not found: {plan_id}")
|
||||
return self._with_sections(row)
|
||||
|
||||
def latest_version(self, article_id: UUID) -> int:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT COALESCE(MAX(version), 0) AS version
|
||||
FROM article_plans
|
||||
WHERE article_id = {placeholder}
|
||||
""",
|
||||
(str(article_id),),
|
||||
).fetchone()
|
||||
return int(_row_value(row, "version"))
|
||||
|
||||
def update_status(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID,
|
||||
plan_id: UUID,
|
||||
status: PlanReviewStatus,
|
||||
) -> PlanSummary:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
connection.execute(
|
||||
f"""
|
||||
UPDATE article_plans
|
||||
SET status = {placeholder}
|
||||
WHERE article_id = {placeholder} AND id = {placeholder}
|
||||
""",
|
||||
(status.value, str(article_id), str(plan_id)),
|
||||
)
|
||||
return self.get(article_id=article_id, plan_id=plan_id)
|
||||
|
||||
def _with_sections(self, row: Any) -> PlanSummary:
|
||||
plan_id = _row_value(row, "id")
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
section_rows = connection.execute(
|
||||
f"""
|
||||
SELECT
|
||||
id,
|
||||
article_plan_id,
|
||||
sort_order,
|
||||
heading,
|
||||
purpose,
|
||||
key_points,
|
||||
evidence_needs,
|
||||
claims_to_support,
|
||||
target_word_count
|
||||
FROM plan_sections
|
||||
WHERE article_plan_id = {placeholder}
|
||||
ORDER BY sort_order
|
||||
""",
|
||||
(str(plan_id),),
|
||||
).fetchall()
|
||||
|
||||
return _plan_summary_from_row(row, [_plan_section_from_row(item) for item in section_rows])
|
||||
|
||||
def _select_columns(self) -> str:
|
||||
return """
|
||||
id,
|
||||
article_id,
|
||||
version,
|
||||
status,
|
||||
title_options,
|
||||
recommended_title,
|
||||
reader_persona,
|
||||
search_intent,
|
||||
thesis,
|
||||
claims_to_prove,
|
||||
evidence_needs,
|
||||
visual_needs,
|
||||
seo_notes,
|
||||
source_requirements,
|
||||
excluded_sources,
|
||||
tone,
|
||||
audience,
|
||||
risks,
|
||||
created_at
|
||||
"""
|
||||
|
||||
|
||||
class AgentJobsRepository:
|
||||
def __init__(self, repository: BackendRepository) -> None:
|
||||
self._repository = repository
|
||||
@@ -1418,6 +1673,48 @@ def _boundary_question_from_row(row: Any) -> BoundaryQuestionSummary:
|
||||
)
|
||||
|
||||
|
||||
def _plan_section_from_row(row: Any) -> PlanSectionSummary:
|
||||
return PlanSectionSummary(
|
||||
id=_row_value(row, "id"),
|
||||
article_plan_id=_row_value(row, "article_plan_id"),
|
||||
sort_order=_row_value(row, "sort_order"),
|
||||
heading=_row_value(row, "heading"),
|
||||
purpose=_row_value(row, "purpose"),
|
||||
key_points=_json_from_row(row, "key_points"),
|
||||
evidence_needs=_json_from_row(row, "evidence_needs"),
|
||||
claims_to_support=_json_from_row(row, "claims_to_support"),
|
||||
target_word_count=_row_value(row, "target_word_count"),
|
||||
)
|
||||
|
||||
|
||||
def _plan_summary_from_row(
|
||||
row: Any,
|
||||
sections: list[PlanSectionSummary],
|
||||
) -> PlanSummary:
|
||||
return PlanSummary(
|
||||
id=_row_value(row, "id"),
|
||||
article_id=_row_value(row, "article_id"),
|
||||
version=_row_value(row, "version"),
|
||||
status=_row_value(row, "status"),
|
||||
title_options=_json_from_row(row, "title_options"),
|
||||
recommended_title=_row_value(row, "recommended_title"),
|
||||
reader_persona=_row_value(row, "reader_persona"),
|
||||
search_intent=_row_value(row, "search_intent"),
|
||||
thesis=_row_value(row, "thesis"),
|
||||
sections=sections,
|
||||
claims_to_prove=_json_from_row(row, "claims_to_prove"),
|
||||
evidence_needs=_json_from_row(row, "evidence_needs"),
|
||||
visual_needs=_json_from_row(row, "visual_needs"),
|
||||
seo_notes=_json_from_row(row, "seo_notes"),
|
||||
source_requirements=_json_from_row(row, "source_requirements"),
|
||||
excluded_sources=_json_from_row(row, "excluded_sources"),
|
||||
tone=_row_value(row, "tone"),
|
||||
audience=_row_value(row, "audience"),
|
||||
risks=_json_from_row(row, "risks"),
|
||||
created_at=_row_value(row, "created_at"),
|
||||
)
|
||||
|
||||
|
||||
def _agent_job_summary_from_row(row: Any) -> AgentJobSummary:
|
||||
return AgentJobSummary(
|
||||
id=_row_value(row, "id"),
|
||||
|
||||
Reference in New Issue
Block a user