mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 02:28:30 +08:00
refactor(api): migrate web chat endpoints to BaseModel (#37962)
This commit is contained in:
parent
89d5f74a40
commit
458aa4892d
@ -1,13 +1,11 @@
|
||||
import logging
|
||||
|
||||
from flask import request
|
||||
from flask_restx import fields, marshal_with
|
||||
from pydantic import field_validator
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
|
||||
import services
|
||||
from controllers.common.controller_schemas import TextToAudioPayload as TextToAudioPayloadBase
|
||||
from controllers.common.fields import AudioBinaryResponse, AudioTranscriptResponse
|
||||
from controllers.web import web_ns
|
||||
from controllers.web.error import (
|
||||
AppUnavailableError,
|
||||
@ -23,8 +21,9 @@ from controllers.web.error import (
|
||||
from controllers.web.wraps import WebApiResource
|
||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from graphon.model_runtime.errors.invoke import InvokeError
|
||||
from libs.helper import uuid_value
|
||||
from libs.helper import dump_response, uuid_value
|
||||
from models.model import App, EndUser
|
||||
from services.app_ref_service import AppRefService
|
||||
from services.audio_service import AudioService
|
||||
@ -38,6 +37,10 @@ from services.errors.audio import (
|
||||
from ..common.schema import register_response_schema_models, register_schema_models
|
||||
|
||||
|
||||
class AudioToTextResponse(ResponseModel):
|
||||
text: str
|
||||
|
||||
|
||||
class TextToAudioPayload(TextToAudioPayloadBase):
|
||||
@field_validator("message_id")
|
||||
@classmethod
|
||||
@ -48,18 +51,13 @@ class TextToAudioPayload(TextToAudioPayloadBase):
|
||||
|
||||
|
||||
register_schema_models(web_ns, TextToAudioPayload)
|
||||
register_response_schema_models(web_ns, AudioBinaryResponse, AudioTranscriptResponse)
|
||||
register_response_schema_models(web_ns, AudioToTextResponse)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@web_ns.route("/audio-to-text")
|
||||
class AudioApi(WebApiResource):
|
||||
audio_to_text_response_fields = {
|
||||
"text": fields.String,
|
||||
}
|
||||
|
||||
@marshal_with(audio_to_text_response_fields)
|
||||
@web_ns.doc("Audio to Text")
|
||||
@web_ns.doc(description="Convert audio file to text using speech-to-text service.")
|
||||
@web_ns.doc(
|
||||
@ -73,7 +71,7 @@ class AudioApi(WebApiResource):
|
||||
500: "Internal Server Error",
|
||||
}
|
||||
)
|
||||
@web_ns.response(200, "Success", web_ns.models[AudioTranscriptResponse.__name__])
|
||||
@web_ns.response(200, "Success", web_ns.models[AudioToTextResponse.__name__])
|
||||
def post(self, app_model: App, end_user: EndUser):
|
||||
"""Convert audio to text"""
|
||||
file = request.files["file"]
|
||||
@ -81,7 +79,7 @@ class AudioApi(WebApiResource):
|
||||
try:
|
||||
response = AudioService.transcript_asr(app_model=app_model, file=file, end_user=end_user.external_user_id)
|
||||
|
||||
return response
|
||||
return dump_response(AudioToTextResponse, response)
|
||||
except services.errors.app_model_config.AppModelConfigBrokenError:
|
||||
logger.exception("App model config broken.")
|
||||
raise AppUnavailableError()
|
||||
@ -122,7 +120,8 @@ class TextApi(WebApiResource):
|
||||
500: "Internal Server Error",
|
||||
}
|
||||
)
|
||||
@web_ns.response(200, "Success", web_ns.models[AudioBinaryResponse.__name__])
|
||||
# response-contract:ignore provider audio bytes; TODO: model binary audio response if shape is standardized.
|
||||
@web_ns.response(200, "Success")
|
||||
def post(self, app_model: App, end_user: EndUser):
|
||||
"""Convert text to audio"""
|
||||
try:
|
||||
@ -139,7 +138,7 @@ class TextApi(WebApiResource):
|
||||
message_id,
|
||||
end_user_id=end_user.id,
|
||||
)
|
||||
response = AudioService.transcript_tts(
|
||||
return AudioService.transcript_tts(
|
||||
app_model=app_model,
|
||||
session=db.session(),
|
||||
text=text,
|
||||
@ -147,8 +146,6 @@ class TextApi(WebApiResource):
|
||||
end_user=end_user.external_user_id,
|
||||
message_ref=message_ref,
|
||||
)
|
||||
|
||||
return response
|
||||
except services.errors.app_model_config.AppModelConfigBrokenError:
|
||||
logger.exception("App model config broken.")
|
||||
raise AppUnavailableError()
|
||||
|
||||
@ -133,6 +133,7 @@ class CompletionApi(WebApiResource):
|
||||
streaming=streaming,
|
||||
)
|
||||
|
||||
# response-contract:ignore compact_generate_response
|
||||
return helper.compact_generate_response(response)
|
||||
except services.errors.conversation.ConversationNotExistsError:
|
||||
raise NotFound("Conversation Not Exists.")
|
||||
@ -185,7 +186,7 @@ class CompletionStopApi(WebApiResource):
|
||||
app_mode=AppMode.value_of(app_model.mode),
|
||||
)
|
||||
|
||||
return {"result": "success"}, 200
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
|
||||
|
||||
@web_ns.route("/chat-messages")
|
||||
@ -235,6 +236,7 @@ class ChatApi(WebApiResource):
|
||||
streaming=streaming,
|
||||
)
|
||||
|
||||
# response-contract:ignore compact_generate_response
|
||||
return helper.compact_generate_response(response)
|
||||
except services.errors.conversation.ConversationNotExistsError:
|
||||
raise NotFound("Conversation Not Exists.")
|
||||
@ -290,4 +292,4 @@ class ChatStopApi(WebApiResource):
|
||||
app_mode=app_mode,
|
||||
)
|
||||
|
||||
return {"result": "success"}, 200
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
|
||||
@ -13,6 +13,7 @@ from controllers.web import web_ns
|
||||
from controllers.web.wraps import WebApiResource
|
||||
from extensions.ext_database import db
|
||||
from fields.file_fields import FileResponse
|
||||
from libs.helper import dump_response
|
||||
from models.model import App, EndUser
|
||||
from services.file_service import FileService
|
||||
|
||||
@ -84,5 +85,4 @@ class FileApi(WebApiResource):
|
||||
except services.errors.file.UnsupportedFileTypeError:
|
||||
raise UnsupportedFileTypeError()
|
||||
|
||||
response = FileResponse.model_validate(upload_file, from_attributes=True)
|
||||
return response.model_dump(mode="json"), 201
|
||||
return dump_response(FileResponse, upload_file), 201
|
||||
|
||||
@ -29,6 +29,7 @@ from extensions.ext_database import db
|
||||
from fields.file_fields import FileResponse, FileWithSignedUrl
|
||||
from graphon.file import helpers as file_helpers
|
||||
from libs.exception import BaseHTTPException
|
||||
from libs.helper import dump_response
|
||||
from repositories.factory import DifyAPIRepositoryFactory
|
||||
from services.file_service import FileService
|
||||
from services.human_input_file_upload_service import (
|
||||
@ -141,8 +142,7 @@ def _upload_local_file(context):
|
||||
except services.errors.file.BlockedFileExtensionError as exc:
|
||||
raise BlockedFileExtensionError() from exc
|
||||
|
||||
response = FileResponse.model_validate(upload_file, from_attributes=True)
|
||||
return upload_file.id, response
|
||||
return upload_file.id, dump_response(FileResponse, upload_file)
|
||||
|
||||
|
||||
def _upload_remote_file(context, url: str):
|
||||
@ -186,7 +186,7 @@ def _upload_remote_file(context, url: str):
|
||||
created_by=upload_file.created_by,
|
||||
created_at=int(upload_file.created_at.timestamp()),
|
||||
)
|
||||
return upload_file.id, response
|
||||
return upload_file.id, response.model_dump(mode="json")
|
||||
|
||||
|
||||
@web_ns.route("/human-input-forms/files")
|
||||
@ -209,4 +209,5 @@ class HumanInputFileUploadApi(Resource):
|
||||
file_id, response = _upload_local_file(context=context)
|
||||
|
||||
upload_service.record_upload_file(context=context, file_id=file_id)
|
||||
return response.model_dump(mode="json"), 201
|
||||
# response-contract:ignore pre-dumped response. See above
|
||||
return response, 201
|
||||
|
||||
@ -2,14 +2,12 @@
|
||||
Web App Human Input Form APIs.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, NotRequired, TypedDict
|
||||
from typing import Self
|
||||
|
||||
from flask import Response, request
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from werkzeug.exceptions import Forbidden
|
||||
@ -20,35 +18,58 @@ from controllers.common.human_input import HumanInputFormSubmitPayload, stringif
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_models
|
||||
from controllers.web import web_ns
|
||||
from controllers.web.error import WebFormRateLimitExceededError
|
||||
from controllers.web.site import serialize_app_site_payload
|
||||
from core.workflow.nodes.human_input.entities import FormInputConfig
|
||||
from controllers.web.site import WebAppSiteResponse
|
||||
from core.workflow.nodes.human_input.entities import FormInputConfig, UserActionConfig
|
||||
from extensions.ext_database import db
|
||||
from libs.helper import RateLimiter, extract_remote_ip, to_timestamp
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import RateLimiter, dump_response, extract_remote_ip, to_timestamp
|
||||
from models.account import TenantStatus
|
||||
from models.model import App, Site
|
||||
from repositories.factory import DifyAPIRepositoryFactory
|
||||
from services.feature_service import FeatureService
|
||||
from services.human_input_file_upload_service import HumanInputFileUploadService
|
||||
from services.human_input_service import Form, FormNotFoundError, HumanInputService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HumanInputUploadTokenResponse(BaseModel):
|
||||
class HumanInputUploadTokenResponse(ResponseModel):
|
||||
upload_token: str
|
||||
expires_at: int
|
||||
|
||||
|
||||
class HumanInputFormDefinitionResponse(BaseModel):
|
||||
form_content: Any
|
||||
inputs: Any
|
||||
class HumanInputFormDefinitionResponse(ResponseModel):
|
||||
form_content: str
|
||||
inputs: list[FormInputConfig]
|
||||
resolved_default_values: dict[str, str]
|
||||
user_actions: Any
|
||||
user_actions: list[UserActionConfig]
|
||||
expiration_time: int
|
||||
site: dict[str, Any] | None = Field(default=None)
|
||||
site: WebAppSiteResponse | None = None
|
||||
|
||||
@classmethod
|
||||
def from_form(
|
||||
cls,
|
||||
form: Form,
|
||||
*,
|
||||
inputs: Sequence[FormInputConfig] = (),
|
||||
site: WebAppSiteResponse | None = None,
|
||||
) -> Self:
|
||||
definition_payload = form.get_definition().model_dump(mode="json")
|
||||
expiration_time = to_timestamp(form.expiration_time)
|
||||
if expiration_time is None:
|
||||
raise ValueError("Human input form expiration_time is required")
|
||||
return cls(
|
||||
form_content=definition_payload["rendered_content"],
|
||||
inputs=list(inputs),
|
||||
resolved_default_values=stringify_form_default_values(definition_payload["default_values"]),
|
||||
user_actions=definition_payload["user_actions"],
|
||||
expiration_time=expiration_time,
|
||||
site=site,
|
||||
)
|
||||
|
||||
|
||||
class HumanInputFormSubmitResponse(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
class HumanInputFormSubmitResponse(ResponseModel):
|
||||
pass
|
||||
|
||||
|
||||
register_schema_models(web_ns, HumanInputFormSubmitPayload)
|
||||
@ -86,40 +107,26 @@ def _create_upload_service() -> HumanInputFileUploadService:
|
||||
)
|
||||
|
||||
|
||||
class FormDefinitionPayload(TypedDict):
|
||||
form_content: Any
|
||||
inputs: Any
|
||||
resolved_default_values: dict[str, str]
|
||||
user_actions: Any
|
||||
expiration_time: int
|
||||
site: NotRequired[dict]
|
||||
|
||||
|
||||
def _jsonify_form_definition(
|
||||
form: Form,
|
||||
*,
|
||||
inputs: Sequence[FormInputConfig] = (),
|
||||
site_payload: dict | None = None,
|
||||
) -> Response:
|
||||
"""Return the form payload (optionally with site) as a JSON response."""
|
||||
definition_payload = form.get_definition().model_dump(mode="json")
|
||||
payload: FormDefinitionPayload = {
|
||||
"form_content": definition_payload["rendered_content"],
|
||||
"inputs": [i.model_dump(mode="json") for i in inputs],
|
||||
"resolved_default_values": stringify_form_default_values(definition_payload["default_values"]),
|
||||
"user_actions": definition_payload["user_actions"],
|
||||
"expiration_time": to_timestamp(form.expiration_time),
|
||||
}
|
||||
if site_payload is not None:
|
||||
payload["site"] = site_payload
|
||||
return Response(json.dumps(payload, ensure_ascii=False), mimetype="application/json")
|
||||
|
||||
|
||||
@web_ns.route("/form/human_input/<string:form_token>/upload-token")
|
||||
class HumanInputFormUploadTokenApi(Resource):
|
||||
"""API for issuing HITL upload tokens for active human input forms."""
|
||||
|
||||
@web_ns.response(200, "Success", web_ns.models[HumanInputUploadTokenResponse.__name__])
|
||||
@web_ns.doc("create_human_input_form_upload_token")
|
||||
@web_ns.doc(description="Issue an upload token for an active human input form")
|
||||
@web_ns.doc(params={"form_token": "Human input form token"})
|
||||
@web_ns.doc(
|
||||
responses={
|
||||
200: "Upload token issued successfully",
|
||||
404: "Form not found",
|
||||
412: "Form already submitted or expired",
|
||||
429: "Too many requests",
|
||||
}
|
||||
)
|
||||
@web_ns.response(
|
||||
200,
|
||||
"Upload token issued successfully",
|
||||
web_ns.models[HumanInputUploadTokenResponse.__name__],
|
||||
)
|
||||
def post(self, form_token: str):
|
||||
"""
|
||||
Issue an upload token for a human input form.
|
||||
@ -136,11 +143,9 @@ class HumanInputFormUploadTokenApi(Resource):
|
||||
except FormNotFoundError:
|
||||
raise NotFoundError("Form not found")
|
||||
|
||||
response = HumanInputUploadTokenResponse(
|
||||
upload_token=token.upload_token,
|
||||
expires_at=to_timestamp(token.expires_at),
|
||||
)
|
||||
return response.model_dump(mode="json"), 200
|
||||
return HumanInputUploadTokenResponse(
|
||||
upload_token=token.upload_token, expires_at=to_timestamp(token.expires_at)
|
||||
).model_dump(mode="json"), 200
|
||||
|
||||
|
||||
@web_ns.route("/form/human_input/<string:form_token>")
|
||||
@ -150,7 +155,23 @@ class HumanInputFormApi(Resource):
|
||||
# NOTE(QuantumGhost): this endpoint is unauthenticated on purpose for now.
|
||||
|
||||
# def get(self, _app_model: App, _end_user: EndUser, form_token: str):
|
||||
@web_ns.response(200, "Success", web_ns.models[HumanInputFormDefinitionResponse.__name__])
|
||||
@web_ns.doc("get_human_input_form")
|
||||
@web_ns.doc(description="Get a human input form definition by token")
|
||||
@web_ns.doc(params={"form_token": "Human input form token"})
|
||||
@web_ns.doc(
|
||||
responses={
|
||||
200: "Form retrieved successfully",
|
||||
403: "Forbidden",
|
||||
404: "Form not found",
|
||||
412: "Form already submitted or expired",
|
||||
429: "Too many requests",
|
||||
}
|
||||
)
|
||||
@web_ns.response(
|
||||
200,
|
||||
"Form retrieved successfully",
|
||||
web_ns.models[HumanInputFormDefinitionResponse.__name__],
|
||||
)
|
||||
def get(self, form_token: str):
|
||||
"""
|
||||
Get human input form definition by token.
|
||||
@ -172,17 +193,47 @@ class HumanInputFormApi(Resource):
|
||||
|
||||
service.ensure_form_active(form)
|
||||
app_model, site = _get_app_site_from_form(form)
|
||||
tenant = app_model.tenant
|
||||
if tenant is None:
|
||||
raise Forbidden()
|
||||
inputs = service.resolve_form_inputs(form)
|
||||
features = FeatureService.get_features(app_model.tenant_id, exclude_vector_space=True)
|
||||
|
||||
return _jsonify_form_definition(
|
||||
form,
|
||||
inputs=inputs,
|
||||
site_payload=serialize_app_site_payload(app_model, site, None),
|
||||
return dump_response(
|
||||
HumanInputFormDefinitionResponse,
|
||||
HumanInputFormDefinitionResponse.from_form(
|
||||
form,
|
||||
inputs=inputs,
|
||||
site=WebAppSiteResponse.from_app_site(
|
||||
tenant=tenant,
|
||||
app_model=app_model,
|
||||
site=site,
|
||||
end_user_id=None,
|
||||
features=features,
|
||||
can_replace_logo=features.can_replace_logo,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# def post(self, _app_model: App, _end_user: EndUser, form_token: str):
|
||||
@web_ns.response(200, "Success", web_ns.models[HumanInputFormSubmitResponse.__name__])
|
||||
@web_ns.expect(web_ns.models[HumanInputFormSubmitPayload.__name__])
|
||||
@web_ns.doc("submit_human_input_form")
|
||||
@web_ns.doc(description="Submit a human input form by token")
|
||||
@web_ns.doc(params={"form_token": "Human input form token"})
|
||||
@web_ns.doc(
|
||||
responses={
|
||||
200: "Form submitted successfully",
|
||||
400: "Bad request - invalid submission data",
|
||||
404: "Form not found",
|
||||
412: "Form already submitted or expired",
|
||||
429: "Too many requests",
|
||||
}
|
||||
)
|
||||
@web_ns.response(
|
||||
200,
|
||||
"Form submitted successfully",
|
||||
web_ns.models[HumanInputFormSubmitResponse.__name__],
|
||||
)
|
||||
def post(self, form_token: str):
|
||||
"""
|
||||
Submit human input form by token.
|
||||
@ -225,7 +276,7 @@ class HumanInputFormApi(Resource):
|
||||
except FormNotFoundError:
|
||||
raise NotFoundError("Form not found")
|
||||
|
||||
return {}, 200
|
||||
return HumanInputFormSubmitResponse().model_dump(mode="json"), 200
|
||||
|
||||
|
||||
def _get_app_site_from_form(form: Form) -> tuple[App, Site]:
|
||||
@ -238,7 +289,7 @@ def _get_app_site_from_form(form: Form) -> tuple[App, Site]:
|
||||
if site is None:
|
||||
raise Forbidden()
|
||||
|
||||
if app_model.tenant and app_model.tenant.status == TenantStatus.ARCHIVE:
|
||||
if app_model.tenant is None or app_model.tenant.status == TenantStatus.ARCHIVE:
|
||||
raise Forbidden()
|
||||
|
||||
return app_model, site
|
||||
|
||||
@ -188,6 +188,7 @@ class MessageMoreLikeThisApi(WebApiResource):
|
||||
streaming=streaming,
|
||||
)
|
||||
|
||||
# response-contract:ignore compact_generate_response
|
||||
return helper.compact_generate_response(response)
|
||||
except MessageNotExistsError:
|
||||
raise NotFound("Message Not Exists.")
|
||||
|
||||
@ -65,11 +65,10 @@ class RemoteFileInfoApi(WebApiResource):
|
||||
# failed back to get method
|
||||
resp = remote_fetcher.make_request("GET", decoded_url, timeout=3)
|
||||
resp.raise_for_status()
|
||||
info = RemoteFileInfo(
|
||||
return RemoteFileInfo(
|
||||
file_type=resp.headers.get("Content-Type", "application/octet-stream"),
|
||||
file_length=int(resp.headers.get("Content-Length", -1)),
|
||||
)
|
||||
return info.model_dump(mode="json")
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@web_ns.route("/remote-files/upload")
|
||||
@ -141,7 +140,7 @@ class RemoteFileUploadApi(WebApiResource):
|
||||
except services.errors.file.UnsupportedFileTypeError:
|
||||
raise UnsupportedFileTypeError
|
||||
|
||||
payload1 = FileWithSignedUrl(
|
||||
return FileWithSignedUrl(
|
||||
id=upload_file.id,
|
||||
name=upload_file.name,
|
||||
size=upload_file.size,
|
||||
@ -150,5 +149,4 @@ class RemoteFileUploadApi(WebApiResource):
|
||||
mime_type=upload_file.mime_type,
|
||||
created_by=upload_file.created_by,
|
||||
created_at=int(upload_file.created_at.timestamp()),
|
||||
)
|
||||
return payload1.model_dump(mode="json"), 201
|
||||
).model_dump(mode="json"), 201
|
||||
|
||||
@ -49,9 +49,7 @@ class SavedMessageListApi(WebApiResource):
|
||||
adapter = TypeAdapter(SavedMessageItem)
|
||||
items = [adapter.validate_python(message, from_attributes=True) for message in pagination.data]
|
||||
return SavedMessageInfiniteScrollPagination(
|
||||
limit=pagination.limit,
|
||||
has_more=pagination.has_more,
|
||||
data=items,
|
||||
limit=pagination.limit, has_more=pagination.has_more, data=items
|
||||
).model_dump(mode="json")
|
||||
|
||||
@web_ns.doc("Save Message")
|
||||
@ -102,6 +100,7 @@ class SavedMessageApi(WebApiResource):
|
||||
500: "Internal Server Error",
|
||||
}
|
||||
)
|
||||
@web_ns.response(204, "Message removed successfully")
|
||||
def delete(self, app_model: App, end_user: EndUser, message_id: UUID):
|
||||
message_id_str = str(message_id)
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from typing import Any, cast
|
||||
from typing import Any, Self
|
||||
|
||||
from flask_restx import fields, marshal, marshal_with
|
||||
from pydantic import Field
|
||||
from pydantic import AliasChoices, Field, computed_field
|
||||
from sqlalchemy import select
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
@ -11,30 +10,19 @@ from controllers.web import web_ns
|
||||
from controllers.web.wraps import WebApiResource
|
||||
from extensions.ext_database import db
|
||||
from fields.base import ResponseModel
|
||||
from libs.helper import AppIconUrlField
|
||||
from models.account import TenantStatus
|
||||
from libs.helper import build_icon_url
|
||||
from models.account import Tenant, TenantStatus
|
||||
from models.model import App, EndUser, Site
|
||||
from services.feature_service import FeatureModel, FeatureService
|
||||
|
||||
|
||||
class AppSiteModelConfigResponse(ResponseModel):
|
||||
opening_statement: str | None = None
|
||||
suggested_questions: Any
|
||||
suggested_questions_after_answer: Any
|
||||
more_like_this: Any
|
||||
model: Any
|
||||
user_input_form: Any
|
||||
pre_prompt: str | None = None
|
||||
|
||||
|
||||
class AppSiteResponse(ResponseModel):
|
||||
title: str | None = None
|
||||
class WebSiteResponse(ResponseModel):
|
||||
title: str
|
||||
chat_color_theme: str | None = None
|
||||
chat_color_theme_inverted: bool | None = None
|
||||
chat_color_theme_inverted: bool
|
||||
icon_type: str | None = None
|
||||
icon: str | None = None
|
||||
icon_background: str | None = None
|
||||
icon_url: str | None = None
|
||||
description: str | None = None
|
||||
copyright: str | None = None
|
||||
privacy_policy: str | None = None
|
||||
@ -45,65 +33,98 @@ class AppSiteResponse(ResponseModel):
|
||||
show_workflow_steps: bool | None = None
|
||||
use_icon_as_answer_icon: bool | None = None
|
||||
|
||||
@computed_field(return_type=str | None) # type: ignore[prop-decorator]
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return build_icon_url(self.icon_type, self.icon)
|
||||
|
||||
class AppSiteInfoResponse(ResponseModel):
|
||||
|
||||
class WebModelConfigResponse(ResponseModel):
|
||||
opening_statement: str | None = None
|
||||
suggested_questions: Any = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("suggested_questions_list", "suggested_questions"),
|
||||
)
|
||||
suggested_questions_after_answer: Any = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("suggested_questions_after_answer_dict", "suggested_questions_after_answer"),
|
||||
)
|
||||
more_like_this: Any = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("more_like_this_dict", "more_like_this"),
|
||||
)
|
||||
model: Any = Field(default=None, validation_alias=AliasChoices("model_dict", "model"))
|
||||
user_input_form: Any = Field(
|
||||
default=None,
|
||||
validation_alias=AliasChoices("user_input_form_list", "user_input_form"),
|
||||
)
|
||||
pre_prompt: str | None = None
|
||||
|
||||
|
||||
class WebAppCustomConfigResponse(ResponseModel):
|
||||
remove_webapp_brand: bool
|
||||
replace_webapp_logo: str | None = None
|
||||
|
||||
|
||||
class WebAppSiteResponse(ResponseModel):
|
||||
app_id: str
|
||||
end_user_id: str | None = None
|
||||
enable_site: bool
|
||||
site: AppSiteResponse
|
||||
model_config_: AppSiteModelConfigResponse | None = Field(default=None, alias="model_config")
|
||||
plan: str | None = None
|
||||
site: WebSiteResponse
|
||||
model_config_: WebModelConfigResponse | None = Field(
|
||||
default=None, validation_alias="model_config", serialization_alias="model_config"
|
||||
)
|
||||
plan: str
|
||||
can_replace_logo: bool
|
||||
custom_config: dict[str, Any] | None = Field(default=None)
|
||||
custom_config: WebAppCustomConfigResponse | None = None
|
||||
|
||||
@classmethod
|
||||
def from_app_site(
|
||||
cls,
|
||||
*,
|
||||
tenant: Tenant,
|
||||
app_model: App,
|
||||
site: Site,
|
||||
end_user_id: str | None,
|
||||
features: FeatureModel,
|
||||
can_replace_logo: bool,
|
||||
) -> Self:
|
||||
custom_config = None
|
||||
if can_replace_logo:
|
||||
replace_webapp_logo = (
|
||||
f"{dify_config.FILES_URL}/files/workspaces/{tenant.id}/webapp-logo"
|
||||
if tenant.custom_config_dict.get("replace_webapp_logo")
|
||||
else None
|
||||
)
|
||||
custom_config = WebAppCustomConfigResponse(
|
||||
remove_webapp_brand=tenant.custom_config_dict.get("remove_webapp_brand", False),
|
||||
replace_webapp_logo=replace_webapp_logo,
|
||||
)
|
||||
|
||||
site_response = WebSiteResponse.model_validate(site, from_attributes=True)
|
||||
if features.billing.enabled and not features.webapp_copyright_enabled:
|
||||
site_response.copyright = None
|
||||
site_response.input_placeholder = None
|
||||
|
||||
return cls(
|
||||
app_id=app_model.id,
|
||||
end_user_id=end_user_id,
|
||||
enable_site=app_model.enable_site,
|
||||
site=site_response,
|
||||
model_config_=None,
|
||||
plan=tenant.plan,
|
||||
can_replace_logo=can_replace_logo,
|
||||
custom_config=custom_config,
|
||||
)
|
||||
|
||||
|
||||
register_response_schema_models(web_ns, AppSiteInfoResponse)
|
||||
register_response_schema_models(
|
||||
web_ns, WebSiteResponse, WebModelConfigResponse, WebAppCustomConfigResponse, WebAppSiteResponse
|
||||
)
|
||||
|
||||
|
||||
@web_ns.route("/site")
|
||||
class AppSiteApi(WebApiResource):
|
||||
"""Resource for app sites."""
|
||||
|
||||
model_config_fields = {
|
||||
"opening_statement": fields.String,
|
||||
"suggested_questions": fields.Raw(attribute="suggested_questions_list"),
|
||||
"suggested_questions_after_answer": fields.Raw(attribute="suggested_questions_after_answer_dict"),
|
||||
"more_like_this": fields.Raw(attribute="more_like_this_dict"),
|
||||
"model": fields.Raw(attribute="model_dict"),
|
||||
"user_input_form": fields.Raw(attribute="user_input_form_list"),
|
||||
"pre_prompt": fields.String,
|
||||
}
|
||||
|
||||
site_fields = {
|
||||
"title": fields.String,
|
||||
"chat_color_theme": fields.String,
|
||||
"chat_color_theme_inverted": fields.Boolean,
|
||||
"icon_type": fields.String,
|
||||
"icon": fields.String,
|
||||
"icon_background": fields.String,
|
||||
"icon_url": AppIconUrlField,
|
||||
"description": fields.String,
|
||||
"copyright": fields.String,
|
||||
"privacy_policy": fields.String,
|
||||
"input_placeholder": fields.String,
|
||||
"custom_disclaimer": fields.String,
|
||||
"default_language": fields.String,
|
||||
"prompt_public": fields.Boolean,
|
||||
"show_workflow_steps": fields.Boolean,
|
||||
"use_icon_as_answer_icon": fields.Boolean,
|
||||
}
|
||||
|
||||
app_fields = {
|
||||
"app_id": fields.String,
|
||||
"end_user_id": fields.String,
|
||||
"enable_site": fields.Boolean,
|
||||
"site": fields.Nested(site_fields),
|
||||
"model_config": fields.Nested(model_config_fields, allow_null=True),
|
||||
"plan": fields.String,
|
||||
"can_replace_logo": fields.Boolean,
|
||||
"custom_config": fields.Raw(attribute="custom_config"),
|
||||
}
|
||||
|
||||
@web_ns.doc("Get App Site Info")
|
||||
@web_ns.doc(description="Retrieve app site information and configuration.")
|
||||
@web_ns.doc(
|
||||
@ -116,79 +137,26 @@ class AppSiteApi(WebApiResource):
|
||||
500: "Internal Server Error",
|
||||
}
|
||||
)
|
||||
@web_ns.response(200, "Success", web_ns.models[AppSiteInfoResponse.__name__])
|
||||
@marshal_with(app_fields)
|
||||
@web_ns.response(200, "Success", web_ns.models[WebAppSiteResponse.__name__])
|
||||
def get(self, app_model: App, end_user: EndUser):
|
||||
"""Retrieve app site info."""
|
||||
# get site
|
||||
site = db.session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1))
|
||||
|
||||
if not site:
|
||||
if site is None:
|
||||
raise Forbidden()
|
||||
|
||||
if app_model.tenant and app_model.tenant.status == TenantStatus.ARCHIVE:
|
||||
tenant = app_model.tenant
|
||||
if tenant is None or tenant.status == TenantStatus.ARCHIVE:
|
||||
raise Forbidden()
|
||||
|
||||
features = FeatureService.get_features(app_model.tenant_id, exclude_vector_space=True)
|
||||
|
||||
return AppSiteInfo(
|
||||
app_model.tenant,
|
||||
app_model,
|
||||
serialize_runtime_site(site, features),
|
||||
end_user.id,
|
||||
features.can_replace_logo,
|
||||
)
|
||||
|
||||
|
||||
class AppSiteInfo:
|
||||
"""Class to store site information."""
|
||||
|
||||
def __init__(self, tenant, app, site, end_user, can_replace_logo):
|
||||
"""Initialize AppSiteInfo instance."""
|
||||
self.app_id = app.id
|
||||
self.end_user_id = end_user
|
||||
self.enable_site = app.enable_site
|
||||
self.site = site
|
||||
self.model_config = None
|
||||
self.plan = tenant.plan
|
||||
self.can_replace_logo = can_replace_logo
|
||||
|
||||
if can_replace_logo:
|
||||
base_url = dify_config.FILES_URL
|
||||
remove_webapp_brand = tenant.custom_config_dict.get("remove_webapp_brand", False)
|
||||
replace_webapp_logo = (
|
||||
f"{base_url}/files/workspaces/{tenant.id}/webapp-logo"
|
||||
if tenant.custom_config_dict.get("replace_webapp_logo")
|
||||
else None
|
||||
)
|
||||
self.custom_config = {
|
||||
"remove_webapp_brand": remove_webapp_brand,
|
||||
"replace_webapp_logo": replace_webapp_logo,
|
||||
}
|
||||
|
||||
|
||||
def serialize_site(site: Site) -> dict[str, Any]:
|
||||
"""Serialize Site model using the same schema as AppSiteApi."""
|
||||
return cast(dict[str, Any], marshal(site, AppSiteApi.site_fields))
|
||||
|
||||
|
||||
def serialize_runtime_site(site: Site, features: FeatureModel) -> dict[str, Any]:
|
||||
site_payload = serialize_site(site)
|
||||
if not features.billing.enabled or features.webapp_copyright_enabled:
|
||||
return site_payload
|
||||
|
||||
site_payload["copyright"] = None
|
||||
site_payload["input_placeholder"] = None
|
||||
return site_payload
|
||||
|
||||
|
||||
def serialize_app_site_payload(app_model: App, site: Site, end_user_id: str | None) -> dict[str, Any]:
|
||||
features = FeatureService.get_features(app_model.tenant_id, exclude_vector_space=True)
|
||||
app_site_info = AppSiteInfo(
|
||||
app_model.tenant,
|
||||
app_model,
|
||||
serialize_runtime_site(site, features),
|
||||
end_user_id,
|
||||
features.can_replace_logo,
|
||||
)
|
||||
return cast(dict[str, Any], marshal(app_site_info, AppSiteApi.app_fields))
|
||||
return WebAppSiteResponse.from_app_site(
|
||||
tenant=tenant,
|
||||
app_model=app_model,
|
||||
site=site,
|
||||
end_user_id=end_user.id,
|
||||
features=features,
|
||||
can_replace_logo=features.can_replace_logo,
|
||||
).model_dump(mode="json")
|
||||
|
||||
@ -76,6 +76,7 @@ class WorkflowRunApi(WebApiResource):
|
||||
streaming=True,
|
||||
)
|
||||
|
||||
# response-contract:ignore compact_generate_response
|
||||
return helper.compact_generate_response(response)
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ProviderNotInitializeError(ex.description)
|
||||
@ -129,4 +130,4 @@ class WorkflowTaskStopApi(WebApiResource):
|
||||
# New graph engine command channel mechanism
|
||||
GraphEngineManager(redis_client).send_stop_command(task_id)
|
||||
|
||||
return {"result": "success"}
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
@ -11,14 +11,14 @@ from libs.helper import to_timestamp
|
||||
class UploadConfig(ResponseModel):
|
||||
file_size_limit: int
|
||||
batch_count_limit: int
|
||||
file_upload_limit: int | None = None
|
||||
file_upload_limit: int
|
||||
image_file_size_limit: int
|
||||
video_file_size_limit: int
|
||||
audio_file_size_limit: int
|
||||
workflow_file_upload_limit: int
|
||||
image_file_batch_limit: int
|
||||
single_chunk_attachment_limit: int
|
||||
attachment_image_file_size_limit: int | None = None
|
||||
attachment_image_file_size_limit: int
|
||||
|
||||
|
||||
class FileResponse(ResponseModel):
|
||||
|
||||
@ -22489,11 +22489,11 @@ Payload for updating a snippet.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| attachment_image_file_size_limit | integer | | No |
|
||||
| attachment_image_file_size_limit | integer | | Yes |
|
||||
| audio_file_size_limit | integer | | Yes |
|
||||
| batch_count_limit | integer | | Yes |
|
||||
| file_size_limit | integer | | Yes |
|
||||
| file_upload_limit | integer | | No |
|
||||
| file_upload_limit | integer | | Yes |
|
||||
| image_file_batch_limit | integer | | Yes |
|
||||
| image_file_size_limit | integer | | Yes |
|
||||
| single_chunk_attachment_limit | integer | | Yes |
|
||||
|
||||
@ -21,7 +21,7 @@ Convert audio file to text using speech-to-text service.
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [AudioTranscriptResponse](#audiotranscriptresponse)<br> |
|
||||
| 200 | Success | **application/json**: [AudioToTextResponse](#audiototextresponse)<br> |
|
||||
| 400 | Bad Request | |
|
||||
| 401 | Unauthorized | |
|
||||
| 403 | Forbidden | |
|
||||
@ -346,23 +346,29 @@ Verify password reset token validity
|
||||
### [GET] /form/human_input/{form_token}
|
||||
**Get human input form definition by token**
|
||||
|
||||
Get a human input form definition by token
|
||||
GET /api/form/human_input/<form_token>
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| form_token | path | | Yes | string |
|
||||
| form_token | path | Human input form token | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [HumanInputFormDefinitionResponse](#humaninputformdefinitionresponse)<br> |
|
||||
| 200 | Form retrieved successfully | **application/json**: [HumanInputFormDefinitionResponse](#humaninputformdefinitionresponse)<br> |
|
||||
| 403 | Forbidden | |
|
||||
| 404 | Form not found | |
|
||||
| 412 | Form already submitted or expired | |
|
||||
| 429 | Too many requests | |
|
||||
|
||||
### [POST] /form/human_input/{form_token}
|
||||
**Submit human input form by token**
|
||||
|
||||
Submit a human input form by token
|
||||
POST /api/form/human_input/<form_token>
|
||||
|
||||
Request body:
|
||||
@ -377,7 +383,7 @@ Request body:
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| form_token | path | | Yes | string |
|
||||
| form_token | path | Human input form token | Yes | string |
|
||||
|
||||
#### Request Body
|
||||
|
||||
@ -389,24 +395,32 @@ Request body:
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [HumanInputFormSubmitResponse](#humaninputformsubmitresponse)<br> |
|
||||
| 200 | Form submitted successfully | **application/json**: [HumanInputFormSubmitResponse](#humaninputformsubmitresponse)<br> |
|
||||
| 400 | Bad request - invalid submission data | |
|
||||
| 404 | Form not found | |
|
||||
| 412 | Form already submitted or expired | |
|
||||
| 429 | Too many requests | |
|
||||
|
||||
### [POST] /form/human_input/{form_token}/upload-token
|
||||
**Issue an upload token for a human input form**
|
||||
|
||||
Issue an upload token for an active human input form
|
||||
POST /api/form/human_input/<form_token>/upload-token
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| form_token | path | | Yes | string |
|
||||
| form_token | path | Human input form token | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [HumanInputUploadTokenResponse](#humaninputuploadtokenresponse)<br> |
|
||||
| 200 | Upload token issued successfully | **application/json**: [HumanInputUploadTokenResponse](#humaninputuploadtokenresponse)<br> |
|
||||
| 404 | Form not found | |
|
||||
| 412 | Form already submitted or expired | |
|
||||
| 429 | Too many requests | |
|
||||
|
||||
### [POST] /human-input-forms/files
|
||||
**Upload one local file or remote URL file for a HITL human input form**
|
||||
@ -752,7 +766,7 @@ Retrieve app site information and configuration.
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [AppSiteInfoResponse](#appsiteinforesponse)<br> |
|
||||
| 200 | Success | **application/json**: [WebAppSiteResponse](#webappsiteresponse)<br> |
|
||||
| 400 | Bad Request | |
|
||||
| 401 | Unauthorized | |
|
||||
| 403 | Forbidden | |
|
||||
@ -799,13 +813,13 @@ Convert text to audio using text-to-speech service.
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [AudioBinaryResponse](#audiobinaryresponse)<br> |
|
||||
| 400 | Bad Request | |
|
||||
| 401 | Unauthorized | |
|
||||
| 403 | Forbidden | |
|
||||
| 500 | Internal Server Error | |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 200 | Success |
|
||||
| 400 | Bad Request |
|
||||
| 401 | Unauthorized |
|
||||
| 403 | Forbidden |
|
||||
| 500 | Internal Server Error |
|
||||
|
||||
### [GET] /webapp/access-mode
|
||||
Retrieve the access mode for a web application (public or restricted).
|
||||
@ -968,59 +982,7 @@ Returns Server-Sent Events stream.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| appId | string | Application ID | Yes |
|
||||
|
||||
#### AppSiteInfoResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| app_id | string | | Yes |
|
||||
| can_replace_logo | boolean | | Yes |
|
||||
| custom_config | object | | No |
|
||||
| enable_site | boolean | | Yes |
|
||||
| end_user_id | string | | No |
|
||||
| model_config | [AppSiteModelConfigResponse](#appsitemodelconfigresponse) | | No |
|
||||
| plan | string | | No |
|
||||
| site | [AppSiteResponse](#appsiteresponse) | | Yes |
|
||||
|
||||
#### AppSiteModelConfigResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| model | | | Yes |
|
||||
| more_like_this | | | Yes |
|
||||
| opening_statement | string | | No |
|
||||
| pre_prompt | string | | No |
|
||||
| suggested_questions | | | Yes |
|
||||
| suggested_questions_after_answer | | | Yes |
|
||||
| user_input_form | | | Yes |
|
||||
|
||||
#### AppSiteResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| chat_color_theme | string | | No |
|
||||
| chat_color_theme_inverted | boolean | | No |
|
||||
| copyright | string | | No |
|
||||
| custom_disclaimer | string | | No |
|
||||
| default_language | string | | No |
|
||||
| description | string | | No |
|
||||
| icon | string | | No |
|
||||
| icon_background | string | | No |
|
||||
| icon_type | string | | No |
|
||||
| icon_url | string | | No |
|
||||
| input_placeholder | string | | No |
|
||||
| privacy_policy | string | | No |
|
||||
| prompt_public | boolean | | No |
|
||||
| show_workflow_steps | boolean | | No |
|
||||
| title | string | | No |
|
||||
| use_icon_as_answer_icon | boolean | | No |
|
||||
|
||||
#### AudioBinaryResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| AudioBinaryResponse | string | | |
|
||||
|
||||
#### AudioTranscriptResponse
|
||||
#### AudioToTextResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
@ -1262,11 +1224,11 @@ Parsed multipart form fields for HITL uploads.
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| expiration_time | integer | | Yes |
|
||||
| form_content | | | Yes |
|
||||
| inputs | | | Yes |
|
||||
| form_content | string | | Yes |
|
||||
| inputs | [ [FormInputConfig](#forminputconfig) ] | | Yes |
|
||||
| resolved_default_values | object | | Yes |
|
||||
| site | object | | No |
|
||||
| user_actions | | | Yes |
|
||||
| site | [WebAppSiteResponse](#webappsiteresponse) | | No |
|
||||
| user_actions | [ [UserActionConfig](#useractionconfig) ] | | Yes |
|
||||
|
||||
#### HumanInputFormSubmissionData
|
||||
|
||||
@ -1689,6 +1651,26 @@ in form definiton, or a variable while the workflow is running.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| protocol | string | | Yes |
|
||||
|
||||
#### WebAppCustomConfigResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| remove_webapp_brand | boolean | | Yes |
|
||||
| replace_webapp_logo | string | | No |
|
||||
|
||||
#### WebAppSiteResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| app_id | string | | Yes |
|
||||
| can_replace_logo | boolean | | Yes |
|
||||
| custom_config | [WebAppCustomConfigResponse](#webappcustomconfigresponse) | | No |
|
||||
| enable_site | boolean | | Yes |
|
||||
| end_user_id | string | | No |
|
||||
| model_config | [WebModelConfigResponse](#webmodelconfigresponse) | | No |
|
||||
| plan | string | | Yes |
|
||||
| site | [WebSiteResponse](#websiteresponse) | | Yes |
|
||||
|
||||
#### WebMessageInfiniteScrollPagination
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@ -1717,6 +1699,39 @@ in form definiton, or a variable while the workflow is running.
|
||||
| retriever_resources | [ [RetrieverResource](#retrieverresource) ] | | Yes |
|
||||
| status | string | | Yes |
|
||||
|
||||
#### WebModelConfigResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| model | | | No |
|
||||
| more_like_this | | | No |
|
||||
| opening_statement | string | | No |
|
||||
| pre_prompt | string | | No |
|
||||
| suggested_questions | | | No |
|
||||
| suggested_questions_after_answer | | | No |
|
||||
| user_input_form | | | No |
|
||||
|
||||
#### WebSiteResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| chat_color_theme | string | | No |
|
||||
| chat_color_theme_inverted | boolean | | Yes |
|
||||
| copyright | string | | No |
|
||||
| custom_disclaimer | string | | No |
|
||||
| default_language | string | | No |
|
||||
| description | string | | No |
|
||||
| icon | string | | No |
|
||||
| icon_background | string | | No |
|
||||
| icon_type | string | | No |
|
||||
| icon_url | string | | Yes |
|
||||
| input_placeholder | string | | No |
|
||||
| privacy_policy | string | | No |
|
||||
| prompt_public | boolean | | No |
|
||||
| show_workflow_steps | boolean | | No |
|
||||
| title | string | | Yes |
|
||||
| use_icon_as_answer_icon | boolean | | No |
|
||||
|
||||
#### WorkflowRunPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|
||||
@ -173,7 +173,7 @@ class FileService:
|
||||
|
||||
return upload_file
|
||||
|
||||
def get_file_preview(self, file_id: str, tenant_id: str):
|
||||
def get_file_preview(self, file_id: str, tenant_id: str) -> str:
|
||||
"""
|
||||
Return a short text preview extracted from a document file.
|
||||
"""
|
||||
@ -191,9 +191,7 @@ class FileService:
|
||||
raise UnsupportedFileTypeError()
|
||||
|
||||
text = ExtractProcessor.load_from_upload_file(upload_file, return_text=True)
|
||||
text = text[0:PREVIEW_WORDS_LIMIT] if text else ""
|
||||
|
||||
return text
|
||||
return text[0:PREVIEW_WORDS_LIMIT] if text else ""
|
||||
|
||||
def get_image_preview(self, file_id: str, timestamp: str, nonce: str, sign: str):
|
||||
result = file_helpers.verify_image_signature(
|
||||
|
||||
@ -2,22 +2,22 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.web.site import AppSiteApi, AppSiteInfo
|
||||
from controllers.web.site import AppSiteApi, WebAppSiteResponse, WebModelConfigResponse
|
||||
from models import Tenant, TenantStatus
|
||||
from models.model import App, AppMode, CustomizeTokenStrategy, Site
|
||||
from models.account import TenantCustomConfigDict
|
||||
from models.model import App, AppMode, AppModelConfig, CustomizeTokenStrategy, EndUser, Site
|
||||
from services.feature_service import FeatureModel
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app(flask_app_with_containers) -> Flask:
|
||||
def app(flask_app_with_containers: Flask) -> Flask:
|
||||
return flask_app_with_containers
|
||||
|
||||
|
||||
@ -42,95 +42,201 @@ def _create_app(db_session: Session, tenant_id: str, *, enable_site: bool = True
|
||||
|
||||
|
||||
def _create_site(db_session: Session, app_id: str) -> Site:
|
||||
site = Site(
|
||||
site = _site_model(app_id=app_id)
|
||||
db_session.add(site)
|
||||
db_session.commit()
|
||||
return site
|
||||
|
||||
|
||||
def _end_user(tenant_id: str, app_id: str) -> EndUser:
|
||||
return EndUser(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_id,
|
||||
type="browser",
|
||||
session_id=f"session-{app_id}",
|
||||
)
|
||||
|
||||
|
||||
def _site_model(*, app_id: str) -> Site:
|
||||
return Site(
|
||||
app_id=app_id,
|
||||
title="Site",
|
||||
icon_type="emoji",
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
description="desc",
|
||||
input_placeholder="Ask the app",
|
||||
default_language="en",
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=False,
|
||||
input_placeholder="Ask the app",
|
||||
custom_disclaimer="",
|
||||
customize_token_strategy=CustomizeTokenStrategy.NOT_ALLOW,
|
||||
code=f"code-{app_id[-6:]}",
|
||||
prompt_public=False,
|
||||
show_workflow_steps=True,
|
||||
use_icon_as_answer_icon=False,
|
||||
)
|
||||
db_session.add(site)
|
||||
db_session.commit()
|
||||
return site
|
||||
|
||||
|
||||
class TestAppSiteApi:
|
||||
@patch("controllers.web.site.FeatureService.get_features")
|
||||
def test_happy_path(self, mock_features, app: Flask, db_session_with_containers: Session) -> None:
|
||||
def test_happy_path(self, mock_features: MagicMock, app: Flask, db_session_with_containers: Session) -> None:
|
||||
app.config["RESTX_MASK_HEADER"] = "X-Fields"
|
||||
tenant = _create_tenant(db_session_with_containers)
|
||||
app_model = _create_app(db_session_with_containers, tenant.id)
|
||||
_create_site(db_session_with_containers, app_model.id)
|
||||
end_user = SimpleNamespace(id="eu-1")
|
||||
mock_features.return_value = FeatureModel(can_replace_logo=False, webapp_copyright_enabled=True)
|
||||
end_user = _end_user(tenant.id, app_model.id)
|
||||
mock_features.return_value = FeatureModel(can_replace_logo=False)
|
||||
|
||||
with app.test_request_context("/site"):
|
||||
result = AppSiteApi().get(app_model, end_user)
|
||||
|
||||
assert result["app_id"] == app_model.id
|
||||
assert result["end_user_id"] == end_user.id
|
||||
assert result["plan"] == "basic"
|
||||
assert result["enable_site"] is True
|
||||
assert result["site"]["input_placeholder"] == "Ask the app"
|
||||
|
||||
def test_missing_site_raises_forbidden(self, app: Flask, db_session_with_containers: Session) -> None:
|
||||
app.config["RESTX_MASK_HEADER"] = "X-Fields"
|
||||
tenant = _create_tenant(db_session_with_containers)
|
||||
app_model = _create_app(db_session_with_containers, tenant.id)
|
||||
end_user = SimpleNamespace(id="eu-1")
|
||||
end_user = _end_user(tenant.id, app_model.id)
|
||||
|
||||
with app.test_request_context("/site"):
|
||||
with pytest.raises(Forbidden):
|
||||
AppSiteApi().get(app_model, end_user)
|
||||
|
||||
@patch("controllers.web.site.FeatureService.get_features")
|
||||
def test_archived_tenant_raises_forbidden(
|
||||
self, mock_features, app: Flask, db_session_with_containers: Session
|
||||
) -> None:
|
||||
def test_archived_tenant_raises_forbidden(self, app: Flask, db_session_with_containers: Session) -> None:
|
||||
app.config["RESTX_MASK_HEADER"] = "X-Fields"
|
||||
tenant = _create_tenant(db_session_with_containers, status=TenantStatus.ARCHIVE)
|
||||
app_model = _create_app(db_session_with_containers, tenant.id)
|
||||
_create_site(db_session_with_containers, app_model.id)
|
||||
end_user = SimpleNamespace(id="eu-1")
|
||||
mock_features.return_value = FeatureModel(can_replace_logo=False, webapp_copyright_enabled=True)
|
||||
end_user = _end_user(tenant.id, app_model.id)
|
||||
|
||||
with app.test_request_context("/site"):
|
||||
with pytest.raises(Forbidden):
|
||||
AppSiteApi().get(app_model, end_user)
|
||||
|
||||
|
||||
class TestAppSiteInfo:
|
||||
def _tenant_model(*, plan: str = "basic", custom_config: TenantCustomConfigDict | None = None) -> Tenant:
|
||||
tenant = Tenant(name="test-tenant", plan=plan)
|
||||
tenant.custom_config_dict = custom_config or {}
|
||||
return tenant
|
||||
|
||||
|
||||
def _app_model(*, tenant: Tenant, enable_site: bool = True) -> App:
|
||||
app_model = App(
|
||||
tenant_id=tenant.id,
|
||||
mode=AppMode.CHAT,
|
||||
name="test-app",
|
||||
enable_site=enable_site,
|
||||
enable_api=True,
|
||||
)
|
||||
app_model.id = "app-test"
|
||||
return app_model
|
||||
|
||||
|
||||
class TestWebAppSiteResponse:
|
||||
def test_basic_fields(self) -> None:
|
||||
tenant = SimpleNamespace(id="tenant-1", plan="basic", custom_config_dict={})
|
||||
site_obj = SimpleNamespace()
|
||||
info = AppSiteInfo(tenant, SimpleNamespace(id="app-1", enable_site=True), site_obj, "eu-1", False)
|
||||
|
||||
assert info.app_id == "app-1"
|
||||
assert info.end_user_id == "eu-1"
|
||||
assert info.enable_site is True
|
||||
assert info.plan == "basic"
|
||||
assert info.can_replace_logo is False
|
||||
assert info.model_config is None
|
||||
|
||||
@patch("controllers.web.site.dify_config", SimpleNamespace(FILES_URL="https://files.example.com"))
|
||||
def test_can_replace_logo_sets_custom_config(self) -> None:
|
||||
tenant = SimpleNamespace(
|
||||
id="tenant-1",
|
||||
plan="pro",
|
||||
custom_config_dict={"remove_webapp_brand": True, "replace_webapp_logo": True},
|
||||
tenant = _tenant_model()
|
||||
app_model = _app_model(tenant=tenant)
|
||||
response = WebAppSiteResponse.from_app_site(
|
||||
tenant=tenant,
|
||||
app_model=app_model,
|
||||
site=_site_model(app_id=app_model.id),
|
||||
end_user_id="eu-1",
|
||||
features=FeatureModel(can_replace_logo=False, webapp_copyright_enabled=True),
|
||||
can_replace_logo=False,
|
||||
)
|
||||
site_obj = SimpleNamespace()
|
||||
info = AppSiteInfo(tenant, SimpleNamespace(id="app-1", enable_site=True), site_obj, "eu-1", True)
|
||||
|
||||
assert info.can_replace_logo is True
|
||||
assert info.custom_config["remove_webapp_brand"] is True
|
||||
assert "webapp-logo" in info.custom_config["replace_webapp_logo"]
|
||||
assert response.app_id == app_model.id
|
||||
assert response.end_user_id == "eu-1"
|
||||
assert response.enable_site is True
|
||||
assert response.plan == "basic"
|
||||
assert response.can_replace_logo is False
|
||||
assert response.model_config_ is None
|
||||
assert response.custom_config is None
|
||||
assert response.site.input_placeholder == "Ask the app"
|
||||
assert response.site.custom_disclaimer == ""
|
||||
|
||||
def test_nullable_site_fields_preserve_none(self) -> None:
|
||||
tenant = _tenant_model()
|
||||
app_model = _app_model(tenant=tenant)
|
||||
site = _site_model(app_id=app_model.id)
|
||||
site.chat_color_theme = None
|
||||
site.icon_type = None
|
||||
site.icon = None
|
||||
site.icon_background = None
|
||||
site.description = None
|
||||
site.copyright = None
|
||||
site.privacy_policy = None
|
||||
|
||||
response = WebAppSiteResponse.from_app_site(
|
||||
tenant=tenant,
|
||||
app_model=app_model,
|
||||
site=site,
|
||||
end_user_id=None,
|
||||
features=FeatureModel(can_replace_logo=False, webapp_copyright_enabled=True),
|
||||
can_replace_logo=False,
|
||||
)
|
||||
|
||||
dumped = response.model_dump(mode="json")
|
||||
assert dumped["end_user_id"] is None
|
||||
assert dumped["site"]["chat_color_theme"] is None
|
||||
assert dumped["site"]["icon_type"] is None
|
||||
assert dumped["site"]["icon"] is None
|
||||
assert dumped["site"]["icon_background"] is None
|
||||
assert dumped["site"]["description"] is None
|
||||
assert dumped["site"]["copyright"] is None
|
||||
assert dumped["site"]["privacy_policy"] is None
|
||||
assert dumped["site"]["custom_disclaimer"] == ""
|
||||
|
||||
@patch("controllers.web.site.dify_config.FILES_URL", "https://files.example.com")
|
||||
def test_can_replace_logo_sets_custom_config(self) -> None:
|
||||
tenant = _tenant_model(
|
||||
plan="pro",
|
||||
custom_config={"remove_webapp_brand": True, "replace_webapp_logo": "enabled"},
|
||||
)
|
||||
app_model = _app_model(tenant=tenant)
|
||||
response = WebAppSiteResponse.from_app_site(
|
||||
tenant=tenant,
|
||||
app_model=app_model,
|
||||
site=_site_model(app_id=app_model.id),
|
||||
end_user_id="eu-1",
|
||||
features=FeatureModel(can_replace_logo=True, webapp_copyright_enabled=True),
|
||||
can_replace_logo=True,
|
||||
)
|
||||
|
||||
assert response.can_replace_logo is True
|
||||
assert response.custom_config is not None
|
||||
assert response.custom_config.remove_webapp_brand is True
|
||||
assert response.custom_config.replace_webapp_logo is not None
|
||||
assert "webapp-logo" in response.custom_config.replace_webapp_logo
|
||||
|
||||
|
||||
class TestWebModelConfigResponse:
|
||||
def test_serializes_internal_model_config_properties_to_public_keys(self) -> None:
|
||||
model_config = AppModelConfig(
|
||||
app_id="app-test",
|
||||
opening_statement="Hello",
|
||||
suggested_questions='["Question?"]',
|
||||
suggested_questions_after_answer='{"enabled": true}',
|
||||
more_like_this='{"enabled": false}',
|
||||
model='{"provider": "openai", "name": "gpt-4o", "mode": "chat"}',
|
||||
user_input_form='[{"text-input": {"label": "Name", "variable": "name", "required": true}}]',
|
||||
pre_prompt="System prompt",
|
||||
created_by="account-1",
|
||||
updated_by="account-1",
|
||||
)
|
||||
|
||||
dumped = WebModelConfigResponse.model_validate(model_config, from_attributes=True).model_dump(mode="json")
|
||||
|
||||
assert dumped == {
|
||||
"opening_statement": "Hello",
|
||||
"suggested_questions": ["Question?"],
|
||||
"suggested_questions_after_answer": {"enabled": True},
|
||||
"more_like_this": {"enabled": False},
|
||||
"model": {"provider": "openai", "name": "gpt-4o", "mode": "chat"},
|
||||
"user_input_form": [{"text-input": {"label": "Name", "variable": "name", "required": True}}],
|
||||
"pre_prompt": "System prompt",
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
@ -107,13 +106,13 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
description="desc",
|
||||
input_placeholder="Ask the app",
|
||||
default_language="en",
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=False,
|
||||
copyright=None,
|
||||
privacy_policy=None,
|
||||
input_placeholder="Ask me anything",
|
||||
custom_disclaimer=None,
|
||||
custom_disclaimer="",
|
||||
prompt_public=False,
|
||||
show_workflow_steps=True,
|
||||
use_icon_as_answer_icon=False,
|
||||
@ -139,7 +138,7 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
|
||||
with app.test_request_context("/api/form/human_input/token-1", method="GET"):
|
||||
response = HumanInputFormApi().get("token-1")
|
||||
|
||||
body = json.loads(response.get_data(as_text=True))
|
||||
body = response
|
||||
assert set(body.keys()) == {
|
||||
"site",
|
||||
"form_content",
|
||||
@ -168,8 +167,8 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
|
||||
"description": "desc",
|
||||
"copyright": None,
|
||||
"privacy_policy": None,
|
||||
"input_placeholder": "Ask me anything",
|
||||
"custom_disclaimer": None,
|
||||
"input_placeholder": "Ask the app",
|
||||
"custom_disclaimer": "",
|
||||
"default_language": "en",
|
||||
"prompt_public": False,
|
||||
"show_workflow_steps": True,
|
||||
@ -188,72 +187,6 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
|
||||
limiter_mock.increment_rate_limit.assert_called_once_with("203.0.113.10")
|
||||
|
||||
|
||||
def test_serialize_app_site_payload_masks_paid_webapp_fields_when_feature_disabled(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Runtime site payload hides paid-only fields for plans that cannot use them."""
|
||||
|
||||
tenant = SimpleNamespace(id="tenant-1", plan="sandbox", custom_config_dict={})
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", tenant=tenant, enable_site=True)
|
||||
site_model = SimpleNamespace(
|
||||
title="My Site",
|
||||
icon_type="emoji",
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
description="desc",
|
||||
default_language="en",
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=False,
|
||||
copyright="Dify",
|
||||
privacy_policy=None,
|
||||
input_placeholder="Ask me anything",
|
||||
custom_disclaimer=None,
|
||||
prompt_public=False,
|
||||
show_workflow_steps=True,
|
||||
use_icon_as_answer_icon=False,
|
||||
)
|
||||
features = FeatureModel(can_replace_logo=False, webapp_copyright_enabled=False)
|
||||
features.billing.enabled = True
|
||||
monkeypatch.setattr(site_module.FeatureService, "get_features", lambda tenant_id, **_kwargs: features)
|
||||
|
||||
payload = site_module.serialize_app_site_payload(app_model, site_model, end_user_id=None)
|
||||
|
||||
assert payload["site"]["copyright"] is None
|
||||
assert payload["site"]["input_placeholder"] is None
|
||||
|
||||
|
||||
def test_serialize_app_site_payload_keeps_paid_webapp_fields_when_billing_disabled(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Self-hosted community runtimes keep site fields because billing is not enforcing the paid gate."""
|
||||
|
||||
tenant = SimpleNamespace(id="tenant-1", plan="basic", custom_config_dict={})
|
||||
app_model = SimpleNamespace(id="app-1", tenant_id="tenant-1", tenant=tenant, enable_site=True)
|
||||
site_model = SimpleNamespace(
|
||||
title="My Site",
|
||||
icon_type="emoji",
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
description="desc",
|
||||
default_language="en",
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=False,
|
||||
copyright="Dify",
|
||||
privacy_policy=None,
|
||||
input_placeholder="Ask me anything",
|
||||
custom_disclaimer=None,
|
||||
prompt_public=False,
|
||||
show_workflow_steps=True,
|
||||
use_icon_as_answer_icon=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
site_module.FeatureService,
|
||||
"get_features",
|
||||
lambda tenant_id, **_kwargs: FeatureModel(can_replace_logo=False, webapp_copyright_enabled=False),
|
||||
)
|
||||
|
||||
payload = site_module.serialize_app_site_payload(app_model, site_model, end_user_id=None)
|
||||
|
||||
assert payload["site"]["copyright"] == "Dify"
|
||||
assert payload["site"]["input_placeholder"] == "Ask me anything"
|
||||
|
||||
|
||||
def test_get_form_uses_runtime_select_options(monkeypatch: pytest.MonkeyPatch, app: Flask):
|
||||
"""GET returns variable-backed select options resolved from runtime state."""
|
||||
|
||||
@ -319,13 +252,13 @@ def test_get_form_uses_runtime_select_options(monkeypatch: pytest.MonkeyPatch, a
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
description="desc",
|
||||
input_placeholder="Ask the app",
|
||||
default_language="en",
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=False,
|
||||
copyright=None,
|
||||
privacy_policy=None,
|
||||
input_placeholder="Ask me anything",
|
||||
custom_disclaimer=None,
|
||||
custom_disclaimer="",
|
||||
prompt_public=False,
|
||||
show_workflow_steps=True,
|
||||
use_icon_as_answer_icon=False,
|
||||
@ -346,7 +279,7 @@ def test_get_form_uses_runtime_select_options(monkeypatch: pytest.MonkeyPatch, a
|
||||
with app.test_request_context("/api/form/human_input/token-1", method="GET"):
|
||||
response = HumanInputFormApi().get("token-1")
|
||||
|
||||
body = json.loads(response.get_data(as_text=True))
|
||||
body = response
|
||||
assert body["inputs"] == [input_config.model_dump(mode="json") for input_config in runtime_inputs]
|
||||
service_mock.resolve_form_inputs.assert_called_once_with(form)
|
||||
|
||||
@ -444,13 +377,13 @@ def test_get_form_allows_backstage_token(monkeypatch: pytest.MonkeyPatch, app: F
|
||||
icon="robot",
|
||||
icon_background="#fff",
|
||||
description="desc",
|
||||
input_placeholder="Ask the app",
|
||||
default_language="en",
|
||||
chat_color_theme="light",
|
||||
chat_color_theme_inverted=False,
|
||||
copyright=None,
|
||||
privacy_policy=None,
|
||||
input_placeholder="Ask me anything",
|
||||
custom_disclaimer=None,
|
||||
custom_disclaimer="",
|
||||
prompt_public=False,
|
||||
show_workflow_steps=True,
|
||||
use_icon_as_answer_icon=False,
|
||||
@ -473,7 +406,7 @@ def test_get_form_allows_backstage_token(monkeypatch: pytest.MonkeyPatch, app: F
|
||||
with app.test_request_context("/api/form/human_input/token-1", method="GET"):
|
||||
response = HumanInputFormApi().get("token-1")
|
||||
|
||||
body = json.loads(response.get_data(as_text=True))
|
||||
body = response
|
||||
assert set(body.keys()) == {
|
||||
"site",
|
||||
"form_content",
|
||||
@ -502,8 +435,8 @@ def test_get_form_allows_backstage_token(monkeypatch: pytest.MonkeyPatch, app: F
|
||||
"description": "desc",
|
||||
"copyright": None,
|
||||
"privacy_policy": None,
|
||||
"input_placeholder": "Ask me anything",
|
||||
"custom_disclaimer": None,
|
||||
"input_placeholder": "Ask the app",
|
||||
"custom_disclaimer": "",
|
||||
"default_language": "en",
|
||||
"prompt_public": False,
|
||||
"show_workflow_steps": True,
|
||||
|
||||
@ -9,11 +9,11 @@ export type AllowedExtensionsResponse = {
|
||||
}
|
||||
|
||||
export type UploadConfig = {
|
||||
attachment_image_file_size_limit?: number | null
|
||||
attachment_image_file_size_limit: number
|
||||
audio_file_size_limit: number
|
||||
batch_count_limit: number
|
||||
file_size_limit: number
|
||||
file_upload_limit?: number | null
|
||||
file_upload_limit: number
|
||||
image_file_batch_limit: number
|
||||
image_file_size_limit: number
|
||||
single_chunk_attachment_limit: number
|
||||
|
||||
@ -13,11 +13,11 @@ export const zAllowedExtensionsResponse = z.object({
|
||||
* UploadConfig
|
||||
*/
|
||||
export const zUploadConfig = z.object({
|
||||
attachment_image_file_size_limit: z.int().nullish(),
|
||||
attachment_image_file_size_limit: z.int(),
|
||||
audio_file_size_limit: z.int(),
|
||||
batch_count_limit: z.int(),
|
||||
file_size_limit: z.int(),
|
||||
file_upload_limit: z.int().nullish(),
|
||||
file_upload_limit: z.int(),
|
||||
image_file_batch_limit: z.int(),
|
||||
image_file_size_limit: z.int(),
|
||||
single_chunk_attachment_limit: z.int(),
|
||||
|
||||
@ -453,11 +453,13 @@ export const forgotPassword = {
|
||||
/**
|
||||
* Issue an upload token for a human input form
|
||||
*
|
||||
* Issue an upload token for an active human input form
|
||||
* POST /api/form/human_input/<form_token>/upload-token
|
||||
*/
|
||||
export const post13 = oc
|
||||
.route({
|
||||
description: 'POST /api/form/human_input/<form_token>/upload-token',
|
||||
description:
|
||||
'Issue an upload token for an active human input form\nPOST /api/form/human_input/<form_token>/upload-token',
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postFormHumanInputByFormTokenUploadToken',
|
||||
@ -475,11 +477,13 @@ export const uploadToken = {
|
||||
/**
|
||||
* Get human input form definition by token
|
||||
*
|
||||
* Get a human input form definition by token
|
||||
* GET /api/form/human_input/<form_token>
|
||||
*/
|
||||
export const get2 = oc
|
||||
.route({
|
||||
description: 'GET /api/form/human_input/<form_token>',
|
||||
description:
|
||||
'Get a human input form definition by token\nGET /api/form/human_input/<form_token>',
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
operationId: 'getFormHumanInputByFormToken',
|
||||
@ -493,6 +497,7 @@ export const get2 = oc
|
||||
/**
|
||||
* Submit human input form by token
|
||||
*
|
||||
* Submit a human input form by token
|
||||
* POST /api/form/human_input/<form_token>
|
||||
*
|
||||
* Request body:
|
||||
@ -506,7 +511,7 @@ export const get2 = oc
|
||||
export const post14 = oc
|
||||
.route({
|
||||
description:
|
||||
'POST /api/form/human_input/<form_token>\n\nRequest body:\n{\n "inputs": {\n "content": "User input content"\n },\n "action": "Approve"\n}',
|
||||
'Submit a human input form by token\nPOST /api/form/human_input/<form_token>\n\nRequest body:\n{\n "inputs": {\n "content": "User input content"\n },\n "action": "Approve"\n}',
|
||||
inputStructure: 'detailed',
|
||||
method: 'POST',
|
||||
operationId: 'postFormHumanInputByFormToken',
|
||||
|
||||
@ -47,51 +47,7 @@ export type AppPermissionQuery = {
|
||||
appId: string
|
||||
}
|
||||
|
||||
export type AppSiteInfoResponse = {
|
||||
app_id: string
|
||||
can_replace_logo: boolean
|
||||
custom_config?: {
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
enable_site: boolean
|
||||
end_user_id?: string | null
|
||||
model_config?: AppSiteModelConfigResponse | null
|
||||
plan?: string | null
|
||||
site: AppSiteResponse
|
||||
}
|
||||
|
||||
export type AppSiteModelConfigResponse = {
|
||||
model: unknown
|
||||
more_like_this: unknown
|
||||
opening_statement?: string | null
|
||||
pre_prompt?: string | null
|
||||
suggested_questions: unknown
|
||||
suggested_questions_after_answer: unknown
|
||||
user_input_form: unknown
|
||||
}
|
||||
|
||||
export type AppSiteResponse = {
|
||||
chat_color_theme?: string | null
|
||||
chat_color_theme_inverted?: boolean | null
|
||||
copyright?: string | null
|
||||
custom_disclaimer?: string | null
|
||||
default_language?: string | null
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
icon_url?: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
prompt_public?: boolean | null
|
||||
show_workflow_steps?: boolean | null
|
||||
title?: string | null
|
||||
use_icon_as_answer_icon?: boolean | null
|
||||
}
|
||||
|
||||
export type AudioBinaryResponse = Blob | File
|
||||
|
||||
export type AudioTranscriptResponse = {
|
||||
export type AudioToTextResponse = {
|
||||
text: string
|
||||
}
|
||||
|
||||
@ -289,15 +245,13 @@ export type HumanInputFormDefinition = {
|
||||
|
||||
export type HumanInputFormDefinitionResponse = {
|
||||
expiration_time: number
|
||||
form_content: unknown
|
||||
inputs: unknown
|
||||
form_content: string
|
||||
inputs: Array<FormInputConfig>
|
||||
resolved_default_values: {
|
||||
[key: string]: string
|
||||
}
|
||||
site?: {
|
||||
[key: string]: unknown
|
||||
} | null
|
||||
user_actions: unknown
|
||||
site?: WebAppSiteResponse | null
|
||||
user_actions: Array<UserActionConfig>
|
||||
}
|
||||
|
||||
export type HumanInputFormSubmissionData = {
|
||||
@ -319,7 +273,7 @@ export type HumanInputFormSubmitPayload = {
|
||||
}
|
||||
|
||||
export type HumanInputFormSubmitResponse = {
|
||||
[key: string]: never
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type HumanInputUploadTokenResponse = {
|
||||
@ -620,6 +574,22 @@ export type WebAppAuthSsoModel = {
|
||||
protocol: string
|
||||
}
|
||||
|
||||
export type WebAppCustomConfigResponse = {
|
||||
remove_webapp_brand: boolean
|
||||
replace_webapp_logo?: string | null
|
||||
}
|
||||
|
||||
export type WebAppSiteResponse = {
|
||||
app_id: string
|
||||
can_replace_logo: boolean
|
||||
custom_config?: WebAppCustomConfigResponse | null
|
||||
enable_site: boolean
|
||||
end_user_id?: string | null
|
||||
model_config?: WebModelConfigResponse | null
|
||||
plan: string
|
||||
site: WebSiteResponse
|
||||
}
|
||||
|
||||
export type WebMessageInfiniteScrollPagination = {
|
||||
data: Array<WebMessageListItem>
|
||||
has_more: boolean
|
||||
@ -646,6 +616,35 @@ export type WebMessageListItem = {
|
||||
status: string
|
||||
}
|
||||
|
||||
export type WebModelConfigResponse = {
|
||||
model?: unknown
|
||||
more_like_this?: unknown
|
||||
opening_statement?: string | null
|
||||
pre_prompt?: string | null
|
||||
suggested_questions?: unknown
|
||||
suggested_questions_after_answer?: unknown
|
||||
user_input_form?: unknown
|
||||
}
|
||||
|
||||
export type WebSiteResponse = {
|
||||
chat_color_theme?: string | null
|
||||
chat_color_theme_inverted: boolean
|
||||
copyright?: string | null
|
||||
custom_disclaimer?: string | null
|
||||
default_language?: string | null
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
readonly icon_url: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
prompt_public?: boolean | null
|
||||
show_workflow_steps?: boolean | null
|
||||
title: string
|
||||
use_icon_as_answer_icon?: boolean | null
|
||||
}
|
||||
|
||||
export type WorkflowRunPayload = {
|
||||
files?: Array<{
|
||||
transfer_method: 'local_file' | 'remote_url'
|
||||
@ -658,6 +657,52 @@ export type WorkflowRunPayload = {
|
||||
}
|
||||
}
|
||||
|
||||
export type GeneratedAppResponseWritable = JsonValue
|
||||
|
||||
export type HumanInputFormDefinitionResponseWritable = {
|
||||
expiration_time: number
|
||||
form_content: string
|
||||
inputs: Array<FormInputConfig>
|
||||
resolved_default_values: {
|
||||
[key: string]: string
|
||||
}
|
||||
site?: WebAppSiteResponseWritable | null
|
||||
user_actions: Array<UserActionConfig>
|
||||
}
|
||||
|
||||
export type HumanInputFormSubmitResponseWritable = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type WebAppSiteResponseWritable = {
|
||||
app_id: string
|
||||
can_replace_logo: boolean
|
||||
custom_config?: WebAppCustomConfigResponse | null
|
||||
enable_site: boolean
|
||||
end_user_id?: string | null
|
||||
model_config?: WebModelConfigResponse | null
|
||||
plan: string
|
||||
site: WebSiteResponseWritable
|
||||
}
|
||||
|
||||
export type WebSiteResponseWritable = {
|
||||
chat_color_theme?: string | null
|
||||
chat_color_theme_inverted: boolean
|
||||
copyright?: string | null
|
||||
custom_disclaimer?: string | null
|
||||
default_language?: string | null
|
||||
description?: string | null
|
||||
icon?: string | null
|
||||
icon_background?: string | null
|
||||
icon_type?: string | null
|
||||
input_placeholder?: string | null
|
||||
privacy_policy?: string | null
|
||||
prompt_public?: boolean | null
|
||||
show_workflow_steps?: boolean | null
|
||||
title: string
|
||||
use_icon_as_answer_icon?: boolean | null
|
||||
}
|
||||
|
||||
export type PostAudioToTextData = {
|
||||
body?: never
|
||||
path?: never
|
||||
@ -675,7 +720,7 @@ export type PostAudioToTextErrors = {
|
||||
}
|
||||
|
||||
export type PostAudioToTextResponses = {
|
||||
200: AudioTranscriptResponse
|
||||
200: AudioToTextResponse
|
||||
}
|
||||
|
||||
export type PostAudioToTextResponse = PostAudioToTextResponses[keyof PostAudioToTextResponses]
|
||||
@ -1022,6 +1067,13 @@ export type GetFormHumanInputByFormTokenData = {
|
||||
url: '/form/human_input/{form_token}'
|
||||
}
|
||||
|
||||
export type GetFormHumanInputByFormTokenErrors = {
|
||||
403: unknown
|
||||
404: unknown
|
||||
412: unknown
|
||||
429: unknown
|
||||
}
|
||||
|
||||
export type GetFormHumanInputByFormTokenResponses = {
|
||||
200: HumanInputFormDefinitionResponse
|
||||
}
|
||||
@ -1038,6 +1090,13 @@ export type PostFormHumanInputByFormTokenData = {
|
||||
url: '/form/human_input/{form_token}'
|
||||
}
|
||||
|
||||
export type PostFormHumanInputByFormTokenErrors = {
|
||||
400: unknown
|
||||
404: unknown
|
||||
412: unknown
|
||||
429: unknown
|
||||
}
|
||||
|
||||
export type PostFormHumanInputByFormTokenResponses = {
|
||||
200: HumanInputFormSubmitResponse
|
||||
}
|
||||
@ -1054,6 +1113,12 @@ export type PostFormHumanInputByFormTokenUploadTokenData = {
|
||||
url: '/form/human_input/{form_token}/upload-token'
|
||||
}
|
||||
|
||||
export type PostFormHumanInputByFormTokenUploadTokenErrors = {
|
||||
404: unknown
|
||||
412: unknown
|
||||
429: unknown
|
||||
}
|
||||
|
||||
export type PostFormHumanInputByFormTokenUploadTokenResponses = {
|
||||
200: HumanInputUploadTokenResponse
|
||||
}
|
||||
@ -1422,7 +1487,7 @@ export type GetSiteErrors = {
|
||||
}
|
||||
|
||||
export type GetSiteResponses = {
|
||||
200: AppSiteInfoResponse
|
||||
200: WebAppSiteResponse
|
||||
}
|
||||
|
||||
export type GetSiteResponse = GetSiteResponses[keyof GetSiteResponses]
|
||||
@ -1459,7 +1524,9 @@ export type PostTextToAudioErrors = {
|
||||
}
|
||||
|
||||
export type PostTextToAudioResponses = {
|
||||
200: AudioBinaryResponse
|
||||
200: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type PostTextToAudioResponse = PostTextToAudioResponses[keyof PostTextToAudioResponses]
|
||||
|
||||
@ -47,63 +47,9 @@ export const zAppPermissionQuery = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* AppSiteModelConfigResponse
|
||||
* AudioToTextResponse
|
||||
*/
|
||||
export const zAppSiteModelConfigResponse = z.object({
|
||||
model: z.unknown(),
|
||||
more_like_this: z.unknown(),
|
||||
opening_statement: z.string().nullish(),
|
||||
pre_prompt: z.string().nullish(),
|
||||
suggested_questions: z.unknown(),
|
||||
suggested_questions_after_answer: z.unknown(),
|
||||
user_input_form: z.unknown(),
|
||||
})
|
||||
|
||||
/**
|
||||
* AppSiteResponse
|
||||
*/
|
||||
export const zAppSiteResponse = z.object({
|
||||
chat_color_theme: z.string().nullish(),
|
||||
chat_color_theme_inverted: z.boolean().nullish(),
|
||||
copyright: z.string().nullish(),
|
||||
custom_disclaimer: z.string().nullish(),
|
||||
default_language: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
icon_url: z.string().nullish(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
prompt_public: z.boolean().nullish(),
|
||||
show_workflow_steps: z.boolean().nullish(),
|
||||
title: z.string().nullish(),
|
||||
use_icon_as_answer_icon: z.boolean().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* AppSiteInfoResponse
|
||||
*/
|
||||
export const zAppSiteInfoResponse = z.object({
|
||||
app_id: z.string(),
|
||||
can_replace_logo: z.boolean(),
|
||||
custom_config: z.record(z.string(), z.unknown()).nullish(),
|
||||
enable_site: z.boolean(),
|
||||
end_user_id: z.string().nullish(),
|
||||
model_config: zAppSiteModelConfigResponse.nullish(),
|
||||
plan: z.string().nullish(),
|
||||
site: zAppSiteResponse,
|
||||
})
|
||||
|
||||
/**
|
||||
* AudioBinaryResponse
|
||||
*/
|
||||
export const zAudioBinaryResponse = z.custom<Blob | File>()
|
||||
|
||||
/**
|
||||
* AudioTranscriptResponse
|
||||
*/
|
||||
export const zAudioTranscriptResponse = z.object({
|
||||
export const zAudioToTextResponse = z.object({
|
||||
text: z.string(),
|
||||
})
|
||||
|
||||
@ -321,22 +267,10 @@ export const zHumanInputFileUploadFormPayload = z.object({
|
||||
url: z.url().min(1).max(2083).nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* HumanInputFormDefinitionResponse
|
||||
*/
|
||||
export const zHumanInputFormDefinitionResponse = z.object({
|
||||
expiration_time: z.int(),
|
||||
form_content: z.unknown(),
|
||||
inputs: z.unknown(),
|
||||
resolved_default_values: z.record(z.string(), z.string()),
|
||||
site: z.record(z.string(), z.unknown()).nullish(),
|
||||
user_actions: z.unknown(),
|
||||
})
|
||||
|
||||
/**
|
||||
* HumanInputFormSubmitResponse
|
||||
*/
|
||||
export const zHumanInputFormSubmitResponse = z.record(z.string(), z.never())
|
||||
export const zHumanInputFormSubmitResponse = z.record(z.string(), z.unknown())
|
||||
|
||||
/**
|
||||
* HumanInputUploadTokenResponse
|
||||
@ -883,6 +817,14 @@ export const zSystemFeatureModel = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
/**
|
||||
* WebAppCustomConfigResponse
|
||||
*/
|
||||
export const zWebAppCustomConfigResponse = z.object({
|
||||
remove_webapp_brand: z.boolean(),
|
||||
replace_webapp_logo: z.string().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* WebMessageListItem
|
||||
*/
|
||||
@ -913,6 +855,67 @@ export const zWebMessageInfiniteScrollPagination = z.object({
|
||||
limit: z.int(),
|
||||
})
|
||||
|
||||
/**
|
||||
* WebModelConfigResponse
|
||||
*/
|
||||
export const zWebModelConfigResponse = z.object({
|
||||
model: z.unknown().optional(),
|
||||
more_like_this: z.unknown().optional(),
|
||||
opening_statement: z.string().nullish(),
|
||||
pre_prompt: z.string().nullish(),
|
||||
suggested_questions: z.unknown().optional(),
|
||||
suggested_questions_after_answer: z.unknown().optional(),
|
||||
user_input_form: z.unknown().optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
* WebSiteResponse
|
||||
*/
|
||||
export const zWebSiteResponse = z.object({
|
||||
chat_color_theme: z.string().nullish(),
|
||||
chat_color_theme_inverted: z.boolean(),
|
||||
copyright: z.string().nullish(),
|
||||
custom_disclaimer: z.string().nullish(),
|
||||
default_language: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
icon_url: z.string().nullable(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
prompt_public: z.boolean().nullish(),
|
||||
show_workflow_steps: z.boolean().nullish(),
|
||||
title: z.string(),
|
||||
use_icon_as_answer_icon: z.boolean().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* WebAppSiteResponse
|
||||
*/
|
||||
export const zWebAppSiteResponse = z.object({
|
||||
app_id: z.string(),
|
||||
can_replace_logo: z.boolean(),
|
||||
custom_config: zWebAppCustomConfigResponse.nullish(),
|
||||
enable_site: z.boolean(),
|
||||
end_user_id: z.string().nullish(),
|
||||
model_config: zWebModelConfigResponse.nullish(),
|
||||
plan: z.string(),
|
||||
site: zWebSiteResponse,
|
||||
})
|
||||
|
||||
/**
|
||||
* HumanInputFormDefinitionResponse
|
||||
*/
|
||||
export const zHumanInputFormDefinitionResponse = z.object({
|
||||
expiration_time: z.int(),
|
||||
form_content: z.string(),
|
||||
inputs: z.array(zFormInputConfig),
|
||||
resolved_default_values: z.record(z.string(), z.string()),
|
||||
site: zWebAppSiteResponse.nullish(),
|
||||
user_actions: z.array(zUserActionConfig),
|
||||
})
|
||||
|
||||
/**
|
||||
* WorkflowRunPayload
|
||||
*/
|
||||
@ -930,10 +933,67 @@ export const zWorkflowRunPayload = z.object({
|
||||
inputs: z.record(z.string(), z.unknown()),
|
||||
})
|
||||
|
||||
/**
|
||||
* GeneratedAppResponse
|
||||
*/
|
||||
export const zGeneratedAppResponseWritable = zJsonValue
|
||||
|
||||
/**
|
||||
* HumanInputFormSubmitResponse
|
||||
*/
|
||||
export const zHumanInputFormSubmitResponseWritable = z.record(z.string(), z.unknown())
|
||||
|
||||
/**
|
||||
* WebSiteResponse
|
||||
*/
|
||||
export const zWebSiteResponseWritable = z.object({
|
||||
chat_color_theme: z.string().nullish(),
|
||||
chat_color_theme_inverted: z.boolean(),
|
||||
copyright: z.string().nullish(),
|
||||
custom_disclaimer: z.string().nullish(),
|
||||
default_language: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
icon: z.string().nullish(),
|
||||
icon_background: z.string().nullish(),
|
||||
icon_type: z.string().nullish(),
|
||||
input_placeholder: z.string().nullish(),
|
||||
privacy_policy: z.string().nullish(),
|
||||
prompt_public: z.boolean().nullish(),
|
||||
show_workflow_steps: z.boolean().nullish(),
|
||||
title: z.string(),
|
||||
use_icon_as_answer_icon: z.boolean().nullish(),
|
||||
})
|
||||
|
||||
/**
|
||||
* WebAppSiteResponse
|
||||
*/
|
||||
export const zWebAppSiteResponseWritable = z.object({
|
||||
app_id: z.string(),
|
||||
can_replace_logo: z.boolean(),
|
||||
custom_config: zWebAppCustomConfigResponse.nullish(),
|
||||
enable_site: z.boolean(),
|
||||
end_user_id: z.string().nullish(),
|
||||
model_config: zWebModelConfigResponse.nullish(),
|
||||
plan: z.string(),
|
||||
site: zWebSiteResponseWritable,
|
||||
})
|
||||
|
||||
/**
|
||||
* HumanInputFormDefinitionResponse
|
||||
*/
|
||||
export const zHumanInputFormDefinitionResponseWritable = z.object({
|
||||
expiration_time: z.int(),
|
||||
form_content: z.string(),
|
||||
inputs: z.array(zFormInputConfig),
|
||||
resolved_default_values: z.record(z.string(), z.string()),
|
||||
site: zWebAppSiteResponseWritable.nullish(),
|
||||
user_actions: z.array(zUserActionConfig),
|
||||
})
|
||||
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zPostAudioToTextResponse = zAudioTranscriptResponse
|
||||
export const zPostAudioToTextResponse = zAudioToTextResponse
|
||||
|
||||
export const zPostChatMessagesBody = zChatMessagePayload
|
||||
|
||||
@ -1070,7 +1130,7 @@ export const zGetFormHumanInputByFormTokenPath = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* Success
|
||||
* Form retrieved successfully
|
||||
*/
|
||||
export const zGetFormHumanInputByFormTokenResponse = zHumanInputFormDefinitionResponse
|
||||
|
||||
@ -1081,7 +1141,7 @@ export const zPostFormHumanInputByFormTokenPath = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* Success
|
||||
* Form submitted successfully
|
||||
*/
|
||||
export const zPostFormHumanInputByFormTokenResponse = zHumanInputFormSubmitResponse
|
||||
|
||||
@ -1090,7 +1150,7 @@ export const zPostFormHumanInputByFormTokenUploadTokenPath = z.object({
|
||||
})
|
||||
|
||||
/**
|
||||
* Success
|
||||
* Upload token issued successfully
|
||||
*/
|
||||
export const zPostFormHumanInputByFormTokenUploadTokenResponse = zHumanInputUploadTokenResponse
|
||||
|
||||
@ -1238,7 +1298,7 @@ export const zDeleteSavedMessagesByMessageIdResponse = z.void()
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zGetSiteResponse = zAppSiteInfoResponse
|
||||
export const zGetSiteResponse = zWebAppSiteResponse
|
||||
|
||||
/**
|
||||
* System features retrieved successfully
|
||||
@ -1250,7 +1310,7 @@ export const zPostTextToAudioBody = zTextToAudioPayload
|
||||
/**
|
||||
* Success
|
||||
*/
|
||||
export const zPostTextToAudioResponse = zAudioBinaryResponse
|
||||
export const zPostTextToAudioResponse = z.record(z.string(), z.unknown())
|
||||
|
||||
export const zGetWebappAccessModeQuery = z.object({
|
||||
appCode: z.string().optional(),
|
||||
|
||||
@ -36,7 +36,7 @@ const HumanInputForm = ({
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
const isActionDisabled = isSubmitting || hasInvalidSelectOrFileInput(renderedFormInputs, inputs)
|
||||
const isActionDisabled = isSubmitting || !formToken || hasInvalidSelectOrFileInput(renderedFormInputs, inputs)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -55,7 +55,7 @@ const HumanInputForm = ({
|
||||
key={action.id}
|
||||
disabled={isActionDisabled}
|
||||
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
|
||||
onClick={() => submit(formToken, action.id, inputs)}
|
||||
onClick={() => formToken && submit(formToken, action.id, inputs)}
|
||||
>
|
||||
{action.title}
|
||||
</Button>
|
||||
|
||||
@ -342,10 +342,10 @@ export type HumanInputFormData = {
|
||||
form_content: string
|
||||
inputs: FormInputItem[]
|
||||
actions: UserAction[]
|
||||
form_token: string
|
||||
form_token: string | null
|
||||
resolved_default_values: Record<string, HumanInputResolvedValue>
|
||||
display_in_ui: boolean
|
||||
expiration_time: number
|
||||
expiration_time: number | null
|
||||
}
|
||||
|
||||
export type HumanInputRequiredResponse = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user