Task 007 implement agent job queue and fake runner path
This commit is contained in:
@@ -9,6 +9,10 @@ from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from src.domain.contracts import (
|
||||
AgentJobErrorCategory,
|
||||
AgentJobStatus,
|
||||
AgentJobSummary,
|
||||
AgentJobType,
|
||||
ArticleSummary,
|
||||
PublishingRules,
|
||||
PublishingStatus,
|
||||
@@ -36,6 +40,7 @@ class BackendRepository:
|
||||
self.users = UsersRepository(self)
|
||||
self.target_sites = TargetSitesRepository(self)
|
||||
self.articles = ArticlesRepository(self)
|
||||
self.agent_jobs = AgentJobsRepository(self)
|
||||
self.script_config_versions = ScriptConfigVersionsRepository(self)
|
||||
self.script_config_version_events = ScriptConfigVersionAuditEventsRepository(self)
|
||||
self.schema = SchemaRepository(self)
|
||||
@@ -588,6 +593,233 @@ class ArticlesRepository:
|
||||
return [_workflow_event_from_row(row) for row in rows]
|
||||
|
||||
|
||||
class AgentJobsRepository:
|
||||
def __init__(self, repository: BackendRepository) -> None:
|
||||
self._repository = repository
|
||||
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
article_id: UUID | None,
|
||||
parent_job_id: UUID | None,
|
||||
attempt: int,
|
||||
job_type: AgentJobType,
|
||||
agent_profile: str,
|
||||
status: AgentJobStatus,
|
||||
input_files: list[JsonObject],
|
||||
queued_at: datetime,
|
||||
) -> AgentJobSummary:
|
||||
job_id = uuid4()
|
||||
placeholder = self._repository.placeholder()
|
||||
json_cast = self._repository.json_cast()
|
||||
sql = f"""
|
||||
INSERT INTO agent_jobs (
|
||||
id,
|
||||
article_id,
|
||||
parent_job_id,
|
||||
attempt,
|
||||
job_type,
|
||||
agent_profile,
|
||||
status,
|
||||
input_files,
|
||||
queued_at
|
||||
)
|
||||
VALUES (
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder},
|
||||
{placeholder}{json_cast},
|
||||
{placeholder}
|
||||
)
|
||||
"""
|
||||
with self._repository.connection() as connection:
|
||||
connection.execute(
|
||||
sql,
|
||||
(
|
||||
str(job_id),
|
||||
_uuid_value(article_id),
|
||||
_uuid_value(parent_job_id),
|
||||
attempt,
|
||||
job_type.value,
|
||||
agent_profile,
|
||||
status.value,
|
||||
_json_value(input_files),
|
||||
_datetime_value(queued_at),
|
||||
),
|
||||
)
|
||||
|
||||
return self.get(job_id)
|
||||
|
||||
def list(self) -> list[AgentJobSummary]:
|
||||
with self._repository.connection() as connection:
|
||||
rows = connection.execute(
|
||||
f"""
|
||||
SELECT {self._select_columns()}
|
||||
FROM agent_jobs
|
||||
ORDER BY queued_at DESC
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
return [_agent_job_summary_from_row(row) for row in rows]
|
||||
|
||||
def get(self, job_id: UUID) -> AgentJobSummary:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT {self._select_columns()}
|
||||
FROM agent_jobs
|
||||
WHERE id = {placeholder}
|
||||
""",
|
||||
(str(job_id),),
|
||||
).fetchone()
|
||||
|
||||
if row is None:
|
||||
raise LookupError(f"Agent job not found: {job_id}")
|
||||
return _agent_job_summary_from_row(row)
|
||||
|
||||
def claim_next_queued(self, *, started_at: datetime) -> AgentJobSummary | None:
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT id
|
||||
FROM agent_jobs
|
||||
WHERE status = {placeholder}
|
||||
ORDER BY queued_at
|
||||
LIMIT 1
|
||||
""",
|
||||
(AgentJobStatus.QUEUED.value,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
job_id = _row_value(row, "id")
|
||||
connection.execute(
|
||||
f"""
|
||||
UPDATE agent_jobs
|
||||
SET status = {placeholder}, started_at = {placeholder}
|
||||
WHERE id = {placeholder} AND status = {placeholder}
|
||||
""",
|
||||
(
|
||||
AgentJobStatus.RUNNING.value,
|
||||
_datetime_value(started_at),
|
||||
str(job_id),
|
||||
AgentJobStatus.QUEUED.value,
|
||||
),
|
||||
)
|
||||
|
||||
return self.get(UUID(str(job_id)))
|
||||
|
||||
def complete(
|
||||
self,
|
||||
*,
|
||||
job_id: UUID,
|
||||
status: AgentJobStatus,
|
||||
workspace_path: str | None,
|
||||
output_files: list[JsonObject],
|
||||
error_category: AgentJobErrorCategory | None,
|
||||
error_message: str | None,
|
||||
stdout: str,
|
||||
stderr: str,
|
||||
exit_code: int | None,
|
||||
duration_ms: int | None,
|
||||
finished_at: datetime,
|
||||
) -> AgentJobSummary:
|
||||
existing = self.get(job_id)
|
||||
if existing.status == AgentJobStatus.CANCELLED:
|
||||
return existing
|
||||
|
||||
placeholder = self._repository.placeholder()
|
||||
json_cast = self._repository.json_cast()
|
||||
with self._repository.connection() as connection:
|
||||
connection.execute(
|
||||
f"""
|
||||
UPDATE agent_jobs
|
||||
SET
|
||||
status = {placeholder},
|
||||
workspace_path = {placeholder},
|
||||
output_files = {placeholder}{json_cast},
|
||||
error_category = {placeholder},
|
||||
error_message = {placeholder},
|
||||
stdout = {placeholder},
|
||||
stderr = {placeholder},
|
||||
exit_code = {placeholder},
|
||||
duration_ms = {placeholder},
|
||||
finished_at = {placeholder}
|
||||
WHERE id = {placeholder}
|
||||
""",
|
||||
(
|
||||
status.value,
|
||||
workspace_path,
|
||||
_json_value(output_files),
|
||||
_agent_error_category_value(error_category),
|
||||
error_message,
|
||||
stdout,
|
||||
stderr,
|
||||
exit_code,
|
||||
duration_ms,
|
||||
_datetime_value(finished_at),
|
||||
str(job_id),
|
||||
),
|
||||
)
|
||||
|
||||
return self.get(job_id)
|
||||
|
||||
def cancel(self, *, job_id: UUID, finished_at: datetime) -> AgentJobSummary:
|
||||
existing = self.get(job_id)
|
||||
if existing.status in {AgentJobStatus.SUCCEEDED, AgentJobStatus.FAILED}:
|
||||
return existing
|
||||
|
||||
placeholder = self._repository.placeholder()
|
||||
with self._repository.connection() as connection:
|
||||
connection.execute(
|
||||
f"""
|
||||
UPDATE agent_jobs
|
||||
SET
|
||||
status = {placeholder},
|
||||
error_message = {placeholder},
|
||||
finished_at = {placeholder}
|
||||
WHERE id = {placeholder}
|
||||
""",
|
||||
(
|
||||
AgentJobStatus.CANCELLED.value,
|
||||
"Cancelled",
|
||||
_datetime_value(finished_at),
|
||||
str(job_id),
|
||||
),
|
||||
)
|
||||
|
||||
return self.get(job_id)
|
||||
|
||||
def _select_columns(self) -> str:
|
||||
return """
|
||||
id,
|
||||
article_id,
|
||||
parent_job_id,
|
||||
attempt,
|
||||
job_type,
|
||||
agent_profile,
|
||||
status,
|
||||
workspace_path,
|
||||
input_files,
|
||||
output_files,
|
||||
error_category,
|
||||
error_message,
|
||||
stdout,
|
||||
stderr,
|
||||
exit_code,
|
||||
duration_ms,
|
||||
queued_at,
|
||||
started_at,
|
||||
finished_at
|
||||
"""
|
||||
|
||||
|
||||
class ScriptConfigVersionsRepository:
|
||||
def __init__(self, repository: BackendRepository) -> None:
|
||||
self._repository = repository
|
||||
@@ -1015,6 +1247,30 @@ def _workflow_event_from_row(row: Any) -> WorkflowEventSummary:
|
||||
)
|
||||
|
||||
|
||||
def _agent_job_summary_from_row(row: Any) -> AgentJobSummary:
|
||||
return AgentJobSummary(
|
||||
id=_row_value(row, "id"),
|
||||
article_id=_row_value(row, "article_id"),
|
||||
parent_job_id=_row_value(row, "parent_job_id"),
|
||||
attempt=_row_value(row, "attempt"),
|
||||
job_type=_row_value(row, "job_type"),
|
||||
agent_profile=_row_value(row, "agent_profile"),
|
||||
status=_row_value(row, "status"),
|
||||
workspace_path=_row_value(row, "workspace_path"),
|
||||
input_files=_json_from_row(row, "input_files"),
|
||||
output_files=_json_from_row(row, "output_files"),
|
||||
error_category=_row_value(row, "error_category"),
|
||||
error_message=_row_value(row, "error_message"),
|
||||
stdout=_row_value(row, "stdout"),
|
||||
stderr=_row_value(row, "stderr"),
|
||||
exit_code=_row_value(row, "exit_code"),
|
||||
duration_ms=_row_value(row, "duration_ms"),
|
||||
queued_at=_row_value(row, "queued_at"),
|
||||
started_at=_row_value(row, "started_at"),
|
||||
finished_at=_row_value(row, "finished_at"),
|
||||
)
|
||||
|
||||
|
||||
def _script_config_version_event_from_row(row: Any) -> dict[str, Any]:
|
||||
return {
|
||||
"id": _row_value(row, "id"),
|
||||
@@ -1079,3 +1335,9 @@ def _article_status_value(value: ArticleWorkflowStatus | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return value.value
|
||||
|
||||
|
||||
def _agent_error_category_value(value: AgentJobErrorCategory | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return value.value
|
||||
|
||||
Reference in New Issue
Block a user