Files
dify/api/controllers/console/human_input_v2/channel.py
T
8cbb6c0130 feat: HITL IM integration (#44864)
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-10-30 02:43:31 +00:00

697 lines
29 KiB
Python

"""Canonical Console transport for Human Input Email and IM Channels."""
from __future__ import annotations
from collections.abc import Sequence
from enum import StrEnum
from http import HTTPStatus
from typing import Annotated, Literal
from uuid import UUID
from flask import request
from flask.typing import ResponseReturnValue
from flask_restx import Resource
from pydantic import Field, StringConstraints
from controllers.common.schema import (
query_params_from_model,
register_response_schema_models,
register_schema_models,
)
from controllers.console import console_ns
from controllers.console.wraps import with_current_tenant_id, with_current_user
from core.human_input_v2.entities import (
ChannelKind,
EmailProviderType,
IMProvider,
)
from core.human_input_v2.shared import (
AccountId,
EmailProviderId,
NormalizedEmail,
TenantId,
WorkspaceScope,
)
from fields.base import ResponseModel
from fields.timestamp import Timestamp
from libs.helper import EmailStr
from models.account import Account
from repositories.human_input_v2.email_channel import EmailChannelView, EmailConfigurationSnapshot
from repositories.human_input_v2.email_channel.entities import ResendCandidate
from repositories.human_input_v2.im_channel_repository import IMChannelId, IMChannelStatus
from services.human_input_v2.credential_update import ResendChannelUpdate, Unchanged
from services.human_input_v2.email_channel_management_composition import (
build_human_input_email_channel_management_service,
)
from services.human_input_v2.errors import (
ChannelAlreadyConfiguredError,
ChannelNotFoundError,
ChannelProviderError,
ProviderConfigurationUpdatedError,
ReplacementRequiredError,
)
from services.human_input_v2.im_channel_service import IMChannelView, WorkspaceIMChannelService
from services.human_input_v2.im_channel_service_composition import (
build_workspace_im_channel_service,
)
from ._common import StrictModel
from ._decorators import require_admin_or_owner, workspace_channel_api_admission
from .config_version import (
InvalidConfigVersionError,
decode_email_config_version,
decode_im_config_version,
encode_email_config_version,
encode_im_config_version,
)
from .errors import (
ChannelAlreadyConfiguredHttpError,
ChannelNotFoundHttpError,
ChannelProviderBadRequestHttpError,
ChannelProviderConfigurationUpdatedHttpError,
ChannelReplacementRequiredHttpError,
)
from .provider_updates import EmailProviderCredentialsUpdateInput, IMProviderCredentialsUpdateInput, UnchangedInput
from .provider_views import (
IMProviderCredentialsResponse,
Redacted,
ResendCredentialsResponse,
im_credentials_response,
)
from .providers import EmailProviderCredentials, IMProviderCredentialsInput
type ChannelId = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
type ConfigVersion = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
class WorkspaceChannelResource(Resource):
"""Apply the edition gate outside every canonical handler decorator stack."""
method_decorators = [workspace_channel_api_admission]
class ChannelStatus(StrEnum):
CONFIGURED = "configured"
CONNECTED = "connected"
INVALID_CREDENTIALS = "invalid_credentials"
CONNECTION_FAILURE = "connection_failure"
class ConnectionMode(StrEnum):
CUSTOM_APP = "custom_app"
class ChannelTestResponse(ResponseModel):
status: ChannelStatus
status_description: str
class ChannelDeleteResponse(ResponseModel):
channel_id: ChannelId
class ChannelConflictResponse(ResponseModel):
code: Literal["replacement_required", "provider_configuration_updated"]
message: str
status: Literal[HTTPStatus.CONFLICT] = HTTPStatus.CONFLICT
class ChannelProvider(ResponseModel):
provider: EmailProviderType | IMProvider
connection_mode: Literal[ConnectionMode.CUSTOM_APP] = ConnectionMode.CUSTOM_APP
class ListChannelProvidersResponse(ResponseModel):
email_providers: Sequence[ChannelProvider]
im_providers: Sequence[ChannelProvider]
class ChannelSummary(ResponseModel):
id: ChannelId
created_at: Timestamp
updated_at: Timestamp
kind: Literal[ChannelKind.EMAIL, ChannelKind.IM]
provider: EmailProviderType | IMProvider = Field(
description="The provider of the Channel. The concrete provider type depends on the `kind` field."
)
status: ChannelStatus
status_description: str = Field(
description="Human-readable status description. Empty when the status is `configured` or `connected`."
)
display_identifier: str = Field(description="The display identifier of the Channel.")
webhook_url: str | None = Field(
description=(
"Webhook URL to configure on the provider side. None when webhook configuration is not required "
"or the provider does not support webhooks."
)
)
config_version: ConfigVersion = Field(
description="The current opaque configuration version used for optimistic concurrency control."
)
class ListChannelsResponse(ResponseModel):
channels: Sequence[ChannelSummary]
type _SenderName = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=255)]
class _EmailChannelCandidatePayload(StrictModel):
sender_email: EmailStr
sender_name: _SenderName
credentials: EmailProviderCredentials
def to_owner_candidate(self) -> ResendCandidate:
return ResendCandidate(
sender_email=NormalizedEmail(self.sender_email),
sender_name=self.sender_name,
credentials=self.credentials.to_owner_credentials(),
)
class EmailChannelTestPayload(_EmailChannelCandidatePayload):
pass
class EmailChannelCreatePayload(_EmailChannelCandidatePayload):
pass
class EmailChannelMutationResponse(ResponseModel):
summary: ChannelSummary
class EmailChannelDetailResponse(ResponseModel):
summary: ChannelSummary
sender_name: str
sender_email: str
credentials: ResendCredentialsResponse
class EmailChannelUpdatePayload(StrictModel):
sender_email: EmailStr | UnchangedInput
sender_name: _SenderName | UnchangedInput
credentials: EmailProviderCredentialsUpdateInput
expected_config_version: ConfigVersion
def to_owner_update(self) -> ResendChannelUpdate:
return ResendChannelUpdate(
sender_email=(
Unchanged.VALUE if isinstance(self.sender_email, UnchangedInput) else NormalizedEmail(self.sender_email)
),
sender_name=Unchanged.VALUE if isinstance(self.sender_name, UnchangedInput) else self.sender_name,
credentials=self.credentials.to_owner_update(),
)
class ChannelDeleteQuery(StrictModel):
expected_config_version: ConfigVersion
class IMChannelTestPayload(StrictModel):
credentials: IMProviderCredentialsInput
class IMChannelCreatePayload(StrictModel):
credentials: IMProviderCredentialsInput
class IMChannelMutationResponse(ResponseModel):
summary: ChannelSummary
class IMChannelDetailResponse(ResponseModel):
summary: ChannelSummary
credentials: IMProviderCredentialsResponse
class IMChannelUpdatePayload(StrictModel):
credentials: IMProviderCredentialsUpdateInput
expected_config_version: ConfigVersion
class IMChannelReplacementPayload(StrictModel):
credentials: IMProviderCredentialsInput
expected_config_version: ConfigVersion
register_schema_models(
console_ns,
ChannelDeleteQuery,
EmailChannelTestPayload,
EmailChannelCreatePayload,
EmailChannelUpdatePayload,
IMChannelTestPayload,
IMChannelCreatePayload,
IMChannelUpdatePayload,
IMChannelReplacementPayload,
)
register_response_schema_models(
console_ns,
ChannelTestResponse,
ChannelDeleteResponse,
ChannelConflictResponse,
ChannelProvider,
ListChannelProvidersResponse,
ChannelSummary,
ListChannelsResponse,
EmailChannelMutationResponse,
EmailChannelDetailResponse,
IMChannelMutationResponse,
IMChannelDetailResponse,
)
def _workspace_scope(tenant_id: str) -> WorkspaceScope:
return WorkspaceScope(id=TenantId(tenant_id))
def _actor_id(account: Account) -> AccountId:
return AccountId(account.id)
def _channel_provider_response(provider: EmailProviderType | IMProvider) -> ChannelProvider:
return ChannelProvider(provider=provider)
def _email_channel_summary_response(view: EmailChannelView) -> ChannelSummary:
return ChannelSummary(
id=view.id,
created_at=view.created_at,
updated_at=view.updated_at,
kind=ChannelKind.EMAIL,
provider=view.provider,
status=ChannelStatus.CONFIGURED,
status_description="",
display_identifier=" ".join(part for part in (view.sender_name, view.sender_email) if part),
webhook_url=None,
config_version=encode_email_config_version(view.revision),
)
def _im_channel_summary_response(view: IMChannelView) -> ChannelSummary:
status, status_description = _im_channel_status(view)
return ChannelSummary(
id=view.id,
created_at=view.created_at,
updated_at=view.updated_at,
kind=ChannelKind.IM,
provider=view.provider,
status=status,
status_description=status_description,
display_identifier=view.app_identifier,
webhook_url=view.webhook_url,
config_version=encode_im_config_version(view.id, view.config_version),
)
def _im_channel_status(view: IMChannelView) -> tuple[ChannelStatus, str]:
if view.status is IMChannelStatus.CONNECTED:
return ChannelStatus.CONNECTED, ""
if view.status is IMChannelStatus.INVALID_CREDENTIALS:
return (
ChannelStatus.INVALID_CREDENTIALS,
view.status_reason or "The configured credentials are no longer accepted.",
)
return (
ChannelStatus.CONNECTION_FAILURE,
view.status_reason or "The configured provider connection is unavailable.",
)
def _email_revision(value: str, channel_id: EmailProviderId) -> EmailConfigurationSnapshot:
try:
return decode_email_config_version(value, channel_id)
except InvalidConfigVersionError as error:
raise ChannelProviderConfigurationUpdatedHttpError() from error
def _im_config_version(value: str, channel_id: IMChannelId) -> int:
try:
return decode_im_config_version(value, channel_id)
except InvalidConfigVersionError as error:
raise ChannelProviderConfigurationUpdatedHttpError() from error
def _workspace_im_channel_service(tenant_id: str, account: Account) -> WorkspaceIMChannelService:
return build_workspace_im_channel_service(TenantId(tenant_id), _actor_id(account))
@console_ns.route("/workspaces/current/human-input/v2/channel-providers")
class ListChannelProvidersApi(WorkspaceChannelResource):
@console_ns.response(200, "Success", console_ns.models[ListChannelProvidersResponse.__name__])
@require_admin_or_owner
@with_current_user
@with_current_tenant_id
def get(self, tenant_id: str, current_user: Account) -> ResponseReturnValue:
email_providers = build_human_input_email_channel_management_service().available_providers()
im_providers = _workspace_im_channel_service(tenant_id, current_user).available_providers()
return ListChannelProvidersResponse(
email_providers=[_channel_provider_response(provider) for provider in email_providers],
im_providers=[_channel_provider_response(provider) for provider in im_providers],
).model_dump(mode="json")
@console_ns.route("/workspaces/current/human-input/v2/channels")
class ListChannelsApi(WorkspaceChannelResource):
@console_ns.response(200, "Success", console_ns.models[ListChannelsResponse.__name__])
@require_admin_or_owner
@with_current_user
@with_current_tenant_id
def get(self, tenant_id: str, current_user: Account) -> ResponseReturnValue:
workspace_scope = _workspace_scope(tenant_id)
email = build_human_input_email_channel_management_service().get_current(workspace_scope)
im = _workspace_im_channel_service(tenant_id, current_user).get_current()
channels: list[ChannelSummary] = []
if email is not None:
channels.append(_email_channel_summary_response(email))
if im is not None:
channels.append(_im_channel_summary_response(im))
return ListChannelsResponse(channels=channels).model_dump(mode="json")
@console_ns.route("/workspaces/current/human-input/v2/channels/email/test")
class EmailChannelTestApi(WorkspaceChannelResource):
@console_ns.expect(console_ns.models[EmailChannelTestPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ChannelTestResponse.__name__])
@require_admin_or_owner
@with_current_tenant_id
def post(self, tenant_id: str) -> ResponseReturnValue:
request_body = EmailChannelTestPayload.model_validate(console_ns.payload or {})
candidate = request_body.to_owner_candidate()
try:
build_human_input_email_channel_management_service().test(
_workspace_scope(tenant_id),
candidate,
)
except ChannelProviderError as error:
return ChannelTestResponse(
status=ChannelStatus(error.kind.value),
status_description=error.status_description,
).model_dump(mode="json")
return ChannelTestResponse(
status=ChannelStatus.CONNECTED,
status_description="",
).model_dump(mode="json")
@console_ns.route("/workspaces/current/human-input/v2/channels/email/<uuid:channel_id>/test")
class EmailChannelSavedTestApi(WorkspaceChannelResource):
@console_ns.expect(console_ns.models[EmailChannelUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ChannelTestResponse.__name__])
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
@require_admin_or_owner
@with_current_tenant_id
def post(self, tenant_id: str, channel_id: UUID) -> ResponseReturnValue:
request_body = EmailChannelUpdatePayload.model_validate(console_ns.payload or {})
addressed_id = EmailProviderId(str(channel_id))
try:
build_human_input_email_channel_management_service().test_saved(
_workspace_scope(tenant_id),
addressed_id,
_email_revision(request_body.expected_config_version, addressed_id),
request_body.to_owner_update(),
)
except ChannelNotFoundError as error:
raise ChannelNotFoundHttpError() from error
except ProviderConfigurationUpdatedError as error:
raise ChannelProviderConfigurationUpdatedHttpError() from error
except ChannelProviderError as error:
return ChannelTestResponse(
status=ChannelStatus(error.kind.value),
status_description=error.status_description,
).model_dump(mode="json")
return ChannelTestResponse(status=ChannelStatus.CONNECTED, status_description="").model_dump(mode="json")
@console_ns.route("/workspaces/current/human-input/v2/channels/email")
class EmailChannelCreateApi(WorkspaceChannelResource):
@console_ns.expect(console_ns.models[EmailChannelCreatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[EmailChannelMutationResponse.__name__])
@require_admin_or_owner
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, current_user: Account) -> ResponseReturnValue:
request_body = EmailChannelCreatePayload.model_validate(console_ns.payload or {})
try:
snapshot = build_human_input_email_channel_management_service().create(
_workspace_scope(tenant_id),
_actor_id(current_user),
request_body.to_owner_candidate(),
)
except ChannelAlreadyConfiguredError as error:
raise ChannelAlreadyConfiguredHttpError() from error
except ChannelProviderError as error:
raise ChannelProviderBadRequestHttpError(error.status_description) from error
return EmailChannelMutationResponse(summary=_email_channel_summary_response(snapshot)).model_dump(mode="json")
@console_ns.route("/workspaces/current/human-input/v2/channels/email/<uuid:channel_id>")
class EmailChannelApi(WorkspaceChannelResource):
@console_ns.response(200, "Success", console_ns.models[EmailChannelDetailResponse.__name__])
@require_admin_or_owner
@with_current_tenant_id
def get(self, tenant_id: str, channel_id: UUID) -> ResponseReturnValue:
try:
snapshot = build_human_input_email_channel_management_service().get(
_workspace_scope(tenant_id),
EmailProviderId(str(channel_id)),
)
except ChannelNotFoundError as error:
raise ChannelNotFoundHttpError() from error
return EmailChannelDetailResponse(
summary=_email_channel_summary_response(snapshot),
sender_name=snapshot.sender_name,
sender_email=snapshot.sender_email,
credentials=ResendCredentialsResponse(provider=EmailProviderType.RESEND, api_key=Redacted()),
).model_dump(mode="json")
@console_ns.expect(console_ns.models[EmailChannelUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[EmailChannelMutationResponse.__name__])
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
@require_admin_or_owner
@with_current_user
@with_current_tenant_id
def put(self, tenant_id: str, current_user: Account, channel_id: UUID) -> ResponseReturnValue:
request_body = EmailChannelUpdatePayload.model_validate(console_ns.payload or {})
addressed_id = EmailProviderId(str(channel_id))
try:
snapshot = build_human_input_email_channel_management_service().update(
_workspace_scope(tenant_id),
addressed_id,
_email_revision(request_body.expected_config_version, addressed_id),
_actor_id(current_user),
request_body.to_owner_update(),
)
except ChannelNotFoundError as error:
raise ChannelNotFoundHttpError() from error
except ProviderConfigurationUpdatedError as error:
raise ChannelProviderConfigurationUpdatedHttpError() from error
except ChannelProviderError as error:
raise ChannelProviderBadRequestHttpError(error.status_description) from error
return EmailChannelMutationResponse(summary=_email_channel_summary_response(snapshot)).model_dump(mode="json")
@console_ns.doc(params=query_params_from_model(ChannelDeleteQuery))
@console_ns.response(200, "Success", console_ns.models[ChannelDeleteResponse.__name__])
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
@require_admin_or_owner
@with_current_tenant_id
def delete(self, tenant_id: str, channel_id: UUID) -> ResponseReturnValue:
query = ChannelDeleteQuery.model_validate(request.args.to_dict(flat=True))
addressed_id = EmailProviderId(str(channel_id))
try:
deleted_id = build_human_input_email_channel_management_service().delete(
_workspace_scope(tenant_id),
addressed_id,
_email_revision(query.expected_config_version, addressed_id),
)
except ChannelNotFoundError as error:
raise ChannelNotFoundHttpError() from error
except ProviderConfigurationUpdatedError as error:
raise ChannelProviderConfigurationUpdatedHttpError() from error
return ChannelDeleteResponse(channel_id=deleted_id).model_dump(mode="json")
@console_ns.route("/workspaces/current/human-input/v2/channels/im/test")
class IMChannelTestApi(WorkspaceChannelResource):
@console_ns.expect(console_ns.models[IMChannelTestPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ChannelTestResponse.__name__])
@require_admin_or_owner
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, current_user: Account) -> ResponseReturnValue:
request_body = IMChannelTestPayload.model_validate(console_ns.payload or {})
try:
_workspace_im_channel_service(tenant_id, current_user).test(request_body.credentials.to_owner_credentials())
except ChannelProviderError as error:
return ChannelTestResponse(
status=ChannelStatus(error.kind.value),
status_description=error.status_description,
).model_dump(mode="json")
return ChannelTestResponse(
status=ChannelStatus.CONNECTED,
status_description="",
).model_dump(mode="json")
@console_ns.route("/workspaces/current/human-input/v2/channels/im/<uuid:channel_id>/test")
class IMChannelSavedTestApi(WorkspaceChannelResource):
@console_ns.expect(console_ns.models[IMChannelUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ChannelTestResponse.__name__])
@console_ns.response(400, "The test provider must match the saved IM Channel.")
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
@require_admin_or_owner
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, current_user: Account, channel_id: UUID) -> ResponseReturnValue:
request_body = IMChannelUpdatePayload.model_validate(console_ns.payload or {})
addressed_id = IMChannelId(str(channel_id))
try:
_workspace_im_channel_service(tenant_id, current_user).test_saved(
addressed_id,
_im_config_version(request_body.expected_config_version, addressed_id),
request_body.credentials.to_owner_update(),
)
except ChannelNotFoundError as error:
raise ChannelNotFoundHttpError() from error
except ProviderConfigurationUpdatedError as error:
raise ChannelProviderConfigurationUpdatedHttpError() from error
except ReplacementRequiredError as error:
raise ChannelProviderBadRequestHttpError("The test provider must match the saved IM Channel.") from error
except ChannelProviderError as error:
return ChannelTestResponse(
status=ChannelStatus(error.kind.value),
status_description=error.status_description,
).model_dump(mode="json")
return ChannelTestResponse(status=ChannelStatus.CONNECTED, status_description="").model_dump(mode="json")
@console_ns.route("/workspaces/current/human-input/v2/channels/im")
class IMChannelCreateApi(WorkspaceChannelResource):
@console_ns.expect(console_ns.models[IMChannelCreatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[IMChannelMutationResponse.__name__])
@require_admin_or_owner
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, current_user: Account) -> ResponseReturnValue:
request_body = IMChannelCreatePayload.model_validate(console_ns.payload or {})
try:
snapshot = _workspace_im_channel_service(tenant_id, current_user).create(
request_body.credentials.to_owner_credentials()
)
except ChannelAlreadyConfiguredError as error:
raise ChannelAlreadyConfiguredHttpError() from error
except ChannelProviderError as error:
raise ChannelProviderBadRequestHttpError(error.status_description) from error
return IMChannelMutationResponse(summary=_im_channel_summary_response(snapshot)).model_dump(mode="json")
@console_ns.route("/workspaces/current/human-input/v2/channels/im/<uuid:channel_id>")
class IMChannelApi(WorkspaceChannelResource):
@console_ns.response(200, "Success", console_ns.models[IMChannelDetailResponse.__name__])
@require_admin_or_owner
@with_current_user
@with_current_tenant_id
def get(self, tenant_id: str, current_user: Account, channel_id: UUID) -> ResponseReturnValue:
try:
detail = _workspace_im_channel_service(tenant_id, current_user).get_detail(IMChannelId(str(channel_id)))
except ChannelNotFoundError as error:
raise ChannelNotFoundHttpError() from error
return IMChannelDetailResponse(
summary=_im_channel_summary_response(detail.summary),
credentials=im_credentials_response(detail.credentials),
).model_dump(mode="json")
@console_ns.expect(console_ns.models[IMChannelUpdatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[IMChannelMutationResponse.__name__])
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
@require_admin_or_owner
@with_current_user
@with_current_tenant_id
def put(self, tenant_id: str, current_user: Account, channel_id: UUID) -> ResponseReturnValue:
request_body = IMChannelUpdatePayload.model_validate(console_ns.payload or {})
addressed_id = IMChannelId(str(channel_id))
try:
snapshot = _workspace_im_channel_service(tenant_id, current_user).update(
addressed_id,
_im_config_version(request_body.expected_config_version, addressed_id),
request_body.credentials.to_owner_update(),
)
except ChannelNotFoundError as error:
raise ChannelNotFoundHttpError() from error
except ReplacementRequiredError as error:
raise ChannelReplacementRequiredHttpError() from error
except ProviderConfigurationUpdatedError as error:
raise ChannelProviderConfigurationUpdatedHttpError() from error
except ChannelProviderError as error:
raise ChannelProviderBadRequestHttpError(error.status_description) from error
return IMChannelMutationResponse(summary=_im_channel_summary_response(snapshot)).model_dump(mode="json")
@console_ns.doc(params=query_params_from_model(ChannelDeleteQuery))
@console_ns.response(200, "Success", console_ns.models[ChannelDeleteResponse.__name__])
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
@require_admin_or_owner
@with_current_user
@with_current_tenant_id
def delete(self, tenant_id: str, current_user: Account, channel_id: UUID) -> ResponseReturnValue:
query = ChannelDeleteQuery.model_validate(request.args.to_dict(flat=True))
addressed_id = IMChannelId(str(channel_id))
try:
deleted_id = _workspace_im_channel_service(tenant_id, current_user).delete(
addressed_id,
_im_config_version(query.expected_config_version, addressed_id),
)
except ChannelNotFoundError as error:
raise ChannelNotFoundHttpError() from error
except ProviderConfigurationUpdatedError as error:
raise ChannelProviderConfigurationUpdatedHttpError() from error
return ChannelDeleteResponse(channel_id=deleted_id).model_dump(mode="json")
@console_ns.route("/workspaces/current/human-input/v2/channels/im/<uuid:channel_id>/replacement")
class IMChannelReplacementApi(WorkspaceChannelResource):
@console_ns.expect(console_ns.models[IMChannelReplacementPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[IMChannelMutationResponse.__name__])
@console_ns.response(409, "Configuration conflict", console_ns.models[ChannelConflictResponse.__name__])
@require_admin_or_owner
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, current_user: Account, channel_id: UUID) -> ResponseReturnValue:
request_body = IMChannelReplacementPayload.model_validate(console_ns.payload or {})
addressed_id = IMChannelId(str(channel_id))
try:
snapshot = _workspace_im_channel_service(tenant_id, current_user).replace(
addressed_id,
_im_config_version(request_body.expected_config_version, addressed_id),
request_body.credentials.to_owner_credentials(),
)
except ChannelNotFoundError as error:
raise ChannelNotFoundHttpError() from error
except ProviderConfigurationUpdatedError as error:
raise ChannelProviderConfigurationUpdatedHttpError() from error
except ChannelProviderError as error:
raise ChannelProviderBadRequestHttpError(error.status_description) from error
return IMChannelMutationResponse(summary=_im_channel_summary_response(snapshot)).model_dump(mode="json")
__all__ = [
"ChannelConflictResponse",
"ChannelDeleteQuery",
"ChannelDeleteResponse",
"ChannelProvider",
"ChannelSummary",
"ChannelTestResponse",
"EmailChannelCreatePayload",
"EmailChannelDetailResponse",
"EmailChannelTestPayload",
"EmailChannelUpdatePayload",
"IMChannelCreatePayload",
"IMChannelDetailResponse",
"IMChannelReplacementPayload",
"IMChannelTestPayload",
"IMChannelUpdatePayload",
"ListChannelProvidersResponse",
"ListChannelsResponse",
]