mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
Run workflow streaming inside the Builder worker to avoid queue deadlocks. Forward native events to shared editor callbacks and wait for graph refresh before retesting repairs. Keep command cleanup stable, preserve batched canvas events, and allow failed or unfinished historical runs to refresh again.
250 lines
8.4 KiB
Python
250 lines
8.4 KiB
Python
"""Dify Builder outbound ports.
|
|
|
|
Port of dify-enterprise/server/pkg/enterprise/biz/dify_builder/ports.go.
|
|
|
|
Deltas from the Go source (per the P1 port plan's Global Constraints / ADR):
|
|
|
|
- ``ForwardAuth`` is dropped entirely. ``DifyPort`` methods take
|
|
``actor: Actor`` instead of a forwarded console token — the fake used in
|
|
P1 ignores it; the real adapter (P2) uses it to call ``WorkflowService``.
|
|
- Method names are ``snake_case``.
|
|
- The value types Go defines in this file (``Diagnosis``, ``Risk``,
|
|
``MutationIntent``, ``ChangeSet``, ``NodeEvent``, ``ApplyResult``) live in
|
|
``models.py`` instead, alongside the rest of the domain dataclasses —
|
|
``DifyBuilderContext`` embeds several of them directly, and defining them here
|
|
(where the models are imported) would create a models<->ports import
|
|
cycle. They are re-exported from here for convenience.
|
|
- ``interface`` becomes ``typing.Protocol``, marked ``@runtime_checkable`` so
|
|
conformance can be asserted with ``isinstance`` in tests (see
|
|
``seam_test.go``'s ``TestSeam_AnyConformingAgent_DrivesFlowToTerminal``).
|
|
"""
|
|
|
|
from collections.abc import Callable, Mapping
|
|
from typing import Any, Protocol, runtime_checkable
|
|
|
|
from core.dify_builder.contract import ConversationPage, ResourceOption
|
|
from core.dify_builder.models import (
|
|
Actor,
|
|
ApplyResult,
|
|
BuildNodesResult,
|
|
ChangeSet,
|
|
ChecklistError,
|
|
Checkpoint,
|
|
ConversationItem,
|
|
Diagnosis,
|
|
DifyBuilderContext,
|
|
Graph,
|
|
Inputs,
|
|
MutationIntent,
|
|
NodeEvent,
|
|
NodeOutput,
|
|
Risk,
|
|
Run,
|
|
Session,
|
|
Snapshot,
|
|
StartSchema,
|
|
TestInput,
|
|
)
|
|
from core.dify_builder.state import PcState
|
|
|
|
__all__ = [
|
|
"ApplyResult",
|
|
"ChangeSet",
|
|
"Diagnosis",
|
|
"DifyBuilderAgent",
|
|
"DifyPort",
|
|
"MutationIntent",
|
|
"NodeEvent",
|
|
"ReasoningStreamingAgent",
|
|
"Repository",
|
|
"Risk",
|
|
]
|
|
|
|
|
|
@runtime_checkable
|
|
class DifyBuilderAgent(Protocol):
|
|
"""The cognitive seam implemented by both LLM and fallback agents."""
|
|
|
|
def diagnose(self, failed_run: Run, graph: Graph, node_outputs: list[NodeOutput]) -> Diagnosis: ...
|
|
|
|
def diagnose_checklist(self, errors: list[ChecklistError], graph: Graph) -> Diagnosis: ...
|
|
|
|
def propose_repair(self, diagnosis: Diagnosis, graph: Graph) -> tuple[list[MutationIntent], Risk]: ...
|
|
|
|
def generate_mock_inputs(self, schema: StartSchema, prior_failed: Inputs) -> Inputs: ...
|
|
|
|
# -- Build cognition --
|
|
|
|
def analyze_goal(self, goal_text: str) -> dict[str, Any]: ...
|
|
|
|
# Short human title for a prompt-created app. Returns the raw proposal; the
|
|
# service layer normalizes and stores it.
|
|
def propose_app_name(self, goal_text: str, requirements: dict[str, Any]) -> str: ...
|
|
|
|
def propose_plan_v1(self, requirements: dict[str, Any]) -> list[str]: ...
|
|
|
|
def discover_resources(self, plan_items: list[str]) -> list[ResourceOption]: ...
|
|
|
|
# One plain sentence naming a plan step nothing installed can perform, or
|
|
# "" when the recommended resources cover the plan. Never raises.
|
|
def assess_capability_gap(self, plan_items: list[str], options: list[ResourceOption]) -> str: ...
|
|
|
|
def bind_resources(self, plan_items: list[str], resource_ids: list[str]) -> list[str]: ...
|
|
|
|
def build_nodes(self, plan_items: list[str], resource_ids: list[str] | None = None) -> BuildNodesResult: ...
|
|
|
|
def learn_from_build(
|
|
self,
|
|
goal_text: str,
|
|
requirements: dict[str, Any],
|
|
plan_items: list[str],
|
|
built_node_ids: list[str],
|
|
) -> str: ...
|
|
|
|
# -- Edit cognition --
|
|
|
|
def analyze_impact(self, goal_text: str, graph: Graph) -> dict[str, Any]: ...
|
|
|
|
def propose_edit_plan(self, edit_rules: dict[str, Any], graph: Graph) -> list[str]: ...
|
|
|
|
def build_edit_intents(self, edit_rules: dict[str, Any], graph: Graph) -> list[MutationIntent]: ...
|
|
|
|
def respond_to_message(
|
|
self,
|
|
state: PcState,
|
|
context: DifyBuilderContext,
|
|
history: list[ConversationItem],
|
|
graph: Graph,
|
|
text: str,
|
|
on_delta: Callable[[str], None] | None = None,
|
|
) -> str: ...
|
|
|
|
# The agent's resolved model instance (or None when it can't resolve).
|
|
# Typed loosely as ``object | None`` to keep this pure port free of any
|
|
# model-runtime import; the reply-language Localizer uses it as its model
|
|
# provider. Implementers that have no model return None.
|
|
def model_or_none(self) -> object | None: ...
|
|
|
|
|
|
@runtime_checkable
|
|
class ReasoningStreamingAgent(Protocol):
|
|
"""Optional capability for agents that expose model-provided reasoning."""
|
|
|
|
def set_reasoning_callback(self, callback: Callable[[str, str], None] | None) -> None: ...
|
|
|
|
|
|
@runtime_checkable
|
|
class DifyPort(Protocol):
|
|
"""The outbound port to Dify.
|
|
|
|
Implemented by the token-forwarding adapter in P2. ``actor`` identifies
|
|
who is driving the call (Go: ``auth ForwardAuth``).
|
|
"""
|
|
|
|
def read_graph(self, app_id: str, actor: Actor) -> tuple[Graph, str]:
|
|
"""Return the graph and its execution revision, excluding canvas presentation."""
|
|
...
|
|
|
|
def node_outputs(self, app_id: str, actor: Actor, run_id: str) -> list[NodeOutput]: ...
|
|
|
|
def apply_repair(
|
|
self,
|
|
app_id: str,
|
|
actor: Actor,
|
|
intents: list[MutationIntent],
|
|
on_canvas: Callable[[dict], None] | None = None,
|
|
*,
|
|
expected_revision: str,
|
|
) -> ApplyResult: ...
|
|
|
|
def run_draft(
|
|
self,
|
|
app_id: str,
|
|
actor: Actor,
|
|
inputs: Inputs,
|
|
on_event: Callable[[NodeEvent], None],
|
|
*,
|
|
on_workflow_event: Callable[[Mapping[str, object]], None] | None = None,
|
|
) -> Run: ...
|
|
|
|
def publish(self, app_id: str, actor: Actor) -> None: ...
|
|
|
|
def restore_graph(self, app_id: str, actor: Actor, graph: Graph, *, expected_revision: str) -> str: ...
|
|
|
|
def structural_fingerprint(self, graph: Graph) -> str:
|
|
"""Stable hash of the graph's structure (node identity+type + edges),
|
|
excluding config -- the core-visible primitive backing recovery (C-1).
|
|
The adapter delegates to ``graph_ops.structural_fingerprint``."""
|
|
...
|
|
|
|
def graph_node_ids(self, graph: Graph) -> list[str]:
|
|
"""The ids of every node in ``graph`` -- backs recovery's
|
|
target-presence check. Delegates to ``graph_ops.node_ids``."""
|
|
...
|
|
|
|
|
|
@runtime_checkable
|
|
class Repository(Protocol):
|
|
"""The durable store.
|
|
|
|
``compare_and_advance`` is the single concurrency primitive: it appends a
|
|
commit and advances the session version iff the current version equals
|
|
``base_version``, else raises ``ConflictError``.
|
|
"""
|
|
|
|
def create_session(
|
|
self, session: Session, initial_fc: DifyBuilderContext, items: list[ConversationItem]
|
|
) -> None: ...
|
|
|
|
def get_session(self, id: str) -> tuple[Session, DifyBuilderContext]: ...
|
|
|
|
def compare_and_advance(
|
|
self,
|
|
session_id: str,
|
|
base_version: int,
|
|
next: PcState,
|
|
fc: DifyBuilderContext,
|
|
items: list[ConversationItem],
|
|
) -> int: ...
|
|
|
|
def create_checkpoint(self, cp: Checkpoint, snap: Snapshot) -> None: ...
|
|
|
|
def get_checkpoint(self, id: str) -> tuple[Checkpoint, Snapshot]: ...
|
|
|
|
def save_run(self, session_id: str, run: Run) -> None:
|
|
"""Persist a run.
|
|
|
|
Implementations MUST preserve a caller-supplied non-empty ``run.id``
|
|
(do not regenerate it): ``handle_verify`` pre-generates the id and sets
|
|
``fc.verify_run_id`` to the same value, so a regenerated id would
|
|
silently desync the persisted row from the session context (surfacing
|
|
later as a wrong-run / not-found lookup). If ``run.id`` is empty the
|
|
implementation assigns one.
|
|
"""
|
|
...
|
|
|
|
def get_run(self, id: str) -> Run: ...
|
|
|
|
def save_test_input(self, ti: TestInput) -> None: ...
|
|
|
|
def get_test_input(self, id: str) -> TestInput: ...
|
|
|
|
def list_conversation(self, session_id: str) -> list[ConversationItem]: ...
|
|
|
|
def list_recent_conversation(self, session_id: str, *, limit: int) -> list[ConversationItem]: ...
|
|
|
|
def get_conversation_turn_kinds(self, session_id: str, turn_id: str) -> frozenset[str]: ...
|
|
|
|
def list_conversation_page(
|
|
self,
|
|
session_id: str,
|
|
*,
|
|
limit: int,
|
|
before_seq: int | None = None,
|
|
after_seq: int | None = None,
|
|
) -> ConversationPage: ...
|
|
|
|
def get_latest_conversation_item(self, session_id: str, kinds: frozenset[str]) -> ConversationItem | None: ...
|
|
|
|
def invalidate_conversation_items(self, session_id: str, from_seq: int) -> None: ...
|