refactor(api): align app model config write contract (#42979)

This commit is contained in:
yyh
2026-09-28 03:22:37 +00:00
committed by GitHub
parent fc5053edf0
commit c42ee2fbac
9 changed files with 3085 additions and 150 deletions
+6 -47
View File
@@ -1,9 +1,6 @@
import json
from typing import Any, cast
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from controllers.common.fields import SimpleResultResponse
@@ -11,6 +8,7 @@ from controllers.common.rbac import PlainApp, RBACCheck
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.common.session import with_session
from controllers.console import console_ns
from controllers.console.app.model_config_payload import AppModelConfigPayload
from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
RBACPermission,
@@ -30,47 +28,7 @@ from libs.login import login_required
from models.model import App, AppMode, AppModelConfig
from services.app_model_config_service import AppModelConfigService
class ModelConfigRequest(BaseModel):
provider: str | None = Field(default=None, description="Model provider")
model: str | None = Field(default=None, description="Model name")
configs: dict[str, Any] | None = Field(
default=None,
description="Model configuration parameters",
)
opening_statement: str | None = Field(default=None, description="Opening statement")
suggested_questions: list[str] | None = Field(default=None, description="Suggested questions")
more_like_this: dict[str, Any] | None = Field(
default=None,
description="More like this configuration",
)
speech_to_text: dict[str, Any] | None = Field(
default=None,
description="Speech to text configuration",
)
text_to_speech: dict[str, Any] | None = Field(
default=None,
description="Text to speech configuration",
)
retrieval_model: dict[str, Any] | None = Field(
default=None,
description="Retrieval model configuration",
)
tools: list[dict[str, Any]] | None = Field(
default=None,
description="Available tools",
)
dataset_configs: dict[str, Any] | None = Field(
default=None,
description="Dataset configurations",
)
agent_mode: dict[str, Any] | None = Field(
default=None,
description="Agent mode configuration",
)
register_schema_models(console_ns, ModelConfigRequest)
register_schema_models(console_ns, AppModelConfigPayload)
register_response_schema_models(console_ns, SimpleResultResponse)
@@ -79,7 +37,7 @@ class ModelConfigResource(Resource):
@console_ns.doc("update_app_model_config")
@console_ns.doc(description="Update application model configuration")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[ModelConfigRequest.__name__])
@console_ns.expect(console_ns.models[AppModelConfigPayload.__name__])
@console_ns.response(
200,
"Model configuration updated successfully",
@@ -98,10 +56,11 @@ class ModelConfigResource(Resource):
@get_app_model(mode=[AppMode.AGENT_CHAT, AppMode.CHAT, AppMode.COMPLETION])
def post(self, session: Session, current_tenant_id: str, current_user_id: str, app_model: App):
"""Modify the app model config and dataset joins in one request transaction."""
# validate config
payload = AppModelConfigPayload.model_validate(console_ns.payload or {})
model_configuration = AppModelConfigService.validate_configuration(
tenant_id=current_tenant_id,
config=cast(dict, request.json),
# Preserve omission so feature validators retain their existing defaults.
config=payload.model_dump(exclude_unset=True),
app_mode=AppMode.value_of(app_model.mode),
session=session,
)
@@ -0,0 +1,384 @@
from typing import Literal, NotRequired
from pydantic import BaseModel, ConfigDict, Field, with_config
from typing_extensions import TypedDict
from core.entities.agent_entities import PlanningStrategy
from core.rag.entities.metadata_entities import SupportedComparisonOperator
from core.rag.rerank.rerank_type import RerankMode
from core.tools.entities.tool_entities import ToolProviderType
from graphon.file.enums import FileTransferMethod, FileType
from graphon.model_runtime.entities.llm_entities import LLMMode
# The recursive schema also keeps generated clients from turning JSON values into `any`.
type AppConfigJsonValue = str | int | float | bool | None | list[AppConfigJsonValue] | dict[str, AppConfigJsonValue]
# Mypy does not yet support PEP 728 extra_items; pyrefly and Pydantic check these declarations.
# https://github.com/python/mypy/issues/18176
class AppModelSelectionPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
provider: str
name: str
completion_params: dict[str, AppConfigJsonValue]
mode: NotRequired[str | None]
# These config managers persist nested extensions alongside their known fields.
# Keep those JSON values when an existing configuration is published again.
class AppPromptMessagePayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
text: str
role: str
class AppChatPromptPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
prompt: NotRequired[list[AppPromptMessagePayload]]
class AppConversationRolesPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
user_prefix: str
assistant_prefix: str
class AppCompletionPromptTextPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
text: str
class AppCompletionPromptPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
prompt: NotRequired[AppCompletionPromptTextPayload]
conversation_histories_role: NotRequired[AppConversationRolesPayload]
@with_config(ConfigDict(extra="allow"))
class AppInputFieldBasePayload(TypedDict):
label: str
variable: str
description: NotRequired[str]
required: NotRequired[bool | None]
default: NotRequired[AppConfigJsonValue]
max_length: NotRequired[int | None]
hide: NotRequired[bool]
type: NotRequired[str]
enabled: NotRequired[bool]
config: NotRequired[dict[str, AppConfigJsonValue]]
icon: NotRequired[str | None]
icon_background: NotRequired[str | None]
json_schema: NotRequired[str | dict[str, AppConfigJsonValue] | None]
class AppInputFieldPayload(AppInputFieldBasePayload, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
options: NotRequired[list[str]]
class AppSelectFieldPayload(AppInputFieldBasePayload, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
options: NotRequired[list[str] | None]
class AppTextInputPayload(BaseModel):
model_config = ConfigDict(extra="forbid", serialize_by_alias=True)
text_input: AppInputFieldPayload = Field(alias="text-input")
class AppSelectInputPayload(BaseModel):
model_config = ConfigDict(extra="forbid")
select: AppSelectFieldPayload
class AppParagraphInputPayload(BaseModel):
model_config = ConfigDict(extra="forbid")
paragraph: AppInputFieldPayload
class AppNumberInputPayload(BaseModel):
model_config = ConfigDict(extra="forbid")
number: AppInputFieldPayload
class AppCheckboxInputPayload(BaseModel):
model_config = ConfigDict(extra="forbid")
checkbox: AppInputFieldPayload
class AppExternalDataInputPayload(BaseModel):
model_config = ConfigDict(extra="forbid")
external_data_tool: AppInputFieldPayload
type AppUserInputFormPayload = (
AppTextInputPayload
| AppSelectInputPayload
| AppParagraphInputPayload
| AppNumberInputPayload
| AppCheckboxInputPayload
| AppExternalDataInputPayload
)
class AppFeaturePayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
enabled: NotRequired[bool | None]
class AppSuggestedQuestionsModelPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
provider: str
name: str
mode: NotRequired[LLMMode | Literal[""]]
completion_params: NotRequired[dict[str, AppConfigJsonValue]]
class AppSuggestedQuestionsPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
enabled: NotRequired[bool | None]
prompt: NotRequired[str]
model: NotRequired[AppSuggestedQuestionsModelPayload]
class AppTextToSpeechPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
enabled: NotRequired[bool | None]
voice: NotRequired[str]
language: NotRequired[str]
autoPlay: NotRequired[Literal["enabled", "disabled"]]
class AppModerationContentPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
enabled: NotRequired[bool | None]
preset_response: NotRequired[str | None]
class AppModerationConfigPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
inputs_config: NotRequired[AppModerationContentPayload | None]
outputs_config: NotRequired[AppModerationContentPayload | None]
keywords: NotRequired[str | None]
api_based_extension_id: NotRequired[str | None]
class AppModerationPayload(BaseModel):
enabled: bool | None = None
type: str | None = None
config: AppModerationConfigPayload | None = None
class AppExternalDataToolPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
enabled: NotRequired[bool | None]
type: NotRequired[str]
label: NotRequired[str]
variable: NotRequired[str]
config: NotRequired[dict[str, AppConfigJsonValue]]
icon: NotRequired[str]
icon_background: NotRequired[str]
class AppDatasetSelectionPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
id: NotRequired[str]
enabled: NotRequired[bool]
class AppLegacyDatasetSelectionPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
id: NotRequired[str]
enabled: NotRequired[bool | None]
class AppDatasetToolPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
dataset: AppDatasetSelectionPayload
class AppEmptyDatasetCollectionPayload(TypedDict, closed=True):
pass
class AppDatasetCollectionPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
strategy: NotRequired[str]
datasets: list[AppDatasetToolPayload]
class AppRerankingModelPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
reranking_provider_name: NotRequired[str]
reranking_model_name: NotRequired[str]
class AppVectorWeightPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
vector_weight: float
embedding_provider_name: str
embedding_model_name: str
class AppKeywordWeightPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
keyword_weight: float
class AppRetrievalWeightsPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
weight_type: NotRequired[Literal["semantic_first", "keyword_first", "customized"]]
vector_setting: AppVectorWeightPayload
keyword_setting: AppKeywordWeightPayload
class AppMetadataModelPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
provider: NotRequired[str]
name: NotRequired[str]
mode: NotRequired[LLMMode | Literal[""]]
completion_params: NotRequired[dict[str, AppConfigJsonValue]]
class AppMetadataConditionPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
id: NotRequired[str]
name: str
metadata_id: NotRequired[str]
comparison_operator: SupportedComparisonOperator
value: NotRequired[str | int | float | list[str] | None]
class AppMetadataFilteringPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
logical_operator: NotRequired[Literal["and", "or"] | None]
conditions: NotRequired[list[AppMetadataConditionPayload] | None]
class AppDatasetConfigPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
retrieval_model: NotRequired[Literal["single", "multiple"]]
datasets: NotRequired[AppDatasetCollectionPayload | AppEmptyDatasetCollectionPayload | None]
top_k: NotRequired[int]
score_threshold: NotRequired[float | None]
score_threshold_enabled: NotRequired[bool]
reranking_model: NotRequired[AppRerankingModelPayload | None]
weights: NotRequired[AppRetrievalWeightsPayload | None]
reranking_enabled: NotRequired[bool]
reranking_enable: NotRequired[bool]
reranking_mode: NotRequired[RerankMode]
metadata_filtering_mode: NotRequired[Literal["disabled", "automatic", "manual"]]
metadata_model_config: NotRequired[AppMetadataModelPayload | None]
metadata_filtering_conditions: NotRequired[AppMetadataFilteringPayload | None]
class AppAgentPromptPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
first_prompt: NotRequired[str]
next_iteration: NotRequired[str]
class AppProviderAgentToolPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
enabled: NotRequired[bool | None]
provider_type: ToolProviderType
provider_id: str
provider_name: NotRequired[str]
tool_name: str
tool_label: NotRequired[str]
tool_parameters: dict[str, AppConfigJsonValue]
plugin_unique_identifier: NotRequired[str | None]
credential_id: NotRequired[str | None]
isDeleted: NotRequired[bool]
notAuthor: NotRequired[bool]
class AppLegacyDatasetToolPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
dataset: AppLegacyDatasetSelectionPayload
class AppLegacyGoogleSearchToolPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
google_search: AppFeaturePayload
class AppLegacyWebReaderToolPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
web_reader: AppFeaturePayload
class AppLegacyWikipediaToolPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
wikipedia: AppFeaturePayload
class AppLegacyCurrentDatetimeToolPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
current_datetime: AppFeaturePayload
class AppLegacySensitiveWordConfigPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
enabled: bool
words: list[str]
canned_response: str
AppLegacySensitiveWordToolPayload = TypedDict( # type: ignore[misc]
"AppLegacySensitiveWordToolPayload",
{"sensitive-word-avoidance": AppLegacySensitiveWordConfigPayload},
extra_items=AppConfigJsonValue,
)
type AppAgentToolPayload = (
AppProviderAgentToolPayload
| AppLegacyDatasetToolPayload
| AppLegacyGoogleSearchToolPayload
| AppLegacyWebReaderToolPayload
| AppLegacyWikipediaToolPayload
| AppLegacyCurrentDatetimeToolPayload
| AppLegacySensitiveWordToolPayload
)
class AppAgentModePayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
enabled: NotRequired[bool | None]
strategy: NotRequired[PlanningStrategy | Literal["cot", "function-calling", ""] | None]
max_iteration: NotRequired[int]
tools: NotRequired[list[AppAgentToolPayload] | None]
prompt: NotRequired[AppAgentPromptPayload | str | None]
class AppImageUploadPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
enabled: NotRequired[bool]
number_limits: NotRequired[int]
detail: NotRequired[Literal["low", "high"] | None]
transfer_methods: NotRequired[list[FileTransferMethod]]
class AppImageConfigPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
number_limits: NotRequired[int]
detail: NotRequired[Literal["low", "high"] | None]
transfer_methods: NotRequired[list[FileTransferMethod]]
class AppFileTypeUploadPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
enabled: NotRequired[bool | None]
number_limits: NotRequired[int | None]
transfer_methods: NotRequired[list[FileTransferMethod] | None]
class AppFilePreviewPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
mode: NotRequired[str | None]
file_type_list: NotRequired[list[str] | None]
class AppFileUploadPayload(TypedDict, extra_items=AppConfigJsonValue): # type: ignore[call-arg]
enabled: NotRequired[bool]
image: NotRequired[AppImageUploadPayload]
image_config: NotRequired[AppImageConfigPayload | None]
document: NotRequired[AppFileTypeUploadPayload | None]
audio: NotRequired[AppFileTypeUploadPayload | None]
video: NotRequired[AppFileTypeUploadPayload | None]
custom: NotRequired[AppFileTypeUploadPayload | None]
preview_config: NotRequired[AppFilePreviewPayload | None]
allowed_file_types: NotRequired[list[FileType]]
allowed_file_extensions: NotRequired[list[str]]
allowed_file_upload_methods: NotRequired[list[FileTransferMethod]]
number_limits: NotRequired[int]
class AppModelConfigPayload(BaseModel):
"""Write transport; app-mode validators own defaults and feature-specific rules."""
model: AppModelSelectionPayload
prompt_type: Literal["simple", "advanced", ""] | None = None
pre_prompt: str | None = None
chat_prompt_config: AppChatPromptPayload | None = None
completion_prompt_config: AppCompletionPromptPayload | None = None
user_input_form: list[AppUserInputFormPayload] | None = None
dataset_query_variable: str | None = None
dataset_configs: AppDatasetConfigPayload | None = None
agent_mode: AppAgentModePayload | None = None
external_data_tools: list[AppExternalDataToolPayload] | None = None
file_upload: AppFileUploadPayload | None = None
opening_statement: str | None = None
suggested_questions: list[str] | None = None
suggested_questions_after_answer: AppSuggestedQuestionsPayload | None = None
more_like_this: AppFeaturePayload | None = None
speech_to_text: AppFeaturePayload | None = None
text_to_speech: AppTextToSpeechPayload | None = None
retriever_resource: AppFeaturePayload | None = None
sensitive_word_avoidance: AppModerationPayload | None = None
@@ -207,6 +207,36 @@ class DatasetConfigManager:
if not isinstance(config["agent_mode"]["tools"], list):
raise ValueError("tools in agent_mode must be a list of objects")
# All app modes persist these tools, including modes that skip the agent validator.
for tool in config["agent_mode"]["tools"]:
if not isinstance(tool, dict):
continue
if all(field in tool for field in ("provider_type", "provider_id", "tool_name", "tool_parameters")):
tool_config = tool
else:
tool_name = next(
(
name
for name in (
"dataset",
"google_search",
"web_reader",
"wikipedia",
"current_datetime",
"sensitive-word-avoidance",
)
if name in tool
),
None,
)
if tool_name is None:
continue
tool_config = tool[tool_name]
if not isinstance(tool_config, dict):
continue
if "enabled" not in tool_config or tool_config["enabled"] is None:
tool_config["enabled"] = False
# strategy
if "strategy" not in config["agent_mode"] or not config["agent_mode"].get("strategy"):
config["agent_mode"]["strategy"] = PlanningStrategy.ROUTER
+460 -18
View File
@@ -2811,7 +2811,7 @@ Update application model configuration
| Required | Schema |
| -------- | ------ |
| Yes | **application/json**: [ModelConfigRequest](#modelconfigrequest)<br> |
| Yes | **application/json**: [AppModelConfigPayload](#appmodelconfigpayload)<br> |
#### Responses
@@ -15742,6 +15742,16 @@ This class is used to store the schema information of an api based tool.
| app_id | string | | No |
| items | [ [AccessMatrixItem](#accessmatrixitem) ] | | No |
#### AppAgentModePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enabled | boolean | | No |
| max_iteration | integer | | No |
| prompt | [AppAgentPromptPayload](#appagentpromptpayload)<br>string | | No |
| strategy | [PlanningStrategy](#planningstrategy)<br>string, <br>**Available values:** "", "cot", "function-calling" | | No |
| tools | [ [AppAgentToolPayload](#appagenttoolpayload) ] | | No |
#### AppAgentModeResponse
| Name | Type | Description | Required |
@@ -15752,6 +15762,13 @@ This class is used to store the schema information of an api based tool.
| strategy | [PlanningStrategy](#planningstrategy)<br>string, <br>**Available values:** "cot", "function-calling" | | No |
| tools | [ [AppProviderAgentToolResponse](#appprovideragenttoolresponse)<br>[AppLegacyDatasetToolResponse](#applegacydatasettoolresponse)<br>[AppLegacyGoogleSearchToolResponse](#applegacygooglesearchtoolresponse)<br>[AppLegacyWebReaderToolResponse](#applegacywebreadertoolresponse)<br>[AppLegacyWikipediaToolResponse](#applegacywikipediatoolresponse)<br>[AppLegacyCurrentDatetimeToolResponse](#applegacycurrentdatetimetoolresponse)<br>[AppLegacySensitiveWordToolResponseItem](#applegacysensitivewordtoolresponseitem) ] | | No |
#### AppAgentPromptPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| first_prompt | string | | No |
| next_iteration | string | | No |
#### AppAgentPromptResponse
| Name | Type | Description | Required |
@@ -15759,6 +15776,12 @@ This class is used to store the schema information of an api based tool.
| first_prompt | string | | No |
| next_iteration | string | | No |
#### AppAgentToolPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| AppAgentToolPayload | [AppProviderAgentToolPayload](#appprovideragenttoolpayload)<br>[AppLegacyDatasetToolPayload](#applegacydatasettoolpayload)<br>[AppLegacyGoogleSearchToolPayload](#applegacygooglesearchtoolpayload)<br>[AppLegacyWebReaderToolPayload](#applegacywebreadertoolpayload)<br>[AppLegacyWikipediaToolPayload](#applegacywikipediatoolpayload)<br>[AppLegacyCurrentDatetimeToolPayload](#applegacycurrentdatetimetoolpayload)<br>[AppLegacySensitiveWordToolPayload](#applegacysensitivewordtoolpayload) | | |
#### AppAnnotationReplyDisabledResponse
| Name | Type | Description | Required |
@@ -15793,12 +15816,24 @@ This class is used to store the schema information of an api based tool.
| role | string | | Yes |
| text | string | | Yes |
#### AppChatPromptPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| prompt | [ [AppPromptMessagePayload](#apppromptmessagepayload) ] | | No |
#### AppCheckboxFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| checkbox | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppCheckboxInputPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| checkbox | [AppInputFieldPayload](#appinputfieldpayload) | | Yes |
#### AppCompletionPromptConfigResponse
| Name | Type | Description | Required |
@@ -15806,12 +15841,31 @@ This class is used to store the schema information of an api based tool.
| conversation_histories_role | [AppConversationHistoriesRoleResponse](#appconversationhistoriesroleresponse) | | No |
| prompt | [AppCompletionPromptTextResponse](#appcompletionprompttextresponse) | | No |
#### AppCompletionPromptPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| conversation_histories_role | [AppConversationRolesPayload](#appconversationrolespayload) | | No |
| prompt | [AppCompletionPromptTextPayload](#appcompletionprompttextpayload) | | No |
#### AppCompletionPromptTextPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| text | string | | Yes |
#### AppCompletionPromptTextResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| text | string | | Yes |
#### AppConfigJsonValue
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| AppConfigJsonValue | string<br>integer<br>number<br>boolean<br>[ [AppConfigJsonValue](#appconfigjsonvalue) ]<br>object | | |
#### AppConversationHistoriesRoleResponse
| Name | Type | Description | Required |
@@ -15819,6 +15873,38 @@ This class is used to store the schema information of an api based tool.
| assistant_prefix | string | | Yes |
| user_prefix | string | | Yes |
#### AppConversationRolesPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| assistant_prefix | string | | Yes |
| user_prefix | string | | Yes |
#### AppDatasetCollectionPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| datasets | [ [AppDatasetToolPayload](#appdatasettoolpayload) ] | | Yes |
| strategy | string | | No |
#### AppDatasetConfigPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| datasets | [AppDatasetCollectionPayload](#appdatasetcollectionpayload)<br>[AppEmptyDatasetCollectionPayload](#appemptydatasetcollectionpayload) | | No |
| metadata_filtering_conditions | [AppMetadataFilteringPayload](#appmetadatafilteringpayload) | | No |
| metadata_filtering_mode | string, <br>**Available values:** "automatic", "disabled", "manual" | *Enum:* `"automatic"`, `"disabled"`, `"manual"` | No |
| metadata_model_config | [AppMetadataModelPayload](#appmetadatamodelpayload) | | No |
| reranking_enable | boolean | | No |
| reranking_enabled | boolean | | No |
| reranking_mode | [RerankMode](#rerankmode) | | No |
| reranking_model | [AppRerankingModelPayload](#apprerankingmodelpayload) | | No |
| retrieval_model | string, <br>**Available values:** "multiple", "single" | *Enum:* `"multiple"`, `"single"` | No |
| score_threshold | number | | No |
| score_threshold_enabled | boolean | | No |
| top_k | integer | | No |
| weights | [AppRetrievalWeightsPayload](#appretrievalweightspayload) | | No |
#### AppDatasetConfigsResponse
| Name | Type | Description | Required |
@@ -15857,6 +15943,19 @@ This class is used to store the schema information of an api based tool.
| enabled | boolean | | No |
| id | string | | No |
#### AppDatasetSelectionPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enabled | boolean | | No |
| id | string | | No |
#### AppDatasetToolPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| dataset | [AppDatasetSelectionPayload](#appdatasetselectionpayload) | | Yes |
#### AppDetail
| Name | Type | Description | Required |
@@ -15970,6 +16069,11 @@ This class is used to store the schema information of an api based tool.
| embedding_model_name | string | | Yes |
| embedding_provider_name | string | | Yes |
#### AppEmptyDatasetCollectionPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
#### AppEnabledConfigResponse
| Name | Type | Description | Required |
@@ -16003,12 +16107,36 @@ This class is used to store the schema information of an api based tool.
| ---- | ---- | ----------- | -------- |
| data | string | | Yes |
#### AppExternalDataInputPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| external_data_tool | [AppInputFieldPayload](#appinputfieldpayload) | | Yes |
#### AppExternalDataToolFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| external_data_tool | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppExternalDataToolPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | object | | No |
| enabled | boolean | | No |
| icon | string | | No |
| icon_background | string | | No |
| label | string | | No |
| type | string | | No |
| variable | string | | No |
#### AppFeaturePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enabled | boolean | | No |
#### AppFileFormResponse
| Name | Type | Description | Required |
@@ -16021,6 +16149,38 @@ This class is used to store the schema information of an api based tool.
| ---- | ---- | ----------- | -------- |
| file-list | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppFilePreviewPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| file_type_list | [ string ] | | No |
| mode | string | | No |
#### AppFileTypeUploadPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enabled | boolean | | No |
| number_limits | integer | | No |
| transfer_methods | [ [FileTransferMethod](#filetransfermethod) ] | | No |
#### AppFileUploadPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| allowed_file_extensions | [ string ] | | No |
| allowed_file_types | [ [FileType](#filetype) ] | | No |
| allowed_file_upload_methods | [ [FileTransferMethod](#filetransfermethod) ] | | No |
| audio | [AppFileTypeUploadPayload](#appfiletypeuploadpayload) | | No |
| custom | [AppFileTypeUploadPayload](#appfiletypeuploadpayload) | | No |
| document | [AppFileTypeUploadPayload](#appfiletypeuploadpayload) | | No |
| enabled | boolean | | No |
| image | [AppImageUploadPayload](#appimageuploadpayload) | | No |
| image_config | [AppImageConfigPayload](#appimageconfigpayload) | | No |
| number_limits | integer | | No |
| preview_config | [AppFilePreviewPayload](#appfilepreviewpayload) | | No |
| video | [AppFileTypeUploadPayload](#appfiletypeuploadpayload) | | No |
#### AppFileUploadResponse
| Name | Type | Description | Required |
@@ -16040,6 +16200,23 @@ This class is used to store the schema information of an api based tool.
| icon_background | string | Icon background color | No |
| icon_type | [IconType](#icontype) | Icon type | No |
#### AppImageConfigPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| detail | string, <br>**Available values:** "high", "low" | | No |
| number_limits | integer | | No |
| transfer_methods | [ [FileTransferMethod](#filetransfermethod) ] | | No |
#### AppImageUploadPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| detail | string, <br>**Available values:** "high", "low" | | No |
| enabled | boolean | | No |
| number_limits | integer | | No |
| transfer_methods | [ [FileTransferMethod](#filetransfermethod) ] | | No |
#### AppImageUploadResponse
| Name | Type | Description | Required |
@@ -16076,6 +16253,25 @@ This class is used to store the schema information of an api based tool.
| status | [ImportStatus](#importstatus) | | Yes |
| warnings | [ [DslImportWarning](#dslimportwarning) ] | | No |
#### AppInputFieldPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | object | | No |
| default | [AppConfigJsonValue](#appconfigjsonvalue) | | No |
| description | string | | No |
| enabled | boolean | | No |
| hide | boolean | | No |
| icon | string | | No |
| icon_background | string | | No |
| json_schema | string<br>object | | No |
| label | string | | Yes |
| max_length | integer | | No |
| options | [ string ] | | No |
| required | boolean | | No |
| type | string | | No |
| variable | string | | Yes |
#### AppJsonObjectFormResponse
| Name | Type | Description | Required |
@@ -16088,24 +16284,69 @@ This class is used to store the schema information of an api based tool.
| ---- | ---- | ----------- | -------- |
| keyword_weight | number | | Yes |
#### AppKeywordWeightPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| keyword_weight | number | | Yes |
#### AppLegacyCurrentDatetimeToolPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| current_datetime | [AppFeaturePayload](#appfeaturepayload) | | Yes |
#### AppLegacyCurrentDatetimeToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| current_datetime | object | | Yes |
#### AppLegacyDatasetSelectionPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enabled | boolean | | No |
| id | string | | No |
#### AppLegacyDatasetToolPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| dataset | [AppLegacyDatasetSelectionPayload](#applegacydatasetselectionpayload) | | Yes |
#### AppLegacyDatasetToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| dataset | [AppDatasetReferenceResponse](#appdatasetreferenceresponse) | | Yes |
#### AppLegacyGoogleSearchToolPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| google_search | [AppFeaturePayload](#appfeaturepayload) | | Yes |
#### AppLegacyGoogleSearchToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| google_search | object | | Yes |
#### AppLegacySensitiveWordConfigPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| canned_response | string | | Yes |
| enabled | boolean | | Yes |
| words | [ string ] | | Yes |
#### AppLegacySensitiveWordToolPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| sensitive-word-avoidance | [AppLegacySensitiveWordConfigPayload](#applegacysensitivewordconfigpayload) | | Yes |
#### AppLegacySensitiveWordToolResponse
| Name | Type | Description | Required |
@@ -16120,12 +16361,24 @@ This class is used to store the schema information of an api based tool.
| ---- | ---- | ----------- | -------- |
| sensitive-word-avoidance | [AppLegacySensitiveWordToolResponse](#applegacysensitivewordtoolresponse) | | Yes |
#### AppLegacyWebReaderToolPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| web_reader | [AppFeaturePayload](#appfeaturepayload) | | Yes |
#### AppLegacyWebReaderToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| web_reader | object | | Yes |
#### AppLegacyWikipediaToolPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| wikipedia | [AppFeaturePayload](#appfeaturepayload) | | Yes |
#### AppLegacyWikipediaToolResponse
| Name | Type | Description | Required |
@@ -16166,6 +16419,16 @@ AppMCPServer Status Enum
| ---- | ---- | ----------- | -------- |
| AppMCPServerStatus | string | AppMCPServer Status Enum | |
#### AppMetadataConditionPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| comparison_operator | string, <br>**Available values:** "<", "=", ">", "after", "before", "contains", "empty", "end with", "in", "is", "is not", "not contains", "not empty", "not in", "start with", "≠", "≤", "≥" | *Enum:* `"<"`, `"="`, `">"`, `"after"`, `"before"`, `"contains"`, `"empty"`, `"end with"`, `"in"`, `"is"`, `"is not"`, `"not contains"`, `"not empty"`, `"not in"`, `"start with"`, `"≠"`, `"≤"`, `"≥"` | Yes |
| id | string | | No |
| metadata_id | string | | No |
| name | string | | Yes |
| value | string<br>[ string ]<br>integer<br>number | | No |
#### AppMetadataConditionResponse
| Name | Type | Description | Required |
@@ -16183,12 +16446,54 @@ AppMCPServer Status Enum
| conditions | [ [AppMetadataConditionResponse](#appmetadataconditionresponse) ] | | No |
| logical_operator | string, <br>**Available values:** "and", "or" | | No |
#### AppMetadataFilteringPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| conditions | [ [AppMetadataConditionPayload](#appmetadataconditionpayload) ] | | No |
| logical_operator | string, <br>**Available values:** "and", "or" | | No |
#### AppMetadataModelPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| completion_params | object | | No |
| mode | [LLMMode](#llmmode)<br>string | | No |
| name | string | | No |
| provider | string | | No |
#### AppMode
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| AppMode | string | | |
#### AppModelConfigPayload
Write transport; app-mode validators own defaults and feature-specific rules.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| agent_mode | [AppAgentModePayload](#appagentmodepayload) | | No |
| chat_prompt_config | [AppChatPromptPayload](#appchatpromptpayload) | | No |
| completion_prompt_config | [AppCompletionPromptPayload](#appcompletionpromptpayload) | | No |
| dataset_configs | [AppDatasetConfigPayload](#appdatasetconfigpayload) | | No |
| dataset_query_variable | string | | No |
| external_data_tools | [ [AppExternalDataToolPayload](#appexternaldatatoolpayload) ] | | No |
| file_upload | [AppFileUploadPayload](#appfileuploadpayload) | | No |
| model | [AppModelSelectionPayload](#appmodelselectionpayload) | | Yes |
| more_like_this | [AppFeaturePayload](#appfeaturepayload) | | No |
| opening_statement | string | | No |
| pre_prompt | string | | No |
| prompt_type | string, <br>**Available values:** "", "advanced", "simple" | | No |
| retriever_resource | [AppFeaturePayload](#appfeaturepayload) | | No |
| sensitive_word_avoidance | [AppModerationPayload](#appmoderationpayload) | | No |
| speech_to_text | [AppFeaturePayload](#appfeaturepayload) | | No |
| suggested_questions | [ string ] | | No |
| suggested_questions_after_answer | [AppSuggestedQuestionsPayload](#appsuggestedquestionspayload) | | No |
| text_to_speech | [AppTextToSpeechPayload](#apptexttospeechpayload) | | No |
| user_input_form | [ [AppUserInputFormPayload](#appuserinputformpayload) ] | | No |
#### AppModelConfigResponse
| Name | Type | Description | Required |
@@ -16218,6 +16523,15 @@ AppMCPServer Status Enum
| updated_by | string | | Yes |
| user_input_form | [ [AppTextInputFormResponse](#apptextinputformresponse)<br>[AppSelectFormResponse](#appselectformresponse)<br>[AppParagraphFormResponse](#appparagraphformresponse)<br>[AppNumberFormResponse](#appnumberformresponse)<br>[AppCheckboxFormResponse](#appcheckboxformresponse)<br>[AppFileFormResponse](#appfileformresponse)<br>[AppFileListFormResponse](#appfilelistformresponse)<br>[AppExternalDataToolFormResponse](#appexternaldatatoolformresponse)<br>[AppJsonObjectFormResponse](#appjsonobjectformresponse) ] | | Yes |
#### AppModelSelectionPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| completion_params | object | | Yes |
| mode | string | | No |
| name | string | | Yes |
| provider | string | | Yes |
#### AppModelSelectionResponse
| Name | Type | Description | Required |
@@ -16227,6 +16541,30 @@ AppMCPServer Status Enum
| name | string | | No |
| provider | string | | No |
#### AppModerationConfigPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| api_based_extension_id | string | | No |
| inputs_config | [AppModerationContentPayload](#appmoderationcontentpayload) | | No |
| keywords | string | | No |
| outputs_config | [AppModerationContentPayload](#appmoderationcontentpayload) | | No |
#### AppModerationContentPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enabled | boolean | | No |
| preset_response | string | | No |
#### AppModerationPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | [AppModerationConfigPayload](#appmoderationconfigpayload) | | No |
| enabled | boolean | | No |
| type | string | | No |
#### AppNamePayload
| Name | Type | Description | Required |
@@ -16239,6 +16577,12 @@ AppMCPServer Status Enum
| ---- | ---- | ----------- | -------- |
| number | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppNumberInputPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| number | [AppInputFieldPayload](#appinputfieldpayload) | | Yes |
#### AppPagination
| Name | Type | Description | Required |
@@ -16255,6 +16599,12 @@ AppMCPServer Status Enum
| ---- | ---- | ----------- | -------- |
| paragraph | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppParagraphInputPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| paragraph | [AppInputFieldPayload](#appinputfieldpayload) | | Yes |
#### AppPartial
| Name | Type | Description | Required |
@@ -16286,6 +16636,29 @@ AppMCPServer Status Enum
| use_icon_as_answer_icon | boolean | | No |
| workflow | [WorkflowPartial](#workflowpartial) | | No |
#### AppPromptMessagePayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| role | string | | Yes |
| text | string | | Yes |
#### AppProviderAgentToolPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| credential_id | string | | No |
| enabled | boolean | | No |
| isDeleted | boolean | | No |
| notAuthor | boolean | | No |
| plugin_unique_identifier | string | | No |
| provider_id | string | | Yes |
| provider_name | string | | No |
| provider_type | [ToolProviderType](#toolprovidertype) | | Yes |
| tool_label | string | | No |
| tool_name | string | | Yes |
| tool_parameters | object | | Yes |
#### AppProviderAgentToolResponse
| Name | Type | Description | Required |
@@ -16302,6 +16675,13 @@ AppMCPServer Status Enum
| tool_name | string | | Yes |
| tool_parameters | object | | Yes |
#### AppRerankingModelPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| reranking_model_name | string | | No |
| reranking_provider_name | string | | No |
#### AppRerankingModelResponse
| Name | Type | Description | Required |
@@ -16309,12 +16689,45 @@ AppMCPServer Status Enum
| reranking_model_name | string | | No |
| reranking_provider_name | string | | No |
#### AppRetrievalWeightsPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| keyword_setting | [AppKeywordWeightPayload](#appkeywordweightpayload) | | Yes |
| vector_setting | [AppVectorWeightPayload](#appvectorweightpayload) | | Yes |
| weight_type | string, <br>**Available values:** "customized", "keyword_first", "semantic_first" | *Enum:* `"customized"`, `"keyword_first"`, `"semantic_first"` | No |
#### AppSelectFieldPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | object | | No |
| default | [AppConfigJsonValue](#appconfigjsonvalue) | | No |
| description | string | | No |
| enabled | boolean | | No |
| hide | boolean | | No |
| icon | string | | No |
| icon_background | string | | No |
| json_schema | string<br>object | | No |
| label | string | | Yes |
| max_length | integer | | No |
| options | [ string ] | | No |
| required | boolean | | No |
| type | string | | No |
| variable | string | | Yes |
#### AppSelectFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| select | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppSelectInputPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| select | [AppSelectFieldPayload](#appselectfieldpayload) | | Yes |
#### AppSelectorScope
| Name | Type | Description | Required |
@@ -16387,12 +16800,44 @@ AppMCPServer Status Enum
| model | [AppModelSelectionResponse](#appmodelselectionresponse) | | No |
| prompt | string | | No |
#### AppSuggestedQuestionsModelPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| completion_params | object | | No |
| mode | [LLMMode](#llmmode)<br>string | | No |
| name | string | | Yes |
| provider | string | | Yes |
#### AppSuggestedQuestionsPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enabled | boolean | | No |
| model | [AppSuggestedQuestionsModelPayload](#appsuggestedquestionsmodelpayload) | | No |
| prompt | string | | No |
#### AppTextInputFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| text-input | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppTextInputPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| text-input | [AppInputFieldPayload](#appinputfieldpayload) | | Yes |
#### AppTextToSpeechPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| autoPlay | string, <br>**Available values:** "disabled", "enabled" | *Enum:* `"disabled"`, `"enabled"` | No |
| enabled | boolean | | No |
| language | string | | No |
| voice | string | | No |
#### AppTextToSpeechResponse
| Name | Type | Description | Required |
@@ -16438,6 +16883,12 @@ AppMCPServer Status Enum
| type | string | | No |
| variable | string | | Yes |
#### AppUserInputFormPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| AppUserInputFormPayload | [AppTextInputPayload](#apptextinputpayload)<br>[AppSelectInputPayload](#appselectinputpayload)<br>[AppParagraphInputPayload](#appparagraphinputpayload)<br>[AppNumberInputPayload](#appnumberinputpayload)<br>[AppCheckboxInputPayload](#appcheckboxinputpayload)<br>[AppExternalDataInputPayload](#appexternaldatainputpayload) | | |
#### AppVariableConfig
| Name | Type | Description | Required |
@@ -16455,6 +16906,14 @@ AppMCPServer Status Enum
| embedding_provider_name | string | | Yes |
| vector_weight | number | | Yes |
#### AppVectorWeightPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| embedding_model_name | string | | Yes |
| embedding_provider_name | string | | Yes |
| vector_weight | number | | Yes |
#### AppWeightsResponse
| Name | Type | Description | Required |
@@ -20097,23 +20556,6 @@ Metadata operation data
| updated_at | integer | | No |
| updated_by | string | | No |
#### ModelConfigRequest
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| agent_mode | object | Agent mode configuration | No |
| configs | object | Model configuration parameters | No |
| dataset_configs | object | Dataset configurations | No |
| model | string | Model name | No |
| more_like_this | object | More like this configuration | No |
| opening_statement | string | Opening statement | No |
| provider | string | Model provider | No |
| retrieval_model | object | Retrieval model configuration | No |
| speech_to_text | object | Speech to text configuration | No |
| suggested_questions | [ string ] | Suggested questions | No |
| text_to_speech | object | Text to speech configuration | No |
| tools | [ object ] | Available tools | No |
#### ModelCredentialResponse
| Name | Type | Description | Required |
@@ -3,23 +3,656 @@ from __future__ import annotations
import importlib
import json
import os
from copy import deepcopy
from dataclasses import dataclass
from inspect import unwrap
from unittest.mock import MagicMock
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import DEFAULT, MagicMock
from uuid import uuid4
import pytest
import yaml
from flask import Flask
from flask.testing import FlaskClient
from flask_restx import Resource
from sqlalchemy import func, select
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session, object_session, sessionmaker
from controllers.common import session as controller_session
from controllers.console.app import model_config as model_config_module
from core.app.app_config.easy_ui_based_app.dataset.manager import DatasetConfigManager
from core.app.app_config.easy_ui_based_app.model_config import manager as model_config_manager_module
from core.app.apps.agent_chat.app_config_manager import AgentChatAppConfigManager
from core.app.apps.chat.app_config_manager import ChatAppConfigManager
from core.app.apps.completion.app_config_manager import CompletionAppConfigManager
from fields.app_model_config_response import AppModelConfigResponse
from libs.external_api import ExternalApi
from models.model import App, AppMode, AppModelConfig
from models.provider_ids import ModelProviderID
app_wraps_module = importlib.import_module("controllers.console.app.wraps")
def _model_config_payload(**fields: object) -> dict[str, object]:
return {
"model": {
"provider": "langgenius/openai/openai",
"name": "gpt-4o-mini",
"completion_params": {},
},
**fields,
}
def _existing_publish_configs() -> list[tuple[str, AppMode, dict[str, object]]]:
recommended_apps_path = Path(__file__).resolve().parents[5] / "constants" / "recommended_apps.json"
recommended_apps = json.loads(recommended_apps_path.read_text())["app_details"]
cases = []
for app_detail in recommended_apps.values():
payload = yaml.safe_load(app_detail["export_data"]).get("model_config")
if payload is not None:
cases.append((app_detail["name"], AppMode.value_of(app_detail["mode"]), payload))
# Mirrors the publish body's advanced-prompt, dataset-selection, and feature fields.
cases.append(
(
"configuration-editor-publish",
AppMode.CHAT,
_model_config_payload(
model={
"provider": "langgenius/openai/openai",
"name": "gpt-4o",
"mode": "chat",
"completion_params": {"temperature": 0.7},
},
pre_prompt="",
prompt_type="advanced",
chat_prompt_config={"prompt": [{"role": "system", "text": "hi"}]},
completion_prompt_config={
"prompt": {"text": "completion"},
"conversation_histories_role": {"assistant_prefix": "assistant", "user_prefix": "user"},
},
user_input_form=[{"text-input": {"label": "Name", "variable": "name", "required": True}}],
dataset_query_variable="context",
dataset_configs={
"retrieval_model": "multiple",
"datasets": {"datasets": [{"dataset": {"enabled": True, "id": "dataset-1"}}]},
},
agent_mode={"enabled": True, "max_iteration": 3, "strategy": "function_call", "tools": []},
external_data_tools=[],
more_like_this={"enabled": True},
opening_statement="hello",
suggested_questions=["how are you?"],
suggested_questions_after_answer={"enabled": False},
speech_to_text={"enabled": False},
text_to_speech={"enabled": False, "voice": "", "language": ""},
retriever_resource={"enabled": True},
sensitive_word_avoidance={"enabled": False},
file_upload={
"enabled": True,
"image": {
"enabled": True,
"detail": "high",
"number_limits": 2,
"transfer_methods": ["local_file"],
},
},
system_parameters={
"audio_file_size_limit": 1,
"file_size_limit": 1,
"image_file_size_limit": 1,
"video_file_size_limit": 1,
"workflow_file_upload_limit": 1,
},
),
)
)
return cases
@dataclass
class _ModelConfigHttp:
client: FlaskClient
app_model: App
session: Session
validate_configuration: MagicMock
signal: MagicMock
@pytest.fixture
def model_config_http(sqlite_session: Session, monkeypatch: pytest.MonkeyPatch) -> _ModelConfigHttp:
app_model = App(
id="app-1",
mode=AppMode.CHAT,
app_model_config_id="config-0",
updated_by=None,
updated_at=None,
)
original_config = AppModelConfig(app_id=app_model.id, created_by="u1", updated_by="u1")
original_config.id = "config-0"
sqlite_session.add(original_config)
sqlite_session.commit()
validate_configuration = MagicMock(
wraps=model_config_module.AppModelConfigService.validate_configuration,
return_value={"pre_prompt": "Validated prompt"},
)
monkeypatch.setattr(model_config_module.AppModelConfigService, "validate_configuration", validate_configuration)
signal = MagicMock()
monkeypatch.setattr(model_config_module.app_model_config_was_updated, "send", signal)
class ModelConfigHttpResource(Resource):
def post(self):
# Admission is supplied by this harness; parsing and HTTP errors use the real controller and API.
return unwrap(model_config_module.ModelConfigResource.post)(
model_config_module.ModelConfigResource(), sqlite_session, "t1", "u1", app_model=app_model
)
http_app = Flask(__name__)
http_app.config.update(TESTING=True)
ExternalApi(http_app).add_resource(ModelConfigHttpResource, "/apps/app-1/model-config")
return _ModelConfigHttp(http_app.test_client(), app_model, sqlite_session, validate_configuration, signal)
@pytest.fixture
def real_model_config_validation(model_config_http: _ModelConfigHttp, monkeypatch: pytest.MonkeyPatch) -> None:
assembly = MagicMock()
assembly.model_provider_factory.get_providers.return_value = [SimpleNamespace(provider="langgenius/openai/openai")]
assembly.provider_manager.get_configurations.return_value.get_models.return_value = [
SimpleNamespace(model="gpt-4o-mini", model_properties={"mode": "chat"})
]
monkeypatch.setattr(model_config_manager_module, "create_plugin_model_assembly", lambda **_kwargs: assembly)
monkeypatch.setattr(DatasetConfigManager, "is_dataset_exists", lambda *_args: True)
monkeypatch.setattr(
model_config_module.ToolManager,
"get_agent_tool_runtime",
MagicMock(side_effect=ValueError("Tool provider is unavailable in this test")),
)
model_config_http.validate_configuration.return_value = DEFAULT
def test_post_preserves_configuration_wire_for_business_validation(model_config_http: _ModelConfigHttp) -> None:
payload = _model_config_payload(
model={
"provider": "langgenius/openai/openai",
"name": "gpt-4o-mini",
"mode": "chat",
"completion_params": {
"temperature": 0,
"stop": [],
"provider_options": {"cache": False, "routing": {"fallback": None}},
},
},
pre_prompt="Answer {{topic}}",
prompt_type="advanced",
chat_prompt_config={"prompt": [{"role": "system", "text": "Answer {{topic}}"}]},
completion_prompt_config={
"prompt": {"text": "{{topic}}"},
"conversation_histories_role": {"user_prefix": "Human", "assistant_prefix": "Assistant"},
},
user_input_form=[
{"text-input": {"label": "Topic", "variable": "topic", "required": False, "default": ""}},
{"select": {"label": "Style", "variable": "style", "options": ["brief", "detailed"], "required": True}},
],
dataset_query_variable="topic",
dataset_configs={
"retrieval_model": "multiple",
"top_k": 3,
"score_threshold": 0,
"score_threshold_enabled": False,
"datasets": {"datasets": [{"dataset": {"id": "dataset-1", "enabled": True}}]},
"weights": {
"vector_setting": {
"vector_weight": 0.7,
"embedding_provider_name": "provider",
"embedding_model_name": "model",
},
"keyword_setting": {"keyword_weight": 0.3},
},
},
agent_mode={
"enabled": True,
"strategy": "function_call",
"max_iteration": 5,
"tools": [
{
"provider_id": "provider",
"provider_type": "builtin",
"tool_name": "search",
"tool_parameters": {
"query": "{{topic}}",
"limit": 0,
"filters": {"archived": False, "tags": ["docs", None, {"score": 0.5}]},
},
}
],
},
file_upload={
"enabled": True,
"number_limits": 3,
"allowed_file_types": ["image", "document"],
"allowed_file_extensions": [".pdf"],
"allowed_file_upload_methods": ["local_file", "remote_url"],
"image": {"enabled": True, "number_limits": 3, "transfer_methods": ["remote_url"]},
},
external_data_tools=[
{
"enabled": True,
"type": "api",
"label": "Context",
"variable": "context",
"config": {"api_based_extension_id": "extension-1", "options": {"limit": 0, "enabled": False}},
}
],
opening_statement="Hello",
suggested_questions=["What can you do?"],
suggested_questions_after_answer={"enabled": True},
more_like_this={"enabled": False},
speech_to_text={"enabled": True},
text_to_speech={"enabled": True, "voice": "alloy", "language": "en-US"},
retriever_resource={"enabled": True},
sensitive_word_avoidance={
"enabled": True,
"type": "keywords",
"config": {"keywords": "blocked", "inputs_config": {"enabled": True, "preset_response": ""}},
},
)
response = model_config_http.client.post("/apps/app-1/model-config", json=payload)
assert response.status_code == 200, response.get_json()
assert response.get_json() == {"result": "success"}
model_config_http.validate_configuration.assert_called_once_with(
tenant_id="t1", config=payload, app_mode=AppMode.CHAT, session=model_config_http.session
)
stored_config = model_config_http.session.get(AppModelConfig, model_config_http.app_model.app_model_config_id)
assert stored_config is not None
assert stored_config.pre_prompt == "Validated prompt"
assert model_config_http.signal.call_args.kwargs["session"] is model_config_http.session
@pytest.mark.parametrize(
"optional_fields",
[
{},
{
"pre_prompt": "",
"opening_statement": "",
"suggested_questions": [],
"chat_prompt_config": None,
"completion_prompt_config": None,
"file_upload": None,
"dataset_configs": {"score_threshold": 0, "score_threshold_enabled": False},
"agent_mode": {"enabled": False, "tools": []},
"speech_to_text": {"enabled": False},
"text_to_speech": {"enabled": False, "voice": ""},
"external_data_tools": [],
},
],
ids=["omitted", "explicit-empty-disabled-and-null"],
)
def test_post_keeps_optional_values_for_domain_defaults(
model_config_http: _ModelConfigHttp, optional_fields: dict[str, object]
) -> None:
payload = _model_config_payload(**optional_fields)
response = model_config_http.client.post("/apps/app-1/model-config", json=payload)
assert response.status_code == 200, response.get_json()
assert model_config_http.validate_configuration.call_args.kwargs["config"] == payload
@pytest.mark.parametrize(("case_name", "app_mode", "payload"), _existing_publish_configs())
def test_post_preserves_existing_publish_results(
model_config_http: _ModelConfigHttp,
monkeypatch: pytest.MonkeyPatch,
case_name: str,
app_mode: AppMode,
payload: dict[str, object],
) -> None:
selected_model = payload["model"]
assert isinstance(selected_model, dict)
assembly = MagicMock()
assembly.model_provider_factory.get_providers.return_value = [
SimpleNamespace(provider=str(ModelProviderID(selected_model["provider"])))
]
assembly.provider_manager.get_configurations.return_value.get_models.return_value = [
SimpleNamespace(model=selected_model["name"], model_properties={"mode": selected_model.get("mode", "chat")})
]
monkeypatch.setattr(model_config_manager_module, "create_plugin_model_assembly", lambda **_kwargs: assembly)
monkeypatch.setattr(
model_config_module.ToolManager,
"get_agent_tool_runtime",
MagicMock(side_effect=ValueError("Tool provider is unavailable in this test")),
)
config_manager = {
AppMode.CHAT: ChatAppConfigManager,
AppMode.COMPLETION: CompletionAppConfigManager,
AppMode.AGENT_CHAT: AgentChatAppConfigManager,
}[app_mode]
expected_config = config_manager.config_validate("t1", deepcopy(payload), model_config_http.session)
expected_record = AppModelConfig(app_id="app-1", created_by="u1", updated_by="u1").from_model_config_dict(
expected_config
)
assert "system_parameters" not in expected_config
assert "annotation_reply" not in expected_config
model_config_http.app_model.mode = app_mode
model_config_http.validate_configuration.return_value = DEFAULT
response = model_config_http.client.post("/apps/app-1/model-config", json=payload)
assert response.status_code == 200, (case_name, response.get_json())
assert response.get_json() == {"result": "success"}
stored_config = model_config_http.session.get(AppModelConfig, model_config_http.app_model.app_model_config_id)
assert stored_config is not None
assert stored_config.to_dict(annotation_reply={"enabled": False}) == expected_record.to_dict(
annotation_reply={"enabled": False}
), case_name
assert model_config_http.signal.call_args.kwargs["session"] is model_config_http.session
@pytest.mark.parametrize("app_mode", [AppMode.CHAT, AppMode.COMPLETION, AppMode.AGENT_CHAT])
@pytest.mark.usefixtures("real_model_config_validation")
@pytest.mark.parametrize(
("fields", "stored_path", "expected_value"),
[
(
{
"chat_prompt_config": {"prompt": [{"text": "Chat", "role": "system"}]},
"completion_prompt_config": {"prompt": {"text": "Completion"}},
},
("completion_prompt_config", "prompt"),
{"text": "Completion"},
),
(
{"text_to_speech": {}, "suggested_questions_after_answer": {}},
("text_to_speech",),
{"enabled": False, "voice": "", "language": ""},
),
(
{
"user_input_form": [
{
"text-input": {
"label": "Topic",
"variable": "topic",
"max_length": None,
"json_schema": None,
"icon": None,
"extension": {"values": [None, False, 0]},
}
}
]
},
("user_input_form",),
[
{
"text-input": {
"label": "Topic",
"variable": "topic",
"max_length": None,
"json_schema": None,
"icon": None,
"required": False,
"extension": {"values": [None, False, 0]},
}
}
],
),
(
{"user_input_form": [{"select": {"label": "Topic", "variable": "topic", "options": None}}]},
("user_input_form",),
[{"select": {"label": "Topic", "variable": "topic", "options": [], "required": False}}],
),
(
{"dataset_configs": {"datasets": {}}},
("dataset_configs", "datasets"),
{"strategy": "router", "datasets": []},
),
(
{"file_upload": {"image_config": {"enabled": None}}},
("file_upload",),
{"image_config": {"enabled": None}},
),
(
{
"agent_mode": {
"strategy": "react",
"tools": [
{
"enabled": None,
"provider_type": "builtin",
"provider_id": "time",
"tool_name": "time",
"tool_parameters": {},
"credential_id": None,
"plugin_unique_identifier": None,
}
],
"prompt": None,
}
},
("agent_mode", "tools"),
[
{
"provider_type": "builtin",
"provider_id": "time",
"tool_name": "time",
"tool_parameters": {},
"credential_id": None,
"plugin_unique_identifier": None,
"enabled": False,
}
],
),
],
ids=[
"completion-prompt-without-role",
"omitted-feature-options",
"nullable-input-values-and-json-extensions",
"select-null-options",
"empty-dataset-collection",
"image-config-null-extension",
"nullable-agent-credentials-and-prompt",
],
)
def test_post_persists_readable_configuration_variants(
model_config_http: _ModelConfigHttp,
app_mode: AppMode,
fields: dict[str, object],
stored_path: tuple[str, ...],
expected_value: object,
) -> None:
model_config_http.app_model.mode = app_mode
response = model_config_http.client.post("/apps/app-1/model-config", json=_model_config_payload(**deepcopy(fields)))
assert response.status_code == 200, response.get_json()
stored_config = model_config_http.session.get(AppModelConfig, model_config_http.app_model.app_model_config_id)
assert stored_config is not None
wire = stored_config.to_dict(annotation_reply={"enabled": False})
AppModelConfigResponse.model_validate(
{
**wire,
"created_by": stored_config.created_by,
"created_at": stored_config.created_at,
"updated_by": stored_config.updated_by,
"updated_at": stored_config.updated_at,
}
)
actual: object = wire
for key in stored_path:
assert isinstance(actual, dict)
actual = actual[key]
assert actual == expected_value
@pytest.mark.parametrize("app_mode", [AppMode.CHAT, AppMode.COMPLETION])
@pytest.mark.parametrize("strategy", ["router", "react"])
@pytest.mark.usefixtures("real_model_config_validation")
def test_post_keeps_legacy_tools_readable_across_planning_strategies(
model_config_http: _ModelConfigHttp, app_mode: AppMode, strategy: str
) -> None:
model_config_http.app_model.mode = app_mode
tools = [
{
"extension": {"values": [None, False]},
"dataset": {"id": "00000000-0000-0000-0000-000000000001", "enabled": None},
},
{"google_search": {"enabled": None}},
{"web_reader": {}},
{"wikipedia": {"enabled": False}},
{"current_datetime": {"enabled": True}},
{"sensitive-word-avoidance": {"enabled": False, "words": [], "canned_response": ""}},
]
response = model_config_http.client.post(
"/apps/app-1/model-config", json=_model_config_payload(agent_mode={"strategy": strategy, "tools": tools})
)
assert response.status_code == 200, response.get_json()
stored_config = model_config_http.session.get(AppModelConfig, model_config_http.app_model.app_model_config_id)
assert stored_config is not None
AppModelConfigResponse.model_validate(
{
**stored_config.to_dict(annotation_reply={"enabled": False}),
"created_by": stored_config.created_by,
"created_at": stored_config.created_at,
"updated_by": stored_config.updated_by,
"updated_at": stored_config.updated_at,
}
)
assert stored_config.agent_mode_dict["tools"] == [
{
"dataset": {"id": "00000000-0000-0000-0000-000000000001", "enabled": False},
"extension": {"values": [None, False]},
},
{"google_search": {"enabled": False}},
{"web_reader": {"enabled": False}},
{"wikipedia": {"enabled": False}},
{"current_datetime": {"enabled": True}},
{"sensitive-word-avoidance": {"enabled": False, "words": [], "canned_response": ""}},
]
@pytest.mark.parametrize(
"invalid_fields",
[
{"model": "gpt-4o-mini"},
{"model": {"provider": "langgenius/openai/openai", "name": "gpt-4o-mini", "completion_params": []}},
{"user_input_form": {"text-input": {"variable": "topic"}}},
{"file_upload": []},
{"chat_prompt_config": {"prompt": [{"role": "system", "text": {"invalid": "prompt"}}]}},
{"completion_prompt_config": {"conversation_histories_role": {"user_prefix": []}}},
{"user_input_form": [{"select": {"label": "Style", "variable": "style", "options": {"brief": True}}}]},
{"dataset_configs": {"weights": {"vector_setting": {"vector_weight": {"invalid": 0.7}}}}},
{
"agent_mode": {
"tools": [
{
"provider_id": "provider",
"provider_type": "builtin",
"tool_name": "search",
"tool_parameters": [],
}
]
}
},
{"file_upload": {"image": {"transfer_methods": "remote_url"}}},
{"speech_to_text": {"enabled": {"invalid": True}}},
{"external_data_tools": [{"enabled": True, "type": "api", "config": []}]},
{"chat_prompt_config": {"prompt": [{"text": "Missing role"}]}},
{"chat_prompt_config": {"prompt": None}},
{"completion_prompt_config": {"conversation_histories_role": {"user_prefix": "Human"}}},
{"text_to_speech": {"voice": None}},
{"suggested_questions_after_answer": {"model": None}},
{
"suggested_questions_after_answer": {
"model": {"provider": "provider", "name": "model", "completion_params": None}
}
},
{"file_upload": {"allowed_file_types": None}},
{"user_input_form": [{"text-input": {"label": "Topic", "variable": "topic", "options": None}}]},
{"dataset_configs": {"datasets": {"datasets": [{"dataset": {"enabled": None}}]}}},
{"file_upload": {"image": {"enabled": None}}},
{"dataset_configs": {"datasets": {"strategy": "router"}}},
{"dataset_configs": {"weights": {"vector_setting": {"vector_weight": 0.7}}}},
{"dataset_configs": {"metadata_filtering_conditions": {"conditions": [{"comparison_operator": "is"}]}}},
{"dataset_configs": {"retrieval_model": "unsupported"}},
{
"dataset_configs": {
"metadata_filtering_conditions": {
"conditions": [{"name": "topic", "comparison_operator": "unsupported"}]
}
}
},
{"suggested_questions_after_answer": {"model": {"provider": "p", "name": "m", "mode": "unsupported"}}},
{"agent_mode": {"tools": [{}]}},
{"agent_mode": {"tools": [{"enabled": False}]}},
{"agent_mode": {"tools": [{"provider_type": "builtin", "provider_id": "time", "tool_name": "time"}]}},
{"agent_mode": {"tools": [{"dataset": None}]}},
],
ids=[
"model-string",
"completion-params-list",
"form-object",
"file-upload-list",
"chat-prompt-text-object",
"completion-prefix-list",
"form-options-object",
"dataset-weight-object",
"agent-tool-parameters-list",
"image-transfer-methods-string",
"feature-enabled-object",
"external-tool-config-list",
"chat-prompt-role-missing",
"chat-prompt-null",
"completion-assistant-prefix-missing",
"text-to-speech-voice-null",
"suggested-model-null",
"suggested-completion-params-null",
"allowed-file-types-null",
"text-input-options-null",
"modern-dataset-enabled-null",
"legacy-image-enabled-null",
"dataset-list-missing",
"retrieval-weights-incomplete",
"metadata-condition-name-missing",
"retrieval-mode-unsupported",
"metadata-comparison-unsupported",
"suggested-model-mode-unsupported",
"empty-agent-tool",
"agent-tool-only-enabled",
"provider-tool-parameters-missing",
"legacy-dataset-null",
],
)
def test_post_rejects_invalid_wire_before_business_validation(
model_config_http: _ModelConfigHttp, invalid_fields: dict[str, object]
) -> None:
response = model_config_http.client.post("/apps/app-1/model-config", json=_model_config_payload(**invalid_fields))
assert response.status_code == 400, response.get_json()
assert response.get_json()["code"] == "invalid_param"
model_config_http.validate_configuration.assert_not_called()
model_config_http.signal.assert_not_called()
assert model_config_http.app_model.app_model_config_id == "config-0"
assert model_config_http.session.scalar(select(func.count()).select_from(AppModelConfig)) == 1
def test_post_keeps_business_validation_as_the_configuration_policy_owner(model_config_http: _ModelConfigHttp) -> None:
model_config_http.validate_configuration.side_effect = ValueError("model.name must be in the specified model list")
response = model_config_http.client.post("/apps/app-1/model-config", json=_model_config_payload())
assert response.status_code == 400, response.get_json()
assert response.get_json()["message"] == "model.name must be in the specified model list"
model_config_http.signal.assert_not_called()
assert model_config_http.app_model.app_model_config_id == "config-0"
assert model_config_http.session.scalar(select(func.count()).select_from(AppModelConfig)) == 1
def _assert_no_implicit_app_config_properties() -> None:
# `App.is_agent` and `App.app_model_config` used to be poisoned here with a
# raising property. Neither exists as an implicit accessor any more, so their
@@ -69,7 +702,9 @@ def test_post_updates_non_agent_model_config_without_implicit_properties(
send_mock = MagicMock()
monkeypatch.setattr(model_config_module.app_model_config_was_updated, "send", send_mock)
with app.test_request_context("/console/api/apps/app-1/model-config", method="POST", json={"pre_prompt": "hi"}):
with app.test_request_context(
"/console/api/apps/app-1/model-config", method="POST", json=_model_config_payload(pre_prompt="hi")
):
response = method(api, sqlite_session, "t1", "u1", app_model=app_model)
assert send_mock.call_args.kwargs["session"] is sqlite_session
@@ -150,7 +785,9 @@ def test_post_uses_one_session_and_rolls_back_when_signal_fails(
api = model_config_module.ModelConfigResource()
with (
app.test_request_context(f"/console/api/apps/{app_id}/model-config", method="POST", json={}),
app.test_request_context(
f"/console/api/apps/{app_id}/model-config", method="POST", json=_model_config_payload()
),
pytest.raises(RuntimeError, match="signal failed"),
):
method(
@@ -248,7 +885,9 @@ def test_post_encrypts_agent_tool_parameters(
send_mock = MagicMock()
monkeypatch.setattr(model_config_module.app_model_config_was_updated, "send", send_mock)
with app.test_request_context("/console/api/apps/app-1/model-config", method="POST", json={"pre_prompt": "hi"}):
with app.test_request_context(
"/console/api/apps/app-1/model-config", method="POST", json=_model_config_payload(pre_prompt="hi")
):
response = method(api, sqlite_session, "t1", "u1", app_model=app_model)
stored_config = sqlite_session.get(AppModelConfig, app_model.app_model_config_id)
+170
View File
@@ -0,0 +1,170 @@
import type { InferContractRouterInputs } from '@orpc/contract'
import type { z } from 'zod'
import type { apps } from './generated/api/console/apps/orpc.gen.ts'
import type { AppModelConfigPayload } from './generated/api/console/apps/types.gen.ts'
import { describe, expect, it } from 'vite-plus/test'
import {
zAppConfigJsonValueWritable,
zAppModelConfigPayload,
} from './generated/api/console/apps/zod.gen.ts'
const model = {
provider: 'langgenius/openai/openai',
name: 'gpt-4o-mini',
completion_params: {
temperature: 0,
stop: [],
provider_options: { cache: false, fallback: null },
},
} satisfies AppModelConfigPayload['model']
describe('generated app model-config write contract', () => {
it('keeps recursive JSON strict in the generated writable schema', () => {
const value = { nested: [null, false, { count: 2 }] }
expect(zAppConfigJsonValueWritable.parse(value)).toEqual(value)
// @ts-expect-error The derived writable schema must reject non-JSON values at any depth.
const invalid: z.infer<typeof zAppConfigJsonValueWritable> = {
nested: { callback: () => true },
}
expect(zAppConfigJsonValueWritable.safeParse(invalid).success).toBe(false)
})
it('preserves dynamic JSON parameters without synthesizing omitted configuration', () => {
const body = {
model: { ...model, provider_metadata: { cached: false } },
speech_to_text: { enabled: false, extension: { locale: 'en' } },
} satisfies AppModelConfigPayload
expect(zAppModelConfigPayload.parse(body)).toEqual(body)
})
it('requires one supported form field per input item', () => {
const field = { label: 'Topic', variable: 'topic' }
for (const item of [{}, { 'text-input': field, select: field }]) {
expect(zAppModelConfigPayload.safeParse({ model, user_input_form: [item] }).success).toBe(
false,
)
}
})
it('preserves omitted and nullable values according to their configuration variant', () => {
const body = {
model,
completion_prompt_config: { prompt: { text: 'Completion without a role' } },
text_to_speech: {},
dataset_configs: { datasets: {} },
agent_mode: { tools: [{ dataset: { enabled: null } }] },
file_upload: { image_config: { enabled: null } },
user_input_form: [
{
'text-input': {
label: 'Topic',
variable: 'topic',
max_length: null,
json_schema: null,
extension: { values: [false, null, 0] },
},
},
{ select: { label: 'Choice', variable: 'choice', options: null } },
],
} satisfies AppModelConfigPayload
expect(zAppModelConfigPayload.parse(body)).toEqual(body)
})
it('rejects structures that cannot be persisted as readable configuration', () => {
const invalidVoice: AppModelConfigPayload = {
model,
// @ts-expect-error Omission preserves the domain default; an explicit null is invalid.
text_to_speech: { voice: null },
}
const missingRole: AppModelConfigPayload = {
model,
// @ts-expect-error A chat message requires its role, unlike a completion prompt.
chat_prompt_config: { prompt: [{ text: 'Missing role' }] },
}
const invalidClientVoice: InferContractRouterInputs<
typeof apps
>['byAppId']['modelConfig']['post'] = {
params: { app_id: 'app-1' },
body: {
model,
// @ts-expect-error The Zod-derived client input also distinguishes omitted from null.
text_to_speech: { voice: null },
},
}
for (const invalidBody of [
invalidVoice,
missingRole,
invalidClientVoice.body,
{ model, dataset_configs: { datasets: { strategy: 'router' } } },
{ model, dataset_configs: { datasets: { datasets: [{ dataset: { enabled: null } }] } } },
{ model, dataset_configs: { retrieval_model: 'unsupported' } },
{ model, file_upload: { image: { enabled: null } } },
{
model,
user_input_form: [{ 'text-input': { label: 'Topic', variable: 'topic', options: null } }],
},
]) {
expect(zAppModelConfigPayload.safeParse(invalidBody).success).toBe(false)
}
})
it('types and validates the fixed configuration fields', () => {
const body = {
model,
file_upload: { enabled: false, number_limits: 3 },
dataset_configs: { retrieval_model: 'multiple', top_k: 4 },
chat_prompt_config: { prompt: [{ role: 'system', text: 'Answer {{topic}}' }] },
user_input_form: [{ 'text-input': { label: 'Topic', variable: 'topic', required: false } }],
} satisfies AppModelConfigPayload
expect(zAppModelConfigPayload.parse(body)).toEqual(body)
const invalidUpload: AppModelConfigPayload = {
model,
// @ts-expect-error Upload enablement is a boolean in the generated write contract.
file_upload: { enabled: 'false' },
}
const invalidDataset: AppModelConfigPayload = {
model,
// @ts-expect-error The retrieval limit is numeric in the generated write contract.
dataset_configs: { top_k: '4' },
}
const invalidPrompt: AppModelConfigPayload = {
model,
// @ts-expect-error Prompt text is a string in the generated write contract.
chat_prompt_config: { prompt: [{ role: 'system', text: 1 }] },
}
const invalidParameters: AppModelConfigPayload = {
model: {
...model,
// @ts-expect-error Provider parameters remain JSON values, not arbitrary JavaScript values.
completion_params: { callback: () => 'not JSON' },
},
}
const invalidClientParameters: InferContractRouterInputs<
typeof apps
>['byAppId']['modelConfig']['post'] = {
params: { app_id: 'app-1' },
body: {
model: {
...model,
// @ts-expect-error Recursive JSON stays typed in the Zod-backed client input too.
completion_params: { nested: { callback: () => 'not JSON' } },
},
},
}
for (const invalidBody of [
invalidUpload,
invalidDataset,
invalidPrompt,
invalidParameters,
invalidClientParameters.body,
]) {
expect(zAppModelConfigPayload.safeParse(invalidBody).success).toBe(false)
}
})
})
@@ -537,35 +537,26 @@ export type MessageDetailResponse = {
workflow_run_id?: string | null
}
export type ModelConfigRequest = {
agent_mode?: {
[key: string]: unknown
} | null
configs?: {
[key: string]: unknown
} | null
dataset_configs?: {
[key: string]: unknown
} | null
model?: string | null
more_like_this?: {
[key: string]: unknown
} | null
export type AppModelConfigPayload = {
agent_mode?: AppAgentModePayload | null
chat_prompt_config?: AppChatPromptPayload | null
completion_prompt_config?: AppCompletionPromptPayload | null
dataset_configs?: AppDatasetConfigPayload | null
dataset_query_variable?: string | null
external_data_tools?: Array<AppExternalDataToolPayload> | null
file_upload?: AppFileUploadPayload | null
model: AppModelSelectionPayload
more_like_this?: AppFeaturePayload | null
opening_statement?: string | null
provider?: string | null
retrieval_model?: {
[key: string]: unknown
} | null
speech_to_text?: {
[key: string]: unknown
} | null
pre_prompt?: string | null
prompt_type?: '' | 'advanced' | 'simple' | null
retriever_resource?: AppFeaturePayload | null
sensitive_word_avoidance?: AppModerationPayload | null
speech_to_text?: AppFeaturePayload | null
suggested_questions?: Array<string> | null
text_to_speech?: {
[key: string]: unknown
} | null
tools?: Array<{
[key: string]: unknown
}> | null
suggested_questions_after_answer?: AppSuggestedQuestionsPayload | null
text_to_speech?: AppTextToSpeechPayload | null
user_input_form?: Array<AppUserInputFormPayload> | null
}
export type AppNamePayload = {
@@ -1620,6 +1611,200 @@ export type MessageFile = {
url?: string | null
}
export type AppAgentModePayload = {
enabled?: boolean | null
max_iteration?: number
prompt?: AppAgentPromptPayload | string | null
strategy?: PlanningStrategy | '' | 'cot' | 'function-calling' | null
tools?: Array<AppAgentToolPayload> | null
[key: string]:
| AppConfigJsonValue
| boolean
| null
| number
| AppAgentPromptPayload
| string
| null
| PlanningStrategy
| ''
| 'cot'
| 'function-calling'
| null
| Array<AppAgentToolPayload>
| null
| undefined
}
export type AppChatPromptPayload = {
prompt?: Array<AppPromptMessagePayload>
[key: string]: AppConfigJsonValue | Array<AppPromptMessagePayload> | undefined
}
export type AppCompletionPromptPayload = {
conversation_histories_role?: AppConversationRolesPayload
prompt?: AppCompletionPromptTextPayload
[key: string]:
| AppConfigJsonValue
| AppConversationRolesPayload
| AppCompletionPromptTextPayload
| undefined
}
export type AppDatasetConfigPayload = {
datasets?: AppDatasetCollectionPayload | AppEmptyDatasetCollectionPayload | null
metadata_filtering_conditions?: AppMetadataFilteringPayload | null
metadata_filtering_mode?: 'automatic' | 'disabled' | 'manual'
metadata_model_config?: AppMetadataModelPayload | null
reranking_enable?: boolean
reranking_enabled?: boolean
reranking_mode?: RerankMode
reranking_model?: AppRerankingModelPayload | null
retrieval_model?: 'multiple' | 'single'
score_threshold?: number | null
score_threshold_enabled?: boolean
top_k?: number
weights?: AppRetrievalWeightsPayload | null
[key: string]:
| AppConfigJsonValue
| AppDatasetCollectionPayload
| AppEmptyDatasetCollectionPayload
| null
| AppMetadataFilteringPayload
| null
| 'automatic'
| 'disabled'
| 'manual'
| AppMetadataModelPayload
| null
| boolean
| RerankMode
| AppRerankingModelPayload
| null
| 'multiple'
| 'single'
| number
| null
| number
| AppRetrievalWeightsPayload
| null
| undefined
}
export type AppExternalDataToolPayload = {
config?: {
[key: string]: AppConfigJsonValue
}
enabled?: boolean | null
icon?: string
icon_background?: string
label?: string
type?: string
variable?: string
[key: string]:
| AppConfigJsonValue
| {
[key: string]: AppConfigJsonValue
}
| boolean
| null
| string
| undefined
}
export type AppFileUploadPayload = {
allowed_file_extensions?: Array<string>
allowed_file_types?: Array<FileType>
allowed_file_upload_methods?: Array<FileTransferMethod>
audio?: AppFileTypeUploadPayload | null
custom?: AppFileTypeUploadPayload | null
document?: AppFileTypeUploadPayload | null
enabled?: boolean
image?: AppImageUploadPayload
image_config?: AppImageConfigPayload | null
number_limits?: number
preview_config?: AppFilePreviewPayload | null
video?: AppFileTypeUploadPayload | null
[key: string]:
| AppConfigJsonValue
| Array<string>
| Array<FileType>
| Array<FileTransferMethod>
| AppFileTypeUploadPayload
| null
| AppFileTypeUploadPayload
| null
| AppFileTypeUploadPayload
| null
| boolean
| AppImageUploadPayload
| AppImageConfigPayload
| null
| number
| AppFilePreviewPayload
| null
| AppFileTypeUploadPayload
| null
| undefined
}
export type AppModelSelectionPayload = {
completion_params: {
[key: string]: AppConfigJsonValue
}
mode?: string | null
name: string
provider: string
[key: string]:
| AppConfigJsonValue
| {
[key: string]: AppConfigJsonValue
}
| string
| null
| string
| undefined
}
export type AppFeaturePayload = {
enabled?: boolean | null
[key: string]: AppConfigJsonValue | boolean | null | undefined
}
export type AppModerationPayload = {
config?: AppModerationConfigPayload | null
enabled?: boolean | null
type?: string | null
}
export type AppSuggestedQuestionsPayload = {
enabled?: boolean | null
model?: AppSuggestedQuestionsModelPayload
prompt?: string
[key: string]:
| AppConfigJsonValue
| boolean
| null
| AppSuggestedQuestionsModelPayload
| string
| undefined
}
export type AppTextToSpeechPayload = {
autoPlay?: 'disabled' | 'enabled'
enabled?: boolean | null
language?: string
voice?: string
[key: string]: AppConfigJsonValue | 'disabled' | 'enabled' | boolean | null | string | undefined
}
export type AppUserInputFormPayload =
| AppTextInputPayload
| AppSelectInputPayload
| AppParagraphInputPayload
| AppNumberInputPayload
| AppCheckboxInputPayload
| AppExternalDataInputPayload
export type AppMcpServerStatus = 'active' | 'inactive' | 'normal'
export type AverageResponseTimeStatisticItem = {
@@ -2378,6 +2563,229 @@ export type HumanInputFormSubmissionData = {
export type ExecutionContentType = 'human_input'
export type AppConfigJsonValue =
| string
| number
| number
| boolean
| Array<AppConfigJsonValue>
| {
[key: string]: AppConfigJsonValue
}
| null
export type AppAgentPromptPayload = {
first_prompt?: string
next_iteration?: string
[key: string]: AppConfigJsonValue | string | undefined
}
export type PlanningStrategy = 'function_call' | 'react' | 'react_router' | 'router'
export type AppAgentToolPayload =
| AppProviderAgentToolPayload
| AppLegacyDatasetToolPayload
| AppLegacyGoogleSearchToolPayload
| AppLegacyWebReaderToolPayload
| AppLegacyWikipediaToolPayload
| AppLegacyCurrentDatetimeToolPayload
| AppLegacySensitiveWordToolPayload
export type AppPromptMessagePayload = {
role: string
text: string
[key: string]: AppConfigJsonValue | string
}
export type AppConversationRolesPayload = {
assistant_prefix: string
user_prefix: string
[key: string]: AppConfigJsonValue | string
}
export type AppCompletionPromptTextPayload = {
text: string
[key: string]: AppConfigJsonValue | string
}
export type AppDatasetCollectionPayload = {
datasets: Array<AppDatasetToolPayload>
strategy?: string
[key: string]: AppConfigJsonValue | Array<AppDatasetToolPayload> | string | undefined
}
export type AppEmptyDatasetCollectionPayload = {
[key: string]: never
}
export type AppMetadataFilteringPayload = {
conditions?: Array<AppMetadataConditionPayload> | null
logical_operator?: 'and' | 'or' | null
[key: string]:
| AppConfigJsonValue
| Array<AppMetadataConditionPayload>
| null
| 'and'
| 'or'
| null
| undefined
}
export type AppMetadataModelPayload = {
completion_params?: {
[key: string]: AppConfigJsonValue
}
mode?: LlmMode | ''
name?: string
provider?: string
[key: string]:
| AppConfigJsonValue
| {
[key: string]: AppConfigJsonValue
}
| LlmMode
| ''
| string
| undefined
}
export type RerankMode = 'reranking_model' | 'weighted_score'
export type AppRerankingModelPayload = {
reranking_model_name?: string
reranking_provider_name?: string
[key: string]: AppConfigJsonValue | string | undefined
}
export type AppRetrievalWeightsPayload = {
keyword_setting: AppKeywordWeightPayload
vector_setting: AppVectorWeightPayload
weight_type?: 'customized' | 'keyword_first' | 'semantic_first'
[key: string]:
| AppConfigJsonValue
| AppKeywordWeightPayload
| AppVectorWeightPayload
| 'customized'
| 'keyword_first'
| 'semantic_first'
| undefined
}
export type FileType = 'audio' | 'custom' | 'document' | 'image' | 'video'
export type FileTransferMethod = 'datasource_file' | 'local_file' | 'remote_url' | 'tool_file'
export type AppFileTypeUploadPayload = {
enabled?: boolean | null
number_limits?: number | null
transfer_methods?: Array<FileTransferMethod> | null
[key: string]:
| AppConfigJsonValue
| boolean
| null
| number
| null
| Array<FileTransferMethod>
| null
| undefined
}
export type AppImageUploadPayload = {
detail?: 'high' | 'low' | null
enabled?: boolean
number_limits?: number
transfer_methods?: Array<FileTransferMethod>
[key: string]:
| AppConfigJsonValue
| 'high'
| 'low'
| null
| boolean
| number
| Array<FileTransferMethod>
| undefined
}
export type AppImageConfigPayload = {
detail?: 'high' | 'low' | null
number_limits?: number
transfer_methods?: Array<FileTransferMethod>
[key: string]:
| AppConfigJsonValue
| 'high'
| 'low'
| null
| number
| Array<FileTransferMethod>
| undefined
}
export type AppFilePreviewPayload = {
file_type_list?: Array<string> | null
mode?: string | null
[key: string]: AppConfigJsonValue | Array<string> | null | string | null | undefined
}
export type AppModerationConfigPayload = {
api_based_extension_id?: string | null
inputs_config?: AppModerationContentPayload | null
keywords?: string | null
outputs_config?: AppModerationContentPayload | null
[key: string]:
| AppConfigJsonValue
| string
| null
| AppModerationContentPayload
| null
| string
| null
| AppModerationContentPayload
| null
| undefined
}
export type AppSuggestedQuestionsModelPayload = {
completion_params?: {
[key: string]: AppConfigJsonValue
}
mode?: LlmMode | ''
name: string
provider: string
[key: string]:
| AppConfigJsonValue
| {
[key: string]: AppConfigJsonValue
}
| LlmMode
| ''
| string
| undefined
}
export type AppTextInputPayload = {
'text-input': AppInputFieldPayload
}
export type AppSelectInputPayload = {
select: AppSelectFieldPayload
}
export type AppParagraphInputPayload = {
paragraph: AppInputFieldPayload
}
export type AppNumberInputPayload = {
number: AppInputFieldPayload
}
export type AppCheckboxInputPayload = {
checkbox: AppInputFieldPayload
}
export type AppExternalDataInputPayload = {
external_data_tool: AppInputFieldPayload
}
export type WorkflowRunForLogResponse = {
created_at?: number | null
elapsed_time?: number | null
@@ -2674,8 +3082,6 @@ export type AppAgentPromptResponse = {
[key: string]: unknown
}
export type PlanningStrategy = 'function_call' | 'react' | 'react_router' | 'router'
export type AppProviderAgentToolResponse = {
credential_id?: string | null
enabled?: boolean
@@ -2762,8 +3168,6 @@ export type AppMetadataFilteringConditionsResponse = {
[key: string]: unknown
}
export type RerankMode = 'reranking_model' | 'weighted_score'
export type AppRerankingModelResponse = {
reranking_model_name?: string
reranking_provider_name?: string
@@ -2779,10 +3183,6 @@ export type AppWeightsResponse = {
export type JsonValue2 = unknown
export type FileType = 'audio' | 'custom' | 'document' | 'image' | 'video'
export type FileTransferMethod = 'datasource_file' | 'local_file' | 'remote_url' | 'tool_file'
export type AppImageUploadResponse = {
detail?: 'high' | 'low' | null
enabled?: boolean
@@ -2838,6 +3238,236 @@ export type FormInputConfig =
type: 'file-list'
} & FileListInputConfig)
export type AppProviderAgentToolPayload = {
credential_id?: string | null
enabled?: boolean | null
isDeleted?: boolean
notAuthor?: boolean
plugin_unique_identifier?: string | null
provider_id: string
provider_name?: string
provider_type: ToolProviderType
tool_label?: string
tool_name: string
tool_parameters: {
[key: string]: AppConfigJsonValue
}
[key: string]:
| AppConfigJsonValue
| string
| null
| boolean
| null
| boolean
| string
| null
| string
| ToolProviderType
| {
[key: string]: AppConfigJsonValue
}
| undefined
}
export type AppLegacyDatasetToolPayload = {
dataset: AppLegacyDatasetSelectionPayload
[key: string]: AppConfigJsonValue | AppLegacyDatasetSelectionPayload
}
export type AppLegacyGoogleSearchToolPayload = {
google_search: AppFeaturePayload
[key: string]: AppConfigJsonValue | AppFeaturePayload
}
export type AppLegacyWebReaderToolPayload = {
web_reader: AppFeaturePayload
[key: string]: AppConfigJsonValue | AppFeaturePayload
}
export type AppLegacyWikipediaToolPayload = {
wikipedia: AppFeaturePayload
[key: string]: AppConfigJsonValue | AppFeaturePayload
}
export type AppLegacyCurrentDatetimeToolPayload = {
current_datetime: AppFeaturePayload
[key: string]: AppConfigJsonValue | AppFeaturePayload
}
export type AppLegacySensitiveWordToolPayload = {
'sensitive-word-avoidance': AppLegacySensitiveWordConfigPayload
[key: string]: AppConfigJsonValue | AppLegacySensitiveWordConfigPayload
}
export type AppDatasetToolPayload = {
dataset: AppDatasetSelectionPayload
[key: string]: AppConfigJsonValue | AppDatasetSelectionPayload
}
export type AppMetadataConditionPayload = {
comparison_operator:
| '<'
| '='
| '>'
| 'after'
| 'before'
| 'contains'
| 'empty'
| 'end with'
| 'in'
| 'is'
| 'is not'
| 'not contains'
| 'not empty'
| 'not in'
| 'start with'
| '≠'
| '≤'
| '≥'
id?: string
metadata_id?: string
name: string
value?: string | Array<string> | number | number | null
[key: string]:
| AppConfigJsonValue
| '<'
| '='
| '>'
| 'after'
| 'before'
| 'contains'
| 'empty'
| 'end with'
| 'in'
| 'is'
| 'is not'
| 'not contains'
| 'not empty'
| 'not in'
| 'start with'
| '≠'
| '≤'
| '≥'
| string
| string
| Array<string>
| number
| number
| null
| undefined
}
export type AppKeywordWeightPayload = {
keyword_weight: number
[key: string]: AppConfigJsonValue | number
}
export type AppVectorWeightPayload = {
embedding_model_name: string
embedding_provider_name: string
vector_weight: number
[key: string]: AppConfigJsonValue | string | number
}
export type AppModerationContentPayload = {
enabled?: boolean | null
preset_response?: string | null
[key: string]: AppConfigJsonValue | boolean | null | string | null | undefined
}
export type AppInputFieldPayload = {
config?: {
[key: string]: AppConfigJsonValue
}
default?: AppConfigJsonValue
description?: string
enabled?: boolean
hide?: boolean
icon?: string | null
icon_background?: string | null
json_schema?:
| string
| {
[key: string]: AppConfigJsonValue
}
| null
label: string
max_length?: number | null
options?: Array<string>
required?: boolean | null
type?: string
variable: string
[key: string]:
| AppConfigJsonValue
| {
[key: string]: AppConfigJsonValue
}
| string
| boolean
| string
| null
| string
| null
| string
| {
[key: string]: AppConfigJsonValue
}
| null
| number
| null
| Array<string>
| boolean
| null
| undefined
}
export type AppSelectFieldPayload = {
config?: {
[key: string]: AppConfigJsonValue
}
default?: AppConfigJsonValue
description?: string
enabled?: boolean
hide?: boolean
icon?: string | null
icon_background?: string | null
json_schema?:
| string
| {
[key: string]: AppConfigJsonValue
}
| null
label: string
max_length?: number | null
options?: Array<string> | null
required?: boolean | null
type?: string
variable: string
[key: string]:
| AppConfigJsonValue
| {
[key: string]: AppConfigJsonValue
}
| string
| boolean
| string
| null
| string
| null
| string
| {
[key: string]: AppConfigJsonValue
}
| null
| number
| null
| Array<string>
| null
| boolean
| null
| undefined
}
export type WorkflowFileUploadTransferPayload = {
enabled?: boolean | null
number_limits?: number | null
@@ -3172,6 +3802,25 @@ export type FileListInputConfig = {
type?: 'file-list'
}
export type AppLegacyDatasetSelectionPayload = {
enabled?: boolean | null
id?: string
[key: string]: AppConfigJsonValue | boolean | null | string | undefined
}
export type AppLegacySensitiveWordConfigPayload = {
canned_response: string
enabled: boolean
words: Array<string>
[key: string]: AppConfigJsonValue | string | boolean | Array<string>
}
export type AppDatasetSelectionPayload = {
enabled?: boolean
id?: string
[key: string]: AppConfigJsonValue | boolean | string | undefined
}
export type AgentFileUploadImageFeatureConfig = {
enabled?: boolean
[key: string]: unknown
@@ -3502,6 +4151,21 @@ export type WorkflowCommentReplyWritable = {
id: string
}
export type AppConfigJsonValueWritable =
| string
| number
| number
| boolean
| Array<AppConfigJsonValueWritable>
| {
[key: string]: AppConfigJsonValueWritable
}
| null
export type AppEmptyDatasetCollectionPayloadWritable = {
[key: string]: never
}
export type GetAppsData = {
body?: never
path?: never
@@ -4944,7 +5608,7 @@ export type GetAppsByAppIdMessagesByMessageIdResponse =
GetAppsByAppIdMessagesByMessageIdResponses[keyof GetAppsByAppIdMessagesByMessageIdResponses]
export type PostAppsByAppIdModelConfigData = {
body: ModelConfigRequest
body: AppModelConfigPayload
path: {
app_id: string
}
@@ -321,24 +321,6 @@ export const zMessageFeedbackPayload = z.object({
*/
export const zTextFileResponse = z.string()
/**
* ModelConfigRequest
*/
export const zModelConfigRequest = z.object({
agent_mode: z.record(z.string(), z.unknown()).nullish(),
configs: z.record(z.string(), z.unknown()).nullish(),
dataset_configs: z.record(z.string(), z.unknown()).nullish(),
model: z.string().nullish(),
more_like_this: z.record(z.string(), z.unknown()).nullish(),
opening_statement: z.string().nullish(),
provider: z.string().nullish(),
retrieval_model: z.record(z.string(), z.unknown()).nullish(),
speech_to_text: z.record(z.string(), z.unknown()).nullish(),
suggested_questions: z.array(z.string()).nullish(),
text_to_speech: z.record(z.string(), z.unknown()).nullish(),
tools: z.array(z.record(z.string(), z.unknown())).nullish(),
})
/**
* AppNamePayload
*/
@@ -2457,6 +2439,253 @@ export const zConversationMessageDetail = z.object({
*/
export const zExecutionContentType = z.enum(['human_input'])
export const zAppConfigJsonValue = z.json().nullable()
/**
* AppExternalDataToolPayload
*/
export const zAppExternalDataToolPayload = z
.object({
config: z.record(z.string(), zAppConfigJsonValue).optional(),
enabled: z.boolean().nullish(),
icon: z.string().optional(),
icon_background: z.string().optional(),
label: z.string().optional(),
type: z.string().optional(),
variable: z.string().optional(),
})
.catchall(z.json())
/**
* AppModelSelectionPayload
*/
export const zAppModelSelectionPayload = z
.object({
completion_params: z.record(z.string(), zAppConfigJsonValue),
mode: z.string().nullish(),
name: z.string(),
provider: z.string(),
})
.catchall(z.json())
/**
* AppFeaturePayload
*/
export const zAppFeaturePayload = z
.object({
enabled: z.boolean().nullish(),
})
.catchall(z.json())
/**
* AppTextToSpeechPayload
*/
export const zAppTextToSpeechPayload = z
.object({
autoPlay: z.enum(['disabled', 'enabled']).optional(),
enabled: z.boolean().nullish(),
language: z.string().optional(),
voice: z.string().optional(),
})
.catchall(z.json())
/**
* AppAgentPromptPayload
*/
export const zAppAgentPromptPayload = z
.object({
first_prompt: z.string().optional(),
next_iteration: z.string().optional(),
})
.catchall(z.json())
/**
* PlanningStrategy
*/
export const zPlanningStrategy = z.enum(['function_call', 'react', 'react_router', 'router'])
/**
* AppPromptMessagePayload
*/
export const zAppPromptMessagePayload = z
.object({
role: z.string(),
text: z.string(),
})
.catchall(z.json())
/**
* AppChatPromptPayload
*/
export const zAppChatPromptPayload = z
.object({
prompt: z.array(zAppPromptMessagePayload).optional(),
})
.catchall(z.json())
/**
* AppConversationRolesPayload
*/
export const zAppConversationRolesPayload = z
.object({
assistant_prefix: z.string(),
user_prefix: z.string(),
})
.catchall(z.json())
/**
* AppCompletionPromptTextPayload
*/
export const zAppCompletionPromptTextPayload = z
.object({
text: z.string(),
})
.catchall(z.json())
/**
* AppCompletionPromptPayload
*/
export const zAppCompletionPromptPayload = z
.object({
conversation_histories_role: zAppConversationRolesPayload.optional(),
prompt: zAppCompletionPromptTextPayload.optional(),
})
.catchall(z.json())
/**
* AppEmptyDatasetCollectionPayload
*/
export const zAppEmptyDatasetCollectionPayload = z.record(z.string(), z.never())
/**
* AppMetadataModelPayload
*/
export const zAppMetadataModelPayload = z
.object({
completion_params: z.record(z.string(), zAppConfigJsonValue).optional(),
mode: z.union([zLlmMode, z.literal('')]).optional(),
name: z.string().optional(),
provider: z.string().optional(),
})
.catchall(z.json())
/**
* RerankMode
*/
export const zRerankMode = z.enum(['reranking_model', 'weighted_score'])
/**
* AppRerankingModelPayload
*/
export const zAppRerankingModelPayload = z
.object({
reranking_model_name: z.string().optional(),
reranking_provider_name: z.string().optional(),
})
.catchall(z.json())
/**
* FileType
*/
export const zFileType = z.enum(['audio', 'custom', 'document', 'image', 'video'])
/**
* FileTransferMethod
*/
export const zFileTransferMethod = z.enum([
'datasource_file',
'local_file',
'remote_url',
'tool_file',
])
/**
* AppFileTypeUploadPayload
*/
export const zAppFileTypeUploadPayload = z
.object({
enabled: z.boolean().nullish(),
number_limits: z.int().nullish(),
transfer_methods: z.array(zFileTransferMethod).nullish(),
})
.catchall(z.json())
/**
* AppImageUploadPayload
*/
export const zAppImageUploadPayload = z
.object({
detail: z.enum(['high', 'low']).nullish(),
enabled: z.boolean().optional(),
number_limits: z.int().optional(),
transfer_methods: z.array(zFileTransferMethod).optional(),
})
.catchall(z.json())
/**
* AppImageConfigPayload
*/
export const zAppImageConfigPayload = z
.object({
detail: z.enum(['high', 'low']).nullish(),
number_limits: z.int().optional(),
transfer_methods: z.array(zFileTransferMethod).optional(),
})
.catchall(z.json())
/**
* AppFilePreviewPayload
*/
export const zAppFilePreviewPayload = z
.object({
file_type_list: z.array(z.string()).nullish(),
mode: z.string().nullish(),
})
.catchall(z.json())
/**
* AppFileUploadPayload
*/
export const zAppFileUploadPayload = z
.object({
allowed_file_extensions: z.array(z.string()).optional(),
allowed_file_types: z.array(zFileType).optional(),
allowed_file_upload_methods: z.array(zFileTransferMethod).optional(),
audio: zAppFileTypeUploadPayload.nullish(),
custom: zAppFileTypeUploadPayload.nullish(),
document: zAppFileTypeUploadPayload.nullish(),
enabled: z.boolean().optional(),
image: zAppImageUploadPayload.optional(),
image_config: zAppImageConfigPayload.nullish(),
number_limits: z.int().optional(),
preview_config: zAppFilePreviewPayload.nullish(),
video: zAppFileTypeUploadPayload.nullish(),
})
.catchall(z.json())
/**
* AppSuggestedQuestionsModelPayload
*/
export const zAppSuggestedQuestionsModelPayload = z
.object({
completion_params: z.record(z.string(), zAppConfigJsonValue).optional(),
mode: z.union([zLlmMode, z.literal('')]).optional(),
name: z.string(),
provider: z.string(),
})
.catchall(z.json())
/**
* AppSuggestedQuestionsPayload
*/
export const zAppSuggestedQuestionsPayload = z
.object({
enabled: z.boolean().nullish(),
model: zAppSuggestedQuestionsModelPayload.optional(),
prompt: z.string().optional(),
})
.catchall(z.json())
/**
* WorkflowRunForLogResponse
*/
@@ -2799,11 +3028,6 @@ export const zAppAgentPromptResponse = z.object({
next_iteration: z.string().optional(),
})
/**
* PlanningStrategy
*/
export const zPlanningStrategy = z.enum(['function_call', 'react', 'react_router', 'router'])
/**
* AppEmbeddingModelResponse
*/
@@ -2860,11 +3084,6 @@ export const zAppCompletionPromptConfigResponse = z.object({
prompt: zAppCompletionPromptTextResponse.optional(),
})
/**
* RerankMode
*/
export const zRerankMode = z.enum(['reranking_model', 'weighted_score'])
/**
* AppRerankingModelResponse
*/
@@ -3025,21 +3244,6 @@ export const zAppLegacyCurrentDatetimeToolResponse = z.object({
current_datetime: z.record(z.string(), zJsonValue2),
})
/**
* FileType
*/
export const zFileType = z.enum(['audio', 'custom', 'document', 'image', 'video'])
/**
* FileTransferMethod
*/
export const zFileTransferMethod = z.enum([
'datasource_file',
'local_file',
'remote_url',
'tool_file',
])
/**
* AppImageUploadResponse
*/
@@ -3148,6 +3352,253 @@ export const zAppJsonObjectFormResponse = z.object({
json_object: zAppUserInputFormConfigResponse,
})
/**
* AppLegacyGoogleSearchToolPayload
*/
export const zAppLegacyGoogleSearchToolPayload = z
.object({
google_search: zAppFeaturePayload,
})
.catchall(z.json())
/**
* AppLegacyWebReaderToolPayload
*/
export const zAppLegacyWebReaderToolPayload = z
.object({
web_reader: zAppFeaturePayload,
})
.catchall(z.json())
/**
* AppLegacyWikipediaToolPayload
*/
export const zAppLegacyWikipediaToolPayload = z
.object({
wikipedia: zAppFeaturePayload,
})
.catchall(z.json())
/**
* AppLegacyCurrentDatetimeToolPayload
*/
export const zAppLegacyCurrentDatetimeToolPayload = z
.object({
current_datetime: zAppFeaturePayload,
})
.catchall(z.json())
/**
* AppMetadataConditionPayload
*/
export const zAppMetadataConditionPayload = z
.object({
comparison_operator: z.enum([
'<',
'=',
'>',
'after',
'before',
'contains',
'empty',
'end with',
'in',
'is',
'is not',
'not contains',
'not empty',
'not in',
'start with',
'≠',
'≤',
'≥',
]),
id: z.string().optional(),
metadata_id: z.string().optional(),
name: z.string(),
value: z.union([z.string(), z.array(z.string()), z.int(), z.number()]).nullish(),
})
.catchall(z.json())
/**
* AppMetadataFilteringPayload
*/
export const zAppMetadataFilteringPayload = z
.object({
conditions: z.array(zAppMetadataConditionPayload).nullish(),
logical_operator: z.enum(['and', 'or']).nullish(),
})
.catchall(z.json())
/**
* AppKeywordWeightPayload
*/
export const zAppKeywordWeightPayload = z
.object({
keyword_weight: z.number(),
})
.catchall(z.json())
/**
* AppVectorWeightPayload
*/
export const zAppVectorWeightPayload = z
.object({
embedding_model_name: z.string(),
embedding_provider_name: z.string(),
vector_weight: z.number(),
})
.catchall(z.json())
/**
* AppRetrievalWeightsPayload
*/
export const zAppRetrievalWeightsPayload = z
.object({
keyword_setting: zAppKeywordWeightPayload,
vector_setting: zAppVectorWeightPayload,
weight_type: z.enum(['customized', 'keyword_first', 'semantic_first']).optional(),
})
.catchall(z.json())
/**
* AppModerationContentPayload
*/
export const zAppModerationContentPayload = z
.object({
enabled: z.boolean().nullish(),
preset_response: z.string().nullish(),
})
.catchall(z.json())
/**
* AppModerationConfigPayload
*/
export const zAppModerationConfigPayload = z
.object({
api_based_extension_id: z.string().nullish(),
inputs_config: zAppModerationContentPayload.nullish(),
keywords: z.string().nullish(),
outputs_config: zAppModerationContentPayload.nullish(),
})
.catchall(z.json())
/**
* AppModerationPayload
*/
export const zAppModerationPayload = z.object({
config: zAppModerationConfigPayload.nullish(),
enabled: z.boolean().nullish(),
type: z.string().nullish(),
})
/**
* AppInputFieldPayload
*/
export const zAppInputFieldPayload = z
.object({
config: z.record(z.string(), zAppConfigJsonValue).optional(),
default: zAppConfigJsonValue.optional(),
description: z.string().optional(),
enabled: z.boolean().optional(),
hide: z.boolean().optional(),
icon: z.string().nullish(),
icon_background: z.string().nullish(),
json_schema: z.union([z.string(), z.record(z.string(), zAppConfigJsonValue)]).nullish(),
label: z.string(),
max_length: z.int().nullish(),
options: z.array(z.string()).optional(),
required: z.boolean().nullish(),
type: z.string().optional(),
variable: z.string(),
})
.catchall(z.json())
/**
* AppTextInputPayload
*/
export const zAppTextInputPayload = z
.object({
'text-input': zAppInputFieldPayload,
})
.strict()
/**
* AppParagraphInputPayload
*/
export const zAppParagraphInputPayload = z
.object({
paragraph: zAppInputFieldPayload,
})
.strict()
/**
* AppNumberInputPayload
*/
export const zAppNumberInputPayload = z
.object({
number: zAppInputFieldPayload,
})
.strict()
/**
* AppCheckboxInputPayload
*/
export const zAppCheckboxInputPayload = z
.object({
checkbox: zAppInputFieldPayload,
})
.strict()
/**
* AppExternalDataInputPayload
*/
export const zAppExternalDataInputPayload = z
.object({
external_data_tool: zAppInputFieldPayload,
})
.strict()
/**
* AppSelectFieldPayload
*/
export const zAppSelectFieldPayload = z
.object({
config: z.record(z.string(), zAppConfigJsonValue).optional(),
default: zAppConfigJsonValue.optional(),
description: z.string().optional(),
enabled: z.boolean().optional(),
hide: z.boolean().optional(),
icon: z.string().nullish(),
icon_background: z.string().nullish(),
json_schema: z.union([z.string(), z.record(z.string(), zAppConfigJsonValue)]).nullish(),
label: z.string(),
max_length: z.int().nullish(),
options: z.array(z.string()).nullish(),
required: z.boolean().nullish(),
type: z.string().optional(),
variable: z.string(),
})
.catchall(z.json())
/**
* AppSelectInputPayload
*/
export const zAppSelectInputPayload = z
.object({
select: zAppSelectFieldPayload,
})
.strict()
export const zAppUserInputFormPayload = z.union([
zAppTextInputPayload,
zAppSelectInputPayload,
zAppParagraphInputPayload,
zAppNumberInputPayload,
zAppCheckboxInputPayload,
zAppExternalDataInputPayload,
])
/**
* WorkflowFileUploadTransferPayload
*/
@@ -3589,6 +4040,25 @@ export const zAppProviderAgentToolResponse = z.object({
tool_parameters: z.record(z.string(), zJsonValue2),
})
/**
* AppProviderAgentToolPayload
*/
export const zAppProviderAgentToolPayload = z
.object({
credential_id: z.string().nullish(),
enabled: z.boolean().nullish(),
isDeleted: z.boolean().optional(),
notAuthor: z.boolean().optional(),
plugin_unique_identifier: z.string().nullish(),
provider_id: z.string(),
provider_name: z.string().optional(),
provider_type: zToolProviderType,
tool_label: z.string().optional(),
tool_name: z.string(),
tool_parameters: z.record(z.string(), zAppConfigJsonValue),
})
.catchall(z.json())
/**
* AppDatasetReferenceResponse
*/
@@ -3891,6 +4361,145 @@ export const zFileListInputConfig = z.object({
type: z.literal('file-list').optional().default('file-list'),
})
/**
* AppLegacyDatasetSelectionPayload
*/
export const zAppLegacyDatasetSelectionPayload = z
.object({
enabled: z.boolean().nullish(),
id: z.string().optional(),
})
.catchall(z.json())
/**
* AppLegacyDatasetToolPayload
*/
export const zAppLegacyDatasetToolPayload = z
.object({
dataset: zAppLegacyDatasetSelectionPayload,
})
.catchall(z.json())
/**
* AppLegacySensitiveWordConfigPayload
*/
export const zAppLegacySensitiveWordConfigPayload = z
.object({
canned_response: z.string(),
enabled: z.boolean(),
words: z.array(z.string()),
})
.catchall(z.json())
/**
* AppLegacySensitiveWordToolPayload
*/
export const zAppLegacySensitiveWordToolPayload = z
.object({
'sensitive-word-avoidance': zAppLegacySensitiveWordConfigPayload,
})
.catchall(z.json())
export const zAppAgentToolPayload = z.union([
zAppProviderAgentToolPayload,
zAppLegacyDatasetToolPayload,
zAppLegacyGoogleSearchToolPayload,
zAppLegacyWebReaderToolPayload,
zAppLegacyWikipediaToolPayload,
zAppLegacyCurrentDatetimeToolPayload,
zAppLegacySensitiveWordToolPayload,
])
/**
* AppAgentModePayload
*/
export const zAppAgentModePayload = z
.object({
enabled: z.boolean().nullish(),
max_iteration: z.int().optional(),
prompt: z.union([zAppAgentPromptPayload, z.string()]).nullish(),
strategy: z.union([zPlanningStrategy, z.enum(['', 'cot', 'function-calling'])]).nullish(),
tools: z.array(zAppAgentToolPayload).nullish(),
})
.catchall(z.json())
/**
* AppDatasetSelectionPayload
*/
export const zAppDatasetSelectionPayload = z
.object({
enabled: z.boolean().optional(),
id: z.string().optional(),
})
.catchall(z.json())
/**
* AppDatasetToolPayload
*/
export const zAppDatasetToolPayload = z
.object({
dataset: zAppDatasetSelectionPayload,
})
.catchall(z.json())
/**
* AppDatasetCollectionPayload
*/
export const zAppDatasetCollectionPayload = z
.object({
datasets: z.array(zAppDatasetToolPayload),
strategy: z.string().optional(),
})
.catchall(z.json())
/**
* AppDatasetConfigPayload
*/
export const zAppDatasetConfigPayload = z
.object({
datasets: z.union([zAppDatasetCollectionPayload, zAppEmptyDatasetCollectionPayload]).nullish(),
metadata_filtering_conditions: zAppMetadataFilteringPayload.nullish(),
metadata_filtering_mode: z.enum(['automatic', 'disabled', 'manual']).optional(),
metadata_model_config: zAppMetadataModelPayload.nullish(),
reranking_enable: z.boolean().optional(),
reranking_enabled: z.boolean().optional(),
reranking_mode: zRerankMode.optional(),
reranking_model: zAppRerankingModelPayload.nullish(),
retrieval_model: z.enum(['multiple', 'single']).optional(),
score_threshold: z.number().nullish(),
score_threshold_enabled: z.boolean().optional(),
top_k: z.int().optional(),
weights: zAppRetrievalWeightsPayload.nullish(),
})
.catchall(z.json())
/**
* AppModelConfigPayload
*
* Write transport; app-mode validators own defaults and feature-specific rules.
*/
export const zAppModelConfigPayload = z.object({
agent_mode: zAppAgentModePayload.nullish(),
chat_prompt_config: zAppChatPromptPayload.nullish(),
completion_prompt_config: zAppCompletionPromptPayload.nullish(),
dataset_configs: zAppDatasetConfigPayload.nullish(),
dataset_query_variable: z.string().nullish(),
external_data_tools: z.array(zAppExternalDataToolPayload).nullish(),
file_upload: zAppFileUploadPayload.nullish(),
model: zAppModelSelectionPayload,
more_like_this: zAppFeaturePayload.nullish(),
opening_statement: z.string().nullish(),
pre_prompt: z.string().nullish(),
prompt_type: z.enum(['', 'advanced', 'simple']).nullish(),
retriever_resource: zAppFeaturePayload.nullish(),
sensitive_word_avoidance: zAppModerationPayload.nullish(),
speech_to_text: zAppFeaturePayload.nullish(),
suggested_questions: z.array(z.string()).nullish(),
suggested_questions_after_answer: zAppSuggestedQuestionsPayload.nullish(),
text_to_speech: zAppTextToSpeechPayload.nullish(),
user_input_form: z.array(zAppUserInputFormPayload).nullish(),
})
/**
* AgentFileUploadImageFeatureConfig
*/
@@ -4744,6 +5353,13 @@ export const zWorkflowCommentDetailWritable = z.object({
updated_at: z.int().nullish(),
})
export const zAppConfigJsonValueWritable = z.json().nullable()
/**
* AppEmptyDatasetCollectionPayload
*/
export const zAppEmptyDatasetCollectionPayloadWritable = z.record(z.string(), z.never())
export const zGetAppsQuery = z.object({
creator_ids: z.array(z.string()).optional(),
is_created_by_me: z.boolean().optional(),
@@ -5654,7 +6270,7 @@ export const zGetAppsByAppIdMessagesByMessageIdPath = z.object({
*/
export const zGetAppsByAppIdMessagesByMessageIdResponse = zMessageDetailResponse
export const zPostAppsByAppIdModelConfigBody = zModelConfigRequest
export const zPostAppsByAppIdModelConfigBody = zAppModelConfigPayload
export const zPostAppsByAppIdModelConfigPath = z.object({
app_id: z.uuid(),
+32 -1
View File
@@ -60,7 +60,16 @@ const currentDir = path.dirname(fileURLToPath(import.meta.url))
const apiOpenApiDir = path.resolve(currentDir, 'openapi')
const operationMethods = new Set(['delete', 'get', 'patch', 'post', 'put'])
const strictZodSchemaNames = new Set(['AccountProfilePatchPayload', 'Parameters'])
const strictZodSchemaNames = new Set([
'AccountProfilePatchPayload',
'Parameters',
'AppTextInputPayload',
'AppSelectInputPayload',
'AppParagraphInputPayload',
'AppNumberInputPayload',
'AppCheckboxInputPayload',
'AppExternalDataInputPayload',
])
const pydanticDecimalStringPattern = '^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$'
const codegenSafeDecimalStringPattern = '^(?![-+.]*$)[+-]?0*\\d*\\.?\\d*$'
const fastOpenApiConsoleSpecFilename = 'fastopenapi-console-openapi.json'
@@ -628,9 +637,31 @@ const createApiConfig = (job: ApiJob): UserConfig => ({
{
name: 'zod',
'~resolvers': {
union: (ctx) => {
// The generator's recursive lazy schemas infer `any`. Keep this JSON
// leaf typed through Zod so oRPC clients retain the request contract.
if (
ctx.path['~ref'].some(
(segment) =>
segment === 'AppConfigJsonValue' || segment === 'AppConfigJsonValueWritable',
)
)
return $(ctx.symbols.z).attr('json').call()
return ctx.nodes.base(ctx)
},
object: (ctx) => {
const objectSchema = ctx.nodes.base(ctx)
const additionalProperties = ctx.schema.additionalProperties
// Shaped objects otherwise discard the backend's typed JSON extensions.
if (
additionalProperties &&
typeof additionalProperties !== 'boolean' &&
additionalProperties.$ref === '#/components/schemas/AppConfigJsonValue' &&
ctx.schema.properties
)
return objectSchema.attr('catchall').call($(ctx.symbols.z).attr('json').call())
// openapi-ts normalizes `additionalProperties: false` to `never`, but
// does not make shaped Zod objects strict.
const isStrictSchema = ctx.path['~ref'].some(