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>
This commit is contained in:
Charles YaoandClaude Opus 5 committed 2026-09-17 17:55:38 -07:00
1 parent cc9ca8b1dd
commit aa2b4fe4ab
30 files changed
+1163 -18

No files matched your search

+8 -1
View File
@@ -156,7 +156,13 @@ class StarredAppListQuery(AppListBaseQuery):
class CreateAppPayload(BaseModel):
name: str = Field(..., min_length=1, description="App name")
# Optional since App Builder: an app created from the homepage prompt sends
# ``prompt`` and no ``name``. A supplied name still wins -- that is
# create-from-blank, where the user names the app and Builder inherits it.
name: str | None = Field(default=None, min_length=1, description="App name; derived from prompt when omitted")
prompt: str | None = Field(
default=None, description="Goal prompt the app was created from; used to derive the name"
)
description: str | None = Field(default=None, description="App description (max 400 chars)", max_length=400)
mode: Literal["chat", "agent-chat", "advanced-chat", "workflow", "completion"] = Field(..., description="App mode")
icon_type: IconType | None = Field(default=None, description="Icon type")
@@ -708,6 +714,7 @@ class AppListApi(Resource):
"""Create app"""
params = CreateAppParams(
name=req_data.name,
prompt=req_data.prompt,
description=req_data.description,
mode=req_data.mode,
icon_type=req_data.icon_type,
+3 -2
View File
@@ -52,7 +52,7 @@ from core.dify_builder.models import Action, Actor
from libs.helper import dump_response
from libs.login import login_required
from services.dify_builder import progress_bus
from services.dify_builder.service import resolve_action_kind
from services.dify_builder.service import resolve_submitted_action
from services.dify_builder.wiring import (
build_service,
dify_builder_error_response,
@@ -140,6 +140,7 @@ def _create(body, actor: Actor) -> Response | tuple[dict, int]:
actor=actor,
goal_text=payload.goal_text,
model_config=model_config,
derive_app_name=payload.derive_app_name,
)
)
if isinstance(payload, DifyBuilderCreateEditSessionPayload):
@@ -179,7 +180,7 @@ def _action(session_id: str, body, actor: Actor) -> Response | tuple[dict, int]:
except ValidationError:
return {"code": "bad_request"}, 400
action = Action(
kind=resolve_action_kind(payload.action_id),
kind=resolve_submitted_action(payload.action_id, payload.payload),
payload=payload.payload,
base_version=payload.base_version,
base_app_revision=payload.base_app_revision,
@@ -16,6 +16,7 @@ from core.dify_builder.contract import (
ChangeSetCard,
CheckpointCard,
CheckpointRef,
Decision,
DecisionItem,
ErrorCard,
ErrorEventData,
@@ -52,6 +53,10 @@ 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")
@@ -252,6 +257,7 @@ class DifyBuilderSessionViewResponse(ResponseModel):
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