diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 335aefdaf2d..a286768d380 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -246,7 +246,7 @@ class ModelConfigPartial(ResponseModel): return to_timestamp(value) -class ModelConfig(ResponseModel): +class AppModelConfigResponse(ResponseModel): opening_statement: str | None = None suggested_questions: Any | None = Field( default=None, validation_alias=AliasChoices("suggested_questions_list", "suggested_questions") @@ -419,7 +419,7 @@ class AppDetail(AppResponseModel): icon_background: str | None = None enable_site: bool enable_api: bool - model_config_: ModelConfig | None = Field( + model_config_: AppModelConfigResponse | None = Field( default=None, validation_alias=AliasChoices("app_model_config", "model_config"), alias="model_config", @@ -525,7 +525,13 @@ def _enrich_app_list_items(session: Session, *, apps: Sequence[App], tenant_id: register_enum_models(console_ns, RetrievalMethod, WorkflowExecutionStatus, DatasetPermissionEnum) register_response_schema_models( - console_ns, RedirectUrlResponse, SimpleResultResponse, AppImportResponse, AppTraceResponse + console_ns, + RedirectUrlResponse, + SimpleResultResponse, + AppImportResponse, + AppTraceResponse, + AppModelConfigResponse, + AppDetail, ) register_schema_models( @@ -544,10 +550,8 @@ register_schema_models( Tag, WorkflowPartial, ModelConfigPartial, - ModelConfig, AppDetailSiteResponse, DeletedTool, - AppDetail, AppExportResponse, Segmentation, PreProcessingRule, diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 4be0ab2211d..6025d02fe39 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -359,6 +359,12 @@ class WorkflowPublishResponse(ResponseModel): created_at: int +class SyncDraftWorkflowResponse(ResponseModel): + result: str + hash: str + updated_at: int + + class WorkflowRestoreResponse(ResponseModel): result: str hash: str @@ -441,6 +447,7 @@ register_response_schema_models( WorkflowOnlineUsersByApp, WorkflowOnlineUsersResponse, WorkflowPublishResponse, + SyncDraftWorkflowResponse, WorkflowRestoreResponse, DefaultBlockConfigsResponse, DefaultBlockConfigResponse, @@ -556,14 +563,7 @@ class DraftWorkflowApi(Resource): @console_ns.response( 200, "Draft workflow synced successfully", - console_ns.model( - "SyncDraftWorkflowResponse", - { - "result": fields.String, - "hash": fields.String, - "updated_at": fields.String, - }, - ), + console_ns.models[SyncDraftWorkflowResponse.__name__], ) @console_ns.response(400, "Invalid workflow configuration") @console_ns.response(403, "Permission denied") @@ -618,11 +618,14 @@ class DraftWorkflowApi(Resource): except VariableError as e: raise InvalidArgumentError(description=str(e)) - return { - "result": "success", - "hash": workflow.unique_hash, - "updated_at": TimestampField().format(workflow.updated_at or workflow.created_at), - } + return dump_response( + SyncDraftWorkflowResponse, + { + "result": "success", + "hash": workflow.unique_hash, + "updated_at": TimestampField().format(workflow.updated_at or workflow.created_at), + }, + ) @console_ns.route("/apps//advanced-chat/workflows/draft/run") diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index ecefaac1954..7b3a11ccf91 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -13278,7 +13278,7 @@ Model class for AI model. | maintainer | string | | No | | max_active_requests | integer | | No | | mode | string | | Yes | -| model_config | [ModelConfig](#modelconfig) | | No | +| model_config | [AppModelConfigResponse](#appmodelconfigresponse) | | No | | name | string | | Yes | | permission_keys | [ string ] | | No | | role | string | | No | @@ -15348,7 +15348,6 @@ This class is used to store the schema information of an api based tool. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | access_mode | string | | No | -| app_model_config | [ModelConfig](#modelconfig) | | No | | created_at | integer | | No | | created_by | string | | No | | description | string | | No | @@ -15358,7 +15357,8 @@ This class is used to store the schema information of an api based tool. | icon_background | string | | No | | id | string | | Yes | | maintainer | string | | No | -| mode_compatible_with_agent | string | | Yes | +| mode | string | | Yes | +| model_config | [AppModelConfigResponse](#appmodelconfigresponse) | | No | | name | string | | Yes | | permission_keys | [ string ] | | No | | tags | [ [Tag](#tag) ] | | No | @@ -15420,7 +15420,7 @@ This class is used to store the schema information of an api based tool. | maintainer | string | | No | | max_active_requests | integer | | No | | mode | string | | Yes | -| model_config | [ModelConfig](#modelconfig) | | No | +| model_config | [AppModelConfigResponse](#appmodelconfigresponse) | | No | | name | string | | Yes | | permission_keys | [ string ] | | No | | site | [AppDetailSiteResponse](#appdetailsiteresponse) | | No | @@ -15519,6 +15519,35 @@ AppMCPServer Status Enum | ---- | ---- | ----------- | -------- | | AppMCPServerStatus | string | AppMCPServer Status Enum | | +#### AppModelConfigResponse + +| 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 | + #### AppNamePayload | Name | Type | Description | Required | @@ -21954,9 +21983,9 @@ The subscription constructor of the trigger provider | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| hash | string | | No | -| result | string | | No | -| updated_at | string | | No | +| hash | string | | Yes | +| result | string | | Yes | +| updated_at | integer | | Yes | #### SystemConfigurationResponse diff --git a/api/tests/unit_tests/commands/test_generate_swagger_specs.py b/api/tests/unit_tests/commands/test_generate_swagger_specs.py index a0caf0263e7..72669270ad1 100644 --- a/api/tests/unit_tests/commands/test_generate_swagger_specs.py +++ b/api/tests/unit_tests/commands/test_generate_swagger_specs.py @@ -212,6 +212,17 @@ def test_generate_specs_include_console_contract_shapes_for_schema_migration(tmp assert {"type": "null"} in app_detail_nullable_schema["anyOf"] assert schemas["RecommendedAppInfoResponse"]["properties"]["icon_url"]["readOnly"] is True assert schemas["InstalledAppInfoResponse"]["properties"]["icon_url"]["readOnly"] is True + assert _response_schema(paths["/apps/{app_id}"]["get"])["$ref"] == "#/components/schemas/AppDetailWithSite" + app_model_config = schemas["AppDetailWithSite"]["properties"]["model_config"] + assert {"$ref": "#/components/schemas/AppModelConfigResponse"} in app_model_config["anyOf"] + app_detail = schemas["AppDetail"] + assert "mode" in app_detail["properties"] + assert "mode_compatible_with_agent" not in app_detail["properties"] + sync_draft_workflow = schemas["SyncDraftWorkflowResponse"] + assert _response_schema(paths["/apps/{app_id}/workflows/draft"]["post"])["$ref"] == ( + "#/components/schemas/SyncDraftWorkflowResponse" + ) + assert sync_draft_workflow["properties"]["updated_at"]["type"] == "integer" tool_icon_schema = schemas["ExploreAppMetaResponse"]["properties"]["tool_icons"]["additionalProperties"] assert {"type": "string"} in tool_icon_schema["anyOf"] assert {"additionalProperties": True, "type": "object"} in tool_icon_schema["anyOf"] diff --git a/e2e/features/agent-v2/support/access-point.ts b/e2e/features/agent-v2/support/access-point.ts index fa0b53e5b25..4d5dea98973 100644 --- a/e2e/features/agent-v2/support/access-point.ts +++ b/e2e/features/agent-v2/support/access-point.ts @@ -1,12 +1,18 @@ import type { AgentApiAccessResponse, + AgentApiStatusPayload, ApiKeyItem, } from '@dify/contracts/api/console/agent/types.gen' import type { ChatRequestPayloadWithUser, PostChatMessagesResponse, } from '@dify/contracts/api/service/types.gen' -import { createApiContext, expectApiResponseOK, setAppSiteEnabled } from '../../../support/api' +import { + zPostAgentByAgentIdApiEnableResponse, + zPostAgentByAgentIdApiKeysResponse, +} from '@dify/contracts/api/console/agent/zod.gen' +import { createConsoleApiContext, expectApiResponseOK } from '../../../support/api/console-context' +import { setAppSiteEnabled } from '../../../support/api/web-apps' import { getTestAgent } from './agent' import { consumeServiceApiSse, SERVICE_API_STREAM_TIMEOUT_MS } from './service-api-sse' @@ -38,47 +44,40 @@ async function parseServiceApiChatResponse(response: Response) { } } -export async function setAgentSiteAccessAndGetURL( - agentId: string, - enabled: boolean, -): Promise { +export async function setAgentSiteAccess(agentId: string, enabled: boolean): Promise { const agent = await getTestAgent(agentId) const appId = agent.app_id ?? agent.backing_app_id if (!appId) throw new Error(`Agent v2 ${agentId} does not expose a backing app ID.`) - const appDetail = await setAppSiteEnabled(appId, enabled) - const token = agent.site?.access_token ?? agent.site?.code ?? appDetail.site?.access_token - const baseURL = agent.site?.app_base_url ?? appDetail.site?.app_base_url - if (!token || !baseURL) throw new Error(`Agent v2 ${agentId} does not expose a Web App URL.`) - - return `${baseURL.replace(/\/$/, '')}/agent/${token}` + await setAppSiteEnabled(appId, enabled) } export async function setAgentApiAccess( agentId: string, enabled: boolean, ): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { + const data = { enable_api: enabled } satisfies AgentApiStatusPayload const response = await ctx.post(`/console/api/agent/${agentId}/api-enable`, { - data: { enable_api: enabled }, + data, }) await expectApiResponseOK( response, `${enabled ? 'Enable' : 'Disable'} Agent v2 API access for ${agentId}`, ) - return (await response.json()) as AgentApiAccessResponse + return zPostAgentByAgentIdApiEnableResponse.parse(await response.json()) } finally { await ctx.dispose() } } export async function createAgentApiKey(agentId: string): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.post(`/console/api/agent/${agentId}/api-keys`) await expectApiResponseOK(response, `Create Agent v2 API key for ${agentId}`) - return (await response.json()) as ApiKeyItem + return zPostAgentByAgentIdApiKeysResponse.parse(await response.json()) } finally { await ctx.dispose() } diff --git a/e2e/features/agent-v2/support/agent-build-draft.ts b/e2e/features/agent-v2/support/agent-build-draft.ts index 7d897ef3e91..6b1405654cb 100644 --- a/e2e/features/agent-v2/support/agent-build-draft.ts +++ b/e2e/features/agent-v2/support/agent-build-draft.ts @@ -2,10 +2,10 @@ import type { AgentBuildDraftResponse, AgentSoulConfig, } from '@dify/contracts/api/console/agent/types.gen' -import { createApiContext, expectApiResponseOK } from '../../../support/api' +import { createConsoleApiContext, expectApiResponseOK } from '../../../support/api/console-context' export async function checkoutAgentBuildDraft(agentId: string): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.post(`/console/api/agent/${agentId}/build-draft/checkout`, { data: { force: true }, @@ -21,7 +21,7 @@ export async function saveAgentBuildDraft( agentId: string, agentSoul: AgentSoulConfig, ): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.put(`/console/api/agent/${agentId}/build-draft`, { data: { @@ -38,7 +38,7 @@ export async function saveAgentBuildDraft( } export async function agentBuildDraftExists(agentId: string): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/agent/${agentId}/build-draft`) if (response.status() === 404) return false @@ -51,7 +51,7 @@ export async function agentBuildDraftExists(agentId: string): Promise { } export async function getAgentBuildDraft(agentId: string): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/agent/${agentId}/build-draft`) await expectApiResponseOK(response, `Get Agent v2 build draft for ${agentId}`) @@ -62,7 +62,7 @@ export async function getAgentBuildDraft(agentId: string): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.delete(`/console/api/agent/${agentId}/build-draft`) await expectApiResponseOK(response, `Discard Agent v2 build draft for ${agentId}`) diff --git a/e2e/features/agent-v2/support/agent-drive.ts b/e2e/features/agent-v2/support/agent-drive.ts index 28c4772f6dd..1b0fe20e189 100644 --- a/e2e/features/agent-v2/support/agent-drive.ts +++ b/e2e/features/agent-v2/support/agent-drive.ts @@ -10,7 +10,7 @@ import type { import { Buffer } from 'node:buffer' import { readFile } from 'node:fs/promises' import path from 'node:path' -import { createApiContext, expectApiResponseOK } from '../../../support/api' +import { createConsoleApiContext, expectApiResponseOK } from '../../../support/api/console-context' export type UploadedConsoleFile = { id: string @@ -126,7 +126,7 @@ export async function uploadAgentDriveSkill({ fileName: string filePath: string }): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const upload = await toSkillArchiveUpload({ fileName, filePath }) const response = await ctx.post(`/console/api/agent/${agentId}/skills/upload`, { @@ -154,7 +154,7 @@ export async function uploadAgentConfigFileToDraft({ fileName: string filePath: string }): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const uploadResponse = await ctx.post('/console/api/files/upload', { multipart: { @@ -203,7 +203,7 @@ export async function uploadAgentConfigSkillToDraft({ fileName: string filePath: string }): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const upload = await toSkillArchiveUpload({ fileName, filePath }) const response = await ctx.post(`/console/api/agent/${agentId}/config/skills/upload`, { @@ -236,7 +236,7 @@ export async function uploadAgentConfigSkillToDraft({ } export async function getAgentDriveSkills(agentId: string): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/agent/${agentId}/drive/skills`) await expectApiResponseOK(response, `Get Agent v2 drive skills for ${agentId}`) @@ -248,7 +248,7 @@ export async function getAgentDriveSkills(agentId: string): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.delete( `/console/api/agent/${agentId}/config/files/${encodeURIComponent(name)}`, @@ -260,7 +260,7 @@ export async function deleteAgentConfigFile(agentId: string, name: string): Prom } export async function deleteAgentConfigSkill(agentId: string, name: string): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.delete( `/console/api/agent/${agentId}/config/skills/${encodeURIComponent(name)}`, @@ -272,7 +272,7 @@ export async function deleteAgentConfigSkill(agentId: string, name: string): Pro } export async function deleteAgentDriveFile(agentId: string, key: string): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const searchParams = new URLSearchParams({ key }) const response = await ctx.delete(`/console/api/agent/${agentId}/files?${searchParams}`) diff --git a/e2e/features/agent-v2/support/agent.ts b/e2e/features/agent-v2/support/agent.ts index 167b964bfed..dfcd407f754 100644 --- a/e2e/features/agent-v2/support/agent.ts +++ b/e2e/features/agent-v2/support/agent.ts @@ -1,11 +1,16 @@ import type { AgentAppComposerResponse, + AgentAppCreatePayload, AgentAppDetailWithSite, AgentReferencingWorkflowResponse, AgentReferencingWorkflowsResponse, AgentSoulConfig, } from '@dify/contracts/api/console/agent/types.gen' -import { createApiContext, expectApiResponseOK } from '../../../support/api' +import { + zGetAgentByAgentIdResponse, + zPostAgentResponse, +} from '@dify/contracts/api/console/agent/zod.gen' +import { createConsoleApiContext, expectApiResponseOK } from '../../../support/api/console-context' import { assertE2EResourceName, createE2EResourceName } from '../../../support/naming' import { createPublishableAgentSoulConfig, @@ -13,21 +18,6 @@ import { normalAgentSoulConfig, } from './agent-soul' -export type AgentSeed = Pick< - AgentAppDetailWithSite, - | 'active_config_is_published' - | 'app_id' - | 'backing_app_id' - | 'description' - | 'enable_site' - | 'id' - | 'name' - | 'role' - | 'site' -> & { - active_config_snapshot_id?: string | null -} - export type CreateTestAgentOptions = { description?: string name?: string @@ -41,22 +31,23 @@ export async function createTestAgent({ description = 'Created by Dify E2E.', name = createE2EResourceName('Agent'), role = 'E2E test assistant', -}: CreateTestAgentOptions = {}): Promise { +}: CreateTestAgentOptions = {}): Promise { assertE2EResourceName(name, 'Agent') - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { + const data = { + description, + icon: '🤖', + icon_background: '#FFEAD5', + icon_type: 'emoji', + name, + role, + } satisfies AgentAppCreatePayload const response = await ctx.post('/console/api/agent', { - data: { - description, - icon: '🤖', - icon_background: '#FFEAD5', - icon_type: 'emoji', - name, - role, - }, + data, }) await expectApiResponseOK(response, 'Create Agent v2 test agent') - return (await response.json()) as AgentSeed + return zPostAgentResponse.parse(await response.json()) } finally { await ctx.dispose() } @@ -68,25 +59,25 @@ export async function createConfiguredTestAgent({ }: { agentSoul?: AgentSoulConfig seed?: CreateTestAgentOptions -} = {}): Promise { +} = {}): Promise { const agent = await createTestAgent(seed) await saveAgentComposerDraft(agent.id, agentSoul) return agent } -export async function getTestAgent(agentId: string): Promise { - const ctx = await createApiContext() +export async function getTestAgent(agentId: string): Promise { + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/agent/${agentId}`) await expectApiResponseOK(response, `Get Agent v2 test agent ${agentId}`) - return (await response.json()) as AgentSeed + return zGetAgentByAgentIdResponse.parse(await response.json()) } finally { await ctx.dispose() } } export async function deleteTestAgent(agentId: string): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.delete(`/console/api/agent/${agentId}`) await expectApiResponseOK(response, `Delete Agent v2 test agent ${agentId}`) @@ -99,7 +90,7 @@ export async function saveAgentComposerDraft( agentId: string, agentSoul: AgentSoulConfig = defaultAgentSoulConfig, ): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.put(`/console/api/agent/${agentId}/composer`, { data: { @@ -118,7 +109,7 @@ export async function saveAgentComposerDraft( export async function getAgentReferencingWorkflows( agentId: string, ): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/agent/${agentId}/referencing-workflows`) await expectApiResponseOK(response, `Get Agent v2 referencing workflows for ${agentId}`) @@ -130,7 +121,7 @@ export async function getAgentReferencingWorkflows( } export async function getAgentComposerDraft(agentId: string): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/agent/${agentId}/composer`) await expectApiResponseOK(response, `Get Agent v2 composer draft for ${agentId}`) @@ -150,7 +141,7 @@ export async function ensureAgentComposerDraftIsPublishable(agentId: string): Pr } export async function publishAgent(agentId: string, versionNote = 'E2E publish'): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.post(`/console/api/agent/${agentId}/publish`, { data: { version_note: versionNote }, diff --git a/e2e/features/agent-v2/support/fixtures/access.ts b/e2e/features/agent-v2/support/fixtures/access.ts index 3dfb7ce414d..ba73df40c86 100644 --- a/e2e/features/agent-v2/support/fixtures/access.ts +++ b/e2e/features/agent-v2/support/fixtures/access.ts @@ -1,7 +1,10 @@ import type { AgentReferencingWorkflowsResponse } from '@dify/contracts/api/console/agent/types.gen' import type { DifyWorld } from '../../../support/world' import type { PreseededResource } from './common' -import { createApiContext, expectApiResponseOK } from '../../../../support/api' +import { + createConsoleApiContext, + expectApiResponseOK, +} from '../../../../support/api/console-context' import { requirePreseededAgent, requirePreseededWorkflow } from './agents' import { failFixturePrerequisite } from './common' @@ -14,7 +17,7 @@ export async function requirePreseededAgentWorkflowReference( const workflow = await requirePreseededWorkflow(world, workflowName) - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/agent/${agent.id}/referencing-workflows`) await expectApiResponseOK(response, `Check preseeded Agent workflow reference ${agentName}`) diff --git a/e2e/features/agent-v2/support/fixtures/agents.ts b/e2e/features/agent-v2/support/fixtures/agents.ts index 3173177f86c..303f2e448b6 100644 --- a/e2e/features/agent-v2/support/fixtures/agents.ts +++ b/e2e/features/agent-v2/support/fixtures/agents.ts @@ -4,7 +4,10 @@ import type { } from '@dify/contracts/api/console/agent/types.gen' import type { DifyWorld } from '../../../support/world' import type { PreseededResource } from './common' -import { createApiContext, expectApiResponseOK } from '../../../../support/api' +import { + createConsoleApiContext, + expectApiResponseOK, +} from '../../../../support/api/console-context' import { agentBuilderExpectedTokens, agentBuilderFixedInputs, @@ -124,7 +127,7 @@ export async function requirePreseededAgentDriveSkill( ): Promise { const agent = await requirePreseededAgent(world, agentName) - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/agent/${agent.id}/drive/skills`) await expectApiResponseOK(response, `Check preseeded Agent skill ${skillName}`) @@ -169,7 +172,7 @@ export async function requirePreseededFullConfigAgentCoreConfiguration( agentBuilderPreseededResources.agentKnowledgeBase, ) - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/agent/${agent.id}/composer`) await expectApiResponseOK(response, `Check preseeded Agent core configuration ${agentName}`) @@ -246,7 +249,7 @@ export async function requirePreseededToolStatesAgentConfiguration( agentBuilderPreseededResources.tavilySearchTool, ) - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/agent/${agent.id}/composer`) await expectApiResponseOK(response, `Check preseeded Agent tool states ${agentName}`) @@ -320,7 +323,7 @@ export async function requirePreseededDualRetrievalAgentConfiguration( agentBuilderPreseededResources.agentKnowledgeBase, ) - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/agent/${agent.id}/composer`) await expectApiResponseOK(response, `Check preseeded Agent dual retrieval ${agentName}`) diff --git a/e2e/features/agent-v2/support/fixtures/common.ts b/e2e/features/agent-v2/support/fixtures/common.ts index a73bcf841e4..802b7bf5ac3 100644 --- a/e2e/features/agent-v2/support/fixtures/common.ts +++ b/e2e/features/agent-v2/support/fixtures/common.ts @@ -1,5 +1,8 @@ import type { DifyWorld } from '../../../support/world' -import { createApiContext, expectApiResponseOK } from '../../../../support/api' +import { + createConsoleApiContext, + expectApiResponseOK, +} from '../../../../support/api/console-context' export type PreseededResource = NonNullable< DifyWorld['agentBuilder']['fixtures']['preseededResources'][string] @@ -45,7 +48,7 @@ export const findConsoleResourceByName = async { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(path) await expectApiResponseOK(response, action) diff --git a/e2e/features/agent-v2/support/fixtures/datasets.ts b/e2e/features/agent-v2/support/fixtures/datasets.ts index c06aa24f9bf..c7bc58601fb 100644 --- a/e2e/features/agent-v2/support/fixtures/datasets.ts +++ b/e2e/features/agent-v2/support/fixtures/datasets.ts @@ -6,7 +6,10 @@ import type { } from '@dify/contracts/api/console/datasets/types.gen' import type { DifyWorld } from '../../../support/world' import type { PreseededResource } from './common' -import { createApiContext, expectApiResponseOK } from '../../../../support/api' +import { + createConsoleApiContext, + expectApiResponseOK, +} from '../../../../support/api/console-context' import { agentBuilderExpectedTokens, agentBuilderFixedInputs, @@ -34,7 +37,7 @@ export const getPreseededDataset = async (resourceName: string) => { } const getDatasetIndexingStatuses = async (datasetId: string, resourceName: string) => { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/datasets/${datasetId}/indexing-status`) await expectApiResponseOK(response, `Check preseeded dataset indexing status ${resourceName}`) @@ -48,7 +51,7 @@ const getDatasetIndexingStatuses = async (datasetId: string, resourceName: strin const getDatasetDocuments = async (datasetId: string, resourceName: string) => { const documents: DocumentWithSegmentsListResponse['data'] = [] - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { let page = 1 let hasMore = true @@ -76,7 +79,7 @@ const datasetHasEnabledSegmentContainingTokens = async ( expectedTokens: string[], ) => { const documents = await getDatasetDocuments(datasetId, resourceName) - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { for (const document of documents) { const query = buildQuery({ diff --git a/e2e/features/agent-v2/support/fixtures/models.ts b/e2e/features/agent-v2/support/fixtures/models.ts index 1c4d210a423..d1acc1f2ec2 100644 --- a/e2e/features/agent-v2/support/fixtures/models.ts +++ b/e2e/features/agent-v2/support/fixtures/models.ts @@ -3,7 +3,10 @@ import type { ProviderWithModelsResponse, } from '@dify/contracts/api/console/workspaces/types.gen' import type { DifyWorld } from '../../../support/world' -import { createApiContext, expectApiResponseOK } from '../../../../support/api' +import { + createConsoleApiContext, + expectApiResponseOK, +} from '../../../../support/api/console-context' import { agentBuilderPreseededResources } from '../agent-builder-resources' import { failFixturePrerequisite } from './common' @@ -82,7 +85,7 @@ async function requireAgentBuilderModel( ): Promise> { if (!config.ok) return failFixturePrerequisite(world, config.reason) - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get( `/console/api/workspaces/current/models/model-types/${config.type}`, @@ -132,7 +135,7 @@ export async function requireAgentBuilderStableChatModel( export async function requireAgentBuilderSpeechToTextModel( world: DifyWorld, ): Promise> { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() let defaultModel: NonNullable try { diff --git a/e2e/features/agent-v2/support/fixtures/tools.ts b/e2e/features/agent-v2/support/fixtures/tools.ts index e5639dcf40d..b8be21c04ec 100644 --- a/e2e/features/agent-v2/support/fixtures/tools.ts +++ b/e2e/features/agent-v2/support/fixtures/tools.ts @@ -1,6 +1,9 @@ import type { DifyWorld } from '../../../support/world' import type { LocalizedLabel, PreseededResource } from './common' -import { createApiContext, expectApiResponseOK } from '../../../../support/api' +import { + createConsoleApiContext, + expectApiResponseOK, +} from '../../../../support/api/console-context' import { asRecord, asString, failFixturePrerequisite, matchesNameOrLabel } from './common' type BuiltinToolProvider = { @@ -93,7 +96,7 @@ export async function requirePreseededTool( const parsed = splitToolDisplayName(resourceName) if (!parsed.ok) return failFixturePrerequisite(world, parsed.reason) - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get('/console/api/workspaces/current/tools/builtin') await expectApiResponseOK(response, `Check preseeded tool ${resourceName}`) diff --git a/e2e/features/agent-v2/support/seed.ts b/e2e/features/agent-v2/support/seed.ts index 45bca4df042..08c592e8166 100644 --- a/e2e/features/agent-v2/support/seed.ts +++ b/e2e/features/agent-v2/support/seed.ts @@ -18,13 +18,9 @@ import type { import type { SeedContext, SeedResource, SeedTask } from '../../../support/seed' import type { UploadedConsoleFile } from './agent-drive' import { readFile } from 'node:fs/promises' -import { - createApiContext, - createTestApp, - expectApiResponseOK, - publishWorkflowApp, - syncAgentV2WorkflowDraft, -} from '../../../support/api' +import { createTestApp } from '../../../support/api/apps' +import { createConsoleApiContext, expectApiResponseOK } from '../../../support/api/console-context' +import { publishWorkflowApp } from '../../../support/api/workflows' import { bootstrapMarketplacePlugins } from '../../../support/marketplace-plugins' import { sleep } from '../../../support/process' import { blocked, created, skipped, updated, verified } from '../../../support/seed' @@ -53,6 +49,7 @@ import { } from './fixtures/common' import { splitToolDisplayName } from './fixtures/tools' import { agentBuilderTestMaterials, getAgentBuilderTestMaterialPath } from './test-materials' +import { syncAgentV2WorkflowDraft } from './workflow' type StableModel = { name: string @@ -127,7 +124,7 @@ const parseJsonEnv = (envName: string) => { } const findModel = async (config: StableModel, title: string) => { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get( `/console/api/workspaces/current/models/model-types/${config.type}`, @@ -156,7 +153,7 @@ const findModel = async (config: StableModel, title: string) => { } const resolveProvider = async (config: StableModel) => { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get( `/console/api/workspaces/current/model-providers?${buildQuery({ model_type: config.type })}`, @@ -178,7 +175,7 @@ const resolveProvider = async (config: StableModel) => { } const selectCustomProviderCredential = async (provider: string, credentialId?: string) => { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { if (credentialId) { const switchResponse = await ctx.post( @@ -210,7 +207,7 @@ const upsertStableProviderCredential = async ( credentials: Record, credentialId?: string, ) => { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { if (credentialId) { const updateResponse = await ctx.put( @@ -336,7 +333,7 @@ const seedAgentDecisionModel = async (context: SeedContext) => }) const getDefaultModel = async (modelType: string) => { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get( `/console/api/workspaces/current/default-model?${buildQuery({ model_type: modelType })}`, @@ -350,7 +347,7 @@ const getDefaultModel = async (modelType: string) => { } const setDefaultModel = async (model: StableModel) => { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.post('/console/api/workspaces/current/default-model', { data: { @@ -431,7 +428,7 @@ const findBuiltinTool = async (displayName: string) => { const parsed = splitToolDisplayName(displayName) if (!parsed.ok) return { ok: false as const, reason: parsed.reason } - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get('/console/api/workspaces/current/tools/builtin') await expectApiResponseOK(response, `Check built-in tool ${displayName}`) @@ -476,7 +473,7 @@ const uploadConsoleFile = async ( fileName: string, filePath: string, ): Promise => { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.post('/console/api/files/upload', { multipart: { @@ -504,7 +501,7 @@ const findDataset = (name: string) => { } const getDatasetDocuments = async (datasetId: string) => { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get( `/console/api/datasets/${datasetId}/documents?${buildQuery({ limit: '100', page: '1' })}`, @@ -525,7 +522,7 @@ const requiredKnowledgeSegmentTokens = [ const datasetHasKnowledgeSegment = async (datasetId: string) => { const documents = await getDatasetDocuments(datasetId) - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { for (const document of documents) { const response = await ctx.get( @@ -563,7 +560,7 @@ const waitForDatasetCompleted = async (datasetId: string) => { let status = 'missing' while (Date.now() < deadline) { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.get(`/console/api/datasets/${datasetId}/indexing-status`) await expectApiResponseOK(response, `Check dataset indexing ${datasetId}`) @@ -614,7 +611,7 @@ const addKnowledgeDocument = async (datasetId: string) => { }, } satisfies KnowledgeConfig - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.post(`/console/api/datasets/${datasetId}/documents`, { data: body }) await expectApiResponseOK(response, `Seed knowledge document ${datasetId}`) @@ -624,7 +621,7 @@ const addKnowledgeDocument = async (datasetId: string) => { } const createDataset = async (name: string) => { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.post('/console/api/datasets', { data: { diff --git a/e2e/features/agent-v2/support/workflow.ts b/e2e/features/agent-v2/support/workflow.ts new file mode 100644 index 00000000000..54605223b05 --- /dev/null +++ b/e2e/features/agent-v2/support/workflow.ts @@ -0,0 +1,72 @@ +import type { SyncDraftWorkflowPayload } from '@dify/contracts/api/console/apps/types.gen' +import { zPostAppsByAppIdWorkflowsDraftResponse } from '@dify/contracts/api/console/apps/zod.gen' +import * as z from 'zod' +import { createConsoleApiContext, expectApiResponseOK } from '../../../support/api/console-context' +import { getWorkflowDraft } from '../../../support/api/workflows' + +const agentV2WorkflowNodeId = 'agent-v2' +const zWorkflowGraph = z.object({ + nodes: z.array( + z.object({ + data: z.record(z.string(), z.unknown()).optional(), + id: z.string(), + }), + ), +}) + +export async function getAgentV2WorkflowNodeData(appId: string) { + const draft = await getWorkflowDraft(appId) + const graph = zWorkflowGraph.parse(draft.graph) + const agentNode = graph.nodes.find((node) => node.id === agentV2WorkflowNodeId) + if (!agentNode) + throw new Error( + `Workflow draft ${appId} does not include Agent v2 node ${agentV2WorkflowNodeId}.`, + ) + + return agentNode.data ?? {} +} + +export async function syncAgentV2WorkflowDraft(appId: string, agentId: string): Promise { + const ctx = await createConsoleApiContext() + try { + const data = { + graph: { + nodes: [ + { + id: 'start', + type: 'custom', + position: { x: 80, y: 282 }, + data: { id: 'start', type: 'start', title: 'Start', variables: [] }, + }, + { + id: 'agent-v2', + type: 'custom', + position: { x: 420, y: 282 }, + data: { + id: 'agent-v2', + type: 'agent', + title: 'Agent', + desc: '', + agent_binding: { + binding_type: 'roster_agent', + agent_id: agentId, + }, + agent_node_kind: 'dify_agent', + version: '2', + }, + }, + ], + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }, + features: {}, + environment_variables: [], + conversation_variables: [], + } satisfies SyncDraftWorkflowPayload + const response = await ctx.post(`/console/api/apps/${appId}/workflows/draft`, { data }) + await expectApiResponseOK(response, `Sync Agent v2 workflow draft for ${appId}`) + zPostAppsByAppIdWorkflowsDraftResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} diff --git a/e2e/features/agent-v2/tools.feature b/e2e/features/agent-v2/tools.feature index 42e3cb975c2..c4d3d286ebe 100644 --- a/e2e/features/agent-v2/tools.feature +++ b/e2e/features/agent-v2/tools.feature @@ -28,11 +28,11 @@ Feature: Agent v2 tools Then the Agent v2 Backend service API response should include the JSON Replace E2E marker @core - Scenario: Tool selector shows an empty state for a missing tool search + Scenario: Tool selector recovers after an unavailable installed-tool search Given I am signed in as the default E2E admin And a basic configured Agent v2 test agent has been created via API When I open the Agent v2 configure page And I search for the missing Agent v2 tool from the Tools selector - Then I should see the Agent v2 tool selector empty state + Then I should see the unavailable Agent v2 installed-tool search applied When I clear the Agent v2 tool selector search Then I should see the Agent v2 tool selector ready for another search diff --git a/e2e/features/step-definitions/agent-v2/access-point.steps.ts b/e2e/features/step-definitions/agent-v2/access-point.steps.ts index 61858c02dfb..e3ba96d1635 100644 --- a/e2e/features/step-definitions/agent-v2/access-point.steps.ts +++ b/e2e/features/step-definitions/agent-v2/access-point.steps.ts @@ -2,7 +2,7 @@ import type { DifyWorld } from '../../support/world' import type { AccessSurfaceName } from './access-point-helpers' import { Given, Then, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' -import { setAgentApiAccess, setAgentSiteAccessAndGetURL } from '../../agent-v2/support/access-point' +import { setAgentApiAccess, setAgentSiteAccess } from '../../agent-v2/support/access-point' import { publishAgentWithPublishableDraft } from '../../agent-v2/support/agent' import { getAccessRegion, @@ -19,10 +19,7 @@ Given( /^Agent v2 (Web app|Backend service API) access has been enabled via API$/, async function (this: DifyWorld, surface: AccessSurfaceName) { if (surface === 'Web app') { - this.agentBuilder.accessPoint.webAppURL = await setAgentSiteAccessAndGetURL( - getCurrentAgentId(this), - true, - ) + await setAgentSiteAccess(getCurrentAgentId(this), true) return } diff --git a/e2e/features/step-definitions/agent-v2/output-variables.steps.ts b/e2e/features/step-definitions/agent-v2/output-variables.steps.ts index 79819b1af22..ead1abf1bcd 100644 --- a/e2e/features/step-definitions/agent-v2/output-variables.steps.ts +++ b/e2e/features/step-definitions/agent-v2/output-variables.steps.ts @@ -2,10 +2,11 @@ import type { DataTable } from '@cucumber/cucumber' import type { DeclaredOutputConfig } from '@dify/contracts/api/console/apps/types.gen' import type { AgentV2WorkflowOutputVariable, DifyWorld } from '../../support/world' import { Then, When } from '@cucumber/cucumber' +import { zDeclaredOutputConfig } from '@dify/contracts/api/console/apps/zod.gen' import { expect } from '@playwright/test' -import { getWorkflowDraft } from '../../../support/api' +import * as z from 'zod' +import { getAgentV2WorkflowNodeData } from '../../agent-v2/support/workflow' -const agentV2WorkflowNodeId = 'agent-v2' const taskOutputName = 'e2e_report' const renamedTaskOutputName = 'e2e_final_report' @@ -18,23 +19,12 @@ const getCurrentAppId = (world: DifyWorld) => { return appId } -const getAgentV2WorkflowNodeData = async (appId: string) => { - const draft = await getWorkflowDraft(appId) - const agentNode = draft.graph.nodes.find((node) => node.id === agentV2WorkflowNodeId) - if (!agentNode) - throw new Error( - `Workflow draft ${appId} does not include Agent v2 node ${agentV2WorkflowNodeId}.`, - ) - - return agentNode.data ?? {} -} +const parseDeclaredOutputs = (value: unknown): DeclaredOutputConfig[] => + z.array(zDeclaredOutputConfig).optional().default([]).parse(value) const getDeclaredOutputsFromDraft = async (appId: string): Promise => { const data = await getAgentV2WorkflowNodeData(appId) - const outputs = data.agent_declared_outputs - if (!Array.isArray(outputs)) return [] - - return outputs as DeclaredOutputConfig[] + return parseDeclaredOutputs(data.agent_declared_outputs) } const getOutputVariablesFromDraft = async (appId: string) => getDeclaredOutputsFromDraft(appId) @@ -310,9 +300,7 @@ async function expectAgentTaskOutputReference( .poll( async () => { const data = await getAgentV2WorkflowNodeData(appId) - const outputs = Array.isArray(data.agent_declared_outputs) - ? (data.agent_declared_outputs as DeclaredOutputConfig[]) - : [] + const outputs = parseDeclaredOutputs(data.agent_declared_outputs) const expectedOutput = outputs.find((output) => output.name === expectedName) return { diff --git a/e2e/features/step-definitions/agent-v2/tools.steps.ts b/e2e/features/step-definitions/agent-v2/tools.steps.ts index 6219beb50be..bdd5c26e4e5 100644 --- a/e2e/features/step-definitions/agent-v2/tools.steps.ts +++ b/e2e/features/step-definitions/agent-v2/tools.steps.ts @@ -249,15 +249,16 @@ Then( }, ) -Then('I should see the Agent v2 tool selector empty state', async function (this: DifyWorld) { - const page = this.getPage() +Then( + 'I should see the unavailable Agent v2 installed-tool search applied', + async function (this: DifyWorld) { + const page = this.getPage() + const search = getToolSelectorSearch(this) - await expect(page.getByText('No integrations were found')).toBeVisible({ timeout: 30_000 }) - await expect(page.getByRole('link', { name: 'Requests to the community' })).toBeVisible() - await expect( - page.getByText(agentBuilderFixedInputs.missingToolSearchWithSuffix), - ).not.toBeVisible() -}) + await expect(search).toHaveValue(agentBuilderFixedInputs.missingToolSearchWithSuffix) + await expect(page.getByText('All tools', { exact: true })).not.toBeVisible() + }, +) Then( 'I should see the Agent v2 tool selector ready for another search', diff --git a/e2e/features/step-definitions/agent-v2/workflow-node.steps.ts b/e2e/features/step-definitions/agent-v2/workflow-node.steps.ts index c2fc6fda145..36bb0fa0db2 100644 --- a/e2e/features/step-definitions/agent-v2/workflow-node.steps.ts +++ b/e2e/features/step-definitions/agent-v2/workflow-node.steps.ts @@ -1,7 +1,7 @@ import type { DifyWorld } from '../../support/world' import { Given, Then, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' -import { createTestApp, syncAgentV2WorkflowDraft } from '../../../support/api' +import { createTestApp } from '../../../support/api/apps' import { createE2EResourceName } from '../../../support/naming' import { createConfiguredTestAgent, publishAgent } from '../../agent-v2/support/agent' import { @@ -9,6 +9,7 @@ import { normalAgentPrompt, normalAgentSoulConfig, } from '../../agent-v2/support/agent-soul' +import { syncAgentV2WorkflowDraft } from '../../agent-v2/support/workflow' Given( 'a workflow app with an Agent v2 node has been created via API', diff --git a/e2e/features/step-definitions/apps/duplicate-app.steps.ts b/e2e/features/step-definitions/apps/duplicate-app.steps.ts index 8e976d78cf4..989defab4ce 100644 --- a/e2e/features/step-definitions/apps/duplicate-app.steps.ts +++ b/e2e/features/step-definitions/apps/duplicate-app.steps.ts @@ -1,7 +1,7 @@ import type { DifyWorld } from '../../support/world' import { Given, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' -import { createTestApp } from '../../../support/api' +import { createTestApp } from '../../../support/api/apps' import { createE2EResourceName } from '../../../support/naming' Given('there is an existing E2E app available for testing', async function (this: DifyWorld) { diff --git a/e2e/features/step-definitions/apps/share-app.steps.ts b/e2e/features/step-definitions/apps/share-app.steps.ts index d34e227ace0..b06d63942b5 100644 --- a/e2e/features/step-definitions/apps/share-app.steps.ts +++ b/e2e/features/step-definitions/apps/share-app.steps.ts @@ -1,12 +1,9 @@ import type { DifyWorld } from '../../support/world' import { Given, Then, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' -import { - createTestApp, - enableAppSiteAndGetURL, - publishWorkflowApp, - syncRunnableWorkflowDraft, -} from '../../../support/api' +import { createTestApp } from '../../../support/api/apps' +import { enableAppSiteAndGetURL } from '../../../support/api/web-apps' +import { publishWorkflowApp, syncRunnableWorkflowDraft } from '../../../support/api/workflows' import { createE2EResourceName } from '../../../support/naming' Given('a workflow app has been published and shared via API', async function (this: DifyWorld) { diff --git a/e2e/features/step-definitions/apps/switch-app-mode.steps.ts b/e2e/features/step-definitions/apps/switch-app-mode.steps.ts index 20f513f471b..0ea4c980f5d 100644 --- a/e2e/features/step-definitions/apps/switch-app-mode.steps.ts +++ b/e2e/features/step-definitions/apps/switch-app-mode.steps.ts @@ -1,7 +1,7 @@ import type { DifyWorld } from '../../support/world' import { Given, Then, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' -import { createTestApp } from '../../../support/api' +import { createTestApp } from '../../../support/api/apps' import { createE2EResourceName } from '../../../support/naming' Given( diff --git a/e2e/features/step-definitions/apps/web-app-service.steps.ts b/e2e/features/step-definitions/apps/web-app-service.steps.ts index aee5962b98c..dc4baafee42 100644 --- a/e2e/features/step-definitions/apps/web-app-service.steps.ts +++ b/e2e/features/step-definitions/apps/web-app-service.steps.ts @@ -1,13 +1,9 @@ import type { DifyWorld } from '../../support/world' import { Given, Then, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' -import { - createTestApp, - getAppSiteDetail, - getAppSiteURL, - publishWorkflowApp, - syncRunnableWorkflowDraft, -} from '../../../support/api' +import { createTestApp } from '../../../support/api/apps' +import { getAppSiteDetail, getAppSiteURL } from '../../../support/api/web-apps' +import { publishWorkflowApp, syncRunnableWorkflowDraft } from '../../../support/api/workflows' import { createE2EResourceName } from '../../../support/naming' import { baseURL, defaultLocale } from '../../../test-env' diff --git a/e2e/features/step-definitions/apps/workflow-run.steps.ts b/e2e/features/step-definitions/apps/workflow-run.steps.ts index d9cf64bb9c3..76e0aa2d369 100644 --- a/e2e/features/step-definitions/apps/workflow-run.steps.ts +++ b/e2e/features/step-definitions/apps/workflow-run.steps.ts @@ -1,7 +1,7 @@ import type { DifyWorld } from '../../support/world' import { Given, Then, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' -import { syncRunnableWorkflowDraft } from '../../../support/api' +import { syncRunnableWorkflowDraft } from '../../../support/api/workflows' Given('a minimal runnable workflow draft has been synced', async function (this: DifyWorld) { const appId = this.createdAppIds.at(-1) diff --git a/e2e/features/step-definitions/common/app.steps.ts b/e2e/features/step-definitions/common/app.steps.ts index a3399130f4e..fedec7a3944 100644 --- a/e2e/features/step-definitions/common/app.steps.ts +++ b/e2e/features/step-definitions/common/app.steps.ts @@ -1,12 +1,15 @@ import type { DifyWorld } from '../../support/world' import { Given, When } from '@cucumber/cucumber' +import { zCreateAppPayload } from '@dify/contracts/api/console/apps/zod.gen' import { expect } from '@playwright/test' -import { createTestApp, syncMinimalWorkflowDraft } from '../../../support/api' +import { createTestApp } from '../../../support/api/apps' +import { syncMinimalWorkflowDraft } from '../../../support/api/workflows' import { waitForAppsConsole } from '../../../support/apps' import { createE2EResourceName } from '../../../support/naming' Given('a {string} app has been created via API', async function (this: DifyWorld, mode: string) { - const app = await createTestApp(createE2EResourceName('App', mode), mode) + const appMode = zCreateAppPayload.shape.mode.parse(mode) + const app = await createTestApp(createE2EResourceName('App', appMode), appMode) this.createdAppIds.push(app.id) this.lastCreatedAppName = app.name }) diff --git a/e2e/features/support/hooks.ts b/e2e/features/support/hooks.ts index 40c794c6b14..9155684a7a3 100644 --- a/e2e/features/support/hooks.ts +++ b/e2e/features/support/hooks.ts @@ -8,11 +8,11 @@ import { fileURLToPath } from 'node:url' import { After, AfterAll, Before, setDefaultTimeout, Status } from '@cucumber/cucumber' import { chromium, webkit } from '@playwright/test' import { AUTH_BOOTSTRAP_TIMEOUT_MS, ensureAuthenticatedState } from '../../fixtures/auth' -import { deleteTestApp } from '../../support/api' +import { deleteTestApp } from '../../support/api/apps' +import { deleteTestDataset } from '../../support/api/datasets' +import { deleteBuiltinToolCredential } from '../../support/api/tools' import { runCleanupTasks, shouldFailForCleanupErrors } from '../../support/cleanup' -import { deleteTestDataset } from '../../support/datasets' import { getVoiceInputTestMaterialPath } from '../../support/test-materials' -import { deleteBuiltinToolCredential } from '../../support/tools' import { baseURL, cucumberHeadless, cucumberSlowMo, e2eBrowser } from '../../test-env' import { deleteTestAgent } from '../agent-v2/support/agent' import { diff --git a/e2e/support/api.ts b/e2e/support/api.ts deleted file mode 100644 index 3cc4175debc..00000000000 --- a/e2e/support/api.ts +++ /dev/null @@ -1,270 +0,0 @@ -import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' -import type { APIResponse } from '@playwright/test' -import { readFile } from 'node:fs/promises' -import { zAppDetailWithSite } from '@dify/contracts/api/console/apps/zod.gen' -import { request } from '@playwright/test' -import { authStatePath } from '../fixtures/auth' -import { apiURL } from '../test-env' -import { assertE2EResourceName, createE2EResourceName } from './naming' - -type StorageState = { - cookies: Array<{ name: string; value: string }> -} - -export async function createApiContext() { - const state = JSON.parse(await readFile(authStatePath, 'utf8')) as StorageState - const csrfToken = state.cookies.find((c) => c.name.endsWith('csrf_token'))?.value ?? '' - - return request.newContext({ - baseURL: apiURL, - extraHTTPHeaders: { 'X-CSRF-Token': csrfToken }, - storageState: authStatePath, - }) -} - -export async function expectApiResponseOK(response: APIResponse, action: string): Promise { - if (response.ok()) return - - const body = await response.text().catch(() => '') - throw new Error(`${action} failed with ${response.status()} ${response.statusText()}: ${body}`) -} - -export type AppSeed = { - id: string - name: string -} - -export type WorkflowDraft = { - graph: { - edges: Array> - nodes: Array<{ - data?: Record - id: string - type: string - }> - viewport?: Record - } -} - -export async function createTestApp( - name = createE2EResourceName('App'), - mode = 'workflow', -): Promise { - assertE2EResourceName(name, 'App') - const ctx = await createApiContext() - try { - const response = await ctx.post('/console/api/apps', { - data: { - name, - mode, - icon_type: 'emoji', - icon: '🤖', - icon_background: '#FFEAD5', - }, - }) - await expectApiResponseOK(response, `Create ${mode} app ${name}`) - const body = (await response.json()) as AppSeed - return body - } finally { - await ctx.dispose() - } -} - -export async function getWorkflowDraft(appId: string): Promise { - const ctx = await createApiContext() - try { - const response = await ctx.get(`/console/api/apps/${appId}/workflows/draft`) - await expectApiResponseOK(response, `Get workflow draft for ${appId}`) - return (await response.json()) as WorkflowDraft - } finally { - await ctx.dispose() - } -} - -export async function syncMinimalWorkflowDraft(appId: string): Promise { - const ctx = await createApiContext() - try { - const response = await ctx.post(`/console/api/apps/${appId}/workflows/draft`, { - data: { - graph: { - nodes: [ - { - id: '1', - type: 'custom', - position: { x: 80, y: 282 }, - data: { id: '1', type: 'start', title: 'Start', variables: [] }, - }, - ], - edges: [], - viewport: { x: 0, y: 0, zoom: 1 }, - }, - features: {}, - environment_variables: [], - conversation_variables: [], - }, - }) - await expectApiResponseOK(response, `Sync minimal workflow draft for ${appId}`) - } finally { - await ctx.dispose() - } -} - -export async function syncAgentV2WorkflowDraft(appId: string, agentId: string): Promise { - const ctx = await createApiContext() - try { - const response = await ctx.post(`/console/api/apps/${appId}/workflows/draft`, { - data: { - graph: { - nodes: [ - { - id: 'start', - type: 'custom', - position: { x: 80, y: 282 }, - data: { id: 'start', type: 'start', title: 'Start', variables: [] }, - }, - { - id: 'agent-v2', - type: 'custom', - position: { x: 420, y: 282 }, - data: { - id: 'agent-v2', - type: 'agent', - title: 'Agent', - desc: '', - agent_binding: { - binding_type: 'roster_agent', - agent_id: agentId, - }, - agent_node_kind: 'dify_agent', - version: '2', - }, - }, - ], - edges: [], - viewport: { x: 0, y: 0, zoom: 1 }, - }, - features: {}, - environment_variables: [], - conversation_variables: [], - }, - }) - await expectApiResponseOK(response, `Sync Agent v2 workflow draft for ${appId}`) - } finally { - await ctx.dispose() - } -} - -export async function deleteTestApp(id: string): Promise { - const ctx = await createApiContext() - try { - const response = await ctx.delete(`/console/api/apps/${id}`) - await expectApiResponseOK(response, `Delete app ${id}`) - } finally { - await ctx.dispose() - } -} - -export async function syncRunnableWorkflowDraft(appId: string): Promise { - const ctx = await createApiContext() - try { - const response = await ctx.post(`/console/api/apps/${appId}/workflows/draft`, { - data: { - graph: { - nodes: [ - { - id: 'start', - type: 'custom', - position: { x: 80, y: 282 }, - data: { id: 'start', type: 'start', title: 'Start', variables: [] }, - }, - { - id: 'end', - type: 'custom', - position: { x: 480, y: 282 }, - data: { - id: 'end', - type: 'end', - title: 'End', - outputs: [{ variable: 'result', value_selector: ['sys', 'workflow_run_id'] }], - }, - }, - ], - edges: [ - { - id: 'start-end', - type: 'custom', - source: 'start', - target: 'end', - sourceHandle: 'source', - targetHandle: 'target', - }, - ], - viewport: { x: 0, y: 0, zoom: 1 }, - }, - features: {}, - environment_variables: [], - conversation_variables: [], - }, - }) - await expectApiResponseOK(response, `Sync runnable workflow draft for ${appId}`) - } finally { - await ctx.dispose() - } -} - -export async function publishWorkflowApp(appId: string): Promise { - const ctx = await createApiContext() - try { - const response = await ctx.post(`/console/api/apps/${appId}/workflows/publish`, { - data: { marked_name: '', marked_comment: '' }, - }) - await expectApiResponseOK(response, `Publish workflow app ${appId}`) - } finally { - await ctx.dispose() - } -} - -export function getAppSiteURL({ mode, site }: AppDetailWithSite): string { - if (!site?.app_base_url || !site.access_token) - throw new Error('App detail does not include a Web App URL.') - - const webAppMode = (() => { - if (mode === 'completion' || mode === 'workflow') return mode - if (mode === 'advanced-chat' || mode === 'agent-chat' || mode === 'chat') return 'chat' - throw new Error(`Unsupported Web App mode: ${mode}`) - })() - - return `${site.app_base_url}/${webAppMode}/${site.access_token}` -} - -export async function getAppSiteDetail(appId: string): Promise { - const ctx = await createApiContext() - try { - const response = await ctx.get(`/console/api/apps/${appId}`) - await expectApiResponseOK(response, `Get app site detail for ${appId}`) - return zAppDetailWithSite.parse(await response.json()) - } finally { - await ctx.dispose() - } -} - -export async function enableAppSiteAndGetURL(appId: string): Promise { - return getAppSiteURL(await setAppSiteEnabled(appId, true)) -} - -export async function setAppSiteEnabled( - appId: string, - enabled: boolean, -): Promise { - const ctx = await createApiContext() - try { - const enableResponse = await ctx.post(`/console/api/apps/${appId}/site-enable`, { - data: { enable_site: enabled }, - }) - await expectApiResponseOK(enableResponse, `${enabled ? 'Enable' : 'Disable'} app site ${appId}`) - } finally { - await ctx.dispose() - } - - return getAppSiteDetail(appId) -} diff --git a/e2e/support/api/apps.ts b/e2e/support/api/apps.ts new file mode 100644 index 00000000000..ae7ae30ffaf --- /dev/null +++ b/e2e/support/api/apps.ts @@ -0,0 +1,36 @@ +import type { CreateAppPayload, PostAppsResponse } from '@dify/contracts/api/console/apps/types.gen' +import { zPostAppsResponse } from '@dify/contracts/api/console/apps/zod.gen' +import { assertE2EResourceName, createE2EResourceName } from '../naming' +import { createConsoleApiContext, expectApiResponseOK } from './console-context' + +export async function createTestApp( + name = createE2EResourceName('App'), + mode: CreateAppPayload['mode'] = 'workflow', +): Promise { + assertE2EResourceName(name, 'App') + const ctx = await createConsoleApiContext() + try { + const data = { + name, + mode, + icon_type: 'emoji', + icon: '🤖', + icon_background: '#FFEAD5', + } satisfies CreateAppPayload + const response = await ctx.post('/console/api/apps', { data }) + await expectApiResponseOK(response, `Create ${mode} app ${name}`) + return zPostAppsResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} + +export async function deleteTestApp(id: string): Promise { + const ctx = await createConsoleApiContext() + try { + const response = await ctx.delete(`/console/api/apps/${id}`) + await expectApiResponseOK(response, `Delete app ${id}`) + } finally { + await ctx.dispose() + } +} diff --git a/e2e/support/api/console-context.ts b/e2e/support/api/console-context.ts new file mode 100644 index 00000000000..6ba5c4c33df --- /dev/null +++ b/e2e/support/api/console-context.ts @@ -0,0 +1,34 @@ +import type { APIResponse } from '@playwright/test' +import { readFile } from 'node:fs/promises' +import { request } from '@playwright/test' +import * as z from 'zod' +import { authStatePath } from '../../fixtures/auth' +import { apiURL } from '../../test-env' + +const zStorageState = z.object({ + cookies: z.array( + z.object({ + name: z.string(), + value: z.string(), + }), + ), +}) + +export async function createConsoleApiContext() { + const state = zStorageState.parse(JSON.parse(await readFile(authStatePath, 'utf8'))) + const csrfToken = state.cookies.find((cookie) => cookie.name.endsWith('csrf_token'))?.value + if (!csrfToken) throw new Error(`No CSRF token found in E2E auth state: ${authStatePath}`) + + return request.newContext({ + baseURL: apiURL, + extraHTTPHeaders: { 'X-CSRF-Token': csrfToken }, + storageState: authStatePath, + }) +} + +export async function expectApiResponseOK(response: APIResponse, action: string): Promise { + if (response.ok()) return + + const body = await response.text().catch(() => '') + throw new Error(`${action} failed with ${response.status()} ${response.statusText()}: ${body}`) +} diff --git a/e2e/support/datasets.ts b/e2e/support/api/datasets.ts similarity index 68% rename from e2e/support/datasets.ts rename to e2e/support/api/datasets.ts index 196b335af32..9a696a24d8b 100644 --- a/e2e/support/datasets.ts +++ b/e2e/support/api/datasets.ts @@ -1,7 +1,7 @@ -import { createApiContext, expectApiResponseOK } from './api' +import { createConsoleApiContext, expectApiResponseOK } from './console-context' export async function deleteTestDataset(datasetId: string): Promise { - const ctx = await createApiContext() + const ctx = await createConsoleApiContext() try { const response = await ctx.delete(`/console/api/datasets/${datasetId}`) await expectApiResponseOK(response, `Delete dataset ${datasetId}`) diff --git a/e2e/support/api/marketplace-plugins.ts b/e2e/support/api/marketplace-plugins.ts new file mode 100644 index 00000000000..1dea0c7d95c --- /dev/null +++ b/e2e/support/api/marketplace-plugins.ts @@ -0,0 +1,122 @@ +import type { + GetWorkspacesCurrentPluginTasksByTaskIdResponse, + ParserLatest, + ParserPluginIdentifiers, + PostWorkspacesCurrentPluginInstallMarketplaceResponse, + PostWorkspacesCurrentPluginInstallPkgResponse, + PostWorkspacesCurrentPluginListInstallationsIdsResponse, + PostWorkspacesCurrentPluginListLatestVersionsResponse, + PostWorkspacesCurrentPluginUploadPkgResponse, +} from '@dify/contracts/api/console/workspaces/types.gen' +import type { Buffer } from 'node:buffer' +import { + zGetWorkspacesCurrentPluginTasksByTaskIdResponse, + zPostWorkspacesCurrentPluginInstallMarketplaceResponse, + zPostWorkspacesCurrentPluginInstallPkgResponse, + zPostWorkspacesCurrentPluginListInstallationsIdsResponse, + zPostWorkspacesCurrentPluginListLatestVersionsResponse, + zPostWorkspacesCurrentPluginUploadPkgResponse, +} from '@dify/contracts/api/console/workspaces/zod.gen' +import { createConsoleApiContext, expectApiResponseOK } from './console-context' + +export async function getLatestMarketplacePluginVersions( + pluginIds: string[], +): Promise { + const ctx = await createConsoleApiContext() + try { + const data = { plugin_ids: pluginIds } satisfies ParserLatest + const response = await ctx.post('/console/api/workspaces/current/plugin/list/latest-versions', { + data, + }) + await expectApiResponseOK(response, 'Resolve latest marketplace plugin versions') + return zPostWorkspacesCurrentPluginListLatestVersionsResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} + +export async function getInstalledMarketplacePlugins( + pluginIds: string[], +): Promise { + const ctx = await createConsoleApiContext() + try { + const data = { plugin_ids: pluginIds } satisfies ParserLatest + const response = await ctx.post( + '/console/api/workspaces/current/plugin/list/installations/ids', + { data }, + ) + await expectApiResponseOK(response, 'List installed marketplace plugins') + return zPostWorkspacesCurrentPluginListInstallationsIdsResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} + +export async function getMarketplacePluginInstallTask( + taskId: string, +): Promise { + const ctx = await createConsoleApiContext() + try { + const response = await ctx.get(`/console/api/workspaces/current/plugin/tasks/${taskId}`) + await expectApiResponseOK(response, `Fetch marketplace plugin install task ${taskId}`) + return zGetWorkspacesCurrentPluginTasksByTaskIdResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} + +export async function startMarketplacePluginInstall( + pluginUniqueIdentifiers: string[], +): Promise { + const ctx = await createConsoleApiContext() + try { + const data = { + plugin_unique_identifiers: pluginUniqueIdentifiers, + } satisfies ParserPluginIdentifiers + const response = await ctx.post('/console/api/workspaces/current/plugin/install/marketplace', { + data, + }) + await expectApiResponseOK(response, 'Install marketplace plugins') + return zPostWorkspacesCurrentPluginInstallMarketplaceResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} + +export async function uploadMarketplacePluginPackageFile( + pkg: Buffer, + fileName: string, +): Promise { + const ctx = await createConsoleApiContext() + try { + const response = await ctx.post('/console/api/workspaces/current/plugin/upload/pkg', { + multipart: { + pkg: { + buffer: pkg, + mimeType: 'application/octet-stream', + name: fileName, + }, + }, + }) + await expectApiResponseOK(response, `Upload marketplace package ${fileName}`) + return zPostWorkspacesCurrentPluginUploadPkgResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} + +export async function startUploadedPluginPackageInstall( + pluginUniqueIdentifiers: string[], +): Promise { + const ctx = await createConsoleApiContext() + try { + const data = { + plugin_unique_identifiers: pluginUniqueIdentifiers, + } satisfies ParserPluginIdentifiers + const response = await ctx.post('/console/api/workspaces/current/plugin/install/pkg', { data }) + await expectApiResponseOK(response, 'Install uploaded plugin packages') + return zPostWorkspacesCurrentPluginInstallPkgResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} diff --git a/e2e/support/api/tools.ts b/e2e/support/api/tools.ts new file mode 100644 index 00000000000..10db3d50713 --- /dev/null +++ b/e2e/support/api/tools.ts @@ -0,0 +1,24 @@ +import type { BuiltinToolCredentialDeletePayload } from '@dify/contracts/api/console/workspaces/types.gen' +import { zPostWorkspacesCurrentToolProviderBuiltinByProviderDeleteResponse } from '@dify/contracts/api/console/workspaces/zod.gen' +import { createConsoleApiContext, expectApiResponseOK } from './console-context' + +export async function deleteBuiltinToolCredential( + provider: string, + credentialId: string, +): Promise { + const ctx = await createConsoleApiContext() + try { + const data = { credential_id: credentialId } satisfies BuiltinToolCredentialDeletePayload + const response = await ctx.post( + `/console/api/workspaces/current/tool-provider/builtin/${provider}/delete`, + { data }, + ) + await expectApiResponseOK( + response, + `Delete built-in tool credential ${credentialId} for ${provider}`, + ) + zPostWorkspacesCurrentToolProviderBuiltinByProviderDeleteResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} diff --git a/e2e/support/api/web-apps.ts b/e2e/support/api/web-apps.ts new file mode 100644 index 00000000000..c1f557285dd --- /dev/null +++ b/e2e/support/api/web-apps.ts @@ -0,0 +1,47 @@ +import type { + AppDetailWithSite, + AppSiteStatusPayload, + GetAppsByAppIdResponse, +} from '@dify/contracts/api/console/apps/types.gen' +import { zGetAppsByAppIdResponse } from '@dify/contracts/api/console/apps/zod.gen' +import { createConsoleApiContext, expectApiResponseOK } from './console-context' + +export function getAppSiteURL({ mode, site }: AppDetailWithSite): string { + if (!site?.app_base_url || !site.access_token) + throw new Error('App detail does not include a Web App URL.') + + const webAppMode = (() => { + if (mode === 'completion' || mode === 'workflow') return mode + if (mode === 'advanced-chat' || mode === 'agent-chat' || mode === 'chat') return 'chat' + throw new Error(`Unsupported Web App mode: ${mode}`) + })() + + return `${site.app_base_url}/${webAppMode}/${site.access_token}` +} + +export async function getAppSiteDetail(appId: string): Promise { + const ctx = await createConsoleApiContext() + try { + const response = await ctx.get(`/console/api/apps/${appId}`) + await expectApiResponseOK(response, `Get app site detail for ${appId}`) + return zGetAppsByAppIdResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} + +export async function enableAppSiteAndGetURL(appId: string): Promise { + await setAppSiteEnabled(appId, true) + return getAppSiteURL(await getAppSiteDetail(appId)) +} + +export async function setAppSiteEnabled(appId: string, enabled: boolean): Promise { + const ctx = await createConsoleApiContext() + try { + const data = { enable_site: enabled } satisfies AppSiteStatusPayload + const enableResponse = await ctx.post(`/console/api/apps/${appId}/site-enable`, { data }) + await expectApiResponseOK(enableResponse, `${enabled ? 'Enable' : 'Disable'} app site ${appId}`) + } finally { + await ctx.dispose() + } +} diff --git a/e2e/support/api/workflows.ts b/e2e/support/api/workflows.ts new file mode 100644 index 00000000000..cc826c508fb --- /dev/null +++ b/e2e/support/api/workflows.ts @@ -0,0 +1,115 @@ +import type { + GetAppsByAppIdWorkflowsDraftResponse, + PublishWorkflowPayload, + SyncDraftWorkflowPayload, +} from '@dify/contracts/api/console/apps/types.gen' +import { + zGetAppsByAppIdWorkflowsDraftResponse, + zPostAppsByAppIdWorkflowsDraftResponse, + zPostAppsByAppIdWorkflowsPublishResponse, +} from '@dify/contracts/api/console/apps/zod.gen' +import { createConsoleApiContext, expectApiResponseOK } from './console-context' + +export async function getWorkflowDraft( + appId: string, +): Promise { + const ctx = await createConsoleApiContext() + try { + const response = await ctx.get(`/console/api/apps/${appId}/workflows/draft`) + await expectApiResponseOK(response, `Get workflow draft for ${appId}`) + return zGetAppsByAppIdWorkflowsDraftResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} + +export async function syncMinimalWorkflowDraft(appId: string): Promise { + const ctx = await createConsoleApiContext() + try { + const data = { + graph: { + nodes: [ + { + id: '1', + type: 'custom', + position: { x: 80, y: 282 }, + data: { id: '1', type: 'start', title: 'Start', variables: [] }, + }, + ], + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }, + features: {}, + environment_variables: [], + conversation_variables: [], + } satisfies SyncDraftWorkflowPayload + const response = await ctx.post(`/console/api/apps/${appId}/workflows/draft`, { data }) + await expectApiResponseOK(response, `Sync minimal workflow draft for ${appId}`) + zPostAppsByAppIdWorkflowsDraftResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} + +export async function syncRunnableWorkflowDraft(appId: string): Promise { + const ctx = await createConsoleApiContext() + try { + const data = { + graph: { + nodes: [ + { + id: 'start', + type: 'custom', + position: { x: 80, y: 282 }, + data: { id: 'start', type: 'start', title: 'Start', variables: [] }, + }, + { + id: 'end', + type: 'custom', + position: { x: 480, y: 282 }, + data: { + id: 'end', + type: 'end', + title: 'End', + outputs: [{ variable: 'result', value_selector: ['sys', 'workflow_run_id'] }], + }, + }, + ], + edges: [ + { + id: 'start-end', + type: 'custom', + source: 'start', + target: 'end', + sourceHandle: 'source', + targetHandle: 'target', + }, + ], + viewport: { x: 0, y: 0, zoom: 1 }, + }, + features: {}, + environment_variables: [], + conversation_variables: [], + } satisfies SyncDraftWorkflowPayload + const response = await ctx.post(`/console/api/apps/${appId}/workflows/draft`, { data }) + await expectApiResponseOK(response, `Sync runnable workflow draft for ${appId}`) + zPostAppsByAppIdWorkflowsDraftResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} + +export async function publishWorkflowApp(appId: string): Promise { + const ctx = await createConsoleApiContext() + try { + const data = { + marked_name: '', + marked_comment: '', + } satisfies PublishWorkflowPayload + const response = await ctx.post(`/console/api/apps/${appId}/workflows/publish`, { data }) + await expectApiResponseOK(response, `Publish workflow app ${appId}`) + zPostAppsByAppIdWorkflowsPublishResponse.parse(await response.json()) + } finally { + await ctx.dispose() + } +} diff --git a/e2e/support/marketplace-plugins.ts b/e2e/support/marketplace-plugins.ts index e5d1b25cd64..4932bf14eb6 100644 --- a/e2e/support/marketplace-plugins.ts +++ b/e2e/support/marketplace-plugins.ts @@ -1,36 +1,17 @@ +import type { PluginInstallTask } from '@dify/contracts/api/console/workspaces/types.gen' import type { SeedContext, SeedResult } from './seed' import { Buffer } from 'node:buffer' -import { createApiContext, expectApiResponseOK } from './api' +import { + getInstalledMarketplacePlugins, + getLatestMarketplacePluginVersions, + getMarketplacePluginInstallTask, + startMarketplacePluginInstall, + startUploadedPluginPackageInstall, + uploadMarketplacePluginPackageFile, +} from './api/marketplace-plugins' import { sleep } from './process' import { blocked, created, skipped, verified } from './seed' -type LatestPlugin = { - unique_identifier?: string - version?: string -} - -type PluginInstallation = { - plugin_id: string - plugin_unique_identifier: string -} - -type PluginInstallTask = { - id?: string - plugins?: Array<{ - message?: string - plugin_id?: string - plugin_unique_identifier?: string - status?: string - }> - status?: string -} - -type PluginInstallStartResponse = { - all_installed?: boolean - task?: PluginInstallTask | null - task_id?: string -} - type MarketplacePluginBootstrapConfig = { defaultPluginIds: string[] pluginIdsEnv: string @@ -56,45 +37,23 @@ const getPluginId = (pluginUniqueIdentifier: string) => const resolveLatestPluginIdentifiers = async (pluginIds: string[]) => { if (pluginIds.length === 0) return { identifiers: [] as string[], missing: [] as string[] } - const ctx = await createApiContext() - try { - const response = await ctx.post('/console/api/workspaces/current/plugin/list/latest-versions', { - data: { plugin_ids: pluginIds }, - }) - await expectApiResponseOK(response, 'Resolve latest marketplace plugin versions') - const body = (await response.json()) as { versions?: Record } - const identifiers: string[] = [] - const missing: string[] = [] + const body = await getLatestMarketplacePluginVersions(pluginIds) + const identifiers: string[] = [] + const missing: string[] = [] - for (const pluginId of pluginIds) { - const latest = body.versions?.[pluginId] - if (latest?.unique_identifier) identifiers.push(latest.unique_identifier) - else missing.push(pluginId) - } - - return { identifiers, missing } - } finally { - await ctx.dispose() + for (const pluginId of pluginIds) { + const latest = body.versions[pluginId] + if (latest?.unique_identifier) identifiers.push(latest.unique_identifier) + else missing.push(pluginId) } + + return { identifiers, missing } } const listInstalledPlugins = async (pluginIds: string[]) => { - if (pluginIds.length === 0) return [] as PluginInstallation[] + if (pluginIds.length === 0) return [] - const ctx = await createApiContext() - try { - const response = await ctx.post( - '/console/api/workspaces/current/plugin/list/installations/ids', - { - data: { plugin_ids: pluginIds }, - }, - ) - await expectApiResponseOK(response, 'List installed marketplace plugins') - const body = (await response.json()) as { plugins?: PluginInstallation[] } - return body.plugins ?? [] - } finally { - await ctx.dispose() - } + return (await getInstalledMarketplacePlugins(pluginIds)).plugins } const waitForPluginInstallTask = async (taskId: string, timeoutMs = 300_000) => { @@ -102,15 +61,7 @@ const waitForPluginInstallTask = async (taskId: string, timeoutMs = 300_000) => let lastTask: PluginInstallTask | undefined while (Date.now() < deadline) { - const ctx = await createApiContext() - try { - const response = await ctx.get(`/console/api/workspaces/current/plugin/tasks/${taskId}`) - await expectApiResponseOK(response, `Fetch marketplace plugin install task ${taskId}`) - const body = (await response.json()) as { task?: PluginInstallTask } - lastTask = body.task - } finally { - await ctx.dispose() - } + lastTask = (await getMarketplacePluginInstallTask(taskId)).task if (lastTask?.status === terminalSuccessTaskStatus) return { ok: true as const, task: lastTask } @@ -140,16 +91,7 @@ const waitForPluginInstallTask = async (taskId: string, timeoutMs = 300_000) => } const installMarketplacePlugins = async (pluginUniqueIdentifiers: string[]) => { - const ctx = await createApiContext() - try { - const response = await ctx.post('/console/api/workspaces/current/plugin/install/marketplace', { - data: { plugin_unique_identifiers: pluginUniqueIdentifiers }, - }) - await expectApiResponseOK(response, 'Install marketplace plugins') - return (await response.json()) as PluginInstallStartResponse - } finally { - await ctx.dispose() - } + return startMarketplacePluginInstall(pluginUniqueIdentifiers) } const getMarketplaceDownloadUrl = (pluginUniqueIdentifier: string) => { @@ -174,44 +116,12 @@ const downloadMarketplacePluginPackage = async (pluginUniqueIdentifier: string) const uploadMarketplacePluginPackage = async (pluginUniqueIdentifier: string) => { const pkg = await downloadMarketplacePluginPackage(pluginUniqueIdentifier) - const ctx = await createApiContext() - try { - const response = await ctx.post('/console/api/workspaces/current/plugin/upload/pkg', { - multipart: { - pkg: { - buffer: pkg, - mimeType: 'application/octet-stream', - name: `${getPluginId(pluginUniqueIdentifier).replaceAll('/', '-')}.difypkg`, - }, - }, - }) - await expectApiResponseOK( - response, - `Upload marketplace package ${getPluginId(pluginUniqueIdentifier)}`, - ) - const body = (await response.json()) as { unique_identifier?: string } - if (!body.unique_identifier) - throw new Error( - `Upload marketplace package ${getPluginId(pluginUniqueIdentifier)} did not return a unique identifier.`, - ) - - return body.unique_identifier - } finally { - await ctx.dispose() - } + const fileName = `${getPluginId(pluginUniqueIdentifier).replaceAll('/', '-')}.difypkg` + return (await uploadMarketplacePluginPackageFile(pkg, fileName)).unique_identifier } const installLocalPluginPackages = async (pluginUniqueIdentifiers: string[]) => { - const ctx = await createApiContext() - try { - const response = await ctx.post('/console/api/workspaces/current/plugin/install/pkg', { - data: { plugin_unique_identifiers: pluginUniqueIdentifiers }, - }) - await expectApiResponseOK(response, 'Install uploaded plugin packages') - return (await response.json()) as PluginInstallStartResponse - } finally { - await ctx.dispose() - } + return startUploadedPluginPackageInstall(pluginUniqueIdentifiers) } const shouldFallbackToLocalPackageInstall = (error: string) => diff --git a/e2e/support/tools.ts b/e2e/support/tools.ts deleted file mode 100644 index 11ea82d4dcd..00000000000 --- a/e2e/support/tools.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createApiContext, expectApiResponseOK } from './api' - -export async function deleteBuiltinToolCredential( - provider: string, - credentialId: string, -): Promise { - const ctx = await createApiContext() - try { - const response = await ctx.post( - `/console/api/workspaces/current/tool-provider/builtin/${provider}/delete`, - { - data: { credential_id: credentialId }, - }, - ) - await expectApiResponseOK( - response, - `Delete built-in tool credential ${credentialId} for ${provider}`, - ) - } finally { - await ctx.dispose() - } -} diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index d26268d5f7b..6dc95e1cd7f 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -46,7 +46,7 @@ export type AgentAppDetailWithSite = { maintainer?: string | null max_active_requests?: number | null mode: string - model_config?: ModelConfig | null + model_config?: AppModelConfigResponse | null name: string permission_keys?: Array role?: string | null @@ -536,13 +536,31 @@ export type DeletedTool = { type: string } -export type ModelConfig = { - completion_params?: { - [key: string]: unknown - } - mode: LlmMode - name: string - provider: string +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 } export type AppDetailSiteResponse = { @@ -1095,8 +1113,6 @@ export type AgentAppPublishedReferenceResponse = { app_name: string } -export type LlmMode = 'chat' | 'completion' - export type AgentKind = 'dify_agent' export type AgentPublishedReferenceResponse = { @@ -1894,7 +1910,7 @@ export type AgentAppDetailWithSiteWritable = { maintainer?: string | null max_active_requests?: number | null mode: string - model_config?: ModelConfig | null + model_config?: AppModelConfigResponse | null name: string permission_keys?: Array role?: string | null diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index cae2f7f9eb3..4e60c853ab8 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -288,6 +288,36 @@ export const zDeletedTool = z.object({ type: z.string(), }) +/** + * AppModelConfigResponse + */ +export const zAppModelConfigResponse = z.object({ + agent_mode: z.unknown().nullish(), + annotation_reply: z.unknown().nullish(), + chat_prompt_config: z.unknown().nullish(), + completion_prompt_config: z.unknown().nullish(), + created_at: z.int().nullish(), + created_by: z.string().nullish(), + dataset_configs: z.unknown().nullish(), + dataset_query_variable: z.string().nullish(), + external_data_tools: z.unknown().nullish(), + file_upload: z.unknown().nullish(), + model: z.unknown().nullish(), + more_like_this: z.unknown().nullish(), + opening_statement: z.string().nullish(), + pre_prompt: z.string().nullish(), + prompt_type: z.string().nullish(), + retriever_resource: z.unknown().nullish(), + sensitive_word_avoidance: z.unknown().nullish(), + speech_to_text: z.unknown().nullish(), + suggested_questions: z.unknown().nullish(), + suggested_questions_after_answer: z.unknown().nullish(), + text_to_speech: z.unknown().nullish(), + updated_at: z.int().nullish(), + updated_by: z.string().nullish(), + user_input_form: z.unknown().nullish(), +}) + /** * AppDetailSiteResponse */ @@ -339,6 +369,47 @@ export const zWorkflowPartial = z.object({ updated_by: z.string().nullish(), }) +/** + * AgentAppDetailWithSite + */ +export const zAgentAppDetailWithSite = z.object({ + access_mode: z.string().nullish(), + active_config_is_published: z.boolean().optional().default(false), + api_base_url: z.string().nullish(), + app_id: z.string().nullish(), + backing_app_id: z.string().nullish(), + bound_agent_id: z.string().nullish(), + created_at: z.int().nullish(), + created_by: z.string().nullish(), + debug_conversation_has_messages: z.boolean().optional().default(false), + debug_conversation_id: z.string().nullish(), + debug_conversation_message_count: z.int().optional().default(0), + deleted_tools: z.array(zDeletedTool).optional(), + description: z.string().nullish(), + enable_api: z.boolean(), + enable_site: z.boolean(), + hidden_app_backed: z.boolean().optional().default(false), + icon: z.string().nullish(), + icon_background: z.string().nullish(), + icon_type: z.string().nullish(), + icon_url: z.string().nullable(), + id: z.string(), + maintainer: z.string().nullish(), + max_active_requests: z.int().nullish(), + mode: z.string(), + model_config: zAppModelConfigResponse.nullish(), + name: z.string(), + permission_keys: z.array(z.string()).optional(), + role: z.string().nullish(), + site: zAppDetailSiteResponse.nullish(), + tags: z.array(zTag).optional(), + tracing: z.unknown().nullish(), + updated_at: z.int().nullish(), + updated_by: z.string().nullish(), + use_icon_as_answer_icon: z.boolean().nullish(), + workflow: zWorkflowPartial.nullish(), +}) + /** * ComposerBindingPayload */ @@ -1005,64 +1076,6 @@ export const zAgentAppPagination = z.object({ total: z.int(), }) -/** - * LLMMode - * - * Enum class for large language model mode. - */ -export const zLlmMode = z.enum(['chat', 'completion']) - -/** - * ModelConfig - */ -export const zModelConfig = z.object({ - completion_params: z.record(z.string(), z.unknown()).optional(), - mode: zLlmMode, - name: z.string(), - provider: z.string(), -}) - -/** - * AgentAppDetailWithSite - */ -export const zAgentAppDetailWithSite = z.object({ - access_mode: z.string().nullish(), - active_config_is_published: z.boolean().optional().default(false), - api_base_url: z.string().nullish(), - app_id: z.string().nullish(), - backing_app_id: z.string().nullish(), - bound_agent_id: z.string().nullish(), - created_at: z.int().nullish(), - created_by: z.string().nullish(), - debug_conversation_has_messages: z.boolean().optional().default(false), - debug_conversation_id: z.string().nullish(), - debug_conversation_message_count: z.int().optional().default(0), - deleted_tools: z.array(zDeletedTool).optional(), - description: z.string().nullish(), - enable_api: z.boolean(), - enable_site: z.boolean(), - hidden_app_backed: z.boolean().optional().default(false), - icon: z.string().nullish(), - icon_background: z.string().nullish(), - icon_type: z.string().nullish(), - icon_url: z.string().nullable(), - id: z.string(), - maintainer: z.string().nullish(), - max_active_requests: z.int().nullish(), - mode: z.string(), - model_config: zModelConfig.nullish(), - name: z.string(), - permission_keys: z.array(z.string()).optional(), - role: z.string().nullish(), - site: zAppDetailSiteResponse.nullish(), - tags: z.array(zTag).optional(), - tracing: z.unknown().nullish(), - updated_at: z.int().nullish(), - updated_by: z.string().nullish(), - use_icon_as_answer_icon: z.boolean().nullish(), - workflow: zWorkflowPartial.nullish(), -}) - /** * AgentKind * @@ -2732,7 +2745,7 @@ export const zAgentAppDetailWithSiteWritable = z.object({ maintainer: z.string().nullish(), max_active_requests: z.int().nullish(), mode: z.string(), - model_config: zModelConfig.nullish(), + model_config: zAppModelConfigResponse.nullish(), name: z.string(), permission_keys: z.array(z.string()).optional(), role: z.string().nullish(), diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index f6513363093..5be8467e1d2 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -40,7 +40,7 @@ export type AppDetailWithSite = { maintainer?: string | null max_active_requests?: number | null mode: string - model_config?: ModelConfig | null + model_config?: AppModelConfigResponse | null name: string permission_keys?: Array site?: AppDetailSiteResponse | null @@ -415,7 +415,6 @@ export type AppApiStatusPayload = { export type AppDetail = { access_mode?: string | null - app_model_config?: ModelConfig | null created_at?: number | null created_by?: string | null description?: string | null @@ -425,7 +424,8 @@ export type AppDetail = { icon_background?: string | null id: string maintainer?: string | null - mode_compatible_with_agent: string + mode: string + model_config?: AppModelConfigResponse | null name: string permission_keys?: Array tags?: Array @@ -1020,9 +1020,9 @@ export type SyncDraftWorkflowPayload = { } export type SyncDraftWorkflowResponse = { - hash?: string - result?: string - updated_at?: string + hash: string + result: string + updated_at: number } export type WorkflowDraftVariableList = { @@ -1322,13 +1322,31 @@ export type DeletedTool = { type: string } -export type ModelConfig = { - completion_params?: { - [key: string]: unknown - } - mode: LlmMode - name: string - provider: string +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 } export type AppDetailSiteResponse = { @@ -1608,6 +1626,15 @@ export type FeedbackStat = { like: number } +export type ModelConfig = { + completion_params?: { + [key: string]: unknown + } + mode: LlmMode + name: string + provider: string +} + export type Conversation = { admin_feedback_stats?: FeedbackStat | null annotation?: ConversationAnnotation | null @@ -2196,8 +2223,6 @@ export type ModelConfigPartial = { updated_by?: string | null } -export type LlmMode = 'chat' | 'completion' - export type PluginDependencyType = 'github' | 'marketplace' | 'package' export type Github = { @@ -2259,6 +2284,8 @@ export type StatusCount = { success: number } +export type LlmMode = 'chat' | 'completion' + export type SimpleMessageDetail = { answer: string inputs: { @@ -3075,7 +3102,7 @@ export type AppDetailWithSiteWritable = { maintainer?: string | null max_active_requests?: number | null mode: string - model_config?: ModelConfig | null + model_config?: AppModelConfigResponse | null name: string permission_keys?: Array site?: AppDetailSiteResponseWritable | null diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index 9cc33be2af6..bba24bfa40a 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -663,10 +663,13 @@ export const zSyncDraftWorkflowPayload = z.object({ hash: z.string().nullish(), }) +/** + * SyncDraftWorkflowResponse + */ export const zSyncDraftWorkflowResponse = z.object({ - hash: z.string().optional(), - result: z.string().optional(), - updated_at: z.string().optional(), + hash: z.string(), + result: z.string(), + updated_at: z.int(), }) /** @@ -886,6 +889,36 @@ export const zDeletedTool = z.object({ type: z.string(), }) +/** + * AppModelConfigResponse + */ +export const zAppModelConfigResponse = z.object({ + agent_mode: z.unknown().nullish(), + annotation_reply: z.unknown().nullish(), + chat_prompt_config: z.unknown().nullish(), + completion_prompt_config: z.unknown().nullish(), + created_at: z.int().nullish(), + created_by: z.string().nullish(), + dataset_configs: z.unknown().nullish(), + dataset_query_variable: z.string().nullish(), + external_data_tools: z.unknown().nullish(), + file_upload: z.unknown().nullish(), + model: z.unknown().nullish(), + more_like_this: z.unknown().nullish(), + opening_statement: z.string().nullish(), + pre_prompt: z.string().nullish(), + prompt_type: z.string().nullish(), + retriever_resource: z.unknown().nullish(), + sensitive_word_avoidance: z.unknown().nullish(), + speech_to_text: z.unknown().nullish(), + suggested_questions: z.unknown().nullish(), + suggested_questions_after_answer: z.unknown().nullish(), + text_to_speech: z.unknown().nullish(), + updated_at: z.int().nullish(), + updated_by: z.string().nullish(), + user_input_form: z.unknown().nullish(), +}) + /** * AppDetailSiteResponse */ @@ -937,6 +970,66 @@ export const zWorkflowPartial = z.object({ updated_by: z.string().nullish(), }) +/** + * AppDetailWithSite + */ +export const zAppDetailWithSite = z.object({ + access_mode: z.string().nullish(), + api_base_url: z.string().nullish(), + app_id: z.string().nullish(), + bound_agent_id: z.string().nullish(), + created_at: z.int().nullish(), + created_by: z.string().nullish(), + deleted_tools: z.array(zDeletedTool).optional(), + description: z.string().nullish(), + enable_api: z.boolean(), + enable_site: z.boolean(), + icon: z.string().nullish(), + icon_background: z.string().nullish(), + icon_type: z.string().nullish(), + icon_url: z.string().nullable(), + id: z.string(), + maintainer: z.string().nullish(), + max_active_requests: z.int().nullish(), + mode: z.string(), + model_config: zAppModelConfigResponse.nullish(), + name: z.string(), + permission_keys: z.array(z.string()).optional(), + site: zAppDetailSiteResponse.nullish(), + tags: z.array(zTag).optional(), + tracing: z.unknown().nullish(), + updated_at: z.int().nullish(), + updated_by: z.string().nullish(), + use_icon_as_answer_icon: z.boolean().nullish(), + workflow: zWorkflowPartial.nullish(), +}) + +/** + * AppDetail + */ +export const zAppDetail = z.object({ + access_mode: z.string().nullish(), + created_at: z.int().nullish(), + created_by: z.string().nullish(), + description: z.string().nullish(), + enable_api: z.boolean(), + enable_site: z.boolean(), + icon: z.string().nullish(), + icon_background: z.string().nullish(), + id: z.string(), + maintainer: z.string().nullish(), + mode: z.string(), + model_config: zAppModelConfigResponse.nullish(), + name: z.string(), + permission_keys: z.array(z.string()).optional(), + tags: z.array(zTag).optional(), + tracing: z.unknown().nullish(), + updated_at: z.int().nullish(), + updated_by: z.string().nullish(), + use_icon_as_answer_icon: z.boolean().nullish(), + workflow: zWorkflowPartial.nullish(), +}) + /** * ImportStatus */ @@ -2251,102 +2344,6 @@ export const zAppPagination = z.object({ total: z.int(), }) -/** - * LLMMode - * - * Enum class for large language model mode. - */ -export const zLlmMode = z.enum(['chat', 'completion']) - -/** - * ModelConfig - */ -export const zModelConfig = z.object({ - completion_params: z.record(z.string(), z.unknown()).optional(), - mode: zLlmMode, - name: z.string(), - provider: z.string(), -}) - -/** - * AppDetailWithSite - */ -export const zAppDetailWithSite = z.object({ - access_mode: z.string().nullish(), - api_base_url: z.string().nullish(), - app_id: z.string().nullish(), - bound_agent_id: z.string().nullish(), - created_at: z.int().nullish(), - created_by: z.string().nullish(), - deleted_tools: z.array(zDeletedTool).optional(), - description: z.string().nullish(), - enable_api: z.boolean(), - enable_site: z.boolean(), - icon: z.string().nullish(), - icon_background: z.string().nullish(), - icon_type: z.string().nullish(), - icon_url: z.string().nullable(), - id: z.string(), - maintainer: z.string().nullish(), - max_active_requests: z.int().nullish(), - mode: z.string(), - model_config: zModelConfig.nullish(), - name: z.string(), - permission_keys: z.array(z.string()).optional(), - site: zAppDetailSiteResponse.nullish(), - tags: z.array(zTag).optional(), - tracing: z.unknown().nullish(), - updated_at: z.int().nullish(), - updated_by: z.string().nullish(), - use_icon_as_answer_icon: z.boolean().nullish(), - workflow: zWorkflowPartial.nullish(), -}) - -/** - * AppDetail - */ -export const zAppDetail = z.object({ - access_mode: z.string().nullish(), - app_model_config: zModelConfig.nullish(), - created_at: z.int().nullish(), - created_by: z.string().nullish(), - description: z.string().nullish(), - enable_api: z.boolean(), - enable_site: z.boolean(), - icon: z.string().nullish(), - icon_background: z.string().nullish(), - id: z.string(), - maintainer: z.string().nullish(), - mode_compatible_with_agent: z.string(), - name: z.string(), - permission_keys: z.array(z.string()).optional(), - tags: z.array(zTag).optional(), - tracing: z.unknown().nullish(), - updated_at: z.int().nullish(), - updated_by: z.string().nullish(), - use_icon_as_answer_icon: z.boolean().nullish(), - workflow: zWorkflowPartial.nullish(), -}) - -/** - * ConversationDetail - */ -export const zConversationDetail = z.object({ - admin_feedback_stats: zFeedbackStat.nullish(), - annotated: z.boolean(), - created_at: z.int().nullish(), - from_account_id: z.string().nullish(), - from_end_user_id: z.string().nullish(), - from_source: z.string(), - id: z.string(), - introduction: z.string().nullish(), - message_count: z.int(), - model_config: zModelConfig.nullish(), - status: z.string(), - updated_at: z.int().nullish(), - user_feedback_stats: zFeedbackStat.nullish(), -}) - /** * PluginDependencyType */ @@ -2537,6 +2534,42 @@ export const zConversationWithSummaryPagination = z.object({ total: z.int(), }) +/** + * LLMMode + * + * Enum class for large language model mode. + */ +export const zLlmMode = z.enum(['chat', 'completion']) + +/** + * ModelConfig + */ +export const zModelConfig = z.object({ + completion_params: z.record(z.string(), z.unknown()).optional(), + mode: zLlmMode, + name: z.string(), + provider: z.string(), +}) + +/** + * ConversationDetail + */ +export const zConversationDetail = z.object({ + admin_feedback_stats: zFeedbackStat.nullish(), + annotated: z.boolean(), + created_at: z.int().nullish(), + from_account_id: z.string().nullish(), + from_end_user_id: z.string().nullish(), + from_source: z.string(), + id: z.string(), + introduction: z.string().nullish(), + message_count: z.int(), + model_config: zModelConfig.nullish(), + status: z.string(), + updated_at: z.int().nullish(), + user_feedback_stats: zFeedbackStat.nullish(), +}) + /** * SimpleMessageDetail */ @@ -4234,7 +4267,7 @@ export const zAppDetailWithSiteWritable = z.object({ maintainer: z.string().nullish(), max_active_requests: z.int().nullish(), mode: z.string(), - model_config: zModelConfig.nullish(), + model_config: zAppModelConfigResponse.nullish(), name: z.string(), permission_keys: z.array(z.string()).optional(), site: zAppDetailSiteResponseWritable.nullish(),