import logging from collections.abc import Mapping from dataclasses import asdict from datetime import datetime from typing import Any, Literal from uuid import UUID from flask import Response, request from flask_restx import Resource from pydantic import AliasChoices, BaseModel, Field, field_validator from werkzeug.exceptions import InternalServerError, NotFound, Unauthorized import services from configs import dify_config from controllers.common.fields import ( AudioBinaryResponse, AudioTranscriptResponse, SimpleResultResponse, ) from controllers.common.fields import Parameters as ParametersResponse from controllers.common.fields import Site as SiteResponse from controllers.common.schema import ( JsonResponseWithStatus, query_params_from_model, register_response_schema_models, register_schema_models, ) from controllers.console import console_ns from controllers.console.app.error import ( AppNotFoundError, AppUnavailableError, AudioTooLargeError, CompletionRequestError, ConversationCompletedError, NeedAddIdsError, NoAudioUploadedError, ProviderModelCurrentlyNotSupportError, ProviderNotInitializeError, ProviderNotSupportSpeechToTextError, ProviderQuotaExceededError, SpeechToTextDisabledError, UnsupportedAudioTypeError, ) from controllers.console.app.preview_admission import get_preview_app from controllers.console.explore.error import ( AppPreviewOwnerUnavailableError as AppPreviewOwnerUnavailableHttpError, ) from controllers.console.explore.error import ( AppPreviewSiteUnavailableError as AppPreviewSiteUnavailableHttpError, ) from controllers.console.explore.error import ( AppSuggestedQuestionsAfterAnswerDisabledError, NotChatAppError, NotCompletionAppError, NotWorkflowAppError, ) from controllers.console.explore.trial_app_admission import get_trial_app from controllers.console.explore.wraps import TrialAppResource from controllers.console.files import FILE_UPLOAD_PARAMS, upload_file_from_request_context from controllers.console.flask_admission import console_account_admission from controllers.console.remote_files import RemoteFileUploadPayload, upload_remote_file from controllers.console.wraps import cloud_edition_billing_resource_check, model_validate, with_current_user from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError from core.errors.error import ( AppInvokeQuotaExceededError, ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError, ) from core.helper import encrypter from core.workflow.llm_environment_variable import LLMEnvironmentVariable, dump_environment_variable from extensions.ext_application_services import application_services from extensions.ext_database import db from fields.base import ResponseModel from fields.conversation_variable_fields import WorkflowConversationVariableResponse from fields.file_fields import FileResponse, FileWithSignedUrl from fields.message_fields import SuggestedQuestionsResponse from graphon.model_runtime.errors.invoke import InvokeError from graphon.variables import SecretVariable, VariableBase from libs import helper from libs.helper import dump_response, to_timestamp, uuid_value from libs.url_utils import normalize_api_base_url from machinery.context import RequestContext from models import Account from models.enums import CreatorUserRole from services.account_errors import AccountNotFoundError from services.app_definition_query_service import AppDefinitionUnavailableError from services.app_preview_query_service import ( AppPreviewOwnerUnavailableError, AppPreviewRef, AppPreviewSiteUnavailableError, AppPreviewUnavailableError, ) from services.app_ref_service import AppRefService from services.audio_service import AudioService from services.errors.audio import ( AudioTooLargeServiceError, NoAudioUploadedServiceError, ProviderNotSupportSpeechToTextServiceError, SpeechToTextDisabledServiceError, UnsupportedAudioTypeServiceError, ) from services.errors.conversation import ConversationNotExistsError from services.errors.llm import InvokeRateLimitError from services.errors.message import ( MessageNotExistsError, SuggestedQuestionsAfterAnswerDisabledError, ) from services.file_service import FileUploadActor from services.message_suggested_questions_service import SuggestedQuestionsAccount, SuggestedQuestionsActorNotFoundError from services.trial_app_access_service import TrialAppRef from services.trial_app_generation_service import ( TrialAppNotChatError, TrialAppNotCompletionError, TrialAppNotWorkflowError, ) logger = logging.getLogger(__name__) class WorkflowRunRequest(BaseModel): inputs: dict files: list | None = Field(default=None) class ChatRequest(BaseModel): inputs: dict query: str files: list | None = Field(default=None) conversation_id: str | None = None parent_message_id: str | None = None retriever_from: str = "explore_app" class TextToSpeechRequest(BaseModel): message_id: str | None = None voice: str | None = None text: str | None = None streaming: bool | None = None class CompletionRequest(BaseModel): inputs: dict query: str = "" files: list | None = Field(default=None) response_mode: Literal["blocking", "streaming"] | None = None retriever_from: str = "explore_app" class TrialDatasetListQuery(BaseModel): page: int = Field(default=1, ge=1, description="Page number") limit: int = Field(default=20, ge=1, description="Number of items per page") ids: list[str] = Field(default_factory=list, description="Dataset IDs") type TrialAppMode = Literal["chat", "agent-chat", "advanced-chat", "workflow", "completion"] type TrialIconType = Literal["emoji", "image", "link"] type JsonObject = dict[str, Any] class TrialAppModel(ResponseModel): provider: str name: str mode: str | None = None completion_params: JsonObject = Field(default_factory=dict) class TrialAppAgentMode(ResponseModel): enabled: bool | None = None strategy: str | None = None tools: list[JsonObject] = Field(default_factory=list) class TrialAppModelConfigResponse(ResponseModel): opening_statement: str | None = None suggested_questions: list[str] = Field( default_factory=list, validation_alias=AliasChoices("suggested_questions_list", "suggested_questions"), ) suggested_questions_after_answer: JsonObject | None = Field( default=None, validation_alias=AliasChoices("suggested_questions_after_answer_dict", "suggested_questions_after_answer"), ) speech_to_text: JsonObject | None = Field( default=None, validation_alias=AliasChoices("speech_to_text_dict", "speech_to_text") ) text_to_speech: JsonObject | None = Field( default=None, validation_alias=AliasChoices("text_to_speech_dict", "text_to_speech") ) retriever_resource: JsonObject | None = Field( default=None, validation_alias=AliasChoices("retriever_resource_dict", "retriever_resource") ) annotation_reply: JsonObject | None = Field( default=None, validation_alias=AliasChoices("annotation_reply_dict", "annotation_reply") ) more_like_this: JsonObject | None = Field( default=None, validation_alias=AliasChoices("more_like_this_dict", "more_like_this") ) sensitive_word_avoidance: JsonObject | None = Field( default=None, validation_alias=AliasChoices("sensitive_word_avoidance_dict", "sensitive_word_avoidance") ) external_data_tools: list[JsonObject] = Field( default_factory=list, validation_alias=AliasChoices("external_data_tools_list", "external_data_tools") ) model: TrialAppModel | None = Field(default=None, validation_alias=AliasChoices("model_dict", "model")) user_input_form: list[JsonObject] = Field( default_factory=list, validation_alias=AliasChoices("user_input_form_list", "user_input_form") ) dataset_query_variable: str | None = None pre_prompt: str | None = None agent_mode: TrialAppAgentMode | None = Field( default=None, validation_alias=AliasChoices("agent_mode_dict", "agent_mode"), ) prompt_type: str | None = None chat_prompt_config: JsonObject | None = Field( default=None, validation_alias=AliasChoices("chat_prompt_config_dict", "chat_prompt_config") ) completion_prompt_config: JsonObject | None = Field( default=None, validation_alias=AliasChoices("completion_prompt_config_dict", "completion_prompt_config") ) dataset_configs: JsonObject | None = Field( default=None, validation_alias=AliasChoices("dataset_configs_dict", "dataset_configs"), ) file_upload: JsonObject | None = Field( default=None, validation_alias=AliasChoices("file_upload_dict", "file_upload"), ) created_by: str | None = None created_at: int | None = None updated_by: str | None = None updated_at: int | None = None @field_validator("created_at", "updated_at", mode="before") @classmethod def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: return to_timestamp(value) class TrialDeletedToolResponse(ResponseModel): type: str tool_name: str provider_id: str class TrialTagResponse(ResponseModel): id: str name: str type: str class TrialSiteResponse(ResponseModel): access_token: str | None = Field(default=None, validation_alias="code") code: str | None = None title: str icon_type: TrialIconType | None = None icon: str | None = None icon_background: str | None = None description: str | None = None default_language: str chat_color_theme: str | None = None chat_color_theme_inverted: bool | None = None customize_domain: str | None = None copyright: str | None = None privacy_policy: str | None = None input_placeholder: str | None = None custom_disclaimer: str | None = None customize_token_strategy: str | None = None prompt_public: bool | None = None app_base_url: str | None = None show_workflow_steps: bool | None = None use_icon_as_answer_icon: bool | None = None created_by: str | None = None created_at: int | None = None updated_by: str | None = None updated_at: int | None = None icon_url: str | None = None @field_validator("icon_type", mode="before") @classmethod def _normalize_icon_type(cls, value: Any) -> str | None: if hasattr(value, "value"): return value.value return value @field_validator("created_at", "updated_at", mode="before") @classmethod def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: return to_timestamp(value) class TrialWorkflowPartialResponse(ResponseModel): id: str created_by: str | None = None created_at: int | None = None updated_by: str | None = None updated_at: int | None = None @field_validator("created_at", "updated_at", mode="before") @classmethod def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: return to_timestamp(value) class TrialAppDetailResponse(ResponseModel): id: str name: str description: str | None = None mode: TrialAppMode = Field(validation_alias="mode_compatible_with_agent") icon_type: TrialIconType | None = None icon: str | None = None icon_background: str | None = None icon_url: str | None = None enable_site: bool enable_api: bool model_config_: TrialAppModelConfigResponse | None = Field( default=None, validation_alias=AliasChoices("app_model_config", "model_config"), alias="model_config", ) workflow: TrialWorkflowPartialResponse | None = None api_base_url: str | None = None use_icon_as_answer_icon: bool | None = None max_active_requests: int | None = None created_by: str | None = None created_at: int | None = None updated_by: str | None = None updated_at: int | None = None deleted_tools: list[TrialDeletedToolResponse] = Field(default_factory=list) access_mode: str | None = None tags: list[TrialTagResponse] = Field(default_factory=list) permission_keys: list[str] = Field(default_factory=list) site: TrialSiteResponse @field_validator("icon_type", mode="before") @classmethod def _normalize_icon_type(cls, value: Any) -> str | None: if hasattr(value, "value"): return value.value return value @field_validator("created_at", "updated_at", mode="before") @classmethod def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: return to_timestamp(value) class TrialDatasetResponse(ResponseModel): id: str name: str description: str | None = None permission: str | None = None data_source_type: str | None = None indexing_technique: str | None = None created_by: str | None = None created_at: int | None = None permission_keys: list[str] = Field(default_factory=list) @field_validator("created_at", mode="before") @classmethod def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: return to_timestamp(value) class TrialDatasetListResponse(ResponseModel): data: list[TrialDatasetResponse] has_more: bool limit: int total: int page: int class TrialSimpleAccount(ResponseModel): id: str name: str | None = None email: str | None = None class TrialWorkflowResponse(ResponseModel): id: str graph: JsonObject = Field(validation_alias=AliasChoices("graph_dict", "graph")) features: JsonObject = Field(default_factory=dict, validation_alias=AliasChoices("features_dict", "features")) hash: str | None = Field(default=None, validation_alias=AliasChoices("unique_hash", "hash")) version: str | None = None marked_name: str | None = None marked_comment: str | None = None created_by: TrialSimpleAccount | None = Field( default=None, validation_alias=AliasChoices("created_by_account", "created_by"), ) created_at: int | None = None updated_by: TrialSimpleAccount | None = Field( default=None, validation_alias=AliasChoices("updated_by_account", "updated_by"), ) updated_at: int | None = None tool_published: bool | None = None environment_variables: list[JsonObject] = Field(default_factory=list) conversation_variables: list[WorkflowConversationVariableResponse] = Field(default_factory=list) rag_pipeline_variables: list[JsonObject] = Field(default_factory=list) @field_validator("created_at", "updated_at", mode="before") @classmethod def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: return to_timestamp(value) @field_validator("environment_variables", mode="before") @classmethod def _serialize_environment_variables(cls, value: Any) -> list[Any]: if value is None: return [] result: list[Any] = [] for item in value: if isinstance(item, Mapping): serialized = dict(item) if serialized.get("value_type") == "secret": serialized["value"] = encrypter.full_mask_token() result.append(serialized) elif isinstance(item, SecretVariable): serialized = item.model_dump(mode="json") serialized["value"] = encrypter.full_mask_token() result.append(serialized) elif isinstance(item, LLMEnvironmentVariable): result.append(dump_environment_variable(item, mode="json")) elif isinstance(item, VariableBase): result.append(item.model_dump(mode="json")) else: result.append(item) return result register_schema_models( console_ns, WorkflowRunRequest, ChatRequest, TextToSpeechRequest, CompletionRequest, TrialDatasetListQuery, ) register_response_schema_models( console_ns, ParametersResponse, AudioBinaryResponse, AudioTranscriptResponse, SimpleResultResponse, SiteResponse, SuggestedQuestionsResponse, TrialAppDetailResponse, TrialDatasetListResponse, TrialWorkflowResponse, ) simple_account_model = console_ns.models[TrialSimpleAccount.__name__] class TrialAppFileUploadApi(Resource): @console_ns.doc(consumes=["multipart/form-data"], params=FILE_UPLOAD_PARAMS) @console_ns.response(201, "File uploaded successfully", console_ns.models[FileResponse.__name__]) @console_account_admission() @get_trial_app @cloud_edition_billing_resource_check("documents") def post(self, request_context: RequestContext, trial_app: TrialAppRef) -> JsonResponseWithStatus: """Upload a file into the tenant that owns the trial app.""" upload_file = upload_file_from_request_context( request_context=request_context, resource_tenant_id=trial_app.tenant_id, ) return dump_response(FileResponse, upload_file), 201 class TrialAppRemoteFileUploadApi(Resource): @console_ns.expect(console_ns.models[RemoteFileUploadPayload.__name__]) @console_ns.response(201, "File uploaded successfully", console_ns.models[FileWithSignedUrl.__name__]) @console_account_admission() @get_trial_app @cloud_edition_billing_resource_check("documents") @model_validate(RemoteFileUploadPayload) def post( self, payload: RemoteFileUploadPayload, request_context: RequestContext, trial_app: TrialAppRef ) -> JsonResponseWithStatus: """Upload a remote file into the tenant that owns the trial app.""" remote_file = upload_remote_file( url=payload.url, current_user=FileUploadActor(id=request_context.account_id, creator_role=CreatorUserRole.ACCOUNT), resource_tenant_id=trial_app.tenant_id, ) return dump_response(FileWithSignedUrl, remote_file), 201 class TrialAppWorkflowRunApi(Resource): @console_ns.expect(console_ns.models[WorkflowRunRequest.__name__]) @console_ns.response(200, "Success") @console_account_admission() @get_trial_app @model_validate(WorkflowRunRequest) def post(self, req_data: WorkflowRunRequest, request_context: RequestContext, trial_app: TrialAppRef) -> Response: """ Run workflow """ try: response = application_services().trial_app_generation.generate_workflow( trial_app=trial_app, account_id=request_context.account_id, args=req_data.model_dump(), ) # response-contract:ignore compact_generate_response return helper.compact_generate_response(response) except TrialAppNotWorkflowError as error: raise NotWorkflowAppError() from error except AppDefinitionUnavailableError as error: raise AppUnavailableError() from error except AccountNotFoundError as error: raise Unauthorized("Account no longer exists.") from error except services.errors.app_model_config.AppModelConfigBrokenError as error: raise AppUnavailableError() from error except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) except QuotaExceededError: raise ProviderQuotaExceededError() except ModelCurrentlyNotSupportError: raise ProviderModelCurrentlyNotSupportError() except InvokeError as e: raise CompletionRequestError(e.description) except (AppInvokeQuotaExceededError, InvokeRateLimitError) as ex: raise InvokeRateLimitHttpError(ex.description) except ValueError as e: raise e except Exception: logger.exception("internal server error.") raise InternalServerError() class TrialAppWorkflowTaskStopApi(Resource): @console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__]) @console_account_admission() @get_trial_app def post(self, request_context: RequestContext, trial_app: TrialAppRef, task_id: str) -> dict[str, object]: """ Stop workflow task """ if trial_app.app_mode != "workflow": raise NotWorkflowAppError() application_services().app_tasks.stop_workflow_task_no_user_check(task_id=task_id) return dump_response(SimpleResultResponse, {"result": "success"}) class TrialChatApi(Resource): @console_ns.expect(console_ns.models[ChatRequest.__name__]) @console_ns.response(200, "Success") @console_account_admission() @get_trial_app @model_validate(ChatRequest) def post(self, req_data: ChatRequest, request_context: RequestContext, trial_app: TrialAppRef) -> Response: args = req_data.model_dump() # Validate UUID values if provided if args.get("conversation_id"): args["conversation_id"] = uuid_value(args["conversation_id"]) if args.get("parent_message_id"): args["parent_message_id"] = uuid_value(args["parent_message_id"]) try: response = application_services().trial_app_generation.generate_chat( trial_app=trial_app, account_id=request_context.account_id, args=args, ) # response-contract:ignore compact_generate_response return helper.compact_generate_response(response) except TrialAppNotChatError as error: raise NotChatAppError() from error except AppDefinitionUnavailableError as error: raise AppUnavailableError() from error except AccountNotFoundError as error: raise Unauthorized("Account no longer exists.") from error except services.errors.conversation.ConversationNotExistsError: raise NotFound("Conversation Not Exists.") except services.errors.conversation.ConversationCompletedError: raise ConversationCompletedError() except services.errors.app_model_config.AppModelConfigBrokenError: logger.exception("App model config broken.") raise AppUnavailableError() except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) except QuotaExceededError: raise ProviderQuotaExceededError() except ModelCurrentlyNotSupportError: raise ProviderModelCurrentlyNotSupportError() except InvokeError as e: raise CompletionRequestError(e.description) except (AppInvokeQuotaExceededError, InvokeRateLimitError) as ex: raise InvokeRateLimitHttpError(ex.description) except ValueError as e: raise e except Exception: logger.exception("internal server error.") raise InternalServerError() class TrialMessageSuggestedQuestionApi(Resource): @console_ns.response(200, "Success", console_ns.models[SuggestedQuestionsResponse.__name__]) @console_account_admission() @get_trial_app def get(self, request_context: RequestContext, trial_app: TrialAppRef, message_id: UUID) -> dict[str, object]: if trial_app.app_mode not in {"chat", "agent-chat", "advanced-chat"}: raise NotChatAppError() try: questions = application_services().message_suggested_questions.get_suggested_questions( app_id=trial_app.app_id, app_owner_tenant_id=trial_app.tenant_id, expected_app_mode=trial_app.app_mode, actor=SuggestedQuestionsAccount(account_id=request_context.account_id, invoke_from="explore"), message_id=str(message_id), ) except AppDefinitionUnavailableError as error: raise AppUnavailableError() from error except SuggestedQuestionsActorNotFoundError as error: raise Unauthorized("Account no longer exists.") from error except MessageNotExistsError: raise NotFound("Message not found") except ConversationNotExistsError: raise NotFound("Conversation not found") except SuggestedQuestionsAfterAnswerDisabledError: raise AppSuggestedQuestionsAfterAnswerDisabledError() except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) except QuotaExceededError: raise ProviderQuotaExceededError() except ModelCurrentlyNotSupportError: raise ProviderModelCurrentlyNotSupportError() except InvokeError as e: raise CompletionRequestError(e.description) except Exception: logger.exception("internal server error.") raise InternalServerError() return dump_response(SuggestedQuestionsResponse, {"data": questions}) class TrialChatAudioApi(TrialAppResource): @console_ns.response(200, "Success", console_ns.models[AudioTranscriptResponse.__name__]) @with_current_user def post(self, current_user: Account, trial_app): app_model = trial_app file = request.files.get("file") try: # Get IDs before they might be detached from session app_id = app_model.id user_id = current_user.id response = AudioService.transcript_asr( app_model=app_model, file=file, session=db.session(), end_user=None, ) application_services().trial_app_usage.record(app_id=app_id, account_id=user_id) return response except services.errors.app_model_config.AppModelConfigBrokenError: logger.exception("App model config broken.") raise AppUnavailableError() except NoAudioUploadedServiceError: raise NoAudioUploadedError() except AudioTooLargeServiceError as e: raise AudioTooLargeError(str(e)) except UnsupportedAudioTypeServiceError: raise UnsupportedAudioTypeError() except ProviderNotSupportSpeechToTextServiceError: raise ProviderNotSupportSpeechToTextError() except SpeechToTextDisabledServiceError: raise SpeechToTextDisabledError() except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) except QuotaExceededError: raise ProviderQuotaExceededError() except ModelCurrentlyNotSupportError: raise ProviderModelCurrentlyNotSupportError() except InvokeError as e: raise CompletionRequestError(e.description) except ValueError as e: raise e except Exception as e: logger.exception("internal server error.") raise InternalServerError() class TrialChatTextApi(TrialAppResource): @console_ns.expect(console_ns.models[TextToSpeechRequest.__name__]) @console_ns.response(200, "Success", console_ns.models[AudioBinaryResponse.__name__]) @with_current_user @model_validate(TextToSpeechRequest) def post(self, req_data: TextToSpeechRequest, current_user: Account, trial_app): app_model = trial_app try: message_id = req_data.message_id text = req_data.text voice = req_data.voice message_ref = None if message_id: app_ref = AppRefService.create_app_ref(app_model) message_ref = AppRefService.create_message_ref( app_ref, message_id, account_id=current_user.id, ) # Get IDs before they might be detached from session app_id = app_model.id user_id = current_user.id response = AudioService.transcript_tts( app_model=app_model, session=db.session(), text=text, voice=voice, message_ref=message_ref, ) application_services().trial_app_usage.record(app_id=app_id, account_id=user_id) return response except services.errors.app_model_config.AppModelConfigBrokenError: logger.exception("App model config broken.") raise AppUnavailableError() except NoAudioUploadedServiceError: raise NoAudioUploadedError() except AudioTooLargeServiceError as e: raise AudioTooLargeError(str(e)) except UnsupportedAudioTypeServiceError: raise UnsupportedAudioTypeError() except ProviderNotSupportSpeechToTextServiceError: raise ProviderNotSupportSpeechToTextError() except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) except QuotaExceededError: raise ProviderQuotaExceededError() except ModelCurrentlyNotSupportError: raise ProviderModelCurrentlyNotSupportError() except InvokeError as e: raise CompletionRequestError(e.description) except ValueError as e: raise e except Exception as e: logger.exception("internal server error.") raise InternalServerError() class TrialCompletionApi(Resource): @console_ns.expect(console_ns.models[CompletionRequest.__name__]) @console_ns.response(200, "Success") @console_account_admission() @get_trial_app @model_validate(CompletionRequest) def post(self, req_data: CompletionRequest, request_context: RequestContext, trial_app: TrialAppRef) -> Response: try: response = application_services().trial_app_generation.generate_completion( trial_app=trial_app, account_id=request_context.account_id, args=req_data.model_dump(), ) # response-contract:ignore compact_generate_response return helper.compact_generate_response(response) except TrialAppNotCompletionError as error: raise NotCompletionAppError() from error except AppDefinitionUnavailableError as error: raise AppUnavailableError() from error except AccountNotFoundError as error: raise Unauthorized("Account no longer exists.") from error except services.errors.conversation.ConversationNotExistsError: raise NotFound("Conversation Not Exists.") except services.errors.conversation.ConversationCompletedError: raise ConversationCompletedError() except services.errors.app_model_config.AppModelConfigBrokenError: logger.exception("App model config broken.") raise AppUnavailableError() except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) except QuotaExceededError: raise ProviderQuotaExceededError() except ModelCurrentlyNotSupportError: raise ProviderModelCurrentlyNotSupportError() except InvokeError as e: raise CompletionRequestError(e.description) except (AppInvokeQuotaExceededError, InvokeRateLimitError) as ex: raise InvokeRateLimitHttpError(ex.description) except ValueError as e: raise e except Exception: logger.exception("internal server error.") raise InternalServerError() class TrialSitApi(Resource): """Resource for trial app sites.""" @console_ns.response(200, "Success", console_ns.models[SiteResponse.__name__]) @get_preview_app def get(self, app: AppPreviewRef) -> dict[str, object]: """Retrieve app site info. Returns the site configuration for the application including theme, icons, and text. """ try: site = application_services().app_previews.get_site(app=app) except AppPreviewSiteUnavailableError as error: raise AppPreviewSiteUnavailableHttpError(str(error)) from error except AppPreviewOwnerUnavailableError as error: raise AppPreviewOwnerUnavailableHttpError(str(error)) from error return dump_response(SiteResponse, site) class TrialAppParameterApi(Resource): """Resource for app variables.""" @console_ns.response(200, "Success", console_ns.models[ParametersResponse.__name__]) @get_preview_app def get(self, app: AppPreviewRef) -> dict[str, object]: """Retrieve app parameters.""" try: parameters = application_services().app_definitions.get_parameters(app.app_id) except AppDefinitionUnavailableError: raise AppUnavailableError() from None return dump_response(ParametersResponse, parameters) class AppApi(Resource): @console_ns.response(200, "Success", console_ns.models[TrialAppDetailResponse.__name__]) @console_account_admission() @get_preview_app def get(self, app: AppPreviewRef, request_context: RequestContext) -> dict[str, object]: """Get app detail using the viewer's workspace for tool configuration.""" try: detail = application_services().app_preview_details.get_detail( app=app, account_id=request_context.account_id, active_workspace_id=request_context.active_workspace_id, ) except AppPreviewUnavailableError as error: raise AppNotFoundError() from error except AppDefinitionUnavailableError as error: raise AppUnavailableError() from error except AccountNotFoundError as error: raise Unauthorized("Account no longer exists.") from error except AppPreviewSiteUnavailableError as error: raise AppPreviewSiteUnavailableHttpError(str(error)) from error source = asdict(detail) source["api_base_url"] = normalize_api_base_url(dify_config.SERVICE_API_URL or request.host_url.rstrip("/")) source["site"] = { **asdict(detail.site), "app_base_url": dify_config.APP_WEB_URL or request.url_root.rstrip("/"), } return dump_response(TrialAppDetailResponse, source) class AppWorkflowApi(Resource): @console_ns.response(200, "Success", console_ns.models[TrialWorkflowResponse.__name__]) @get_preview_app def get(self, app: AppPreviewRef) -> dict[str, object]: """Get a detached workflow definition after catalog preview admission.""" try: workflow = application_services().app_preview_details.get_workflow(app=app) except AppPreviewUnavailableError as error: raise AppNotFoundError() from error except AppDefinitionUnavailableError as error: raise AppUnavailableError() from error return dump_response(TrialWorkflowResponse, workflow) class DatasetListApi(Resource): @console_ns.doc(params=query_params_from_model(TrialDatasetListQuery)) @console_ns.response(200, "Success", console_ns.models[TrialDatasetListResponse.__name__]) @get_preview_app def get(self, app: AppPreviewRef) -> dict[str, object]: # These legacy fields are response metadata: the query returns all # requested IDs without pagination. Keep their integer fallback and echo behavior. page = request.args.get("page", default=1, type=int) limit = request.args.get("limit", default=20, type=int) ids = request.args.getlist("ids") if not ids: raise NeedAddIdsError() try: datasets = application_services().app_previews.get_datasets(app=app, ids=ids) except AppPreviewUnavailableError as error: raise AppNotFoundError() from error response = { "data": datasets, "has_more": False, "limit": limit, "total": len(datasets), "page": page, } return dump_response(TrialDatasetListResponse, response) console_ns.add_resource(TrialChatApi, "/trial-apps//chat-messages", endpoint="trial_app_chat_completion") console_ns.add_resource( TrialAppFileUploadApi, "/trial-apps//files/upload", endpoint="trial_app_file_upload", ) console_ns.add_resource( TrialAppRemoteFileUploadApi, "/trial-apps//remote-files/upload", endpoint="trial_app_remote_file_upload", ) console_ns.add_resource( TrialMessageSuggestedQuestionApi, "/trial-apps//messages//suggested-questions", endpoint="trial_app_suggested_question", ) console_ns.add_resource(TrialChatAudioApi, "/trial-apps//audio-to-text", endpoint="trial_app_audio") console_ns.add_resource(TrialChatTextApi, "/trial-apps//text-to-audio", endpoint="trial_app_text") console_ns.add_resource( TrialCompletionApi, "/trial-apps//completion-messages", endpoint="trial_app_completion" ) console_ns.add_resource(TrialSitApi, "/trial-apps//site") console_ns.add_resource(TrialAppParameterApi, "/trial-apps//parameters", endpoint="trial_app_parameters") console_ns.add_resource(AppApi, "/trial-apps/", endpoint="trial_app") console_ns.add_resource( TrialAppWorkflowRunApi, "/trial-apps//workflows/run", endpoint="trial_app_workflow_run" ) console_ns.add_resource(TrialAppWorkflowTaskStopApi, "/trial-apps//workflows/tasks//stop") console_ns.add_resource(AppWorkflowApi, "/trial-apps//workflows", endpoint="trial_app_workflow") console_ns.add_resource(DatasetListApi, "/trial-apps//datasets", endpoint="trial_app_datasets")