Files
dify/api/controllers/console/dify_builder_fields.py
T
Charles YaoandClaude Opus 5 aa2b4fe4ab feat(dify-builder): name apps from their prompt, and turn gate actions into card options
Two App Builder spec changes that share the Builder session contract.

Naming (spec N1-N3): an app created from the homepage prompt is no longer named
by the user. core/dify_builder/naming.py cuts the prompt's opening clause at
create time -- instant and infallible, since it runs inside POST /apps -- and
CreateAppPayload.name becomes optional alongside a new prompt field. The Builder
then refines that name once during goal analysis, where a model and the detected
reply language are already in hand, so the judgement calls that are
English-shaped in pure Python (dropping an imperative lead-in, spotting a
contentless prompt) work in any language. Plan approval freezes the name.

Card options: SessionView gains a Decision carrying the gate's choice as options
inside the card plus a fixed confirm/cancel pair, replacing a button row whose
shape changed at every state. Option ids are the old action ids untouched, so
resolve_action_kind and the per-state legality check are unchanged, and
SessionView.actions keeps being populated -- the deployed web client goes on
working until it reads decision instead.

The regenerated console OpenAPI doc also picks up ErrorCard.diagnostics, missed
when cc9ca8b1dd landed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 17:55:38 -07:00

363 lines
11 KiB
Python

"""Pydantic request and response schemas for the Console Dify Builder API."""
from typing import Annotated, Any, Literal
from pydantic import BaseModel, ConfigDict, Field, RootModel, StrictInt, StringConstraints, model_validator
from core.dify_builder.contract import (
Action,
AgentMessageEventData,
AppRevision,
AssistantTurnItem,
BuilderErrorCode,
BuildLearningCard,
CanvasEventData,
ChallengeCard,
ChangeSetCard,
CheckpointCard,
CheckpointRef,
Decision,
DecisionItem,
ErrorCard,
ErrorEventData,
FormCard,
NodeEventData,
NoticeItem,
Phase,
PlanCard,
PreflightContextCard,
ProgressEventData,
PublishCard,
ReasoningEventData,
RecoveryRef,
ResourceSelectCard,
RunContextCard,
RunStatus,
SessionModel,
SummaryCard,
TestResultCard,
UserItem,
)
from core.dify_builder.models import EntryMode
from fields.base import ResponseModel
NonEmptyString = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
ClientTurnId = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=128)]
class DifyBuilderPayload(BaseModel):
model_config = ConfigDict(extra="forbid", populate_by_name=True)
class DifyBuilderCreateBuildSessionPayload(DifyBuilderPayload):
app_id: NonEmptyString
scenario: Literal["build"]
goal_text: NonEmptyString
# True when the app was created from this same prompt and still carries the
# name cut from it, so Builder may rewrite it once it understands the goal.
# False is create-from-blank: the user named the app, Builder inherits it.
derive_app_name: bool = False
model_config_data: SessionModel | None = Field(default=None, alias="model_config")
class DifyBuilderCreateEditSessionPayload(DifyBuilderPayload):
app_id: NonEmptyString
scenario: Literal["edit"]
goal_text: NonEmptyString
model_config_data: SessionModel | None = Field(default=None, alias="model_config")
class DifyBuilderCreateFixSessionPayload(DifyBuilderPayload):
app_id: NonEmptyString
scenario: Literal["fix"]
failed_run_id: NonEmptyString
model_config_data: SessionModel | None = Field(default=None, alias="model_config")
class DifyBuilderChecklistErrorPayload(DifyBuilderPayload):
node_id: str
node_type: str
title: str
messages: list[str]
unconnected: bool
plugin_missing: bool
class DifyBuilderCreateChecklistFixSessionPayload(DifyBuilderPayload):
app_id: NonEmptyString
scenario: Literal["fix"]
checklist_errors: Annotated[list[DifyBuilderChecklistErrorPayload], Field(min_length=1)]
failed_run_id: NonEmptyString | None = None
model_config_data: SessionModel | None = Field(default=None, alias="model_config")
class DifyBuilderCreateSessionPayload(
RootModel[
DifyBuilderCreateBuildSessionPayload
| DifyBuilderCreateEditSessionPayload
| DifyBuilderCreateFixSessionPayload
| DifyBuilderCreateChecklistFixSessionPayload
]
):
"""Create a Build, Edit, failed-run Fix, or checklist Fix session."""
class DifyBuilderSubmitActionPayload(DifyBuilderPayload):
action_id: NonEmptyString
payload: dict[str, Any] = Field(default_factory=dict)
base_version: StrictInt
base_app_revision: NonEmptyString
class DifyBuilderSubmitMessagePayload(DifyBuilderPayload):
text: NonEmptyString
base_version: StrictInt
client_turn_id: ClientTurnId
class DifyBuilderConversationListQuery(DifyBuilderPayload):
before_seq: int | None = Field(default=None, ge=0, description="Load groups before this sequence")
after_seq: int | None = Field(default=None, ge=-1, description="Load groups after this sequence")
limit: int = Field(default=20, ge=1, le=100, description="Number of conversation groups to return")
@model_validator(mode="after")
def validate_cursor(self) -> "DifyBuilderConversationListQuery":
if self.before_seq is not None and self.after_seq is not None:
raise ValueError("before_seq and after_seq are mutually exclusive")
return self
class DifyBuilderConversationItemBase(ResponseModel):
seq: int
at_version: int
class DifyBuilderUserConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["user"]
payload: UserItem
class DifyBuilderDecisionConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["decision"]
payload: DecisionItem
class DifyBuilderNoticeConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["notice"]
payload: NoticeItem
class DifyBuilderRunContextConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["run_context"]
payload: RunContextCard
class DifyBuilderPreflightContextConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["preflight_context"]
payload: PreflightContextCard
class DifyBuilderAssistantTurnConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["assistant_turn"]
payload: AssistantTurnItem
class DifyBuilderPlanConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["plan"]
payload: PlanCard
class DifyBuilderFormConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["form"]
payload: FormCard
class DifyBuilderChallengeConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["challenge"]
payload: ChallengeCard
class DifyBuilderResourceSelectConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["resource_select"]
payload: ResourceSelectCard
class DifyBuilderCheckpointConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["checkpoint"]
payload: CheckpointCard
class DifyBuilderChangeSetConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["change_set"]
payload: ChangeSetCard
class DifyBuilderTestResultConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["test_result"]
payload: TestResultCard
class DifyBuilderErrorConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["error"]
payload: ErrorCard
class DifyBuilderSummaryConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["summary"]
payload: SummaryCard
class DifyBuilderPublishConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["publish"]
payload: PublishCard
class DifyBuilderBuildLearningConversationItemResponse(DifyBuilderConversationItemBase):
kind: Literal["build_learning"]
payload: BuildLearningCard
DifyBuilderConversationItem = Annotated[
DifyBuilderUserConversationItemResponse
| DifyBuilderDecisionConversationItemResponse
| DifyBuilderNoticeConversationItemResponse
| DifyBuilderRunContextConversationItemResponse
| DifyBuilderPreflightContextConversationItemResponse
| DifyBuilderAssistantTurnConversationItemResponse
| DifyBuilderPlanConversationItemResponse
| DifyBuilderFormConversationItemResponse
| DifyBuilderChallengeConversationItemResponse
| DifyBuilderResourceSelectConversationItemResponse
| DifyBuilderCheckpointConversationItemResponse
| DifyBuilderChangeSetConversationItemResponse
| DifyBuilderTestResultConversationItemResponse
| DifyBuilderErrorConversationItemResponse
| DifyBuilderSummaryConversationItemResponse
| DifyBuilderPublishConversationItemResponse
| DifyBuilderBuildLearningConversationItemResponse,
Field(discriminator="kind"),
]
class DifyBuilderActiveInteractionResponse(ResponseModel):
action_id: str
card: DifyBuilderConversationItem
valid_at_version: int
class DifyBuilderSessionViewResponse(ResponseModel):
session_id: str
app_id: str
version: int
state: str
canvas_read_only: bool
run_status: RunStatus
interrupted: bool
conversation_last_seq: int
entry_mode: EntryMode = EntryMode.FIX
phase: Phase = Phase.UNDERSTAND
actions: list[Action] = Field(default_factory=list)
decision: Decision | None = None
active_interaction: DifyBuilderActiveInteractionResponse | None = None
checkpoint: CheckpointRef | None = None
recovery: RecoveryRef | None = None
model: SessionModel | None = None
app_revision: AppRevision | None = None
class DifyBuilderConversationPageResponse(ResponseModel):
data: list[DifyBuilderConversationItem]
has_more: bool
first_seq: int | None
last_seq: int | None
class DifyBuilderCommitEventData(ResponseModel):
session_id: str
operation_id: str
stage_id: str
at_version: int
version: int
state: str
settled: bool
items: list[DifyBuilderConversationItem]
kind: Literal["commit"] = "commit"
class DifyBuilderStateEventData(DifyBuilderSessionViewResponse):
kind: Literal["state"] = "state"
class DifyBuilderCommandStartedEventData(DifyBuilderSessionViewResponse):
kind: Literal["command_started"] = "command_started"
class DifyBuilderCommandStartedEventResponse(ResponseModel):
event: Literal["command_started"]
data: DifyBuilderCommandStartedEventData
class DifyBuilderNodeEventResponse(ResponseModel):
event: Literal["node"]
data: NodeEventData
class DifyBuilderCanvasEventResponse(ResponseModel):
event: Literal["canvas"]
data: CanvasEventData
class DifyBuilderAgentMessageEventResponse(ResponseModel):
event: Literal["agent_message"]
data: AgentMessageEventData
class DifyBuilderReasoningEventResponse(ResponseModel):
event: Literal["reasoning"]
data: ReasoningEventData
class DifyBuilderProgressEventResponse(ResponseModel):
event: Literal["progress"]
data: ProgressEventData
class DifyBuilderCommitEventResponse(ResponseModel):
event: Literal["commit"]
data: DifyBuilderCommitEventData
class DifyBuilderStateEventResponse(ResponseModel):
event: Literal["state"]
data: DifyBuilderStateEventData
class DifyBuilderErrorEventResponse(ResponseModel):
event: Literal["error"]
data: ErrorEventData
class DifyBuilderStreamEventResponse(
RootModel[
Annotated[
DifyBuilderCommandStartedEventResponse
| DifyBuilderNodeEventResponse
| DifyBuilderCanvasEventResponse
| DifyBuilderAgentMessageEventResponse
| DifyBuilderReasoningEventResponse
| DifyBuilderProgressEventResponse
| DifyBuilderCommitEventResponse
| DifyBuilderStateEventResponse
| DifyBuilderErrorEventResponse,
Field(discriminator="event"),
]
]
):
"""One JSON object carried by an SSE ``data:`` frame."""
class DifyBuilderErrorResponse(ResponseModel):
code: BuilderErrorCode
message: str | None = None
recoverable: bool | None = None