mirror of
https://github.com/langgenius/dify.git
synced 2026-02-27 21:17:13 +08:00
feat(api): adjust model fields and cleanup form creation logic
This commit is contained in:
@ -299,6 +299,7 @@ class WorkflowResponseConverter:
|
||||
form_content=reason.form_content,
|
||||
inputs=reason.inputs,
|
||||
actions=reason.actions,
|
||||
display_in_ui=reason.display_in_ui,
|
||||
form_token=reason.form_token,
|
||||
resolved_placeholder_values=reason.resolved_placeholder_values,
|
||||
),
|
||||
|
||||
@ -326,6 +326,8 @@ def _topic_msg_generator(
|
||||
) -> Generator[Mapping[str, Any], None, None]:
|
||||
last_msg_time = time.time()
|
||||
with topic.subscribe() as sub:
|
||||
# on_subscribe fires only after the Redis subscription is active.
|
||||
# This is used to gate task start and reduce pub/sub race for the first event.
|
||||
if on_subscribe is not None:
|
||||
on_subscribe()
|
||||
while True:
|
||||
|
||||
@ -44,6 +44,8 @@ def _topic_msg_generator(
|
||||
) -> Generator[Mapping[str, Any], None, None]:
|
||||
last_msg_time = time.time()
|
||||
with topic.subscribe() as sub:
|
||||
# on_subscribe fires only after the Redis subscription is active.
|
||||
# This is used to gate task start and reduce pub/sub race for the first event.
|
||||
if on_subscribe is not None:
|
||||
on_subscribe()
|
||||
while True:
|
||||
|
||||
@ -277,6 +277,7 @@ class HumanInputRequiredResponse(StreamResponse):
|
||||
form_content: str
|
||||
inputs: Sequence[FormInput] = Field(default_factory=list)
|
||||
actions: Sequence[UserAction] = Field(default_factory=list)
|
||||
display_in_ui: bool = False
|
||||
form_token: str | None = None
|
||||
resolved_placeholder_values: Mapping[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
@ -342,7 +342,7 @@ class HumanInputFormRepositoryImpl:
|
||||
)
|
||||
session.add(form_model)
|
||||
recipient_models: list[HumanInputFormRecipient] = []
|
||||
for delivery in form_config.delivery_methods:
|
||||
for delivery in params.delivery_methods:
|
||||
delivery_and_recipients = self._delivery_method_to_model(
|
||||
session=session,
|
||||
form_id=form_id,
|
||||
|
||||
@ -18,6 +18,7 @@ class HumanInputRequired(BaseModel):
|
||||
form_content: str
|
||||
inputs: list[FormInput] = Field(default_factory=list)
|
||||
actions: list[UserAction] = Field(default_factory=list)
|
||||
display_in_ui: bool = False
|
||||
node_id: str
|
||||
node_title: str
|
||||
|
||||
@ -32,10 +33,11 @@ class HumanInputRequired(BaseModel):
|
||||
# Only form inputs with placeholder type `VARIABLE` will be resolved and stored in `resolved_placeholder_values`.
|
||||
resolved_placeholder_values: Mapping[str, Any] = Field(default_factory=dict)
|
||||
|
||||
# The `form_token` is the token used to submit the form via webapp. It corresponds to
|
||||
# The `form_token` is the token used to submit the form via UI surfaces. It corresponds to
|
||||
# `HumanInputFormRecipient.access_token`.
|
||||
#
|
||||
# This field is `None` if webapp delivery is not set.
|
||||
# This field is `None` if webapp delivery is not set and not
|
||||
# in orchestrating mode.
|
||||
form_token: str | None = None
|
||||
|
||||
|
||||
|
||||
@ -253,3 +253,9 @@ class FormDefinition(BaseModel):
|
||||
|
||||
# this is used to store the values of the placeholders
|
||||
placeholder_values: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
# node_title records the title of the HumanInput node.
|
||||
node_title: str | None = None
|
||||
|
||||
# display_in_ui controls whether the form should be displayed in UI surfaces.
|
||||
display_in_ui: bool | None = None
|
||||
|
||||
@ -20,8 +20,8 @@ from core.workflow.workflow_type_encoder import WorkflowRuntimeTypeConverter
|
||||
from extensions.ext_database import db
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
|
||||
from .entities import HumanInputNodeData
|
||||
from .enums import HumanInputFormStatus, PlaceholderType
|
||||
from .entities import DeliveryChannelConfig, HumanInputNodeData
|
||||
from .enums import DeliveryMethodType, HumanInputFormStatus, PlaceholderType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.workflow.entities.graph_init_params import GraphInitParams
|
||||
@ -154,24 +154,37 @@ class HumanInputNode(Node[HumanInputNodeData]):
|
||||
|
||||
return resolved_inputs
|
||||
|
||||
def _should_require_form_token(self) -> bool:
|
||||
def _should_require_console_recipient(self) -> bool:
|
||||
if self.invoke_from == InvokeFrom.DEBUGGER:
|
||||
return True
|
||||
if self.invoke_from == InvokeFrom.EXPLORE:
|
||||
return self._node_data.is_webapp_enabled()
|
||||
return False
|
||||
|
||||
def _display_in_ui(self) -> bool:
|
||||
if self.invoke_from == InvokeFrom.DEBUGGER:
|
||||
return True
|
||||
return self._node_data.is_webapp_enabled()
|
||||
|
||||
def _effective_delivery_methods(self) -> Sequence[DeliveryChannelConfig]:
|
||||
enabled_methods = [method for method in self._node_data.delivery_methods if method.enabled]
|
||||
if self.invoke_from in {InvokeFrom.DEBUGGER, InvokeFrom.EXPLORE}:
|
||||
return [method for method in enabled_methods if method.type != DeliveryMethodType.WEBAPP]
|
||||
return enabled_methods
|
||||
|
||||
def _human_input_required_event(self, form_entity: HumanInputFormEntity) -> HumanInputRequired:
|
||||
node_data = self._node_data
|
||||
resolved_placeholder_values = self._resolve_inputs()
|
||||
display_in_ui = self._display_in_ui()
|
||||
form_token = form_entity.web_app_token
|
||||
if self._should_require_form_token() and form_token is None:
|
||||
raise AssertionError("Form token should be available for console execution.")
|
||||
if display_in_ui and form_token is None:
|
||||
raise AssertionError("Form token should be available for UI execution.")
|
||||
return HumanInputRequired(
|
||||
form_id=form_entity.id,
|
||||
form_content=form_entity.rendered_content,
|
||||
inputs=node_data.inputs,
|
||||
actions=node_data.user_actions,
|
||||
display_in_ui=display_in_ui,
|
||||
node_id=self.id,
|
||||
node_title=node_data.title,
|
||||
form_token=form_token,
|
||||
@ -193,13 +206,16 @@ class HumanInputNode(Node[HumanInputNodeData]):
|
||||
repo = self._form_repository
|
||||
form = repo.get_form(self._workflow_execution_id, self.id)
|
||||
if form is None:
|
||||
display_in_ui = self._display_in_ui()
|
||||
params = FormCreateParams(
|
||||
workflow_execution_id=self._workflow_execution_id,
|
||||
node_id=self.id,
|
||||
form_config=self._node_data,
|
||||
rendered_content=self._render_form_content_before_submission(),
|
||||
delivery_methods=self._effective_delivery_methods(),
|
||||
display_in_ui=display_in_ui,
|
||||
resolved_placeholder_values=self._resolve_inputs(),
|
||||
console_recipient_required=self._should_require_form_token(),
|
||||
console_recipient_required=self._should_require_console_recipient(),
|
||||
console_creator_account_id=(
|
||||
self.user_id if self.invoke_from in {InvokeFrom.DEBUGGER, InvokeFrom.EXPLORE} else None
|
||||
),
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import abc
|
||||
import dataclasses
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Mapping, Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any, Protocol
|
||||
|
||||
from core.workflow.nodes.human_input.entities import HumanInputNodeData
|
||||
from core.workflow.nodes.human_input.entities import DeliveryChannelConfig, HumanInputNodeData
|
||||
from core.workflow.nodes.human_input.enums import HumanInputFormStatus
|
||||
|
||||
|
||||
@ -29,6 +29,10 @@ class FormCreateParams:
|
||||
|
||||
form_config: HumanInputNodeData
|
||||
rendered_content: str
|
||||
# Delivery methods already filtered by runtime context (invoke_from).
|
||||
delivery_methods: Sequence[DeliveryChannelConfig]
|
||||
# UI display flag computed by runtime context.
|
||||
display_in_ui: bool
|
||||
|
||||
# resolved_placeholder_values saves the values for placeholders with
|
||||
# type = VARIABLE.
|
||||
|
||||
@ -36,6 +36,7 @@ class MessageStatus(StrEnum):
|
||||
"""
|
||||
|
||||
NORMAL = "normal"
|
||||
PAUSED = "paused"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
|
||||
@ -70,6 +70,8 @@ def _build_form_params(delivery_methods: list[EmailDeliveryMethod]) -> FormCreat
|
||||
node_id="human-input-node",
|
||||
form_config=form_config,
|
||||
rendered_content="<p>Approve?</p>",
|
||||
delivery_methods=delivery_methods,
|
||||
display_in_ui=False,
|
||||
resolved_placeholder_values={},
|
||||
)
|
||||
|
||||
@ -180,6 +182,8 @@ class TestHumanInputFormRepositoryImplWithContainers:
|
||||
user_actions=[UserAction(id="approve", title="Approve")],
|
||||
),
|
||||
rendered_content="<p>Approve?</p>",
|
||||
delivery_methods=[],
|
||||
display_in_ui=False,
|
||||
resolved_placeholder_values=resolved_values,
|
||||
)
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ class HumanInputMessageFixture:
|
||||
form: HumanInputForm
|
||||
action_id: str
|
||||
action_text: str
|
||||
node_title: str
|
||||
|
||||
|
||||
def create_human_input_message_fixture(db_session) -> HumanInputMessageFixture:
|
||||
@ -109,6 +110,7 @@ def create_human_input_message_fixture(db_session) -> HumanInputMessageFixture:
|
||||
|
||||
action_id = "approve"
|
||||
action_text = "Approve request"
|
||||
node_title = "Approval"
|
||||
form_definition = FormDefinition(
|
||||
form_content="content",
|
||||
inputs=[],
|
||||
@ -116,6 +118,8 @@ def create_human_input_message_fixture(db_session) -> HumanInputMessageFixture:
|
||||
rendered_content="Rendered block",
|
||||
timeout=1,
|
||||
timeout_unit=TimeoutUnit.HOUR,
|
||||
node_title=node_title,
|
||||
display_in_ui=True,
|
||||
)
|
||||
form = HumanInputForm(
|
||||
tenant_id=tenant.id,
|
||||
@ -146,4 +150,5 @@ def create_human_input_message_fixture(db_session) -> HumanInputMessageFixture:
|
||||
form=form,
|
||||
action_id=action_id,
|
||||
action_text=action_text,
|
||||
node_title=node_title,
|
||||
)
|
||||
|
||||
@ -92,6 +92,8 @@ def _build_form(db_session_with_containers, tenant, account):
|
||||
node_id="node-1",
|
||||
form_config=node_data,
|
||||
rendered_content="Rendered",
|
||||
delivery_methods=node_data.delivery_methods,
|
||||
display_in_ui=False,
|
||||
resolved_placeholder_values={},
|
||||
)
|
||||
return repo.create_form(params)
|
||||
|
||||
@ -126,6 +126,7 @@ def test_queue_workflow_paused_event_to_stream_responses():
|
||||
FormInput(type=FormInputType.TEXT_INPUT, output_variable_name="field", placeholder=None),
|
||||
],
|
||||
actions=[UserAction(id="approve", title="Approve")],
|
||||
display_in_ui=True,
|
||||
node_id="node-id",
|
||||
node_title="Human Step",
|
||||
form_token="token",
|
||||
@ -144,6 +145,7 @@ def test_queue_workflow_paused_event_to_stream_responses():
|
||||
assert pause_resp.data.paused_nodes == ["node-id"]
|
||||
assert pause_resp.data.outputs == {"answer": "value"}
|
||||
assert pause_resp.data.reasons[0]["form_id"] == "form-1"
|
||||
assert pause_resp.data.reasons[0]["display_in_ui"] is True
|
||||
|
||||
assert isinstance(responses[0], HumanInputRequiredResponse)
|
||||
hi_resp = responses[0]
|
||||
@ -152,3 +154,4 @@ def test_queue_workflow_paused_event_to_stream_responses():
|
||||
assert hi_resp.data.node_title == "Human Step"
|
||||
assert hi_resp.data.inputs[0].output_variable_name == "field"
|
||||
assert hi_resp.data.actions[0].id == "approve"
|
||||
assert hi_resp.data.display_in_ui is True
|
||||
|
||||
Reference in New Issue
Block a user