feat(api): adjust form submission run api
Some checks are pending
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions

Separate `inputs` and `form_inputs` fields.
This commit is contained in:
QuantumGhost 2026-01-13 11:08:31 +08:00
parent 99937aba2e
commit 9c287ee0ae
4 changed files with 92 additions and 75 deletions

View File

@ -528,13 +528,29 @@ class WorkflowDraftRunLoopNodeApi(Resource):
raise InternalServerError()
class HumanInputSubmitPayload(BaseModel):
inputs: dict[str, Any]
action: str
class HumanInputFormPreviewPayload(BaseModel):
inputs: dict[str, Any] = Field(
default_factory=dict,
description="Values used to fill missing upstream variables referenced in form_content",
)
class HumanInputFormSubmitPayload(BaseModel):
form_inputs: dict[str, Any] = Field(..., description="Values the user provides for the form's own fields")
inputs: dict[str, Any] = Field(
...,
description="Values used to fill missing upstream variables referenced in form_content",
)
action: str = Field(..., description="Selected action ID")
class HumanInputDeliveryTestPayload(BaseModel):
delivery_method_id: str
delivery_method_id: str = Field(..., description="Delivery method ID")
reg(HumanInputFormPreviewPayload)
reg(HumanInputFormSubmitPayload)
reg(HumanInputDeliveryTestPayload)
@console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflows/draft/human-input/nodes/<string:node_id>/form/preview")
@ -542,6 +558,7 @@ class AdvancedChatDraftHumanInputFormPreviewApi(Resource):
@console_ns.doc("get_advanced_chat_draft_human_input_form")
@console_ns.doc(description="Get human input form preview for advanced chat workflow")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models[HumanInputFormPreviewPayload.__name__])
@setup_required
@login_required
@account_initialization_required
@ -552,19 +569,15 @@ class AdvancedChatDraftHumanInputFormPreviewApi(Resource):
Preview human input form content and placeholders
"""
current_user, _ = current_account_with_tenant()
payload = request.get_json(silent=True) or {}
manual_inputs = payload.get("inputs") if isinstance(payload, dict) else {}
if manual_inputs is None:
manual_inputs = {}
if not isinstance(manual_inputs, dict):
raise ValueError("inputs must be an object")
args = HumanInputFormPreviewPayload.model_validate(console_ns.payload or {})
inputs = args.inputs
workflow_service = WorkflowService()
preview = workflow_service.get_human_input_form_preview(
app_model=app_model,
account=current_user,
node_id=node_id,
manual_inputs=manual_inputs,
inputs=inputs,
)
return jsonable_encoder(preview)
@ -574,15 +587,7 @@ class AdvancedChatDraftHumanInputFormRunApi(Resource):
@console_ns.doc("submit_advanced_chat_draft_human_input_form")
@console_ns.doc(description="Submit human input form preview for advanced chat workflow")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(
console_ns.model(
"AdvancedChatHumanInputFormSubmitRequest",
{
"inputs": fields.Raw(required=True, description="Form input values"),
"action": fields.String(required=True, description="Selected action ID"),
},
)
)
@console_ns.expect(console_ns.models[HumanInputFormSubmitPayload.__name__])
@setup_required
@login_required
@account_initialization_required
@ -593,13 +598,14 @@ class AdvancedChatDraftHumanInputFormRunApi(Resource):
Submit human input form preview
"""
current_user, _ = current_account_with_tenant()
args = HumanInputSubmitPayload.model_validate(console_ns.payload or {})
args = HumanInputFormSubmitPayload.model_validate(console_ns.payload or {})
workflow_service = WorkflowService()
result = workflow_service.submit_human_input_form_preview(
app_model=app_model,
account=current_user,
node_id=node_id,
form_inputs=args.inputs,
form_inputs=args.form_inputs,
inputs=args.inputs,
action=args.action,
)
return jsonable_encoder(result)
@ -610,6 +616,7 @@ class WorkflowDraftHumanInputFormPreviewApi(Resource):
@console_ns.doc("get_workflow_draft_human_input_form")
@console_ns.doc(description="Get human input form preview for workflow")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(console_ns.models[HumanInputFormPreviewPayload.__name__])
@setup_required
@login_required
@account_initialization_required
@ -620,19 +627,15 @@ class WorkflowDraftHumanInputFormPreviewApi(Resource):
Preview human input form content and placeholders
"""
current_user, _ = current_account_with_tenant()
payload = request.get_json(silent=True) or {}
manual_inputs = payload.get("inputs") if isinstance(payload, dict) else {}
if manual_inputs is None:
manual_inputs = {}
if not isinstance(manual_inputs, dict):
raise ValueError("inputs must be an object")
args = HumanInputFormPreviewPayload.model_validate(console_ns.payload or {})
inputs = args.inputs
workflow_service = WorkflowService()
preview = workflow_service.get_human_input_form_preview(
app_model=app_model,
account=current_user,
node_id=node_id,
manual_inputs=manual_inputs,
inputs=inputs,
)
return jsonable_encoder(preview)
@ -642,15 +645,7 @@ class WorkflowDraftHumanInputFormRunApi(Resource):
@console_ns.doc("submit_workflow_draft_human_input_form")
@console_ns.doc(description="Submit human input form preview for workflow")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(
console_ns.model(
"WorkflowHumanInputFormSubmitRequest",
{
"inputs": fields.Raw(required=True, description="Form input values"),
"action": fields.String(required=True, description="Selected action ID"),
},
)
)
@console_ns.expect(console_ns.models[HumanInputFormSubmitPayload.__name__])
@setup_required
@login_required
@account_initialization_required
@ -662,12 +657,13 @@ class WorkflowDraftHumanInputFormRunApi(Resource):
"""
current_user, _ = current_account_with_tenant()
workflow_service = WorkflowService()
args = HumanInputSubmitPayload.model_validate(console_ns.payload or {})
args = HumanInputFormSubmitPayload.model_validate(console_ns.payload or {})
result = workflow_service.submit_human_input_form_preview(
app_model=app_model,
account=current_user,
node_id=node_id,
form_inputs=args.inputs,
form_inputs=args.form_inputs,
inputs=args.inputs,
action=args.action,
)
return jsonable_encoder(result)
@ -678,14 +674,7 @@ class AdvancedChatDraftHumanInputDeliveryTestApi(Resource):
@console_ns.doc("test_advanced_chat_draft_human_input_delivery")
@console_ns.doc(description="Test human input delivery for advanced chat workflow")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(
console_ns.model(
"AdvancedChatHumanInputDeliveryTestRequest",
{
"delivery_method_id": fields.String(required=True, description="Delivery method ID"),
},
)
)
@console_ns.expect(console_ns.models[HumanInputDeliveryTestPayload.__name__])
@setup_required
@login_required
@account_initialization_required
@ -715,14 +704,7 @@ class WorkflowDraftHumanInputDeliveryTestApi(Resource):
@console_ns.doc("test_workflow_draft_human_input_delivery")
@console_ns.doc(description="Test human input delivery for workflow")
@console_ns.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
@console_ns.expect(
console_ns.model(
"WorkflowHumanInputDeliveryTestRequest",
{
"delivery_method_id": fields.String(required=True, description="Delivery method ID"),
},
)
)
@console_ns.expect(console_ns.models[HumanInputDeliveryTestPayload.__name__])
@setup_required
@login_required
@account_initialization_required

View File

@ -766,8 +766,17 @@ class WorkflowService:
app_model: App,
account: Account,
node_id: str,
manual_inputs: Mapping[str, Any] | None = None,
inputs: Mapping[str, Any] | None = None,
) -> Mapping[str, Any]:
"""
Build a human input form preview for a draft workflow.
Args:
app_model: Target application model.
account: Current account.
node_id: Human input node ID.
inputs: Values used to fill missing upstream variables referenced in form_content.
"""
draft_workflow = self.get_draft_workflow(app_model=app_model)
if not draft_workflow:
raise ValueError("Workflow not initialized")
@ -777,11 +786,12 @@ class WorkflowService:
if node_type is not NodeType.HUMAN_INPUT:
raise ValueError("Node type must be human-input.")
# inputs: values used to fill missing upstream variables referenced in form_content.
variable_pool = self._build_human_input_variable_pool(
app_model=app_model,
workflow=draft_workflow,
node_config=node_config,
manual_inputs=manual_inputs or {},
manual_inputs=inputs or {},
)
node = self._build_human_input_node(
workflow=draft_workflow,
@ -812,8 +822,20 @@ class WorkflowService:
account: Account,
node_id: str,
form_inputs: Mapping[str, Any],
inputs: Mapping[str, Any] | None = None,
action: str,
) -> Mapping[str, Any]:
"""
Submit a human input form preview for a draft workflow.
Args:
app_model: Target application model.
account: Current account.
node_id: Human input node ID.
form_inputs: Values the user provides for the form's own fields.
inputs: Values used to fill missing upstream variables referenced in form_content.
action: Selected action ID.
"""
draft_workflow = self.get_draft_workflow(app_model=app_model)
if not draft_workflow:
raise ValueError("Workflow not initialized")
@ -823,11 +845,13 @@ class WorkflowService:
if node_type is not NodeType.HUMAN_INPUT:
raise ValueError("Node type must be human-input.")
# inputs: values used to fill missing upstream variables referenced in form_content.
# form_inputs: values the user provides for the form's own fields.
variable_pool = self._build_human_input_variable_pool(
app_model=app_model,
workflow=draft_workflow,
node_config=node_config,
manual_inputs={},
manual_inputs=inputs or {},
)
node = self._build_human_input_node(
workflow=draft_workflow,

View File

@ -6,6 +6,7 @@ from unittest.mock import MagicMock
import pytest
from flask import Flask
from pydantic import ValidationError
from controllers.console import wraps as console_wraps
from controllers.console.app import workflow as workflow_module
@ -58,13 +59,13 @@ class PreviewCase:
"case",
[
PreviewCase(
resource_cls=workflow_module.AdvancedChatDraftHumanInputFormApi,
path="/console/api/apps/app-123/advanced-chat/workflows/draft/human-input/nodes/node-42/form",
resource_cls=workflow_module.AdvancedChatDraftHumanInputFormPreviewApi,
path="/console/api/apps/app-123/advanced-chat/workflows/draft/human-input/nodes/node-42/form/preview",
mode=AppMode.ADVANCED_CHAT,
),
PreviewCase(
resource_cls=workflow_module.WorkflowDraftHumanInputFormApi,
path="/console/api/apps/app-123/workflows/draft/human-input/nodes/node-42/form",
resource_cls=workflow_module.WorkflowDraftHumanInputFormPreviewApi,
path="/console/api/apps/app-123/workflows/draft/human-input/nodes/node-42/form/preview",
mode=AppMode.WORKFLOW,
),
],
@ -86,15 +87,15 @@ def test_human_input_preview_delegates_to_service(
service_instance.get_human_input_form_preview.return_value = preview_payload
monkeypatch.setattr(workflow_module, "WorkflowService", MagicMock(return_value=service_instance))
with app.test_request_context(case.path, method="GET", json={"inputs": {"topic": "tech"}}):
response = case.resource_cls().get(app_id=app_model.id, node_id="node-42")
with app.test_request_context(case.path, method="POST", json={"inputs": {"topic": "tech"}}):
response = case.resource_cls().post(app_id=app_model.id, node_id="node-42")
assert response == preview_payload
service_instance.get_human_input_form_preview.assert_called_once_with(
app_model=app_model,
account=account,
node_id="node-42",
manual_inputs={"topic": "tech"},
inputs={"topic": "tech"},
)
@ -109,13 +110,13 @@ class SubmitCase:
"case",
[
SubmitCase(
resource_cls=workflow_module.AdvancedChatDraftHumanInputFormApi,
path="/console/api/apps/app-123/advanced-chat/workflows/draft/human-input/nodes/node-99/form",
resource_cls=workflow_module.AdvancedChatDraftHumanInputFormRunApi,
path="/console/api/apps/app-123/advanced-chat/workflows/draft/human-input/nodes/node-99/form/run",
mode=AppMode.ADVANCED_CHAT,
),
SubmitCase(
resource_cls=workflow_module.WorkflowDraftHumanInputFormApi,
path="/console/api/apps/app-123/workflows/draft/human-input/nodes/node-99/form",
resource_cls=workflow_module.WorkflowDraftHumanInputFormRunApi,
path="/console/api/apps/app-123/workflows/draft/human-input/nodes/node-99/form/run",
mode=AppMode.WORKFLOW,
),
],
@ -133,7 +134,7 @@ def test_human_input_submit_forwards_payload(app: Flask, monkeypatch: pytest.Mon
with app.test_request_context(
case.path,
method="POST",
json={"inputs": {"answer": "42"}, "action": "approve"},
json={"form_inputs": {"answer": "42"}, "inputs": {"#node-1.result#": "LLM output"}, "action": "approve"},
):
response = case.resource_cls().post(app_id=app_model.id, node_id="node-99")
@ -143,6 +144,7 @@ def test_human_input_submit_forwards_payload(app: Flask, monkeypatch: pytest.Mon
account=account,
node_id="node-99",
form_inputs={"answer": "42"},
inputs={"#node-1.result#": "LLM output"},
action="approve",
)
@ -219,9 +221,9 @@ def test_human_input_preview_rejects_non_mapping(app: Flask, monkeypatch: pytest
_patch_console_guards(monkeypatch, account, app_model)
with app.test_request_context(
"/console/api/apps/app-123/advanced-chat/workflows/draft/human-input/nodes/node-1/form",
method="GET",
"/console/api/apps/app-123/advanced-chat/workflows/draft/human-input/nodes/node-1/form/preview",
method="POST",
json={"inputs": ["not-a-dict"]},
):
with pytest.raises(ValueError):
workflow_module.AdvancedChatDraftHumanInputFormApi().get(app_id=app_model.id, node_id="node-1")
with pytest.raises(ValidationError):
workflow_module.AdvancedChatDraftHumanInputFormPreviewApi().post(app_id=app_model.id, node_id="node-1")

View File

@ -225,9 +225,17 @@ class TestWorkflowService:
account=account,
node_id="node-1",
form_inputs={"name": "Ada", "extra": "ignored"},
inputs={"#node-0.result#": "LLM output"},
action="approve",
)
service._build_human_input_variable_pool.assert_called_once_with(
app_model=app_model,
workflow=workflow,
node_config={"id": "node-1", "data": {"type": NodeType.HUMAN_INPUT.value}},
manual_inputs={"#node-0.result#": "LLM output"},
)
node._render_form_content_with_outputs.assert_called_once()
called_args = node._render_form_content_with_outputs.call_args.args
assert called_args[0] == "<p>preview</p>"
@ -271,6 +279,7 @@ class TestWorkflowService:
account=account,
node_id="node-1",
form_inputs={},
inputs={},
action="approve",
)