mirror of
https://github.com/langgenius/dify.git
synced 2026-11-13 14:51:39 +08:00
386 lines
14 KiB
Python
386 lines
14 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, Sequence
|
|
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,
|
|
PublishResult,
|
|
Risk,
|
|
Run,
|
|
Session,
|
|
Snapshot,
|
|
StartSchema,
|
|
TestInput,
|
|
)
|
|
from core.dify_builder.state import PcState
|
|
from services.dify_builder.application.execution_policy import HttpFixtureSetV1, HttpResponseFixtureV1
|
|
from services.dify_builder.effect_disclosure import EffectDisclosure
|
|
from services.dify_builder.plan import WorkflowPlan
|
|
|
|
__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], *, reply_language: str | None = None
|
|
) -> Diagnosis: ...
|
|
|
|
def diagnose_checklist(
|
|
self, errors: list[ChecklistError], graph: Graph, *, reply_language: str | None = None
|
|
) -> Diagnosis: ...
|
|
|
|
def propose_repair(
|
|
self, diagnosis: Diagnosis, graph: Graph, *, reply_language: str | None = None
|
|
) -> tuple[list[MutationIntent], Risk]: ...
|
|
|
|
def generate_mock_inputs(
|
|
self, schema: StartSchema, prior_failed: Inputs, *, business_context: str = ""
|
|
) -> Inputs: ...
|
|
|
|
# -- Build cognition --
|
|
|
|
def analyze_goal(self, goal_text: str, *, reply_language: str | None = None) -> 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], *, goal_text: str, reply_language: str | None = None
|
|
) -> WorkflowPlan | None: ...
|
|
|
|
def discover_resources(
|
|
self,
|
|
plan_items: list[str],
|
|
*,
|
|
goal_text: str,
|
|
requirements: dict[str, Any],
|
|
reply_language: str | None = None,
|
|
) -> 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], *, goal_text: str, requirements: dict[str, Any]
|
|
) -> 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,
|
|
*,
|
|
trusted_text: str = "",
|
|
reply_language: 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, *, reply_language: str | None = None) -> dict[str, Any]: ...
|
|
|
|
# None means no complete proposal was generated; retain the current plan.
|
|
def propose_edit_plan(
|
|
self, edit_rules: dict[str, Any], graph: Graph, *, reply_language: str | None = None
|
|
) -> WorkflowPlan | None: ...
|
|
|
|
# ``edit_target_node_ids``: the nodes analyze_impact said the change
|
|
# touches; an implementation may show the model their current config so it
|
|
# can leave what it was not asked about alone. ``last_edit_rejection``:
|
|
# why the previous attempt was refused, for a corrective re-prompt. Both
|
|
# are keyword-only and optional -- an agent is free to ignore either.
|
|
# ``approved_plan`` is the presentation snapshot the user approved, so the
|
|
# generator can apply that plan together with the submitted edit rules.
|
|
#
|
|
# MAY raise ``errors.ProposalWouldRunWrongError`` instead of returning, when
|
|
# the implementation has judged its own final proposal wrong -- a batch that
|
|
# would apply cleanly, start, run green and not do what was asked. Nothing
|
|
# has been written; the handler surfaces the reason at the approval gate and
|
|
# carries it into the next attempt. An agent with no such judgement (the
|
|
# placeholder) simply never raises it, so this stays optional.
|
|
def build_edit_intents(
|
|
self,
|
|
edit_rules: dict[str, Any],
|
|
graph: Graph,
|
|
*,
|
|
edit_target_node_ids: Sequence[str] = (),
|
|
last_edit_rejection: str | None = None,
|
|
approved_plan: WorkflowPlan | None = None,
|
|
) -> 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 detector uses it as its model
|
|
# provider. Implementers that have no model return None.
|
|
def model_or_none(self) -> object | None: ...
|
|
|
|
|
|
@runtime_checkable
|
|
class DiagnosticReportingAgent(Protocol):
|
|
"""Optional capability for operation-scoped diagnostic events."""
|
|
|
|
def set_diagnostic_callback(self, callback: Callable[[dict], None] | None) -> None: ...
|
|
|
|
|
|
@runtime_checkable
|
|
class ContinuationClassifyingAgent(Protocol):
|
|
"""Optional capability for continuing a completed Builder session."""
|
|
|
|
def continuation_intent(self, context: DifyBuilderContext, graph: Graph, text: str) -> str: ...
|
|
|
|
|
|
@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 stamp_http_fixtures(
|
|
self, app_id: str, actor: Actor, *, base_app_revision: str, fixtures: tuple[HttpResponseFixtureV1, ...]
|
|
) -> HttpFixtureSetV1: ...
|
|
|
|
def get_app_mode(self, app_id: str, actor: Actor) -> str:
|
|
"""Return the persisted app mode after actor/tenant-scoped authorization."""
|
|
...
|
|
|
|
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,
|
|
failed_run_id: str | None = None,
|
|
business_failure_run_id: str | None = None,
|
|
confirmed_resource_ids: Sequence[str] | None = None,
|
|
expected_resource_fingerprints: dict[str, str] | None = None,
|
|
expected_node_resources: dict[str, str] | None = None,
|
|
) -> ApplyResult: ...
|
|
|
|
def run_draft(
|
|
self,
|
|
app_id: str,
|
|
actor: Actor,
|
|
inputs: Inputs,
|
|
on_event: Callable[[NodeEvent], None],
|
|
*,
|
|
session_id: str,
|
|
test_input_id: str,
|
|
on_workflow_event: Callable[[Mapping[str, object]], None] | None = None,
|
|
confirmed_consent_id: str | None = None,
|
|
command_id: str | None = None,
|
|
effect_confirmation: Mapping[str, str] | None = None,
|
|
) -> Run:
|
|
"""Run the saved draft test. Both confirmed IDs ``None``: the default restricted run.
|
|
|
|
Both set: launch the server-reserved confirmed command for this
|
|
invocation only; the adapter reloads the reservation and refuses a
|
|
mismatch. Supplying exactly one is a refusal, never a fallback.
|
|
|
|
``effect_confirmation`` (node ID -> effect signature, see
|
|
``disclose_effects``) applies only when the draft takes the confirmed
|
|
full run: a side effect runs for real only while its signature still
|
|
matches, and every other side effect or destructive node is mocked.
|
|
"""
|
|
...
|
|
|
|
def prepare_code_risks(
|
|
self,
|
|
app_id: str,
|
|
actor: Actor,
|
|
*,
|
|
session_id: str,
|
|
inputs: Mapping[str, Any],
|
|
context: DifyBuilderContext,
|
|
) -> dict[str, dict[str, Any]]:
|
|
"""Prepare/reuse one bounded explanation using trusted in-turn cognition."""
|
|
...
|
|
|
|
def disclose_effects(
|
|
self,
|
|
app_id: str,
|
|
actor: Actor,
|
|
*,
|
|
inputs: Mapping[str, Any] | None = None,
|
|
code_risk_context: DifyBuilderContext | None = None,
|
|
) -> EffectDisclosure:
|
|
"""What a Test Run of the current draft would reach outside Dify (PRD §9.1).
|
|
|
|
``inputs`` binds direct Start references to submitted test values; omit
|
|
it to inspect configuration alone. Empty when nothing needs disclosure.
|
|
"""
|
|
...
|
|
|
|
def publish(
|
|
self, app_id: str, actor: Actor, *, expected_revision: str, expected_graph_revision: str
|
|
) -> PublishResult: ...
|
|
|
|
def graph_revision(self, graph: Graph) -> str:
|
|
"""Opaque executable graph digest, separate from the full execution revision."""
|
|
...
|
|
|
|
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 get_session_at_version(self, session_id: str, version: int) -> tuple[Session, DifyBuilderContext]: ...
|
|
|
|
def is_task_active(self, session_id: str, expected_generation: int) -> bool: ...
|
|
|
|
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: ...
|