mirror of
https://github.com/langgenius/dify.git
synced 2026-06-26 14:51:13 +08:00
fix(agent-v2): recover inline binding during draft refresh
This commit is contained in:
parent
47de929a00
commit
b72a51cf65
@ -9,6 +9,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.workflow.nodes.agent_v2.validators import WorkflowAgentNodeValidationError, WorkflowAgentNodeValidator
|
||||
from models import ToolFile, UploadFile
|
||||
from models.agent import (
|
||||
Agent,
|
||||
AgentConfigSnapshot,
|
||||
@ -19,7 +20,6 @@ from models.agent import (
|
||||
WorkflowAgentNodeBinding,
|
||||
)
|
||||
from models.agent_config_entities import AgentSoulConfig, DeclaredOutputConfig, WorkflowNodeJobConfig
|
||||
from models import ToolFile, UploadFile
|
||||
from models.workflow import Workflow
|
||||
from services.agent.composer_validator import ComposerConfigValidator
|
||||
from services.agent.soul_files_service import AgentSoulFilesService
|
||||
@ -41,10 +41,10 @@ class WorkflowAgentPublishService:
|
||||
|
||||
@classmethod
|
||||
def project_draft_bindings_to_graph(cls, *, session: Session, draft_workflow: Workflow) -> dict[str, Any]:
|
||||
"""Return draft graph with persisted Agent node job config projected into node data.
|
||||
"""Return draft graph with persisted Agent binding fields projected into node data.
|
||||
|
||||
Workflow draft graph is the front-end's editing source of truth, while
|
||||
runtime/publish reads WorkflowAgentNodeBinding.node_job_config. This
|
||||
runtime/publish reads WorkflowAgentNodeBinding. This
|
||||
response-only projection keeps reads aligned without writing binding
|
||||
details back into the stored graph JSON.
|
||||
"""
|
||||
@ -66,6 +66,18 @@ class WorkflowAgentPublishService:
|
||||
node_data = agent_nodes.get(binding.node_id)
|
||||
if not isinstance(node_data, dict):
|
||||
continue
|
||||
graph_binding = node_data.get(cls._AGENT_BINDING_KEY)
|
||||
is_pending_inline_graph_binding = (
|
||||
isinstance(graph_binding, Mapping)
|
||||
and graph_binding.get("binding_type") == WorkflowAgentBindingType.INLINE_AGENT.value
|
||||
and (not graph_binding.get("agent_id") or not graph_binding.get("current_snapshot_id"))
|
||||
)
|
||||
if not is_pending_inline_graph_binding or binding.binding_type == WorkflowAgentBindingType.INLINE_AGENT:
|
||||
node_data[cls._AGENT_BINDING_KEY] = {
|
||||
"binding_type": binding.binding_type.value,
|
||||
"agent_id": binding.agent_id,
|
||||
"current_snapshot_id": binding.current_snapshot_id,
|
||||
}
|
||||
node_job = WorkflowNodeJobConfig.model_validate(binding.node_job_config_dict)
|
||||
if node_job.workflow_prompt is not None:
|
||||
node_data[cls._AGENT_TASK_KEY] = node_job.workflow_prompt
|
||||
@ -273,6 +285,11 @@ class WorkflowAgentPublishService:
|
||||
continue
|
||||
if not isinstance(binding_payload, Mapping):
|
||||
raise ValueError(f"Workflow Agent node {node_id} has invalid agent_binding.")
|
||||
if (
|
||||
binding_payload.get("binding_type") == WorkflowAgentBindingType.INLINE_AGENT.value
|
||||
and (not binding_payload.get("agent_id") or not binding_payload.get("current_snapshot_id"))
|
||||
):
|
||||
continue
|
||||
cls._sync_agent_binding_for_node(
|
||||
session=session,
|
||||
draft_workflow=draft_workflow,
|
||||
|
||||
@ -2878,6 +2878,11 @@ class TestWorkflowAgentDraftBindingSync:
|
||||
)
|
||||
|
||||
node_data = graph["nodes"][0]["data"]
|
||||
assert node_data["agent_binding"] == {
|
||||
"binding_type": "roster_agent",
|
||||
"agent_id": "agent-1",
|
||||
"current_snapshot_id": "snapshot-1",
|
||||
}
|
||||
assert node_data["agent_task"] == "Summarize the upstream result."
|
||||
assert node_data["agent_declared_outputs"][0]["name"] == "summary"
|
||||
assert node_data["agent_declared_outputs"][0]["type"] == "string"
|
||||
@ -2887,6 +2892,103 @@ class TestWorkflowAgentDraftBindingSync:
|
||||
assert profile_output["children"][1]["array_item"]["children"][0]["name"] == "city"
|
||||
assert "agent_declared_outputs" not in workflow.graph_dict["nodes"][0]["data"]
|
||||
|
||||
def test_projects_inline_binding_over_pending_inline_graph_response(self):
|
||||
workflow = Workflow(
|
||||
id="workflow-1",
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
version=Workflow.VERSION_DRAFT,
|
||||
graph=json.dumps(
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "agent-node",
|
||||
"data": {
|
||||
"type": "agent",
|
||||
"version": "2",
|
||||
"agent_binding": {
|
||||
"binding_type": "inline_agent",
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
}
|
||||
),
|
||||
)
|
||||
binding = WorkflowAgentNodeBinding(
|
||||
id="binding-1",
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
workflow_id="workflow-1",
|
||||
workflow_version=Workflow.VERSION_DRAFT,
|
||||
node_id="agent-node",
|
||||
binding_type=WorkflowAgentBindingType.INLINE_AGENT,
|
||||
agent_id="inline-agent-1",
|
||||
current_snapshot_id="inline-snapshot-1",
|
||||
)
|
||||
session = FakeSession(scalars=[[binding]])
|
||||
|
||||
graph = WorkflowAgentPublishService.project_draft_bindings_to_graph(
|
||||
session=session,
|
||||
draft_workflow=workflow,
|
||||
)
|
||||
|
||||
assert graph["nodes"][0]["data"]["agent_binding"] == {
|
||||
"binding_type": "inline_agent",
|
||||
"agent_id": "inline-agent-1",
|
||||
"current_snapshot_id": "inline-snapshot-1",
|
||||
}
|
||||
assert workflow.graph_dict["nodes"][0]["data"]["agent_binding"] == {
|
||||
"binding_type": "inline_agent",
|
||||
}
|
||||
|
||||
def test_keeps_pending_inline_graph_response_over_existing_roster_binding(self):
|
||||
workflow = Workflow(
|
||||
id="workflow-1",
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
version=Workflow.VERSION_DRAFT,
|
||||
graph=json.dumps(
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "agent-node",
|
||||
"data": {
|
||||
"type": "agent",
|
||||
"version": "2",
|
||||
"agent_binding": {
|
||||
"binding_type": "inline_agent",
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
"edges": [],
|
||||
}
|
||||
),
|
||||
)
|
||||
binding = WorkflowAgentNodeBinding(
|
||||
id="binding-1",
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
workflow_id="workflow-1",
|
||||
workflow_version=Workflow.VERSION_DRAFT,
|
||||
node_id="agent-node",
|
||||
binding_type=WorkflowAgentBindingType.ROSTER_AGENT,
|
||||
agent_id="agent-1",
|
||||
current_snapshot_id="snapshot-1",
|
||||
)
|
||||
session = FakeSession(scalars=[[binding]])
|
||||
|
||||
graph = WorkflowAgentPublishService.project_draft_bindings_to_graph(
|
||||
session=session,
|
||||
draft_workflow=workflow,
|
||||
)
|
||||
|
||||
assert graph["nodes"][0]["data"]["agent_binding"] == {
|
||||
"binding_type": "inline_agent",
|
||||
}
|
||||
|
||||
def test_creates_roster_binding_from_agent_node_graph(self):
|
||||
workflow = Workflow(
|
||||
id="workflow-1",
|
||||
@ -3014,6 +3116,52 @@ class TestWorkflowAgentDraftBindingSync:
|
||||
workflow_prompt="Use the current node context.",
|
||||
).model_dump(mode="json")
|
||||
|
||||
def test_keeps_pending_inline_binding_in_draft_graph_without_db_binding(self):
|
||||
workflow = Workflow(
|
||||
id="workflow-1",
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
version=Workflow.VERSION_DRAFT,
|
||||
graph=json.dumps(
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "agent-node",
|
||||
"data": {
|
||||
"type": "agent",
|
||||
"version": "2",
|
||||
"agent_binding": {
|
||||
"binding_type": "inline_agent",
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
)
|
||||
existing_binding = WorkflowAgentNodeBinding(
|
||||
id="binding-1",
|
||||
tenant_id="tenant-1",
|
||||
app_id="app-1",
|
||||
workflow_id="workflow-1",
|
||||
workflow_version=Workflow.VERSION_DRAFT,
|
||||
node_id="agent-node",
|
||||
binding_type=WorkflowAgentBindingType.ROSTER_AGENT,
|
||||
agent_id="agent-1",
|
||||
current_snapshot_id="snapshot-1",
|
||||
)
|
||||
session = FakeSession(scalars=[[existing_binding]])
|
||||
|
||||
WorkflowAgentPublishService.sync_agent_bindings_for_draft(
|
||||
session=session,
|
||||
draft_workflow=workflow,
|
||||
account_id="account-1",
|
||||
)
|
||||
|
||||
assert session.deleted == []
|
||||
assert session.added == []
|
||||
assert session.flushes == 1
|
||||
|
||||
def test_rejects_inline_binding_for_agent_owned_by_another_node(self):
|
||||
workflow = Workflow(
|
||||
id="workflow-1",
|
||||
@ -3093,7 +3241,7 @@ class TestWorkflowAgentDraftBindingSync:
|
||||
account_id="account-1",
|
||||
)
|
||||
|
||||
def test_rejects_inline_binding_without_current_snapshot_id(self):
|
||||
def test_treats_partial_inline_binding_as_pending_draft_state(self):
|
||||
workflow = Workflow(
|
||||
id="workflow-1",
|
||||
tenant_id="tenant-1",
|
||||
@ -3118,12 +3266,17 @@ class TestWorkflowAgentDraftBindingSync:
|
||||
),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="inline_agent binding requires current_snapshot_id"):
|
||||
WorkflowAgentPublishService.sync_agent_bindings_for_draft(
|
||||
session=FakeSession(scalars=[[]]),
|
||||
draft_workflow=workflow,
|
||||
account_id="account-1",
|
||||
)
|
||||
session = FakeSession(scalars=[[]])
|
||||
|
||||
WorkflowAgentPublishService.sync_agent_bindings_for_draft(
|
||||
session=session,
|
||||
draft_workflow=workflow,
|
||||
account_id="account-1",
|
||||
)
|
||||
|
||||
assert session.added == []
|
||||
assert session.deleted == []
|
||||
assert session.flushes == 1
|
||||
|
||||
def test_rejects_inline_binding_with_missing_snapshot(self):
|
||||
workflow = Workflow(
|
||||
|
||||
@ -320,6 +320,9 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () =>
|
||||
desc: '',
|
||||
agent_node_kind: 'dify_agent',
|
||||
version: '2',
|
||||
agent_binding: {
|
||||
binding_type: 'inline_agent',
|
||||
},
|
||||
selected: true,
|
||||
},
|
||||
},
|
||||
|
||||
@ -71,9 +71,6 @@ const useNodesSyncDraftBase = (getNodesReadOnly: () => boolean) => {
|
||||
const features = featuresStore!.getState().features
|
||||
const producedNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (isAgentV2NodeData(node.data) && needsInlineAgentBindingCreation(node.data))
|
||||
delete node.data.agent_binding
|
||||
|
||||
Object.keys(node.data).forEach((key) => {
|
||||
if (key.startsWith('_'))
|
||||
delete node.data[key]
|
||||
|
||||
@ -96,6 +96,7 @@ const CandidateNodeMain: FC<Props> = ({
|
||||
|
||||
if (shouldCreateInlineAgentBinding) {
|
||||
workflowStore.getState().setOpenInlineAgentPanelNodeId(candidateNode.id)
|
||||
handleSyncWorkflowDraft(true, true)
|
||||
createInlineAgentBinding(candidateNode.id, {
|
||||
onError: () => {
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
|
||||
@ -194,6 +194,7 @@ export const useNodesInteractions = () => {
|
||||
onError?: () => void
|
||||
}) => {
|
||||
workflowStore.getState().setOpenInlineAgentPanelNodeId(nodeId)
|
||||
handleSyncWorkflowDraft(true, true)
|
||||
createInlineAgentBinding(nodeId, {
|
||||
onError: () => {
|
||||
options?.onError?.()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user