mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
Spec N4 has the build-complete card headline read "<App name> is ready", with the annotation 名字只在回复句里念一次;记录卡上不出现 -- said once, in that sentence, and never repeated on the completion receipt. We said it nowhere: the card was titled "Build plan" and the engine had no app name to say. DifyBuilderContext now carries app_name, seeded at session creation through a get_app_name_fn seam alongside the existing revision/authorize ones. Env.rename_app returns the name the database actually took rather than None, so the refine records what landed instead of what it proposed -- the service normalizes after the engine hands off, and the two can differ. The headline is interpolated, so it gets a strings.py template or it would survive localization untranslated. That pattern matches almost anything ending in "is ready", so it sits last and every more specific template gets first refusal. The receipt already omitted the name; there is now a test pinning that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
333 lines
11 KiB
Python
333 lines
11 KiB
Python
"""Dify Builder domain models.
|
|
|
|
Port of dify-enterprise/server/pkg/enterprise/biz/dify_builder/models.go.
|
|
|
|
Deltas from the Go source (per the P1 port plan's Global Constraints / ADR):
|
|
|
|
- ``ForwardAuth`` is dropped entirely. ``Turn`` carries an ``Actor``
|
|
(``account_id``, ``tenant_id``) instead of a forwarded console token, which
|
|
is never persisted and only ever valid for a single ``Advance`` call.
|
|
- Every field except ``PcState``/``EntryMode``-typed fields
|
|
(``Session.entry_mode``, ``Session.current_state``, ``Checkpoint.state``)
|
|
``Turn.actor``, and ``ChangedNode.node_id`` gets a default matching the Go zero value (``""``,
|
|
``0``, ``False``, empty list/dict, ``datetime.min``). This mirrors how the
|
|
Go test suite constructs these structs: almost every struct literal in
|
|
``*_test.go`` sets only a handful of fields and relies on Go's implicit
|
|
zero-initialization for the rest (e.g. ``&Session{AppID: "app", ...}``,
|
|
``&DifyBuilderContext{}``, ``&DifyBuilderContext{FailedRunID: "TR-1"}``). The excepted
|
|
fields have no meaningful "empty" value (an empty string is not a valid
|
|
``PcState``/``EntryMode`` member) and are always set explicitly in the Go
|
|
suite, so they stay required here.
|
|
- ``Diagnosis`` / ``Risk`` / ``MutationIntent`` / ``ChangeSet`` / ``NodeEvent``
|
|
/ ``ApplyResult`` (Go: ``ports.go``) live here rather than in ``ports.py``.
|
|
``DifyBuilderContext`` embeds ``Diagnosis``/``Risk``/``ChangeSet``/
|
|
``MutationIntent`` directly, and defining them in ``ports.py`` (which
|
|
imports the rest of these models) would create a models<->ports import
|
|
cycle; ``ports.py`` imports them back out of this module instead.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
from core.dify_builder.state import PcState
|
|
|
|
|
|
class EntryMode(StrEnum):
|
|
FIX = "fix"
|
|
FIX_CHECKLIST = "fix_checklist"
|
|
BUILD = "build"
|
|
EDIT = "edit"
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class ChecklistError:
|
|
"""One pre-publish checklist finding for a node.
|
|
|
|
Config errors (``messages``) are fixable via ``set_node_config``;
|
|
``unconnected`` and ``plugin_missing`` are structural — surfaced to the
|
|
user, not auto-fixed in v1.
|
|
"""
|
|
|
|
node_id: str = ""
|
|
node_type: str = ""
|
|
title: str = ""
|
|
messages: list[str] = field(default_factory=list)
|
|
unconnected: bool = False
|
|
plugin_missing: bool = False
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class Session:
|
|
id: str = ""
|
|
app_id: str = ""
|
|
tenant_id: str = ""
|
|
owner_account_id: str = ""
|
|
entry_mode: EntryMode
|
|
current_state: PcState
|
|
version: int = 0
|
|
created_at: datetime = field(default_factory=lambda: datetime.min)
|
|
updated_at: datetime = field(default_factory=lambda: datetime.min)
|
|
|
|
|
|
# Graph, Inputs and StartSchema are opaque JSON blobs the engine passes
|
|
# through; only the Dify adapter and the agent interpret them.
|
|
Graph = dict[str, Any]
|
|
Inputs = dict[str, Any]
|
|
StartSchema = dict[str, Any]
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class NodeOutput:
|
|
node_id: str = ""
|
|
title: str = ""
|
|
status: str = "" # success | failed | skip
|
|
error: str = ""
|
|
inputs: Inputs = field(default_factory=dict)
|
|
outputs: Inputs = field(default_factory=dict)
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class Run:
|
|
id: str = ""
|
|
kind: str = "" # original-failed | verify
|
|
dify_run_id: str = ""
|
|
status: str = "" # running | succeeded | failed
|
|
per_node: list[NodeOutput] = field(default_factory=list)
|
|
culprit_node_id: str = ""
|
|
# Launch-failure text (run threw before any node executed); "" once a node ran.
|
|
error: str = ""
|
|
inputs_ref: str = ""
|
|
tokens: int = 0
|
|
elapsed_ms: int = 0
|
|
immutable: bool = False
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class Snapshot:
|
|
id: str = ""
|
|
session_id: str = ""
|
|
hash: str = ""
|
|
graph: Graph = field(default_factory=dict)
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class Checkpoint:
|
|
id: str = ""
|
|
session_id: str = ""
|
|
state: PcState
|
|
snapshot_id: str = ""
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class TestInput:
|
|
# Not a pytest test class; ``__test__`` (unannotated, so not a dataclass
|
|
# field) tells pytest's Test*-name collector to skip it.
|
|
__test__ = False
|
|
|
|
id: str = ""
|
|
session_id: str = ""
|
|
source: str = "" # reuse | mock | upload
|
|
inputs: Inputs = field(default_factory=dict)
|
|
start_schema_hash: str = ""
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class ConversationItem:
|
|
seq: int = 0
|
|
kind: str = "" # user | decision | notice | run_context | preflight_context | assistant_turn | plan
|
|
# | form | challenge | resource_select | checkpoint | change_set | test_result | error | summary
|
|
# | publish | build_learning
|
|
payload: dict[str, Any] = field(default_factory=dict)
|
|
at_version: int = 0
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class Diagnosis:
|
|
culprit_node_id: str = ""
|
|
root_cause: str = ""
|
|
severity: str = ""
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class Risk:
|
|
level: str = "" # low | high
|
|
reason: str = ""
|
|
has_external_side_effect: bool = False
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class MutationIntent:
|
|
"""A single graph mutation the dify_builder applies to the draft.
|
|
|
|
``op`` selects one of five verbs (Slice 1: mutation verbs); ``args`` is
|
|
the verb's untyped wire-shaped payload -- ``services.dify_builder
|
|
.graph_ops`` unpacks it as keyword arguments (``apply_fn(graph,
|
|
**intent.args)``), so each verb's arg-dict keys below are exactly that
|
|
verb's ``apply_*`` function's parameter names. Required keys are
|
|
enforced by ``graph_ops.validate_intent_args``; ``?`` marks an optional
|
|
key.
|
|
|
|
- ``set_node_config``: ``{node_id, path, value}``
|
|
- ``create_node``: ``{node_type, config, position?, node_id?}``
|
|
- ``delete_node``: ``{node_id}``
|
|
- ``connect``: ``{from_node, to_node, source_handle?, target_handle?}``
|
|
- ``insert_between``: ``{edge, node_type, config, position?, node_id?}``
|
|
"""
|
|
|
|
op: str = ""
|
|
args: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class ChangeSet:
|
|
changed_nodes: list[str] = field(default_factory=list)
|
|
diff: str = ""
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class NodeEvent:
|
|
"""A per-node progress event emitted while a draft run streams."""
|
|
|
|
node_id: str = ""
|
|
title: str = ""
|
|
status: str = ""
|
|
error: str = ""
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class ChangedNode:
|
|
"""A node's identity and display name captured with a change summary."""
|
|
|
|
node_id: str
|
|
title: str = ""
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class ApplyResult:
|
|
"""Reports what a repair changed and the draft's new hash."""
|
|
|
|
changed_nodes: list[str] = field(default_factory=list)
|
|
nodes: list[ChangedNode] = field(default_factory=list)
|
|
new_hash: str = ""
|
|
changes: list[str] = field(default_factory=list)
|
|
scope: str = ""
|
|
structure_fingerprint: str = ""
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class BuildNodesResult:
|
|
"""Outcome of ``build_nodes``: the create/connect ``intents`` to apply, plus a
|
|
human-readable ``error`` explaining WHY generation failed when ``intents`` is
|
|
empty -- the generator's specific reason (e.g. UNRESOLVED_REFERENCE, non-object
|
|
JSON) or a provider error (e.g. credit_balance_exhausted). Empty on success."""
|
|
|
|
intents: list[MutationIntent] = field(default_factory=list)
|
|
error: str = ""
|
|
# Structured, timestamped breadcrumbs for the exported debug log. Server logs
|
|
# are lost when the api pod restarts, so each failed generation attempt travels
|
|
# back here (server UTC ``at``, generator ``code``/``node_id``/``detail``) and is
|
|
# stamped onto the ErrorCard -> conversation item -> exported trace.
|
|
diagnostics: list[dict[str, Any]] = field(default_factory=list)
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class DifyBuilderContext:
|
|
"""Per-session working state persisted as the commit context.
|
|
|
|
One context type for all entry modes (Fix, checklist, Build, Edit).
|
|
Fix-specific and Build-specific fields coexist; each scenario touches
|
|
only its own, all default-valued (serialized as a JSON blob -- no
|
|
migration on rename or on adding fields).
|
|
"""
|
|
|
|
# -- Fix / checklist fields --
|
|
failed_run_id: str = ""
|
|
diagnosis: Diagnosis | None = None
|
|
staged_repair: list[MutationIntent] = field(default_factory=list)
|
|
risk: Risk | None = None
|
|
change_set: ChangeSet | None = None
|
|
checkpoint_id: str = ""
|
|
verify_run_id: str = ""
|
|
test_input_ref: str = ""
|
|
last_snapshot_hash: str = ""
|
|
next_seq: int = 0
|
|
# source distinguishes diagnosis origin: "" (run, default) | "checklist".
|
|
source: str = ""
|
|
checklist_errors: list[ChecklistError] = field(default_factory=list)
|
|
|
|
# -- Build fields (Slice 2, additive) --
|
|
goal_text: str = ""
|
|
reply_language: str = "" # detected user-input language (BCP-47); drives reply/localization
|
|
requirements: dict[str, Any] = field(default_factory=dict)
|
|
plan_items: list[str] = field(default_factory=list)
|
|
plan_version_tag: str = ""
|
|
resource_selection: dict[str, Any] = field(default_factory=dict)
|
|
built_node_ids: list[str] = field(default_factory=list)
|
|
form_fields: list[dict] = field(default_factory=list)
|
|
# The app's name as last seen by the engine, so the build-complete card can
|
|
# say it (spec N4). Empty means unknown -- that card then keeps its generic
|
|
# title rather than naming the app wrongly.
|
|
app_name: str = ""
|
|
# True while the app still carries the name cut from its creating prompt, so
|
|
# Builder may refine it once (spec N1); cleared the moment it does. Plan
|
|
# approval freezes the name for good (spec N3, 批准即定名).
|
|
app_name_auto: bool = False
|
|
|
|
# -- Edit fields (Slice 3, additive) --
|
|
edit_rules: dict[str, Any] = field(default_factory=dict)
|
|
edit_target_node_ids: list[str] = field(default_factory=list)
|
|
|
|
# -- Lifecycle fields (Slice 4, additive) --
|
|
paused: bool = False
|
|
checkpoint_seq: int = 0
|
|
|
|
# -- Recovery fields (C-1, additive) --
|
|
last_structure_fingerprint: str = ""
|
|
recovery_class: str = ""
|
|
|
|
# -- Governance fields (C-2, additive) --
|
|
skill_learning_policy: str = "ask"
|
|
|
|
# -- Agent model selection (Foundation slice, additive) --
|
|
# The user's per-session model choice, in Dify's model-selector shape
|
|
# {provider, name, mode, completion_params}. Empty == use the tenant default LLM.
|
|
# Kept a plain dict so core imports no services/model-runtime type.
|
|
model_config: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class Action:
|
|
"""A user-driven event entering the machine."""
|
|
|
|
# request_fix | approve_repair | reject_repair | run_verify | provide_testdata |
|
|
# publish | keep_draft | undo | re_fix | message
|
|
kind: str = ""
|
|
payload: dict[str, Any] = field(default_factory=dict)
|
|
base_version: int = 0
|
|
# Opaque draft-workflow revision observed in the latest server view.
|
|
# Empty means the action does not mutate or otherwise depend on the app
|
|
# draft (for example a free-text conversation turn).
|
|
base_app_revision: str = ""
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class Actor:
|
|
"""The acting console identity for one Advance call. Never persisted."""
|
|
|
|
account_id: str
|
|
tenant_id: str
|
|
|
|
|
|
@dataclass(kw_only=True)
|
|
class Turn:
|
|
"""Per-request data carried through one Advance.
|
|
|
|
Replaces Go's ``Turn.Auth ForwardAuth`` — there is no forwarded console
|
|
token in this port; ``actor`` identifies who is driving this turn.
|
|
"""
|
|
|
|
action: Action | None = None
|
|
actor: Actor
|