From 3f2d22ec0f9a3cbafb727a18cdaa9a8b19191bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9B=90=E7=B2=92=20Yanli?= Date: Thu, 18 Jun 2026 13:03:34 +0800 Subject: [PATCH] feat(agent-v2): sync nightly updates to main (#37599) Co-authored-by: Jingyi-Dify Co-authored-by: yyh Co-authored-by: Joel Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: hjlarry Co-authored-by: Bond Zhu <783504079@qq.com> Co-authored-by: Yansong Zhang <916125788@qq.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- api/controllers/console/agent/roster.py | 91 +- api/controllers/console/app/agent.py | 117 +-- api/controllers/console/app/app.py | 14 +- api/openapi/markdown/console-openapi.md | 127 ++- .../agent/skill_standardize_service.py | 6 +- api/services/app_service.py | 16 +- api/services/entities/agent_entities.py | 1 + .../console/agent/test_agent_controllers.py | 2 +- .../console/app/test_agent_skills.py | 87 +- .../console/app/test_app_response_models.py | 4 + .../services/agent/test_agent_services.py | 2 + .../unit_tests/services/test_app_service.py | 105 ++ docker/.env.example | 1 + docker/envs/core-services/web.env.example | 1 + eslint-suppressions.json | 67 +- .../generated/api/console/agent/orpc.gen.ts | 45 +- .../generated/api/console/agent/types.gen.ts | 41 +- .../generated/api/console/agent/zod.gen.ts | 35 +- .../generated/api/console/apps/orpc.gen.ts | 267 +++-- .../generated/api/console/apps/types.gen.ts | 84 +- .../generated/api/console/apps/zod.gen.ts | 86 +- .../assets/public/agent/building-blocks.svg | 3 + .../assets/vender/agent-v2/access-point.svg | 8 + .../assets/vender/agent-v2/end-user-auth.svg | 5 + .../assets/vender/agent-v2/plan.svg | 3 + .../assets/vender/agent-v2/prompt-insert.svg | 3 + .../assets/vender/agent-v2/robot-3.svg | 5 + .../assets/vender/main-nav/roster-active.svg | 6 + .../assets/vender/main-nav/roster.svg | 9 + .../assets/vender/workflow/agent.svg | 10 +- .../custom-public/icons.json | 5 +- .../custom-public/info.json | 6 +- .../custom-vender/icons.json | 40 +- .../custom-vender/info.json | 14 +- web/.env.example | 3 + web/AGENTS.md | 9 + web/__tests__/env.spec.ts | 42 + web/app/(commonLayout)/role-route-guard.tsx | 2 +- .../roster/__tests__/feature-guard.spec.ts | 41 + .../roster/__tests__/layout.spec.tsx | 43 + .../roster/agent/[agentId]/access/page.tsx | 13 + .../roster/agent/[agentId]/configure/page.tsx | 13 + .../roster/agent/[agentId]/layout.tsx | 20 + .../roster/agent/[agentId]/logs/page.tsx | 13 + .../agent/[agentId]/monitoring/page.tsx | 13 + .../roster/agent/[agentId]/page.tsx | 13 + .../(commonLayout)/roster/feature-guard.ts | 7 + web/app/(commonLayout)/roster/layout.tsx | 12 + web/app/(commonLayout)/roster/page.tsx | 5 + web/app/(shareLayout)/agent/[token]/page.tsx | 15 + .../__tests__/setting-built-in-tool.spec.tsx | 14 + .../agent-tools/setting-built-in-tool.tsx | 4 +- .../dataset-config/select-dataset/index.tsx | 6 +- .../dataset-config/settings-modal/index.tsx | 6 +- web/app/components/base/app-icon/index.tsx | 26 +- .../__tests__/index.spec.tsx | 8 + .../base/features/new-feature-panel/index.tsx | 20 +- .../moderation-setting-modal.spec.tsx | 36 +- .../moderation/moderation-content.tsx | 95 +- .../moderation/moderation-setting-modal.tsx | 273 +++-- .../base/icons/src/vender/workflow/Agent.json | 44 +- .../prompt-editor/__tests__/index.spec.tsx | 25 +- .../__tests__/prompt-editor-content.spec.tsx | 140 +++ .../components/base/prompt-editor/index.tsx | 24 +- .../__tests__/component.spec.tsx | 369 +++++++ .../__tests__/node.spec.tsx | 106 ++ .../plugins/agent-output-block/commands.ts | 3 + .../plugins/agent-output-block/component.tsx | 211 ++++ .../plugins/agent-output-block/index.tsx | 138 +++ .../plugins/agent-output-block/node.tsx | 131 +++ .../plugins/agent-output-block/utils.ts | 174 ++++ .../__tests__/index.spec.tsx | 47 + .../plugins/component-picker-block/index.tsx | 40 +- .../__tests__/node.spec.tsx | 119 +++ .../roster-reference-block/component.tsx | 55 + .../plugins/roster-reference-block/context.ts | 4 + .../plugins/roster-reference-block/index.tsx | 63 ++ .../plugins/roster-reference-block/node.tsx | 76 ++ .../plugins/roster-reference-block/utils.ts | 111 ++ .../prompt-editor/prompt-editor-content.tsx | 91 +- .../components/base/prompt-editor/types.ts | 13 + .../components/base/prompt-editor/utils.ts | 13 +- .../use-integrations-setting.spec.ts | 21 + .../header/account-setting/menu-dialog.tsx | 4 +- .../model-provider-page/declarations.ts | 1 + .../model-selector/__tests__/index.spec.tsx | 6 +- .../model-selector/index.tsx | 21 +- .../model-selector/popup.tsx | 6 +- .../use-integrations-setting.ts | 13 +- .../main-nav/__tests__/index.spec.tsx | 93 +- web/app/components/main-nav/index.tsx | 84 +- .../__tests__/authorized-in-node.spec.tsx | 20 +- .../plugin-auth/authorized-in-node.tsx | 17 +- web/app/components/plugins/types.ts | 7 +- .../use-available-nodes-meta-data.spec.ts | 43 +- .../hooks/use-available-nodes-meta-data.ts | 18 +- .../tools/integrations-setting-modal.tsx | 17 +- .../components/workflow-children.tsx | 4 +- .../__tests__/features-trigger.spec.tsx | 78 +- .../workflow-header/features-trigger.tsx | 11 +- .../workflow-onboarding-modal/index.tsx | 4 +- .../start-node-selection-panel.tsx | 6 +- .../use-available-nodes-meta-data.spec.ts | 31 + .../hooks/use-available-nodes-meta-data.ts | 16 +- .../__tests__/candidate-node-main.spec.tsx | 116 +++ web/app/components/workflow/block-icon.tsx | 2 + .../block-selector/__tests__/blocks.spec.tsx | 338 +++++- .../block-selector/__tests__/index.spec.tsx | 4 + .../block-selector/agent-selector.tsx | 311 ++++++ .../workflow/block-selector/blocks.tsx | 104 +- .../workflow/block-selector/constants.tsx | 15 +- .../workflow/block-selector/tool-picker.tsx | 135 +-- .../block-selector/tool/action-item.tsx | 1 + .../workflow/block-selector/tool/tool.tsx | 4 +- .../workflow/block-selector/types.ts | 28 + .../workflow/candidate-node-main.tsx | 37 +- web/app/components/workflow/constants.ts | 11 +- web/app/components/workflow/constants/node.ts | 2 + .../__tests__/use-nodes-interactions.spec.ts | 96 ++ .../workflow/hooks/use-checklist.ts | 13 +- .../workflow/hooks/use-nodes-interactions.ts | 100 +- .../workflow/hooks/use-nodes-meta-data.ts | 3 +- .../nodes/_base/components/node-handle.tsx | 6 +- .../variable/__tests__/utils.spec.ts | 116 ++- .../nodes/_base/components/variable/utils.ts | 146 ++- .../workflow-panel/__tests__/index.spec.tsx | 37 +- .../_base/components/workflow-panel/index.tsx | 98 +- .../workflow-panel/last-run/use-last-run.ts | 5 +- .../nodes/agent-v2/__tests__/default.spec.ts | 110 ++ .../nodes/agent-v2/__tests__/hooks.spec.tsx | 280 +++++ .../nodes/agent-v2/__tests__/node.spec.tsx | 186 ++++ .../nodes/agent-v2/__tests__/panel.spec.tsx | 978 ++++++++++++++++++ .../nodes/agent-v2/agent-soul-config.ts | 170 +++ .../components/agent-advanced-settings.tsx | 27 + .../agent-orchestrate-drawer-panel.tsx | 119 +++ .../__tests__/utils.spec.ts | 57 + .../agent-output-variables/edit-card.tsx | 186 ++++ .../agent-output-variables/index.tsx | 151 +++ .../agent-output-variables/type-select.tsx | 51 + .../agent-output-variables/utils.ts | 200 ++++ .../components/agent-roster-field.tsx | 375 +++++++ .../agent-v2/components/agent-task-field.tsx | 152 +++ .../workflow/nodes/agent-v2/default.ts | 36 + .../workflow/nodes/agent-v2/hooks.ts | 114 ++ .../workflow/nodes/agent-v2/node.tsx | 126 +++ .../nodes/agent-v2/output-variables.ts | 86 ++ .../workflow/nodes/agent-v2/panel.tsx | 216 ++++ .../workflow/nodes/agent-v2/types.ts | 45 + .../components/workflow/nodes/components.ts | 38 +- .../components/add-dataset.tsx | 3 + .../components/dataset-item.tsx | 25 +- .../components/dataset-list.tsx | 12 + .../components/retrieval-config.tsx | 3 + .../extract-parameter/import-from-tool.tsx | 4 +- .../operator/__tests__/add-block.spec.tsx | 162 ++- .../workflow/operator/add-block.tsx | 68 +- .../version-history-item.tsx | 7 +- .../run/__tests__/result-panel.spec.tsx | 15 +- .../__tests__/special-result-panel.spec.tsx | 4 +- .../__tests__/agent-log-nav.spec.tsx | 2 +- .../__tests__/agent-log-trigger.spec.tsx | 6 +- .../workflow/run/agent-log/agent-log-nav.tsx | 11 +- .../run/agent-log/agent-log-trigger.tsx | 30 +- .../run/agent-log/agent-result-panel.tsx | 5 +- .../workflow/run/agent-log/index.tsx | 2 - web/app/components/workflow/run/node.tsx | 2 +- .../components/workflow/run/result-panel.tsx | 2 +- .../workflow/run/special-result-panel.tsx | 2 +- .../workflow/__tests__/node-slice.spec.ts | 3 + .../workflow/store/workflow/node-slice.ts | 4 + web/app/components/workflow/types.ts | 8 +- web/app/components/workflow/utils/node.ts | 20 + web/app/components/workflow/utils/workflow.ts | 3 +- .../workflow/variable-inspect/right.tsx | 9 - web/context/modal-context-provider.tsx | 1 + web/context/modal-context.ts | 1 + web/contract/console/files.ts | 16 + web/contract/router.ts | 17 +- web/docker/entrypoint.sh | 1 + web/env.ts | 9 +- .../agent-v2/__tests__/feature-flag.spec.ts | 18 + .../agent-composer/__tests__/store.spec.ts | 444 ++++++++ .../agent-v2/agent-composer/conversions.ts | 497 +++++++++ .../agent-v2/agent-composer/form-state.ts | 118 +++ .../agent-v2/agent-composer/provider.tsx | 37 + .../agent-composer/reference-labels.ts | 75 ++ .../store-modules/app-features.ts | 25 + .../agent-composer/store-modules/env.ts | 17 + .../agent-composer/store-modules/files.ts | 17 + .../agent-composer/store-modules/knowledge.ts | 24 + .../agent-composer/store-modules/model.ts | 17 + .../agent-composer/store-modules/prompt.ts | 16 + .../agent-composer/store-modules/skills.ts | 26 + .../agent-composer/store-modules/tools.ts | 107 ++ .../agent-composer/store-modules/utils.ts | 10 + web/features/agent-v2/agent-composer/store.ts | 132 +++ .../__tests__/navigation.spec.tsx | 73 ++ .../agent-detail/access/access-sources.ts | 34 + .../workflow-references-table.spec.tsx | 138 +++ .../components/workflow-references-table.tsx | 171 +++ .../agent-v2/agent-detail/access/page.tsx | 308 ++++++ .../configure/__tests__/page.spec.tsx | 144 +++ .../use-agent-configure-sync.spec.tsx | 202 ++++ .../__tests__/agent-prompt-editor.spec.tsx | 445 ++++++++ .../__tests__/add-actions.spec.tsx | 65 ++ .../__tests__/empty-sections.spec.tsx | 52 + .../__tests__/publish-bar.spec.tsx | 320 ++++++ .../orchestrate/add-actions-context.ts | 51 + .../components/orchestrate/add-actions.tsx | 53 + .../advanced/__tests__/env.spec.tsx | 191 ++++ .../advanced/content-moderation.tsx | 184 ++++ .../orchestrate/advanced/env-utils.ts | 80 ++ .../components/orchestrate/advanced/env.tsx | 539 ++++++++++ .../components/orchestrate/advanced/index.tsx | 27 + .../orchestrate/common/add-button.tsx | 36 + .../orchestrate/common/configurable-item.tsx | 61 ++ .../components/orchestrate/common/empty.tsx | 20 + .../components/orchestrate/common/section.tsx | 105 ++ .../files/__tests__/index.spec.tsx | 162 +++ .../components/orchestrate/files/index.tsx | 192 ++++ .../components/orchestrate/files/tree.tsx | 156 +++ .../orchestrate/files/upload-dialog.tsx | 244 +++++ .../components/orchestrate/header.tsx | 23 + .../components/orchestrate/index.tsx | 116 +++ .../knowledge/__tests__/index.spec.tsx | 298 ++++++ .../orchestrate/knowledge/dialog.tsx | 472 +++++++++ .../orchestrate/knowledge/index.tsx | 129 +++ .../components/orchestrate/memory.tsx | 125 +++ .../orchestrate/model-config/field.tsx | 54 + .../orchestrate/prompt-editor/index.tsx | 425 ++++++++ .../orchestrate/prompt-editor/option-menu.tsx | 59 ++ .../orchestrate/prompt-editor/options.ts | 83 ++ .../orchestrate/prompt-editor/slash.tsx | 695 +++++++++++++ .../orchestrate/publish-bar/index.tsx | 214 ++++ .../publish-bar/publish-impact-popover.tsx | 155 +++ .../orchestrate/read-only-context.ts | 7 + .../skills/__tests__/index.spec.tsx | 309 ++++++ .../orchestrate/skills/detail-dialog.tsx | 189 ++++ .../components/orchestrate/skills/index.tsx | 82 ++ .../components/orchestrate/skills/item.tsx | 112 ++ .../orchestrate/skills/upload-dialog.tsx | 227 ++++ .../orchestrate/tools/__tests__/hooks.spec.ts | 56 + .../tools/__tests__/index.spec.tsx | 389 +++++++ .../tools/cli-tool/__tests__/dialog.spec.tsx | 169 +++ .../orchestrate/tools/cli-tool/dialog.tsx | 236 +++++ .../orchestrate/tools/cli-tool/item.tsx | 37 + .../components/orchestrate/tools/hooks.ts | 211 ++++ .../components/orchestrate/tools/index.tsx | 469 +++++++++ .../tools/provider-tool/dialog.tsx | 86 ++ .../orchestrate/tools/provider-tool/item.tsx | 292 ++++++ .../components/orchestrate/tools/types.ts | 15 + .../configure/components/orchestrate/utils.ts | 20 + .../preview/__tests__/chat.spec.tsx | 198 ++++ .../preview/chat-features-panel.tsx | 119 +++ .../configure/components/preview/chat.tsx | 441 ++++++++ .../configure/components/preview/header.tsx | 47 + .../components/preview/versions-panel.tsx | 201 ++++ .../agent-v2/agent-detail/configure/page.tsx | 263 +++++ .../configure/use-agent-configure-sync.ts | 161 +++ web/features/agent-v2/agent-detail/layout.tsx | 36 + .../agent-v2/agent-detail/logs/mock-data.ts | 376 +++++++ .../agent-v2/agent-detail/logs/page.tsx | 345 ++++++ .../agent-detail/monitoring/chart-utils.ts | 252 +++++ .../agent-detail/monitoring/chart.tsx | 84 ++ .../agent-detail/monitoring/mock-data.ts | 85 ++ .../agent-v2/agent-detail/monitoring/page.tsx | 142 +++ .../monitoring/time-range-picker.tsx | 228 ++++ .../agent-v2/agent-detail/navigation.tsx | 263 +++++ web/features/agent-v2/agent-detail/page.tsx | 31 + web/features/agent-v2/agent-detail/routes.ts | 15 + web/features/agent-v2/agent-detail/section.ts | 1 + web/features/agent-v2/feature-flag.ts | 3 + .../__tests__/agent-roster-list.spec.tsx | 140 +++ .../__tests__/create-agent-dialog.spec.tsx | 114 ++ .../__tests__/edit-agent-dialog.spec.tsx | 221 ++++ .../__tests__/roster-toolbar.spec.tsx | 63 ++ .../roster/components/agent-roster-list.tsx | 259 +++++ .../agent-workflow-references-dropdown.tsx | 79 ++ .../roster/components/create-agent-dialog.tsx | 219 ++++ .../roster/components/delete-agent-dialog.tsx | 77 ++ .../roster/components/edit-agent-dialog.tsx | 302 ++++++ .../roster/components/roster-filter.ts | 2 + .../roster/components/roster-toolbar.tsx | 95 ++ web/features/agent-v2/roster/page.tsx | 150 +++ web/i18n-config/resources.ts | 3 + web/i18n/ar-TN/common.json | 1 + web/i18n/ar-TN/workflow.json | 5 +- web/i18n/de-DE/common.json | 1 + web/i18n/de-DE/workflow.json | 3 + web/i18n/en-US/agent-v-2.json | 382 +++++++ web/i18n/en-US/common.json | 1 + web/i18n/en-US/workflow.json | 42 +- web/i18n/es-ES/common.json | 1 + web/i18n/es-ES/workflow.json | 5 +- web/i18n/fa-IR/common.json | 1 + web/i18n/fa-IR/workflow.json | 5 +- web/i18n/fr-FR/common.json | 1 + web/i18n/fr-FR/workflow.json | 3 + web/i18n/hi-IN/common.json | 1 + web/i18n/hi-IN/workflow.json | 5 +- web/i18n/id-ID/common.json | 1 + web/i18n/id-ID/workflow.json | 5 +- web/i18n/it-IT/common.json | 1 + web/i18n/it-IT/workflow.json | 5 +- web/i18n/ja-JP/common.json | 1 + web/i18n/ja-JP/workflow.json | 5 +- web/i18n/ko-KR/common.json | 1 + web/i18n/ko-KR/workflow.json | 5 +- web/i18n/nl-NL/common.json | 1 + web/i18n/nl-NL/workflow.json | 3 + web/i18n/pl-PL/common.json | 1 + web/i18n/pl-PL/workflow.json | 3 + web/i18n/pt-BR/common.json | 1 + web/i18n/pt-BR/workflow.json | 5 +- web/i18n/ro-RO/common.json | 1 + web/i18n/ro-RO/workflow.json | 3 + web/i18n/ru-RU/common.json | 1 + web/i18n/ru-RU/workflow.json | 5 +- web/i18n/sl-SI/common.json | 1 + web/i18n/sl-SI/workflow.json | 3 + web/i18n/th-TH/common.json | 1 + web/i18n/th-TH/workflow.json | 5 +- web/i18n/tr-TR/common.json | 1 + web/i18n/tr-TR/workflow.json | 5 +- web/i18n/uk-UA/common.json | 1 + web/i18n/uk-UA/workflow.json | 5 +- web/i18n/vi-VN/common.json | 1 + web/i18n/vi-VN/workflow.json | 5 +- web/i18n/zh-Hans/agent-v-2.json | 382 +++++++ web/i18n/zh-Hans/common.json | 1 + web/i18n/zh-Hans/workflow.json | 42 +- web/i18n/zh-Hant/common.json | 1 + web/i18n/zh-Hant/workflow.json | 5 +- web/knip.config.ts | 4 + web/service/client.spec.ts | 244 +++++ web/service/client.ts | 140 ++- 336 files changed, 26946 insertions(+), 1282 deletions(-) create mode 100644 packages/iconify-collections/assets/public/agent/building-blocks.svg create mode 100644 packages/iconify-collections/assets/vender/agent-v2/access-point.svg create mode 100644 packages/iconify-collections/assets/vender/agent-v2/end-user-auth.svg create mode 100644 packages/iconify-collections/assets/vender/agent-v2/plan.svg create mode 100644 packages/iconify-collections/assets/vender/agent-v2/prompt-insert.svg create mode 100644 packages/iconify-collections/assets/vender/agent-v2/robot-3.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/roster-active.svg create mode 100644 packages/iconify-collections/assets/vender/main-nav/roster.svg create mode 100644 web/__tests__/env.spec.ts create mode 100644 web/app/(commonLayout)/roster/__tests__/feature-guard.spec.ts create mode 100644 web/app/(commonLayout)/roster/__tests__/layout.spec.tsx create mode 100644 web/app/(commonLayout)/roster/agent/[agentId]/access/page.tsx create mode 100644 web/app/(commonLayout)/roster/agent/[agentId]/configure/page.tsx create mode 100644 web/app/(commonLayout)/roster/agent/[agentId]/layout.tsx create mode 100644 web/app/(commonLayout)/roster/agent/[agentId]/logs/page.tsx create mode 100644 web/app/(commonLayout)/roster/agent/[agentId]/monitoring/page.tsx create mode 100644 web/app/(commonLayout)/roster/agent/[agentId]/page.tsx create mode 100644 web/app/(commonLayout)/roster/feature-guard.ts create mode 100644 web/app/(commonLayout)/roster/layout.tsx create mode 100644 web/app/(commonLayout)/roster/page.tsx create mode 100644 web/app/(shareLayout)/agent/[token]/page.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/agent-output-block/__tests__/component.spec.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/agent-output-block/__tests__/node.spec.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/agent-output-block/commands.ts create mode 100644 web/app/components/base/prompt-editor/plugins/agent-output-block/component.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/agent-output-block/index.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/agent-output-block/node.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/agent-output-block/utils.ts create mode 100644 web/app/components/base/prompt-editor/plugins/roster-reference-block/__tests__/node.spec.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/roster-reference-block/component.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/roster-reference-block/context.ts create mode 100644 web/app/components/base/prompt-editor/plugins/roster-reference-block/index.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/roster-reference-block/node.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/roster-reference-block/utils.ts create mode 100644 web/app/components/workflow/block-selector/agent-selector.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/__tests__/default.spec.ts create mode 100644 web/app/components/workflow/nodes/agent-v2/__tests__/hooks.spec.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/__tests__/node.spec.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/agent-soul-config.ts create mode 100644 web/app/components/workflow/nodes/agent-v2/components/agent-advanced-settings.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/components/agent-output-variables/__tests__/utils.spec.ts create mode 100644 web/app/components/workflow/nodes/agent-v2/components/agent-output-variables/edit-card.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/components/agent-output-variables/index.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/components/agent-output-variables/type-select.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/components/agent-output-variables/utils.ts create mode 100644 web/app/components/workflow/nodes/agent-v2/components/agent-roster-field.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/components/agent-task-field.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/default.ts create mode 100644 web/app/components/workflow/nodes/agent-v2/hooks.ts create mode 100644 web/app/components/workflow/nodes/agent-v2/node.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/output-variables.ts create mode 100644 web/app/components/workflow/nodes/agent-v2/panel.tsx create mode 100644 web/app/components/workflow/nodes/agent-v2/types.ts delete mode 100644 web/app/components/workflow/run/agent-log/index.tsx create mode 100644 web/contract/console/files.ts create mode 100644 web/features/agent-v2/__tests__/feature-flag.spec.ts create mode 100644 web/features/agent-v2/agent-composer/__tests__/store.spec.ts create mode 100644 web/features/agent-v2/agent-composer/conversions.ts create mode 100644 web/features/agent-v2/agent-composer/form-state.ts create mode 100644 web/features/agent-v2/agent-composer/provider.tsx create mode 100644 web/features/agent-v2/agent-composer/reference-labels.ts create mode 100644 web/features/agent-v2/agent-composer/store-modules/app-features.ts create mode 100644 web/features/agent-v2/agent-composer/store-modules/env.ts create mode 100644 web/features/agent-v2/agent-composer/store-modules/files.ts create mode 100644 web/features/agent-v2/agent-composer/store-modules/knowledge.ts create mode 100644 web/features/agent-v2/agent-composer/store-modules/model.ts create mode 100644 web/features/agent-v2/agent-composer/store-modules/prompt.ts create mode 100644 web/features/agent-v2/agent-composer/store-modules/skills.ts create mode 100644 web/features/agent-v2/agent-composer/store-modules/tools.ts create mode 100644 web/features/agent-v2/agent-composer/store-modules/utils.ts create mode 100644 web/features/agent-v2/agent-composer/store.ts create mode 100644 web/features/agent-v2/agent-detail/__tests__/navigation.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/access/access-sources.ts create mode 100644 web/features/agent-v2/agent-detail/access/components/__tests__/workflow-references-table.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/access/components/workflow-references-table.tsx create mode 100644 web/features/agent-v2/agent-detail/access/page.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/__tests__/page.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/add-actions.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/empty-sections.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/add-actions-context.ts create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/add-actions.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/__tests__/env.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/content-moderation.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/env-utils.ts create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/env.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/advanced/index.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/common/add-button.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/common/configurable-item.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/common/empty.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/common/section.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/files/index.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/files/tree.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/files/upload-dialog.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/header.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/index.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/__tests__/index.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/index.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/memory.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/model-config/field.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/index.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/option-menu.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/options.ts create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/index.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/publish-bar/publish-impact-popover.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/read-only-context.ts create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/detail-dialog.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/index.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/item.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/upload-dialog.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/hooks.spec.ts create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/cli-tool/__tests__/dialog.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/cli-tool/dialog.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/cli-tool/item.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/hooks.ts create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/index.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/provider-tool/dialog.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/provider-tool/item.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/types.ts create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/utils.ts create mode 100644 web/features/agent-v2/agent-detail/configure/components/preview/__tests__/chat.spec.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/preview/chat-features-panel.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/preview/chat.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/preview/header.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/components/preview/versions-panel.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/page.tsx create mode 100644 web/features/agent-v2/agent-detail/configure/use-agent-configure-sync.ts create mode 100644 web/features/agent-v2/agent-detail/layout.tsx create mode 100644 web/features/agent-v2/agent-detail/logs/mock-data.ts create mode 100644 web/features/agent-v2/agent-detail/logs/page.tsx create mode 100644 web/features/agent-v2/agent-detail/monitoring/chart-utils.ts create mode 100644 web/features/agent-v2/agent-detail/monitoring/chart.tsx create mode 100644 web/features/agent-v2/agent-detail/monitoring/mock-data.ts create mode 100644 web/features/agent-v2/agent-detail/monitoring/page.tsx create mode 100644 web/features/agent-v2/agent-detail/monitoring/time-range-picker.tsx create mode 100644 web/features/agent-v2/agent-detail/navigation.tsx create mode 100644 web/features/agent-v2/agent-detail/page.tsx create mode 100644 web/features/agent-v2/agent-detail/routes.ts create mode 100644 web/features/agent-v2/agent-detail/section.ts create mode 100644 web/features/agent-v2/feature-flag.ts create mode 100644 web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx create mode 100644 web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx create mode 100644 web/features/agent-v2/roster/components/__tests__/edit-agent-dialog.spec.tsx create mode 100644 web/features/agent-v2/roster/components/__tests__/roster-toolbar.spec.tsx create mode 100644 web/features/agent-v2/roster/components/agent-roster-list.tsx create mode 100644 web/features/agent-v2/roster/components/agent-workflow-references-dropdown.tsx create mode 100644 web/features/agent-v2/roster/components/create-agent-dialog.tsx create mode 100644 web/features/agent-v2/roster/components/delete-agent-dialog.tsx create mode 100644 web/features/agent-v2/roster/components/edit-agent-dialog.tsx create mode 100644 web/features/agent-v2/roster/components/roster-filter.ts create mode 100644 web/features/agent-v2/roster/components/roster-toolbar.tsx create mode 100644 web/features/agent-v2/roster/page.tsx create mode 100644 web/i18n/en-US/agent-v-2.json create mode 100644 web/i18n/zh-Hans/agent-v-2.json diff --git a/api/controllers/console/agent/roster.py b/api/controllers/console/agent/roster.py index ce75f9f1d06..c45d18ca73e 100644 --- a/api/controllers/console/agent/roster.py +++ b/api/controllers/console/agent/roster.py @@ -2,20 +2,28 @@ from uuid import UUID from flask import abort, request from flask_restx import Resource -from pydantic import BaseModel, Field, field_validator +from pydantic import AliasChoices, BaseModel, Field, field_validator from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.agent.app_helpers import resolve_agent_app_model from controllers.console.app.app import ( - AppDetailWithSite, + AppDetailWithSite as GenericAppDetailWithSite, +) +from controllers.console.app.app import ( AppListQuery, - AppPagination, - AppPartial, CopyAppPayload, - UpdateAppPayload, _normalize_app_list_query_args, ) +from controllers.console.app.app import ( + AppPagination as GenericAppPagination, +) +from controllers.console.app.app import ( + AppPartial as GenericAppPartial, +) +from controllers.console.app.app import ( + UpdateAppPayload as GenericUpdateAppPayload, +) from controllers.console.wraps import ( account_initialization_required, cloud_edition_billing_resource_check, @@ -80,7 +88,10 @@ class AgentAppCreatePayload(BaseModel): return role -class AgentAppUpdatePayload(UpdateAppPayload): +# Keep agent-app roster DTOs agent-specific instead of reusing the shared +# /apps response/request models. The roster surface needs Agent-only fields such +# as `role`, while the generic console/apps contracts must stay unchanged. +class AgentAppUpdatePayload(GenericUpdateAppPayload): role: str = Field(..., min_length=1, description="Agent role", max_length=255) @field_validator("role") @@ -100,19 +111,6 @@ class AgentAppPublishedReferenceResponse(BaseModel): app_icon_background: str | None = None -class AgentAppPartial(AppPartial): - published_reference_count: int = 0 - published_references: list[AgentAppPublishedReferenceResponse] = Field(default_factory=list) - - -class AgentAppPagination(BaseModel): - page: int - limit: int - total: int - has_more: bool - data: list[AgentAppPartial] - - class AgentLogsQuery(BaseModel): page: int = Field(default=1, ge=1, description="Page number") limit: int = Field(default=20, ge=1, le=100, description="Page size") @@ -149,6 +147,26 @@ class AgentStatisticsQuery(BaseModel): return value +class AgentAppPartial(GenericAppPartial): + app_id: str | None = None + role: str | None = None + active_config_is_published: bool = False + published_reference_count: int = 0 + published_references: list[AgentAppPublishedReferenceResponse] = Field(default_factory=list) + + +class AgentAppDetailWithSite(GenericAppDetailWithSite): + app_id: str | None = None + role: str | None = None + active_config_is_published: bool = False + + +class AgentAppPagination(GenericAppPagination): + data: list[AgentAppPartial] = Field( # type: ignore[assignment] # pyrefly: ignore[bad-override-mutable-attribute] + validation_alias=AliasChoices("items", "data") + ) + + register_schema_models( console_ns, AgentAppCreatePayload, @@ -159,14 +177,14 @@ register_schema_models( AgentStatisticsQuery, AgentIdPath, AppListQuery, - UpdateAppPayload, RosterListQuery, ) register_response_schema_models( console_ns, - AppDetailWithSite, AgentAppPagination, AgentAppPublishedReferenceResponse, + AgentAppDetailWithSite, + AgentAppPartial, AgentConfigSnapshotDetailResponse, AgentConfigSnapshotListResponse, AgentInviteOptionsResponse, @@ -184,16 +202,25 @@ def _agent_roster_service() -> AgentRosterService: def _serialize_agent_app_detail(app_model) -> dict: + """Serialize an Agent App detail using roster-only DTOs. + + `/agent` responses are roster-shaped rather than raw app-shaped: `id` + becomes the backing roster Agent id, `app_id` carries the underlying App + id, and `role` is injected from the backing roster Agent. Keeping that + remap in this serializer lets generated console/agent contracts expose the + roster persona fields without widening the shared /apps detail schema. + """ + app_model = AppService().get_app(app_model) if FeatureService.get_system_features().webapp_auth.enabled: app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id)) app_model.access_mode = app_setting.access_mode # type: ignore[attr-defined] roster_service = _agent_roster_service() - agent = roster_service.get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=app_model.id) + payload = AgentAppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json") + agent = roster_service.get_app_backing_agent(tenant_id=app_model.tenant_id, app_id=str(app_model.id)) if not agent: raise AgentNotFoundError() - payload = AppDetailWithSite.model_validate(app_model, from_attributes=True).model_dump(mode="json") payload.pop("bound_agent_id", None) payload["app_id"] = str(app_model.id) payload["id"] = agent.id @@ -206,6 +233,14 @@ def _serialize_agent_app_detail(app_model) -> dict: def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict: + """Serialize Agent App lists with roster-shaped items. + + Each item starts from the shared App list shape, then drops + `bound_agent_id`, rewrites `id` to the backing roster Agent id, stores the + original App id in `app_id`, and injects roster-only `role` when a backing + Agent is present. + """ + app_ids = [str(app.id) for app in app_pagination.items] roster_service = _agent_roster_service() agents_by_app_id = roster_service.load_app_backing_agents_by_app_id( @@ -220,7 +255,7 @@ def _serialize_agent_app_pagination(app_pagination, *, tenant_id: str) -> dict: tenant_id=tenant_id, agent_ids=[agent.id for agent in agents_by_app_id.values()], ) - payload = AppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json") + payload = AgentAppPagination.model_validate(app_pagination, from_attributes=True).model_dump(mode="json") for item in payload["data"]: app_id = item["id"] item.pop("bound_agent_id", None) @@ -294,7 +329,7 @@ class AgentAppListApi(Resource): return _serialize_agent_app_pagination(app_pagination, tenant_id=current_tenant_id) @console_ns.expect(console_ns.models[AgentAppCreatePayload.__name__]) - @console_ns.response(201, "Agent app created successfully", console_ns.models[AppDetailWithSite.__name__]) + @console_ns.response(201, "Agent app created successfully", console_ns.models[AgentAppDetailWithSite.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(400, "Invalid request parameters") @setup_required @@ -322,7 +357,7 @@ class AgentAppListApi(Resource): @console_ns.route("/agent/") class AgentAppApi(Resource): - @console_ns.response(200, "Agent app detail", console_ns.models[AppDetailWithSite.__name__]) + @console_ns.response(200, "Agent app detail", console_ns.models[AgentAppDetailWithSite.__name__]) @setup_required @login_required @account_initialization_required @@ -333,7 +368,7 @@ class AgentAppApi(Resource): return _serialize_agent_app_detail(app_model) @console_ns.expect(console_ns.models[AgentAppUpdatePayload.__name__]) - @console_ns.response(200, "Agent app updated successfully", console_ns.models[AppDetailWithSite.__name__]) + @console_ns.response(200, "Agent app updated successfully", console_ns.models[AgentAppDetailWithSite.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(400, "Invalid request parameters") @setup_required @@ -373,7 +408,7 @@ class AgentAppApi(Resource): @console_ns.route("/agent//copy") class AgentAppCopyApi(Resource): @console_ns.expect(console_ns.models[CopyAppPayload.__name__]) - @console_ns.response(201, "Agent app copied successfully", console_ns.models[AppDetailWithSite.__name__]) + @console_ns.response(201, "Agent app copied successfully", console_ns.models[AgentAppDetailWithSite.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(400, "Invalid request parameters") @setup_required diff --git a/api/controllers/console/app/agent.py b/api/controllers/console/app/agent.py index 23ccd28ad6f..6731be67831 100644 --- a/api/controllers/console/app/agent.py +++ b/api/controllers/console/app/agent.py @@ -30,7 +30,7 @@ from models import Account from models.agent_config_entities import AgentFileRefConfig, AgentSkillRefConfig from models.model import App, AppMode, UploadFile from services.agent.composer_service import AgentComposerService -from services.agent.skill_package_service import SkillManifest, SkillPackageError, SkillPackageService +from services.agent.skill_package_service import SkillManifest, SkillPackageError from services.agent.skill_standardize_service import SkillStandardizeService from services.agent.skill_tool_inference_service import ( SkillToolInferenceError, @@ -45,11 +45,18 @@ from services.agent_drive_service import ( normalize_drive_key, ) from services.agent_service import AgentService -from services.file_service import FileService logger = logging.getLogger(__name__) _WORKFLOW_AGENT_DRIVE_APP_MODES = [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT] +_AGENT_SKILL_UPLOAD_PARAMS = { + "file": { + "in": "formData", + "type": "file", + "required": True, + "description": "Skill package (.zip or .skill).", + } +} class AgentLogQuery(BaseModel): @@ -125,11 +132,6 @@ class AgentSkillUploadResponse(ResponseModel): manifest: SkillManifest -class AgentSkillStandardizeResponse(ResponseModel): - skill: AgentSkillRefConfig - manifest: SkillManifest - - class AgentDriveFileResponse(ResponseModel): name: str drive_key: str @@ -156,7 +158,6 @@ register_response_schema_models( AgentDriveFileCommitResponse, AgentDriveFileResponse, AgentLogResponse, - AgentSkillStandardizeResponse, AgentSkillUploadResponse, SkillToolInferenceResult, ) @@ -174,30 +175,9 @@ def _agent_not_bound() -> tuple[dict[str, str], int]: return {"code": "agent_not_bound", "message": "no agent is bound for this app/node"}, 400 -def _upload_skill_for_app(*, current_user: Account): - if "file" not in request.files: - return {"code": "no_file", "message": "no skill file uploaded"}, 400 - if len(request.files) > 1: - return {"code": "too_many_files", "message": "only one skill file is allowed"}, 400 +def _upload_skill_for_app(*, current_user: Account, app_model: App): + """Upload one skill package and commit its normalized files into the agent drive.""" - upload = request.files["file"] - content = upload.stream.read() - try: - manifest = SkillPackageService().validate_and_extract(content=content, filename=upload.filename or "") - except SkillPackageError as exc: - return {"code": exc.code, "message": exc.message}, exc.status_code - - upload_file = FileService(db.engine).upload_file( - filename=upload.filename or "skill.zip", - content=content, - mimetype=upload.mimetype or "application/zip", - user=current_user, - ) - skill_ref = manifest.to_skill_ref(file_id=upload_file.id) - return {"skill": skill_ref.model_dump(exclude_none=True), "manifest": manifest.model_dump()}, 201 - - -def _standardize_skill_for_app(*, current_user: Account, app_model: App): query = query_params_from_request(AgentDriveMutationQuery) agent_id = _resolve_agent_id(app_model, query.node_id) if not agent_id: @@ -382,51 +362,9 @@ class AgentLogApi(Resource): @console_ns.route("/agent//skills/upload") class AgentSkillUploadByAgentApi(Resource): @console_ns.doc("upload_agent_skill_by_agent") - @console_ns.doc(description="Upload + validate a Skill package for an Agent App") - @console_ns.doc(params={"agent_id": "Agent ID"}) - @console_ns.response(201, "Skill validated", console_ns.models[AgentSkillUploadResponse.__name__]) - @console_ns.response(400, "Invalid skill package") - @setup_required - @login_required - @account_initialization_required - @with_current_user - @with_current_tenant_id - def post(self, tenant_id: str, current_user: Account, agent_id: UUID): - resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) - return _upload_skill_for_app(current_user=current_user) - - -@console_ns.route("/apps//agent/skills/upload") -class AgentSkillUploadApi(Resource): - @console_ns.doc("upload_agent_skill") - @console_ns.doc(description="Upload + validate a Skill package (.zip/.skill) and extract its manifest") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.response(201, "Skill validated", console_ns.models[AgentSkillUploadResponse.__name__]) - @console_ns.response(400, "Invalid skill package") - @setup_required - @login_required - @account_initialization_required - @get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES) - @with_current_user - def post(self, current_user: Account, app_model: App): - """Validate an uploaded Skill package and persist the archive. - - Returns a validated skill ref (to bind into the Agent soul config on save) - plus its manifest. Standardizing into the agent drive is ENG-594. - """ - return _upload_skill_for_app(current_user=current_user) - - -@console_ns.route("/agent//skills/standardize") -class AgentSkillStandardizeByAgentApi(Resource): - @console_ns.doc("standardize_agent_skill_by_agent") - @console_ns.doc(description="Validate + standardize a Skill into an Agent App drive") - @console_ns.doc(params={"agent_id": "Agent ID"}) - @console_ns.response( - 201, - "Skill standardized into drive", - console_ns.models[AgentSkillStandardizeResponse.__name__], - ) + @console_ns.doc(description="Upload + standardize a Skill into an Agent App drive") + @console_ns.doc(consumes=["multipart/form-data"], params={"agent_id": "Agent ID", **_AGENT_SKILL_UPLOAD_PARAMS}) + @console_ns.response(201, "Skill uploaded into drive", console_ns.models[AgentSkillUploadResponse.__name__]) @console_ns.response(400, "Invalid skill package or no bound agent") @setup_required @login_required @@ -435,19 +373,22 @@ class AgentSkillStandardizeByAgentApi(Resource): @with_current_tenant_id def post(self, tenant_id: str, current_user: Account, agent_id: UUID): app_model = resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id) - return _standardize_skill_for_app(current_user=current_user, app_model=app_model) + return _upload_skill_for_app(current_user=current_user, app_model=app_model) -@console_ns.route("/apps//agent/skills/standardize") -class AgentSkillStandardizeApi(Resource): - @console_ns.doc("standardize_agent_skill") - @console_ns.doc(description="Validate + standardize a Skill into the agent drive (ENG-594)") - @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(AgentDriveMutationQuery)}) - @console_ns.response( - 201, - "Skill standardized into drive", - console_ns.models[AgentSkillStandardizeResponse.__name__], +@console_ns.route("/apps//agent/skills/upload") +class AgentSkillUploadApi(Resource): + @console_ns.doc("upload_agent_skill") + @console_ns.doc(description="Upload + standardize a Skill into the agent drive") + @console_ns.doc( + consumes=["multipart/form-data"], + params={ + "app_id": "Application ID", + **query_params_from_model(AgentDriveMutationQuery), + **_AGENT_SKILL_UPLOAD_PARAMS, + }, ) + @console_ns.response(201, "Skill uploaded into drive", console_ns.models[AgentSkillUploadResponse.__name__]) @console_ns.response(400, "Invalid skill package or no bound agent") @setup_required @login_required @@ -455,8 +396,8 @@ class AgentSkillStandardizeApi(Resource): @get_app_model(mode=_WORKFLOW_AGENT_DRIVE_APP_MODES) @with_current_user def post(self, current_user: Account, app_model: App): - """Upload a Skill, validate it, and standardize it into the app agent's drive.""" - return _standardize_skill_for_app(current_user=current_user, app_model=app_model) + """Upload a Skill, validate it, and commit drive-backed skill files.""" + return _upload_skill_for_app(current_user=current_user, app_model=app_model) @console_ns.route("/agent//files") diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 3a8d0c1eb56..0e897ac44de 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -402,8 +402,6 @@ class AppPartial(ResponseModel): bound_agent_id: str | None = None # For Agent App responses exposed through /agent. app_id: str | None = None - role: str | None = None - active_config_is_published: bool = False is_starred: bool = False @computed_field(return_type=str | None) # type: ignore @@ -457,8 +455,6 @@ class AppDetailWithSite(AppDetail): bound_agent_id: str | None = None # For Agent App responses exposed through /agent. app_id: str | None = None - role: str | None = None - active_config_is_published: bool = False @computed_field(return_type=str | None) # type: ignore @property @@ -541,10 +537,7 @@ register_schema_models( ModelConfig, Site, DeletedTool, - AppPartial, AppDetail, - AppDetailWithSite, - AppPagination, AppExportResponse, Segmentation, PreProcessingRule, @@ -564,6 +557,13 @@ register_schema_models( LoadBalancingPayload, ) +register_response_schema_models( + console_ns, + AppPartial, + AppDetailWithSite, + AppPagination, +) + @console_ns.route("/apps") class AppListApi(Resource): diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index bcbe23d4c3e..ccc4bc12487 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -323,7 +323,7 @@ Check if activation token is valid | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Agent app created successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| +| 201 | Agent app created successfully | **application/json**: [AgentAppDetailWithSite](#agentappdetailwithsite)
| | 400 | Invalid request parameters | | | 403 | Insufficient permissions | | @@ -368,7 +368,7 @@ Check if activation token is valid | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent app detail | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| +| 200 | Agent app detail | **application/json**: [AgentAppDetailWithSite](#agentappdetailwithsite)
| ### [PUT] /agent/{agent_id} #### Parameters @@ -387,7 +387,7 @@ Check if activation token is valid | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Agent app updated successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| +| 200 | Agent app updated successfully | **application/json**: [AgentAppDetailWithSite](#agentappdetailwithsite)
| | 400 | Invalid request parameters | | | 403 | Insufficient permissions | | @@ -524,7 +524,7 @@ Stop a running Agent App chat message generation | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Agent app copied successfully | **application/json**: [AppDetailWithSite](#appdetailwithsite)
| +| 201 | Agent app copied successfully | **application/json**: [AgentAppDetailWithSite](#agentappdetailwithsite)
| | 400 | Invalid request parameters | | | 403 | Insufficient permissions | | @@ -800,24 +800,8 @@ Upload one Agent App sandbox file as a Dify ToolFile mapping | ---- | ----------- | ------ | | 200 | Uploaded | **application/json**: [SandboxUploadResponse](#sandboxuploadresponse)
| -### [POST] /agent/{agent_id}/skills/standardize -Validate + standardize a Skill into an Agent App drive - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| agent_id | path | Agent ID | Yes | string (uuid) | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | Skill standardized into drive | **application/json**: [AgentSkillStandardizeResponse](#agentskillstandardizeresponse)
| -| 400 | Invalid skill package or no bound agent | | - ### [POST] /agent/{agent_id}/skills/upload -Upload + validate a Skill package for an Agent App +Upload + standardize a Skill into an Agent App drive #### Parameters @@ -825,12 +809,18 @@ Upload + validate a Skill package for an Agent App | ---- | ---------- | ----------- | -------- | ------ | | agent_id | path | Agent ID | Yes | string (uuid) | +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **multipart/form-data**: { **"file"**: binary }
| + #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Skill validated | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)
| -| 400 | Invalid skill package | | +| 201 | Skill uploaded into drive | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)
| +| 400 | Invalid skill package or no bound agent | | ### [DELETE] /agent/{agent_id}/skills/{slug} Delete a standardized skill from an Agent App drive @@ -1517,10 +1507,10 @@ Get agent execution logs for an application | 200 | Agent logs retrieved successfully | **application/json**: [AgentLogResponse](#agentlogresponse)
| | 400 | Invalid request parameters | | -### [POST] /apps/{app_id}/agent/skills/standardize -**Upload a Skill, validate it, and standardize it into the app agent's drive** +### [POST] /apps/{app_id}/agent/skills/upload +**Upload a Skill, validate it, and commit drive-backed skill files** -Validate + standardize a Skill into the agent drive (ENG-594) +Upload + standardize a Skill into the agent drive #### Parameters @@ -1529,33 +1519,19 @@ Validate + standardize a Skill into the agent drive (ENG-594) | app_id | path | Application ID | Yes | string (uuid) | | node_id | query | Workflow node ID (workflow composer variant) | No | string | +#### Request Body + +| Required | Schema | +| -------- | ------ | +| Yes | **multipart/form-data**: { **"file"**: binary }
| + #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 201 | Skill standardized into drive | **application/json**: [AgentSkillStandardizeResponse](#agentskillstandardizeresponse)
| +| 201 | Skill uploaded into drive | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)
| | 400 | Invalid skill package or no bound agent | | -### [POST] /apps/{app_id}/agent/skills/upload -**Validate an uploaded Skill package and persist the archive** - -Upload + validate a Skill package (.zip/.skill) and extract its manifest -Returns a validated skill ref (to bind into the Agent soul config on save) -plus its manifest. Standardizing into the agent drive is ENG-594. - -#### Parameters - -| Name | Located in | Description | Required | Schema | -| ---- | ---------- | ----------- | -------- | ------ | -| app_id | path | Application ID | Yes | string (uuid) | - -#### Responses - -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 201 | Skill validated | **application/json**: [AgentSkillUploadResponse](#agentskilluploadresponse)
| -| 400 | Invalid skill package | | - ### [DELETE] /apps/{app_id}/agent/skills/{slug} Delete a standardized skill: soul ref first, then the / drive prefix (ENG-625 D5) @@ -11378,6 +11354,39 @@ Default namespace | name | string | Agent name | Yes | | role | string | Agent role | Yes | +#### AgentAppDetailWithSite + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| access_mode | string | | No | +| active_config_is_published | boolean | | No | +| api_base_url | string | | No | +| app_id | string | | No | +| bound_agent_id | string | | No | +| created_at | integer | | No | +| created_by | string | | No | +| deleted_tools | [ [DeletedTool](#deletedtool) ] | | No | +| description | string | | No | +| enable_api | boolean | | Yes | +| enable_site | boolean | | Yes | +| icon | string | | No | +| icon_background | string | | No | +| icon_type | string | | No | +| icon_url | string | | Yes | +| id | string | | Yes | +| max_active_requests | integer | | No | +| mode | string | | Yes | +| model_config | [ModelConfig](#modelconfig) | | No | +| name | string | | Yes | +| role | string | | No | +| site | [Site](#site) | | No | +| tags | [ [Tag](#tag) ] | | No | +| tracing | [JSONValue](#jsonvalue) | | No | +| updated_at | integer | | No | +| updated_by | string | | No | +| use_icon_as_answer_icon | boolean | | No | +| workflow | [WorkflowPartial](#workflowpartial) | | No | + #### AgentAppFeaturesPayload Presentation features configurable on an Agent App. @@ -12296,13 +12305,6 @@ Visibility and lifecycle scope of an Agent record. | skill_md_file_id | string | | No | | skill_md_key | string | | No | -#### AgentSkillStandardizeResponse - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| manifest | [SkillManifest](#skillmanifest) | | Yes | -| skill | [AgentSkillRefConfig](#agentskillrefconfig) | | Yes | - #### AgentSkillUploadResponse | Name | Type | Description | Required | @@ -12864,7 +12866,6 @@ Enum class for api provider schema type. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | access_mode | string | | No | -| active_config_is_published | boolean | | No | | api_base_url | string | | No | | app_id | string | | No | | bound_agent_id | string | | No | @@ -12883,7 +12884,6 @@ Enum class for api provider schema type. | mode | string | | Yes | | model_config | [ModelConfig](#modelconfig) | | No | | name | string | | Yes | -| role | string | | No | | site | [Site](#site) | | No | | tags | [ [Tag](#tag) ] | | No | | tracing | [JSONValue](#jsonvalue) | | No | @@ -12977,10 +12977,10 @@ AppMCPServer Status Enum | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| has_next | boolean | | Yes | -| items | [ [AppPartial](#apppartial) ] | | Yes | +| data | [ [AppPartial](#apppartial) ] | | Yes | +| has_more | boolean | | Yes | +| limit | integer | | Yes | | page | integer | | Yes | -| per_page | integer | | Yes | | total | integer | | Yes | #### AppPartial @@ -12988,25 +12988,24 @@ AppMCPServer Status Enum | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | access_mode | string | | No | -| active_config_is_published | boolean | | No | | app_id | string | | No | -| app_model_config | [ModelConfigPartial](#modelconfigpartial) | | No | | author_name | string | | No | | bound_agent_id | string | | No | | create_user_name | string | | No | | created_at | integer | | No | | created_by | string | | No | -| desc_or_prompt | string | | No | +| description | string | | No | | has_draft_trigger | boolean | | No | | icon | string | | No | | icon_background | string | | No | | icon_type | string | | No | +| icon_url | string | | Yes | | id | string | | Yes | | is_starred | boolean | | No | | max_active_requests | integer | | No | -| mode_compatible_with_agent | string | | Yes | +| mode | string | | Yes | +| model_config | [ModelConfigPartial](#modelconfigpartial) | | No | | name | string | | Yes | -| role | string | | No | | tags | [ [Tag](#tag) ] | | No | | updated_at | integer | | No | | updated_by | string | | No | diff --git a/api/services/agent/skill_standardize_service.py b/api/services/agent/skill_standardize_service.py index 71bb8daded9..b83004f3c4b 100644 --- a/api/services/agent/skill_standardize_service.py +++ b/api/services/agent/skill_standardize_service.py @@ -10,7 +10,9 @@ to the agent drive (Agent Files §5.4 / §4): Both are stored as ``ToolFile`` records and bound via ``AgentDriveService.commit`` with ``value_owned_by_drive=True`` (the drive owns their lifecycle). The returned skill ref records the stable drive paths + file ids (not just the raw upload id), -so the Composer can reload the bound skill list. +so the Composer can reload the bound skill list. The console ``/skills/upload`` +endpoints delegate to this service so "upload" now always means drive-backed skill +normalization. """ from __future__ import annotations @@ -34,7 +36,7 @@ def slugify_skill_name(name: str) -> str: class SkillStandardizeService: - """Validate + standardize a Skill package into a per-agent drive.""" + """Validate + standardize a Skill package into a per-agent drive upload result.""" def __init__( self, diff --git a/api/services/app_service.py b/api/services/app_service.py index 18f4e70df4b..cd0d08bf3e7 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -542,17 +542,21 @@ class AppService: *, name: str | None = None, description: str | None = None, + role: str | None = None, icon_type: IconType | str | None = None, icon: str | None = None, icon_background: str | None = None, - role: str | None = None, account_id: str | None = None, updated_at: datetime | None = None, ) -> None: """Keep the Roster identity aligned with its Agent App shell. Agent Soul remains versioned through Composer. This helper only mirrors - user-facing identity fields so Roster and Agent Console do not drift. + user-facing identity fields, including the roster role/persona label, + so Roster and Agent Console do not drift. + + Role omission is intentional: ``role=None`` preserves the backing + Agent's current role, while ``role=""`` explicitly clears it. """ agent = self._get_backing_agent_for_update(app) if agent is None: @@ -562,14 +566,14 @@ class AppService: agent.name = name if description is not None: agent.description = description + if role is not None: + agent.role = role if icon_type is not None: agent.icon_type = self._to_agent_icon_type(icon_type) if icon is not None: agent.icon = icon if icon_background is not None: agent.icon_background = icon_background - if role is not None: - agent.role = role agent.updated_by = account_id if updated_at is not None: agent.updated_at = updated_at @@ -601,10 +605,12 @@ class AppService: app, name=app.name, description=app.description, + # Omitted role must stay omitted here: None means "preserve current + # backing-agent role", while an empty string is an explicit clear. + role=args.get("role"), icon_type=app.icon_type, icon=app.icon, icon_background=app.icon_background, - role=args.get("role"), account_id=current_user.id, updated_at=app.updated_at, ) diff --git a/api/services/entities/agent_entities.py b/api/services/entities/agent_entities.py index 63ae101533d..e7b5cbd7c6d 100644 --- a/api/services/entities/agent_entities.py +++ b/api/services/entities/agent_entities.py @@ -60,6 +60,7 @@ class ComposerSavePayload(BaseModel): class RosterAgentCreatePayload(BaseModel): name: str = Field(min_length=1, max_length=255) + mode: Literal["agent"] = "agent" description: str = "" role: str = Field(default="", max_length=255) icon_type: AgentIconType | None = None diff --git a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py index dfd51f462ea..320d841ca8c 100644 --- a/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py +++ b/api/tests/unit_tests/controllers/console/agent/test_agent_controllers.py @@ -95,6 +95,7 @@ def _agent_app_composer_response() -> dict: def _app_detail_obj(**overrides): data = { "id": "app-1", + "tenant_id": "tenant-1", "name": "Iris", "description": "Agent app", "mode_compatible_with_agent": "agent", @@ -118,7 +119,6 @@ def _app_detail_obj(**overrides): "deleted_tools": [], "site": None, "bound_agent_id": "00000000-0000-0000-0000-000000000001", - "tenant_id": "tenant-1", } data.update(overrides) return SimpleNamespace(**data) diff --git a/api/tests/unit_tests/controllers/console/app/test_agent_skills.py b/api/tests/unit_tests/controllers/console/app/test_agent_skills.py index bcb4aeab462..e94536aa012 100644 --- a/api/tests/unit_tests/controllers/console/app/test_agent_skills.py +++ b/api/tests/unit_tests/controllers/console/app/test_agent_skills.py @@ -10,7 +10,7 @@ from __future__ import annotations import inspect import io from types import SimpleNamespace -from unittest.mock import MagicMock, patch +from unittest.mock import patch from flask import Flask @@ -18,8 +18,6 @@ from controllers.console.app.agent import ( AgentDriveFilesByAgentApi, AgentSkillByAgentApi, AgentSkillInferToolsByAgentApi, - AgentSkillStandardizeApi, - AgentSkillStandardizeByAgentApi, AgentSkillUploadApi, AgentSkillUploadByAgentApi, ) @@ -45,47 +43,39 @@ _APP = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT, bou _WORKFLOW_APP = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW, bound_agent_id=None) -def test_upload_validates_and_returns_skill_ref(): +def test_upload_standardizes_into_drive_and_returns_skill_ref(): raw = _raw(AgentSkillUploadApi.post) - manifest = MagicMock() - manifest.to_skill_ref.return_value.model_dump.return_value = {"name": "S", "file_id": "uf-1"} - manifest.model_dump.return_value = {"name": "S"} with _file_ctx(files={"file": b"zip-bytes"}): with ( - patch(f"{_MOD}.SkillPackageService") as pkg, - patch(f"{_MOD}.FileService") as fs, - patch(f"{_MOD}.db"), + patch(f"{_MOD}.SkillStandardizeService") as svc, ): - pkg.return_value.validate_and_extract.return_value = manifest - fs.return_value.upload_file.return_value = SimpleNamespace(id="uf-1") + svc.return_value.standardize.return_value = { + "skill": {"path": "skill-a", "skill_md_key": "skill-a/SKILL.md"}, + "manifest": {"name": "Skill A"}, + } body, status = raw(AgentSkillUploadApi(), _USER, _APP) assert status == 201 - assert body["skill"] == {"name": "S", "file_id": "uf-1"} - manifest.to_skill_ref.assert_called_once_with(file_id="uf-1") + assert body["skill"] == {"path": "skill-a", "skill_md_key": "skill-a/SKILL.md"} + assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1" -def test_upload_by_agent_resolves_app_and_returns_skill_ref(): +def test_upload_by_agent_resolves_app_and_standardizes_into_drive(): raw = _raw(AgentSkillUploadByAgentApi.post) - manifest = MagicMock() - manifest.to_skill_ref.return_value.model_dump.return_value = {"name": "S", "file_id": "uf-1"} - manifest.model_dump.return_value = {"name": "S"} with _file_ctx(files={"file": b"zip-bytes"}): with ( patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, - patch(f"{_MOD}.SkillPackageService") as pkg, - patch(f"{_MOD}.FileService") as fs, - patch(f"{_MOD}.db"), + patch(f"{_MOD}.SkillStandardizeService") as svc, ): - pkg.return_value.validate_and_extract.return_value = manifest - fs.return_value.upload_file.return_value = SimpleNamespace(id="uf-1") + svc.return_value.standardize.return_value = {"skill": {"path": "skill-a"}, "manifest": {}} body, status = raw(AgentSkillUploadByAgentApi(), "tenant-1", _USER, "agent-1") assert status == 201 - assert body["skill"] == {"name": "S", "file_id": "uf-1"} + assert body["skill"] == {"path": "skill-a"} resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1") + assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1" def test_upload_no_file_is_400(): @@ -99,8 +89,8 @@ def test_upload_no_file_is_400(): def test_upload_maps_package_error(): raw = _raw(AgentSkillUploadApi.post) with _file_ctx(files={"file": b"bad"}): - with patch(f"{_MOD}.SkillPackageService") as pkg: - pkg.return_value.validate_and_extract.side_effect = SkillPackageError( + with patch(f"{_MOD}.SkillStandardizeService") as svc: + svc.return_value.standardize.side_effect = SkillPackageError( "missing_skill_md", "no SKILL.md", status_code=400 ) body, status = raw(AgentSkillUploadApi(), _USER, _APP) @@ -108,44 +98,17 @@ def test_upload_maps_package_error(): assert body["code"] == "missing_skill_md" -def test_standardize_returns_result(): - raw = _raw(AgentSkillStandardizeApi.post) - with _file_ctx(files={"file": b"zip"}): - with patch(f"{_MOD}.SkillStandardizeService") as svc: - svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}} - body, status = raw(AgentSkillStandardizeApi(), _USER, _APP) - assert status == 201 - assert body["skill"] == {"path": "s"} - assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1" - - -def test_standardize_by_agent_resolves_app(): - raw = _raw(AgentSkillStandardizeByAgentApi.post) - with _file_ctx(files={"file": b"zip"}): - with ( - patch(f"{_MOD}.resolve_agent_app_model", return_value=_APP) as resolve_app, - patch(f"{_MOD}.SkillStandardizeService") as svc, - ): - svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}} - body, status = raw(AgentSkillStandardizeByAgentApi(), "tenant-1", _USER, "agent-1") - - assert status == 201 - assert body["skill"] == {"path": "s"} - resolve_app.assert_called_once_with(tenant_id="tenant-1", agent_id="agent-1") - assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "agent-1" - - -def test_standardize_no_bound_agent_is_400(): - raw = _raw(AgentSkillStandardizeApi.post) +def test_upload_no_bound_agent_is_400(): + raw = _raw(AgentSkillUploadApi.post) app_without_agent = SimpleNamespace(id="app-1", tenant_id="tenant-1", mode=AppMode.AGENT, bound_agent_id=None) with _file_ctx(files={"file": b"zip"}): - body, status = raw(AgentSkillStandardizeApi(), _USER, app_without_agent) + body, status = raw(AgentSkillUploadApi(), _USER, app_without_agent) assert status == 400 assert body["code"] == "agent_not_bound" -def test_standardize_resolves_workflow_node_agent(): - raw = _raw(AgentSkillStandardizeApi.post) +def test_upload_resolves_workflow_node_agent(): + raw = _raw(AgentSkillUploadApi.post) with app.test_request_context( "/?node_id=agent-node-1", method="POST", data={"file": (io.BytesIO(b"zip"), "skill.zip")} ): @@ -155,19 +118,19 @@ def test_standardize_resolves_workflow_node_agent(): ): composer.resolve_workflow_node_agent_id.return_value = "wf-agent-1" svc.return_value.standardize.return_value = {"skill": {"path": "s"}, "manifest": {}} - body, status = raw(AgentSkillStandardizeApi(), _USER, _WORKFLOW_APP) + body, status = raw(AgentSkillUploadApi(), _USER, _WORKFLOW_APP) assert status == 201 assert body["skill"] == {"path": "s"} assert svc.return_value.standardize.call_args.kwargs["agent_id"] == "wf-agent-1" -def test_standardize_maps_drive_error(): - raw = _raw(AgentSkillStandardizeApi.post) +def test_upload_maps_drive_error(): + raw = _raw(AgentSkillUploadApi.post) with _file_ctx(files={"file": b"zip"}): with patch(f"{_MOD}.SkillStandardizeService") as svc: svc.return_value.standardize.side_effect = AgentDriveError("source_not_found", "nope", status_code=404) - body, status = raw(AgentSkillStandardizeApi(), _USER, _APP) + body, status = raw(AgentSkillUploadApi(), _USER, _APP) assert status == 404 assert body["code"] == "source_not_found" diff --git a/api/tests/unit_tests/controllers/console/app/test_app_response_models.py b/api/tests/unit_tests/controllers/console/app/test_app_response_models.py index ef8f90e5c9c..9cff0c8d3ee 100644 --- a/api/tests/unit_tests/controllers/console/app/test_app_response_models.py +++ b/api/tests/unit_tests/controllers/console/app/test_app_response_models.py @@ -351,6 +351,7 @@ def test_app_partial_serialization_uses_aliases(app_models): create_user_name="Creator", author_name="Author", has_draft_trigger=True, + role="Should stay agent-only", ) serialized = AppPartial.model_validate(app_obj, from_attributes=True).model_dump(mode="json") @@ -363,6 +364,7 @@ def test_app_partial_serialization_uses_aliases(app_models): assert serialized["model_config"]["model"] == {"provider": "openai", "name": "gpt-4o"} assert serialized["workflow"]["id"] == "wf-1" assert serialized["tags"][0]["name"] == "Utilities" + assert "role" not in serialized def test_app_detail_with_site_includes_nested_serialization(app_models): @@ -405,6 +407,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models): deleted_tools=[{"type": "api", "tool_name": "search", "provider_id": "prov"}], site=site, bound_agent_id="agent-1", + role="Should stay agent-only", ) serialized = AppDetailWithSite.model_validate(app_obj, from_attributes=True).model_dump(mode="json") @@ -415,6 +418,7 @@ def test_app_detail_with_site_includes_nested_serialization(app_models): assert serialized["site"]["icon_url"] == "signed:site-icon" assert serialized["site"]["created_at"] == int(timestamp.timestamp()) assert serialized["bound_agent_id"] == "agent-1" + assert "role" not in serialized def test_app_pagination_aliases_per_page_and_has_next(app_models): diff --git a/api/tests/unit_tests/services/agent/test_agent_services.py b/api/tests/unit_tests/services/agent/test_agent_services.py index 41d3d434ba4..b0a099c9bd8 100644 --- a/api/tests/unit_tests/services/agent/test_agent_services.py +++ b/api/tests/unit_tests/services/agent/test_agent_services.py @@ -1247,6 +1247,7 @@ class TestAgentAppBackingAgent: app_id="app-1", name="Iris", description="clarifier", + role="research assistant", ) # Agent is bound to the app and is a roster/agent_app entry. @@ -1256,6 +1257,7 @@ class TestAgentAppBackingAgent: assert agent.status == AgentStatus.ACTIVE assert agent.agent_kind == AgentKind.DIFY_AGENT assert agent.name == "Iris" + assert agent.role == "research assistant" # A v1 snapshot + revision are seeded and wired as the active version. snapshots = [a for a in session.added if isinstance(a, AgentConfigSnapshot)] assert len(snapshots) == 1 diff --git a/api/tests/unit_tests/services/test_app_service.py b/api/tests/unit_tests/services/test_app_service.py index bb764112640..e595721e169 100644 --- a/api/tests/unit_tests/services/test_app_service.py +++ b/api/tests/unit_tests/services/test_app_service.py @@ -168,6 +168,7 @@ class TestAgentAppType: mode=AppMode.AGENT, name="Old", description="old", + role="draft", icon_type=IconType.EMOJI, icon="robot", icon_background="#fff", @@ -178,6 +179,7 @@ class TestAgentAppType: backing_agent = SimpleNamespace( name="Old", description="old", + role="draft", icon_type=AgentIconType.EMOJI, icon="robot", icon_background="#fff", @@ -195,6 +197,7 @@ class TestAgentAppType: { "name": "Iris", "description": "agent app", + "role": "research assistant", "icon_type": "image", "icon": "file-id", "icon_background": "#123456", @@ -206,12 +209,114 @@ class TestAgentAppType: assert updated_app.name == "Iris" assert backing_agent.name == "Iris" assert backing_agent.description == "agent app" + assert backing_agent.role == "research assistant" assert backing_agent.icon_type == AgentIconType.IMAGE assert backing_agent.icon == "file-id" assert backing_agent.icon_background == "#123456" assert backing_agent.updated_by == "account-2" assert backing_agent.updated_at == updated_app.updated_at + def test_update_agent_app_preserves_role_when_args_omit_it(self): + from models.agent import AgentIconType + from models.model import AppMode, IconType + from services.app_service import AppService + + app = SimpleNamespace( + id="app-1", + tenant_id="tenant-1", + mode=AppMode.AGENT, + name="Old", + description="old", + role="draft", + icon_type=IconType.EMOJI, + icon="robot", + icon_background="#fff", + use_icon_as_answer_icon=False, + max_active_requests=None, + created_by="account-1", + ) + backing_agent = SimpleNamespace( + name="Old", + description="old", + role="research assistant", + icon_type=AgentIconType.EMOJI, + icon="robot", + icon_background="#fff", + updated_by=None, + updated_at=None, + ) + + with ( + patch("services.app_service.db") as mock_db, + patch("services.app_service.current_user", SimpleNamespace(id="account-2")), + ): + mock_db.session.scalar.return_value = backing_agent + AppService().update_app( + app, # type: ignore[arg-type] + { + "name": "Iris", + "description": "agent app", + "icon_type": "image", + "icon": "file-id", + "icon_background": "#123456", + "use_icon_as_answer_icon": False, + "max_active_requests": 0, + }, + ) + + assert backing_agent.role == "research assistant" + + def test_update_agent_app_clears_role_when_args_set_empty_string(self): + from models.agent import AgentIconType + from models.model import AppMode, IconType + from services.app_service import AppService + + app = SimpleNamespace( + id="app-1", + tenant_id="tenant-1", + mode=AppMode.AGENT, + name="Old", + description="old", + role="draft", + icon_type=IconType.EMOJI, + icon="robot", + icon_background="#fff", + use_icon_as_answer_icon=False, + max_active_requests=None, + created_by="account-1", + ) + backing_agent = SimpleNamespace( + name="Old", + description="old", + role="research assistant", + icon_type=AgentIconType.EMOJI, + icon="robot", + icon_background="#fff", + updated_by=None, + updated_at=None, + ) + + with ( + patch("services.app_service.db") as mock_db, + patch("services.app_service.current_user", SimpleNamespace(id="account-2")), + ): + mock_db.session.scalar.return_value = backing_agent + AppService().update_app( + app, # type: ignore[arg-type] + { + "name": "Iris", + "description": "agent app", + "role": "", + "icon_type": "image", + "icon": "file-id", + "icon_background": "#123456", + "use_icon_as_answer_icon": False, + "max_active_requests": 0, + }, + ) + + assert backing_agent.role == "" + def test_delete_agent_app_archives_backing_agent(self): from models.agent import AgentStatus from models.model import AppMode diff --git a/docker/.env.example b/docker/.env.example index 8daa82d05a1..76b0add67ef 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -150,6 +150,7 @@ NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false # Enable preview features still in development (currently the /create and # /refine slash commands in the "Go to Anything" command palette). NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=false +ENABLE_AGENT_V2=false EXPERIMENTAL_ENABLE_VINEXT=false # Storage and default vector store diff --git a/docker/envs/core-services/web.env.example b/docker/envs/core-services/web.env.example index 4c119106316..bd788a1b16c 100644 --- a/docker/envs/core-services/web.env.example +++ b/docker/envs/core-services/web.env.example @@ -25,6 +25,7 @@ ENABLE_WEBSITE_FIRECRAWL=true ENABLE_WEBSITE_WATERCRAWL=true NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=false +ENABLE_AGENT_V2=false NEXT_PUBLIC_COOKIE_DOMAIN= NEXT_PUBLIC_BATCH_CONCURRENCY=5 CSP_WHITELIST= diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 8a75a82a9da..0641521eb13 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1089,14 +1089,6 @@ "count": 1 } }, - "web/app/components/base/app-icon/index.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/base/audio-btn/audio.ts": { "node/prefer-global/buffer": { "count": 1 @@ -1529,17 +1521,6 @@ "count": 1 } }, - "web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 2 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/base/features/types.ts": { "erasable-syntax-only/enums": { "count": 2 @@ -2502,11 +2483,6 @@ "count": 2 } }, - "web/app/components/base/prompt-editor/utils.ts": { - "ts/no-explicit-any": { - "count": 4 - } - }, "web/app/components/base/prompt-log-modal/index.stories.tsx": { "no-console": { "count": 1 @@ -5012,14 +4988,6 @@ "count": 1 } }, - "web/app/components/workflow/block-selector/blocks.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/workflow/block-selector/featured-tools.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 1 @@ -5152,11 +5120,6 @@ "count": 1 } }, - "web/app/components/workflow/candidate-node-main.tsx": { - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/workflow/comment/comment-input.tsx": { "jsx-a11y/no-autofocus": { "count": 1 @@ -5300,11 +5263,6 @@ "count": 1 } }, - "web/app/components/workflow/hooks/use-nodes-interactions.ts": { - "ts/no-explicit-any": { - "count": 8 - } - }, "web/app/components/workflow/hooks/use-serial-async-callback.ts": { "ts/no-explicit-any": { "count": 1 @@ -5704,7 +5662,7 @@ "count": 2 }, "ts/no-explicit-any": { - "count": 6 + "count": 4 } }, "web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx": { @@ -5885,11 +5843,6 @@ "count": 5 } }, - "web/app/components/workflow/nodes/components.ts": { - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/workflow/nodes/data-source-empty/hooks.ts": { "ts/no-explicit-any": { "count": 1 @@ -7060,19 +7013,6 @@ "count": 1 } }, - "web/app/components/workflow/run/agent-log/agent-log-trigger.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, - "web/app/components/workflow/run/agent-log/index.tsx": { - "no-barrel-files/no-barrel-files": { - "count": 2 - } - }, "web/app/components/workflow/run/hooks.ts": { "ts/no-explicit-any": { "count": 1 @@ -7750,11 +7690,6 @@ "count": 1 } }, - "web/service/client.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "web/service/common.ts": { "no-restricted-imports": { "count": 1 diff --git a/packages/contracts/generated/api/console/agent/orpc.gen.ts b/packages/contracts/generated/api/console/agent/orpc.gen.ts index fcfd3d114fb..fbfca3be118 100644 --- a/packages/contracts/generated/api/console/agent/orpc.gen.ts +++ b/packages/contracts/generated/api/console/agent/orpc.gen.ts @@ -83,8 +83,7 @@ import { zPostAgentByAgentIdSandboxFilesUploadResponse, zPostAgentByAgentIdSkillsBySlugInferToolsPath, zPostAgentByAgentIdSkillsBySlugInferToolsResponse, - zPostAgentByAgentIdSkillsStandardizePath, - zPostAgentByAgentIdSkillsStandardizeResponse, + zPostAgentByAgentIdSkillsUploadBody, zPostAgentByAgentIdSkillsUploadPath, zPostAgentByAgentIdSkillsUploadResponse, zPostAgentResponse, @@ -599,31 +598,11 @@ export const sandbox = { } /** - * Validate + standardize a Skill into an Agent App drive + * Upload + standardize a Skill into an Agent App drive */ export const post8 = oc .route({ - description: 'Validate + standardize a Skill into an Agent App drive', - inputStructure: 'detailed', - method: 'POST', - operationId: 'postAgentByAgentIdSkillsStandardize', - path: '/agent/{agent_id}/skills/standardize', - successStatus: 201, - tags: ['console'], - }) - .input(z.object({ params: zPostAgentByAgentIdSkillsStandardizePath })) - .output(zPostAgentByAgentIdSkillsStandardizeResponse) - -export const standardize = { - post: post8, -} - -/** - * Upload + validate a Skill package for an Agent App - */ -export const post9 = oc - .route({ - description: 'Upload + validate a Skill package for an Agent App', + description: 'Upload + standardize a Skill into an Agent App drive', inputStructure: 'detailed', method: 'POST', operationId: 'postAgentByAgentIdSkillsUpload', @@ -631,17 +610,22 @@ export const post9 = oc successStatus: 201, tags: ['console'], }) - .input(z.object({ params: zPostAgentByAgentIdSkillsUploadPath })) + .input( + z.object({ + body: zPostAgentByAgentIdSkillsUploadBody, + params: zPostAgentByAgentIdSkillsUploadPath, + }), + ) .output(zPostAgentByAgentIdSkillsUploadResponse) export const upload2 = { - post: post9, + post: post8, } /** * Infer CLI tool + ENV suggestions from a standardized Agent App skill */ -export const post10 = oc +export const post9 = oc .route({ description: 'Infer CLI tool + ENV suggestions from a standardized Agent App skill', inputStructure: 'detailed', @@ -654,7 +638,7 @@ export const post10 = oc .output(zPostAgentByAgentIdSkillsBySlugInferToolsResponse) export const inferTools = { - post: post10, + post: post9, } /** @@ -678,7 +662,6 @@ export const bySlug = { } export const skills = { - standardize, upload: upload2, bySlug, } @@ -804,7 +787,7 @@ export const get20 = oc .input(z.object({ query: zGetAgentQuery.optional() })) .output(zGetAgentResponse) -export const post11 = oc +export const post10 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -818,7 +801,7 @@ export const post11 = oc export const agent = { get: get20, - post: post11, + post: post10, inviteOptions, byAgentId, } diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index 46cb0545405..19a818cf9a6 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -21,7 +21,7 @@ export type AgentAppCreatePayload = { role: string } -export type AppDetailWithSite = { +export type AgentAppDetailWithSite = { access_mode?: string | null active_config_is_published?: boolean api_base_url?: string | null @@ -255,11 +255,6 @@ export type SandboxUploadResponse = { path: string } -export type AgentSkillStandardizeResponse = { - manifest: SkillManifest - skill: AgentSkillRefConfig -} - export type AgentSkillUploadResponse = { manifest: SkillManifest skill: AgentSkillRefConfig @@ -1431,7 +1426,7 @@ export type AgentAppPaginationWritable = { total: number } -export type AppDetailWithSiteWritable = { +export type AgentAppDetailWithSiteWritable = { access_mode?: string | null active_config_is_published?: boolean api_base_url?: string | null @@ -1550,7 +1545,7 @@ export type PostAgentErrors = { } export type PostAgentResponses = { - 201: AppDetailWithSite + 201: AgentAppDetailWithSite } export type PostAgentResponse = PostAgentResponses[keyof PostAgentResponses] @@ -1604,7 +1599,7 @@ export type GetAgentByAgentIdData = { } export type GetAgentByAgentIdResponses = { - 200: AppDetailWithSite + 200: AgentAppDetailWithSite } export type GetAgentByAgentIdResponse = GetAgentByAgentIdResponses[keyof GetAgentByAgentIdResponses] @@ -1624,7 +1619,7 @@ export type PutAgentByAgentIdErrors = { } export type PutAgentByAgentIdResponses = { - 200: AppDetailWithSite + 200: AgentAppDetailWithSite } export type PutAgentByAgentIdResponse = PutAgentByAgentIdResponses[keyof PutAgentByAgentIdResponses] @@ -1770,7 +1765,7 @@ export type PostAgentByAgentIdCopyErrors = { } export type PostAgentByAgentIdCopyResponses = { - 201: AppDetailWithSite + 201: AgentAppDetailWithSite } export type PostAgentByAgentIdCopyResponse @@ -2065,28 +2060,10 @@ export type PostAgentByAgentIdSandboxFilesUploadResponses = { export type PostAgentByAgentIdSandboxFilesUploadResponse = PostAgentByAgentIdSandboxFilesUploadResponses[keyof PostAgentByAgentIdSandboxFilesUploadResponses] -export type PostAgentByAgentIdSkillsStandardizeData = { - body?: never - path: { - agent_id: string - } - query?: never - url: '/agent/{agent_id}/skills/standardize' -} - -export type PostAgentByAgentIdSkillsStandardizeErrors = { - 400: unknown -} - -export type PostAgentByAgentIdSkillsStandardizeResponses = { - 201: AgentSkillStandardizeResponse -} - -export type PostAgentByAgentIdSkillsStandardizeResponse - = PostAgentByAgentIdSkillsStandardizeResponses[keyof PostAgentByAgentIdSkillsStandardizeResponses] - export type PostAgentByAgentIdSkillsUploadData = { - body?: never + body: { + file: Blob | File + } path: { agent_id: string } diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index 6811b8a80bb..fad0a3f0048 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -549,14 +549,6 @@ export const zAgentSkillRefConfig = z.object({ skill_md_key: z.string().max(512).nullish(), }) -/** - * AgentSkillStandardizeResponse - */ -export const zAgentSkillStandardizeResponse = z.object({ - manifest: zSkillManifest, - skill: zAgentSkillRefConfig, -}) - /** * AgentSkillUploadResponse */ @@ -667,9 +659,9 @@ export const zModelConfig = z.object({ }) /** - * AppDetailWithSite + * AgentAppDetailWithSite */ -export const zAppDetailWithSite = z.object({ +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(), @@ -2063,9 +2055,9 @@ export const zSiteWritable = z.object({ }) /** - * AppDetailWithSite + * AgentAppDetailWithSite */ -export const zAppDetailWithSiteWritable = z.object({ +export const zAgentAppDetailWithSiteWritable = z.object({ access_mode: z.string().nullish(), active_config_is_published: z.boolean().optional().default(false), api_base_url: z.string().nullish(), @@ -2131,7 +2123,7 @@ export const zPostAgentBody = zAgentAppCreatePayload /** * Agent app created successfully */ -export const zPostAgentResponse = zAppDetailWithSite +export const zPostAgentResponse = zAgentAppDetailWithSite export const zGetAgentInviteOptionsQuery = z.object({ app_id: z.string().optional(), @@ -2161,7 +2153,7 @@ export const zGetAgentByAgentIdPath = z.object({ /** * Agent app detail */ -export const zGetAgentByAgentIdResponse = zAppDetailWithSite +export const zGetAgentByAgentIdResponse = zAgentAppDetailWithSite export const zPutAgentByAgentIdBody = zAgentAppUpdatePayload @@ -2172,7 +2164,7 @@ export const zPutAgentByAgentIdPath = z.object({ /** * Agent app updated successfully */ -export const zPutAgentByAgentIdResponse = zAppDetailWithSite +export const zPutAgentByAgentIdResponse = zAgentAppDetailWithSite export const zGetAgentByAgentIdChatMessagesPath = z.object({ agent_id: z.uuid(), @@ -2259,7 +2251,7 @@ export const zPostAgentByAgentIdCopyPath = z.object({ /** * Agent app copied successfully */ -export const zPostAgentByAgentIdCopyResponse = zAppDetailWithSite +export const zPostAgentByAgentIdCopyResponse = zAgentAppDetailWithSite export const zGetAgentByAgentIdDriveFilesPath = z.object({ agent_id: z.uuid(), @@ -2452,21 +2444,16 @@ export const zPostAgentByAgentIdSandboxFilesUploadPath = z.object({ */ export const zPostAgentByAgentIdSandboxFilesUploadResponse = zSandboxUploadResponse -export const zPostAgentByAgentIdSkillsStandardizePath = z.object({ - agent_id: z.uuid(), +export const zPostAgentByAgentIdSkillsUploadBody = z.object({ + file: z.custom(), }) -/** - * Skill standardized into drive - */ -export const zPostAgentByAgentIdSkillsStandardizeResponse = zAgentSkillStandardizeResponse - export const zPostAgentByAgentIdSkillsUploadPath = z.object({ agent_id: z.uuid(), }) /** - * Skill validated + * Skill uploaded into drive */ export const zPostAgentByAgentIdSkillsUploadResponse = zAgentSkillUploadResponse diff --git a/packages/contracts/generated/api/console/apps/orpc.gen.ts b/packages/contracts/generated/api/console/apps/orpc.gen.ts index 3952812a5a8..eab3c17eb43 100644 --- a/packages/contracts/generated/api/console/apps/orpc.gen.ts +++ b/packages/contracts/generated/api/console/apps/orpc.gen.ts @@ -271,10 +271,9 @@ import { zPostAppsByAppIdAgentSkillsBySlugInferToolsPath, zPostAppsByAppIdAgentSkillsBySlugInferToolsQuery, zPostAppsByAppIdAgentSkillsBySlugInferToolsResponse, - zPostAppsByAppIdAgentSkillsStandardizePath, - zPostAppsByAppIdAgentSkillsStandardizeQuery, - zPostAppsByAppIdAgentSkillsStandardizeResponse, + zPostAppsByAppIdAgentSkillsUploadBody, zPostAppsByAppIdAgentSkillsUploadPath, + zPostAppsByAppIdAgentSkillsUploadQuery, zPostAppsByAppIdAgentSkillsUploadResponse, zPostAppsByAppIdAnnotationReplyByActionBody, zPostAppsByAppIdAnnotationReplyByActionPath, @@ -939,57 +938,32 @@ export const logs = { } /** - * Upload a Skill, validate it, and standardize it into the app agent's drive + * Upload a Skill, validate it, and commit drive-backed skill files * - * Validate + standardize a Skill into the agent drive (ENG-594) + * Upload + standardize a Skill into the agent drive */ export const post10 = oc .route({ - description: 'Validate + standardize a Skill into the agent drive (ENG-594)', - inputStructure: 'detailed', - method: 'POST', - operationId: 'postAppsByAppIdAgentSkillsStandardize', - path: '/apps/{app_id}/agent/skills/standardize', - successStatus: 201, - summary: 'Upload a Skill, validate it, and standardize it into the app agent\'s drive', - tags: ['console'], - }) - .input( - z.object({ - params: zPostAppsByAppIdAgentSkillsStandardizePath, - query: zPostAppsByAppIdAgentSkillsStandardizeQuery.optional(), - }), - ) - .output(zPostAppsByAppIdAgentSkillsStandardizeResponse) - -export const standardize = { - post: post10, -} - -/** - * Validate an uploaded Skill package and persist the archive - * - * Upload + validate a Skill package (.zip/.skill) and extract its manifest - * Returns a validated skill ref (to bind into the Agent soul config on save) - * plus its manifest. Standardizing into the agent drive is ENG-594. - */ -export const post11 = oc - .route({ - description: - 'Upload + validate a Skill package (.zip/.skill) and extract its manifest\nReturns a validated skill ref (to bind into the Agent soul config on save)\nplus its manifest. Standardizing into the agent drive is ENG-594.', + description: 'Upload + standardize a Skill into the agent drive', inputStructure: 'detailed', method: 'POST', operationId: 'postAppsByAppIdAgentSkillsUpload', path: '/apps/{app_id}/agent/skills/upload', successStatus: 201, - summary: 'Validate an uploaded Skill package and persist the archive', + summary: 'Upload a Skill, validate it, and commit drive-backed skill files', tags: ['console'], }) - .input(z.object({ params: zPostAppsByAppIdAgentSkillsUploadPath })) + .input( + z.object({ + body: zPostAppsByAppIdAgentSkillsUploadBody, + params: zPostAppsByAppIdAgentSkillsUploadPath, + query: zPostAppsByAppIdAgentSkillsUploadQuery.optional(), + }), + ) .output(zPostAppsByAppIdAgentSkillsUploadResponse) export const upload = { - post: post11, + post: post10, } /** @@ -998,7 +972,7 @@ export const upload = { * Infer CLI tool + ENV suggestions from a standardized skill's SKILL.md (draft only, ENG-371) * Saving still goes through composer validation. */ -export const post12 = oc +export const post11 = oc .route({ description: 'Infer CLI tool + ENV suggestions from a standardized skill\'s SKILL.md (draft only, ENG-371)\nSaving still goes through composer validation.', @@ -1018,7 +992,7 @@ export const post12 = oc .output(zPostAppsByAppIdAgentSkillsBySlugInferToolsResponse) export const inferTools = { - post: post12, + post: post11, } /** @@ -1048,7 +1022,6 @@ export const bySlug = { } export const skills = { - standardize, upload, bySlug, } @@ -1086,7 +1059,7 @@ export const status = { /** * Enable or disable annotation reply for an app */ -export const post13 = oc +export const post12 = oc .route({ description: 'Enable or disable annotation reply for an app', inputStructure: 'detailed', @@ -1104,7 +1077,7 @@ export const post13 = oc .output(zPostAppsByAppIdAnnotationReplyByActionResponse) export const byAction = { - post: post13, + post: post12, status, } @@ -1134,7 +1107,7 @@ export const annotationSetting = { /** * Update annotation settings for an app */ -export const post14 = oc +export const post13 = oc .route({ description: 'Update annotation settings for an app', inputStructure: 'detailed', @@ -1152,7 +1125,7 @@ export const post14 = oc .output(zPostAppsByAppIdAnnotationSettingsByAnnotationSettingIdResponse) export const byAnnotationSettingId = { - post: post14, + post: post13, } export const annotationSettings = { @@ -1162,7 +1135,7 @@ export const annotationSettings = { /** * Batch import annotations from CSV file with rate limiting and security checks */ -export const post15 = oc +export const post14 = oc .route({ description: 'Batch import annotations from CSV file with rate limiting and security checks', inputStructure: 'detailed', @@ -1175,7 +1148,7 @@ export const post15 = oc .output(zPostAppsByAppIdAnnotationsBatchImportResponse) export const batchImport = { - post: post15, + post: post14, } /** @@ -1278,7 +1251,7 @@ export const delete3 = oc /** * Update or delete an annotation */ -export const post16 = oc +export const post15 = oc .route({ description: 'Update or delete an annotation', inputStructure: 'detailed', @@ -1297,7 +1270,7 @@ export const post16 = oc export const byAnnotationId = { delete: delete3, - post: post16, + post: post15, hitHistories, } @@ -1336,7 +1309,7 @@ export const get15 = oc /** * Create a new annotation for an app */ -export const post17 = oc +export const post16 = oc .route({ description: 'Create a new annotation for an app', inputStructure: 'detailed', @@ -1354,7 +1327,7 @@ export const post17 = oc export const annotations = { delete: delete4, get: get15, - post: post17, + post: post16, batchImport, batchImportStatus, count: count2, @@ -1365,7 +1338,7 @@ export const annotations = { /** * Enable or disable app API */ -export const post18 = oc +export const post17 = oc .route({ description: 'Enable or disable app API', inputStructure: 'detailed', @@ -1378,13 +1351,13 @@ export const post18 = oc .output(zPostAppsByAppIdApiEnableResponse) export const apiEnable = { - post: post18, + post: post17, } /** * Transcript audio to text for chat messages */ -export const post19 = oc +export const post18 = oc .route({ description: 'Transcript audio to text for chat messages', inputStructure: 'detailed', @@ -1397,7 +1370,7 @@ export const post19 = oc .output(zPostAppsByAppIdAudioToTextResponse) export const audioToText = { - post: post19, + post: post18, } /** @@ -1487,7 +1460,7 @@ export const byMessageId = { /** * Stop a running chat message generation */ -export const post20 = oc +export const post19 = oc .route({ description: 'Stop a running chat message generation', inputStructure: 'detailed', @@ -1500,7 +1473,7 @@ export const post20 = oc .output(zPostAppsByAppIdChatMessagesByTaskIdStopResponse) export const stop = { - post: post20, + post: post19, } export const byTaskId = { @@ -1594,7 +1567,7 @@ export const completionConversations = { /** * Stop a running completion message generation */ -export const post21 = oc +export const post20 = oc .route({ description: 'Stop a running completion message generation', inputStructure: 'detailed', @@ -1607,7 +1580,7 @@ export const post21 = oc .output(zPostAppsByAppIdCompletionMessagesByTaskIdStopResponse) export const stop2 = { - post: post21, + post: post20, } export const byTaskId2 = { @@ -1617,7 +1590,7 @@ export const byTaskId2 = { /** * Generate completion message for debugging */ -export const post22 = oc +export const post21 = oc .route({ description: 'Generate completion message for debugging', inputStructure: 'detailed', @@ -1635,7 +1608,7 @@ export const post22 = oc .output(zPostAppsByAppIdCompletionMessagesResponse) export const completionMessages = { - post: post22, + post: post21, byTaskId: byTaskId2, } @@ -1670,7 +1643,7 @@ export const conversationVariables = { * Convert expert mode of chatbot app to workflow mode * Convert Completion App to Workflow App */ -export const post23 = oc +export const post22 = oc .route({ description: 'Convert application to workflow mode\nConvert expert mode of chatbot app to workflow mode\nConvert Completion App to Workflow App', @@ -1690,7 +1663,7 @@ export const post23 = oc .output(zPostAppsByAppIdConvertToWorkflowResponse) export const convertToWorkflow = { - post: post23, + post: post22, } /** @@ -1698,7 +1671,7 @@ export const convertToWorkflow = { * * Create a copy of an existing application */ -export const post24 = oc +export const post23 = oc .route({ description: 'Create a copy of an existing application', inputStructure: 'detailed', @@ -1713,7 +1686,7 @@ export const post24 = oc .output(zPostAppsByAppIdCopyResponse) export const copy = { - post: post24, + post: post23, } /** @@ -1767,7 +1740,7 @@ export const export3 = { /** * Create or update message feedback (like/dislike) */ -export const post25 = oc +export const post24 = oc .route({ description: 'Create or update message feedback (like/dislike)', inputStructure: 'detailed', @@ -1780,14 +1753,14 @@ export const post25 = oc .output(zPostAppsByAppIdFeedbacksResponse) export const feedbacks = { - post: post25, + post: post24, export: export3, } /** * Update application icon */ -export const post26 = oc +export const post25 = oc .route({ description: 'Update application icon', inputStructure: 'detailed', @@ -1800,7 +1773,7 @@ export const post26 = oc .output(zPostAppsByAppIdIconResponse) export const icon = { - post: post26, + post: post25, } /** @@ -1831,7 +1804,7 @@ export const messages = { * * Update application model configuration */ -export const post27 = oc +export const post26 = oc .route({ description: 'Update application model configuration', inputStructure: 'detailed', @@ -1847,13 +1820,13 @@ export const post27 = oc .output(zPostAppsByAppIdModelConfigResponse) export const modelConfig = { - post: post27, + post: post26, } /** * Check if app name is available */ -export const post28 = oc +export const post27 = oc .route({ description: 'Check if app name is available', inputStructure: 'detailed', @@ -1866,13 +1839,13 @@ export const post28 = oc .output(zPostAppsByAppIdNameResponse) export const name = { - post: post28, + post: post27, } /** * Publish app to Creators Platform */ -export const post29 = oc +export const post28 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -1885,7 +1858,7 @@ export const post29 = oc .output(zPostAppsByAppIdPublishToCreatorsPlatformResponse) export const publishToCreatorsPlatform = { - post: post29, + post: post28, } /** @@ -1906,7 +1879,7 @@ export const get26 = oc /** * Create MCP server configuration for an application */ -export const post30 = oc +export const post29 = oc .route({ description: 'Create MCP server configuration for an application', inputStructure: 'detailed', @@ -1936,14 +1909,14 @@ export const put = oc export const server = { get: get26, - post: post30, + post: post29, put, } /** * Reset access token for application site */ -export const post31 = oc +export const post30 = oc .route({ description: 'Reset access token for application site', inputStructure: 'detailed', @@ -1956,13 +1929,13 @@ export const post31 = oc .output(zPostAppsByAppIdSiteAccessTokenResetResponse) export const accessTokenReset = { - post: post31, + post: post30, } /** * Update application site configuration */ -export const post32 = oc +export const post31 = oc .route({ description: 'Update application site configuration', inputStructure: 'detailed', @@ -1975,14 +1948,14 @@ export const post32 = oc .output(zPostAppsByAppIdSiteResponse) export const site = { - post: post32, + post: post31, accessTokenReset, } /** * Enable or disable app site */ -export const post33 = oc +export const post32 = oc .route({ description: 'Enable or disable app site', inputStructure: 'detailed', @@ -1995,7 +1968,7 @@ export const post33 = oc .output(zPostAppsByAppIdSiteEnableResponse) export const siteEnable = { - post: post33, + post: post32, } /** @@ -2016,7 +1989,7 @@ export const delete7 = oc /** * Star an application for the current account */ -export const post34 = oc +export const post33 = oc .route({ description: 'Star an application for the current account', inputStructure: 'detailed', @@ -2030,7 +2003,7 @@ export const post34 = oc export const star = { delete: delete7, - post: post34, + post: post33, } /** @@ -2263,7 +2236,7 @@ export const voices = { /** * Convert text to speech for chat messages */ -export const post35 = oc +export const post34 = oc .route({ description: 'Convert text to speech for chat messages', inputStructure: 'detailed', @@ -2278,7 +2251,7 @@ export const post35 = oc .output(zPostAppsByAppIdTextToAudioResponse) export const textToAudio = { - post: post35, + post: post34, voices, } @@ -2303,7 +2276,7 @@ export const get36 = oc /** * Update app tracing configuration */ -export const post36 = oc +export const post35 = oc .route({ description: 'Update app tracing configuration', inputStructure: 'detailed', @@ -2317,7 +2290,7 @@ export const post36 = oc export const trace = { get: get36, - post: post36, + post: post35, } /** @@ -2386,7 +2359,7 @@ export const patch = oc * * Create a new tracing configuration for an application */ -export const post37 = oc +export const post36 = oc .route({ description: 'Create a new tracing configuration for an application', inputStructure: 'detailed', @@ -2406,13 +2379,13 @@ export const traceConfig = { delete: delete8, get: get37, patch, - post: post37, + post: post36, } /** * Update app trigger (enable/disable) */ -export const post38 = oc +export const post37 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -2430,7 +2403,7 @@ export const post38 = oc .output(zPostAppsByAppIdTriggerEnableResponse) export const triggerEnable = { - post: post38, + post: post37, } /** @@ -2538,7 +2511,7 @@ export const count3 = { * * Stop running workflow task */ -export const post39 = oc +export const post38 = oc .route({ description: 'Stop running workflow task', inputStructure: 'detailed', @@ -2552,7 +2525,7 @@ export const post39 = oc .output(zPostAppsByAppIdWorkflowRunsTasksByTaskIdStopResponse) export const stop3 = { - post: post39, + post: post38, } export const byTaskId3 = { @@ -2655,7 +2628,7 @@ export const read = { /** * Upload one workflow Agent sandbox file as a Dify ToolFile mapping */ -export const post40 = oc +export const post39 = oc .route({ description: 'Upload one workflow Agent sandbox file as a Dify ToolFile mapping', inputStructure: 'detailed', @@ -2673,7 +2646,7 @@ export const post40 = oc .output(zPostAppsByAppIdWorkflowRunsByWorkflowRunIdAgentNodesByNodeIdSandboxFilesUploadResponse) export const upload2 = { - post: post40, + post: post39, } /** @@ -2824,7 +2797,7 @@ export const byReplyId = { * * Add a reply to a workflow comment */ -export const post41 = oc +export const post40 = oc .route({ description: 'Add a reply to a workflow comment', inputStructure: 'detailed', @@ -2844,7 +2817,7 @@ export const post41 = oc .output(zPostAppsByAppIdWorkflowCommentsByCommentIdRepliesResponse) export const replies = { - post: post41, + post: post40, byReplyId, } @@ -2853,7 +2826,7 @@ export const replies = { * * Resolve a workflow comment */ -export const post42 = oc +export const post41 = oc .route({ description: 'Resolve a workflow comment', inputStructure: 'detailed', @@ -2867,7 +2840,7 @@ export const post42 = oc .output(zPostAppsByAppIdWorkflowCommentsByCommentIdResolveResponse) export const resolve = { - post: post42, + post: post41, } /** @@ -2961,7 +2934,7 @@ export const get50 = oc * * Create a new workflow comment */ -export const post43 = oc +export const post42 = oc .route({ description: 'Create a new workflow comment', inputStructure: 'detailed', @@ -2982,7 +2955,7 @@ export const post43 = oc export const comments = { get: get50, - post: post43, + post: post42, mentionUsers, byCommentId, } @@ -3163,7 +3136,7 @@ export const get57 = oc /** * Update conversation variables for workflow draft */ -export const post44 = oc +export const post43 = oc .route({ description: 'Update conversation variables for workflow draft', inputStructure: 'detailed', @@ -3182,7 +3155,7 @@ export const post44 = oc export const conversationVariables2 = { get: get57, - post: post44, + post: post43, } /** @@ -3206,7 +3179,7 @@ export const get58 = oc /** * Update environment variables for workflow draft */ -export const post45 = oc +export const post44 = oc .route({ description: 'Update environment variables for workflow draft', inputStructure: 'detailed', @@ -3225,13 +3198,13 @@ export const post45 = oc export const environmentVariables = { get: get58, - post: post45, + post: post44, } /** * Update draft workflow features */ -export const post46 = oc +export const post45 = oc .route({ description: 'Update draft workflow features', inputStructure: 'detailed', @@ -3249,7 +3222,7 @@ export const post46 = oc .output(zPostAppsByAppIdWorkflowsDraftFeaturesResponse) export const features = { - post: post46, + post: post45, } /** @@ -3257,7 +3230,7 @@ export const features = { * * Test human input delivery for workflow */ -export const post47 = oc +export const post46 = oc .route({ description: 'Test human input delivery for workflow', inputStructure: 'detailed', @@ -3276,7 +3249,7 @@ export const post47 = oc .output(zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdDeliveryTestResponse) export const deliveryTest = { - post: post47, + post: post46, } /** @@ -3284,7 +3257,7 @@ export const deliveryTest = { * * Get human input form preview for workflow */ -export const post48 = oc +export const post47 = oc .route({ description: 'Get human input form preview for workflow', inputStructure: 'detailed', @@ -3303,7 +3276,7 @@ export const post48 = oc .output(zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormPreviewResponse) export const preview3 = { - post: post48, + post: post47, } /** @@ -3311,7 +3284,7 @@ export const preview3 = { * * Submit human input form preview for workflow */ -export const post49 = oc +export const post48 = oc .route({ description: 'Submit human input form preview for workflow', inputStructure: 'detailed', @@ -3330,7 +3303,7 @@ export const post49 = oc .output(zPostAppsByAppIdWorkflowsDraftHumanInputNodesByNodeIdFormRunResponse) export const run5 = { - post: post49, + post: post48, } export const form2 = { @@ -3356,7 +3329,7 @@ export const humanInput2 = { * * Run draft workflow iteration node */ -export const post50 = oc +export const post49 = oc .route({ description: 'Run draft workflow iteration node', inputStructure: 'detailed', @@ -3375,7 +3348,7 @@ export const post50 = oc .output(zPostAppsByAppIdWorkflowsDraftIterationNodesByNodeIdRunResponse) export const run6 = { - post: post50, + post: post49, } export const byNodeId6 = { @@ -3395,7 +3368,7 @@ export const iteration2 = { * * Run draft workflow loop node */ -export const post51 = oc +export const post50 = oc .route({ description: 'Run draft workflow loop node', inputStructure: 'detailed', @@ -3414,7 +3387,7 @@ export const post51 = oc .output(zPostAppsByAppIdWorkflowsDraftLoopNodesByNodeIdRunResponse) export const run7 = { - post: post51, + post: post50, } export const byNodeId7 = { @@ -3446,7 +3419,7 @@ export const candidates = { get: get59, } -export const post52 = oc +export const post51 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -3463,10 +3436,10 @@ export const post52 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerImpactResponse) export const impact = { - post: post52, + post: post51, } -export const post53 = oc +export const post52 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -3483,10 +3456,10 @@ export const post53 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerSaveToRosterResponse) export const saveToRoster = { - post: post53, + post: post52, } -export const post54 = oc +export const post53 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -3503,7 +3476,7 @@ export const post54 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdAgentComposerValidateResponse) export const validate = { - post: post54, + post: post53, } export const get60 = oc @@ -3566,7 +3539,7 @@ export const lastRun = { * * Run draft workflow node */ -export const post55 = oc +export const post54 = oc .route({ description: 'Run draft workflow node', inputStructure: 'detailed', @@ -3585,7 +3558,7 @@ export const post55 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdRunResponse) export const run8 = { - post: post55, + post: post54, } /** @@ -3593,7 +3566,7 @@ export const run8 = { * * Poll for trigger events and execute single node when event arrives */ -export const post56 = oc +export const post55 = oc .route({ description: 'Poll for trigger events and execute single node when event arrives', inputStructure: 'detailed', @@ -3607,7 +3580,7 @@ export const post56 = oc .output(zPostAppsByAppIdWorkflowsDraftNodesByNodeIdTriggerRunResponse) export const run9 = { - post: post56, + post: post55, } export const trigger = { @@ -3667,7 +3640,7 @@ export const nodes7 = { * * Run draft workflow */ -export const post57 = oc +export const post56 = oc .route({ description: 'Run draft workflow', inputStructure: 'detailed', @@ -3686,7 +3659,7 @@ export const post57 = oc .output(zPostAppsByAppIdWorkflowsDraftRunResponse) export const run10 = { - post: post57, + post: post56, } /** @@ -3808,7 +3781,7 @@ export const systemVariables = { * * Poll for trigger events and execute full workflow when event arrives */ -export const post58 = oc +export const post57 = oc .route({ description: 'Poll for trigger events and execute full workflow when event arrives', inputStructure: 'detailed', @@ -3827,7 +3800,7 @@ export const post58 = oc .output(zPostAppsByAppIdWorkflowsDraftTriggerRunResponse) export const run11 = { - post: post58, + post: post57, } /** @@ -3835,7 +3808,7 @@ export const run11 = { * * Full workflow debug when the start node is a trigger */ -export const post59 = oc +export const post58 = oc .route({ description: 'Full workflow debug when the start node is a trigger', inputStructure: 'detailed', @@ -3854,7 +3827,7 @@ export const post59 = oc .output(zPostAppsByAppIdWorkflowsDraftTriggerRunAllResponse) export const runAll = { - post: post59, + post: post58, } export const trigger2 = { @@ -4007,7 +3980,7 @@ export const get70 = oc * * Sync draft workflow configuration */ -export const post60 = oc +export const post59 = oc .route({ description: 'Sync draft workflow configuration', inputStructure: 'detailed', @@ -4027,7 +4000,7 @@ export const post60 = oc export const draft2 = { get: get70, - post: post60, + post: post59, conversationVariables: conversationVariables2, environmentVariables, features, @@ -4063,7 +4036,7 @@ export const get71 = oc /** * Publish workflow */ -export const post61 = oc +export const post60 = oc .route({ inputStructure: 'detailed', method: 'POST', @@ -4082,7 +4055,7 @@ export const post61 = oc export const publish = { get: get71, - post: post61, + post: post60, } /** @@ -4219,7 +4192,7 @@ export const triggers2 = { /** * Restore a published workflow version into the draft workflow */ -export const post62 = oc +export const post61 = oc .route({ description: 'Restore a published workflow version into the draft workflow', inputStructure: 'detailed', @@ -4232,7 +4205,7 @@ export const post62 = oc .output(zPostAppsByAppIdWorkflowsByWorkflowIdRestoreResponse) export const restore = { - post: post62, + post: post61, } /** @@ -4457,7 +4430,7 @@ export const get79 = oc * * Create a new API key for an app */ -export const post63 = oc +export const post62 = oc .route({ description: 'Create a new API key for an app', inputStructure: 'detailed', @@ -4473,7 +4446,7 @@ export const post63 = oc export const apiKeys = { get: get79, - post: post63, + post: post62, byApiKeyId, } @@ -4531,7 +4504,7 @@ export const get81 = oc * * Create a new application */ -export const post64 = oc +export const post63 = oc .route({ description: 'Create a new application', inputStructure: 'detailed', @@ -4547,7 +4520,7 @@ export const post64 = oc export const apps = { get: get81, - post: post64, + post: post63, imports, starred, workflows, diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index 8ccf899b451..debd7ff1991 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -5,10 +5,10 @@ export type ClientOptions = { } export type AppPagination = { - has_next: boolean - items: Array + data: Array + has_more: boolean + limit: number page: number - per_page: number total: number } @@ -23,7 +23,6 @@ export type CreateAppPayload = { export type AppDetailWithSite = { access_mode?: string | null - active_config_is_published?: boolean api_base_url?: string | null app_id?: string | null bound_agent_id?: string | null @@ -42,7 +41,6 @@ export type AppDetailWithSite = { mode: string model_config?: ModelConfig | null name: string - role?: string | null site?: Site | null tags?: Array tracing?: JsonValue | null @@ -213,11 +211,6 @@ export type AgentLogResponse = { meta: AgentLogMetaResponse } -export type AgentSkillStandardizeResponse = { - manifest: SkillManifest - skill: AgentSkillRefConfig -} - export type AgentSkillUploadResponse = { manifest: SkillManifest skill: AgentSkillRefConfig @@ -1156,25 +1149,24 @@ export type ApiKeyItem = { export type AppPartial = { access_mode?: string | null - active_config_is_published?: boolean app_id?: string | null - app_model_config?: ModelConfigPartial | null author_name?: string | null bound_agent_id?: string | null create_user_name?: string | null created_at?: number | null created_by?: string | null - desc_or_prompt?: string | null + description?: string | null has_draft_trigger?: boolean | null icon?: string | null icon_background?: string | null icon_type?: string | null + readonly icon_url: string | null id: string is_starred?: boolean max_active_requests?: number | null - mode_compatible_with_agent: string + mode: string + model_config?: ModelConfigPartial | null name: string - role?: string | null tags?: Array updated_at?: number | null updated_by?: string | null @@ -2575,9 +2567,16 @@ export type AgentModerationIoConfig = { export type ValueSourceType = 'constant' | 'variable' +export type AppPaginationWritable = { + data: Array + has_more: boolean + limit: number + page: number + total: number +} + export type AppDetailWithSiteWritable = { access_mode?: string | null - active_config_is_published?: boolean api_base_url?: string | null app_id?: string | null bound_agent_id?: string | null @@ -2595,7 +2594,6 @@ export type AppDetailWithSiteWritable = { mode: string model_config?: ModelConfig | null name: string - role?: string | null site?: SiteWritable | null tags?: Array tracing?: JsonValue | null @@ -2628,6 +2626,32 @@ export type WorkflowCommentDetailWritable = { updated_at?: number | null } +export type AppPartialWritable = { + access_mode?: string | null + app_id?: string | null + author_name?: string | null + bound_agent_id?: string | null + create_user_name?: string | null + created_at?: number | null + created_by?: string | null + description?: string | null + has_draft_trigger?: boolean | null + icon?: string | null + icon_background?: string | null + icon_type?: string | null + id: string + is_starred?: boolean + max_active_requests?: number | null + mode: string + model_config?: ModelConfigPartial | null + name: string + tags?: Array + updated_at?: number | null + updated_by?: string | null + use_icon_as_answer_icon?: boolean | null + workflow?: WorkflowPartial | null +} + export type SiteWritable = { chat_color_theme?: string | null chat_color_theme_inverted: boolean @@ -3144,34 +3168,16 @@ export type GetAppsByAppIdAgentLogsResponses = { export type GetAppsByAppIdAgentLogsResponse = GetAppsByAppIdAgentLogsResponses[keyof GetAppsByAppIdAgentLogsResponses] -export type PostAppsByAppIdAgentSkillsStandardizeData = { - body?: never +export type PostAppsByAppIdAgentSkillsUploadData = { + body: { + file: Blob | File + } path: { app_id: string } query?: { node_id?: string } - url: '/apps/{app_id}/agent/skills/standardize' -} - -export type PostAppsByAppIdAgentSkillsStandardizeErrors = { - 400: unknown -} - -export type PostAppsByAppIdAgentSkillsStandardizeResponses = { - 201: AgentSkillStandardizeResponse -} - -export type PostAppsByAppIdAgentSkillsStandardizeResponse - = PostAppsByAppIdAgentSkillsStandardizeResponses[keyof PostAppsByAppIdAgentSkillsStandardizeResponses] - -export type PostAppsByAppIdAgentSkillsUploadData = { - body?: never - path: { - app_id: string - } - query?: never url: '/apps/{app_id}/agent/skills/upload' } diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index df275e5fb9c..83a0b79c624 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -990,14 +990,6 @@ export const zAgentSkillRefConfig = z.object({ skill_md_key: z.string().max(512).nullish(), }) -/** - * AgentSkillStandardizeResponse - */ -export const zAgentSkillStandardizeResponse = z.object({ - manifest: zSkillManifest, - skill: zAgentSkillRefConfig, -}) - /** * AgentSkillUploadResponse */ @@ -1946,25 +1938,24 @@ export const zModelConfigPartial = z.object({ */ export const zAppPartial = z.object({ access_mode: z.string().nullish(), - active_config_is_published: z.boolean().optional().default(false), app_id: z.string().nullish(), - app_model_config: zModelConfigPartial.nullish(), author_name: z.string().nullish(), bound_agent_id: z.string().nullish(), create_user_name: z.string().nullish(), created_at: z.int().nullish(), created_by: z.string().nullish(), - desc_or_prompt: z.string().nullish(), + description: z.string().nullish(), has_draft_trigger: z.boolean().nullish(), icon: z.string().nullish(), icon_background: z.string().nullish(), icon_type: z.string().nullish(), + icon_url: z.string().nullable(), id: z.string(), is_starred: z.boolean().optional().default(false), max_active_requests: z.int().nullish(), - mode_compatible_with_agent: z.string(), + mode: z.string(), + model_config: zModelConfigPartial.nullish(), name: z.string(), - role: z.string().nullish(), tags: z.array(zTag).optional(), updated_at: z.int().nullish(), updated_by: z.string().nullish(), @@ -1976,10 +1967,10 @@ export const zAppPartial = z.object({ * AppPagination */ export const zAppPagination = z.object({ - has_next: z.boolean(), - items: z.array(zAppPartial), + data: z.array(zAppPartial), + has_more: z.boolean(), + limit: z.int(), page: z.int(), - per_page: z.int(), total: z.int(), }) @@ -2005,7 +1996,6 @@ export const zModelConfig = z.object({ */ export const zAppDetailWithSite = 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(), bound_agent_id: z.string().nullish(), @@ -2024,7 +2014,6 @@ export const zAppDetailWithSite = z.object({ mode: z.string(), model_config: zModelConfig.nullish(), name: z.string(), - role: z.string().nullish(), site: zSite.nullish(), tags: z.array(zTag).optional(), tracing: zJsonValue.nullish(), @@ -3472,6 +3461,46 @@ export const zMessageInfiniteScrollPaginationResponse = z.object({ */ export const zGeneratedAppResponseWritable = zJsonValue +/** + * AppPartial + */ +export const zAppPartialWritable = z.object({ + access_mode: z.string().nullish(), + app_id: z.string().nullish(), + author_name: z.string().nullish(), + bound_agent_id: z.string().nullish(), + create_user_name: z.string().nullish(), + created_at: z.int().nullish(), + created_by: z.string().nullish(), + description: z.string().nullish(), + has_draft_trigger: z.boolean().nullish(), + icon: z.string().nullish(), + icon_background: z.string().nullish(), + icon_type: z.string().nullish(), + id: z.string(), + is_starred: z.boolean().optional().default(false), + max_active_requests: z.int().nullish(), + mode: z.string(), + model_config: zModelConfigPartial.nullish(), + name: z.string(), + tags: z.array(zTag).optional(), + updated_at: z.int().nullish(), + updated_by: z.string().nullish(), + use_icon_as_answer_icon: z.boolean().nullish(), + workflow: zWorkflowPartial.nullish(), +}) + +/** + * AppPagination + */ +export const zAppPaginationWritable = z.object({ + data: z.array(zAppPartialWritable), + has_more: z.boolean(), + limit: z.int(), + page: z.int(), + total: z.int(), +}) + /** * Site */ @@ -3496,7 +3525,6 @@ export const zSiteWritable = z.object({ */ export const zAppDetailWithSiteWritable = 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(), bound_agent_id: z.string().nullish(), @@ -3514,7 +3542,6 @@ export const zAppDetailWithSiteWritable = z.object({ mode: z.string(), model_config: zModelConfig.nullish(), name: z.string(), - role: z.string().nullish(), site: zSiteWritable.nullish(), tags: z.array(zTag).optional(), tracing: zJsonValue.nullish(), @@ -3917,25 +3944,20 @@ export const zGetAppsByAppIdAgentLogsQuery = z.object({ */ export const zGetAppsByAppIdAgentLogsResponse = zAgentLogResponse -export const zPostAppsByAppIdAgentSkillsStandardizePath = z.object({ - app_id: z.uuid(), +export const zPostAppsByAppIdAgentSkillsUploadBody = z.object({ + file: z.custom(), }) -export const zPostAppsByAppIdAgentSkillsStandardizeQuery = z.object({ - node_id: z.string().optional(), -}) - -/** - * Skill standardized into drive - */ -export const zPostAppsByAppIdAgentSkillsStandardizeResponse = zAgentSkillStandardizeResponse - export const zPostAppsByAppIdAgentSkillsUploadPath = z.object({ app_id: z.uuid(), }) +export const zPostAppsByAppIdAgentSkillsUploadQuery = z.object({ + node_id: z.string().optional(), +}) + /** - * Skill validated + * Skill uploaded into drive */ export const zPostAppsByAppIdAgentSkillsUploadResponse = zAgentSkillUploadResponse diff --git a/packages/iconify-collections/assets/public/agent/building-blocks.svg b/packages/iconify-collections/assets/public/agent/building-blocks.svg new file mode 100644 index 00000000000..8037f6f0865 --- /dev/null +++ b/packages/iconify-collections/assets/public/agent/building-blocks.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/agent-v2/access-point.svg b/packages/iconify-collections/assets/vender/agent-v2/access-point.svg new file mode 100644 index 00000000000..6f2c2f1926f --- /dev/null +++ b/packages/iconify-collections/assets/vender/agent-v2/access-point.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/iconify-collections/assets/vender/agent-v2/end-user-auth.svg b/packages/iconify-collections/assets/vender/agent-v2/end-user-auth.svg new file mode 100644 index 00000000000..1632376e944 --- /dev/null +++ b/packages/iconify-collections/assets/vender/agent-v2/end-user-auth.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/iconify-collections/assets/vender/agent-v2/plan.svg b/packages/iconify-collections/assets/vender/agent-v2/plan.svg new file mode 100644 index 00000000000..a9456e4806b --- /dev/null +++ b/packages/iconify-collections/assets/vender/agent-v2/plan.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/agent-v2/prompt-insert.svg b/packages/iconify-collections/assets/vender/agent-v2/prompt-insert.svg new file mode 100644 index 00000000000..f0be50ee5fe --- /dev/null +++ b/packages/iconify-collections/assets/vender/agent-v2/prompt-insert.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/agent-v2/robot-3.svg b/packages/iconify-collections/assets/vender/agent-v2/robot-3.svg new file mode 100644 index 00000000000..0f8617ef3a0 --- /dev/null +++ b/packages/iconify-collections/assets/vender/agent-v2/robot-3.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/roster-active.svg b/packages/iconify-collections/assets/vender/main-nav/roster-active.svg new file mode 100644 index 00000000000..0d0fbb95fe9 --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/roster-active.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/iconify-collections/assets/vender/main-nav/roster.svg b/packages/iconify-collections/assets/vender/main-nav/roster.svg new file mode 100644 index 00000000000..5169111425e --- /dev/null +++ b/packages/iconify-collections/assets/vender/main-nav/roster.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/iconify-collections/assets/vender/workflow/agent.svg b/packages/iconify-collections/assets/vender/workflow/agent.svg index f30c0b455fc..09bf8c0683c 100644 --- a/packages/iconify-collections/assets/vender/workflow/agent.svg +++ b/packages/iconify-collections/assets/vender/workflow/agent.svg @@ -1,8 +1,6 @@ - - - - - - + + + + diff --git a/packages/iconify-collections/custom-public/icons.json b/packages/iconify-collections/custom-public/icons.json index 7a4ea8bdaf1..0f83cf3b3e8 100644 --- a/packages/iconify-collections/custom-public/icons.json +++ b/packages/iconify-collections/custom-public/icons.json @@ -1,7 +1,10 @@ { "prefix": "custom-public", - "lastModified": 1781246368, + "lastModified": 1781515983, "icons": { + "agent-building-blocks": { + "body": "" + }, "avatar-user": { "body": "", "width": 512, diff --git a/packages/iconify-collections/custom-public/info.json b/packages/iconify-collections/custom-public/info.json index 115e9e25f90..1b439b73724 100644 --- a/packages/iconify-collections/custom-public/info.json +++ b/packages/iconify-collections/custom-public/info.json @@ -1,7 +1,7 @@ { "prefix": "custom-public", "name": "Dify Custom Public", - "total": 144, + "total": 145, "version": "0.0.0-private", "author": { "name": "LangGenius, Inc.", @@ -13,12 +13,12 @@ "url": "https://github.com/langgenius/dify/blob/main/LICENSE" }, "samples": [ + "agent-building-blocks", "avatar-user", "billing-ar-cube-1", "billing-asterisk", "billing-aws-marketplace-dark", - "billing-aws-marketplace-light", - "billing-azure" + "billing-aws-marketplace-light" ], "palette": false } diff --git a/packages/iconify-collections/custom-vender/icons.json b/packages/iconify-collections/custom-vender/icons.json index bf08a795057..048c5af7553 100644 --- a/packages/iconify-collections/custom-vender/icons.json +++ b/packages/iconify-collections/custom-vender/icons.json @@ -1,7 +1,29 @@ { "prefix": "custom-vender", - "lastModified": 1781246368, + "lastModified": 1781515983, "icons": { + "agent-v2-access-point": { + "body": "", + "width": 15, + "height": 15 + }, + "agent-v2-end-user-auth": { + "body": "" + }, + "agent-v2-plan": { + "body": "", + "width": 18, + "height": 18 + }, + "agent-v2-prompt-insert": { + "body": "", + "width": 14, + "height": 14 + }, + "agent-v2-robot-3": { + "body": "", + "width": 17 + }, "features-citations": { "body": "", "width": 24, @@ -811,6 +833,16 @@ "width": 24, "height": 24 }, + "main-nav-roster": { + "body": "", + "width": 18, + "height": 18 + }, + "main-nav-roster-active": { + "body": "", + "width": 18, + "height": 18 + }, "main-nav-studio": { "body": "", "width": 20, @@ -1295,9 +1327,9 @@ "height": 24 }, "workflow-agent": { - "body": "", - "width": 16, - "height": 16 + "body": "", + "width": 24, + "height": 24 }, "workflow-answer": { "body": "", diff --git a/packages/iconify-collections/custom-vender/info.json b/packages/iconify-collections/custom-vender/info.json index cef352ac549..608b5020f04 100644 --- a/packages/iconify-collections/custom-vender/info.json +++ b/packages/iconify-collections/custom-vender/info.json @@ -1,7 +1,7 @@ { "prefix": "custom-vender", "name": "Dify Custom Vender", - "total": 319, + "total": 326, "version": "0.0.0-private", "author": { "name": "LangGenius, Inc.", @@ -13,12 +13,12 @@ "url": "https://github.com/langgenius/dify/blob/main/LICENSE" }, "samples": [ - "features-citations", - "features-content-moderation", - "features-document", - "features-folder-upload", - "features-love-message", - "features-message-fast" + "agent-v2-access-point", + "agent-v2-end-user-auth", + "agent-v2-plan", + "agent-v2-prompt-insert", + "agent-v2-robot-3", + "features-citations" ], "palette": false } diff --git a/web/.env.example b/web/.env.example index 762db54dafa..112232e529c 100644 --- a/web/.env.example +++ b/web/.env.example @@ -90,6 +90,9 @@ NEXT_PUBLIC_ENABLE_SINGLE_DOLLAR_LATEX=false # /refine slash commands in the "Go to Anything" command palette) NEXT_PUBLIC_ENABLE_FEATURE_PREVIEW=false +# Enable Agent v2 frontend entry points. +NEXT_PUBLIC_ENABLE_AGENT_V2=false + # The maximum number of tree node depth for workflow NEXT_PUBLIC_MAX_TREE_DEPTH=50 diff --git a/web/AGENTS.md b/web/AGENTS.md index 21def787b9e..6221b2376ae 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -38,6 +38,15 @@ - Do not access `localStorage`, `window.localStorage`, or `globalThis.localStorage` directly in app code; use the storage hook boundary and preserve existing raw/custom storage formats. - Do not add ad hoc global event listeners for shared state. Prefer atoms, existing stores, or a shared subscription hook so listeners are centralized and deduplicated. +## Agent V2 Frontend + +- Keep Agent V2 separate from legacy workflow Agent. Use `web/features/agent-v2`, `web/app/components/workflow/nodes/agent-v2`, the `agent_node_kind: 'dify_agent'` and `version: '2'` payload discriminator, and `BlockEnum.AgentV2` where the graph type is already migrated. Do not bridge Agent V2 to legacy `agent_strategy_*` behavior or data shapes. +- Use generated contracts and `consoleQuery` / `consoleClient` from `@/service/client` for Agent V2 backend calls. Do not add handwritten REST helpers, handwritten API types, mock-backed app state, or direct edits to generated contract files. +- Treat TanStack Query as the server source of truth. Scope editable drafts with an instance-level `AgentComposerProvider`, hydrate Jotai `originalDraft`, `publishedDraft`, and `draft` from contract data, and compute dirty or unpublished state from those draft snapshots. +- Keep transitional defaults and mock data at the owning surface, such as the configure page or workflow node, not in shared composer defaults. +- Use `@langgenius/dify-ui/*` primitives and primitive data/CSS selectors first. Add call-site Tailwind only for real design deltas, avoid arbitrary values when token utilities exist, and keep focus rings visible without making inert layout regions focusable. +- Keep Agent V2 copy in the `agentV2` i18n namespace, currently backed by `agent-v-2.json` in the maintained locale set. + ## Automated Test Generation - Use `./docs/test.md` as the canonical instruction set for generating frontend automated tests. diff --git a/web/__tests__/env.spec.ts b/web/__tests__/env.spec.ts new file mode 100644 index 00000000000..89781d32685 --- /dev/null +++ b/web/__tests__/env.spec.ts @@ -0,0 +1,42 @@ +describe('env runtime transport', () => { + const originalAgentV2Env = process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 + + beforeEach(() => { + vi.clearAllMocks() + vi.resetModules() + vi.doUnmock('../utils/client') + document.body.removeAttribute('data-enable-agent-v2') + document.body.removeAttribute('data-enable-agent-v-2') + delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 + }) + + afterAll(() => { + if (originalAgentV2Env === undefined) + delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 + else + process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = originalAgentV2Env + }) + + it('should read NEXT_PUBLIC_ENABLE_AGENT_V2 from the browser runtime dataset key', async () => { + document.body.setAttribute('data-enable-agent-v2', 'true') + + const { env } = await import('../env') + + expect(env.NEXT_PUBLIC_ENABLE_AGENT_V2).toBe(true) + }) + + it('should emit the Agent v2 runtime dataset attribute from getDatasetMap on the server', async () => { + process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = 'true' + + vi.doMock('../utils/client', () => ({ + isClient: false, + isServer: true, + })) + + const { getDatasetMap } = await import('../env') + const datasetMap = getDatasetMap() + + expect(datasetMap['data-enable-agent-v2']).toBe(true) + expect(datasetMap['data-enable-agent-v-2']).toBeUndefined() + }) +}) diff --git a/web/app/(commonLayout)/role-route-guard.tsx b/web/app/(commonLayout)/role-route-guard.tsx index 05cf59aee42..0fee1d72f08 100644 --- a/web/app/(commonLayout)/role-route-guard.tsx +++ b/web/app/(commonLayout)/role-route-guard.tsx @@ -7,7 +7,7 @@ import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { redirect, usePathname } from '@/next/navigation' import { consoleQuery } from '@/service/client' -const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/deployments', '/snippets', '/explore', '/tools', '/integrations'] as const +const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/deployments', '/snippets', '/roster', '/explore', '/tools', '/integrations'] as const function isPathUnderRoute(pathname: string, route: string) { return pathname === route || pathname.startsWith(`${route}/`) diff --git a/web/app/(commonLayout)/roster/__tests__/feature-guard.spec.ts b/web/app/(commonLayout)/roster/__tests__/feature-guard.spec.ts new file mode 100644 index 00000000000..343e0797776 --- /dev/null +++ b/web/app/(commonLayout)/roster/__tests__/feature-guard.spec.ts @@ -0,0 +1,41 @@ +const mocks = vi.hoisted(() => ({ + agentV2Enabled: true, + notFound: vi.fn(() => { + throw new Error('NEXT_NOT_FOUND') + }), +})) + +vi.mock('@/env', () => ({ + env: { + get NEXT_PUBLIC_ENABLE_AGENT_V2() { + return mocks.agentV2Enabled + }, + }, +})) + +vi.mock('@/next/navigation', () => ({ + notFound: () => mocks.notFound(), +})) + +describe('guardAgentV2Route', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.agentV2Enabled = true + }) + + it('should allow roster routes when Agent v2 is enabled', async () => { + const { guardAgentV2Route } = await import('../feature-guard') + + expect(() => guardAgentV2Route()).not.toThrow() + expect(mocks.notFound).not.toHaveBeenCalled() + }) + + it('should throw notFound when Agent v2 is disabled', async () => { + mocks.agentV2Enabled = false + + const { guardAgentV2Route } = await import('../feature-guard') + + expect(() => guardAgentV2Route()).toThrow('NEXT_NOT_FOUND') + expect(mocks.notFound).toHaveBeenCalledTimes(1) + }) +}) diff --git a/web/app/(commonLayout)/roster/__tests__/layout.spec.tsx b/web/app/(commonLayout)/roster/__tests__/layout.spec.tsx new file mode 100644 index 00000000000..0d4dbc66dc2 --- /dev/null +++ b/web/app/(commonLayout)/roster/__tests__/layout.spec.tsx @@ -0,0 +1,43 @@ +import { render, screen } from '@testing-library/react' + +const mocks = vi.hoisted(() => ({ + guardAgentV2Route: vi.fn(), +})) + +vi.mock('../feature-guard', () => ({ + guardAgentV2Route: () => mocks.guardAgentV2Route(), +})) + +describe('RosterLayout', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should render children when Agent v2 is enabled', async () => { + const { default: RosterLayout } = await import('../layout') + + render( + +
Roster content
+ , + ) + + expect(mocks.guardAgentV2Route).toHaveBeenCalledTimes(1) + expect(screen.getByText('Roster content')).toBeInTheDocument() + }) + + it('should block rendering when the roster guard throws notFound', async () => { + mocks.guardAgentV2Route.mockImplementation(() => { + throw new Error('NEXT_NOT_FOUND') + }) + + const { default: RosterLayout } = await import('../layout') + + expect(() => render( + +
Roster content
+
, + )).toThrow('NEXT_NOT_FOUND') + expect(mocks.guardAgentV2Route).toHaveBeenCalled() + }) +}) diff --git a/web/app/(commonLayout)/roster/agent/[agentId]/access/page.tsx b/web/app/(commonLayout)/roster/agent/[agentId]/access/page.tsx new file mode 100644 index 00000000000..31d2fac8652 --- /dev/null +++ b/web/app/(commonLayout)/roster/agent/[agentId]/access/page.tsx @@ -0,0 +1,13 @@ +import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page' + +type PageProps = { + params: Promise<{ agentId: string }> +} + +export default async function Page({ + params, +}: PageProps) { + const { agentId } = await params + + return +} diff --git a/web/app/(commonLayout)/roster/agent/[agentId]/configure/page.tsx b/web/app/(commonLayout)/roster/agent/[agentId]/configure/page.tsx new file mode 100644 index 00000000000..a8a25ba6c3f --- /dev/null +++ b/web/app/(commonLayout)/roster/agent/[agentId]/configure/page.tsx @@ -0,0 +1,13 @@ +import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page' + +type PageProps = { + params: Promise<{ agentId: string }> +} + +export default async function Page({ + params, +}: PageProps) { + const { agentId } = await params + + return +} diff --git a/web/app/(commonLayout)/roster/agent/[agentId]/layout.tsx b/web/app/(commonLayout)/roster/agent/[agentId]/layout.tsx new file mode 100644 index 00000000000..384443d58bc --- /dev/null +++ b/web/app/(commonLayout)/roster/agent/[agentId]/layout.tsx @@ -0,0 +1,20 @@ +import type { ReactNode } from 'react' +import { AgentDetailLayout } from '@/features/agent-v2/agent-detail/layout' + +type LayoutProps = { + children: ReactNode + params: Promise<{ agentId: string }> +} + +export default async function Layout({ + children, + params, +}: LayoutProps) { + const { agentId } = await params + + return ( + + {children} + + ) +} diff --git a/web/app/(commonLayout)/roster/agent/[agentId]/logs/page.tsx b/web/app/(commonLayout)/roster/agent/[agentId]/logs/page.tsx new file mode 100644 index 00000000000..1cbcd7bbd90 --- /dev/null +++ b/web/app/(commonLayout)/roster/agent/[agentId]/logs/page.tsx @@ -0,0 +1,13 @@ +import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page' + +type PageProps = { + params: Promise<{ agentId: string }> +} + +export default async function Page({ + params, +}: PageProps) { + const { agentId } = await params + + return +} diff --git a/web/app/(commonLayout)/roster/agent/[agentId]/monitoring/page.tsx b/web/app/(commonLayout)/roster/agent/[agentId]/monitoring/page.tsx new file mode 100644 index 00000000000..ad61db5f8b8 --- /dev/null +++ b/web/app/(commonLayout)/roster/agent/[agentId]/monitoring/page.tsx @@ -0,0 +1,13 @@ +import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page' + +type PageProps = { + params: Promise<{ agentId: string }> +} + +export default async function Page({ + params, +}: PageProps) { + const { agentId } = await params + + return +} diff --git a/web/app/(commonLayout)/roster/agent/[agentId]/page.tsx b/web/app/(commonLayout)/roster/agent/[agentId]/page.tsx new file mode 100644 index 00000000000..470fa13f12c --- /dev/null +++ b/web/app/(commonLayout)/roster/agent/[agentId]/page.tsx @@ -0,0 +1,13 @@ +import { redirect } from '@/next/navigation' + +type PageProps = { + params: Promise<{ agentId: string }> +} + +export default async function Page({ + params, +}: PageProps) { + const { agentId } = await params + + redirect(`/roster/agent/${agentId}/configure`) +} diff --git a/web/app/(commonLayout)/roster/feature-guard.ts b/web/app/(commonLayout)/roster/feature-guard.ts new file mode 100644 index 00000000000..41f74b6696e --- /dev/null +++ b/web/app/(commonLayout)/roster/feature-guard.ts @@ -0,0 +1,7 @@ +import { env } from '@/env' +import { notFound } from '@/next/navigation' + +export const guardAgentV2Route = () => { + if (!env.NEXT_PUBLIC_ENABLE_AGENT_V2) + notFound() +} diff --git a/web/app/(commonLayout)/roster/layout.tsx b/web/app/(commonLayout)/roster/layout.tsx new file mode 100644 index 00000000000..be88ab89e2c --- /dev/null +++ b/web/app/(commonLayout)/roster/layout.tsx @@ -0,0 +1,12 @@ +import type { ReactNode } from 'react' +import { guardAgentV2Route } from './feature-guard' + +export default function Layout({ + children, +}: { + children: ReactNode +}) { + guardAgentV2Route() + + return children +} diff --git a/web/app/(commonLayout)/roster/page.tsx b/web/app/(commonLayout)/roster/page.tsx new file mode 100644 index 00000000000..fff610b6448 --- /dev/null +++ b/web/app/(commonLayout)/roster/page.tsx @@ -0,0 +1,5 @@ +import RosterPage from '@/features/agent-v2/roster/page' + +export default function Page() { + return +} diff --git a/web/app/(shareLayout)/agent/[token]/page.tsx b/web/app/(shareLayout)/agent/[token]/page.tsx new file mode 100644 index 00000000000..5dc2777c9c4 --- /dev/null +++ b/web/app/(shareLayout)/agent/[token]/page.tsx @@ -0,0 +1,15 @@ +'use client' + +import * as React from 'react' +import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history' +import AuthenticatedLayout from '../../components/authenticated-layout' + +function Agent() { + return ( + + + + ) +} + +export default React.memo(Agent) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/setting-built-in-tool.spec.tsx b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/setting-built-in-tool.spec.tsx index 083e93b086d..cd72279fe38 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/setting-built-in-tool.spec.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/setting-built-in-tool.spec.tsx @@ -186,6 +186,20 @@ describe('SettingBuiltInTool', () => { expect(screen.getByTestId('mock-form')).toBeInTheDocument() }) + it('should render a masked drawer with balanced vertical offsets', async () => { + const { baseElement } = renderComponent() + await waitFor(() => { + expect(screen.getByTestId('mock-form')).toBeInTheDocument() + }) + + expect(baseElement.querySelector('.bg-background-overlay')).toBeInTheDocument() + const drawerPopup = baseElement.querySelector('[role="dialog"]') + expect(drawerPopup).toHaveClass( + 'data-[swipe-direction=right]:top-6', + 'data-[swipe-direction=right]:bottom-6', + ) + }) + it('should call onSave with updated values when save button clicked', async () => { const { onSave } = renderComponent() await waitFor(() => expect(screen.getByTestId('mock-form')).toBeInTheDocument()) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx index ccfbd3be14b..113ef2a1c40 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx @@ -181,9 +181,9 @@ const SettingBuiltInTool: FC = ({ }} > - + - + {isLoading && } {!isLoading && ( diff --git a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx index 45647160a01..8b1d7b106f8 100644 --- a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx +++ b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx @@ -19,6 +19,7 @@ import { useInfiniteDatasets } from '@/service/knowledge/use-dataset' type ISelectDataSetProps = { isShow: boolean + modal?: boolean onClose: () => void selectedIds: string[] onSelect: (dataSet: DataSet[]) => void @@ -26,6 +27,7 @@ type ISelectDataSetProps = { const SelectDataSet: FC = ({ isShow, + modal, onClose, selectedIds, onSelect, @@ -90,8 +92,8 @@ const SelectDataSet: FC = ({ }, [handleClose]) return ( - - + + {t('feature.dataSet.selectTitle', { ns: 'appDebug' })} diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index ba11de128db..5350bb04474 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -30,6 +30,7 @@ import { RetrievalChangeTip, RetrievalSection } from './retrieval-section' type SettingsModalProps = { currentDataset: DataSet + height?: string onCancel: () => void onSave: (newDataset: DataSet) => void } @@ -44,6 +45,7 @@ const labelClass = ` const SettingsModal: FC = ({ currentDataset, + height = 'calc(100vh - 72px)', onCancel, onSave, }) => { @@ -186,9 +188,9 @@ const SettingsModal: FC = ({ return (
diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index 1010720e838..fa83560c0ce 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -8,10 +8,18 @@ import { useHover } from 'ahooks' import { cva } from 'class-variance-authority' import { init } from 'emoji-mart' import * as React from 'react' -import { useRef } from 'react' +import { useRef, useSyncExternalStore } from 'react' init({ data }) +const subscribeHydrationState = () => () => {} + +const useIsHydrated = () => useSyncExternalStore( + subscribeHydrationState, + () => true, + () => false, +) + type AppIconProps = { size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | 'xl' | 'xxl' rounded?: boolean @@ -105,9 +113,20 @@ const AppIcon: FC = ({ }) => { const isValidImageIcon = iconType === 'image' && imageUrl const emojiIcon = (icon && icon !== '') ? icon : '🤖' - const Icon = + const isHydrated = useIsHydrated() + const Icon = isHydrated ? : emojiIcon const wrapperRef = useRef(null) const isHovering = useHover(wrapperRef) + const handleKeyDown = (event: React.KeyboardEvent) => { + if (!onClick) + return + + if (event.key !== 'Enter' && event.key !== ' ') + return + + event.preventDefault() + onClick() + } return ( = ({ className={cn(appIconVariants({ size, rounded }), className)} style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }} onClick={onClick} + onKeyDown={onClick ? handleKeyDown : undefined} + role={onClick ? 'button' : undefined} + tabIndex={onClick ? 0 : undefined} > { isValidImageIcon diff --git a/web/app/components/base/features/new-feature-panel/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/__tests__/index.spec.tsx index 4947d2c7c01..7946523c7ba 100644 --- a/web/app/components/base/features/new-feature-panel/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/__tests__/index.spec.tsx @@ -57,6 +57,7 @@ const renderPanel = (props: Partial<{ onClose: () => void inWorkflow: boolean showFileUpload: boolean + showAnnotationReply: boolean }> = {}) => { return render( @@ -68,6 +69,7 @@ const renderPanel = (props: Partial<{ onClose={props.onClose ?? vi.fn()} inWorkflow={props.inWorkflow} showFileUpload={props.showFileUpload} + showAnnotationReply={props.showAnnotationReply} /> , ) @@ -191,5 +193,11 @@ describe('NewFeaturePanel', () => { expect(screen.queryByText(/feature\.annotation\.title/)).not.toBeInTheDocument() }) + + it('should not render AnnotationReply when showAnnotationReply is false', () => { + renderPanel({ isChatMode: true, inWorkflow: false, showAnnotationReply: false }) + + expect(screen.queryByText(/feature\.annotation\.title/)).not.toBeInTheDocument() + }) }) }) diff --git a/web/app/components/base/features/new-feature-panel/index.tsx b/web/app/components/base/features/new-feature-panel/index.tsx index 3c8f441521a..2cb21d9382c 100644 --- a/web/app/components/base/features/new-feature-panel/index.tsx +++ b/web/app/components/base/features/new-feature-panel/index.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react' import type { OnFeaturesChange } from '@/app/components/base/features/types' import type { InputVar } from '@/app/components/workflow/types' import type { PromptVariable } from '@/models/debug' @@ -26,9 +27,14 @@ type Props = Readonly<{ onClose: () => void inWorkflow?: boolean showFileUpload?: boolean + showModeration?: boolean + showAnnotationReply?: boolean promptVariables?: PromptVariable[] workflowVariables?: InputVar[] onAutoAddPromptVariable?: (variable: PromptVariable[]) => void + title?: ReactNode + description?: ReactNode + drawerClassName?: string }> const NewFeaturePanel = ({ @@ -39,9 +45,14 @@ const NewFeaturePanel = ({ onClose, inWorkflow = true, showFileUpload = true, + showModeration = true, + showAnnotationReply = true, promptVariables, workflowVariables, onAutoAddPromptVariable, + title, + description, + drawerClassName, }: Props) => { const { t } = useTranslation() const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text) @@ -52,13 +63,14 @@ const NewFeaturePanel = ({ show={show} onClose={onClose} inWorkflow={inWorkflow} + className={drawerClassName} >
{/* header */}
-
{t('common.features', { ns: 'workflow' })}
-
{t('common.featuresDescription', { ns: 'workflow' })}
+
{title ?? t('common.features', { ns: 'workflow' })}
+
{description ?? t('common.featuresDescription', { ns: 'workflow' })}
)} - {(isChatMode || !inWorkflow) && } - {!inWorkflow && isChatMode && ( + {showModeration && (isChatMode || !inWorkflow) && } + {showAnnotationReply && !inWorkflow && isChatMode && ( )}
diff --git a/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx b/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx index 4f55e6eb7b2..c6359f3713b 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/__tests__/moderation-setting-modal.spec.tsx @@ -302,6 +302,37 @@ describe('ModerationSettingModal', () => { })) }) + it('should save the latest preset response when content textarea changes', async () => { + const data: ModerationConfig = { + ...defaultData, + config: { + keywords: 'bad', + inputs_config: { enabled: true, preset_response: 'blocked' }, + outputs_config: { enabled: false, preset_response: '' }, + }, + } + await renderModal( + , + ) + + fireEvent.change(screen.getByRole('textbox', { name: /feature\.moderation\.modal\.content\.preset/ }), { + target: { value: 'updated blocked response' }, + }) + fireEvent.click(screen.getByText(/operation\.save/)) + + expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ + config: expect.objectContaining({ + inputs_config: expect.objectContaining({ + preset_response: 'updated blocked response', + }), + }), + })) + }) + it('should show api selector when api type is selected', async () => { await renderModal( { expect(mockSetShowAccountSettingModal).toHaveBeenCalled() - expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider' }) + expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ + payload: 'provider', + onCancelCallback: expect.any(Function), + }) }) it('should not save when OpenAI type is selected but not configured', async () => { diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-content.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-content.tsx index 9df107bf848..b008490ae6f 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-content.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-content.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import type { ModerationContentConfig } from '@/models/debug' import { Switch } from '@langgenius/dify-ui/switch' import { Textarea } from '@langgenius/dify-ui/textarea' +import { useState } from 'react' import { useTranslation } from 'react-i18next' type ModerationContentProps = { @@ -19,57 +20,71 @@ const ModerationContent: FC = ({ onConfigChange, }) => { const { t } = useTranslation() + const [presetResponse, setPresetResponse] = useState(config.preset_response || '') const handleConfigChange = (field: string, value: boolean | string) => { if (field === 'preset_response' && typeof value === 'string') value = value.slice(0, 100) - onConfigChange({ ...config, [field]: value }) + + onConfigChange({ + ...config, + preset_response: field === 'preset_response' ? value as string : presetResponse, + [field]: value, + }) + } + + const handlePresetResponseChange = (value: string) => { + const nextValue = value.slice(0, 100) + setPresetResponse(nextValue) + handleConfigChange('preset_response', nextValue) } return ( -
-
-
-
{title}
-
- { - info && ( -
{info}
- ) - } - handleConfigChange('enabled', v)} - /> -
+
+
+
{title}
+
+ { + info && ( +
{info}
+ ) + } + handleConfigChange('enabled', v)} + />
- { - config.enabled && showPreset && ( -
-
+
+ { + config.enabled && showPreset && ( +
+
+ {t('feature.moderation.modal.content.preset', { ns: 'appDebug' })} - {t('feature.moderation.modal.content.supportMarkdown', { ns: 'appDebug' })} -
- {/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */} -
-