feat: scaffold agentic interior design workflow
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Domain models and workflow rules."""
|
||||
@@ -0,0 +1,138 @@
|
||||
from datetime import UTC, datetime
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class WorkflowStage(StrEnum):
|
||||
UPLOADED = "uploaded"
|
||||
REGION_SELECTION = "region_selection"
|
||||
PLAN_REVIEW = "plan_review"
|
||||
BLOCKOUT = "blockout"
|
||||
STYLE_BRIEF = "style_brief"
|
||||
DIRECTION_SELECTION = "direction_selection"
|
||||
RENDER_REVIEW = "render_review"
|
||||
EDITING = "editing"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class WorkflowCommand(StrEnum):
|
||||
START_INGESTION = "start_ingestion"
|
||||
SELECT_REGION = "select_region"
|
||||
CONFIRM_PLAN = "confirm_plan"
|
||||
BUILD_BLOCKOUT = "build_blockout"
|
||||
SUBMIT_STYLE_BRIEF = "submit_style_brief"
|
||||
SELECT_DIRECTION = "select_direction"
|
||||
REQUEST_RENDER = "request_render"
|
||||
APPLY_EDIT = "apply_edit"
|
||||
COMPLETE_PROJECT = "complete_project"
|
||||
RETRY = "retry"
|
||||
|
||||
|
||||
class Point2D(BaseModel):
|
||||
x: float
|
||||
y: float
|
||||
|
||||
|
||||
class PlanRegion(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
bounds: list[float] = Field(min_length=4, max_length=4)
|
||||
recommended: bool = False
|
||||
confidence: float = Field(ge=0, le=1)
|
||||
|
||||
|
||||
class PlanLayer(BaseModel):
|
||||
id: str
|
||||
label: str
|
||||
category: str
|
||||
visible: bool = True
|
||||
source_names: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class PlanIssue(BaseModel):
|
||||
id: str
|
||||
kind: str
|
||||
message: str
|
||||
confidence: float = Field(ge=0, le=1)
|
||||
resolved: bool = False
|
||||
|
||||
|
||||
class PlanState(BaseModel):
|
||||
source_name: str
|
||||
source_kind: str
|
||||
page_count: int = 1
|
||||
vector_based: bool = False
|
||||
cad_layer_count: int = 0
|
||||
regions: list[PlanRegion] = Field(default_factory=list)
|
||||
selected_region_id: str | None = None
|
||||
layers: list[PlanLayer] = Field(default_factory=list)
|
||||
issues: list[PlanIssue] = Field(default_factory=list)
|
||||
scale_mm_per_unit: float | None = None
|
||||
ceiling_height_mm: int = 2800
|
||||
|
||||
|
||||
class CameraView(BaseModel):
|
||||
id: str
|
||||
label: str
|
||||
room: str
|
||||
status: str = "draft"
|
||||
|
||||
|
||||
class SceneState(BaseModel):
|
||||
blockout_asset_key: str | None = None
|
||||
cameras: list[CameraView] = Field(default_factory=list)
|
||||
locked_object_ids: list[str] = Field(default_factory=list)
|
||||
furniture_mode: str = "preserve_reference"
|
||||
|
||||
|
||||
class ColorToken(BaseModel):
|
||||
name: str
|
||||
hex: str
|
||||
role: str
|
||||
|
||||
|
||||
class StyleState(BaseModel):
|
||||
concept: str = "克制、自然、明亮的现代住宅"
|
||||
keywords: list[str] = Field(default_factory=lambda: ["轻盈", "自然材质", "留白"])
|
||||
palette: list[ColorToken] = Field(default_factory=list)
|
||||
materials: list[str] = Field(default_factory=list)
|
||||
lighting: str = "低对比度的自然光与柔和间接照明"
|
||||
forms: str = "低矮、水平延展、少量圆角"
|
||||
density: str = "适度留白"
|
||||
avoid: list[str] = Field(default_factory=list)
|
||||
locked_decisions: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ProjectSnapshot(BaseModel):
|
||||
project_id: str
|
||||
name: str
|
||||
stage: WorkflowStage
|
||||
revision: int = 1
|
||||
plan: PlanState
|
||||
scene: SceneState = Field(default_factory=SceneState)
|
||||
style: StyleState = Field(default_factory=StyleState)
|
||||
available_commands: list[WorkflowCommand] = Field(default_factory=list)
|
||||
last_stable_stage: WorkflowStage | None = None
|
||||
failure_reason: str | None = None
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
||||
|
||||
|
||||
class CommandRequest(BaseModel):
|
||||
command: WorkflowCommand
|
||||
expected_revision: int = Field(ge=1)
|
||||
payload: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class WorkflowStageDefinition(BaseModel):
|
||||
id: WorkflowStage
|
||||
label: str
|
||||
purpose: str
|
||||
human_confirmation: bool
|
||||
|
||||
|
||||
class WorkflowDefinition(BaseModel):
|
||||
stages: list[WorkflowStageDefinition]
|
||||
transitions: dict[str, list[str]]
|
||||
@@ -0,0 +1,127 @@
|
||||
from copy import deepcopy
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from app.domain.models import (
|
||||
CommandRequest,
|
||||
ProjectSnapshot,
|
||||
WorkflowCommand,
|
||||
WorkflowDefinition,
|
||||
WorkflowStage,
|
||||
WorkflowStageDefinition,
|
||||
)
|
||||
|
||||
|
||||
class WorkflowConflictError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidTransitionError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
TRANSITIONS: dict[WorkflowStage, dict[WorkflowCommand, WorkflowStage]] = {
|
||||
WorkflowStage.UPLOADED: {
|
||||
WorkflowCommand.START_INGESTION: WorkflowStage.REGION_SELECTION,
|
||||
},
|
||||
WorkflowStage.REGION_SELECTION: {
|
||||
WorkflowCommand.SELECT_REGION: WorkflowStage.PLAN_REVIEW,
|
||||
},
|
||||
WorkflowStage.PLAN_REVIEW: {
|
||||
WorkflowCommand.CONFIRM_PLAN: WorkflowStage.BLOCKOUT,
|
||||
},
|
||||
WorkflowStage.BLOCKOUT: {
|
||||
WorkflowCommand.BUILD_BLOCKOUT: WorkflowStage.STYLE_BRIEF,
|
||||
},
|
||||
WorkflowStage.STYLE_BRIEF: {
|
||||
WorkflowCommand.SUBMIT_STYLE_BRIEF: WorkflowStage.DIRECTION_SELECTION,
|
||||
},
|
||||
WorkflowStage.DIRECTION_SELECTION: {
|
||||
WorkflowCommand.SELECT_DIRECTION: WorkflowStage.RENDER_REVIEW,
|
||||
},
|
||||
WorkflowStage.RENDER_REVIEW: {
|
||||
WorkflowCommand.REQUEST_RENDER: WorkflowStage.EDITING,
|
||||
WorkflowCommand.COMPLETE_PROJECT: WorkflowStage.COMPLETED,
|
||||
},
|
||||
WorkflowStage.EDITING: {
|
||||
WorkflowCommand.APPLY_EDIT: WorkflowStage.EDITING,
|
||||
WorkflowCommand.REQUEST_RENDER: WorkflowStage.EDITING,
|
||||
WorkflowCommand.COMPLETE_PROJECT: WorkflowStage.COMPLETED,
|
||||
},
|
||||
WorkflowStage.FAILED: {
|
||||
WorkflowCommand.RETRY: WorkflowStage.PLAN_REVIEW,
|
||||
},
|
||||
WorkflowStage.COMPLETED: {
|
||||
WorkflowCommand.APPLY_EDIT: WorkflowStage.EDITING,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def available_commands(stage: WorkflowStage) -> list[WorkflowCommand]:
|
||||
return list(TRANSITIONS.get(stage, {}).keys())
|
||||
|
||||
|
||||
def apply_command(project: ProjectSnapshot, request: CommandRequest) -> ProjectSnapshot:
|
||||
if request.expected_revision != project.revision:
|
||||
raise WorkflowConflictError(
|
||||
f"Expected revision {request.expected_revision}, current revision is {project.revision}."
|
||||
)
|
||||
|
||||
target = TRANSITIONS.get(project.stage, {}).get(request.command)
|
||||
if target is None:
|
||||
raise InvalidTransitionError(
|
||||
f"Command '{request.command}' is not allowed while project is in '{project.stage}'."
|
||||
)
|
||||
|
||||
updated = deepcopy(project)
|
||||
updated.last_stable_stage = project.stage
|
||||
updated.stage = target
|
||||
updated.revision += 1
|
||||
updated.updated_at = datetime.now(UTC)
|
||||
updated.failure_reason = None
|
||||
|
||||
if request.command == WorkflowCommand.SELECT_REGION:
|
||||
updated.plan.selected_region_id = request.payload.get("region_id")
|
||||
elif request.command == WorkflowCommand.CONFIRM_PLAN:
|
||||
updated.plan.ceiling_height_mm = int(
|
||||
request.payload.get("ceiling_height_mm", updated.plan.ceiling_height_mm)
|
||||
)
|
||||
elif request.command == WorkflowCommand.SUBMIT_STYLE_BRIEF:
|
||||
for field in ("concept", "lighting", "forms", "density"):
|
||||
if field in request.payload:
|
||||
setattr(updated.style, field, request.payload[field])
|
||||
for field in ("keywords", "materials", "avoid", "locked_decisions"):
|
||||
if field in request.payload:
|
||||
setattr(updated.style, field, list(request.payload[field]))
|
||||
|
||||
updated.available_commands = available_commands(updated.stage)
|
||||
return updated
|
||||
|
||||
|
||||
def workflow_definition() -> WorkflowDefinition:
|
||||
labels = {
|
||||
WorkflowStage.UPLOADED: ("文件已上传", "验证文件并准备解析", False),
|
||||
WorkflowStage.REGION_SELECTION: ("选择户型", "从复杂图纸中选择目标平面区域", True),
|
||||
WorkflowStage.PLAN_REVIEW: ("确认结构", "检查墙、门窗、比例和保留家具", True),
|
||||
WorkflowStage.BLOCKOUT: ("生成白模", "建立空间骨架、家具占位和相机", False),
|
||||
WorkflowStage.STYLE_BRIEF: ("明确风格", "形成结构化 Style DNA", True),
|
||||
WorkflowStage.DIRECTION_SELECTION: ("选择方向", "比较并锁定主设计方向", True),
|
||||
WorkflowStage.RENDER_REVIEW: ("审阅效果", "检查空间忠实度与审美一致性", True),
|
||||
WorkflowStage.EDITING: ("多轮修改", "通过结构化操作局部修改", True),
|
||||
WorkflowStage.COMPLETED: ("方案完成", "导出概念方案与版本记录", False),
|
||||
WorkflowStage.FAILED: ("需要处理", "展示错误并从稳定阶段恢复", True),
|
||||
}
|
||||
return WorkflowDefinition(
|
||||
stages=[
|
||||
WorkflowStageDefinition(
|
||||
id=stage,
|
||||
label=labels[stage][0],
|
||||
purpose=labels[stage][1],
|
||||
human_confirmation=labels[stage][2],
|
||||
)
|
||||
for stage in WorkflowStage
|
||||
],
|
||||
transitions={
|
||||
stage.value: [command.value for command in commands]
|
||||
for stage, commands in TRANSITIONS.items()
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user