dify/api/models/human_input.py
QuantumGhost 4f48b8a57d WIP: P2
2025-12-26 11:36:19 +08:00

181 lines
5.6 KiB
Python

from datetime import datetime
from enum import StrEnum
from typing import Annotated, Any, ClassVar, Literal, Self, final
import sqlalchemy as sa
from pydantic import BaseModel, Field
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.workflow.nodes.human_input.entities import (
DeliveryMethodType,
EmailRecipientType,
HumanInputFormStatus,
)
from libs.helper import generate_string
from .base import Base, DefaultFieldsMixin
from .types import EnumText, StringUUID
_token_length = 22
# A 32-character string can store a base64-encoded value with 192 bits of entropy
# or a base62-encoded value with over 180 bits of entropy, providing sufficient
# uniqueness for most use cases.
_token_field_length = 32
_email_field_length = 330
def _generate_token() -> str:
return generate_string(_token_length)
class HumanInputForm(DefaultFieldsMixin, Base):
__tablename__ = "human_input_forms"
tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
workflow_run_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
# The human input node the current form corresponds to.
node_id: Mapped[str] = mapped_column(sa.String(60), nullable=False)
form_definition: Mapped[str] = mapped_column(sa.Text, nullable=False)
rendered_content: Mapped[str] = mapped_column(sa.Text, nullable=False)
status: Mapped[HumanInputFormStatus] = mapped_column(
EnumText(HumanInputFormStatus),
nullable=False,
default=HumanInputFormStatus.WAITING,
)
expiration_time: Mapped[datetime] = mapped_column(
sa.DateTime,
nullable=False,
)
# Submission-related fields (nullable until a submission happens).
selected_action_id: Mapped[str | None] = mapped_column(sa.String(200), nullable=True)
submitted_data: Mapped[str | None] = mapped_column(sa.Text, nullable=True)
submitted_at: Mapped[datetime | None] = mapped_column(sa.DateTime, nullable=True)
submission_user_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
submission_end_user_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
completed_by_recipient_id: Mapped[str | None] = mapped_column(
StringUUID,
sa.ForeignKey("human_input_recipients.id"),
nullable=True,
)
deliveries: Mapped[list["HumanInputDelivery"]] = relationship(
"HumanInputDelivery",
back_populates="form",
lazy="raise",
)
completed_by_recipient: Mapped["HumanInputRecipient | None"] = relationship(
"HumanInputRecipient",
primaryjoin="HumanInputForm.completed_by_recipient_id == HumanInputRecipient.id",
lazy="raise",
viewonly=True,
)
class HumanInputDelivery(DefaultFieldsMixin, Base):
__tablename__ = "human_input_deliveries"
form_id: Mapped[str] = mapped_column(
StringUUID,
sa.ForeignKey("human_input_forms.id"),
nullable=False,
)
delivery_method_type: Mapped[DeliveryMethodType] = mapped_column(
EnumText(DeliveryMethodType),
nullable=False,
)
delivery_config_id: Mapped[str | None] = mapped_column(StringUUID, nullable=True)
channel_payload: Mapped[None] = mapped_column(sa.Text, nullable=True)
form: Mapped[HumanInputForm] = relationship(
"HumanInputForm",
back_populates="deliveries",
lazy="raise",
)
recipients: Mapped[list["HumanInputRecipient"]] = relationship(
"HumanInputRecipient",
back_populates="delivery",
cascade="all, delete-orphan",
lazy="raise",
)
class RecipientType(StrEnum):
# EMAIL_MEMBER member means that the
EMAIL_MEMBER = "email_member"
EMAIL_EXTERNAL = "email_external"
WEBAPP = "webapp"
@final
class EmailMemberRecipientPayload(BaseModel):
TYPE: Literal[RecipientType.EMAIL_MEMBER] = RecipientType.EMAIL_MEMBER
user_id: str
# The `email` field here is only used for mail sending.
email: str
@final
class EmailExternalRecipientPayload(BaseModel):
TYPE: Literal[RecipientType.EMAIL_EXTERNAL] = RecipientType.EMAIL_EXTERNAL
email: str
@final
class WebAppRecipientPayload(BaseModel):
TYPE: Literal[RecipientType.WEBAPP] = RecipientType.WEBAPP
RecipientPayload = Annotated[
EmailMemberRecipientPayload | EmailExternalRecipientPayload | WebAppRecipientPayload,
Field(discriminator="TYPE"),
]
class HumanInputRecipient(DefaultFieldsMixin, Base):
__tablename__ = "human_input_recipients"
form_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
)
delivery_id: Mapped[str] = mapped_column(
StringUUID,
nullable=False,
)
recipient_type: Mapped["RecipientType"] = mapped_column(EnumText(RecipientType), nullable=False)
recipient_payload: Mapped[str] = mapped_column(sa.Text, nullable=False)
# Token primarily used for authenticated resume links (email, etc.).
access_token: Mapped[str | None] = mapped_column(
sa.VARCHAR(_token_field_length),
nullable=True,
default=_generate_token,
)
delivery: Mapped[HumanInputDelivery] = relationship(
"HumanInputDelivery",
back_populates="recipients",
lazy="raise",
)
@classmethod
def new(
cls,
form_id: str,
delivery_id: str,
payload: RecipientPayload,
) -> Self:
recipient_model = cls(
form_id=form_id,
delivery_id=delivery_id,
recipient_type=payload.TYPE,
recipient_payload=payload.model_dump_json(),
access_token=_generate_token(),
)
return recipient_model