refactor(api): tighten app detail response schema (#42876)

This commit is contained in:
yyh authored and GitHub committed 2026-09-24 08:53:49 +00:00
1 parent edf1a57b7b
commit ef33df5e22
52 files changed
+4295 -1373

No files matched your search

+4 -3
View File
@@ -274,6 +274,7 @@ class AgentAppPartial(GenericAppPartial):
class AgentAppDetailWithSite(GenericAppDetailWithSite):
permission_keys: list[str]
bound_agent_id: str | None = None
app_id: str | None = None
backing_app_id: str | None = None
hidden_app_backed: bool = False
@@ -404,14 +405,14 @@ def _serialize_agent_app_detail(
roster persona fields without widening the shared /apps detail schema.
"""
app_model = AppService().get_app(app_model, session=session)
access_mode = None
if SystemFeatureService.is_webapp_auth_enabled():
app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
app_model.access_mode = app_setting.access_mode # type: ignore[attr-defined]
access_mode = app_setting.access_mode
roster_service = _agent_roster_service(session)
payload = GenericAppDetailWithSite.model_validate(
AppResponseView(app_model, session=session),
AppResponseView(app_model, session=session, account=current_user, access_mode=access_mode),
from_attributes=True,
).model_dump(mode="json")
agent = (
+7 -5
View File
@@ -1,4 +1,5 @@
import uuid
from dataclasses import asdict
from http import HTTPStatus
from typing import Literal, Self, override
@@ -303,11 +304,12 @@ _ADMIN_ROLES = frozenset({TenantAccountRole.OWNER, TenantAccountRole.ADMIN})
def _app_detail_response(app: AppRecord) -> AppDetailWithSite:
"""Resolve transport URLs here so repositories work without a Flask request."""
response = AppDetailWithSite.model_validate(app, from_attributes=True)
response.api_base_url = normalize_api_base_url(dify_config.SERVICE_API_URL or request.host_url.rstrip("/"))
if response.site is not None:
response.site.app_base_url = dify_config.APP_WEB_URL or request.url_root.rstrip("/")
return response
data = asdict(app)
data["api_base_url"] = normalize_api_base_url(dify_config.SERVICE_API_URL or request.host_url.rstrip("/"))
data["app_id"] = None
if data["site"] is not None:
data["site"]["app_base_url"] = dify_config.APP_WEB_URL or request.url_root.rstrip("/")
return AppDetailWithSite.model_validate(data)
class AppResource(Resource):
+2 -2
View File
@@ -2,7 +2,7 @@ import json
import logging
from collections.abc import Sequence
from datetime import datetime
from typing import Any, NotRequired, Self, TypedDict
from typing import Any, Literal, NotRequired, Self, TypedDict
from flask import abort, request
from flask_restx import Resource
@@ -216,7 +216,7 @@ class WorkflowSuggestedQuestionsAfterAnswerPayload(WorkflowFeatureTogglePayload)
class WorkflowTextToSpeechPayload(WorkflowFeatureTogglePayload):
language: str | None = None
voice: str | None = None
autoPlay: str | None = None
autoPlay: Literal["enabled", "disabled"] | None = None
class WorkflowSensitiveWordAvoidancePayload(WorkflowFeatureTogglePayload):
@@ -4,6 +4,23 @@ CUSTOM_FOLLOW_UP_PROMPT_MAX_LENGTH = 1000
class SuggestedQuestionsAfterAnswerConfigManager:
@staticmethod
def validate_optional_fields(config: dict[str, Any]) -> None:
feature = config.get("suggested_questions_after_answer")
if feature is None:
return
if not isinstance(feature, dict):
raise ValueError("suggested_questions_after_answer must be of dict type")
if "prompt" in feature:
prompt = feature["prompt"]
if not isinstance(prompt, str):
raise ValueError("prompt in suggested_questions_after_answer must be of string type")
if len(prompt) > CUSTOM_FOLLOW_UP_PROMPT_MAX_LENGTH:
raise ValueError(
f"prompt in suggested_questions_after_answer must be less than or equal to "
f"{CUSTOM_FOLLOW_UP_PROMPT_MAX_LENGTH} characters"
)
@classmethod
def convert(cls, config: dict[str, Any]) -> bool:
"""
@@ -45,14 +62,7 @@ class SuggestedQuestionsAfterAnswerConfigManager:
if not isinstance(config["suggested_questions_after_answer"]["enabled"], bool):
raise ValueError("enabled in suggested_questions_after_answer must be of boolean type")
prompt = config["suggested_questions_after_answer"].get("prompt")
if prompt is not None and not isinstance(prompt, str):
raise ValueError("prompt in suggested_questions_after_answer must be of string type")
if isinstance(prompt, str) and len(prompt) > CUSTOM_FOLLOW_UP_PROMPT_MAX_LENGTH:
raise ValueError(
f"prompt in suggested_questions_after_answer must be less than or equal to "
f"{CUSTOM_FOLLOW_UP_PROMPT_MAX_LENGTH} characters"
)
cls.validate_optional_fields(config)
if "model" in config["suggested_questions_after_answer"]:
model_config = config["suggested_questions_after_answer"]["model"]
@@ -4,6 +4,19 @@ from core.app.app_config.entities import TextToSpeechEntity
class TextToSpeechConfigManager:
@staticmethod
def validate_optional_fields(config: dict[str, Any]) -> None:
feature = config.get("text_to_speech")
if feature is None:
return
if not isinstance(feature, dict):
raise ValueError("text_to_speech must be of dict type")
for key in ("voice", "language"):
if key in feature and not isinstance(feature[key], str):
raise ValueError(f"{key} in text_to_speech must be of string type")
if "autoPlay" in feature and feature["autoPlay"] not in ("enabled", "disabled"):
raise ValueError("autoPlay in text_to_speech must be enabled or disabled")
@classmethod
def convert(cls, config: dict[str, Any]):
"""
@@ -36,6 +49,8 @@ class TextToSpeechConfigManager:
if not isinstance(config["text_to_speech"], dict):
raise ValueError("text_to_speech must be of dict type")
cls.validate_optional_fields(config)
if "enabled" not in config["text_to_speech"] or not config["text_to_speech"]["enabled"]:
config["text_to_speech"]["enabled"] = False
config["text_to_speech"]["voice"] = ""
+55 -113
View File
@@ -1,13 +1,15 @@
"""Shared Console app response schemas for materialized application results."""
from datetime import datetime
from typing import Any
from pydantic import AliasChoices, Field, computed_field, field_validator
from enums import WebAppAccessMode
from fields.app_model_config_response import AppModelConfigResponse, AppModelSelectionResponse
from fields.base import ResponseModel
from libs.helper import build_icon_url, to_timestamp
from models.model import IconType
from models.enums import CustomizeTokenStrategy
from models.model import AppMode, IconType
from services.entities.app_entities import RecentAppMode
from services.entities.dsl_entities import DslImportWarning, ImportStatus
@@ -37,7 +39,7 @@ class WorkflowPartial(ResponseModel):
class ModelConfigPartial(ResponseModel):
model: Any | None = Field(default=None, validation_alias=AliasChoices("model_dict", "model"))
model: AppModelSelectionResponse | None = Field(default=None, validation_alias=AliasChoices("model_dict", "model"))
pre_prompt: str | None = None
created_by: str | None = None
created_at: int | None = None
@@ -50,90 +52,31 @@ class ModelConfigPartial(ResponseModel):
return to_timestamp(value)
class AppModelConfigResponse(ResponseModel):
opening_statement: str | None = None
suggested_questions: Any | None = Field(
default=None, validation_alias=AliasChoices("suggested_questions_list", "suggested_questions")
)
suggested_questions_after_answer: Any | None = Field(
default=None,
validation_alias=AliasChoices("suggested_questions_after_answer_dict", "suggested_questions_after_answer"),
)
speech_to_text: Any | None = Field(
default=None, validation_alias=AliasChoices("speech_to_text_dict", "speech_to_text")
)
text_to_speech: Any | None = Field(
default=None, validation_alias=AliasChoices("text_to_speech_dict", "text_to_speech")
)
retriever_resource: Any | None = Field(
default=None, validation_alias=AliasChoices("retriever_resource_dict", "retriever_resource")
)
annotation_reply: Any | None = Field(
default=None, validation_alias=AliasChoices("annotation_reply_dict", "annotation_reply")
)
more_like_this: Any | None = Field(
default=None, validation_alias=AliasChoices("more_like_this_dict", "more_like_this")
)
sensitive_word_avoidance: Any | None = Field(
default=None, validation_alias=AliasChoices("sensitive_word_avoidance_dict", "sensitive_word_avoidance")
)
external_data_tools: Any | None = Field(
default=None, validation_alias=AliasChoices("external_data_tools_list", "external_data_tools")
)
model: Any | None = Field(default=None, validation_alias=AliasChoices("model_dict", "model"))
user_input_form: Any | None = Field(
default=None, validation_alias=AliasChoices("user_input_form_list", "user_input_form")
)
dataset_query_variable: str | None = None
pre_prompt: str | None = None
agent_mode: Any | None = Field(default=None, validation_alias=AliasChoices("agent_mode_dict", "agent_mode"))
prompt_type: str | None = None
chat_prompt_config: Any | None = Field(
default=None, validation_alias=AliasChoices("chat_prompt_config_dict", "chat_prompt_config")
)
completion_prompt_config: Any | None = Field(
default=None, validation_alias=AliasChoices("completion_prompt_config_dict", "completion_prompt_config")
)
dataset_configs: Any | None = Field(
default=None, validation_alias=AliasChoices("dataset_configs_dict", "dataset_configs")
)
file_upload: Any | None = Field(default=None, validation_alias=AliasChoices("file_upload_dict", "file_upload"))
created_by: str | None = None
created_at: int | None = None
updated_by: str | None = None
updated_at: int | None = None
@field_validator("created_at", "updated_at", mode="before")
@classmethod
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
class AppDetailSiteResponse(ResponseModel):
access_token: str | None = Field(default=None, validation_alias="code")
code: str | None = None
title: str | None = None
icon_type: str | IconType | None = None
icon: str | None = None
icon_background: str | None = None
description: str | None = None
default_language: str | None = None
chat_color_theme: str | None = None
chat_color_theme_inverted: bool | None = None
customize_domain: str | None = None
copyright: str | None = None
privacy_policy: str | None = None
input_placeholder: str | None = None
custom_disclaimer: str | None = None
customize_token_strategy: str | None = None
prompt_public: bool | None = None
app_base_url: str | None = None
show_workflow_steps: bool | None = None
use_icon_as_answer_icon: bool | None = None
created_by: str | None = None
created_at: int | None = None
updated_by: str | None = None
updated_at: int | None = None
access_token: str | None = Field(validation_alias="code")
code: str | None
title: str
icon_type: IconType | None
icon: str | None
icon_background: str | None
description: str | None
default_language: str
chat_color_theme: str | None
chat_color_theme_inverted: bool
customize_domain: str | None
copyright: str | None
privacy_policy: str | None
input_placeholder: str | None
custom_disclaimer: str
customize_token_strategy: CustomizeTokenStrategy
prompt_public: bool
app_base_url: str
show_workflow_steps: bool
use_icon_as_answer_icon: bool
created_by: str | None
created_at: int
updated_by: str | None
updated_at: int
@computed_field(return_type=str | None) # type: ignore
@property
@@ -164,8 +107,8 @@ class AppPartial(ResponseModel):
name: str
max_active_requests: int | None = None
description: str | None = Field(default=None, validation_alias=AliasChoices("desc_or_prompt", "description"))
mode: str = Field(validation_alias="mode_compatible_with_agent")
icon_type: str | None = None
mode: AppMode = Field(validation_alias="mode_compatible_with_agent")
icon_type: IconType | None = None
icon: str | None = None
icon_background: str | None = None
model_config_: ModelConfigPartial | None = Field(
@@ -180,7 +123,7 @@ class AppPartial(ResponseModel):
updated_by: str | None = None
updated_at: int | None = None
tags: list[Tag] = Field(default_factory=list)
access_mode: str | None = None
access_mode: WebAppAccessMode | None = None
create_user_name: str | None = None
author_name: str | None = None
has_draft_trigger: bool | None = None
@@ -233,28 +176,27 @@ class RecentAppListResponse(ResponseModel):
class AppDetail(ResponseModel):
id: str
name: str
description: str | None = None
mode: str = Field(validation_alias="mode_compatible_with_agent")
icon: str | None = None
icon_background: str | None = None
description: str
mode: AppMode = Field(validation_alias="mode_compatible_with_agent")
icon: str | None
icon_background: str | None
enable_site: bool
enable_api: bool
model_config_: AppModelConfigResponse | None = Field(
default=None,
validation_alias=AliasChoices("app_model_config", "model_config"),
alias="model_config",
)
workflow: WorkflowPartial | None = None
tracing: Any | None = None
use_icon_as_answer_icon: bool | None = None
created_by: str | None = None
created_at: int | None = None
updated_by: str | None = None
updated_at: int | None = None
access_mode: str | None = None
tags: list[Tag] = Field(default_factory=list)
permission_keys: list[str] = Field(default_factory=list)
maintainer: str | None = None
workflow: WorkflowPartial | None
tracing: str | None
use_icon_as_answer_icon: bool
created_by: str | None
created_at: int
updated_by: str | None
updated_at: int
access_mode: WebAppAccessMode | None
tags: list[Tag]
permission_keys: list[str]
maintainer: str | None
@field_validator("created_at", "updated_at", mode="before")
@classmethod
@@ -263,15 +205,15 @@ class AppDetail(ResponseModel):
class AppDetailWithSite(AppDetail):
icon_type: str | None = None
api_base_url: str | None = None
max_active_requests: int | None = None
deleted_tools: list[DeletedTool] = Field(default_factory=list)
site: AppDetailSiteResponse | None = None
icon_type: IconType | None
api_base_url: str
max_active_requests: int | None
deleted_tools: list[DeletedTool]
site: AppDetailSiteResponse | None
# For Agent App type: the roster Agent backing this app (None otherwise).
bound_agent_id: str | None = None
bound_agent_id: str | None
# For Agent App responses exposed through /agent.
app_id: str | None = None
app_id: str | None
@computed_field(return_type=str | None) # type: ignore
@property
@@ -295,7 +237,7 @@ class AppImportResponse(ResponseModel):
id: str
status: ImportStatus
app_id: str | None = None
app_mode: str | None = None
app_mode: AppMode | None = None
current_dsl_version: str
imported_dsl_version: str = ""
error: str = ""
+388
View File
@@ -0,0 +1,388 @@
from datetime import datetime
from typing import Literal, NotRequired, TypedDict
from pydantic import AliasChoices, ConfigDict, Field, JsonValue, field_validator, with_config
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 fields.base import ResponseModel
from graphon.file import FileTransferMethod, FileType
from graphon.model_runtime.entities.llm_entities import LLMMode
from libs.helper import to_timestamp
from models.enums import PromptType
class AppEnabledConfigResponse(TypedDict):
enabled: bool
@with_config(ConfigDict(extra="allow"))
class AppModelSelectionResponse(TypedDict, total=False):
provider: str
name: str
mode: LLMMode | Literal[""]
completion_params: dict[str, JsonValue]
class AppSuggestedQuestionsAfterAnswerResponse(TypedDict):
enabled: bool
model: NotRequired[AppModelSelectionResponse]
prompt: NotRequired[str]
class AppTextToSpeechResponse(TypedDict):
enabled: bool
voice: NotRequired[str]
language: NotRequired[str]
autoPlay: NotRequired[Literal["enabled", "disabled"]]
class AppEmbeddingModelResponse(TypedDict):
embedding_provider_name: str
embedding_model_name: str
class AppAnnotationReplyDisabledResponse(TypedDict):
enabled: Literal[False]
class AppAnnotationReplyEnabledResponse(TypedDict):
id: str
enabled: Literal[True]
score_threshold: float
embedding_model: AppEmbeddingModelResponse
@with_config(ConfigDict(extra="allow"))
class AppSensitiveWordAvoidanceResponse(TypedDict):
enabled: bool
type: NotRequired[str]
config: NotRequired[dict[str, JsonValue]]
configs: NotRequired[list[JsonValue]]
@with_config(ConfigDict(extra="allow"))
class AppEnabledExternalDataToolResponse(TypedDict):
enabled: Literal[True]
variable: str
type: str
config: dict[str, JsonValue]
label: NotRequired[str]
icon: NotRequired[str]
icon_background: NotRequired[str]
@with_config(ConfigDict(extra="allow"))
class AppDisabledExternalDataToolResponse(TypedDict):
enabled: Literal[False]
variable: NotRequired[str]
type: NotRequired[str]
config: NotRequired[dict[str, JsonValue]]
label: NotRequired[str]
icon: NotRequired[str]
icon_background: NotRequired[str]
@with_config(ConfigDict(extra="allow"))
class AppUserInputFormConfigResponse(TypedDict):
variable: str
label: str
description: NotRequired[str]
required: NotRequired[bool]
max_length: NotRequired[int | None]
options: NotRequired[list[str]]
default: NotRequired[JsonValue]
type: NotRequired[str]
hide: NotRequired[bool]
allowed_file_types: NotRequired[list[FileType]]
allowed_file_extensions: NotRequired[list[str]]
allowed_file_upload_methods: NotRequired[list[FileTransferMethod]]
json_schema: NotRequired[str | dict[str, JsonValue] | None]
config: NotRequired[dict[str, JsonValue]]
enabled: NotRequired[bool]
icon: NotRequired[str | None]
icon_background: NotRequired[str | None]
AppTextInputFormResponse = TypedDict("AppTextInputFormResponse", {"text-input": AppUserInputFormConfigResponse})
class AppSelectFormResponse(TypedDict):
select: AppUserInputFormConfigResponse
class AppParagraphFormResponse(TypedDict):
paragraph: AppUserInputFormConfigResponse
class AppNumberFormResponse(TypedDict):
number: AppUserInputFormConfigResponse
class AppCheckboxFormResponse(TypedDict):
checkbox: AppUserInputFormConfigResponse
class AppFileFormResponse(TypedDict):
file: AppUserInputFormConfigResponse
AppFileListFormResponse = TypedDict("AppFileListFormResponse", {"file-list": AppUserInputFormConfigResponse})
class AppExternalDataToolFormResponse(TypedDict):
external_data_tool: AppUserInputFormConfigResponse
class AppJsonObjectFormResponse(TypedDict):
json_object: AppUserInputFormConfigResponse
AppUserInputFormResponse = (
AppTextInputFormResponse
| AppSelectFormResponse
| AppParagraphFormResponse
| AppNumberFormResponse
| AppCheckboxFormResponse
| AppFileFormResponse
| AppFileListFormResponse
| AppExternalDataToolFormResponse
| AppJsonObjectFormResponse
)
class AppDatasetReferenceResponse(TypedDict, total=False):
id: str
enabled: bool
class AppLegacySensitiveWordToolResponse(TypedDict):
enabled: bool
words: list[str]
canned_response: str
@with_config(ConfigDict(extra="allow"))
class AppProviderAgentToolResponse(TypedDict):
provider_type: ToolProviderType
provider_id: str
tool_name: str
tool_parameters: dict[str, JsonValue]
provider_name: NotRequired[str]
tool_label: NotRequired[str]
plugin_unique_identifier: NotRequired[str | None]
credential_id: NotRequired[str | None]
enabled: NotRequired[bool]
isDeleted: NotRequired[bool]
notAuthor: NotRequired[bool]
class AppLegacyDatasetToolResponse(TypedDict):
dataset: AppDatasetReferenceResponse
class AppLegacyGoogleSearchToolResponse(TypedDict):
google_search: dict[str, JsonValue]
class AppLegacyWebReaderToolResponse(TypedDict):
web_reader: dict[str, JsonValue]
class AppLegacyWikipediaToolResponse(TypedDict):
wikipedia: dict[str, JsonValue]
class AppLegacyCurrentDatetimeToolResponse(TypedDict):
current_datetime: dict[str, JsonValue]
AppLegacySensitiveWordToolResponseItem = TypedDict(
"AppLegacySensitiveWordToolResponseItem",
{"sensitive-word-avoidance": AppLegacySensitiveWordToolResponse},
)
AppAgentToolResponse = (
AppProviderAgentToolResponse
| AppLegacyDatasetToolResponse
| AppLegacyGoogleSearchToolResponse
| AppLegacyWebReaderToolResponse
| AppLegacyWikipediaToolResponse
| AppLegacyCurrentDatetimeToolResponse
| AppLegacySensitiveWordToolResponseItem
)
class AppAgentPromptResponse(TypedDict, total=False):
first_prompt: str
next_iteration: str
@with_config(ConfigDict(extra="allow"))
class AppAgentModeResponse(TypedDict):
enabled: bool
strategy: NotRequired[PlanningStrategy | Literal["cot", "function-calling"] | None]
tools: NotRequired[list[AppAgentToolResponse]]
prompt: NotRequired[AppAgentPromptResponse | str | None]
max_iteration: NotRequired[int]
class AppChatPromptMessageResponse(TypedDict):
text: str
role: str
class AppChatPromptConfigResponse(TypedDict, total=False):
prompt: list[AppChatPromptMessageResponse]
class AppCompletionPromptTextResponse(TypedDict):
text: str
class AppConversationHistoriesRoleResponse(TypedDict):
user_prefix: str
assistant_prefix: str
class AppCompletionPromptConfigResponse(TypedDict, total=False):
prompt: AppCompletionPromptTextResponse
conversation_histories_role: AppConversationHistoriesRoleResponse
class AppDatasetItemResponse(TypedDict):
dataset: AppDatasetReferenceResponse
class AppDatasetListResponse(TypedDict):
datasets: list[AppDatasetItemResponse]
strategy: NotRequired[str]
class AppRerankingModelResponse(TypedDict, total=False):
reranking_provider_name: str
reranking_model_name: str
class AppVectorSettingResponse(TypedDict):
vector_weight: float
embedding_provider_name: str
embedding_model_name: str
class AppKeywordSettingResponse(TypedDict):
keyword_weight: float
class AppWeightsResponse(TypedDict):
vector_setting: AppVectorSettingResponse
keyword_setting: AppKeywordSettingResponse
weight_type: NotRequired[Literal["semantic_first", "keyword_first", "customized"]]
@with_config(ConfigDict(extra="allow"))
class AppMetadataConditionResponse(TypedDict):
id: NotRequired[str]
metadata_id: NotRequired[str]
name: str
comparison_operator: SupportedComparisonOperator
value: NotRequired[str | list[str] | int | float | None]
class AppMetadataFilteringConditionsResponse(TypedDict, total=False):
logical_operator: Literal["and", "or"] | None
conditions: list[AppMetadataConditionResponse] | None
@with_config(ConfigDict(extra="allow"))
class AppDatasetConfigsResponse(TypedDict):
retrieval_model: Literal["single", "multiple"]
datasets: NotRequired[AppDatasetListResponse]
top_k: NotRequired[int]
score_threshold: NotRequired[float | None]
score_threshold_enabled: NotRequired[bool]
reranking_model: NotRequired[AppRerankingModelResponse | None]
weights: NotRequired[AppWeightsResponse | 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[AppModelSelectionResponse | None]
metadata_filtering_conditions: NotRequired[AppMetadataFilteringConditionsResponse | None]
class AppImageUploadResponse(TypedDict, total=False):
enabled: bool
number_limits: int
detail: Literal["low", "high"] | None
transfer_methods: list[FileTransferMethod]
class AppFileUploadResponse(TypedDict, total=False):
image: AppImageUploadResponse
enabled: bool
allowed_file_types: list[FileType]
allowed_file_extensions: list[str]
allowed_file_upload_methods: list[FileTransferMethod]
number_limits: int
class AppModelConfigResponse(ResponseModel):
opening_statement: str | None
suggested_questions: list[str] = Field(
validation_alias=AliasChoices("suggested_questions_list", "suggested_questions")
)
suggested_questions_after_answer: AppSuggestedQuestionsAfterAnswerResponse = Field(
validation_alias=AliasChoices("suggested_questions_after_answer_dict", "suggested_questions_after_answer"),
)
speech_to_text: AppEnabledConfigResponse = Field(
validation_alias=AliasChoices("speech_to_text_dict", "speech_to_text")
)
text_to_speech: AppTextToSpeechResponse = Field(
validation_alias=AliasChoices("text_to_speech_dict", "text_to_speech")
)
retriever_resource: AppEnabledConfigResponse = Field(
validation_alias=AliasChoices("retriever_resource_dict", "retriever_resource")
)
annotation_reply: AppAnnotationReplyEnabledResponse | AppAnnotationReplyDisabledResponse = Field(
validation_alias=AliasChoices("annotation_reply_dict", "annotation_reply")
)
more_like_this: AppEnabledConfigResponse = Field(
validation_alias=AliasChoices("more_like_this_dict", "more_like_this")
)
sensitive_word_avoidance: AppSensitiveWordAvoidanceResponse = Field(
validation_alias=AliasChoices("sensitive_word_avoidance_dict", "sensitive_word_avoidance")
)
external_data_tools: list[AppEnabledExternalDataToolResponse | AppDisabledExternalDataToolResponse] = Field(
validation_alias=AliasChoices("external_data_tools_list", "external_data_tools")
)
model: AppModelSelectionResponse = Field(validation_alias=AliasChoices("model_dict", "model"))
user_input_form: list[AppUserInputFormResponse] = Field(
validation_alias=AliasChoices("user_input_form_list", "user_input_form")
)
dataset_query_variable: str | None
pre_prompt: str | None
agent_mode: AppAgentModeResponse = Field(validation_alias=AliasChoices("agent_mode_dict", "agent_mode"))
prompt_type: PromptType
chat_prompt_config: AppChatPromptConfigResponse = Field(
validation_alias=AliasChoices("chat_prompt_config_dict", "chat_prompt_config")
)
completion_prompt_config: AppCompletionPromptConfigResponse = Field(
validation_alias=AliasChoices("completion_prompt_config_dict", "completion_prompt_config")
)
dataset_configs: AppDatasetConfigsResponse = Field(
validation_alias=AliasChoices("dataset_configs_dict", "dataset_configs")
)
file_upload: AppFileUploadResponse = Field(validation_alias=AliasChoices("file_upload_dict", "file_upload"))
created_by: str | None
created_at: int
updated_by: str | None
updated_at: int
@field_validator("created_at", "updated_at", mode="before")
@classmethod
def _normalize_timestamp(cls, value: datetime | int | None) -> int | None:
return to_timestamp(value)
+1 -1
View File
@@ -546,7 +546,7 @@ class AgentFeatureToggleConfig(AgentFlexibleConfig):
class AgentTextToSpeechFeatureConfig(AgentFeatureToggleConfig):
language: str | None = None
voice: str | None = None
autoPlay: str | None = None
autoPlay: Literal["enabled", "disabled"] | None = None
class AgentSuggestedQuestionsAfterAnswerModelConfig(AgentFlexibleConfig):
+19 -5
View File
@@ -99,6 +99,13 @@ class EnabledConfig(TypedDict):
enabled: bool
class TextToSpeechConfig(TypedDict):
enabled: bool
voice: NotRequired[str]
language: NotRequired[str]
autoPlay: NotRequired[Literal["enabled", "disabled"]]
class SuggestedQuestionsAfterAnswerModelConfig(TypedDict):
provider: str
name: str
@@ -148,7 +155,7 @@ class AgentToolConfig(TypedDict):
class AgentModeConfig(TypedDict):
enabled: bool
strategy: str | None
strategy: NotRequired[str | None]
tools: list[AgentToolConfig | dict[str, Any]]
prompt: str | None
@@ -175,6 +182,9 @@ class ExternalDataToolConfig(TypedDict):
variable: str
type: str
config: dict[str, Any]
label: NotRequired[str]
icon: NotRequired[str]
icon_background: NotRequired[str]
class UserInputFormItemConfig(TypedDict):
@@ -187,6 +197,9 @@ class UserInputFormItemConfig(TypedDict):
default: NotRequired[str]
type: NotRequired[str]
config: NotRequired[dict[str, Any]]
enabled: NotRequired[bool]
icon: NotRequired[str]
icon_background: NotRequired[str]
# Each item is a single-key dict, e.g. {"text-input": UserInputFormItemConfig}
@@ -202,6 +215,7 @@ class DatasetConfigs(TypedDict):
reranking_model: NotRequired[dict[str, Any] | None]
weights: NotRequired[dict[str, Any] | None]
reranking_enabled: NotRequired[bool]
reranking_enable: NotRequired[bool]
reranking_mode: NotRequired[str]
metadata_filtering_mode: NotRequired[str]
metadata_model_config: NotRequired[dict[str, Any] | None]
@@ -243,7 +257,7 @@ class AppModelConfigDict(TypedDict):
suggested_questions: list[str]
suggested_questions_after_answer: SuggestedQuestionsAfterAnswerConfig
speech_to_text: EnabledConfig
text_to_speech: EnabledConfig
text_to_speech: TextToSpeechConfig
retriever_resource: EnabledConfig
annotation_reply: AnnotationReplyConfig
more_like_this: EnabledConfig
@@ -786,8 +800,8 @@ class AppModelConfig(TypeBase):
return self._get_enabled_config(self.speech_to_text)
@property
def text_to_speech_dict(self) -> EnabledConfig:
return self._get_enabled_config(self.text_to_speech)
def text_to_speech_dict(self) -> TextToSpeechConfig:
return cast(TextToSpeechConfig, self._get_enabled_config(self.text_to_speech))
@property
def retriever_resource_dict(self) -> EnabledConfig:
@@ -843,7 +857,7 @@ class AppModelConfig(TypeBase):
if self.dataset_configs:
dataset_configs = json.loads(self.dataset_configs)
if "retrieval_model" not in dataset_configs:
return {"retrieval_model": "single"}
return {**dataset_configs, "retrieval_model": "single"}
else:
return cast(DatasetConfigs, dataset_configs)
return {
+529 -118
View File
@@ -13670,41 +13670,41 @@ Model class for AI model.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| access_mode | string | | No |
| access_mode | [WebAppAccessMode](#webappaccessmode) | | Yes |
| access_ready | boolean | | No |
| api_base_url | string | | No |
| api_base_url | string | | Yes |
| app_id | string | | No |
| backing_app_id | string | | No |
| bound_agent_id | string | | No |
| created_at | integer | | No |
| created_by | string | | No |
| created_at | integer | | Yes |
| created_by | string | | Yes |
| debug_conversation_has_messages | boolean | | No |
| debug_conversation_id | string | | No |
| debug_conversation_message_count | integer | | No |
| deleted_tools | [ [DeletedTool](#deletedtool) ] | | No |
| description | string | | No |
| deleted_tools | [ [DeletedTool](#deletedtool) ] | | Yes |
| description | string | | Yes |
| enable_api | boolean | | Yes |
| enable_site | boolean | | Yes |
| hidden_app_backed | boolean | | No |
| icon | string | | No |
| icon_background | string | | No |
| icon_type | string | | No |
| icon | string | | Yes |
| icon_background | string | | Yes |
| icon_type | [IconType](#icontype) | | Yes |
| icon_url | string | | Yes |
| id | string | | Yes |
| maintainer | string | | No |
| max_active_requests | integer | | No |
| mode | string | | Yes |
| model_config | [AppModelConfigResponse](#appmodelconfigresponse) | | No |
| maintainer | string | | Yes |
| max_active_requests | integer | | Yes |
| mode | [AppMode](#appmode) | | Yes |
| model_config | [AppModelConfigResponse](#appmodelconfigresponse) | | Yes |
| name | string | | Yes |
| permission_keys | [ string ] | | Yes |
| role | string | | No |
| site | [AppDetailSiteResponse](#appdetailsiteresponse) | | No |
| tags | [ [Tag](#tag) ] | | No |
| tracing | | | No |
| updated_at | integer | | No |
| updated_by | string | | No |
| use_icon_as_answer_icon | boolean | | No |
| workflow | [WorkflowPartial](#workflowpartial) | | No |
| site | [AppDetailSiteResponse](#appdetailsiteresponse) | | Yes |
| tags | [ [Tag](#tag) ] | | Yes |
| tracing | string | | Yes |
| updated_at | integer | | Yes |
| updated_by | string | | Yes |
| use_icon_as_answer_icon | boolean | | Yes |
| workflow | [WorkflowPartial](#workflowpartial) | | Yes |
#### AgentAppFeaturesPayload
@@ -13752,7 +13752,7 @@ default (the config form sends the full desired feature state on save).
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| access_mode | string | | No |
| access_mode | [WebAppAccessMode](#webappaccessmode) | | No |
| active_config_is_published | boolean | | No |
| app_id | string | | No |
| author_name | string | | No |
@@ -13767,13 +13767,13 @@ default (the config form sends the full desired feature state on save).
| hidden_app_backed | boolean | | No |
| icon | string | | No |
| icon_background | string | | No |
| icon_type | string | | No |
| icon_type | [IconType](#icontype) | | No |
| icon_url | string | | Yes |
| id | string | | Yes |
| is_starred | boolean | | No |
| maintainer | string | | No |
| max_active_requests | integer | | No |
| mode | string | | Yes |
| mode | [AppMode](#appmode) | | Yes |
| model_config | [ModelConfigPartial](#modelconfigpartial) | | No |
| name | string | | Yes |
| permission_keys | [ string ] | | Yes |
@@ -15357,7 +15357,7 @@ Legacy Chat App model config used only for follow-up question generation.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| autoPlay | string | | No |
| autoPlay | string, <br>**Available values:** "disabled", "enabled" | | No |
| enabled | boolean | | No |
| language | string | | No |
| voice | string | | No |
@@ -15742,99 +15742,220 @@ This class is used to store the schema information of an api based tool.
| app_id | string | | No |
| items | [ [AccessMatrixItem](#accessmatrixitem) ] | | No |
#### AppAgentModeResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enabled | boolean | | Yes |
| max_iteration | integer | | No |
| prompt | [AppAgentPromptResponse](#appagentpromptresponse)<br>string | | No |
| 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 |
#### AppAgentPromptResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| first_prompt | string | | No |
| next_iteration | string | | No |
#### AppAnnotationReplyDisabledResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enabled | boolean | | Yes |
#### AppAnnotationReplyEnabledResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| embedding_model | [AppEmbeddingModelResponse](#appembeddingmodelresponse) | | Yes |
| enabled | boolean | | Yes |
| id | string | | Yes |
| score_threshold | number | | Yes |
#### AppApiStatusPayload
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enable_api | boolean | Enable or disable API | Yes |
#### AppChatPromptConfigResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| prompt | [ [AppChatPromptMessageResponse](#appchatpromptmessageresponse) ] | | No |
#### AppChatPromptMessageResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| role | string | | Yes |
| text | string | | Yes |
#### AppCheckboxFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| checkbox | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppCompletionPromptConfigResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| conversation_histories_role | [AppConversationHistoriesRoleResponse](#appconversationhistoriesroleresponse) | | No |
| prompt | [AppCompletionPromptTextResponse](#appcompletionprompttextresponse) | | No |
#### AppCompletionPromptTextResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| text | string | | Yes |
#### AppConversationHistoriesRoleResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| assistant_prefix | string | | Yes |
| user_prefix | string | | Yes |
#### AppDatasetConfigsResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| datasets | [AppDatasetListResponse](#appdatasetlistresponse) | | No |
| metadata_filtering_conditions | [AppMetadataFilteringConditionsResponse](#appmetadatafilteringconditionsresponse) | | No |
| metadata_filtering_mode | string, <br>**Available values:** "automatic", "disabled", "manual" | *Enum:* `"automatic"`, `"disabled"`, `"manual"` | No |
| metadata_model_config | [AppModelSelectionResponse](#appmodelselectionresponse) | | No |
| reranking_enable | boolean | | No |
| reranking_enabled | boolean | | No |
| reranking_mode | [RerankMode](#rerankmode) | | No |
| reranking_model | [AppRerankingModelResponse](#apprerankingmodelresponse) | | No |
| retrieval_model | string, <br>**Available values:** "multiple", "single" | *Enum:* `"multiple"`, `"single"` | Yes |
| score_threshold | number | | No |
| score_threshold_enabled | boolean | | No |
| top_k | integer | | No |
| weights | [AppWeightsResponse](#appweightsresponse) | | No |
#### AppDatasetItemResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| dataset | [AppDatasetReferenceResponse](#appdatasetreferenceresponse) | | Yes |
#### AppDatasetListResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| datasets | [ [AppDatasetItemResponse](#appdatasetitemresponse) ] | | Yes |
| strategy | string | | No |
#### AppDatasetReferenceResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enabled | boolean | | No |
| id | string | | No |
#### AppDetail
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| access_mode | string | | No |
| created_at | integer | | No |
| created_by | string | | No |
| description | string | | No |
| access_mode | [WebAppAccessMode](#webappaccessmode) | | Yes |
| created_at | integer | | Yes |
| created_by | string | | Yes |
| description | string | | Yes |
| enable_api | boolean | | Yes |
| enable_site | boolean | | Yes |
| icon | string | | No |
| icon_background | string | | No |
| icon | string | | Yes |
| icon_background | string | | Yes |
| id | string | | Yes |
| maintainer | string | | No |
| mode | string | | Yes |
| model_config | [AppModelConfigResponse](#appmodelconfigresponse) | | No |
| maintainer | string | | Yes |
| mode | [AppMode](#appmode) | | Yes |
| model_config | [AppModelConfigResponse](#appmodelconfigresponse) | | Yes |
| name | string | | Yes |
| permission_keys | [ string ] | | No |
| tags | [ [Tag](#tag) ] | | No |
| tracing | | | No |
| updated_at | integer | | No |
| updated_by | string | | No |
| use_icon_as_answer_icon | boolean | | No |
| workflow | [WorkflowPartial](#workflowpartial) | | No |
| permission_keys | [ string ] | | Yes |
| tags | [ [Tag](#tag) ] | | Yes |
| tracing | string | | Yes |
| updated_at | integer | | Yes |
| updated_by | string | | Yes |
| use_icon_as_answer_icon | boolean | | Yes |
| workflow | [WorkflowPartial](#workflowpartial) | | Yes |
#### AppDetailSiteResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| access_token | string | | No |
| app_base_url | string | | No |
| chat_color_theme | string | | No |
| chat_color_theme_inverted | boolean | | No |
| code | string | | No |
| copyright | string | | No |
| created_at | integer | | No |
| created_by | string | | No |
| custom_disclaimer | string | | No |
| customize_domain | string | | No |
| customize_token_strategy | string | | No |
| default_language | string | | No |
| description | string | | No |
| icon | string | | No |
| icon_background | string | | No |
| icon_type | string<br>[IconType](#icontype) | | No |
| access_token | string | | Yes |
| app_base_url | string | | Yes |
| chat_color_theme | string | | Yes |
| chat_color_theme_inverted | boolean | | Yes |
| code | string | | Yes |
| copyright | string | | Yes |
| created_at | integer | | Yes |
| created_by | string | | Yes |
| custom_disclaimer | string | | Yes |
| customize_domain | string | | Yes |
| customize_token_strategy | [CustomizeTokenStrategy](#customizetokenstrategy) | | Yes |
| default_language | string | | Yes |
| description | string | | Yes |
| icon | string | | Yes |
| icon_background | string | | Yes |
| icon_type | [IconType](#icontype) | | Yes |
| icon_url | string | | Yes |
| input_placeholder | string | | No |
| privacy_policy | string | | No |
| prompt_public | boolean | | No |
| show_workflow_steps | boolean | | No |
| title | string | | No |
| updated_at | integer | | No |
| updated_by | string | | No |
| use_icon_as_answer_icon | boolean | | No |
| input_placeholder | string | | Yes |
| privacy_policy | string | | Yes |
| prompt_public | boolean | | Yes |
| show_workflow_steps | boolean | | Yes |
| title | string | | Yes |
| updated_at | integer | | Yes |
| updated_by | string | | Yes |
| use_icon_as_answer_icon | boolean | | Yes |
#### AppDetailWithSite
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| access_mode | string | | No |
| api_base_url | string | | No |
| app_id | string | | No |
| bound_agent_id | string | | No |
| created_at | integer | | No |
| created_by | string | | No |
| deleted_tools | [ [DeletedTool](#deletedtool) ] | | No |
| description | string | | No |
| access_mode | [WebAppAccessMode](#webappaccessmode) | | Yes |
| api_base_url | string | | Yes |
| app_id | string | | Yes |
| bound_agent_id | string | | Yes |
| created_at | integer | | Yes |
| created_by | string | | Yes |
| deleted_tools | [ [DeletedTool](#deletedtool) ] | | Yes |
| description | string | | Yes |
| enable_api | boolean | | Yes |
| enable_site | boolean | | Yes |
| icon | string | | No |
| icon_background | string | | No |
| icon_type | string | | No |
| icon | string | | Yes |
| icon_background | string | | Yes |
| icon_type | [IconType](#icontype) | | Yes |
| icon_url | string | | Yes |
| id | string | | Yes |
| maintainer | string | | No |
| max_active_requests | integer | | No |
| mode | string | | Yes |
| model_config | [AppModelConfigResponse](#appmodelconfigresponse) | | No |
| maintainer | string | | Yes |
| max_active_requests | integer | | Yes |
| mode | [AppMode](#appmode) | | Yes |
| model_config | [AppModelConfigResponse](#appmodelconfigresponse) | | Yes |
| name | string | | Yes |
| permission_keys | [ string ] | | No |
| site | [AppDetailSiteResponse](#appdetailsiteresponse) | | No |
| tags | [ [Tag](#tag) ] | | No |
| tracing | | | No |
| updated_at | integer | | No |
| updated_by | string | | No |
| use_icon_as_answer_icon | boolean | | No |
| workflow | [WorkflowPartial](#workflowpartial) | | No |
| permission_keys | [ string ] | | Yes |
| site | [AppDetailSiteResponse](#appdetailsiteresponse) | | Yes |
| tags | [ [Tag](#tag) ] | | Yes |
| tracing | string | | Yes |
| updated_at | integer | | Yes |
| updated_by | string | | Yes |
| use_icon_as_answer_icon | boolean | | Yes |
| workflow | [WorkflowPartial](#workflowpartial) | | Yes |
#### AppDisabledExternalDataToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | object | | No |
| enabled | boolean | | Yes |
| icon | string | | No |
| icon_background | string | | No |
| label | string | | No |
| type | string | | No |
| variable | string | | No |
#### AppDslVersionResponse
@@ -15842,6 +15963,31 @@ This class is used to store the schema information of an api based tool.
| ---- | ---- | ----------- | -------- |
| app_dsl_version | string | | Yes |
#### AppEmbeddingModelResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| embedding_model_name | string | | Yes |
| embedding_provider_name | string | | Yes |
#### AppEnabledConfigResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enabled | boolean | | Yes |
#### AppEnabledExternalDataToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | object | | Yes |
| enabled | boolean | | Yes |
| icon | string | | No |
| icon_background | string | | No |
| label | string | | No |
| type | string | | Yes |
| variable | string | | Yes |
#### AppExportQuery
| Name | Type | Description | Required |
@@ -15857,6 +16003,35 @@ This class is used to store the schema information of an api based tool.
| ---- | ---- | ----------- | -------- |
| data | string | | Yes |
#### AppExternalDataToolFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| external_data_tool | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppFileFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| file | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppFileListFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| file-list | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppFileUploadResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| allowed_file_extensions | [ string ] | | No |
| allowed_file_types | [ [FileType](#filetype) ] | | No |
| allowed_file_upload_methods | [ [FileTransferMethod](#filetransfermethod) ] | | No |
| enabled | boolean | | No |
| image | [AppImageUploadResponse](#appimageuploadresponse) | | No |
| number_limits | integer | | No |
#### AppIconPayload
| Name | Type | Description | Required |
@@ -15865,6 +16040,15 @@ 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 |
#### AppImageUploadResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| detail | string, <br>**Available values:** "high", "low" | | No |
| enabled | boolean | | No |
| number_limits | integer | | No |
| transfer_methods | [ [FileTransferMethod](#filetransfermethod) ] | | No |
#### AppImportPayload
| Name | Type | Description | Required |
@@ -15884,7 +16068,7 @@ This class is used to store the schema information of an api based tool.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| app_id | string | | No |
| app_mode | string | | No |
| app_mode | [AppMode](#appmode) | | No |
| current_dsl_version | string | | Yes |
| error | string | | No |
| id | string | | Yes |
@@ -15892,6 +16076,62 @@ This class is used to store the schema information of an api based tool.
| status | [ImportStatus](#importstatus) | | Yes |
| warnings | [ [DslImportWarning](#dslimportwarning) ] | | No |
#### AppJsonObjectFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| json_object | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppKeywordSettingResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| keyword_weight | number | | Yes |
#### AppLegacyCurrentDatetimeToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| current_datetime | object | | Yes |
#### AppLegacyDatasetToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| dataset | [AppDatasetReferenceResponse](#appdatasetreferenceresponse) | | Yes |
#### AppLegacyGoogleSearchToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| google_search | object | | Yes |
#### AppLegacySensitiveWordToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| canned_response | string | | Yes |
| enabled | boolean | | Yes |
| words | [ string ] | | Yes |
#### AppLegacySensitiveWordToolResponseItem
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| sensitive-word-avoidance | [AppLegacySensitiveWordToolResponse](#applegacysensitivewordtoolresponse) | | Yes |
#### AppLegacyWebReaderToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| web_reader | object | | Yes |
#### AppLegacyWikipediaToolResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| wikipedia | object | | Yes |
#### AppListQuery
| Name | Type | Description | Required |
@@ -15926,6 +16166,23 @@ AppMCPServer Status Enum
| ---- | ---- | ----------- | -------- |
| AppMCPServerStatus | string | AppMCPServer Status Enum | |
#### AppMetadataConditionResponse
| 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 |
#### AppMetadataFilteringConditionsResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| conditions | [ [AppMetadataConditionResponse](#appmetadataconditionresponse) ] | | No |
| logical_operator | string, <br>**Available values:** "and", "or" | | No |
#### AppMode
| Name | Type | Description | Required |
@@ -15936,30 +16193,39 @@ AppMCPServer Status Enum
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| agent_mode | | | No |
| annotation_reply | | | No |
| chat_prompt_config | | | No |
| completion_prompt_config | | | No |
| created_at | integer | | No |
| created_by | string | | No |
| dataset_configs | | | No |
| dataset_query_variable | string | | No |
| external_data_tools | | | No |
| file_upload | | | No |
| model | | | No |
| more_like_this | | | No |
| opening_statement | string | | No |
| pre_prompt | string | | No |
| prompt_type | string | | No |
| retriever_resource | | | No |
| sensitive_word_avoidance | | | No |
| speech_to_text | | | No |
| suggested_questions | | | No |
| suggested_questions_after_answer | | | No |
| text_to_speech | | | No |
| updated_at | integer | | No |
| updated_by | string | | No |
| user_input_form | | | No |
| agent_mode | [AppAgentModeResponse](#appagentmoderesponse) | | Yes |
| annotation_reply | [AppAnnotationReplyEnabledResponse](#appannotationreplyenabledresponse)<br>[AppAnnotationReplyDisabledResponse](#appannotationreplydisabledresponse) | | Yes |
| chat_prompt_config | [AppChatPromptConfigResponse](#appchatpromptconfigresponse) | | Yes |
| completion_prompt_config | [AppCompletionPromptConfigResponse](#appcompletionpromptconfigresponse) | | Yes |
| created_at | integer | | Yes |
| created_by | string | | Yes |
| dataset_configs | [AppDatasetConfigsResponse](#appdatasetconfigsresponse) | | Yes |
| dataset_query_variable | string | | Yes |
| external_data_tools | [ [AppEnabledExternalDataToolResponse](#appenabledexternaldatatoolresponse)<br>[AppDisabledExternalDataToolResponse](#appdisabledexternaldatatoolresponse) ] | | Yes |
| file_upload | [AppFileUploadResponse](#appfileuploadresponse) | | Yes |
| model | [AppModelSelectionResponse](#appmodelselectionresponse) | | Yes |
| more_like_this | [AppEnabledConfigResponse](#appenabledconfigresponse) | | Yes |
| opening_statement | string | | Yes |
| pre_prompt | string | | Yes |
| prompt_type | [PromptType](#prompttype) | | Yes |
| retriever_resource | [AppEnabledConfigResponse](#appenabledconfigresponse) | | Yes |
| sensitive_word_avoidance | [AppSensitiveWordAvoidanceResponse](#appsensitivewordavoidanceresponse) | | Yes |
| speech_to_text | [AppEnabledConfigResponse](#appenabledconfigresponse) | | Yes |
| suggested_questions | [ string ] | | Yes |
| suggested_questions_after_answer | [AppSuggestedQuestionsAfterAnswerResponse](#appsuggestedquestionsafteranswerresponse) | | Yes |
| text_to_speech | [AppTextToSpeechResponse](#apptexttospeechresponse) | | Yes |
| updated_at | integer | | Yes |
| 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 |
#### AppModelSelectionResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| completion_params | object | | No |
| mode | [LLMMode](#llmmode)<br>string | | No |
| name | string | | No |
| provider | string | | No |
#### AppNamePayload
@@ -15967,6 +16233,12 @@ AppMCPServer Status Enum
| ---- | ---- | ----------- | -------- |
| name | string | Name to check | Yes |
#### AppNumberFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| number | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppPagination
| Name | Type | Description | Required |
@@ -15977,11 +16249,17 @@ AppMCPServer Status Enum
| page | integer | | Yes |
| total | integer | | Yes |
#### AppParagraphFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| paragraph | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppPartial
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| access_mode | string | | No |
| access_mode | [WebAppAccessMode](#webappaccessmode) | | No |
| app_id | string | | No |
| author_name | string | | No |
| bound_agent_id | string | | No |
@@ -15992,13 +16270,13 @@ AppMCPServer Status Enum
| has_draft_trigger | boolean | | No |
| icon | string | | No |
| icon_background | string | | No |
| icon_type | string | | No |
| icon_type | [IconType](#icontype) | | No |
| icon_url | string | | Yes |
| id | string | | Yes |
| is_starred | boolean | | No |
| maintainer | string | | No |
| max_active_requests | integer | | No |
| mode | string | | Yes |
| mode | [AppMode](#appmode) | | Yes |
| model_config | [ModelConfigPartial](#modelconfigpartial) | | No |
| name | string | | Yes |
| permission_keys | [ string ] | | No |
@@ -16008,12 +16286,50 @@ AppMCPServer Status Enum
| use_icon_as_answer_icon | boolean | | No |
| workflow | [WorkflowPartial](#workflowpartial) | | No |
#### AppProviderAgentToolResponse
| 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 |
#### AppRerankingModelResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| reranking_model_name | string | | No |
| reranking_provider_name | string | | No |
#### AppSelectFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| select | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppSelectorScope
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| AppSelectorScope | string | | |
#### AppSensitiveWordAvoidanceResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| config | object | | No |
| configs | [ [JsonValue](#jsonvalue) ] | | No |
| enabled | boolean | | Yes |
| type | string | | No |
#### AppSiteResponse
| Name | Type | Description | Required |
@@ -16063,6 +16379,29 @@ AppMCPServer Status Enum
| title | string | | No |
| use_icon_as_answer_icon | boolean | | No |
#### AppSuggestedQuestionsAfterAnswerResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| enabled | boolean | | Yes |
| model | [AppModelSelectionResponse](#appmodelselectionresponse) | | No |
| prompt | string | | No |
#### AppTextInputFormResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| text-input | [AppUserInputFormConfigResponse](#appuserinputformconfigresponse) | | Yes |
#### AppTextToSpeechResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| autoPlay | string, <br>**Available values:** "disabled", "enabled" | *Enum:* `"disabled"`, `"enabled"` | No |
| enabled | boolean | | Yes |
| language | string | | No |
| voice | string | | No |
#### AppTracePayload
| Name | Type | Description | Required |
@@ -16077,6 +16416,28 @@ AppMCPServer Status Enum
| enabled | boolean | | No |
| tracing_provider | string | | No |
#### AppUserInputFormConfigResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| allowed_file_extensions | [ string ] | | No |
| allowed_file_types | [ [FileType](#filetype) ] | | No |
| allowed_file_upload_methods | [ [FileTransferMethod](#filetransfermethod) ] | | No |
| config | object | | No |
| default | [JsonValue](#jsonvalue) | | 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 |
#### AppVariableConfig
| Name | Type | Description | Required |
@@ -16086,6 +16447,22 @@ AppMCPServer Status Enum
| required | boolean | | No |
| type | string | | Yes |
#### AppVectorSettingResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| embedding_model_name | string | | Yes |
| embedding_provider_name | string | | Yes |
| vector_weight | number | | Yes |
#### AppWeightsResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| keyword_setting | [AppKeywordSettingResponse](#appkeywordsettingresponse) | | Yes |
| vector_setting | [AppVectorSettingResponse](#appvectorsettingresponse) | | Yes |
| weight_type | string, <br>**Available values:** "customized", "keyword_first", "semantic_first" | *Enum:* `"customized"`, `"keyword_first"`, `"semantic_first"` | No |
#### AudioBinaryResponse
| Name | Type | Description | Required |
@@ -16977,6 +17354,14 @@ Model class for provider custom model configuration.
| model_type | [ModelType](#modeltype) | | Yes |
| unadded_to_model_list | boolean | | No |
#### CustomizeTokenStrategy
Site token customization strategy
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| CustomizeTokenStrategy | string | Site token customization strategy | |
#### CustomizedPipelineTemplatePayload
| Name | Type | Description | Required |
@@ -19050,7 +19435,7 @@ How Dify forwards the end-user's identity to an MCP server.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| app_id | string | | No |
| app_mode | string | | No |
| app_mode | [AppMode](#appmode) | | No |
| current_dsl_version | string, <br>**Default:** 0.7.0 | | No |
| error | string | | No |
| id | string | | Yes |
@@ -19707,7 +20092,7 @@ Metadata operation data
| ---- | ---- | ----------- | -------- |
| created_at | integer | | No |
| created_by | string | | No |
| model | | | No |
| model | [AppModelSelectionResponse](#appmodelselectionresponse) | | No |
| pre_prompt | string | | No |
| updated_at | integer | | No |
| updated_by | string | | No |
@@ -20755,6 +21140,12 @@ Shared permission levels for resources (datasets, credentials, etc.)
| unit | string | | No |
| variable | string | | Yes |
#### PlanningStrategy
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| PlanningStrategy | string | | |
#### PluginAutoUpgradeChangeResponse
| Name | Type | Description | Required |
@@ -21263,6 +21654,14 @@ Dataset Process Rule Mode
| mode | [ProcessRuleMode](#processrulemode) | | Yes |
| rules | [Rule](#rule) | | No |
#### PromptType
Prompt configuration type
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| PromptType | string | Prompt configuration type | |
#### ProviderConfig
Model class for common provider settings like credentials
@@ -21755,6 +22154,12 @@ Resource types understood by access policies.
| request | object | The request of the request log | Yes |
| response | object | The response of the request log | Yes |
#### RerankMode
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| RerankMode | string | | |
#### RerankingModel
| Name | Type | Description | Required |
@@ -24022,6 +24427,12 @@ in form definition, or a variable while the workflow is running.
| is_valid | boolean | | Yes |
| token | string | | Yes |
#### WebAppAccessMode
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| WebAppAccessMode | string | | |
#### WebAppAuthModel
| Name | Type | Description | Required |
@@ -25112,7 +25523,7 @@ Query parameters for workflow runs.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| autoPlay | string | | No |
| autoPlay | string, <br>**Available values:** "disabled", "enabled" | | No |
| enabled | boolean | | No |
| language | string | | No |
| voice | string | | No |
+2 -2
View File
@@ -694,7 +694,7 @@ Request body for POST /workspaces/<workspace_id>/apps/imports.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| app_id | string | | No |
| app_mode | string | | No |
| app_mode | [AppMode](#appmode) | | No |
| current_dsl_version | string, <br>**Default:** 0.7.0 | | No |
| error | string | | No |
| hints | [ [Hint](#hint) ] | Next steps the caller can take | No |
@@ -971,7 +971,7 @@ A next step the caller can hand straight to `call <op> --input <input>`.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| app_id | string | | No |
| app_mode | string | | No |
| app_mode | [AppMode](#appmode) | | No |
| current_dsl_version | string, <br>**Default:** 0.7.0 | | No |
| error | string | | No |
| id | string | | Yes |
+3 -2
View File
@@ -8,6 +8,7 @@ from typing import BinaryIO, Literal, Protocol
from uuid import UUID, uuid4
from machinery.context import RequestContext
from models.model import AppMode
from services.agent.errors import InvalidRosterAgentPackageError
from services.agent.roster_package_entities import RosterAgentPackageExport
from services.entities.app_entities import (
@@ -228,7 +229,7 @@ class ConsoleAppService:
id=str(uuid4()),
status=ImportStatus.COMPLETED_WITH_WARNINGS if result.warnings else ImportStatus.COMPLETED,
app_id=result.app_id,
app_mode="agent",
app_mode=AppMode.AGENT,
warnings=result.warnings,
)
@@ -300,7 +301,7 @@ class ConsoleAppService:
def update(self, context: RequestContext, app_id: str, params: UpdateAppParams) -> AppRecord:
app = self._apps.update(context, app_id, params)
self._lifecycle.updated(context, app)
return self._lifecycle.present(context, app)
return self.get(context, app_id)
def rename(self, context: RequestContext, app_id: str, name: str) -> AppRecord:
app = self._apps.rename(context, app_id, name)
+6
View File
@@ -17,6 +17,10 @@ from sqlalchemy.orm import Session
from configs import dify_config
from constants.dsl_version import CURRENT_APP_DSL_VERSION
from core.app.app_config.features.suggested_questions_after_answer.manager import (
SuggestedQuestionsAfterAnswerConfigManager,
)
from core.app.app_config.features.text_to_speech.manager import TextToSpeechConfigManager
from core.file import remote_fetcher
from core.plugin.entities.plugin import PluginDependency
from core.rbac import RBACPermission, RBACResourceScope
@@ -694,6 +698,8 @@ class AppDslService:
model_config = data.get("model_config")
if not model_config or not isinstance(model_config, dict):
raise ValueError("Missing model_config for chat/agent-chat/completion app")
SuggestedQuestionsAfterAnswerConfigManager.validate_optional_fields(model_config)
TextToSpeechConfigManager.validate_optional_fields(model_config)
# Initialize or update model config
app_model_config = (
self._session.get(AppModelConfig, app.app_model_config_id) if app.app_model_config_id else None
+32 -44
View File
@@ -1,9 +1,10 @@
import json
import logging
from collections.abc import Callable, Mapping, Sequence
from copy import deepcopy
from dataclasses import dataclass
from datetime import datetime
from typing import Any, NotRequired, TypedDict, cast, override
from typing import Any, NotRequired, TypedDict, cast
import sqlalchemy as sa
from pydantic import JsonValue
@@ -120,9 +121,13 @@ class AppModelConfigResponseView:
class AppResponseView:
"""Expose App response properties through one caller-owned database session."""
def __init__(self, app: App, *, session: Session) -> None:
def __init__(
self, app: App, *, session: Session, account: Account | None = None, access_mode: str | None = None
) -> None:
self._app = app
self._session = session
self._account = account
self._access_mode = access_mode
def __getattr__(self, name: str) -> Any:
return getattr(self._app, name) # guard-ignore: no-new-getattr -- delegates model fields
@@ -140,6 +145,19 @@ class AppResponseView:
app_model_config = self._app.app_model_config_with_session(session=self._session)
if app_model_config is None:
return None
if self._account is not None and (
self._app.mode == AppMode.AGENT_CHAT or self._app.is_agent_with_session(session=self._session)
):
tenant_id = self._account.current_tenant_id
assert tenant_id is not None
masked_agent_mode = mask_agent_tool_parameters(
agent_mode=cast(Mapping[str, JsonValue], app_model_config.agent_mode_dict),
app_id=self._app.id,
tenant_id=tenant_id,
user_id=self._account.id,
)
app_model_config = deepcopy(app_model_config)
app_model_config.agent_mode = json.dumps(masked_agent_mode)
return AppModelConfigResponseView(app_model_config, session=self._session)
@property
@@ -154,6 +172,18 @@ class AppResponseView:
def mode_compatible_with_agent(self) -> str:
return self._app.mode_compatible_with_agent_with_session(session=self._session)
@property
def access_mode(self) -> str | None:
return self._access_mode
@property
def permission_keys(self) -> list[str]:
return []
@property
def app_id(self) -> str | None:
return None
@property
def deleted_tools(self) -> list[Any]:
return self._app.deleted_tools_with_session(session=self._session)
@@ -580,48 +610,6 @@ class AppService:
if dify_config.DEPLOYMENT_EDITION == DeploymentEdition.CLOUD:
BillingService.clean_billing_info_cache(app.tenant_id)
def get_app(self, app: App, *, session: Session) -> App:
"""
Get App
"""
assert isinstance(current_user, Account)
assert current_user.current_tenant_id is not None
# get original app model config
if app.mode == AppMode.AGENT_CHAT or app.is_agent_with_session(session=session):
model_config = app.app_model_config_with_session(session=session)
if not model_config:
return app
agent_mode = mask_agent_tool_parameters(
agent_mode=cast(Mapping[str, JsonValue], model_config.agent_mode_dict),
app_id=app.id,
tenant_id=current_user.current_tenant_id,
user_id=current_user.id,
)
# override agent mode
if model_config:
model_config.agent_mode = json.dumps(agent_mode)
class ModifiedApp(App):
"""
Modified App class
"""
def __init__(self, app):
self.__dict__.update(app.__dict__)
@override
def app_model_config_with_session(self, *, session: Session) -> AppModelConfig | None:
# Hand back the in-memory config the masking pass above produced, and
# deliberately ignore `session`: re-reading the row here would undo the
# masking. Response paths resolve the config through this accessor
# (`AppResponseView.app_model_config`), so the override has to sit here.
return model_config
app = ModifiedApp(app)
return app
class ArgsDict(TypedDict):
name: str
description: str
+1 -2
View File
@@ -10,7 +10,6 @@ import logging
import uuid
from collections.abc import Iterable
from dataclasses import dataclass
from typing import cast
from flask import Response, stream_with_context
from sqlalchemy import select
@@ -269,7 +268,7 @@ class AudioService:
if not text_to_speech_dict.get("enabled"):
raise ValueError("TTS is not enabled")
voice = cast(str | None, text_to_speech_dict.get("voice"))
voice = text_to_speech_dict.get("voice")
return PreparedTextToAudio(text=text, voice=voice)
+1 -1
View File
@@ -82,7 +82,7 @@ class Import(BaseModel):
id: str
status: ImportStatus
app_id: str | None = None
app_mode: str | None = None
app_mode: AppMode | None = None
permission_keys: list[str] = Field(default_factory=list)
current_dsl_version: str = CURRENT_APP_DSL_VERSION
imported_dsl_version: str = ""
@@ -0,0 +1,86 @@
"""App detail responses must not write masked configuration back to storage."""
import json
from flask.testing import FlaskClient
from sqlalchemy.orm import Session
from models.model import AppMode, AppModelConfig
from tests.test_containers_integration_tests.controllers.console.helpers import (
authenticate_console_client,
create_console_account_and_tenant,
create_console_app,
)
def test_put_app_detail_preserves_agent_tool_secret_after_commit(
db_session_with_containers: Session,
test_client_with_containers: FlaskClient,
) -> None:
account, tenant = create_console_account_and_tenant(db_session_with_containers)
app = create_console_app(db_session_with_containers, tenant.id, account.id, AppMode.AGENT_CHAT)
agent_mode = {
"enabled": True,
"strategy": "react",
"tools": [
{
"provider_type": "api",
"provider_id": "00000000-0000-0000-0000-000000000001",
"tool_name": "private_tool",
"tool_parameters": {"api_key": "encrypted-secret"},
}
],
}
config = AppModelConfig(app_id=app.id, agent_mode=json.dumps(agent_mode))
db_session_with_containers.add(config)
db_session_with_containers.flush()
app.app_model_config_id = config.id
db_session_with_containers.commit()
headers = authenticate_console_client(test_client_with_containers, account)
response = test_client_with_containers.put(
f"/console/api/apps/{app.id}",
headers=headers,
json={"name": "Updated App"},
)
assert response.status_code == 200
assert response.json is not None
assert response.json["model_config"]["agent_mode"]["tools"][0]["tool_parameters"] != {"api_key": "encrypted-secret"}
with Session(db_session_with_containers.get_bind()) as reread_session:
persisted = reread_session.get(AppModelConfig, config.id)
assert persisted is not None
assert persisted.agent_mode is not None
assert json.loads(persisted.agent_mode) == agent_mode
def test_get_and_put_app_detail_preserve_historical_agent_mode_without_tools(
db_session_with_containers: Session,
test_client_with_containers: FlaskClient,
) -> None:
account, tenant = create_console_account_and_tenant(db_session_with_containers)
app = create_console_app(db_session_with_containers, tenant.id, account.id, AppMode.AGENT_CHAT)
agent_mode = {"enabled": True, "max_iteration": 5, "strategy": "function_call"}
config = AppModelConfig(app_id=app.id, agent_mode=json.dumps(agent_mode))
db_session_with_containers.add(config)
db_session_with_containers.flush()
app.app_model_config_id = config.id
db_session_with_containers.commit()
headers = authenticate_console_client(test_client_with_containers, account)
get_response = test_client_with_containers.get(f"/console/api/apps/{app.id}", headers=headers)
assert get_response.status_code == 200
assert get_response.json is not None
assert get_response.json["model_config"]["agent_mode"] == agent_mode
put_response = test_client_with_containers.put(
f"/console/api/apps/{app.id}", headers=headers, json={"name": "Updated App"}
)
assert put_response.status_code == 200
assert put_response.json is not None
assert put_response.json["model_config"]["agent_mode"] == agent_mode
with Session(db_session_with_containers.get_bind()) as reread_session:
persisted = reread_session.get(AppModelConfig, config.id)
assert persisted is not None
assert persisted.agent_mode == json.dumps(agent_mode)
@@ -151,55 +151,6 @@ class TestAppService:
assert app.tenant_id == tenant.id
assert app.created_by == account.id
def test_get_app_success(self, db_session_with_containers: Session, mock_external_service_dependencies):
"""
Test successful app retrieval.
"""
fake = Faker()
# Create account and tenant first
account = AccountService.create_account(
email=fake.email(),
name=fake.name(),
interface_language="en-US",
password=generate_valid_password(fake),
session=db_session_with_containers,
)
TenantService.create_owner_tenant_if_not_exist(account, name=fake.company(), session=db_session_with_containers)
tenant = account.current_tenant
# Create app first
# Import here to avoid circular dependency
from services.app_service import AppService, CreateAppParams
app_params = CreateAppParams(
name=fake.company(),
description=fake.text(max_nb_chars=100),
mode="chat",
icon_type="emoji",
icon="🎯",
icon_background="#45B7D1",
)
app_service = AppService()
created_app = app_service.create_app(tenant.id, app_params, account, session=db_session_with_containers)
# Get app using the service - needs current_user mock
mock_current_user = create_autospec(Account, instance=True)
mock_current_user.id = account.id
mock_current_user.current_tenant_id = account.current_tenant_id
with patch("services.app_service.current_user", mock_current_user):
retrieved_app = app_service.get_app(created_app, session=db_session_with_containers)
# Verify retrieved app matches created app
assert retrieved_app.id == created_app.id
assert retrieved_app.name == created_app.name
assert retrieved_app.description == created_app.description
assert retrieved_app.mode == created_app.mode
assert retrieved_app.tenant_id == created_app.tenant_id
assert retrieved_app.created_by == created_app.created_by
def test_get_paginate_apps_success(self, db_session_with_containers: Session, mock_external_service_dependencies):
"""
Test successful paginated app list retrieval.
@@ -812,6 +812,14 @@ def test_generate_specs_include_console_contract_shapes_for_schema_migration(tmp
)
assert {"enabled", "model", "prompt"} <= set(schemas["WorkflowSuggestedQuestionsAfterAnswerPayload"]["properties"])
assert {"enabled", "language", "voice", "autoPlay"} <= set(schemas["WorkflowTextToSpeechPayload"]["properties"])
assert schemas["WorkflowTextToSpeechPayload"]["properties"]["autoPlay"]["anyOf"][0]["enum"] == [
"disabled",
"enabled",
]
assert schemas["AgentTextToSpeechFeatureConfig"]["properties"]["autoPlay"]["anyOf"][0]["enum"] == [
"disabled",
"enabled",
]
assert {"enabled", "type", "config"} <= set(schemas["WorkflowSensitiveWordAvoidancePayload"]["properties"])
file_upload = schemas["WorkflowFileUploadPayload"]["properties"]
assert {"document", "audio", "video", "custom", "preview_config"} <= set(file_upload)
@@ -216,9 +216,9 @@ def _app_detail_obj(**overrides) -> App:
"tracing": None,
"use_icon_as_answer_icon": False,
"created_by": "account-1",
"created_at": None,
"created_at": datetime(2025, 1, 1),
"updated_by": "account-1",
"updated_at": None,
"updated_at": datetime(2025, 1, 1),
"max_active_requests": 0,
}
overrides.pop("bound_agent_id", None)
@@ -369,9 +369,6 @@ def test_agent_app_list_and_create_use_agent_route(
apply_config_overrides(monkeypatch, RBAC_ENABLED=True)
class FakeAppService:
def get_app(self, app_obj: object, *, session: object) -> object:
return app_obj
def get_paginate_apps(self, user_id: str, tenant_id: str, params, session) -> object:
captured["list"] = {"user_id": user_id, "tenant_id": tenant_id, "params": params}
return SimpleNamespace(
@@ -649,10 +646,6 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
)
class FakeAppService:
def get_app(self, app_obj: object, *, session: object) -> object:
captured["get_app"] = {"app": app_obj, "session": session}
return app_obj
def update_app(self, app_obj: object, args: dict[str, object], *, session: object) -> object:
captured["update"] = {"app": app_obj, "args": args}
return _app_detail_obj(id=app_id, tenant_id=tenant_id, name=args["name"], bound_agent_id=agent_id)
@@ -674,7 +667,6 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
assert detail["access_ready"] is False
assert "active_config_is_published" not in detail
assert "bound_agent_id" not in detail
assert captured["get_app"] == {"app": app_model, "session": session}
with app.test_request_context(
"/console/api/agent/00000000-0000-0000-0000-000000000001",
json={"name": "Renamed", "description": "", "role": "Reviewer", "icon_type": "emoji", "icon": "R"},
@@ -1043,9 +1035,6 @@ def test_agent_app_update_allows_empty_role(
)
class FakeAppService:
def get_app(self, app_obj: object, *, session: object) -> object:
return app_obj
def update_app(self, app_obj: object, args: dict[str, object], *, session: object) -> object:
captured["update"] = {"app": app_obj, "args": args}
return _app_detail_obj(id="app-1", name=args["name"], bound_agent_id=agent_id)
@@ -3,8 +3,10 @@
import json
from dataclasses import replace
from datetime import datetime
from pathlib import Path
import pytest
import yaml
from flask import Flask
from pydantic import ValidationError
from sqlalchemy.orm import Session
@@ -12,6 +14,7 @@ from werkzeug.datastructures import MultiDict
from controllers.console.app import app as controller
from fields import app_fields
from fields.app_model_config_response import AppModelConfigResponse
from libs.pagination import PaginatedResult
from models.account import Account
from models.enums import CustomizeTokenStrategy, TagType
@@ -308,7 +311,6 @@ def test_app_detail_with_site_includes_nested_serialization(
monkeypatch: pytest.MonkeyPatch,
sqlite_session: Session,
):
AppDetailWithSite = app_models.AppDetailWithSite
app_obj = _persist_response_graph(sqlite_session)
app_obj.name = "Detailed App"
app_obj.description = "Desc"
@@ -323,16 +325,19 @@ def test_app_detail_with_site_includes_nested_serialization(
assert model_config is not None
model_config.opening_statement = "hi"
model_config.retriever_resource = json.dumps({"enabled": True})
monkeypatch.setattr("repositories.app.response.load_annotation_reply_config", lambda _session, _app_id: {})
monkeypatch.setattr(
"repositories.app.response.load_annotation_reply_config", lambda _session, _app_id: {"enabled": False}
)
with app.test_request_context("/"):
serialized = AppDetailWithSite.model_validate(
response = controller._app_detail_response(
replace(
app_record(app_obj, session=sqlite_session, projection="detail-with-site"),
permission_keys=app_obj.permission_keys,
),
from_attributes=True,
).model_dump(mode="json")
)
assert isinstance(response, app_models.AppDetailWithSite)
serialized = response.model_dump(mode="json")
assert serialized["icon_url"] == "signed:detail-icon"
assert serialized["model_config"]["retriever_resource"] == {"enabled": True}
@@ -427,3 +432,166 @@ def test_app_pagination_aliases_per_page_and_has_next(app_models, sqlite_session
assert len(serialized["data"]) == 2
assert serialized["data"][0]["icon_url"] == "signed:first-icon"
assert serialized["data"][1]["icon_url"] is None
@pytest.mark.parametrize("strategy", [None, "cot", "function-calling"])
def test_app_detail_preserves_historical_agent_strategy(
app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session, strategy: str | None
):
app_obj = _persist_response_graph(sqlite_session)
config = sqlite_session.get(AppModelConfig, CONFIG_ID)
assert config is not None
agent_mode = {"enabled": True, "tools": []}
if strategy is not None:
agent_mode["strategy"] = strategy
config.agent_mode = json.dumps(agent_mode)
sqlite_session.commit()
monkeypatch.setattr(
"repositories.app.response.load_annotation_reply_config", lambda _session, _app_id: {"enabled": False}
)
with app.test_request_context("/"):
response = controller._app_detail_response(
app_record(app_obj, session=sqlite_session, projection="detail-with-site")
).model_dump(mode="json")
assert response["model_config"]["agent_mode"] == agent_mode
def test_app_detail_preserves_historical_agent_mode_without_tools(
app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
):
app_obj = _persist_response_graph(sqlite_session)
app_obj.mode = AppMode.AGENT_CHAT
config = sqlite_session.get(AppModelConfig, CONFIG_ID)
assert config is not None
agent_mode = {"enabled": True, "max_iteration": 5, "strategy": "function_call"}
config.agent_mode = json.dumps(agent_mode)
sqlite_session.commit()
monkeypatch.setattr(
"repositories.app.response.load_annotation_reply_config", lambda _session, _app_id: {"enabled": False}
)
with app.test_request_context("/"):
response = controller._app_detail_response(
app_record(app_obj, session=sqlite_session, projection="detail-with-site")
).model_dump(mode="json")
assert response["model_config"]["agent_mode"] == agent_mode
def test_app_model_config_response_round_trips_persisted_dataset_and_external_tool_fields(
app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
):
app_obj = _persist_response_graph(sqlite_session)
config = sqlite_session.get(AppModelConfig, CONFIG_ID)
assert config is not None
dataset_configs = {
"retrieval_model": "multiple",
"reranking_enable": True,
"reranking_enabled": True,
"metadata_filtering_conditions": {
"logical_operator": "and",
"conditions": [
{
"id": "condition-1",
"metadata_id": "metadata-1",
"name": "category",
"comparison_operator": "is",
"value": "books",
}
],
},
"extension_setting": {"value": 1},
}
external_form = {
"external_data_tool": {
"variable": "customer",
"label": "Customer",
"type": "api",
"enabled": True,
"icon": "icon-key",
"icon_background": "#fff",
"config": {"provider": "crm"},
"extension_setting": {"value": 2},
}
}
config.dataset_configs = json.dumps(dataset_configs)
config.user_input_form = json.dumps([external_form])
sqlite_session.commit()
monkeypatch.setattr(
"repositories.app.response.load_annotation_reply_config", lambda _session, _app_id: {"enabled": False}
)
with app.test_request_context("/"):
response = controller._app_detail_response(
app_record(app_obj, session=sqlite_session, projection="detail-with-site")
).model_dump(mode="json")
model_config = response["model_config"]
assert model_config["dataset_configs"] == dataset_configs
assert model_config["user_input_form"] == [external_form]
reimported = AppModelConfig(app_id=APP_ID)
reimported.from_model_config_dict(model_config)
assert reimported.dataset_configs_dict == dataset_configs
assert reimported.user_input_form_list == [external_form]
def test_recommended_app_model_configs_keep_all_published_fields():
catalog_path = Path(__file__).resolve().parents[5] / "constants" / "recommended_apps.json"
catalog = json.loads(catalog_path.read_text())
def assert_fields_preserved(source, serialized):
if isinstance(source, dict):
for key, value in source.items():
assert key in serialized
assert_fields_preserved(value, serialized[key])
elif isinstance(source, list):
assert len(source) == len(serialized)
for value, serialized_value in zip(source, serialized, strict=True):
assert_fields_preserved(value, serialized_value)
else:
assert serialized == source
for entry in catalog["app_details"].values():
source = yaml.safe_load(entry["export_data"]).get("model_config") or {}
if not source:
continue
payload = {**source, "created_by": None, "created_at": 1, "updated_by": None, "updated_at": 1}
response = AppModelConfigResponse.model_validate(payload).model_dump(mode="json")
assert_fields_preserved(source, response)
def test_app_detail_rejects_invalid_known_configuration_fields(
app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
):
app_obj = _persist_response_graph(sqlite_session)
monkeypatch.setattr(
"repositories.app.response.load_annotation_reply_config", lambda _session, _app_id: {"enabled": False}
)
with app.test_request_context("/"):
detail = controller._app_detail_response(
app_record(app_obj, session=sqlite_session, projection="detail-with-site")
).model_dump(mode="json")
for field in ("mode", "permission_keys", "site", "model_config"):
with pytest.raises(ValidationError):
app_fields.AppDetailWithSite.model_validate({key: value for key, value in detail.items() if key != field})
with pytest.raises(ValidationError):
app_fields.AppDetailWithSite.model_validate({**detail, "mode": "unsupported"})
config = detail["model_config"]
for invalid in (
{"speech_to_text": {"enabled": "not-a-boolean"}},
{"agent_mode": {"enabled": True, "strategy": "unsupported", "tools": []}},
{"agent_mode": {"enabled": True, "tools": "not-a-list"}},
{"agent_mode": {"enabled": True, "tools": [{"provider_id": "only-id"}]}},
{"dataset_configs": {"retrieval_model": "unsupported"}},
{"dataset_configs": {"retrieval_model": "multiple", "reranking_mode": "unsupported"}},
{"model": {"provider": "openai", "name": "gpt-4o", "mode": "unsupported"}},
{"user_input_form": [{"unsupported": {"label": "Name", "variable": "name"}}]},
):
with pytest.raises(ValidationError):
app_fields.AppDetailWithSite.model_validate({**detail, "model_config": {**config, **invalid}})
@@ -3,6 +3,7 @@
import inspect
from collections.abc import Callable
from dataclasses import dataclass, field, replace
from datetime import datetime
from io import BytesIO
from typing import cast
from uuid import UUID
@@ -18,6 +19,7 @@ from controllers.console.app.error import AppNotFoundError
from libs.login import AccountWithTenant
from machinery.context import RequestContext
from models.account import Account, TenantAccountRole
from models.enums import CustomizeTokenStrategy
from services.agent.roster_package_entities import RosterAgentPackageExport
from services.app.console_service import (
AppExportPaidPlanRequiredError,
@@ -41,7 +43,33 @@ from tests.unit_tests.config_override import apply_config_overrides
APP_ID = UUID("11111111-1111-1111-1111-111111111111")
CONTEXT = RequestContext("request", "trace", "actor", "workspace")
RECORD = AppRecord(str(APP_ID), "Example", "chat")
RECORD = AppRecord(
str(APP_ID), "Example", "chat", description="", created_at=datetime(2024, 1, 1), updated_at=datetime(2024, 1, 1)
)
SITE = {
"code": "site",
"title": "Example",
"icon_type": None,
"icon": None,
"icon_background": None,
"description": None,
"default_language": "en-US",
"chat_color_theme": None,
"chat_color_theme_inverted": False,
"customize_domain": None,
"copyright": None,
"privacy_policy": None,
"input_placeholder": None,
"custom_disclaimer": "",
"customize_token_strategy": CustomizeTokenStrategy.NOT_ALLOW,
"prompt_public": False,
"show_workflow_steps": False,
"use_icon_as_answer_icon": False,
"created_by": None,
"created_at": datetime(2024, 1, 1),
"updated_by": None,
"updated_at": datetime(2024, 1, 1),
}
@dataclass
@@ -133,7 +161,7 @@ def test_detail_resolves_urls_at_the_http_boundary(
SERVICE_API_URL="https://api.example.com" if configured else "",
APP_WEB_URL="https://apps.example.com" if configured else "",
)
boundary.record = replace(RECORD, site={"code": "site", "title": "Example"})
boundary.record = replace(RECORD, site=SITE)
with app.test_request_context("/", base_url="https://console.example.com/"):
response = cast(dict[str, object], invoke(controller.AppApi, controller.AppApi.get, app_id=APP_ID))
assert response["api_base_url"] == (
@@ -48,7 +48,7 @@ from repositories.app_preview_query_repository import AppPreviewQueryRepository
from repositories.trial_app_repository import TrialAppRepository
from services.app_preview_details_adapters import AppPreviewDetailsRuntime
from services.app_preview_query_service import AppPreviewQueryService
from services.app_service import AppResponseView, AppService
from services.app_service import AppResponseView
from services.recommended_app_query_service import (
RecommendedAppCatalogPage,
RecommendedAppDetailRecord,
@@ -204,9 +204,8 @@ class _Harness:
with self.app.test_request_context(f"/trial-apps/{self.target.id}"), self.factory() as session:
app = session.get(App, self.target.id)
assert app is not None
app = AppService().get_app(app, session=session)
return trial_module.TrialAppDetailResponse.model_validate(
AppResponseView(app, session=session), from_attributes=True
AppResponseView(app, session=session, account=self.viewer), from_attributes=True
).model_dump(mode="json")
@@ -81,6 +81,10 @@ class TestAdditionalFeatureManagers:
SuggestedQuestionsAfterAnswerConfigManager.validate_and_set_defaults(
{"suggested_questions_after_answer": {"enabled": True, "prompt": 123}}
)
with pytest.raises(ValueError, match="prompt in suggested_questions_after_answer must be of string type"):
SuggestedQuestionsAfterAnswerConfigManager.validate_and_set_defaults(
{"suggested_questions_after_answer": {"enabled": False, "prompt": None}}
)
with pytest.raises(ValueError, match="must be less than or equal to 1000 characters"):
SuggestedQuestionsAfterAnswerConfigManager.validate_and_set_defaults(
{"suggested_questions_after_answer": {"enabled": True, "prompt": "a" * 1001}}
@@ -128,6 +132,16 @@ class TestAdditionalFeatureManagers:
TextToSpeechConfigManager.validate_and_set_defaults({"text_to_speech": "bad"})
with pytest.raises(ValueError):
TextToSpeechConfigManager.validate_and_set_defaults({"text_to_speech": {"enabled": "yes"}})
for field, value in (("voice", None), ("language", 42), ("autoPlay", "auto"), ("autoPlay", None)):
with pytest.raises(ValueError, match=f"{field} in text_to_speech"):
TextToSpeechConfigManager.validate_and_set_defaults(
{"text_to_speech": {"enabled": False, field: value}}
)
valid, _ = TextToSpeechConfigManager.validate_and_set_defaults(
{"text_to_speech": {"enabled": True, "voice": "v", "language": "en", "autoPlay": "enabled"}}
)
assert valid["text_to_speech"]["autoPlay"] == "enabled"
result = TextToSpeechConfigManager.convert(
{"text_to_speech": {"enabled": True, "voice": "v", "language": "en"}}
@@ -427,6 +427,18 @@ class TestAppModelValidation:
class TestAppModelConfig:
"""Test suite for AppModelConfig model."""
def test_legacy_dataset_configs_default_retrieval_model_without_dropping_settings(self):
config = AppModelConfig(
app_id=str(uuid4()),
dataset_configs=json.dumps({"reranking_enable": True, "top_k": 8}),
)
assert config.dataset_configs_dict == {
"retrieval_model": "single",
"reranking_enable": True,
"top_k": 8,
}
def test_app_model_config_creation(self):
"""Test creating an AppModelConfig."""
# Arrange
@@ -9,7 +9,7 @@ from flask import has_request_context
from sqlalchemy import Engine, event, func, select, text
from sqlalchemy.orm import Session, sessionmaker
from fields.app_fields import AppDetailWithSite, AppPagination
from fields.app_fields import AppPagination
from machinery.context import RequestContext
from models.agent import Agent, AgentScope, AgentSource, AgentStatus
from models.enums import CustomizeTokenStrategy
@@ -101,13 +101,11 @@ def test_all_single_app_operations_preserve_visibility(
assert sqlite_session.scalar(select(func.count()).select_from(AppStar)) == 0
def test_detail_is_serializable_after_repository_session_closes(
def test_detail_is_materialized_after_repository_session_closes(
repository: ConsoleAppRepository,
sqlite_session: Session,
sqlite_engine: Engine,
config_overrides: Callable[..., None],
) -> None:
config_overrides(SERVICE_API_URL="", APP_WEB_URL="")
assert not has_request_context()
app = persist_app(sqlite_session)
config = AppModelConfig(app_id=app.id, model='{"provider":"openai","name":"example"}', pre_prompt="Prompt")
@@ -129,14 +127,16 @@ def test_detail_is_serializable_after_repository_session_closes(
event.listen(sqlite_engine, "before_cursor_execute", reject_queries)
try:
data = AppDetailWithSite.model_validate(record, from_attributes=True).model_dump(mode="json")
model_config = record.app_model_config
site_data = record.site
description = record.description
finally:
event.remove(sqlite_engine, "before_cursor_execute", reject_queries)
assert data["model_config"]["model"] == {"provider": "openai", "name": "example"}
assert data["site"]["access_token"] == "site-code"
assert data["description"] == ""
assert data["api_base_url"] is None
assert data["site"]["app_base_url"] is None
assert model_config is not None
assert model_config["model"] == {"provider": "openai", "name": "example"}
assert site_data is not None
assert site_data["code"] == "site-code"
assert description == ""
@pytest.mark.parametrize(
@@ -17,7 +17,6 @@ from core.plugin.impl.plugin import PluginInstaller
from core.tools.entities.tool_entities import ApiProviderSchemaType
from core.tools.tool_manager import ToolManager
from events.app_event import app_was_updated
from fields.app_fields import AppDetailWithSite
from graphon.model_runtime.entities.model_entities import ModelType
from machinery.context import RequestContext
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
@@ -839,13 +838,11 @@ def test_detail_tool_enrichment_releases_database_before_plugin_io(
assert read.scalar(select(App.id).where(App.name == "Created")) is not None
else:
result = read_result()
data = AppDetailWithSite.model_validate(result, from_attributes=True).model_dump(mode="json")
assert data["deleted_tools"] == [
assert result.deleted_tools == [
{"type": "api", "provider_id": missing_api, "tool_name": "tool"},
{"type": "api", "provider_id": providers[1].id, "tool_name": "tool"},
{"type": "builtin", "provider_id": "vendor/missing/tool", "tool_name": "tool"},
]
assert "tool_references" not in data
assert remote_calls == [["vendor/missing/tool", "vendor/installed/tool"]]
assert not checked_out
finally:
@@ -515,6 +515,52 @@ def test_create_or_update_app_flushes_new_model_config_before_signal(
assert sqlite_session.in_transaction()
@pytest.mark.parametrize(
("feature", "field", "value"),
[
("suggested_questions_after_answer", "prompt", None),
("suggested_questions_after_answer", "prompt", 123),
("text_to_speech", "voice", None),
("text_to_speech", "language", 123),
("text_to_speech", "autoPlay", "auto"),
],
)
def test_chat_dsl_import_rejects_invalid_feature_fields(
sqlite_session: Session, feature: str, field: str, value: object
) -> None:
app = _app()
with pytest.raises(ValueError, match=field):
AppDslService(session=sqlite_session)._create_or_update_app(
app=app,
data={"app": {"mode": AppMode.CHAT}, "model_config": {feature: {"enabled": True, field: value}}},
account=_account(),
)
assert app.app_model_config_id is None
assert list(sqlite_session.scalars(select(AppModelConfig))) == []
def test_chat_dsl_import_preserves_valid_feature_fields(sqlite_session: Session) -> None:
app = _app()
model_config = {
"suggested_questions_after_answer": {"enabled": True, "prompt": "Follow up"},
"text_to_speech": {"enabled": True, "voice": "alloy", "language": "en", "autoPlay": "disabled"},
}
AppDslService(session=sqlite_session)._create_or_update_app(
app=app,
data={"app": {"mode": AppMode.CHAT}, "model_config": model_config},
account=_account(),
)
assert app.app_model_config_id is not None
persisted = sqlite_session.get(AppModelConfig, app.app_model_config_id)
assert persisted is not None
assert persisted.suggested_questions_after_answer_dict == model_config["suggested_questions_after_answer"]
assert persisted.text_to_speech_dict == model_config["text_to_speech"]
def test_create_or_update_app_removes_imported_workflow_viewport(monkeypatch: pytest.MonkeyPatch) -> None:
session = cast(Session, SimpleNamespace(add=Mock(), flush=Mock(), get=Mock()))
service = AppDslService(session=session)
@@ -9,7 +9,7 @@ from uuid import uuid4
import pytest
from sqlalchemy import event, select
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, sessionmaker
from enums import DeploymentEdition
from graphon.model_runtime.entities.model_entities import ModelType
@@ -31,7 +31,7 @@ from models.provider import TenantDefaultModel
from models.workflow import Workflow, WorkflowType
from repositories.app.console_repository import ConsoleAppRepository
from services.agent.errors import AgentAccessNotReadyError, AgentNameConflictError
from services.app_service import AppListParams, AppService, CreateAppParams
from services.app_service import AppListParams, AppResponseView, AppService, CreateAppParams
from services.enterprise import rbac_service as enterprise_rbac_service
@@ -586,68 +586,40 @@ def test_get_recent_apps_uses_one_tenant_scoped_projection_query(sqlite_session:
assert "app_model_configs" not in select_statements[0].lower()
class TestGetApp:
def test_legacy_agent_detection_uses_caller_session(self, unbound_session: Session):
app = App(
mode=AppMode.CHAT,
)
class TestAppResponseViewAgentConfig:
def test_masking_persistent_agent_config_does_not_change_database(
self, sqlite_session: Session, sqlite_session_factory: sessionmaker[Session]
):
tenant_id = str(uuid4())
app = _persist_app(sqlite_session, tenant_id=tenant_id)
app.mode = AppMode.AGENT_CHAT
original_agent_mode = {"enabled": True, "tools": [{"parameters": {"secret": "encrypted-value"}}]}
config = AppModelConfig(app_id=app.id, agent_mode=json.dumps(original_agent_mode))
sqlite_session.add(config)
sqlite_session.flush()
app.app_model_config_id = config.id
sqlite_session.commit()
account = Account(name="Test Account", email="test@example.com")
account._current_tenant = Tenant(name="Test Tenant")
account._current_tenant.id = "tenant-1"
account._current_tenant.id = tenant_id
masked_agent_mode = {"enabled": True, "tools": [{"parameters": {"secret": "[masked]"}}]}
with (
patch.object(App, "is_agent_with_session", return_value=False) as is_agent,
patch.object(App, "app_model_config_with_session") as get_model_config,
patch("services.app_service.current_user", account),
patch("services.app_service.mask_agent_tool_parameters", return_value=masked_agent_mode),
patch.object(sqlite_session, "refresh", side_effect=AssertionError("App refresh is unnecessary")),
):
assert AppService().get_app(app, session=unbound_session) is app
response_config = AppResponseView(app, session=sqlite_session, account=account).app_model_config
assert response_config is not None
assert response_config.agent_mode_dict == masked_agent_mode
assert config.agent_mode_dict == original_agent_mode
assert not sqlite_session.is_modified(config)
sqlite_session.commit()
is_agent.assert_called_once_with(session=unbound_session)
get_model_config.assert_not_called()
def test_agent_model_config_uses_caller_session(self, unbound_session: Session):
app = App(
mode=AppMode.AGENT_CHAT,
)
account = Account(name="Test Account", email="test@example.com")
account._current_tenant = Tenant(name="Test Tenant")
account._current_tenant.id = "tenant-1"
with (
patch.object(App, "is_agent_with_session") as is_agent,
patch.object(App, "app_model_config_with_session", return_value=None) as get_model_config,
patch("services.app_service.current_user", account),
):
assert AppService().get_app(app, session=unbound_session) is app
is_agent.assert_not_called()
get_model_config.assert_called_once_with(session=unbound_session)
def test_masked_agent_config_is_served_through_the_session_accessor(self, unbound_session: Session):
"""The masked config must be reachable via `app_model_config_with_session`.
Every response path resolves an `App`'s model config through that accessor
(`AppResponseView.app_model_config`), never through a raw attribute, so the
masking `get_app` applies has to be observable there.
"""
app = App(mode=AppMode.AGENT_CHAT)
app.id = str(uuid4())
masked_config = AppModelConfig(app_id=app.id, agent_mode=json.dumps({"enabled": True, "tools": []}))
# A second, unmasked instance: what a fresh lookup would hand back if the
# returned app ever fell through to the base accessor.
refetched_config = AppModelConfig(app_id=app.id, agent_mode=json.dumps({"enabled": True, "tools": []}))
account = Account(name="Test Account", email="test@example.com")
account._current_tenant = Tenant(name="Test Tenant")
account._current_tenant.id = "tenant-1"
with (
patch.object(App, "app_model_config_with_session", side_effect=[masked_config, refetched_config]),
patch("services.app_service.current_user", account),
):
result = AppService().get_app(app, session=unbound_session)
assert result is not app
assert result.app_model_config_with_session(session=unbound_session) is masked_config
with sqlite_session_factory() as read_session:
saved_config = read_session.get(AppModelConfig, config.id)
assert saved_config is not None
assert saved_config.agent_mode_dict == original_agent_mode
class TestAgentAppType:
@@ -23,41 +23,41 @@ export type AgentAppCreatePayload = {
}
export type AgentAppDetailWithSite = {
access_mode?: string | null
access_mode: WebAppAccessMode | null
access_ready?: boolean
api_base_url?: string | null
api_base_url: string
app_id?: string | null
backing_app_id?: string | null
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
created_at: number
created_by: string | null
debug_conversation_has_messages?: boolean
debug_conversation_id?: string | null
debug_conversation_message_count?: number
deleted_tools?: Array<DeletedTool>
description?: string | null
deleted_tools: Array<DeletedTool>
description: string
enable_api: boolean
enable_site: boolean
hidden_app_backed?: boolean
icon?: string | null
icon_background?: string | null
icon_type?: string | null
icon: string | null
icon_background: string | null
icon_type: IconType | null
readonly icon_url: string | null
id: string
maintainer?: string | null
max_active_requests?: number | null
mode: string
model_config?: AppModelConfigResponse | null
maintainer: string | null
max_active_requests: number | null
mode: AppMode
model_config: AppModelConfigResponse | null
name: string
permission_keys: Array<string>
role?: string | null
site?: AppDetailSiteResponse | null
tags?: Array<Tag>
tracing?: unknown | null
updated_at?: number | null
updated_by?: string | null
use_icon_as_answer_icon?: boolean | null
workflow?: WorkflowPartial | null
site: AppDetailSiteResponse | null
tags: Array<Tag>
tracing: string | null
updated_at: number
updated_by: string | null
use_icon_as_answer_icon: boolean
workflow: WorkflowPartial | null
}
export type AgentInviteOptionsResponse = {
@@ -443,7 +443,7 @@ export type AgentConfigSnapshotRestoreResponse = {
}
export type AgentAppPartial = {
access_mode?: string | null
access_mode?: WebAppAccessMode | null
active_config_is_published?: boolean
app_id?: string | null
author_name?: string | null
@@ -458,13 +458,13 @@ export type AgentAppPartial = {
hidden_app_backed?: boolean
icon?: string | null
icon_background?: string | null
icon_type?: string | null
icon_type?: IconType | null
readonly icon_url: string | null
id: string
is_starred?: boolean
maintainer?: string | null
max_active_requests?: number | null
mode: string
mode: AppMode
model_config?: ModelConfigPartial | null
name: string
permission_keys: Array<string>
@@ -486,65 +486,89 @@ export type AgentPublicationCountsResponse = {
export type IconType = 'emoji' | 'image' | 'link'
export type WebAppAccessMode = 'private' | 'private_all' | 'public' | 'sso_verified'
export type DeletedTool = {
provider_id: string
tool_name: string
type: string
}
export type AppMode =
| 'advanced-chat'
| 'agent'
| 'agent-chat'
| 'channel'
| 'chat'
| 'completion'
| 'rag-pipeline'
| 'workflow'
export type AppModelConfigResponse = {
agent_mode?: unknown | null
annotation_reply?: unknown | null
chat_prompt_config?: unknown | null
completion_prompt_config?: unknown | null
created_at?: number | null
created_by?: string | null
dataset_configs?: unknown | null
dataset_query_variable?: string | null
external_data_tools?: unknown | null
file_upload?: unknown | null
model?: unknown | null
more_like_this?: unknown | null
opening_statement?: string | null
pre_prompt?: string | null
prompt_type?: string | null
retriever_resource?: unknown | null
sensitive_word_avoidance?: unknown | null
speech_to_text?: unknown | null
suggested_questions?: unknown | null
suggested_questions_after_answer?: unknown | null
text_to_speech?: unknown | null
updated_at?: number | null
updated_by?: string | null
user_input_form?: unknown | null
agent_mode: AppAgentModeResponse
annotation_reply: AppAnnotationReplyEnabledResponse | AppAnnotationReplyDisabledResponse
chat_prompt_config: AppChatPromptConfigResponse
completion_prompt_config: AppCompletionPromptConfigResponse
created_at: number
created_by: string | null
dataset_configs: AppDatasetConfigsResponse
dataset_query_variable: string | null
external_data_tools: Array<
AppEnabledExternalDataToolResponse | AppDisabledExternalDataToolResponse
>
file_upload: AppFileUploadResponse
model: AppModelSelectionResponse
more_like_this: AppEnabledConfigResponse
opening_statement: string | null
pre_prompt: string | null
prompt_type: PromptType
retriever_resource: AppEnabledConfigResponse
sensitive_word_avoidance: AppSensitiveWordAvoidanceResponse
speech_to_text: AppEnabledConfigResponse
suggested_questions: Array<string>
suggested_questions_after_answer: AppSuggestedQuestionsAfterAnswerResponse
text_to_speech: AppTextToSpeechResponse
updated_at: number
updated_by: string | null
user_input_form: Array<
| AppTextInputFormResponse
| AppSelectFormResponse
| AppParagraphFormResponse
| AppNumberFormResponse
| AppCheckboxFormResponse
| AppFileFormResponse
| AppFileListFormResponse
| AppExternalDataToolFormResponse
| AppJsonObjectFormResponse
>
}
export type AppDetailSiteResponse = {
access_token?: string | null
app_base_url?: string | null
chat_color_theme?: string | null
chat_color_theme_inverted?: boolean | null
code?: string | null
copyright?: string | null
created_at?: number | null
created_by?: string | null
custom_disclaimer?: string | null
customize_domain?: string | null
customize_token_strategy?: string | null
default_language?: string | null
description?: string | null
icon?: string | null
icon_background?: string | null
icon_type?: string | IconType | null
access_token: string | null
app_base_url: string
chat_color_theme: string | null
chat_color_theme_inverted: boolean
code: string | null
copyright: string | null
created_at: number
created_by: string | null
custom_disclaimer: string
customize_domain: string | null
customize_token_strategy: CustomizeTokenStrategy
default_language: string
description: string | null
icon: string | null
icon_background: string | null
icon_type: IconType | null
readonly icon_url: string | null
input_placeholder?: string | null
privacy_policy?: string | null
prompt_public?: boolean | null
show_workflow_steps?: boolean | null
title?: string | null
updated_at?: number | null
updated_by?: string | null
use_icon_as_answer_icon?: boolean | null
input_placeholder: string | null
privacy_policy: string | null
prompt_public: boolean
show_workflow_steps: boolean
title: string
updated_at: number
updated_by: string | null
use_icon_as_answer_icon: boolean
}
export type Tag = {
@@ -794,7 +818,7 @@ export type AgentSuggestedQuestionsAfterAnswerFeatureConfig = {
}
export type AgentTextToSpeechFeatureConfig = {
autoPlay?: string | null
autoPlay?: 'disabled' | 'enabled' | null
enabled?: boolean
language?: string | null
voice?: string | null
@@ -989,7 +1013,7 @@ export type AgentConfigRevisionResponse = {
export type ModelConfigPartial = {
created_at?: number | null
created_by?: string | null
model?: unknown | null
model?: AppModelSelectionResponse | null
pre_prompt?: string | null
updated_at?: number | null
updated_by?: string | null
@@ -1003,6 +1027,172 @@ export type AgentAppPublishedReferenceResponse = {
app_name: string
}
export type AppAgentModeResponse = {
enabled: boolean
max_iteration?: number
prompt?: AppAgentPromptResponse | string | null
strategy?: PlanningStrategy | 'cot' | 'function-calling' | null
tools?: Array<
| AppProviderAgentToolResponse
| AppLegacyDatasetToolResponse
| AppLegacyGoogleSearchToolResponse
| AppLegacyWebReaderToolResponse
| AppLegacyWikipediaToolResponse
| AppLegacyCurrentDatetimeToolResponse
| AppLegacySensitiveWordToolResponseItem
>
[key: string]: unknown
}
export type AppAnnotationReplyEnabledResponse = {
embedding_model: AppEmbeddingModelResponse
enabled: true
id: string
score_threshold: number
}
export type AppAnnotationReplyDisabledResponse = {
enabled: false
}
export type AppChatPromptConfigResponse = {
prompt?: Array<AppChatPromptMessageResponse>
}
export type AppCompletionPromptConfigResponse = {
conversation_histories_role?: AppConversationHistoriesRoleResponse
prompt?: AppCompletionPromptTextResponse
}
export type AppDatasetConfigsResponse = {
datasets?: AppDatasetListResponse
metadata_filtering_conditions?: AppMetadataFilteringConditionsResponse | null
metadata_filtering_mode?: 'automatic' | 'disabled' | 'manual'
metadata_model_config?: AppModelSelectionResponse | null
reranking_enable?: boolean
reranking_enabled?: boolean
reranking_mode?: RerankMode
reranking_model?: AppRerankingModelResponse | null
retrieval_model: 'multiple' | 'single'
score_threshold?: number | null
score_threshold_enabled?: boolean
top_k?: number
weights?: AppWeightsResponse | null
[key: string]: unknown
}
export type AppEnabledExternalDataToolResponse = {
config: {
[key: string]: JsonValue2
}
enabled: true
icon?: string
icon_background?: string
label?: string
type: string
variable: string
[key: string]: unknown
}
export type AppDisabledExternalDataToolResponse = {
config?: {
[key: string]: JsonValue2
}
enabled: false
icon?: string
icon_background?: string
label?: string
type?: string
variable?: string
[key: string]: unknown
}
export type AppFileUploadResponse = {
allowed_file_extensions?: Array<string>
allowed_file_types?: Array<FileType>
allowed_file_upload_methods?: Array<FileTransferMethod>
enabled?: boolean
image?: AppImageUploadResponse
number_limits?: number
}
export type AppModelSelectionResponse = {
completion_params?: {
[key: string]: JsonValue2
}
mode?: LlmMode | ''
name?: string
provider?: string
[key: string]: unknown
}
export type AppEnabledConfigResponse = {
enabled: boolean
}
export type PromptType = 'advanced' | 'simple'
export type AppSensitiveWordAvoidanceResponse = {
config?: {
[key: string]: JsonValue2
}
configs?: Array<JsonValue2>
enabled: boolean
type?: string
[key: string]: unknown
}
export type AppSuggestedQuestionsAfterAnswerResponse = {
enabled: boolean
model?: AppModelSelectionResponse
prompt?: string
}
export type AppTextToSpeechResponse = {
autoPlay?: 'disabled' | 'enabled'
enabled: boolean
language?: string
voice?: string
}
export type AppTextInputFormResponse = {
'text-input': AppUserInputFormConfigResponse
}
export type AppSelectFormResponse = {
select: AppUserInputFormConfigResponse
}
export type AppParagraphFormResponse = {
paragraph: AppUserInputFormConfigResponse
}
export type AppNumberFormResponse = {
number: AppUserInputFormConfigResponse
}
export type AppCheckboxFormResponse = {
checkbox: AppUserInputFormConfigResponse
}
export type AppFileFormResponse = {
file: AppUserInputFormConfigResponse
}
export type AppFileListFormResponse = {
'file-list': AppUserInputFormConfigResponse
}
export type AppExternalDataToolFormResponse = {
external_data_tool: AppUserInputFormConfigResponse
}
export type AppJsonObjectFormResponse = {
json_object: AppUserInputFormConfigResponse
}
export type CustomizeTokenStrategy = 'allow' | 'must' | 'not_allow' | 'uuid'
export type AgentKind = 'dify_agent'
export type AgentPublishedReferenceResponse = {
@@ -1341,6 +1531,158 @@ export type AgentConfigRevisionOperation =
| 'save_new_version'
| 'save_to_roster'
export type AppAgentPromptResponse = {
first_prompt?: string
next_iteration?: string
[key: string]: unknown
}
export type PlanningStrategy = 'function_call' | 'react' | 'react_router' | 'router'
export type AppProviderAgentToolResponse = {
credential_id?: string | null
enabled?: boolean
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]: JsonValue2
}
[key: string]: unknown
}
export type AppLegacyDatasetToolResponse = {
dataset: AppDatasetReferenceResponse
[key: string]: unknown
}
export type AppLegacyGoogleSearchToolResponse = {
google_search: {
[key: string]: JsonValue2
}
[key: string]: unknown
}
export type AppLegacyWebReaderToolResponse = {
web_reader: {
[key: string]: JsonValue2
}
[key: string]: unknown
}
export type AppLegacyWikipediaToolResponse = {
wikipedia: {
[key: string]: JsonValue2
}
[key: string]: unknown
}
export type AppLegacyCurrentDatetimeToolResponse = {
current_datetime: {
[key: string]: JsonValue2
}
[key: string]: unknown
}
export type AppLegacySensitiveWordToolResponseItem = {
'sensitive-word-avoidance': AppLegacySensitiveWordToolResponse
[key: string]: unknown
}
export type AppEmbeddingModelResponse = {
embedding_model_name: string
embedding_provider_name: string
}
export type AppChatPromptMessageResponse = {
role: string
text: string
}
export type AppConversationHistoriesRoleResponse = {
assistant_prefix: string
user_prefix: string
}
export type AppCompletionPromptTextResponse = {
text: string
}
export type AppDatasetListResponse = {
datasets: Array<AppDatasetItemResponse>
strategy?: string
[key: string]: unknown
}
export type AppMetadataFilteringConditionsResponse = {
conditions?: Array<AppMetadataConditionResponse> | null
logical_operator?: 'and' | 'or' | null
[key: string]: unknown
}
export type RerankMode = 'reranking_model' | 'weighted_score'
export type AppRerankingModelResponse = {
reranking_model_name?: string
reranking_provider_name?: string
[key: string]: unknown
}
export type AppWeightsResponse = {
keyword_setting: AppKeywordSettingResponse
vector_setting: AppVectorSettingResponse
weight_type?: 'customized' | 'keyword_first' | 'semantic_first'
[key: string]: unknown
}
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
number_limits?: number
transfer_methods?: Array<FileTransferMethod>
}
export type LlmMode = 'chat' | 'completion'
export type AppUserInputFormConfigResponse = {
allowed_file_extensions?: Array<string>
allowed_file_types?: Array<FileType>
allowed_file_upload_methods?: Array<FileTransferMethod>
config?: {
[key: string]: JsonValue2
}
default?: JsonValue2
description?: string
enabled?: boolean
hide?: boolean
icon?: string | null
icon_background?: string | null
json_schema?:
| string
| {
[key: string]: JsonValue2
}
| null
label: string
max_length?: number | null
options?: Array<string>
required?: boolean
type?: string
variable: string
[key: string]: unknown
}
export type AgentFileUploadFeatureConfig = {
allowed_file_extensions?: Array<string>
allowed_file_types?: Array<FileType>
@@ -1592,11 +1934,71 @@ export type FormInputConfig =
type: 'file-list'
} & FileListInputConfig)
export type JsonValue2 = unknown
export type ToolProviderType =
| 'api'
| 'app'
| 'builtin'
| 'dataset-retrieval'
| 'mcp'
| 'plugin'
| 'workflow'
export type FileType = 'audio' | 'custom' | 'document' | 'image' | 'video'
export type AppDatasetReferenceResponse = {
enabled?: boolean
id?: string
[key: string]: unknown
}
export type FileTransferMethod = 'datasource_file' | 'local_file' | 'remote_url' | 'tool_file'
export type AppLegacySensitiveWordToolResponse = {
canned_response: string
enabled: boolean
words: Array<string>
[key: string]: unknown
}
export type AppDatasetItemResponse = {
dataset: AppDatasetReferenceResponse
[key: string]: unknown
}
export type AppMetadataConditionResponse = {
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]: unknown
}
export type AppKeywordSettingResponse = {
keyword_weight: number
[key: string]: unknown
}
export type AppVectorSettingResponse = {
embedding_model_name: string
embedding_provider_name: string
vector_weight: number
[key: string]: unknown
}
export type AgentFileUploadImageFeatureConfig = {
enabled?: boolean
@@ -1642,15 +2044,6 @@ export type AgentSoulDifyToolCredentialRef = {
type?: 'provider' | 'tool'
}
export type ToolProviderType =
| 'api'
| 'app'
| 'builtin'
| 'dataset-retrieval'
| 'mcp'
| 'plugin'
| 'workflow'
export type OutputErrorStrategy = 'default_value' | 'fail_branch' | 'stop'
export type DeclaredOutputRetryConfig = {
@@ -1772,44 +2165,44 @@ export type AgentAppPaginationWritable = {
}
export type AgentAppDetailWithSiteWritable = {
access_mode?: string | null
access_mode: WebAppAccessMode | null
access_ready?: boolean
api_base_url?: string | null
api_base_url: string
app_id?: string | null
backing_app_id?: string | null
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
created_at: number
created_by: string | null
debug_conversation_has_messages?: boolean
debug_conversation_id?: string | null
debug_conversation_message_count?: number
deleted_tools?: Array<DeletedTool>
description?: string | null
deleted_tools: Array<DeletedTool>
description: string
enable_api: boolean
enable_site: boolean
hidden_app_backed?: boolean
icon?: string | null
icon_background?: string | null
icon_type?: string | null
icon: string | null
icon_background: string | null
icon_type: IconType | null
id: string
maintainer?: string | null
max_active_requests?: number | null
mode: string
model_config?: AppModelConfigResponse | null
maintainer: string | null
max_active_requests: number | null
mode: AppMode
model_config: AppModelConfigResponse | null
name: string
permission_keys: Array<string>
role?: string | null
site?: AppDetailSiteResponseWritable | null
tags?: Array<Tag>
tracing?: unknown | null
updated_at?: number | null
updated_by?: string | null
use_icon_as_answer_icon?: boolean | null
workflow?: WorkflowPartial | null
site: AppDetailSiteResponseWritable | null
tags: Array<Tag>
tracing: string | null
updated_at: number
updated_by: string | null
use_icon_as_answer_icon: boolean
workflow: WorkflowPartial | null
}
export type AgentAppPartialWritable = {
access_mode?: string | null
access_mode?: WebAppAccessMode | null
active_config_is_published?: boolean
app_id?: string | null
author_name?: string | null
@@ -1824,12 +2217,12 @@ export type AgentAppPartialWritable = {
hidden_app_backed?: boolean
icon?: string | null
icon_background?: string | null
icon_type?: string | null
icon_type?: IconType | null
id: string
is_starred?: boolean
maintainer?: string | null
max_active_requests?: number | null
mode: string
mode: AppMode
model_config?: ModelConfigPartial | null
name: string
permission_keys: Array<string>
@@ -1845,30 +2238,30 @@ export type AgentAppPartialWritable = {
}
export type AppDetailSiteResponseWritable = {
access_token?: string | null
app_base_url?: string | null
chat_color_theme?: string | null
chat_color_theme_inverted?: boolean | null
code?: string | null
copyright?: string | null
created_at?: number | null
created_by?: string | null
custom_disclaimer?: string | null
customize_domain?: string | null
customize_token_strategy?: string | null
default_language?: string | null
description?: string | null
icon?: string | null
icon_background?: string | null
icon_type?: string | IconType | null
input_placeholder?: string | null
privacy_policy?: string | null
prompt_public?: boolean | null
show_workflow_steps?: boolean | null
title?: string | null
updated_at?: number | null
updated_by?: string | null
use_icon_as_answer_icon?: boolean | null
access_token: string | null
app_base_url: string
chat_color_theme: string | null
chat_color_theme_inverted: boolean
code: string | null
copyright: string | null
created_at: number
created_by: string | null
custom_disclaimer: string
customize_domain: string | null
customize_token_strategy: CustomizeTokenStrategy
default_language: string
description: string | null
icon: string | null
icon_background: string | null
icon_type: IconType | null
input_placeholder: string | null
privacy_policy: string | null
prompt_public: boolean
show_workflow_steps: boolean
title: string
updated_at: number
updated_by: string | null
use_icon_as_answer_icon: boolean
}
export type GetAgentData = {
File diff suppressed because it is too large. Load diff
@@ -22,34 +22,34 @@ export type CreateAppPayload = {
}
export type AppDetailWithSite = {
access_mode?: string | null
api_base_url?: string | null
app_id?: string | null
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
deleted_tools?: Array<DeletedTool>
description?: string | null
access_mode: WebAppAccessMode | null
api_base_url: string
app_id: string | null
bound_agent_id: string | null
created_at: number
created_by: string | null
deleted_tools: Array<DeletedTool>
description: string
enable_api: boolean
enable_site: boolean
icon?: string | null
icon_background?: string | null
icon_type?: string | null
icon: string | null
icon_background: string | null
icon_type: IconType | null
readonly icon_url: string | null
id: string
maintainer?: string | null
max_active_requests?: number | null
mode: string
model_config?: AppModelConfigResponse | null
maintainer: string | null
max_active_requests: number | null
mode: AppMode
model_config: AppModelConfigResponse | null
name: string
permission_keys?: Array<string>
site?: AppDetailSiteResponse | null
tags?: Array<Tag>
tracing?: unknown | null
updated_at?: number | null
updated_by?: string | null
use_icon_as_answer_icon?: boolean | null
workflow?: WorkflowPartial | null
permission_keys: Array<string>
site: AppDetailSiteResponse | null
tags: Array<Tag>
tracing: string | null
updated_at: number
updated_by: string | null
use_icon_as_answer_icon: boolean
workflow: WorkflowPartial | null
}
export type AppImportPayload = {
@@ -66,7 +66,7 @@ export type AppImportPayload = {
export type Import = {
app_id?: string | null
app_mode?: string | null
app_mode?: AppMode | null
current_dsl_version?: string
error?: string
id: string
@@ -362,26 +362,26 @@ export type AppApiStatusPayload = {
}
export type AppDetail = {
access_mode?: string | null
created_at?: number | null
created_by?: string | null
description?: string | null
access_mode: WebAppAccessMode | null
created_at: number
created_by: string | null
description: string
enable_api: boolean
enable_site: boolean
icon?: string | null
icon_background?: string | null
icon: string | null
icon_background: string | null
id: string
maintainer?: string | null
mode: string
model_config?: AppModelConfigResponse | null
maintainer: string | null
mode: AppMode
model_config: AppModelConfigResponse | null
name: string
permission_keys?: Array<string>
tags?: Array<Tag>
tracing?: unknown | null
updated_at?: number | null
updated_by?: string | null
use_icon_as_answer_icon?: boolean | null
workflow?: WorkflowPartial | null
permission_keys: Array<string>
tags: Array<Tag>
tracing: string | null
updated_at: number
updated_by: string | null
use_icon_as_answer_icon: boolean
workflow: WorkflowPartial | null
}
export type AudioTranscriptResponse = {
@@ -488,7 +488,7 @@ export type CopyAppPayload = {
export type AppImportResponse = {
app_id?: string | null
app_mode?: string | null
app_mode?: AppMode | null
current_dsl_version: string
error?: string
id: string
@@ -1199,7 +1199,7 @@ export type ApiKeyItem = {
}
export type AppPartial = {
access_mode?: string | null
access_mode?: WebAppAccessMode | null
app_id?: string | null
author_name?: string | null
bound_agent_id?: string | null
@@ -1210,13 +1210,13 @@ export type AppPartial = {
has_draft_trigger?: boolean | null
icon?: string | null
icon_background?: string | null
icon_type?: string | null
icon_type?: IconType | null
readonly icon_url: string | null
id: string
is_starred?: boolean
maintainer?: string | null
max_active_requests?: number | null
mode: string
mode: AppMode
model_config?: ModelConfigPartial | null
name: string
permission_keys?: Array<string>
@@ -1229,65 +1229,89 @@ export type AppPartial = {
export type IconType = 'emoji' | 'image' | 'link'
export type WebAppAccessMode = 'private' | 'private_all' | 'public' | 'sso_verified'
export type DeletedTool = {
provider_id: string
tool_name: string
type: string
}
export type AppMode =
| 'advanced-chat'
| 'agent'
| 'agent-chat'
| 'channel'
| 'chat'
| 'completion'
| 'rag-pipeline'
| 'workflow'
export type AppModelConfigResponse = {
agent_mode?: unknown | null
annotation_reply?: unknown | null
chat_prompt_config?: unknown | null
completion_prompt_config?: unknown | null
created_at?: number | null
created_by?: string | null
dataset_configs?: unknown | null
dataset_query_variable?: string | null
external_data_tools?: unknown | null
file_upload?: unknown | null
model?: unknown | null
more_like_this?: unknown | null
opening_statement?: string | null
pre_prompt?: string | null
prompt_type?: string | null
retriever_resource?: unknown | null
sensitive_word_avoidance?: unknown | null
speech_to_text?: unknown | null
suggested_questions?: unknown | null
suggested_questions_after_answer?: unknown | null
text_to_speech?: unknown | null
updated_at?: number | null
updated_by?: string | null
user_input_form?: unknown | null
agent_mode: AppAgentModeResponse
annotation_reply: AppAnnotationReplyEnabledResponse | AppAnnotationReplyDisabledResponse
chat_prompt_config: AppChatPromptConfigResponse
completion_prompt_config: AppCompletionPromptConfigResponse
created_at: number
created_by: string | null
dataset_configs: AppDatasetConfigsResponse
dataset_query_variable: string | null
external_data_tools: Array<
AppEnabledExternalDataToolResponse | AppDisabledExternalDataToolResponse
>
file_upload: AppFileUploadResponse
model: AppModelSelectionResponse
more_like_this: AppEnabledConfigResponse
opening_statement: string | null
pre_prompt: string | null
prompt_type: PromptType
retriever_resource: AppEnabledConfigResponse
sensitive_word_avoidance: AppSensitiveWordAvoidanceResponse
speech_to_text: AppEnabledConfigResponse
suggested_questions: Array<string>
suggested_questions_after_answer: AppSuggestedQuestionsAfterAnswerResponse
text_to_speech: AppTextToSpeechResponse
updated_at: number
updated_by: string | null
user_input_form: Array<
| AppTextInputFormResponse
| AppSelectFormResponse
| AppParagraphFormResponse
| AppNumberFormResponse
| AppCheckboxFormResponse
| AppFileFormResponse
| AppFileListFormResponse
| AppExternalDataToolFormResponse
| AppJsonObjectFormResponse
>
}
export type AppDetailSiteResponse = {
access_token?: string | null
app_base_url?: string | null
chat_color_theme?: string | null
chat_color_theme_inverted?: boolean | null
code?: string | null
copyright?: string | null
created_at?: number | null
created_by?: string | null
custom_disclaimer?: string | null
customize_domain?: string | null
customize_token_strategy?: string | null
default_language?: string | null
description?: string | null
icon?: string | null
icon_background?: string | null
icon_type?: string | IconType | null
access_token: string | null
app_base_url: string
chat_color_theme: string | null
chat_color_theme_inverted: boolean
code: string | null
copyright: string | null
created_at: number
created_by: string | null
custom_disclaimer: string
customize_domain: string | null
customize_token_strategy: CustomizeTokenStrategy
default_language: string
description: string | null
icon: string | null
icon_background: string | null
icon_type: IconType | null
readonly icon_url: string | null
input_placeholder?: string | null
privacy_policy?: string | null
prompt_public?: boolean | null
show_workflow_steps?: boolean | null
title?: string | null
updated_at?: number | null
updated_by?: string | null
use_icon_as_answer_icon?: boolean | null
input_placeholder: string | null
privacy_policy: string | null
prompt_public: boolean
show_workflow_steps: boolean
title: string
updated_at: number
updated_by: string | null
use_icon_as_answer_icon: boolean
}
export type Tag = {
@@ -2088,12 +2112,178 @@ export type WorkflowDraftVariableFullContentResponse = {
export type ModelConfigPartial = {
created_at?: number | null
created_by?: string | null
model?: unknown | null
model?: AppModelSelectionResponse | null
pre_prompt?: string | null
updated_at?: number | null
updated_by?: string | null
}
export type AppAgentModeResponse = {
enabled: boolean
max_iteration?: number
prompt?: AppAgentPromptResponse | string | null
strategy?: PlanningStrategy | 'cot' | 'function-calling' | null
tools?: Array<
| AppProviderAgentToolResponse
| AppLegacyDatasetToolResponse
| AppLegacyGoogleSearchToolResponse
| AppLegacyWebReaderToolResponse
| AppLegacyWikipediaToolResponse
| AppLegacyCurrentDatetimeToolResponse
| AppLegacySensitiveWordToolResponseItem
>
[key: string]: unknown
}
export type AppAnnotationReplyEnabledResponse = {
embedding_model: AppEmbeddingModelResponse
enabled: true
id: string
score_threshold: number
}
export type AppAnnotationReplyDisabledResponse = {
enabled: false
}
export type AppChatPromptConfigResponse = {
prompt?: Array<AppChatPromptMessageResponse>
}
export type AppCompletionPromptConfigResponse = {
conversation_histories_role?: AppConversationHistoriesRoleResponse
prompt?: AppCompletionPromptTextResponse
}
export type AppDatasetConfigsResponse = {
datasets?: AppDatasetListResponse
metadata_filtering_conditions?: AppMetadataFilteringConditionsResponse | null
metadata_filtering_mode?: 'automatic' | 'disabled' | 'manual'
metadata_model_config?: AppModelSelectionResponse | null
reranking_enable?: boolean
reranking_enabled?: boolean
reranking_mode?: RerankMode
reranking_model?: AppRerankingModelResponse | null
retrieval_model: 'multiple' | 'single'
score_threshold?: number | null
score_threshold_enabled?: boolean
top_k?: number
weights?: AppWeightsResponse | null
[key: string]: unknown
}
export type AppEnabledExternalDataToolResponse = {
config: {
[key: string]: JsonValue2
}
enabled: true
icon?: string
icon_background?: string
label?: string
type: string
variable: string
[key: string]: unknown
}
export type AppDisabledExternalDataToolResponse = {
config?: {
[key: string]: JsonValue2
}
enabled: false
icon?: string
icon_background?: string
label?: string
type?: string
variable?: string
[key: string]: unknown
}
export type AppFileUploadResponse = {
allowed_file_extensions?: Array<string>
allowed_file_types?: Array<FileType>
allowed_file_upload_methods?: Array<FileTransferMethod>
enabled?: boolean
image?: AppImageUploadResponse
number_limits?: number
}
export type AppModelSelectionResponse = {
completion_params?: {
[key: string]: JsonValue2
}
mode?: LlmMode | ''
name?: string
provider?: string
[key: string]: unknown
}
export type AppEnabledConfigResponse = {
enabled: boolean
}
export type PromptType = 'advanced' | 'simple'
export type AppSensitiveWordAvoidanceResponse = {
config?: {
[key: string]: JsonValue2
}
configs?: Array<JsonValue2>
enabled: boolean
type?: string
[key: string]: unknown
}
export type AppSuggestedQuestionsAfterAnswerResponse = {
enabled: boolean
model?: AppModelSelectionResponse
prompt?: string
}
export type AppTextToSpeechResponse = {
autoPlay?: 'disabled' | 'enabled'
enabled: boolean
language?: string
voice?: string
}
export type AppTextInputFormResponse = {
'text-input': AppUserInputFormConfigResponse
}
export type AppSelectFormResponse = {
select: AppUserInputFormConfigResponse
}
export type AppParagraphFormResponse = {
paragraph: AppUserInputFormConfigResponse
}
export type AppNumberFormResponse = {
number: AppUserInputFormConfigResponse
}
export type AppCheckboxFormResponse = {
checkbox: AppUserInputFormConfigResponse
}
export type AppFileFormResponse = {
file: AppUserInputFormConfigResponse
}
export type AppFileListFormResponse = {
'file-list': AppUserInputFormConfigResponse
}
export type AppExternalDataToolFormResponse = {
external_data_tool: AppUserInputFormConfigResponse
}
export type AppJsonObjectFormResponse = {
json_object: AppUserInputFormConfigResponse
}
export type CustomizeTokenStrategy = 'allow' | 'must' | 'not_allow' | 'uuid'
export type PluginDependencyType = 'github' | 'marketplace' | 'package'
export type Github = {
@@ -2250,7 +2440,7 @@ export type WorkflowSuggestedQuestionsAfterAnswerPayload = {
}
export type WorkflowTextToSpeechPayload = {
autoPlay?: string | null
autoPlay?: 'disabled' | 'enabled' | null
enabled?: boolean | null
language?: string | null
voice?: string | null
@@ -2484,6 +2674,156 @@ export type CheckResultView = {
reason?: string | null
}
export type AppAgentPromptResponse = {
first_prompt?: string
next_iteration?: string
[key: string]: unknown
}
export type PlanningStrategy = 'function_call' | 'react' | 'react_router' | 'router'
export type AppProviderAgentToolResponse = {
credential_id?: string | null
enabled?: boolean
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]: JsonValue2
}
[key: string]: unknown
}
export type AppLegacyDatasetToolResponse = {
dataset: AppDatasetReferenceResponse
[key: string]: unknown
}
export type AppLegacyGoogleSearchToolResponse = {
google_search: {
[key: string]: JsonValue2
}
[key: string]: unknown
}
export type AppLegacyWebReaderToolResponse = {
web_reader: {
[key: string]: JsonValue2
}
[key: string]: unknown
}
export type AppLegacyWikipediaToolResponse = {
wikipedia: {
[key: string]: JsonValue2
}
[key: string]: unknown
}
export type AppLegacyCurrentDatetimeToolResponse = {
current_datetime: {
[key: string]: JsonValue2
}
[key: string]: unknown
}
export type AppLegacySensitiveWordToolResponseItem = {
'sensitive-word-avoidance': AppLegacySensitiveWordToolResponse
[key: string]: unknown
}
export type AppEmbeddingModelResponse = {
embedding_model_name: string
embedding_provider_name: string
}
export type AppChatPromptMessageResponse = {
role: string
text: string
}
export type AppConversationHistoriesRoleResponse = {
assistant_prefix: string
user_prefix: string
}
export type AppCompletionPromptTextResponse = {
text: string
}
export type AppDatasetListResponse = {
datasets: Array<AppDatasetItemResponse>
strategy?: string
[key: string]: unknown
}
export type AppMetadataFilteringConditionsResponse = {
conditions?: Array<AppMetadataConditionResponse> | null
logical_operator?: 'and' | 'or' | null
[key: string]: unknown
}
export type RerankMode = 'reranking_model' | 'weighted_score'
export type AppRerankingModelResponse = {
reranking_model_name?: string
reranking_provider_name?: string
[key: string]: unknown
}
export type AppWeightsResponse = {
keyword_setting: AppKeywordSettingResponse
vector_setting: AppVectorSettingResponse
weight_type?: 'customized' | 'keyword_first' | 'semantic_first'
[key: string]: unknown
}
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
number_limits?: number
transfer_methods?: Array<FileTransferMethod>
}
export type AppUserInputFormConfigResponse = {
allowed_file_extensions?: Array<string>
allowed_file_types?: Array<FileType>
allowed_file_upload_methods?: Array<FileTransferMethod>
config?: {
[key: string]: JsonValue2
}
default?: JsonValue2
description?: string
enabled?: boolean
hide?: boolean
icon?: string | null
icon_background?: string | null
json_schema?:
| string
| {
[key: string]: JsonValue2
}
| null
label: string
max_length?: number | null
options?: Array<string>
required?: boolean
type?: string
variable: string
[key: string]: unknown
}
export type UserActionConfig = {
button_style?: ButtonStyle
id: string
@@ -2504,8 +2844,6 @@ export type FormInputConfig =
type: 'file-list'
} & FileListInputConfig)
export type JsonValue2 = unknown
export type WorkflowFileUploadTransferPayload = {
enabled?: boolean | null
number_limits?: number | null
@@ -2556,7 +2894,7 @@ export type AgentSuggestedQuestionsAfterAnswerFeatureConfig = {
}
export type AgentTextToSpeechFeatureConfig = {
autoPlay?: string | null
autoPlay?: 'disabled' | 'enabled' | null
enabled?: boolean
language?: string | null
voice?: string | null
@@ -2743,6 +3081,72 @@ export type AgentComposerKnowledgeDatasetCandidateResponse = {
name?: string | null
}
export type ToolProviderType =
| 'api'
| 'app'
| 'builtin'
| 'dataset-retrieval'
| 'mcp'
| 'plugin'
| 'workflow'
export type AppDatasetReferenceResponse = {
enabled?: boolean
id?: string
[key: string]: unknown
}
export type AppLegacySensitiveWordToolResponse = {
canned_response: string
enabled: boolean
words: Array<string>
[key: string]: unknown
}
export type AppDatasetItemResponse = {
dataset: AppDatasetReferenceResponse
[key: string]: unknown
}
export type AppMetadataConditionResponse = {
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]: unknown
}
export type AppKeywordSettingResponse = {
keyword_weight: number
[key: string]: unknown
}
export type AppVectorSettingResponse = {
embedding_model_name: string
embedding_provider_name: string
vector_weight: number
[key: string]: unknown
}
export type ButtonStyle = 'accent' | 'default' | 'ghost' | 'primary'
export type ParagraphInputConfig = {
@@ -2774,10 +3178,6 @@ export type FileListInputConfig = {
type?: 'file-list'
}
export type FileType = 'audio' | 'custom' | 'document' | 'image' | 'video'
export type FileTransferMethod = 'datasource_file' | 'local_file' | 'remote_url' | 'tool_file'
export type AgentFileUploadImageFeatureConfig = {
enabled?: boolean
[key: string]: unknown
@@ -2840,15 +3240,6 @@ export type AgentSoulDifyToolCredentialRef = {
type?: 'provider' | 'tool'
}
export type ToolProviderType =
| 'api'
| 'app'
| 'builtin'
| 'dataset-retrieval'
| 'mcp'
| 'plugin'
| 'workflow'
export type StringSource = {
selector?: Array<string>
type: ValueSourceType
@@ -2936,33 +3327,33 @@ export type AppPaginationWritable = {
}
export type AppDetailWithSiteWritable = {
access_mode?: string | null
api_base_url?: string | null
app_id?: string | null
bound_agent_id?: string | null
created_at?: number | null
created_by?: string | null
deleted_tools?: Array<DeletedTool>
description?: string | null
access_mode: WebAppAccessMode | null
api_base_url: string
app_id: string | null
bound_agent_id: string | null
created_at: number
created_by: string | null
deleted_tools: Array<DeletedTool>
description: string
enable_api: boolean
enable_site: boolean
icon?: string | null
icon_background?: string | null
icon_type?: string | null
icon: string | null
icon_background: string | null
icon_type: IconType | null
id: string
maintainer?: string | null
max_active_requests?: number | null
mode: string
model_config?: AppModelConfigResponse | null
maintainer: string | null
max_active_requests: number | null
mode: AppMode
model_config: AppModelConfigResponse | null
name: string
permission_keys?: Array<string>
site?: AppDetailSiteResponseWritable | null
tags?: Array<Tag>
tracing?: unknown | null
updated_at?: number | null
updated_by?: string | null
use_icon_as_answer_icon?: boolean | null
workflow?: WorkflowPartial | null
permission_keys: Array<string>
site: AppDetailSiteResponseWritable | null
tags: Array<Tag>
tracing: string | null
updated_at: number
updated_by: string | null
use_icon_as_answer_icon: boolean
workflow: WorkflowPartial | null
}
export type RecentAppListResponseWritable = {
@@ -2997,7 +3388,7 @@ export type WorkflowCommentDetailWritable = {
}
export type AppPartialWritable = {
access_mode?: string | null
access_mode?: WebAppAccessMode | null
app_id?: string | null
author_name?: string | null
bound_agent_id?: string | null
@@ -3008,12 +3399,12 @@ export type AppPartialWritable = {
has_draft_trigger?: boolean | null
icon?: string | null
icon_background?: string | null
icon_type?: string | null
icon_type?: IconType | null
id: string
is_starred?: boolean
maintainer?: string | null
max_active_requests?: number | null
mode: string
mode: AppMode
model_config?: ModelConfigPartial | null
name: string
permission_keys?: Array<string>
@@ -3025,30 +3416,30 @@ export type AppPartialWritable = {
}
export type AppDetailSiteResponseWritable = {
access_token?: string | null
app_base_url?: string | null
chat_color_theme?: string | null
chat_color_theme_inverted?: boolean | null
code?: string | null
copyright?: string | null
created_at?: number | null
created_by?: string | null
custom_disclaimer?: string | null
customize_domain?: string | null
customize_token_strategy?: string | null
default_language?: string | null
description?: string | null
icon?: string | null
icon_background?: string | null
icon_type?: string | IconType | null
input_placeholder?: string | null
privacy_policy?: string | null
prompt_public?: boolean | null
show_workflow_steps?: boolean | null
title?: string | null
updated_at?: number | null
updated_by?: string | null
use_icon_as_answer_icon?: boolean | null
access_token: string | null
app_base_url: string
chat_color_theme: string | null
chat_color_theme_inverted: boolean
code: string | null
copyright: string | null
created_at: number
created_by: string | null
custom_disclaimer: string
customize_domain: string | null
customize_token_strategy: CustomizeTokenStrategy
default_language: string
description: string | null
icon: string | null
icon_background: string | null
icon_type: IconType | null
input_placeholder: string | null
privacy_policy: string | null
prompt_public: boolean
show_workflow_steps: boolean
title: string
updated_at: number
updated_by: string | null
use_icon_as_answer_icon: boolean
}
export type RecentAppResponseWritable = {
File diff suppressed because it is too large. Load diff
@@ -799,7 +799,7 @@ export type AgentSuggestedQuestionsAfterAnswerFeatureConfig = {
}
export type AgentTextToSpeechFeatureConfig = {
autoPlay?: string | null
autoPlay?: 'disabled' | 'enabled' | null
enabled?: boolean
language?: string | null
voice?: string | null
@@ -760,7 +760,7 @@ export const zAgentFeatureToggleConfig = z.object({
* AgentTextToSpeechFeatureConfig
*/
export const zAgentTextToSpeechFeatureConfig = z.object({
autoPlay: z.string().nullish(),
autoPlay: z.enum(['disabled', 'enabled']).nullish(),
enabled: z.boolean().optional().default(false),
language: z.string().nullish(),
voice: z.string().nullish(),
@@ -81,7 +81,7 @@ export type AppDslImportPayload = {
export type AppDslImportResponse = {
app_id?: string | null
app_mode?: string | null
app_mode?: AppMode | null
current_dsl_version?: string
error?: string
hints?: Array<Hint>
@@ -305,7 +305,7 @@ export type HumanInputFormDefinitionResponse = {
export type Import = {
app_id?: string | null
app_mode?: string | null
app_mode?: AppMode | null
current_dsl_version?: string
error?: string
id: string
@@ -398,7 +398,7 @@ export const zImportStatus = z.enum(['completed', 'completed-with-warnings', 'fa
*/
export const zAppDslImportResponse = z.object({
app_id: z.string().nullish(),
app_mode: z.string().nullish(),
app_mode: zAppMode.nullish(),
current_dsl_version: z.string().optional().default('0.7.0'),
error: z.string().optional().default(''),
hints: z.array(zHint).optional(),
@@ -414,7 +414,7 @@ export const zAppDslImportResponse = z.object({
*/
export const zImport = z.object({
app_id: z.string().nullish(),
app_mode: z.string().nullish(),
app_mode: zAppMode.nullish(),
current_dsl_version: z.string().optional().default('0.7.0'),
error: z.string().optional().default(''),
id: z.string(),
@@ -198,7 +198,7 @@ describe('useAppInfoActions', () => {
describe('onEdit', () => {
it('should update app info and close modal on success', async () => {
const updatedApp = { ...mockAppDetail, name: 'Updated' }
const updatedApp = { ...mockAppDetail, name: 'Updated', max_active_requests: null }
mockUpdateAppInfo.mockResolvedValue(updatedApp)
const { result } = renderHook(() => useAppInfoActions({}))
@@ -222,8 +222,8 @@ describe('useAppInfoActions', () => {
expect.any(Function),
)
const updateCachedApp = mockSetQueryData.mock.calls[0]![1]
expect(updateCachedApp({ id: 'app-1', name: 'Old name' })).toEqual(
expect.objectContaining({ id: 'app-1', name: 'Updated' }),
expect(updateCachedApp({ id: 'app-1', name: 'Old name', max_active_requests: 5 })).toEqual(
expect.objectContaining({ id: 'app-1', name: 'Updated', max_active_requests: null }),
)
expect(mockInvalidateQueries).toHaveBeenCalledWith({
queryKey: consoleQuery.apps.get.key(),
@@ -68,7 +68,10 @@ const updateCachedAppMetadata = (cachedApp: AppDetailWithSite | undefined, app:
icon_background: app.icon_background,
icon_type: app.icon_type,
icon_url: app.icon_url,
max_active_requests: app.max_active_requests,
max_active_requests:
app.max_active_requests === undefined
? cachedApp.max_active_requests
: app.max_active_requests,
name: app.name,
updated_at: app.updated_at,
use_icon_as_answer_icon: app.use_icon_as_answer_icon,
@@ -646,8 +646,12 @@ describe('SettingsModal', () => {
it('should disable save button while saving', async () => {
// Arrange
const user = userEvent.setup()
let finishSave!: (dataset: DataSet) => void
mockUpdateDatasetSetting.mockImplementation(
() => new Promise((resolve) => setTimeout(resolve, 100)),
() =>
new Promise((resolve) => {
finishSave = resolve
}),
)
// Act
@@ -658,6 +662,9 @@ describe('SettingsModal', () => {
// Assert
expect(saveButton).toBeDisabled()
finishSave(createDataset())
await waitFor(() => expect(saveButton).toBeEnabled())
})
it('should show error toast when save fails', async () => {
@@ -4,6 +4,7 @@ import { consoleQuery } from '@/service/console'
import { seedAccountProfileQuery } from '@/test/console/account-profile'
import { createQueryClientWrapper } from '@/test/console/query-client'
import { renderHook as renderHookWithConsoleState } from '@/test/console/render'
import { createAppDetailFixture } from '@/test/fixtures/app'
import { createTestQueryClient } from '@/test/query-client'
import { AppModeEnum, ModelModeType } from '@/types/app'
import { AppACLPermission } from '@/utils/permission'
@@ -272,14 +273,17 @@ describe('useConfiguration', () => {
const detailQueryKey = consoleQuery.apps.byAppId.get.queryKey({
input: { params: { app_id: 'app-1' } },
})
queryClient.setQueryData(detailQueryKey, {
enable_api: false,
enable_site: false,
icon_url: null,
id: 'app-1',
mode: 'chat',
name: 'Cached app',
})
queryClient.setQueryData(
detailQueryKey,
createAppDetailFixture({
enable_api: false,
enable_site: false,
icon_url: null,
id: 'app-1',
mode: 'chat',
name: 'Cached app',
}),
)
await waitFor(() => {
expect(result.current.showLoading).toBe(false)
@@ -91,7 +91,7 @@ const createMockApp = (overrides: Partial<AppPartial> = {}): AppPartial => ({
created_at: Date.now(),
updated_at: Date.now(),
tags: [],
access_mode: 'public_access',
access_mode: 'public',
...overrides,
})
@@ -1,5 +1,5 @@
'use client'
import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen'
import type { AppPartial } from '@dify/contracts/api/console/apps/types.gen'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useRef } from 'react'
@@ -13,7 +13,7 @@ type Props = Readonly<{
app_id: string
inputs: Record<string, unknown>
}
appDetail: Pick<AppDetailWithSite, 'id' | 'mode'>
appDetail: Pick<AppPartial, 'id' | 'mode'>
onFormChange: (value: Record<string, unknown>) => void
}>
@@ -1,6 +1,7 @@
'use client'
import type {
AppDetailWithSite,
AppPartial,
WorkflowResponse,
} from '@dify/contracts/api/console/apps/types.gen'
import type { FileUpload } from '@/app/components/base/features/types'
@@ -162,7 +163,7 @@ function buildWorkflowSchema(
}
type UseAppInputsFormSchemaParams = {
appDetail: Pick<AppDetailWithSite, 'id' | 'mode'>
appDetail: Pick<AppPartial, 'id' | 'mode'>
}
type UseAppInputsFormSchemaResult = {
@@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { AgentPermission } from '@/features/agent-v2/acl'
import { createAgentFixture } from '@/test/fixtures/agent'
import { AgentDetailSection, AgentDetailTop } from '../navigation'
const mocks = vi.hoisted(() => ({
@@ -86,22 +87,23 @@ vi.mock('@/service/console', () => ({
},
}))
const createAgent = (overrides: Partial<AgentAppDetailWithSite> = {}): AgentAppDetailWithSite => ({
permission_keys: Object.values(AgentPermission),
app_id: 'app-1',
description: 'Find and summarize market materials.',
enable_api: true,
enable_site: true,
icon: '🧪',
icon_background: '#E0F2FE',
icon_type: 'emoji',
id: 'agent-1',
icon_url: null,
mode: 'agent',
name: 'Research Agent',
role: 'Research Assistant',
...overrides,
})
const createAgent = (overrides: Partial<AgentAppDetailWithSite> = {}): AgentAppDetailWithSite =>
createAgentFixture({
permission_keys: Object.values(AgentPermission),
app_id: 'app-1',
description: 'Find and summarize market materials.',
enable_api: true,
enable_site: true,
icon: '🧪',
icon_background: '#E0F2FE',
icon_type: 'emoji',
id: 'agent-1',
icon_url: null,
mode: 'agent',
name: 'Research Agent',
role: 'Research Assistant',
...overrides,
})
function renderAgentDetailSection(expand = true) {
const queryClient = new QueryClient()
@@ -15,6 +15,8 @@ import { consoleQuery } from '@/service/console'
import { seedAccountProfileQuery } from '@/test/console/account-profile'
import { createQueryClientWrapper } from '@/test/console/query-client'
import { seedFeatures, seedSystemFeatures } from '@/test/console/query-data'
import { createAgentFixture } from '@/test/fixtures/agent'
import { createAppDetailFixture, createAppSiteFixture } from '@/test/fixtures/app'
import { ServiceApiAccessCard } from '../service-api-access-card'
import { WebAppAccessCard } from '../web-app-access-card'
@@ -230,7 +232,7 @@ vi.mock('@/service/console', () => ({
}))
function createAgent(overrides: Partial<AgentAppDetailWithSite> = {}): AgentAppDetailWithSite {
return {
return createAgentFixture({
permission_keys: Object.values(AgentPermission),
access_ready: true,
enable_api: true,
@@ -243,32 +245,23 @@ function createAgent(overrides: Partial<AgentAppDetailWithSite> = {}): AgentAppD
backing_app_id: 'app-1',
api_base_url: 'https://api.example.test/v1',
access_mode: 'sso_verified',
site: {
access_token: 'site-token',
site: createAppSiteFixture({
app_base_url: 'https://chat.example.test',
chat_color_theme_inverted: false,
default_language: 'en-US',
icon_url: null,
show_workflow_steps: false,
title: 'Support Agent',
use_icon_as_answer_icon: false,
} as NonNullable<AgentAppDetailWithSite['site']> & {
access_token: string
app_base_url: string
},
}),
...overrides,
}
})
}
function createAppDetailResponse(overrides: Partial<AppDetail> = {}): AppDetail {
return {
return createAppDetailFixture({
enable_api: true,
enable_site: true,
id: 'app-1',
mode: 'agent',
name: 'Support Agent',
...overrides,
}
})
}
function createAgentApiAccessResponse(
@@ -624,6 +617,35 @@ describe('Agent access surface cards', () => {
expect(toast.error).toHaveBeenCalledWith('common.actionMsg.modifiedUnsuccessfully')
})
it('should replace the Web App URL token without discarding the cached site settings', async () => {
const user = userEvent.setup()
const agent = createAgent()
const queryClient = renderWithQueryClient(
<WebAppAccessCard agent={agent} agentId="agent-1" isLoading={false} />,
)
queryClient.setQueryData(['agent-detail', 'agent-1'], agent)
mocks.siteAccessTokenResetMutation.mockResolvedValueOnce(
createAppSiteFixture({ code: 'new-site-token' }),
)
await user.click(
screen.getByRole('button', { name: 'agentV2.agentDetail.access.webApp.refreshUrl' }),
)
expect(mocks.siteAccessTokenResetMutation.mock.calls[0]?.[0]).toEqual({
params: { app_id: 'app-1' },
})
await waitFor(() => {
expect(
queryClient.getQueryData<AgentAppDetailWithSite>(['agent-detail', 'agent-1'])?.site,
).toMatchObject({
code: 'new-site-token',
access_token: 'new-site-token',
title: 'Support Agent',
})
})
})
it('should open the customize dialog with the backing app id and API base URL', async () => {
const user = userEvent.setup()
@@ -707,7 +729,7 @@ describe('Agent access surface cards', () => {
})
})
it('should save settings through the backing app id and update the agent detail cache', async () => {
it('should save settings through the backing app id and invalidate agent detail', async () => {
const user = userEvent.setup()
mocks.getUserCanAccess.mockResolvedValue({ result: false })
const agent = createAgent({
@@ -781,18 +803,9 @@ describe('Agent access surface cards', () => {
})
})
expect(mocks.siteMutation.mock.calls[0]?.[0].body).not.toHaveProperty('enable_sso')
expect(
queryClient.getQueryData<AgentAppDetailWithSite>(['agent-detail', 'agent-1']),
).toMatchObject({
site: {
access_token: 'new-site-token',
chat_color_theme: '#123456',
description: 'Updated web description.',
icon_url: null,
title: 'Support Portal',
},
await waitFor(() => {
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['agent-detail', 'agent-1'] })
})
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['agent-detail', 'agent-1'] })
})
it('should fall back to the Agent icon tuple when WebApp site icon data is missing', async () => {
@@ -918,10 +931,10 @@ describe('Agent access surface cards', () => {
).toBeDisabled()
})
it('should keep customize disabled until the generated contract provides the required fields', () => {
it('should keep customize disabled when the app base URL is empty', () => {
renderWithQueryClient(
<WebAppAccessCard
agent={createAgent({ api_base_url: null })}
agent={createAgent({ api_base_url: '' })}
agentId="agent-1"
isLoading={false}
/>,
@@ -1246,22 +1259,19 @@ describe('Agent access surface cards', () => {
expect(screen.getByRole('button', { name: organizationAccessLabel })).toBeDisabled()
})
it.each([null, 'future-access-mode'])(
'should hide the access mode entry when the access mode is %s',
(accessMode) => {
renderWithQueryClient(
<WebAppAccessCard
agent={createAgent({ access_mode: accessMode })}
agentId="agent-1"
isLoading={false}
/>,
)
it('should hide the access mode entry when the access mode is unavailable', () => {
renderWithQueryClient(
<WebAppAccessCard
agent={createAgent({ access_mode: null })}
agentId="agent-1"
isLoading={false}
/>,
)
expect(
screen.queryByRole('button', { name: organizationAccessLabel }),
).not.toBeInTheDocument()
},
)
expect(
screen.queryByRole('button', { name: organizationAccessLabel }),
).not.toBeInTheDocument()
})
it('should open the shared App access control dialog for the backing app', async () => {
const user = userEvent.setup()
@@ -117,8 +117,8 @@ export function WebAppAccessCard({
...agentDetail,
site: {
...agentDetail.site,
...site,
access_token: site.code,
code: site.code ?? agentDetail.site.code,
access_token: site.code ?? agentDetail.site.access_token,
},
}
},
@@ -191,39 +191,12 @@ export function WebAppAccessCard({
const sitePayload = body satisfies AppSiteUpdatePayload
try {
const updatedSite = await updateSiteMutation.mutateAsync({
await updateSiteMutation.mutateAsync({
params: {
app_id: appId,
},
body: sitePayload,
})
queryClient.setQueryData<AgentAppDetailWithSite | undefined>(
agentDetailQueryKey,
(agentDetail) =>
agentDetail
? {
...agentDetail,
site: {
...agentDetail.site,
...updatedSite,
...sitePayload,
access_token:
updatedSite.code ??
agentDetail.site?.access_token ??
agentDetail.site?.code ??
null,
code:
updatedSite.code ??
agentDetail.site?.code ??
agentDetail.site?.access_token ??
null,
app_base_url: agentDetail.site?.app_base_url ?? site?.app_base_url ?? null,
icon_url: null,
},
}
: agentDetail,
)
await queryClient.invalidateQueries({ queryKey: agentDetailQueryKey })
toast.success(tCommon(($) => $['actionMsg.modifiedSuccessfully']))
} catch {
@@ -9,6 +9,7 @@ import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { consoleQuery } from '@/service/console'
import { createSystemFeaturesFixture } from '@/test/console/system-features'
import { createAgentFixture } from '@/test/fixtures/agent'
import { createAppSiteFixture } from '@/test/fixtures/app'
import { createTestQueryClient } from '@/test/query-client'
import { AgentConfigurePublishBar } from '../publish-bar'
@@ -49,11 +50,10 @@ async function setup() {
createAgentFixture({
access_ready: true,
enable_site: true,
site: {
site: createAppSiteFixture({
access_token: 'published-token',
app_base_url: 'https://apps.example.test',
icon_url: null,
},
}),
}),
)
queryClient.setQueryData(
@@ -17,6 +17,7 @@ import {
import { agentComposerPromptAtom } from '@/features/agent-v2/agent-composer/store-modules/prompt'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { consoleQuery } from '@/service/console'
import { createAppSiteFixture } from '@/test/fixtures/app'
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
import { AgentConfigurePublishBar } from '../publish-bar'
@@ -1140,11 +1141,10 @@ describe('AgentConfigurePublishBar', () => {
const readyAgent = {
access_ready: true,
enable_site: true,
site: {
site: createAppSiteFixture({
access_token: 'published-token',
app_base_url: 'https://apps.example.test',
icon_url: null,
},
}),
}
it.each([
+7 -28
View File
@@ -6,6 +6,8 @@ import type { MutationFunctionContext, QueryFunctionContext } from '@tanstack/re
import type { consoleQuery as ConsoleQuery } from '@/service/console'
import { MutationObserver, QueryClient, QueryObserver } from '@tanstack/react-query'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import { createAgentFixture } from '@/test/fixtures/agent'
import { createAppDetailFixture } from '@/test/fixtures/app'
import { normalizeConsoleOpenAPIURL } from './openapi-url'
const loadConsoleQuery = async () => {
@@ -84,22 +86,8 @@ const getRetryFn = (queryOptions: object): RetryFn => {
return retry as RetryFn
}
const createAgent = (overrides: Partial<AgentMutationResponse> = {}): AgentMutationResponse => ({
...overrides,
access_ready: overrides.access_ready ?? true,
debug_conversation_has_messages: overrides.debug_conversation_has_messages ?? false,
debug_conversation_message_count: overrides.debug_conversation_message_count ?? 0,
enable_api: overrides.enable_api ?? true,
enable_site: overrides.enable_site ?? true,
description: overrides.description ?? 'Agent description',
hidden_app_backed: overrides.hidden_app_backed ?? false,
id: overrides.id ?? 'agent-1',
icon_url: overrides.icon_url ?? null,
mode: overrides.mode ?? 'agent',
name: overrides.name ?? 'Agent',
permission_keys: overrides.permission_keys ?? [],
role: overrides.role ?? 'Assistant',
})
const createAgent = (overrides: Partial<AgentMutationResponse> = {}): AgentMutationResponse =>
createAgentFixture({ role: 'Assistant', ...overrides })
const createComposerState = (
overrides: Partial<AgentComposerMutationResponse> = {},
@@ -645,13 +633,7 @@ describe('consoleQuery app mutation defaults', () => {
.spyOn(queryClient, 'invalidateQueries')
.mockImplementation(() => new Promise(() => {}))
const context = createMutationContext(queryClient)
const appDetail: AppDetail = {
enable_api: true,
enable_site: true,
id: 'app-1',
mode: 'chat',
name: 'App',
}
const appDetail: AppDetail = createAppDetailFixture()
const appSite: AppSiteResponse = {
app_id: 'app-1',
customize_token_strategy: 'fixed',
@@ -723,14 +705,11 @@ describe('consoleQuery app mutation defaults', () => {
const otherDetailQueryKey = consoleQuery.apps.byAppId.get.queryKey({
input: { params: { app_id: 'app-2' } },
})
const updatedApp = {
const updatedApp = createAppDetailFixture({
enable_api: false,
enable_site: false,
icon_url: null,
id: 'app-1',
mode: 'chat',
name: 'Updated app',
}
})
queryClient.setQueryData(detailQueryKey, { ...updatedApp, name: 'Old app' })
queryClient.setQueryData(otherDetailQueryKey, { ...updatedApp, id: 'app-2', name: 'Other app' })
+2
View File
@@ -1,9 +1,11 @@
import type { AgentAppDetailWithSite } from '@dify/contracts/api/console/agent/types.gen'
import { zAgentAppDetailWithSite } from '@dify/contracts/api/console/agent/zod.gen'
import { AgentPermission } from '@/features/agent-v2/acl'
import { createAppDetailFixture } from './app'
export function createAgentFixture(overrides: Partial<AgentAppDetailWithSite> = {}) {
return zAgentAppDetailWithSite.parse({
...createAppDetailFixture(),
id: 'agent-1',
app_id: 'app-1',
name: 'Agent',
+73
View File
@@ -0,0 +1,73 @@
import type {
AppDetailSiteResponse,
AppDetailWithSite,
} from '@dify/contracts/api/console/apps/types.gen'
export function createAppSiteFixture(
overrides: Partial<AppDetailSiteResponse> = {},
): AppDetailSiteResponse {
return {
access_token: 'site-token',
app_base_url: 'https://apps.example.test',
chat_color_theme: null,
chat_color_theme_inverted: false,
code: 'site-token',
copyright: null,
created_at: 1,
created_by: null,
custom_disclaimer: '',
customize_domain: null,
customize_token_strategy: 'not_allow',
default_language: 'en-US',
description: null,
icon: null,
icon_background: null,
icon_type: null,
icon_url: null,
input_placeholder: null,
privacy_policy: null,
prompt_public: false,
show_workflow_steps: false,
title: 'App',
updated_at: 1,
updated_by: null,
use_icon_as_answer_icon: false,
...overrides,
}
}
export function createAppDetailFixture(
overrides: Partial<AppDetailWithSite> = {},
): AppDetailWithSite {
return {
access_mode: null,
api_base_url: 'https://api.example.test/v1',
app_id: null,
bound_agent_id: null,
created_at: 1,
created_by: null,
deleted_tools: [],
description: '',
enable_api: true,
enable_site: true,
icon: null,
icon_background: null,
icon_type: null,
icon_url: null,
id: 'app-1',
maintainer: null,
max_active_requests: null,
mode: 'chat',
model_config: null,
name: 'App',
permission_keys: [],
site: null,
tags: [],
tracing: null,
updated_at: 1,
updated_by: null,
use_icon_as_answer_icon: false,
workflow: null,
...overrides,
}
}