feat(api): adjust model fields and cleanup form creation logic

This commit is contained in:
QuantumGhost
2026-01-08 10:27:52 +08:00
parent de428bc9bb
commit f988619d2c
14 changed files with 60 additions and 11 deletions

View File

@ -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,
),

View File

@ -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:

View File

@ -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:

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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
),

View File

@ -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.

View File

@ -36,6 +36,7 @@ class MessageStatus(StrEnum):
"""
NORMAL = "normal"
PAUSED = "paused"
ERROR = "error"

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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)

View File

@ -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