mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
206 lines
8.6 KiB
Python
206 lines
8.6 KiB
Python
"""The real ``DifyPort`` adapter -- direct in-process calls into Dify's own services.
|
|
|
|
Port of dify-enterprise/server/pkg/enterprise/data/copilot_dify.go, minus the
|
|
token-forwarding (the P1 port plan's Global Constraints / ADR drop
|
|
``ForwardAuth`` entirely -- see ``core.workflow_copilot.ports.DifyPort``).
|
|
|
|
``WorkflowServiceDifyPort`` wires the pure P1 domain (``core.workflow_copilot``)
|
|
and the pure Task 1-3 helpers in this package (``graph_ops``, ``run_mapping``,
|
|
``identity``) to ``WorkflowService`` / ``AppGenerateService`` / the node-
|
|
execution repository. It satisfies ``DifyPort`` structurally (the Protocol is
|
|
``@runtime_checkable``) -- there is no explicit inheritance, matching
|
|
``PlaceholderAgent``'s relationship to ``CopilotAgent``.
|
|
|
|
Each method opens its own ``Session`` via
|
|
``sessionmaker(bind=db.engine, expire_on_commit=False)`` and resolves
|
|
account/app through the Task 3 helpers. The OSS gotchas baked in here (see
|
|
the P2 plan's Global Constraints):
|
|
|
|
- ``sync_draft_workflow`` has no server-side patch primitive -- ``apply_repair``
|
|
reads the whole draft graph, mutates it client-side via
|
|
``graph_ops.apply_set_node_config``, and writes the whole mutated graph
|
|
back with ``graph_only=True``/``sync_agent_bindings=False`` so only the
|
|
graph changes, nothing else. A ``WorkflowHashNotEqualError`` (the draft
|
|
changed since it was read) is re-mapped to the domain ``HashMismatchError``.
|
|
- ``run_draft`` invokes ``AppGenerateService.generate`` with
|
|
``invoke_from=InvokeFrom.DEBUGGER`` (runs the *draft*, not the published
|
|
workflow) and ``streaming=False`` (blocks in-process -- safe to call from a
|
|
worker/Celery task, no SSE plumbing needed).
|
|
- ``publish`` MUST update ``app.workflow_id`` in the same transaction as
|
|
``publish_workflow``: ``publish_workflow`` only creates the new published
|
|
``Workflow`` row: it does not repoint the app at it. Skipping that update
|
|
makes publish a silent no-op. The agent-retirement tail
|
|
(``WorkflowAgentRetirementService.retire_unowned`` /
|
|
``enqueue_agent_resource_collection``) is intentionally skipped -- it only
|
|
matters for Agent-node workflows and is not part of this port's contract.
|
|
"""
|
|
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
|
from core.workflow_copilot.models import (
|
|
Actor,
|
|
ApplyResult,
|
|
Graph,
|
|
Inputs,
|
|
MutationIntent,
|
|
NodeEvent,
|
|
NodeOutput,
|
|
Run,
|
|
)
|
|
from extensions.ext_database import db
|
|
from libs.datetime_utils import naive_utc_now
|
|
from models.model import App
|
|
from models.workflow import Workflow
|
|
from repositories.factory import DifyAPIRepositoryFactory
|
|
from services.app_generate_service import AppGenerateService
|
|
from services.errors.app import WorkflowHashNotEqualError
|
|
from services.workflow_copilot.errors import HashMismatchError, WorkflowNotInitializedError
|
|
from services.workflow_copilot.graph_ops import apply_set_node_config
|
|
from services.workflow_copilot.identity import load_app, resolve_account
|
|
from services.workflow_copilot.run_mapping import map_run_result, to_node_event
|
|
from services.workflow_service import WorkflowService
|
|
|
|
__all__ = ["WorkflowServiceDifyPort"]
|
|
|
|
|
|
def _session_factory() -> sessionmaker[Session]:
|
|
"""One session per adapter call -- never shared/held across calls."""
|
|
return sessionmaker(bind=db.engine, expire_on_commit=False)
|
|
|
|
|
|
def _unwrap_status(status: Any) -> Any:
|
|
"""Unwrap a ``graphon`` ``StrEnum``'s ``.value`` if present, else pass through.
|
|
|
|
Mirrors ``run_mapping._status_value`` locally so this module doesn't reach
|
|
into another module's private helper.
|
|
"""
|
|
return getattr(status, "value", status)
|
|
|
|
|
|
def _load_draft_workflow_or_raise(app: App, *, session: Session) -> Workflow:
|
|
workflow = WorkflowService().get_draft_workflow(app, session=session)
|
|
if workflow is None:
|
|
raise WorkflowNotInitializedError(f"app has no draft workflow: {app.id}")
|
|
return workflow
|
|
|
|
|
|
class WorkflowServiceDifyPort:
|
|
"""``DifyPort`` implementation backed directly by Dify's own services."""
|
|
|
|
def read_graph(self, app_id: str, actor: Actor) -> tuple[Graph, str]:
|
|
with _session_factory()() as session:
|
|
app = load_app(session, app_id, actor)
|
|
workflow = _load_draft_workflow_or_raise(app, session=session)
|
|
return dict(workflow.graph_dict), workflow.unique_hash
|
|
|
|
def node_outputs(self, app_id: str, actor: Actor, run_id: str) -> list[NodeOutput]:
|
|
with _session_factory()() as session:
|
|
app = load_app(session, app_id, actor)
|
|
tenant_id = app.tenant_id
|
|
|
|
node_execs = DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository(
|
|
sessionmaker(bind=db.engine)
|
|
).get_executions_by_workflow_run(tenant_id, app_id, run_id)
|
|
|
|
return [
|
|
NodeOutput(
|
|
node_id=node_exec.node_id,
|
|
title=node_exec.title,
|
|
status=_unwrap_status(node_exec.status),
|
|
error=node_exec.error or "",
|
|
inputs=node_exec.inputs_dict or {},
|
|
outputs=node_exec.outputs_dict or {},
|
|
)
|
|
for node_exec in node_execs
|
|
]
|
|
|
|
def apply_repair(self, app_id: str, actor: Actor, intents: list[MutationIntent]) -> ApplyResult:
|
|
with _session_factory()() as session:
|
|
account = resolve_account(session, actor)
|
|
app = load_app(session, app_id, actor)
|
|
workflow = _load_draft_workflow_or_raise(app, session=session)
|
|
|
|
graph: Graph = dict(workflow.graph_dict)
|
|
unique_hash = workflow.unique_hash
|
|
changed_nodes: list[str] = []
|
|
for intent in intents:
|
|
if intent.op != "set_node_config":
|
|
continue
|
|
graph, changed = apply_set_node_config(
|
|
graph, intent.args["node_id"], intent.args["path"], intent.args["value"]
|
|
)
|
|
changed_nodes.extend(changed)
|
|
|
|
if not changed_nodes:
|
|
return ApplyResult(changed_nodes=[], new_hash=unique_hash)
|
|
|
|
try:
|
|
updated = WorkflowService().sync_draft_workflow(
|
|
app_model=app,
|
|
graph=graph,
|
|
features=dict(workflow.features_dict),
|
|
unique_hash=unique_hash,
|
|
account=account,
|
|
environment_variables=[],
|
|
conversation_variables=workflow.conversation_variables,
|
|
session=session,
|
|
graph_only=True,
|
|
sync_agent_bindings=False,
|
|
commit=True,
|
|
preserve_environment_variables=True,
|
|
)
|
|
except WorkflowHashNotEqualError as exc:
|
|
raise HashMismatchError(f"draft workflow changed since read: {app_id}") from exc
|
|
|
|
return ApplyResult(changed_nodes=changed_nodes, new_hash=updated.unique_hash)
|
|
|
|
def run_draft(self, app_id: str, actor: Actor, inputs: Inputs, on_event: Callable[[NodeEvent], None]) -> Run:
|
|
with _session_factory()() as session:
|
|
account = resolve_account(session, actor)
|
|
app = load_app(session, app_id, actor)
|
|
tenant_id = app.tenant_id
|
|
|
|
response = AppGenerateService.generate(
|
|
app_model=app,
|
|
user=account,
|
|
args={"inputs": inputs},
|
|
invoke_from=InvokeFrom.DEBUGGER,
|
|
session=session,
|
|
streaming=False,
|
|
)
|
|
|
|
data = response["data"]
|
|
run_id = data["id"]
|
|
|
|
node_execs = DifyAPIRepositoryFactory.create_api_workflow_node_execution_repository(
|
|
sessionmaker(bind=db.engine)
|
|
).get_executions_by_workflow_run(tenant_id, app_id, run_id)
|
|
|
|
for node_exec in node_execs:
|
|
on_event(to_node_event(node_exec))
|
|
|
|
return map_run_result(data, node_execs)
|
|
|
|
def publish(self, app_id: str, actor: Actor) -> None:
|
|
with _session_factory()() as session:
|
|
account = resolve_account(session, actor)
|
|
app = load_app(session, app_id, actor)
|
|
|
|
workflow, _retirement_candidates = WorkflowService().publish_workflow(
|
|
session=session,
|
|
app_model=app,
|
|
account=account,
|
|
)
|
|
|
|
app_in_session = session.get(App, app_id)
|
|
assert app_in_session is not None, f"app disappeared mid-transaction: {app_id}"
|
|
app_in_session.workflow_id = workflow.id
|
|
app_in_session.updated_by = account.id
|
|
app_in_session.updated_at = naive_utc_now()
|
|
|
|
session.commit()
|