mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 13:51:25 +08:00
Track backend integration, Ruff cleanup and migration reconciliation in ENG-1271. Retain required copy contracts without implementing frontend interactions.
188 lines
7.7 KiB
Python
188 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from core.dify_builder.models import Actor
|
|
from services.dify_builder.native_testing.agent_binding import AgentBindingAuthorization
|
|
|
|
from sqlalchemy import select
|
|
|
|
from core.agent.publish_visibility import workflow_callable_active_snapshot_filter
|
|
from core.db.session_factory import session_factory
|
|
from core.workflow.nodes.agent_v2.session_store import (
|
|
WorkflowAgentWorkspaceStore,
|
|
resolve_workflow_agent_workspace_owner_scope,
|
|
)
|
|
from models.agent import (
|
|
Agent,
|
|
AgentConfigSnapshot,
|
|
AgentConfigVersionKind,
|
|
AgentScope,
|
|
AgentStatus,
|
|
WorkflowAgentBindingType,
|
|
WorkflowAgentNodeBinding,
|
|
)
|
|
|
|
|
|
class WorkflowAgentBindingError(Exception):
|
|
error_code: str
|
|
|
|
def __init__(self, error_code: str, message: str) -> None:
|
|
self.error_code = error_code
|
|
super().__init__(message)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class WorkflowAgentBindingBundle:
|
|
binding: WorkflowAgentNodeBinding
|
|
agent: Agent
|
|
snapshot: AgentConfigSnapshot
|
|
|
|
|
|
class WorkflowAgentBindingResolver:
|
|
"""Resolve an owned binding without allowing unpublished roster snapshots to run."""
|
|
|
|
def resolve(
|
|
self,
|
|
*,
|
|
tenant_id: str,
|
|
app_id: str,
|
|
workflow_id: str,
|
|
node_id: str,
|
|
binding_id: str | None = None,
|
|
snapshot_id: str | None = None,
|
|
conversation_id: str | None = None,
|
|
builder_actor: Actor | None = None,
|
|
builder_authorization: AgentBindingAuthorization | None = None,
|
|
selected_agent_id: str | None = None,
|
|
selected_snapshot_id: str | None = None,
|
|
) -> WorkflowAgentBindingBundle:
|
|
"""Resolve the generation pinned by an execution or a Chatflow conversation participant."""
|
|
|
|
if (binding_id is None) != (snapshot_id is None):
|
|
raise WorkflowAgentBindingError(
|
|
"agent_binding_generation_invalid",
|
|
"Workflow Agent binding and config snapshot must be pinned together.",
|
|
)
|
|
|
|
if (selected_agent_id is not None or selected_snapshot_id is not None) and builder_actor is None:
|
|
raise WorkflowAgentBindingError(
|
|
"agent_binding_owner_mismatch", "Builder actor required for snapshot selection."
|
|
)
|
|
pinned_execution = snapshot_id is not None
|
|
with session_factory.create_session() as session:
|
|
binding_stmt = select(WorkflowAgentNodeBinding).where(
|
|
WorkflowAgentNodeBinding.tenant_id == tenant_id,
|
|
WorkflowAgentNodeBinding.app_id == app_id,
|
|
WorkflowAgentNodeBinding.workflow_id == workflow_id,
|
|
WorkflowAgentNodeBinding.node_id == node_id,
|
|
)
|
|
if binding_id is not None:
|
|
binding_stmt = binding_stmt.where(WorkflowAgentNodeBinding.id == binding_id)
|
|
binding = session.scalar(binding_stmt.limit(1))
|
|
if binding is None:
|
|
raise WorkflowAgentBindingError(
|
|
"agent_binding_not_found",
|
|
f"Workflow Agent binding not found for node {node_id}.",
|
|
)
|
|
if binding.agent_id is None:
|
|
raise WorkflowAgentBindingError("agent_not_available", "Workflow Agent binding has no agent.")
|
|
|
|
if builder_actor is not None:
|
|
from services.dify_builder.native_testing.agent_binding import AgentBindingService
|
|
|
|
if builder_actor.tenant_id != tenant_id or (
|
|
selected_agent_id is not None
|
|
and (
|
|
binding.agent_id != selected_agent_id
|
|
or binding.binding_type != WorkflowAgentBindingType.ROSTER_AGENT
|
|
)
|
|
):
|
|
raise WorkflowAgentBindingError("agent_binding_owner_mismatch", "Builder actor owner mismatch.")
|
|
AgentBindingService(session, authorization=builder_authorization).recheck_access(
|
|
actor=builder_actor, app_id=app_id, agent_id=binding.agent_id
|
|
)
|
|
|
|
agent_stmt = select(Agent).where(
|
|
Agent.tenant_id == tenant_id,
|
|
Agent.id == binding.agent_id,
|
|
)
|
|
if binding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT:
|
|
agent_stmt = agent_stmt.where(
|
|
Agent.scope == AgentScope.ROSTER,
|
|
workflow_callable_active_snapshot_filter(),
|
|
)
|
|
agent = session.scalar(agent_stmt.limit(1))
|
|
if agent is None or agent.status == AgentStatus.ARCHIVED:
|
|
raise WorkflowAgentBindingError(
|
|
"agent_not_available",
|
|
f"Agent {binding.agent_id} is not available or has not been published.",
|
|
)
|
|
|
|
# A new node execution in the same conversation must keep its participant's
|
|
# config/Home generation even after the roster Agent publishes a new version.
|
|
if snapshot_id is None and conversation_id:
|
|
participant = WorkflowAgentWorkspaceStore.load_active_participant(
|
|
session=session,
|
|
scope=resolve_workflow_agent_workspace_owner_scope(
|
|
tenant_id=tenant_id,
|
|
app_id=app_id,
|
|
conversation_id=conversation_id,
|
|
workflow_run_id=None,
|
|
node_id=node_id,
|
|
workflow_agent_binding_id=binding.id,
|
|
),
|
|
agent_id=agent.id,
|
|
)
|
|
if participant is not None:
|
|
if participant.agent_config_version_kind != AgentConfigVersionKind.SNAPSHOT:
|
|
raise WorkflowAgentBindingError(
|
|
"agent_binding_generation_invalid",
|
|
"Chatflow Agent participant must reference a config snapshot.",
|
|
)
|
|
snapshot_id = participant.agent_config_version_id
|
|
|
|
effective_snapshot_id = (
|
|
(
|
|
agent.active_config_snapshot_id
|
|
if binding.binding_type == WorkflowAgentBindingType.ROSTER_AGENT
|
|
else binding.current_snapshot_id
|
|
)
|
|
if snapshot_id is None
|
|
else snapshot_id
|
|
)
|
|
if effective_snapshot_id is None:
|
|
raise WorkflowAgentBindingError(
|
|
"agent_config_snapshot_not_found",
|
|
"Workflow Agent binding has no current config snapshot.",
|
|
)
|
|
|
|
if selected_snapshot_id is not None and not pinned_execution:
|
|
from services.dify_builder.native_testing.agent_binding import require_selected_snapshot
|
|
|
|
require_selected_snapshot(
|
|
selected_snapshot_id=selected_snapshot_id, effective_snapshot_id=effective_snapshot_id
|
|
)
|
|
|
|
snapshot = session.scalar(
|
|
select(AgentConfigSnapshot)
|
|
.where(
|
|
AgentConfigSnapshot.tenant_id == tenant_id,
|
|
AgentConfigSnapshot.agent_id == agent.id,
|
|
AgentConfigSnapshot.id == effective_snapshot_id,
|
|
)
|
|
.limit(1)
|
|
)
|
|
if snapshot is None:
|
|
raise WorkflowAgentBindingError(
|
|
"agent_config_snapshot_not_found",
|
|
f"Agent config snapshot {effective_snapshot_id} not found.",
|
|
)
|
|
|
|
session.expunge(binding)
|
|
session.expunge(agent)
|
|
session.expunge(snapshot)
|
|
return WorkflowAgentBindingBundle(binding=binding, agent=agent, snapshot=snapshot)
|