diff --git a/api/controllers/web/human_input_form.py b/api/controllers/web/human_input_form.py index 775e802a70c..bd2b9a56efc 100644 --- a/api/controllers/web/human_input_form.py +++ b/api/controllers/web/human_input_form.py @@ -24,7 +24,7 @@ from extensions.ext_database import db from fields.base import ResponseModel from libs.helper import RateLimiter, dump_response, extract_remote_ip, to_timestamp from models.account import TenantStatus -from models.model import App, Site +from models.model import App, AppMode, Site from repositories.factory import DifyAPIRepositoryFactory from services.feature_service import FeatureService from services.human_input_file_upload_service import HumanInputFileUploadService @@ -207,6 +207,7 @@ class HumanInputFormApi(Resource): site=WebAppSiteResponse.from_app_site( tenant=tenant, app_model=app_model, + mode=AppMode.value_of(app_model.mode), site=site, end_user_id=None, features=features, diff --git a/api/controllers/web/site.py b/api/controllers/web/site.py index f6c4af013a1..1d9a01dcf4c 100644 --- a/api/controllers/web/site.py +++ b/api/controllers/web/site.py @@ -14,7 +14,7 @@ from extensions.storage.storage_type import StorageType from fields.base import ResponseModel from libs.helper import build_icon_url from models.account import Tenant, TenantStatus -from models.model import App, EndUser, IconType, Site +from models.model import App, AppMode, EndUser, IconType, Site from services.feature_service import FeatureModel, FeatureService from services.file_service import FileService @@ -67,6 +67,7 @@ class WebAppCustomConfigResponse(ResponseModel): class WebAppSiteResponse(ResponseModel): app_id: str + mode: AppMode end_user_id: str | None = None enable_site: bool site: WebSiteResponse @@ -83,6 +84,7 @@ class WebAppSiteResponse(ResponseModel): *, tenant: Tenant, app_model: App, + mode: AppMode, site: Site, end_user_id: str | None, features: FeatureModel, @@ -109,6 +111,7 @@ class WebAppSiteResponse(ResponseModel): return cls( app_id=app_model.id, + mode=mode, end_user_id=end_user_id, enable_site=app_model.enable_site, site=site_response, @@ -167,6 +170,7 @@ class AppSiteApi(WebApiResource): return WebAppSiteResponse.from_app_site( tenant=tenant, app_model=app_model, + mode=AppMode.value_of(app_model.mode_compatible_with_agent_with_session(session=db.session())), site=site, end_user_id=end_user.id, features=features, diff --git a/api/openapi/markdown/web-openapi.md b/api/openapi/markdown/web-openapi.md index 09c6842329c..c5fe79b60e5 100644 --- a/api/openapi/markdown/web-openapi.md +++ b/api/openapi/markdown/web-openapi.md @@ -965,6 +965,12 @@ Returns Server-Sent Events stream. | ---- | ---- | ----------- | -------- | | tool_icons | object | Tool icon metadata keyed by tool name | No | +#### AppMode + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| AppMode | string | | | + #### AppPermissionQuery | Name | Type | Description | Required | @@ -1646,6 +1652,7 @@ in form definition, or a variable while the workflow is running. | custom_config | [WebAppCustomConfigResponse](#webappcustomconfigresponse) | | No | | enable_site | boolean | | Yes | | end_user_id | string | | No | +| mode | [AppMode](#appmode) | | Yes | | model_config | [WebModelConfigResponse](#webmodelconfigresponse) | | No | | plan | string | | Yes | | site | [WebSiteResponse](#websiteresponse) | | Yes | diff --git a/api/tests/test_containers_integration_tests/controllers/web/test_site.py b/api/tests/test_containers_integration_tests/controllers/web/test_site.py index 7f4fd45d037..1e4d5aba030 100644 --- a/api/tests/test_containers_integration_tests/controllers/web/test_site.py +++ b/api/tests/test_containers_integration_tests/controllers/web/test_site.py @@ -97,6 +97,7 @@ class TestAppSiteApi: assert result["end_user_id"] == end_user.id assert result["plan"] == "basic" assert result["enable_site"] is True + assert result["mode"] == AppMode.CHAT @patch("controllers.web.site.FileService.get_file_presigned_url") @patch("controllers.web.site.FeatureService.get_features") @@ -178,6 +179,7 @@ class TestWebAppSiteResponse: response = WebAppSiteResponse.from_app_site( tenant=tenant, app_model=app_model, + mode=AppMode.CHAT, site=_site_model(app_id=app_model.id), end_user_id="eu-1", features=FeatureModel(can_replace_logo=False, webapp_copyright_enabled=True), @@ -185,6 +187,7 @@ class TestWebAppSiteResponse: ) assert response.app_id == app_model.id + assert response.mode == AppMode.CHAT assert response.end_user_id == "eu-1" assert response.enable_site is True assert response.plan == "basic" @@ -209,6 +212,7 @@ class TestWebAppSiteResponse: response = WebAppSiteResponse.from_app_site( tenant=tenant, app_model=app_model, + mode=AppMode.CHAT, site=site, end_user_id=None, features=FeatureModel(can_replace_logo=False, webapp_copyright_enabled=True), @@ -236,6 +240,7 @@ class TestWebAppSiteResponse: response = WebAppSiteResponse.from_app_site( tenant=tenant, app_model=app_model, + mode=AppMode.CHAT, site=_site_model(app_id=app_model.id), end_user_id="eu-1", features=FeatureModel(can_replace_logo=True, webapp_copyright_enabled=True), diff --git a/api/tests/unit_tests/controllers/web/test_human_input_form.py b/api/tests/unit_tests/controllers/web/test_human_input_form.py index 3408e3049d1..042cb30a98e 100644 --- a/api/tests/unit_tests/controllers/web/test_human_input_form.py +++ b/api/tests/unit_tests/controllers/web/test_human_input_form.py @@ -163,6 +163,7 @@ def test_get_form_includes_site(monkeypatch: pytest.MonkeyPatch, app: Flask, dat assert body["expiration_time"] == int(expiration_time.timestamp()) assert body["site"] == { "app_id": app_model.id, + "mode": "chat", "end_user_id": None, "enable_site": True, "site": { @@ -383,6 +384,7 @@ def test_get_form_allows_backstage_token(monkeypatch: pytest.MonkeyPatch, app: F assert body["expiration_time"] == int(expiration_time.timestamp()) assert body["site"] == { "app_id": app_model.id, + "mode": "chat", "end_user_id": None, "enable_site": True, "site": { diff --git a/api/tests/unit_tests/controllers/web/test_site.py b/api/tests/unit_tests/controllers/web/test_site.py index 1c2a403994f..f7a44afad6a 100644 --- a/api/tests/unit_tests/controllers/web/test_site.py +++ b/api/tests/unit_tests/controllers/web/test_site.py @@ -3,7 +3,42 @@ from unittest.mock import MagicMock, patch from configs import dify_config from controllers.web import site as site_module from extensions.storage.storage_type import StorageType -from models.model import IconType, Site +from models.model import AppMode, IconType, Site +from services.feature_service import FeatureModel + + +def test_app_site_api_returns_legacy_agent_compatible_mode() -> None: + app_model = MagicMock() + app_model.id = "app-id" + app_model.tenant_id = "tenant-id" + app_model.tenant = MagicMock(id="tenant-id", status="normal") + app_model.mode_compatible_with_agent_with_session.return_value = AppMode.AGENT_CHAT + end_user = MagicMock(id="end-user-id") + site = MagicMock(spec=Site) + response = MagicMock() + response.model_dump.return_value = {"mode": AppMode.AGENT_CHAT} + + with ( + patch.object(site_module, "db") as mock_db, + patch.object(site_module.FeatureService, "get_features", return_value=FeatureModel(can_replace_logo=False)), + patch.object(site_module, "_build_site_icon_url", return_value=None), + patch.object(site_module.WebAppSiteResponse, "from_app_site", return_value=response) as mock_from_app_site, + ): + mock_db.session.scalar.return_value = site + result = site_module.AppSiteApi().get(app_model, end_user) + + assert result["mode"] == AppMode.AGENT_CHAT + app_model.mode_compatible_with_agent_with_session.assert_called_once_with(session=mock_db.session()) + mock_from_app_site.assert_called_once_with( + tenant=app_model.tenant, + app_model=app_model, + mode=AppMode.AGENT_CHAT, + site=site, + end_user_id=end_user.id, + features=FeatureModel(can_replace_logo=False), + can_replace_logo=False, + icon_url=None, + ) def test_build_site_icon_url_uses_s3_presigned_url() -> None: diff --git a/packages/contracts/generated/api/web/types.gen.ts b/packages/contracts/generated/api/web/types.gen.ts index d832ca3d98a..8812b164b9e 100644 --- a/packages/contracts/generated/api/web/types.gen.ts +++ b/packages/contracts/generated/api/web/types.gen.ts @@ -43,6 +43,16 @@ export type AppMetaResponse = { } } +export type AppMode = + | 'advanced-chat' + | 'agent' + | 'agent-chat' + | 'channel' + | 'chat' + | 'completion' + | 'rag-pipeline' + | 'workflow' + export type AppPermissionQuery = { appId: string } @@ -575,6 +585,7 @@ export type WebAppSiteResponse = { custom_config?: WebAppCustomConfigResponse | null enable_site: boolean end_user_id?: string | null + mode: AppMode model_config?: WebModelConfigResponse | null plan: string site: WebSiteResponse diff --git a/packages/contracts/generated/api/web/zod.gen.ts b/packages/contracts/generated/api/web/zod.gen.ts index 0e1cd8fc76d..59eed414d4b 100644 --- a/packages/contracts/generated/api/web/zod.gen.ts +++ b/packages/contracts/generated/api/web/zod.gen.ts @@ -39,6 +39,20 @@ export const zAppMetaResponse = z.object({ tool_icons: z.record(z.string(), z.unknown()).optional(), }) +/** + * AppMode + */ +export const zAppMode = z.enum([ + 'advanced-chat', + 'agent', + 'agent-chat', + 'channel', + 'chat', + 'completion', + 'rag-pipeline', + 'workflow', +]) + /** * AppPermissionQuery */ @@ -884,6 +898,7 @@ export const zWebAppSiteResponse = z.object({ custom_config: zWebAppCustomConfigResponse.nullish(), enable_site: z.boolean(), end_user_id: z.string().nullish(), + mode: zAppMode, model_config: zWebModelConfigResponse.nullish(), plan: z.string(), site: zWebSiteResponse,