feat(api): introduce select, file and file list form input types to Human Input node (#36322)

Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: GPT 5.4 <codex@openai.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
This commit is contained in:
QuantumGhost
2026-06-04 01:54:28 +00:00
committed by GitHub
co-authored by JzoNg GPT 5.4 autofix-ci[bot] -LAN-
parent 44725dde74
commit 3c98f96ae8
188 changed files with 11092 additions and 1156 deletions
+71 -1
View File
@@ -4,7 +4,11 @@ from collections.abc import Mapping, Sequence
from enum import StrEnum
from typing import Any
from graphon.entities.pause_reason import PauseReasonType
from graphon.entities.pause_reason import HumanInputRequired, PauseReason, PauseReasonType
from graphon.nodes.human_input.entities import FormInputConfig, SelectInputConfig
from graphon.nodes.human_input.enums import ValueSourceType
from graphon.runtime.graph_runtime_state_protocol import ReadOnlyVariablePool
from graphon.variables import ArrayStringSegment
from models.human_input import RecipientType
@@ -73,3 +77,69 @@ def enrich_human_input_pause_reasons(
updated["expiration_time"] = expiration_time
enriched.append(updated)
return enriched
def resolve_variable_select_input_options(
inputs: Sequence[FormInputConfig],
*,
variable_pool: ReadOnlyVariablePool | None,
) -> list[FormInputConfig]:
"""Resolve variable-backed select options to runtime values."""
# This function replace the SelectInputConfig.option_source.value
# field with acutial runtime values when option_source.type is VARIABLE.
#
# This is a dirty hacks. However it does reduces the logic leaked to callers of
# the api.
resolved_inputs: list[FormInputConfig] = []
if variable_pool is None:
return list(inputs)
for form_input in inputs:
if not isinstance(form_input, SelectInputConfig):
resolved_inputs.append(form_input)
continue
option_source = form_input.option_source
if option_source.type != ValueSourceType.VARIABLE:
resolved_inputs.append(form_input)
continue
option_values = variable_pool.get(option_source.selector)
if option_values is None:
resolved_inputs.append(form_input)
continue
if not isinstance(option_values, ArrayStringSegment):
raise TypeError(f"expected ArrayStringSegment, got {type(option_values)}")
updated_option_source = option_source.model_copy(update={"value": option_values.value})
# Ensure frontend receives concrete select options instead of unresolved selectors.
resolved_inputs.append(
form_input.model_copy(
update={"option_source": updated_option_source},
)
)
return resolved_inputs
def resolve_human_input_pause_reason_inputs(
reasons: Sequence[PauseReason],
*,
variable_pool: ReadOnlyVariablePool | None,
) -> list[PauseReason]:
if variable_pool is None:
return list(reasons)
resolved_reasons: list[PauseReason] = []
for reason in reasons:
if not isinstance(reason, HumanInputRequired):
resolved_reasons.append(reason)
continue
resolved_inputs = resolve_variable_select_input_options(
reason.inputs,
variable_pool=variable_pool,
)
resolved_reasons.append(reason.model_copy(update={"inputs": resolved_inputs}))
return resolved_reasons
+74 -3
View File
@@ -35,7 +35,7 @@ from core.tools.utils.message_transformer import ToolFileMessageTransformer
from core.workflow.file_reference import build_file_reference
from extensions.ext_database import db
from factories import file_factory
from graphon.file import FileTransferMethod, FileType
from graphon.file import File, FileTransferMethod, FileType
from graphon.model_runtime.entities import LLMMode
from graphon.model_runtime.entities.llm_entities import (
LLMResult,
@@ -47,7 +47,12 @@ from graphon.model_runtime.entities.llm_entities import (
from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool
from graphon.model_runtime.entities.model_entities import AIModelEntity
from graphon.model_runtime.model_providers.base.large_language_model import LargeLanguageModel
from graphon.nodes.human_input.entities import HumanInputNodeData
from graphon.nodes.human_input.entities import (
FileInputConfig,
FileListInputConfig,
FormInputConfig,
HumanInputNodeData,
)
from graphon.nodes.llm.runtime_protocols import (
LLMProtocol,
PromptMessageSerializerProtocol,
@@ -83,7 +88,6 @@ from .system_variables import SystemVariableKey, get_system_text
if TYPE_CHECKING:
from core.tools.__base.tool import Tool
from core.tools.entities.tool_entities import ToolInvokeMessage as CoreToolInvokeMessage
from graphon.file import File
from graphon.nodes.llm.file_saver import LLMFileSaver
from graphon.nodes.tool.entities import ToolNodeData
@@ -682,6 +686,7 @@ class DifyHumanInputNodeRuntime(HumanInputNodeRuntimeProtocol):
self._run_context = resolve_dify_run_context(run_context)
self._workflow_execution_id_getter = workflow_execution_id_getter
self._form_repository = form_repository
self._file_reference_factory = DifyFileReferenceFactory(self._run_context)
def _invoke_source(self) -> str:
invoke_from = self._run_context.invoke_from
@@ -735,6 +740,23 @@ class DifyHumanInputNodeRuntime(HumanInputNodeRuntimeProtocol):
repo = self.build_form_repository()
return repo.get_form(node_id)
def restore_submitted_data(
self,
*,
node_data: HumanInputNodeData,
submitted_data: Mapping[str, Any],
) -> Mapping[str, Any]:
restored_data: dict[str, Any] = dict(submitted_data)
for input_config in node_data.inputs:
output_variable_name = input_config.output_variable_name
if output_variable_name not in submitted_data:
continue
restored_data[output_variable_name] = self._restore_submitted_value(
input_config=input_config,
value=submitted_data[output_variable_name],
)
return restored_data
def create_form(
self,
*,
@@ -755,6 +777,55 @@ class DifyHumanInputNodeRuntime(HumanInputNodeRuntimeProtocol):
)
return repo.create_form(params)
def _restore_submitted_value(
self,
*,
input_config: FormInputConfig,
value: Any,
) -> Any:
if isinstance(input_config, FileInputConfig):
return self._restore_submitted_file_value(
output_variable_name=input_config.output_variable_name,
value=value,
)
if isinstance(input_config, FileListInputConfig):
return self._restore_submitted_file_list_value(
output_variable_name=input_config.output_variable_name,
value=value,
)
return value
def _restore_submitted_file_value(
self,
*,
output_variable_name: str,
value: Any,
) -> Any:
if not isinstance(value, Mapping):
msg = (
"HumanInput file submission must be persisted as a mapping, "
f"output_variable_name={output_variable_name}"
)
raise ValueError(msg)
return self._file_reference_factory.build_from_mapping(mapping=value)
def _restore_submitted_file_list_value(
self,
*,
output_variable_name: str,
value: Any,
) -> list[Any]:
if not isinstance(value, list):
msg = (
"HumanInput file-list submission must be persisted as a list, "
f"output_variable_name={output_variable_name}"
)
raise ValueError(msg)
if any(not isinstance(item, Mapping) for item in value):
msg = f"HumanInput file-list submission must contain mappings, output_variable_name={output_variable_name}"
raise ValueError(msg)
return [self._file_reference_factory.build_from_mapping(mapping=item) for item in value]
def build_dify_llm_file_saver(
*,