This commit is contained in:
chariri 2026-06-25 18:39:36 +00:00 committed by GitHub
commit 79e646d0ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 954 additions and 900 deletions

View File

@ -167,12 +167,16 @@ register_schema_models(
ChatMessagesQuery,
MessageFeedbackPayload,
FeedbackExportQuery,
)
register_response_schema_models(
console_ns,
AnnotationCountResponse,
SuggestedQuestionsResponse,
MessageDetailResponse,
MessageInfiniteScrollPaginationResponse,
SimpleResultResponse,
TextFileResponse,
)
register_response_schema_models(console_ns, SimpleResultResponse, TextFileResponse)
@console_ns.route("/apps/<uuid:app_id>/chat-messages")

View File

@ -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.audio_service import AudioService
from services.errors.audio import (
@ -37,6 +36,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
@ -47,18 +50,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(
@ -72,7 +70,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"]
@ -80,7 +78,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()
@ -121,7 +119,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:
@ -130,7 +129,7 @@ class TextApi(WebApiResource):
message_id = payload.message_id
text = payload.text
voice = payload.voice
response = AudioService.transcript_tts(
return AudioService.transcript_tts(
app_model=app_model,
session=db.session,
text=text,
@ -138,8 +137,6 @@ class TextApi(WebApiResource):
end_user=end_user.external_user_id,
message_id=message_id,
)
return response
except services.errors.app_model_config.AppModelConfigBrokenError:
logger.exception("App model config broken.")
raise AppUnavailableError()

View File

@ -5,7 +5,7 @@ from pydantic import BaseModel, Field, field_validator
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
import services
from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse
from controllers.common.fields import SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.web import web_ns
from controllers.web.error import (
@ -87,7 +87,7 @@ class ChatMessagePayload(BaseModel):
register_schema_models(web_ns, CompletionMessagePayload, ChatMessagePayload)
register_response_schema_models(web_ns, GeneratedAppResponse, SimpleResultResponse)
register_response_schema_models(web_ns, SimpleResultResponse)
# define completion api for user
@ -106,7 +106,7 @@ class CompletionApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[GeneratedAppResponse.__name__])
@web_ns.response(200, "Success")
def post(self, app_model: App, end_user: EndUser):
if app_model.mode != AppMode.COMPLETION:
raise NotCompletionAppError()
@ -122,6 +122,7 @@ class CompletionApi(WebApiResource):
app_model=app_model, user=end_user, args=args, invoke_from=InvokeFrom.WEB_APP, streaming=streaming
)
# response-contract:ignore compact_generate_response
return helper.compact_generate_response(response)
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
@ -172,7 +173,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")
@ -190,7 +191,7 @@ class ChatApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[GeneratedAppResponse.__name__])
@web_ns.response(200, "Success")
def post(self, app_model: App, end_user: EndUser):
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}:
@ -213,6 +214,7 @@ class ChatApi(WebApiResource):
app_model=app_model, user=end_user, args=args, invoke_from=InvokeFrom.WEB_APP, streaming=streaming
)
# response-contract:ignore compact_generate_response
return helper.compact_generate_response(response)
except services.errors.conversation.ConversationNotExistsError:
raise NotFound("Conversation Not Exists.")
@ -266,4 +268,4 @@ class ChatStopApi(WebApiResource):
app_mode=app_mode,
)
return {"result": "success"}, 200
return SimpleResultResponse(result="success").model_dump(mode="json"), 200

View File

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

View File

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

View File

@ -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 controllers.web.site import WebAppSiteResponse
from extensions.ext_database import db
from graphon.nodes.human_input.entities import FormInputConfig
from libs.helper import RateLimiter, extract_remote_ip, to_timestamp
from fields.base import ResponseModel
from graphon.nodes.human_input.entities import FormInputConfig, UserActionConfig
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)
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,
can_replace_logo=FeatureService.get_features(
app_model.tenant_id, exclude_vector_space=True
).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

View File

@ -7,7 +7,6 @@ from pydantic import BaseModel, Field, TypeAdapter
from werkzeug.exceptions import InternalServerError, NotFound
from controllers.common.controller_schemas import MessageFeedbackPayload, MessageListQuery
from controllers.common.fields import GeneratedAppResponse
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.web import web_ns
from controllers.web.error import (
@ -51,7 +50,6 @@ class MessageMoreLikeThisQuery(BaseModel):
register_schema_models(web_ns, MessageListQuery, MessageFeedbackPayload, MessageMoreLikeThisQuery)
register_response_schema_models(
web_ns,
GeneratedAppResponse,
ResultResponse,
SuggestedQuestionsResponse,
WebMessageInfiniteScrollPagination,
@ -161,7 +159,7 @@ class MessageMoreLikeThisApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[GeneratedAppResponse.__name__])
@web_ns.response(200, "Success")
def get(self, app_model: App, end_user: EndUser, message_id: UUID):
if app_model.mode != "completion":
raise NotCompletionAppError()
@ -182,6 +180,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.")

View File

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

View File

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

View File

@ -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 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
@ -44,64 +32,92 @@ 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,
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,
)
return cls(
app_id=app_model.id,
end_user_id=end_user_id,
enable_site=app_model.enable_site,
site=WebSiteResponse.model_validate(site, from_attributes=True),
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,
"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(
@ -114,57 +130,25 @@ 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()
can_replace_logo = FeatureService.get_features(app_model.tenant_id, exclude_vector_space=True).can_replace_logo
return AppSiteInfo(app_model.tenant, app_model, site, end_user.id, 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_app_site_payload(app_model: App, site: Site, end_user_id: str | None) -> dict[str, Any]:
can_replace_logo = FeatureService.get_features(app_model.tenant_id, exclude_vector_space=True).can_replace_logo
app_site_info = AppSiteInfo(app_model.tenant, app_model, site, end_user_id, 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,
can_replace_logo=can_replace_logo,
).model_dump(mode="json")

View File

@ -3,7 +3,7 @@ import logging
from werkzeug.exceptions import InternalServerError
from controllers.common.controller_schemas import WorkflowRunPayload
from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse
from controllers.common.fields import SimpleResultResponse
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.web import web_ns
from controllers.web.error import (
@ -33,7 +33,7 @@ from services.errors.llm import InvokeRateLimitError
logger = logging.getLogger(__name__)
register_schema_models(web_ns, WorkflowRunPayload)
register_response_schema_models(web_ns, GeneratedAppResponse, SimpleResultResponse)
register_response_schema_models(web_ns, SimpleResultResponse)
@web_ns.route("/workflows/run")
@ -51,7 +51,7 @@ class WorkflowRunApi(WebApiResource):
500: "Internal Server Error",
}
)
@web_ns.response(200, "Success", web_ns.models[GeneratedAppResponse.__name__])
@web_ns.response(200, "Workflow run stream started")
def post(self, app_model: App, end_user: EndUser):
"""
Run workflow
@ -68,6 +68,7 @@ class WorkflowRunApi(WebApiResource):
app_model=app_model, user=end_user, args=args, invoke_from=InvokeFrom.WEB_APP, streaming=True
)
# response-contract:ignore compact_generate_response
return helper.compact_generate_response(response)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@ -121,4 +122,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")

View File

@ -1,7 +1,7 @@
import re
import tempfile
from pathlib import Path
from typing import Union
from typing import Literal, overload
from urllib.parse import unquote
from configs import dify_config
@ -40,10 +40,22 @@ USER_AGENT = (
class ExtractProcessor:
@overload
@classmethod
def load_from_upload_file(
cls, upload_file: UploadFile, return_text: Literal[True], is_automatic: bool = False
) -> str: ...
@overload
@classmethod
def load_from_upload_file(
cls, upload_file: UploadFile, return_text: Literal[False] = False, is_automatic: bool = False
) -> list[Document]: ...
@classmethod
def load_from_upload_file(
cls, upload_file: UploadFile, return_text: bool = False, is_automatic: bool = False
) -> Union[list[Document], str]:
) -> list[Document] | str:
extract_setting = ExtractSetting(
datasource_type=DatasourceType.FILE, upload_file=upload_file, document_model="text_model"
)
@ -53,8 +65,16 @@ class ExtractProcessor:
else:
return cls.extract(extract_setting, is_automatic)
@overload
@classmethod
def load_from_url(cls, url: str, return_text: bool = False) -> Union[list[Document], str]:
def load_from_url(cls, url: str, return_text: Literal[True]) -> str: ...
@overload
@classmethod
def load_from_url(cls, url: str, return_text: Literal[False] = False) -> list[Document]: ...
@classmethod
def load_from_url(cls, url: str, return_text: bool = False) -> list[Document] | str:
response = remote_fetcher.make_request("GET", url, headers={"User-Agent": USER_AGENT})
with tempfile.TemporaryDirectory() as temp_dir:

View File

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

View File

@ -13429,7 +13429,6 @@ Soft lifecycle state for Agent records.
| created_at | integer | | No |
| files | [ string ] | | Yes |
| id | string | | Yes |
| message_chain_id | string | | No |
| message_id | string | | Yes |
| observation | string | | No |
| position | integer | | Yes |
@ -14540,8 +14539,8 @@ Enum class for configurate method of provider model.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| annotation_create_account | [SimpleAccount](#simpleaccount) | | No |
| annotation_id | string | | Yes |
| created_at | integer | | No |
| id | string | | Yes |
#### ConversationDetail
@ -17079,6 +17078,7 @@ Enum class for large language model mode.
| agent_thoughts | [ [AgentThought](#agentthought) ] | | No |
| annotation | [ConversationAnnotation](#conversationannotation) | | No |
| annotation_hit_history | [ConversationAnnotationHitHistory](#conversationannotationhithistory) | | No |
| answer | string | | Yes |
| answer_tokens | integer | | No |
| conversation_id | string | | Yes |
| created_at | integer | | No |
@ -17092,12 +17092,11 @@ Enum class for large language model mode.
| inputs | object | | Yes |
| message | [JSONValue](#jsonvalue) | | No |
| message_files | [ [MessageFile](#messagefile) ] | | No |
| message_metadata_dict | [JSONValue](#jsonvalue) | | No |
| message_tokens | integer | | No |
| metadata | [JSONValue](#jsonvalue) | | No |
| parent_message_id | string | | No |
| provider_response_latency | number | | No |
| query | string | | Yes |
| re_sign_file_url_answer | string | | Yes |
| status | string | | Yes |
| workflow_run_id | string | | No |
@ -20187,11 +20186,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 |

View File

@ -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 | |
@ -40,14 +40,14 @@ Create a chat message for conversational applications.
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [GeneratedAppResponse](#generatedappresponse)<br> |
| 400 | Bad Request | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | App Not Found | |
| 500 | Internal Server Error | |
| Code | Description |
| ---- | ----------- |
| 200 | Success |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | App Not Found |
| 500 | Internal Server Error |
### [POST] /chat-messages/{task_id}/stop
Stop a running chat message task.
@ -80,14 +80,14 @@ Create a completion message for text generation applications.
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [GeneratedAppResponse](#generatedappresponse)<br> |
| 400 | Bad Request | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | App Not Found | |
| 500 | Internal Server Error | |
| Code | Description |
| ---- | ----------- |
| 200 | Success |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | App Not Found |
| 500 | Internal Server Error |
### [POST] /completion-messages/{task_id}/stop
Stop a running completion message task.
@ -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**
@ -526,14 +540,14 @@ Generate a new completion similar to an existing message (completion apps only).
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [GeneratedAppResponse](#generatedappresponse)<br> |
| 400 | Bad Request - Not a completion app or feature disabled | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | Message Not Found | |
| 500 | Internal Server Error | |
| Code | Description |
| ---- | ----------- |
| 200 | Success |
| 400 | Bad Request - Not a completion app or feature disabled |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Message Not Found |
| 500 | Internal Server Error |
### [GET] /messages/{message_id}/suggested-questions
Get suggested follow-up questions after a message (chat apps only).
@ -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).
@ -856,14 +870,14 @@ Execute a workflow with provided inputs and files.
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [GeneratedAppResponse](#generatedappresponse)<br> |
| 400 | Bad Request | |
| 401 | Unauthorized | |
| 403 | Forbidden | |
| 404 | App Not Found | |
| 500 | Internal Server Error | |
| Code | Description |
| ---- | ----------- |
| 200 | Success |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | App Not Found |
| 500 | Internal Server Error |
### [POST] /workflows/tasks/{task_id}/stop
**Stop workflow task**
@ -967,58 +981,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 |
| 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 |
| ---- | ---- | ----------- | -------- |
@ -1216,12 +1179,6 @@ Button styles for user actions.
| ---- | ---- | ----------- | -------- |
| FormInputConfig | [ParagraphInputConfig](#paragraphinputconfig)<br>[SelectInputConfig](#selectinputconfig)<br>[FileInputConfig](#fileinputconfig)<br>[FileListInputConfig](#filelistinputconfig) | | |
#### GeneratedAppResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| GeneratedAppResponse | | | |
#### HumanInputContent
| Name | Type | Description | Required |
@ -1260,11 +1217,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
@ -1306,7 +1263,7 @@ Parsed multipart form fields for HITL uploads.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| JSONValue | string<br>integer<br>number<br>boolean<br>object<br>[ object ] | | |
| JSONValue | | | |
#### JSONValueType
@ -1681,6 +1638,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 |
@ -1709,6 +1686,38 @@ 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 |
| 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 |

View File

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

View File

@ -2,21 +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
@ -41,7 +42,23 @@ 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",
@ -51,31 +68,30 @@ def _create_site(db_session: Session, app_id: str) -> Site:
default_language="en",
chat_color_theme="light",
chat_color_theme_inverted=False,
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 = SimpleNamespace(can_replace_logo=False)
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
@ -83,51 +99,139 @@ class TestAppSiteApi:
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 = SimpleNamespace(can_replace_logo=False)
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",
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.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,
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",
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",
}

View File

@ -2,7 +2,6 @@
from __future__ import annotations
import json
from datetime import UTC, datetime
from types import SimpleNamespace
from typing import Any
@ -112,7 +111,7 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
chat_color_theme_inverted=False,
copyright=None,
privacy_policy=None,
custom_disclaimer=None,
custom_disclaimer="",
prompt_public=False,
show_workflow_steps=True,
use_icon_as_answer_icon=False,
@ -138,7 +137,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",
@ -167,7 +166,7 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask):
"description": "desc",
"copyright": None,
"privacy_policy": None,
"custom_disclaimer": None,
"custom_disclaimer": "",
"default_language": "en",
"prompt_public": False,
"show_workflow_steps": True,
@ -256,7 +255,7 @@ def test_get_form_uses_runtime_select_options(monkeypatch: pytest.MonkeyPatch, a
chat_color_theme_inverted=False,
copyright=None,
privacy_policy=None,
custom_disclaimer=None,
custom_disclaimer="",
prompt_public=False,
show_workflow_steps=True,
use_icon_as_answer_icon=False,
@ -277,7 +276,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)
@ -380,7 +379,7 @@ def test_get_form_allows_backstage_token(monkeypatch: pytest.MonkeyPatch, app: F
chat_color_theme_inverted=False,
copyright=None,
privacy_policy=None,
custom_disclaimer=None,
custom_disclaimer="",
prompt_public=False,
show_workflow_steps=True,
use_icon_as_answer_icon=False,
@ -403,7 +402,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",
@ -432,7 +431,7 @@ def test_get_form_allows_backstage_token(monkeypatch: pytest.MonkeyPatch, app: F
"description": "desc",
"copyright": None,
"privacy_policy": None,
"custom_disclaimer": None,
"custom_disclaimer": "",
"default_language": "en",
"prompt_public": False,
"show_workflow_steps": True,

View File

@ -269,6 +269,7 @@ export type MessageDetailResponse = {
agent_thoughts?: Array<AgentThought>
annotation?: ConversationAnnotation | null
annotation_hit_history?: ConversationAnnotationHitHistory | null
answer: string
answer_tokens?: number | null
conversation_id: string
created_at?: number | null
@ -284,12 +285,11 @@ export type MessageDetailResponse = {
}
message?: JsonValue | null
message_files?: Array<MessageFile>
message_metadata_dict?: JsonValue | null
message_tokens?: number | null
metadata?: JsonValue | null
parent_message_id?: string | null
provider_response_latency?: number | null
query: string
re_sign_file_url_answer: string
status: string
workflow_run_id?: string | null
}
@ -723,7 +723,6 @@ export type AgentThought = {
created_at?: number | null
files: Array<string>
id: string
message_chain_id?: string | null
message_id: string
observation?: string | null
position: number
@ -743,8 +742,8 @@ export type ConversationAnnotation = {
export type ConversationAnnotationHitHistory = {
annotation_create_account?: SimpleAccount | null
annotation_id: string
created_at?: number | null
id: string
}
export type HumanInputContent = {

View File

@ -570,7 +570,6 @@ export const zAgentThought = z.object({
created_at: z.int().nullish(),
files: z.array(z.string()),
id: z.string(),
message_chain_id: z.string().nullish(),
message_id: z.string(),
observation: z.string().nullish(),
position: z.int(),
@ -1056,8 +1055,8 @@ export const zConversationAnnotation = z.object({
*/
export const zConversationAnnotationHitHistory = z.object({
annotation_create_account: zSimpleAccount.nullish(),
annotation_id: z.string(),
created_at: z.int().nullish(),
id: z.string(),
})
/**
@ -2035,6 +2034,7 @@ export const zMessageDetailResponse = z.object({
agent_thoughts: z.array(zAgentThought).optional(),
annotation: zConversationAnnotation.nullish(),
annotation_hit_history: zConversationAnnotationHitHistory.nullish(),
answer: z.string(),
answer_tokens: z.int().nullish(),
conversation_id: z.string(),
created_at: z.int().nullish(),
@ -2048,12 +2048,11 @@ export const zMessageDetailResponse = z.object({
inputs: z.record(z.string(), zJsonValue),
message: zJsonValue.nullish(),
message_files: z.array(zMessageFile).optional(),
message_metadata_dict: zJsonValue.nullish(),
message_tokens: z.int().nullish(),
metadata: zJsonValue.nullish(),
parent_message_id: z.string().nullish(),
provider_response_latency: z.number().nullish(),
query: z.string(),
re_sign_file_url_answer: z.string(),
status: z.string(),
workflow_run_id: z.string().nullish(),
})

View File

@ -472,6 +472,7 @@ export type MessageDetailResponse = {
agent_thoughts?: Array<AgentThought>
annotation?: ConversationAnnotation | null
annotation_hit_history?: ConversationAnnotationHitHistory | null
answer: string
answer_tokens?: number | null
conversation_id: string
created_at?: number | null
@ -487,12 +488,11 @@ export type MessageDetailResponse = {
}
message?: JsonValue | null
message_files?: Array<MessageFile>
message_metadata_dict?: JsonValue | null
message_tokens?: number | null
metadata?: JsonValue | null
parent_message_id?: string | null
provider_response_latency?: number | null
query: string
re_sign_file_url_answer: string
status: string
workflow_run_id?: string | null
}
@ -1498,7 +1498,6 @@ export type AgentThought = {
created_at?: number | null
files: Array<string>
id: string
message_chain_id?: string | null
message_id: string
observation?: string | null
position: number
@ -1518,8 +1517,8 @@ export type ConversationAnnotation = {
export type ConversationAnnotationHitHistory = {
annotation_create_account?: SimpleAccount | null
annotation_id: string
created_at?: number | null
id: string
}
export type HumanInputContent = {

View File

@ -1150,7 +1150,6 @@ export const zAgentThought = z.object({
created_at: z.int().nullish(),
files: z.array(z.string()),
id: z.string(),
message_chain_id: z.string().nullish(),
message_id: z.string(),
observation: z.string().nullish(),
position: z.int(),
@ -1371,8 +1370,8 @@ export const zConversationAnnotation = z.object({
*/
export const zConversationAnnotationHitHistory = z.object({
annotation_create_account: zSimpleAccount.nullish(),
annotation_id: z.string(),
created_at: z.int().nullish(),
id: z.string(),
})
/**
@ -3455,6 +3454,7 @@ export const zMessageDetailResponse = z.object({
agent_thoughts: z.array(zAgentThought).optional(),
annotation: zConversationAnnotation.nullish(),
annotation_hit_history: zConversationAnnotationHitHistory.nullish(),
answer: z.string(),
answer_tokens: z.int().nullish(),
conversation_id: z.string(),
created_at: z.int().nullish(),
@ -3468,12 +3468,11 @@ export const zMessageDetailResponse = z.object({
inputs: z.record(z.string(), zJsonValue),
message: zJsonValue.nullish(),
message_files: z.array(zMessageFile).optional(),
message_metadata_dict: zJsonValue.nullish(),
message_tokens: z.int().nullish(),
metadata: zJsonValue.nullish(),
parent_message_id: z.string().nullish(),
provider_response_latency: z.number().nullish(),
query: z.string(),
re_sign_file_url_answer: z.string(),
status: z.string(),
workflow_run_id: z.string().nullish(),
})

View File

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

View File

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

View File

@ -246,7 +246,6 @@ export type AgentThought = {
created_at?: number | null
files: Array<string>
id: string
message_chain_id?: string | null
message_id: string
observation?: string | null
position: number

View File

@ -266,7 +266,6 @@ export const zAgentThought = z.object({
created_at: z.int().nullish(),
files: z.array(z.string()),
id: z.string(),
message_chain_id: z.string().nullish(),
message_id: z.string(),
observation: z.string().nullish(),
position: z.int(),

View File

@ -14,9 +14,6 @@ import {
zGetFormHumanInputByFormTokenResponse,
zGetLoginStatusQuery,
zGetLoginStatusResponse,
zGetMessagesByMessageIdMoreLikeThisPath,
zGetMessagesByMessageIdMoreLikeThisQuery,
zGetMessagesByMessageIdMoreLikeThisResponse,
zGetMessagesByMessageIdSuggestedQuestionsPath,
zGetMessagesByMessageIdSuggestedQuestionsResponse,
zGetMessagesQuery,
@ -42,14 +39,10 @@ import {
zPatchConversationsByCIdUnpinPath,
zPatchConversationsByCIdUnpinResponse,
zPostAudioToTextResponse,
zPostChatMessagesBody,
zPostChatMessagesByTaskIdStopPath,
zPostChatMessagesByTaskIdStopResponse,
zPostChatMessagesResponse,
zPostCompletionMessagesBody,
zPostCompletionMessagesByTaskIdStopPath,
zPostCompletionMessagesByTaskIdStopResponse,
zPostCompletionMessagesResponse,
zPostConversationsByCIdNameBody,
zPostConversationsByCIdNamePath,
zPostConversationsByCIdNameQuery,
@ -83,10 +76,6 @@ import {
zPostSavedMessagesBody,
zPostSavedMessagesQuery,
zPostSavedMessagesResponse,
zPostTextToAudioBody,
zPostTextToAudioResponse,
zPostWorkflowsRunBody,
zPostWorkflowsRunResponse,
zPostWorkflowsTasksByTaskIdStopPath,
zPostWorkflowsTasksByTaskIdStopResponse,
} from './zod.gen'
@ -135,30 +124,14 @@ export const byTaskId = {
stop,
}
/**
* Create a chat message for conversational applications.
*/
export const post3 = oc
.route({
description: 'Create a chat message for conversational applications.',
inputStructure: 'detailed',
method: 'POST',
operationId: 'postChatMessages',
path: '/chat-messages',
tags: ['web'],
})
.input(z.object({ body: zPostChatMessagesBody }))
.output(zPostChatMessagesResponse)
export const chatMessages = {
post: post3,
byTaskId,
}
/**
* Stop a running completion message task.
*/
export const post4 = oc
export const post3 = oc
.route({
description: 'Stop a running completion message task.',
inputStructure: 'detailed',
@ -171,37 +144,21 @@ export const post4 = oc
.output(zPostCompletionMessagesByTaskIdStopResponse)
export const stop2 = {
post: post4,
post: post3,
}
export const byTaskId2 = {
stop: stop2,
}
/**
* Create a completion message for text generation applications.
*/
export const post5 = oc
.route({
description: 'Create a completion message for text generation applications.',
inputStructure: 'detailed',
method: 'POST',
operationId: 'postCompletionMessages',
path: '/completion-messages',
tags: ['web'],
})
.input(z.object({ body: zPostCompletionMessagesBody }))
.output(zPostCompletionMessagesResponse)
export const completionMessages = {
post: post5,
byTaskId: byTaskId2,
}
/**
* Rename a specific conversation with a custom name or auto-generate one.
*/
export const post6 = oc
export const post4 = oc
.route({
description: 'Rename a specific conversation with a custom name or auto-generate one.',
inputStructure: 'detailed',
@ -220,7 +177,7 @@ export const post6 = oc
.output(zPostConversationsByCIdNameResponse)
export const name = {
post: post6,
post: post4,
}
/**
@ -307,7 +264,7 @@ export const conversations = {
/**
* Verify email code and complete login
*/
export const post7 = oc
export const post5 = oc
.route({
description: 'Verify email code and complete login',
inputStructure: 'detailed',
@ -320,13 +277,13 @@ export const post7 = oc
.output(zPostEmailCodeLoginValidityResponse)
export const validity = {
post: post7,
post: post5,
}
/**
* Send email verification code for login
*/
export const post8 = oc
export const post6 = oc
.route({
description: 'Send email verification code for login',
inputStructure: 'detailed',
@ -339,7 +296,7 @@ export const post8 = oc
.output(zPostEmailCodeLoginResponse)
export const emailCodeLogin = {
post: post8,
post: post6,
validity,
}
@ -369,7 +326,7 @@ export const emailCodeLogin = {
* FileTooLargeError: File exceeds size limit
* UnsupportedFileTypeError: File type not supported
*/
export const post9 = oc
export const post7 = oc
.route({
description:
'Upload a file for use in web applications\nAccepts file uploads for use within web applications, supporting\nmultiple file types with automatic validation and storage.\n\nArgs:\n app_model: The associated application model\n end_user: The end user uploading the file\n\nForm Parameters:\n file: The file to upload (required)\n source: Optional source type (datasets or None)\n\nReturns:\n dict: File information including ID, URL, and metadata\n int: HTTP status code 201 for success\n\nRaises:\n NoFileUploadedError: No file provided in request\n TooManyFilesError: Multiple files provided (only one allowed)\n FilenameNotExistsError: File has no filename\n FileTooLargeError: File exceeds size limit\n UnsupportedFileTypeError: File type not supported',
@ -384,7 +341,7 @@ export const post9 = oc
.output(zPostFilesUploadResponse)
export const upload = {
post: post9,
post: post7,
}
export const files = {
@ -394,7 +351,7 @@ export const files = {
/**
* Reset user password with verification token
*/
export const post10 = oc
export const post8 = oc
.route({
description: 'Reset user password with verification token',
inputStructure: 'detailed',
@ -407,13 +364,13 @@ export const post10 = oc
.output(zPostForgotPasswordResetsResponse)
export const resets = {
post: post10,
post: post8,
}
/**
* Verify password reset token validity
*/
export const post11 = oc
export const post9 = oc
.route({
description: 'Verify password reset token validity',
inputStructure: 'detailed',
@ -426,13 +383,13 @@ export const post11 = oc
.output(zPostForgotPasswordValidityResponse)
export const validity2 = {
post: post11,
post: post9,
}
/**
* Send password reset email
*/
export const post12 = oc
export const post10 = oc
.route({
description: 'Send password reset email',
inputStructure: 'detailed',
@ -445,7 +402,7 @@ export const post12 = oc
.output(zPostForgotPasswordResponse)
export const forgotPassword = {
post: post12,
post: post10,
resets,
validity: validity2,
}
@ -453,11 +410,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
export const post11 = 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',
@ -469,17 +428,19 @@ export const post13 = oc
.output(zPostFormHumanInputByFormTokenUploadTokenResponse)
export const uploadToken = {
post: post13,
post: post11,
}
/**
* 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 +454,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:
@ -503,10 +465,10 @@ export const get2 = oc
* "action": "Approve"
* }
*/
export const post14 = oc
export const post12 = 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',
@ -524,7 +486,7 @@ export const post14 = oc
export const byFormToken = {
get: get2,
post: post14,
post: post12,
uploadToken,
}
@ -539,7 +501,7 @@ export const form = {
/**
* Upload one local file or remote URL file for a HITL human input form
*/
export const post15 = oc
export const post13 = oc
.route({
inputStructure: 'detailed',
method: 'POST',
@ -552,7 +514,7 @@ export const post15 = oc
.output(zPostHumanInputFormsFilesResponse)
export const files2 = {
post: post15,
post: post13,
}
export const humanInputForms = {
@ -583,7 +545,7 @@ export const status = {
*
* Authenticate user for web application access
*/
export const post16 = oc
export const post14 = oc
.route({
description: 'Authenticate user for web application access',
inputStructure: 'detailed',
@ -597,14 +559,14 @@ export const post16 = oc
.output(zPostLoginResponse)
export const login = {
post: post16,
post: post14,
status,
}
/**
* Logout user from web application
*/
export const post17 = oc
export const post15 = oc
.route({
description: 'Logout user from web application',
inputStructure: 'detailed',
@ -616,13 +578,13 @@ export const post17 = oc
.output(zPostLogoutResponse)
export const logout = {
post: post17,
post: post15,
}
/**
* Submit feedback (like/dislike) for a specific message.
*/
export const post18 = oc
export const post16 = oc
.route({
description: 'Submit feedback (like/dislike) for a specific message.',
inputStructure: 'detailed',
@ -641,37 +603,13 @@ export const post18 = oc
.output(zPostMessagesByMessageIdFeedbacksResponse)
export const feedbacks = {
post: post18,
}
/**
* Generate a new completion similar to an existing message (completion apps only).
*/
export const get4 = oc
.route({
description: 'Generate a new completion similar to an existing message (completion apps only).',
inputStructure: 'detailed',
method: 'GET',
operationId: 'getMessagesByMessageIdMoreLikeThis',
path: '/messages/{message_id}/more-like-this',
tags: ['web'],
})
.input(
z.object({
params: zGetMessagesByMessageIdMoreLikeThisPath,
query: zGetMessagesByMessageIdMoreLikeThisQuery,
}),
)
.output(zGetMessagesByMessageIdMoreLikeThisResponse)
export const moreLikeThis = {
get: get4,
post: post16,
}
/**
* Get suggested follow-up questions after a message (chat apps only).
*/
export const get5 = oc
export const get4 = oc
.route({
description: 'Get suggested follow-up questions after a message (chat apps only).',
inputStructure: 'detailed',
@ -684,19 +622,18 @@ export const get5 = oc
.output(zGetMessagesByMessageIdSuggestedQuestionsResponse)
export const suggestedQuestions = {
get: get5,
get: get4,
}
export const byMessageId = {
feedbacks,
moreLikeThis,
suggestedQuestions,
}
/**
* Retrieve paginated list of messages from a conversation in a chat application.
*/
export const get6 = oc
export const get5 = oc
.route({
description: 'Retrieve paginated list of messages from a conversation in a chat application.',
inputStructure: 'detailed',
@ -709,7 +646,7 @@ export const get6 = oc
.output(zGetMessagesResponse)
export const messages = {
get: get6,
get: get5,
byMessageId,
}
@ -718,7 +655,7 @@ export const messages = {
*
* Retrieve the metadata for a specific app.
*/
export const get7 = oc
export const get6 = oc
.route({
description: 'Retrieve the metadata for a specific app.',
inputStructure: 'detailed',
@ -731,7 +668,7 @@ export const get7 = oc
.output(zGetMetaResponse)
export const meta = {
get: get7,
get: get6,
}
/**
@ -739,7 +676,7 @@ export const meta = {
*
* Retrieve the parameters for a specific app.
*/
export const get8 = oc
export const get7 = oc
.route({
description: 'Retrieve the parameters for a specific app.',
inputStructure: 'detailed',
@ -752,13 +689,13 @@ export const get8 = oc
.output(zGetParametersResponse)
export const parameters = {
get: get8,
get: get7,
}
/**
* Get authentication passport for web application access
*/
export const get9 = oc
export const get8 = oc
.route({
description: 'Get authentication passport for web application access',
inputStructure: 'detailed',
@ -771,7 +708,7 @@ export const get9 = oc
.output(zGetPassportResponse)
export const passport = {
get: get9,
get: get8,
}
/**
@ -797,7 +734,7 @@ export const passport = {
* FileTooLargeError: File exceeds size limit
* UnsupportedFileTypeError: File type not supported
*/
export const post19 = oc
export const post17 = oc
.route({
description:
'Upload a file from a remote URL\nDownloads a file from the provided remote URL and uploads it\nto the platform storage for use in web applications.\n\nArgs:\n app_model: The associated application model\n end_user: The end user making the request\n\nJSON Parameters:\n url: The remote URL to download the file from (required)\n\nReturns:\n dict: File information including ID, signed URL, and metadata\n int: HTTP status code 201 for success\n\nRaises:\n RemoteFileUploadError: Failed to fetch file from remote URL\n FileTooLargeError: File exceeds size limit\n UnsupportedFileTypeError: File type not supported',
@ -813,7 +750,7 @@ export const post19 = oc
.output(zPostRemoteFilesUploadResponse)
export const upload2 = {
post: post19,
post: post17,
}
/**
@ -834,7 +771,7 @@ export const upload2 = {
* Raises:
* HTTPException: If the remote file cannot be accessed
*/
export const get10 = oc
export const get9 = oc
.route({
description:
'Get information about a remote file\nRetrieves basic information about a file located at a remote URL,\nincluding content type and content length.\n\nArgs:\n app_model: The associated application model\n end_user: The end user making the request\n url: URL-encoded path to the remote file\n\nReturns:\n dict: Remote file information including type and length\n\nRaises:\n HTTPException: If the remote file cannot be accessed',
@ -849,7 +786,7 @@ export const get10 = oc
.output(zGetRemoteFilesByUrlResponse)
export const byUrl = {
get: get10,
get: get9,
}
export const remoteFiles = {
@ -880,7 +817,7 @@ export const byMessageId2 = {
/**
* Retrieve paginated list of saved messages for a completion application.
*/
export const get11 = oc
export const get10 = oc
.route({
description: 'Retrieve paginated list of saved messages for a completion application.',
inputStructure: 'detailed',
@ -895,7 +832,7 @@ export const get11 = oc
/**
* Save a specific message for later reference.
*/
export const post20 = oc
export const post18 = oc
.route({
description: 'Save a specific message for later reference.',
inputStructure: 'detailed',
@ -908,8 +845,8 @@ export const post20 = oc
.output(zPostSavedMessagesResponse)
export const savedMessages = {
get: get11,
post: post20,
get: get10,
post: post18,
byMessageId: byMessageId2,
}
@ -918,7 +855,7 @@ export const savedMessages = {
*
* Retrieve app site information and configuration.
*/
export const get12 = oc
export const get11 = oc
.route({
description: 'Retrieve app site information and configuration.',
inputStructure: 'detailed',
@ -931,7 +868,7 @@ export const get12 = oc
.output(zGetSiteResponse)
export const site = {
get: get12,
get: get11,
}
/**
@ -954,7 +891,7 @@ export const site = {
*
* Only non-sensitive configuration data should be returned by this endpoint.
*/
export const get13 = oc
export const get12 = oc
.route({
description:
'Get system feature flags and configuration\nReturns the current system feature flags and configuration\nthat control various functionalities across the platform.\n\nReturns:\n dict: System feature configuration object\n\nThis endpoint is akin to the `SystemFeatureApi` endpoint in api/controllers/console/feature.py,\nexcept it is intended for use by the web app, instead of the console dashboard.\n\nNOTE: This endpoint is unauthenticated by design, as it provides system features\ndata required for webapp initialization.\n\nAuthentication would create circular dependency (can\'t authenticate without webapp loading).\n\nOnly non-sensitive configuration data should be returned by this endpoint.',
@ -968,35 +905,13 @@ export const get13 = oc
.output(zGetSystemFeaturesResponse)
export const systemFeatures = {
get: get13,
}
/**
* Convert text to audio
*
* Convert text to audio using text-to-speech service.
*/
export const post21 = oc
.route({
description: 'Convert text to audio using text-to-speech service.',
inputStructure: 'detailed',
method: 'POST',
operationId: 'postTextToAudio',
path: '/text-to-audio',
summary: 'Convert text to audio',
tags: ['web'],
})
.input(z.object({ body: zPostTextToAudioBody }))
.output(zPostTextToAudioResponse)
export const textToAudio = {
post: post21,
get: get12,
}
/**
* Retrieve the access mode for a web application (public or restricted).
*/
export const get14 = oc
export const get13 = oc
.route({
description: 'Retrieve the access mode for a web application (public or restricted).',
inputStructure: 'detailed',
@ -1009,13 +924,13 @@ export const get14 = oc
.output(zGetWebappAccessModeResponse)
export const accessMode = {
get: get14,
get: get13,
}
/**
* Check if user has permission to access a web application.
*/
export const get15 = oc
export const get14 = oc
.route({
description: 'Check if user has permission to access a web application.',
inputStructure: 'detailed',
@ -1028,7 +943,7 @@ export const get15 = oc
.output(zGetWebappPermissionResponse)
export const permission = {
get: get15,
get: get14,
}
export const webapp = {
@ -1043,7 +958,7 @@ export const webapp = {
*
* Returns Server-Sent Events stream.
*/
export const get16 = oc
export const get15 = oc
.route({
description: 'GET /api/workflow/<task_id>/events\n\nReturns Server-Sent Events stream.',
inputStructure: 'detailed',
@ -1057,7 +972,7 @@ export const get16 = oc
.output(zGetWorkflowByTaskIdEventsResponse)
export const events = {
get: get16,
get: get15,
}
export const byTaskId3 = {
@ -1068,34 +983,12 @@ export const workflow = {
byTaskId: byTaskId3,
}
/**
* Run workflow
*
* Execute a workflow with provided inputs and files.
*/
export const post22 = oc
.route({
description: 'Execute a workflow with provided inputs and files.',
inputStructure: 'detailed',
method: 'POST',
operationId: 'postWorkflowsRun',
path: '/workflows/run',
summary: 'Run workflow',
tags: ['web'],
})
.input(z.object({ body: zPostWorkflowsRunBody }))
.output(zPostWorkflowsRunResponse)
export const run = {
post: post22,
}
/**
* Stop workflow task
*
* Stop a running workflow task.
*/
export const post23 = oc
export const post19 = oc
.route({
description: 'Stop a running workflow task.',
inputStructure: 'detailed',
@ -1109,7 +1002,7 @@ export const post23 = oc
.output(zPostWorkflowsTasksByTaskIdStopResponse)
export const stop3 = {
post: post23,
post: post19,
}
export const byTaskId4 = {
@ -1121,7 +1014,6 @@ export const tasks = {
}
export const workflows = {
run,
tasks,
}
@ -1145,7 +1037,6 @@ export const contract = {
savedMessages,
site,
systemFeatures,
textToAudio,
webapp,
workflow,
workflows,

View File

@ -46,50 +46,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
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
}
@ -256,8 +213,6 @@ export type FormInputConfig
type: 'file-list'
} & FileListInputConfig)
export type GeneratedAppResponse = JsonValue
export type HumanInputContent = {
form_definition?: HumanInputFormDefinition | null
form_submission_data?: HumanInputFormSubmissionData | null
@ -287,15 +242,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 = {
@ -317,7 +270,7 @@ export type HumanInputFormSubmitPayload = {
}
export type HumanInputFormSubmitResponse = {
[key: string]: never
[key: string]: unknown
}
export type HumanInputUploadTokenResponse = {
@ -329,16 +282,7 @@ export type JsonObject = {
[key: string]: unknown
}
export type JsonValue
= | string
| number
| number
| boolean
| {
[key: string]: unknown
}
| Array<unknown>
| null
export type JsonValue = unknown
export type JsonValueType = unknown
@ -614,6 +558,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
@ -640,6 +600,34 @@ 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
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'
@ -652,6 +640,49 @@ export type WorkflowRunPayload = {
}
}
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
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
@ -669,32 +700,11 @@ export type PostAudioToTextErrors = {
}
export type PostAudioToTextResponses = {
200: AudioTranscriptResponse
200: AudioToTextResponse
}
export type PostAudioToTextResponse = PostAudioToTextResponses[keyof PostAudioToTextResponses]
export type PostChatMessagesData = {
body: ChatMessagePayload
path?: never
query?: never
url: '/chat-messages'
}
export type PostChatMessagesErrors = {
400: unknown
401: unknown
403: unknown
404: unknown
500: unknown
}
export type PostChatMessagesResponses = {
200: GeneratedAppResponse
}
export type PostChatMessagesResponse = PostChatMessagesResponses[keyof PostChatMessagesResponses]
export type PostChatMessagesByTaskIdStopData = {
body?: never
path: {
@ -719,28 +729,6 @@ export type PostChatMessagesByTaskIdStopResponses = {
export type PostChatMessagesByTaskIdStopResponse
= PostChatMessagesByTaskIdStopResponses[keyof PostChatMessagesByTaskIdStopResponses]
export type PostCompletionMessagesData = {
body: CompletionMessagePayload
path?: never
query?: never
url: '/completion-messages'
}
export type PostCompletionMessagesErrors = {
400: unknown
401: unknown
403: unknown
404: unknown
500: unknown
}
export type PostCompletionMessagesResponses = {
200: GeneratedAppResponse
}
export type PostCompletionMessagesResponse
= PostCompletionMessagesResponses[keyof PostCompletionMessagesResponses]
export type PostCompletionMessagesByTaskIdStopData = {
body?: never
path: {
@ -1016,6 +1004,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
}
@ -1032,6 +1027,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
}
@ -1048,6 +1050,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
}
@ -1174,32 +1182,6 @@ export type PostMessagesByMessageIdFeedbacksResponses = {
export type PostMessagesByMessageIdFeedbacksResponse
= PostMessagesByMessageIdFeedbacksResponses[keyof PostMessagesByMessageIdFeedbacksResponses]
export type GetMessagesByMessageIdMoreLikeThisData = {
body?: never
path: {
message_id: string
}
query: {
response_mode: 'blocking' | 'streaming'
}
url: '/messages/{message_id}/more-like-this'
}
export type GetMessagesByMessageIdMoreLikeThisErrors = {
400: unknown
401: unknown
403: unknown
404: unknown
500: unknown
}
export type GetMessagesByMessageIdMoreLikeThisResponses = {
200: GeneratedAppResponse
}
export type GetMessagesByMessageIdMoreLikeThisResponse
= GetMessagesByMessageIdMoreLikeThisResponses[keyof GetMessagesByMessageIdMoreLikeThisResponses]
export type GetMessagesByMessageIdSuggestedQuestionsData = {
body?: never
path: {
@ -1416,7 +1398,7 @@ export type GetSiteErrors = {
}
export type GetSiteResponses = {
200: AppSiteInfoResponse
200: WebAppSiteResponse
}
export type GetSiteResponse = GetSiteResponses[keyof GetSiteResponses]
@ -1438,26 +1420,6 @@ export type GetSystemFeaturesResponses = {
export type GetSystemFeaturesResponse = GetSystemFeaturesResponses[keyof GetSystemFeaturesResponses]
export type PostTextToAudioData = {
body: TextToAudioPayload
path?: never
query?: never
url: '/text-to-audio'
}
export type PostTextToAudioErrors = {
400: unknown
401: unknown
403: unknown
500: unknown
}
export type PostTextToAudioResponses = {
200: AudioBinaryResponse
}
export type PostTextToAudioResponse = PostTextToAudioResponses[keyof PostTextToAudioResponses]
export type GetWebappAccessModeData = {
body?: never
path?: never
@ -1518,27 +1480,6 @@ export type GetWorkflowByTaskIdEventsResponses = {
export type GetWorkflowByTaskIdEventsResponse
= GetWorkflowByTaskIdEventsResponses[keyof GetWorkflowByTaskIdEventsResponses]
export type PostWorkflowsRunData = {
body: WorkflowRunPayload
path?: never
query?: never
url: '/workflows/run'
}
export type PostWorkflowsRunErrors = {
400: unknown
401: unknown
403: unknown
404: unknown
500: unknown
}
export type PostWorkflowsRunResponses = {
200: GeneratedAppResponse
}
export type PostWorkflowsRunResponse = PostWorkflowsRunResponses[keyof PostWorkflowsRunResponses]
export type PostWorkflowsTasksByTaskIdStopData = {
body?: never
path: {

View File

@ -47,62 +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(),
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(),
})
@ -320,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
@ -347,16 +282,7 @@ export const zHumanInputUploadTokenResponse = z.object({
export const zJsonObject = z.record(z.string(), z.unknown())
export const zJsonValue = z
.union([
z.string(),
z.int(),
z.number(),
z.boolean(),
z.record(z.string(), z.unknown()),
z.array(z.unknown()),
])
.nullable()
export const zJsonValue = z.unknown()
/**
* AgentThought
@ -375,11 +301,6 @@ export const zAgentThought = z.object({
tool_labels: zJsonValue,
})
/**
* GeneratedAppResponse
*/
export const zGeneratedAppResponse = zJsonValue
export const zJsonValueType = z.unknown()
export const zJsonValue2 = z.unknown()
@ -874,6 +795,14 @@ export const zSystemFeatureModel = z.object({
}),
})
/**
* WebAppCustomConfigResponse
*/
export const zWebAppCustomConfigResponse = z.object({
remove_webapp_brand: z.boolean(),
replace_webapp_logo: z.string().nullish(),
})
/**
* WebMessageListItem
*/
@ -904,6 +833,66 @@ 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(),
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
*/
@ -922,16 +911,60 @@ export const zWorkflowRunPayload = z.object({
})
/**
* Success
* HumanInputFormSubmitResponse
*/
export const zPostAudioToTextResponse = zAudioTranscriptResponse
export const zHumanInputFormSubmitResponseWritable = z.record(z.string(), z.unknown())
export const zPostChatMessagesBody = zChatMessagePayload
/**
* 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(),
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 zPostChatMessagesResponse = zGeneratedAppResponse
export const zPostAudioToTextResponse = zAudioToTextResponse
export const zPostChatMessagesByTaskIdStopPath = z.object({
task_id: z.string(),
@ -942,13 +975,6 @@ export const zPostChatMessagesByTaskIdStopPath = z.object({
*/
export const zPostChatMessagesByTaskIdStopResponse = zSimpleResultResponse
export const zPostCompletionMessagesBody = zCompletionMessagePayload
/**
* Success
*/
export const zPostCompletionMessagesResponse = zGeneratedAppResponse
export const zPostCompletionMessagesByTaskIdStopPath = z.object({
task_id: z.string(),
})
@ -1061,7 +1087,7 @@ export const zGetFormHumanInputByFormTokenPath = z.object({
})
/**
* Success
* Form retrieved successfully
*/
export const zGetFormHumanInputByFormTokenResponse = zHumanInputFormDefinitionResponse
@ -1072,7 +1098,7 @@ export const zPostFormHumanInputByFormTokenPath = z.object({
})
/**
* Success
* Form submitted successfully
*/
export const zPostFormHumanInputByFormTokenResponse = zHumanInputFormSubmitResponse
@ -1081,7 +1107,7 @@ export const zPostFormHumanInputByFormTokenUploadTokenPath = z.object({
})
/**
* Success
* Upload token issued successfully
*/
export const zPostFormHumanInputByFormTokenUploadTokenResponse = zHumanInputUploadTokenResponse
@ -1139,19 +1165,6 @@ export const zPostMessagesByMessageIdFeedbacksQuery = z.object({
*/
export const zPostMessagesByMessageIdFeedbacksResponse = zResultResponse
export const zGetMessagesByMessageIdMoreLikeThisPath = z.object({
message_id: z.uuid(),
})
export const zGetMessagesByMessageIdMoreLikeThisQuery = z.object({
response_mode: z.enum(['blocking', 'streaming']),
})
/**
* Success
*/
export const zGetMessagesByMessageIdMoreLikeThisResponse = zGeneratedAppResponse
export const zGetMessagesByMessageIdSuggestedQuestionsPath = z.object({
message_id: z.uuid(),
})
@ -1229,20 +1242,13 @@ export const zDeleteSavedMessagesByMessageIdResponse = z.void()
/**
* Success
*/
export const zGetSiteResponse = zAppSiteInfoResponse
export const zGetSiteResponse = zWebAppSiteResponse
/**
* System features retrieved successfully
*/
export const zGetSystemFeaturesResponse = zSystemFeatureModel
export const zPostTextToAudioBody = zTextToAudioPayload
/**
* Success
*/
export const zPostTextToAudioResponse = zAudioBinaryResponse
export const zGetWebappAccessModeQuery = z.object({
appCode: z.string().optional(),
appId: z.string().optional(),
@ -1271,13 +1277,6 @@ export const zGetWorkflowByTaskIdEventsPath = z.object({
*/
export const zGetWorkflowByTaskIdEventsResponse = zEventStreamResponse
export const zPostWorkflowsRunBody = zWorkflowRunPayload
/**
* Success
*/
export const zPostWorkflowsRunResponse = zGeneratedAppResponse
export const zPostWorkflowsTasksByTaskIdStopPath = z.object({
task_id: z.string(),
})

View File

@ -10,13 +10,21 @@ type SwaggerSchema = JsonObject & {
$ref?: string
}
type OpenApiMediaType = JsonObject & {
schema?: SwaggerSchema
}
type OpenApiResponse = JsonObject & {
content?: Record<string, OpenApiMediaType>
}
type OpenApiComponents = JsonObject & {
schemas?: Record<string, SwaggerSchema>
}
type SwaggerOperation = JsonObject & {
operationId?: string
responses?: Record<string, unknown>
responses?: Record<string, OpenApiResponse>
}
type SwaggerDocument = JsonObject & {
@ -52,6 +60,17 @@ const currentDir = path.dirname(fileURLToPath(import.meta.url))
const apiOpenApiDir = path.resolve(currentDir, 'openapi')
const operationMethods = new Set(['delete', 'get', 'patch', 'post', 'put'])
const pydanticDecimalStringPattern = '^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$'
const codegenSafeDecimalStringPattern = '^(?![-+.]*$)[+-]?0*\\d*\\.?\\d*$'
const opaqueJsonContent = (): Record<string, OpenApiMediaType> => ({
'application/json': {
schema: {
additionalProperties: true,
type: 'object',
},
},
})
const apiSpecs: ApiSpec[] = [
{ filename: 'console-openapi.json', name: 'console' },
@ -182,6 +201,46 @@ const addOperationIds = (document: SwaggerDocument) => {
}
}
const isOpaqueContractResponse = (response: OpenApiResponse) => {
const content = response.content
if (!isObject(content))
return false
return Object.entries(content).some(([mediaType, media]) => {
if (!isObject(media))
return false
return (mediaType === 'application/json' || mediaType === 'text/event-stream') && !('schema' in media)
})
}
const hasOpaqueContractSuccessResponse = (operation: SwaggerOperation) => {
return Object.entries(operation.responses ?? {}).some(([status, response]) => {
return /^2\d\d$/.test(status) && isObject(response) && isOpaqueContractResponse(response)
})
}
const normalizeOpaqueContractResponses = (document: SwaggerDocument) => {
// Some backend endpoints has no schema (e.g. external) and will trap heyapi here
// So we forge an opaque schema here
for (const pathItem of Object.values(document.paths ?? {})) {
for (const [method, operation] of Object.entries(pathItem)) {
if (!operationMethods.has(method) || !isObject(operation))
continue
const swaggerOperation = operation as SwaggerOperation
if (!hasOpaqueContractSuccessResponse(swaggerOperation))
continue
Object.values(swaggerOperation.responses ?? {})
.filter(response => isObject(response) && isOpaqueContractResponse(response))
.forEach((response) => {
response.content = opaqueJsonContent()
})
}
}
}
const hasSuccessResponse = (operation: SwaggerOperation) => {
return Object.entries(operation.responses ?? {}).some(([status, response]) => {
if (!/^2\d\d$/.test(status))
@ -215,6 +274,7 @@ const filterContractOperations = (document: SwaggerDocument) => {
}
const normalizeApiSwagger = (document: SwaggerDocument) => {
normalizeOpaqueContractResponses(document)
filterContractOperations(document)
addOperationIds(document)
@ -380,10 +440,20 @@ const createApiConfig = (job: ApiJob): UserConfig => ({
'name': 'zod',
'~resolvers': {
string: (ctx) => {
if (ctx.schema.format !== 'binary')
return undefined
if (ctx.schema.format === 'binary')
return $(ctx.symbols.z).attr('custom').call().generic($.type.or($.type('Blob'), $.type('File')))
return $(ctx.symbols.z).attr('custom').call().generic($.type.or($.type('Blob'), $.type('File')))
if (ctx.schema.pattern === pydanticDecimalStringPattern) {
// the pydantic generated regex will emit error like
// regexp/no-useless-assertions, so patch the regex here
return $(ctx.symbols.z)
.attr('string')
.call()
.attr('regex')
.call($.regexp(codegenSafeDecimalStringPattern))
}
return undefined
},
},
},

View File

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

View File

@ -88,7 +88,7 @@ export const triggerSubscriptionUpdateContract = base
credentials?: Record<string, unknown>
}
}>())
.output(type<{ result: string, id: string }>())
.output(type<{ result: string }>())
export const triggerSubscriptionBuilderLogsContract = base
.route({ path: '/workspaces/current/trigger-provider/{provider}/subscriptions/builder/logs/{subscriptionBuilderId}', method: 'GET' })

View File

@ -217,14 +217,8 @@ const toFeedback = (feedback: NonNullable<MessageDetailResponse['feedbacks']>[nu
}
}
type AgentDebugMessageWithLegacyAnswer = MessageDetailResponse & {
answer?: string | null
}
const getAgentDebugMessageAnswer = (message: MessageDetailResponse) => {
const legacyAnswer = (message as AgentDebugMessageWithLegacyAnswer).answer
return message.re_sign_file_url_answer ?? legacyAnswer ?? ''
return message.answer ?? ''
}
function getFormattedAgentDebugChatTree(messages: MessageDetailResponse[]): ChatItemInTree[] {

View File

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