Expand generation harness observability
This commit is contained in:
150
backend/app/services/harness/executor.py
Normal file
150
backend/app/services/harness/executor.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""Small-step workflow executor helpers for generation harness adoption."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.services.harness.artifacts import AssetCompletionResult
|
||||
from app.services.harness.plans import WorkflowPlan
|
||||
from app.services.harness.trace import TraceRecorder
|
||||
from app.services.harness.types import ArtifactKind, WorkflowStep
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.db.models import GenerationJob
|
||||
|
||||
AssetTask = Callable[[], Awaitable[AssetCompletionResult]]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AssetPlanRunResult:
|
||||
"""Result of executing asset-producing tasks from one workflow plan."""
|
||||
|
||||
task_results: tuple[AssetCompletionResult, ...]
|
||||
executed_task_keys: tuple[str, ...]
|
||||
ignored_task_keys: tuple[str, ...]
|
||||
|
||||
@property
|
||||
def result_assets(self) -> tuple[str, ...]:
|
||||
"""Assets returned by executed task handlers."""
|
||||
|
||||
return tuple(result.asset for result in self.task_results)
|
||||
|
||||
def to_metadata(self, plan: WorkflowPlan) -> dict[str, Any]:
|
||||
"""Return internal executor coverage metadata for admin-only analytics."""
|
||||
|
||||
return {
|
||||
"plan_mode": plan.mode.value,
|
||||
"planned_task_count": len(plan.tasks),
|
||||
"executed_task_count": len(self.executed_task_keys),
|
||||
"ignored_task_count": len(self.ignored_task_keys),
|
||||
"result_count": len(self.task_results),
|
||||
"executed_task_keys": list(self.executed_task_keys),
|
||||
"ignored_task_keys": list(self.ignored_task_keys),
|
||||
"result_assets": list(self.result_assets),
|
||||
}
|
||||
|
||||
|
||||
async def record_workflow_plan(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
job: "GenerationJob | None",
|
||||
plan: WorkflowPlan,
|
||||
) -> None:
|
||||
"""Persist a workflow plan snapshot for a tracked job."""
|
||||
|
||||
await TraceRecorder(db).record_step(
|
||||
job=job,
|
||||
event_type="workflow_planned",
|
||||
status="succeeded",
|
||||
message="Workflow plan selected for this generation request.",
|
||||
metadata={"plan": plan.to_snapshot()},
|
||||
step=WorkflowStep.REQUEST_ACCEPTANCE,
|
||||
artifact=ArtifactKind.NONE,
|
||||
blocks_main_result=True,
|
||||
)
|
||||
|
||||
|
||||
async def record_evaluation_result(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
job: "GenerationJob | None",
|
||||
story_id: int | None = None,
|
||||
metadata: dict[str, Any],
|
||||
status: str,
|
||||
artifact: ArtifactKind | str = ArtifactKind.STORY_TEXT,
|
||||
) -> None:
|
||||
"""Persist a deterministic evaluation result for a tracked job."""
|
||||
|
||||
await TraceRecorder(db).record_step(
|
||||
job=job,
|
||||
story_id=story_id,
|
||||
event_type="evaluation_completed",
|
||||
status=status,
|
||||
message="Generated content evaluation completed.",
|
||||
metadata=metadata,
|
||||
step=WorkflowStep.EVALUATION,
|
||||
artifact=artifact,
|
||||
blocks_main_result=status != "succeeded",
|
||||
)
|
||||
|
||||
|
||||
async def record_executor_result(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
job: "GenerationJob | None",
|
||||
plan: WorkflowPlan,
|
||||
result: AssetPlanRunResult,
|
||||
) -> None:
|
||||
"""Persist internal executor coverage metadata for a tracked job."""
|
||||
|
||||
await TraceRecorder(db).record_step(
|
||||
job=job,
|
||||
event_type="executor_completed",
|
||||
status="succeeded",
|
||||
message="Workflow executor completed planned asset tasks.",
|
||||
metadata=result.to_metadata(plan),
|
||||
step=WorkflowStep.UNKNOWN,
|
||||
artifact=ArtifactKind.NONE,
|
||||
blocks_main_result=False,
|
||||
)
|
||||
|
||||
|
||||
async def run_asset_plan(
|
||||
plan: WorkflowPlan,
|
||||
*,
|
||||
image_task: AssetTask | None = None,
|
||||
audio_task: AssetTask | None = None,
|
||||
) -> AssetPlanRunResult:
|
||||
"""Execute asset-producing tasks in the order declared by a workflow plan."""
|
||||
|
||||
if plan.mode.value not in {"asset_generation", "asset_retry"}:
|
||||
raise ValueError("run_asset_plan only supports asset workflow plans")
|
||||
|
||||
task_results: list[AssetCompletionResult] = []
|
||||
executed_task_keys: list[str] = []
|
||||
ignored_task_keys: list[str] = []
|
||||
|
||||
for task in plan.tasks:
|
||||
if task.key == "complete_image_asset":
|
||||
if image_task is None:
|
||||
raise ValueError("Asset workflow plan requires an image task handler")
|
||||
task_results.append(await image_task())
|
||||
executed_task_keys.append(task.key)
|
||||
continue
|
||||
|
||||
if task.key == "complete_audio_asset":
|
||||
if audio_task is None:
|
||||
raise ValueError("Asset workflow plan requires an audio task handler")
|
||||
task_results.append(await audio_task())
|
||||
executed_task_keys.append(task.key)
|
||||
continue
|
||||
|
||||
ignored_task_keys.append(task.key)
|
||||
|
||||
return AssetPlanRunResult(
|
||||
task_results=tuple(task_results),
|
||||
executed_task_keys=tuple(executed_task_keys),
|
||||
ignored_task_keys=tuple(ignored_task_keys),
|
||||
)
|
||||
Reference in New Issue
Block a user