From 6978ebfbef4d012a25ff8fb16ec112c68e1a89a3 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 6 Apr 2026 22:33:40 +0100 Subject: [PATCH 01/21] Migrate Custom Assistants to Agents with single-table architecture - Replace dual-table (assistant + chatflow) with single chatflow table for Agents. - Add /agents route as default landing page - Full configure page with dynamic field options from server node definitions - Dedicated agents RBAC permission group - Agent marketplace template type with view-only preview, type-validated export/import, and evaluation updates. --- .../agents/OpenAIAssistant/OpenAIAssistant.ts | 2 +- .../nodes/tools/ChatflowTool/ChatflowTool.ts | 4 +- packages/server/src/Interface.ts | 2 +- .../server/src/database/entities/ChatFlow.ts | 3 +- .../1775497538678-AddAgentsPermission.ts | 36 + .../src/database/migrations/mariadb/index.ts | 4 +- .../1775497538678-AddAgentsPermission.ts | 36 + .../src/database/migrations/mysql/index.ts | 4 +- .../1775497538678-AddAgentsPermission.ts | 36 + .../src/database/migrations/postgres/index.ts | 4 +- .../1775497538678-AddAgentsPermission.ts | 41 + .../src/database/migrations/sqlite/index.ts | 4 +- .../server/src/enterprise/rbac/Permissions.ts | 44 +- packages/server/src/routes/chatflows/index.ts | 14 +- .../server/src/services/chatflows/index.ts | 30 +- .../server/src/services/evaluations/index.ts | 15 +- .../src/services/export-import/index.ts | 4 +- .../server/src/services/marketplaces/index.ts | 42 +- packages/server/src/utils/buildChatflow.ts | 2 +- .../Sidebar/MenuList/NavItem/index.jsx | 13 +- packages/ui/src/menu-items/agentsettings.js | 30 +- packages/ui/src/menu-items/customassistant.js | 52 - packages/ui/src/menu-items/dashboard.js | 23 +- packages/ui/src/routes/DefaultRedirect.jsx | 10 +- packages/ui/src/routes/MainRoutes.jsx | 37 +- .../src/ui-component/button/AgentListMenu.jsx | 416 +++ .../src/ui-component/button/FlowListMenu.jsx | 2 +- .../ui-component/dialog/ExpandTextDialog.jsx | 10 +- .../dialog/ExportAsTemplateDialog.jsx | 2 + .../table/ExecutionsListTable.jsx | 2 +- packages/ui/src/utils/exportImport.js | 2 +- packages/ui/src/utils/genericHelper.js | 4 +- .../agentexecutions/ExecutionDetails.jsx | 10 +- .../ui/src/views/agentexecutions/index.jsx | 4 +- packages/ui/src/views/agentflowsv2/Canvas.jsx | 20 + .../views/agents/AgentConfigurePreview.jsx | 9 + packages/ui/src/views/agents/index.jsx | 429 +++ .../custom/AddCustomAssistantDialog.jsx | 8 +- .../CustomAssistantConfigurePreview.jsx | 2701 ++++++++++++----- .../custom/CustomAssistantLayout.jsx | 208 +- .../views/assistants/custom/toolAgentFlow.js | 336 -- packages/ui/src/views/assistants/index.jsx | 14 +- packages/ui/src/views/canvas/CanvasHeader.jsx | 2 +- packages/ui/src/views/canvas/index.jsx | 21 + packages/ui/src/views/chatbot/index.jsx | 4 +- packages/ui/src/views/chatflows/index.jsx | 2 +- packages/ui/src/views/docstore/index.jsx | 2 +- .../evaluations/CreateEvaluationDialog.jsx | 39 +- .../views/evaluations/EvalsResultDialog.jsx | 7 +- .../views/evaluations/EvaluationResult.jsx | 13 +- .../EvaluationResultSideDrawer.jsx | 3 +- .../src/views/evaluators/evaluatorConstant.js | 4 +- packages/ui/src/views/marketplaces/index.jsx | 74 +- packages/ui/src/views/settings/index.jsx | 21 +- 54 files changed, 3553 insertions(+), 1308 deletions(-) create mode 100644 packages/server/src/database/migrations/mariadb/1775497538678-AddAgentsPermission.ts create mode 100644 packages/server/src/database/migrations/mysql/1775497538678-AddAgentsPermission.ts create mode 100644 packages/server/src/database/migrations/postgres/1775497538678-AddAgentsPermission.ts create mode 100644 packages/server/src/database/migrations/sqlite/1775497538678-AddAgentsPermission.ts delete mode 100644 packages/ui/src/menu-items/customassistant.js create mode 100644 packages/ui/src/ui-component/button/AgentListMenu.jsx create mode 100644 packages/ui/src/views/agents/AgentConfigurePreview.jsx create mode 100644 packages/ui/src/views/agents/index.jsx delete mode 100644 packages/ui/src/views/assistants/custom/toolAgentFlow.js diff --git a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts index 314b5421f06..2643dac55ae 100644 --- a/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts +++ b/packages/components/nodes/agents/OpenAIAssistant/OpenAIAssistant.ts @@ -45,7 +45,7 @@ class OpenAIAssistant_Agents implements INode { this.icon = 'assistant.svg' this.description = `An agent that uses OpenAI Assistant API to pick the tool and args to call` this.badge = 'DEPRECATING' - this.deprecateMessage = 'OpenAI Assistant is deprecated and will be removed in a future release. Use Custom Assistant instead.' + this.deprecateMessage = 'OpenAI Assistant is deprecated and will be removed in a future release. Use Agent instead.' this.baseClasses = [this.type] this.inputs = [ { diff --git a/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts b/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts index fa9d463aec1..5e8e4cb6efe 100644 --- a/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts +++ b/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts @@ -141,8 +141,8 @@ class ChatflowTool_Tools implements INode { type = 'AgentflowV2' } else if (type === 'MULTIAGENT') { type = 'AgentflowV1' - } else if (type === 'ASSISTANT') { - type = 'Custom Assistant' + } else if (type === 'ASSISTANT' || type === 'AGENT') { + type = 'Agent' } else { type = 'Chatflow' } diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 60175e23528..8412dd924ce 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -16,7 +16,7 @@ import { UsageCacheManager } from './UsageCacheManager' export type MessageType = 'apiMessage' | 'userMessage' -export type ChatflowType = 'CHATFLOW' | 'MULTIAGENT' | 'ASSISTANT' | 'AGENTFLOW' +export type ChatflowType = 'CHATFLOW' | 'MULTIAGENT' | 'ASSISTANT' | 'AGENTFLOW' | 'AGENT' export type AssistantType = 'CUSTOM' | 'OPENAI' | 'AZURE' diff --git a/packages/server/src/database/entities/ChatFlow.ts b/packages/server/src/database/entities/ChatFlow.ts index d3561aa0017..edda3d75f33 100644 --- a/packages/server/src/database/entities/ChatFlow.ts +++ b/packages/server/src/database/entities/ChatFlow.ts @@ -6,7 +6,8 @@ export enum EnumChatflowType { CHATFLOW = 'CHATFLOW', AGENTFLOW = 'AGENTFLOW', MULTIAGENT = 'MULTIAGENT', - ASSISTANT = 'ASSISTANT' + ASSISTANT = 'ASSISTANT', + AGENT = 'AGENT' } @Entity() diff --git a/packages/server/src/database/migrations/mariadb/1775497538678-AddAgentsPermission.ts b/packages/server/src/database/migrations/mariadb/1775497538678-AddAgentsPermission.ts new file mode 100644 index 00000000000..9d65b201aa6 --- /dev/null +++ b/packages/server/src/database/migrations/mariadb/1775497538678-AddAgentsPermission.ts @@ -0,0 +1,36 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddAgentsPermission1775497538678 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Add agents:* permissions to roles that have corresponding assistants:* permissions + // Uses SQL REPLACE for O(1) bulk operations — safe for millions of rows + + await queryRunner.query( + `UPDATE \`role\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:view"', '"assistants:view","agents:view"') WHERE \`permissions\` LIKE '%assistants:view%' AND \`permissions\` NOT LIKE '%agents:view%';` + ) + await queryRunner.query( + `UPDATE \`role\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:create"', '"assistants:create","agents:create","agents:duplicate","agents:export","agents:import"') WHERE \`permissions\` LIKE '%assistants:create%' AND \`permissions\` NOT LIKE '%agents:create%';` + ) + await queryRunner.query( + `UPDATE \`role\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:update"', '"assistants:update","agents:update","agents:config","agents:domains"') WHERE \`permissions\` LIKE '%assistants:update%' AND \`permissions\` NOT LIKE '%agents:update%';` + ) + await queryRunner.query( + `UPDATE \`role\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:delete"', '"assistants:delete","agents:delete"') WHERE \`permissions\` LIKE '%assistants:delete%' AND \`permissions\` NOT LIKE '%agents:delete%';` + ) + + await queryRunner.query( + `UPDATE \`apikey\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:view"', '"assistants:view","agents:view"') WHERE \`permissions\` LIKE '%assistants:view%' AND \`permissions\` NOT LIKE '%agents:view%';` + ) + await queryRunner.query( + `UPDATE \`apikey\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:create"', '"assistants:create","agents:create","agents:duplicate","agents:export","agents:import"') WHERE \`permissions\` LIKE '%assistants:create%' AND \`permissions\` NOT LIKE '%agents:create%';` + ) + await queryRunner.query( + `UPDATE \`apikey\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:update"', '"assistants:update","agents:update","agents:config","agents:domains"') WHERE \`permissions\` LIKE '%assistants:update%' AND \`permissions\` NOT LIKE '%agents:update%';` + ) + await queryRunner.query( + `UPDATE \`apikey\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:delete"', '"assistants:delete","agents:delete"') WHERE \`permissions\` LIKE '%assistants:delete%' AND \`permissions\` NOT LIKE '%agents:delete%';` + ) + } + + public async down(): Promise {} +} diff --git a/packages/server/src/database/migrations/mariadb/index.ts b/packages/server/src/database/migrations/mariadb/index.ts index f9d3d5fdcd8..8beb4d0d928 100644 --- a/packages/server/src/database/migrations/mariadb/index.ts +++ b/packages/server/src/database/migrations/mariadb/index.ts @@ -54,6 +54,7 @@ import { AddSSOColumns1730519457880 } from '../../../enterprise/database/migrati import { AddPersonalWorkspace1734074497540 } from '../../../enterprise/database/migrations/mariadb/1734074497540-AddPersonalWorkspace' import { RefactorEnterpriseDatabase1737076223692 } from '../../../enterprise/database/migrations/mariadb/1737076223692-RefactorEnterpriseDatabase' import { ExecutionLinkWorkspaceId1746862866554 } from '../../../enterprise/database/migrations/mariadb/1746862866554-ExecutionLinkWorkspaceId' +import { AddAgentsPermission1775497538678 } from './1775497538678-AddAgentsPermission' export const mariadbMigrations = [ Init1693840429259, @@ -111,5 +112,6 @@ export const mariadbMigrations = [ AddChatFlowNameIndex1759424809984, FixDocumentStoreFileChunkLongText1765000000000, AddApiKeyPermission1765360298674, - AddReasonContentToChatMessage1764759496768 + AddReasonContentToChatMessage1764759496768, + AddAgentsPermission1775497538678 ] diff --git a/packages/server/src/database/migrations/mysql/1775497538678-AddAgentsPermission.ts b/packages/server/src/database/migrations/mysql/1775497538678-AddAgentsPermission.ts new file mode 100644 index 00000000000..9d65b201aa6 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1775497538678-AddAgentsPermission.ts @@ -0,0 +1,36 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddAgentsPermission1775497538678 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Add agents:* permissions to roles that have corresponding assistants:* permissions + // Uses SQL REPLACE for O(1) bulk operations — safe for millions of rows + + await queryRunner.query( + `UPDATE \`role\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:view"', '"assistants:view","agents:view"') WHERE \`permissions\` LIKE '%assistants:view%' AND \`permissions\` NOT LIKE '%agents:view%';` + ) + await queryRunner.query( + `UPDATE \`role\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:create"', '"assistants:create","agents:create","agents:duplicate","agents:export","agents:import"') WHERE \`permissions\` LIKE '%assistants:create%' AND \`permissions\` NOT LIKE '%agents:create%';` + ) + await queryRunner.query( + `UPDATE \`role\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:update"', '"assistants:update","agents:update","agents:config","agents:domains"') WHERE \`permissions\` LIKE '%assistants:update%' AND \`permissions\` NOT LIKE '%agents:update%';` + ) + await queryRunner.query( + `UPDATE \`role\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:delete"', '"assistants:delete","agents:delete"') WHERE \`permissions\` LIKE '%assistants:delete%' AND \`permissions\` NOT LIKE '%agents:delete%';` + ) + + await queryRunner.query( + `UPDATE \`apikey\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:view"', '"assistants:view","agents:view"') WHERE \`permissions\` LIKE '%assistants:view%' AND \`permissions\` NOT LIKE '%agents:view%';` + ) + await queryRunner.query( + `UPDATE \`apikey\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:create"', '"assistants:create","agents:create","agents:duplicate","agents:export","agents:import"') WHERE \`permissions\` LIKE '%assistants:create%' AND \`permissions\` NOT LIKE '%agents:create%';` + ) + await queryRunner.query( + `UPDATE \`apikey\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:update"', '"assistants:update","agents:update","agents:config","agents:domains"') WHERE \`permissions\` LIKE '%assistants:update%' AND \`permissions\` NOT LIKE '%agents:update%';` + ) + await queryRunner.query( + `UPDATE \`apikey\` SET \`permissions\` = REPLACE(\`permissions\`, '"assistants:delete"', '"assistants:delete","agents:delete"') WHERE \`permissions\` LIKE '%assistants:delete%' AND \`permissions\` NOT LIKE '%agents:delete%';` + ) + } + + public async down(): Promise {} +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index a22168aefcf..0dab0f9e176 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -55,6 +55,7 @@ import { AddSSOColumns1730519457880 } from '../../../enterprise/database/migrati import { AddPersonalWorkspace1734074497540 } from '../../../enterprise/database/migrations/mysql/1734074497540-AddPersonalWorkspace' import { RefactorEnterpriseDatabase1737076223692 } from '../../../enterprise/database/migrations/mysql/1737076223692-RefactorEnterpriseDatabase' import { ExecutionLinkWorkspaceId1746862866554 } from '../../../enterprise/database/migrations/mysql/1746862866554-ExecutionLinkWorkspaceId' +import { AddAgentsPermission1775497538678 } from './1775497538678-AddAgentsPermission' export const mysqlMigrations = [ Init1693840429259, @@ -113,5 +114,6 @@ export const mysqlMigrations = [ AddChatFlowNameIndex1759424828558, FixDocumentStoreFileChunkLongText1765000000000, AddApiKeyPermission1765360298674, - AddReasonContentToChatMessage1764759496768 + AddReasonContentToChatMessage1764759496768, + AddAgentsPermission1775497538678 ] diff --git a/packages/server/src/database/migrations/postgres/1775497538678-AddAgentsPermission.ts b/packages/server/src/database/migrations/postgres/1775497538678-AddAgentsPermission.ts new file mode 100644 index 00000000000..a2d776a98d8 --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1775497538678-AddAgentsPermission.ts @@ -0,0 +1,36 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddAgentsPermission1775497538678 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Add agents:* permissions to roles that have corresponding assistants:* permissions + // Uses SQL replace() for O(1) bulk operations — safe for millions of rows + + await queryRunner.query( + `UPDATE "role" SET "permissions" = REPLACE("permissions", '"assistants:view"', '"assistants:view","agents:view"') WHERE "permissions" LIKE '%assistants:view%' AND "permissions" NOT LIKE '%agents:view%';` + ) + await queryRunner.query( + `UPDATE "role" SET "permissions" = REPLACE("permissions", '"assistants:create"', '"assistants:create","agents:create","agents:duplicate","agents:export","agents:import"') WHERE "permissions" LIKE '%assistants:create%' AND "permissions" NOT LIKE '%agents:create%';` + ) + await queryRunner.query( + `UPDATE "role" SET "permissions" = REPLACE("permissions", '"assistants:update"', '"assistants:update","agents:update","agents:config","agents:domains"') WHERE "permissions" LIKE '%assistants:update%' AND "permissions" NOT LIKE '%agents:update%';` + ) + await queryRunner.query( + `UPDATE "role" SET "permissions" = REPLACE("permissions", '"assistants:delete"', '"assistants:delete","agents:delete"') WHERE "permissions" LIKE '%assistants:delete%' AND "permissions" NOT LIKE '%agents:delete%';` + ) + + await queryRunner.query( + `UPDATE "apikey" SET "permissions" = REPLACE("permissions", '"assistants:view"', '"assistants:view","agents:view"') WHERE "permissions" LIKE '%assistants:view%' AND "permissions" NOT LIKE '%agents:view%';` + ) + await queryRunner.query( + `UPDATE "apikey" SET "permissions" = REPLACE("permissions", '"assistants:create"', '"assistants:create","agents:create","agents:duplicate","agents:export","agents:import"') WHERE "permissions" LIKE '%assistants:create%' AND "permissions" NOT LIKE '%agents:create%';` + ) + await queryRunner.query( + `UPDATE "apikey" SET "permissions" = REPLACE("permissions", '"assistants:update"', '"assistants:update","agents:update","agents:config","agents:domains"') WHERE "permissions" LIKE '%assistants:update%' AND "permissions" NOT LIKE '%agents:update%';` + ) + await queryRunner.query( + `UPDATE "apikey" SET "permissions" = REPLACE("permissions", '"assistants:delete"', '"assistants:delete","agents:delete"') WHERE "permissions" LIKE '%assistants:delete%' AND "permissions" NOT LIKE '%agents:delete%';` + ) + } + + public async down(): Promise {} +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index 9303033e02b..a4d41573ec7 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -53,6 +53,7 @@ import { AddSSOColumns1730519457880 } from '../../../enterprise/database/migrati import { AddPersonalWorkspace1734074497540 } from '../../../enterprise/database/migrations/postgres/1734074497540-AddPersonalWorkspace' import { RefactorEnterpriseDatabase1737076223692 } from '../../../enterprise/database/migrations/postgres/1737076223692-RefactorEnterpriseDatabase' import { ExecutionLinkWorkspaceId1746862866554 } from '../../../enterprise/database/migrations/postgres/1746862866554-ExecutionLinkWorkspaceId' +import { AddAgentsPermission1775497538678 } from './1775497538678-AddAgentsPermission' export const postgresMigrations = [ Init1693891895163, @@ -109,5 +110,6 @@ export const postgresMigrations = [ AddTextToSpeechToChatFlow1759419194331, AddChatFlowNameIndex1759424903973, AddApiKeyPermission1765360298674, - AddReasonContentToChatMessage1764759496768 + AddReasonContentToChatMessage1764759496768, + AddAgentsPermission1775497538678 ] diff --git a/packages/server/src/database/migrations/sqlite/1775497538678-AddAgentsPermission.ts b/packages/server/src/database/migrations/sqlite/1775497538678-AddAgentsPermission.ts new file mode 100644 index 00000000000..bb9a5d01e60 --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1775497538678-AddAgentsPermission.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddAgentsPermission1775497538678 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Add agents:* permissions to roles that have corresponding assistants:* permissions + // Uses SQL REPLACE for O(1) bulk operations — safe for millions of rows + + // assistants:view → agents:view + await queryRunner.query( + `UPDATE "role" SET "permissions" = REPLACE("permissions", '"assistants:view"', '"assistants:view","agents:view"') WHERE "permissions" LIKE '%assistants:view%' AND "permissions" NOT LIKE '%agents:view%';` + ) + // assistants:create → agents:create,duplicate,export,import + await queryRunner.query( + `UPDATE "role" SET "permissions" = REPLACE("permissions", '"assistants:create"', '"assistants:create","agents:create","agents:duplicate","agents:export","agents:import"') WHERE "permissions" LIKE '%assistants:create%' AND "permissions" NOT LIKE '%agents:create%';` + ) + // assistants:update → agents:update,config,domains + await queryRunner.query( + `UPDATE "role" SET "permissions" = REPLACE("permissions", '"assistants:update"', '"assistants:update","agents:update","agents:config","agents:domains"') WHERE "permissions" LIKE '%assistants:update%' AND "permissions" NOT LIKE '%agents:update%';` + ) + // assistants:delete → agents:delete + await queryRunner.query( + `UPDATE "role" SET "permissions" = REPLACE("permissions", '"assistants:delete"', '"assistants:delete","agents:delete"') WHERE "permissions" LIKE '%assistants:delete%' AND "permissions" NOT LIKE '%agents:delete%';` + ) + + // Same for apikey table + await queryRunner.query( + `UPDATE "apikey" SET "permissions" = REPLACE("permissions", '"assistants:view"', '"assistants:view","agents:view"') WHERE "permissions" LIKE '%assistants:view%' AND "permissions" NOT LIKE '%agents:view%';` + ) + await queryRunner.query( + `UPDATE "apikey" SET "permissions" = REPLACE("permissions", '"assistants:create"', '"assistants:create","agents:create","agents:duplicate","agents:export","agents:import"') WHERE "permissions" LIKE '%assistants:create%' AND "permissions" NOT LIKE '%agents:create%';` + ) + await queryRunner.query( + `UPDATE "apikey" SET "permissions" = REPLACE("permissions", '"assistants:update"', '"assistants:update","agents:update","agents:config","agents:domains"') WHERE "permissions" LIKE '%assistants:update%' AND "permissions" NOT LIKE '%agents:update%';` + ) + await queryRunner.query( + `UPDATE "apikey" SET "permissions" = REPLACE("permissions", '"assistants:delete"', '"assistants:delete","agents:delete"') WHERE "permissions" LIKE '%assistants:delete%' AND "permissions" NOT LIKE '%agents:delete%';` + ) + } + + public async down(): Promise {} +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 90b42a2475f..959b6eb2839 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -51,6 +51,7 @@ import { AddSSOColumns1730519457880 } from '../../../enterprise/database/migrati import { AddPersonalWorkspace1734074497540 } from '../../../enterprise/database/migrations/sqlite/1734074497540-AddPersonalWorkspace' import { RefactorEnterpriseDatabase1737076223692 } from '../../../enterprise/database/migrations/sqlite/1737076223692-RefactorEnterpriseDatabase' import { ExecutionLinkWorkspaceId1746862866554 } from '../../../enterprise/database/migrations/sqlite/1746862866554-ExecutionLinkWorkspaceId' +import { AddAgentsPermission1775497538678 } from './1775497538678-AddAgentsPermission' export const sqliteMigrations = [ Init1693835579790, @@ -105,5 +106,6 @@ export const sqliteMigrations = [ AddTextToSpeechToChatFlow1759419136055, AddChatFlowNameIndex1759424923093, AddApiKeyPermission1765360298674, - AddReasonContentToChatMessage1764759496768 + AddReasonContentToChatMessage1764759496768, + AddAgentsPermission1775497538678 ] diff --git a/packages/server/src/enterprise/rbac/Permissions.ts b/packages/server/src/enterprise/rbac/Permissions.ts index 1fb9e6fccbf..2fa56e57cf4 100644 --- a/packages/server/src/enterprise/rbac/Permissions.ts +++ b/packages/server/src/enterprise/rbac/Permissions.ts @@ -5,17 +5,17 @@ export class Permissions { // auditCategory.addPermission(new Permission('auditLogs:view', 'View Audit Logs')) // this.categories.push(auditCategory) - const chatflowsCategory = new PermissionCategory('chatflows') - chatflowsCategory.addPermission(new Permission('chatflows:view', 'View', true, true, true)) - chatflowsCategory.addPermission(new Permission('chatflows:create', 'Create', true, true, true)) - chatflowsCategory.addPermission(new Permission('chatflows:update', 'Update', true, true, true)) - chatflowsCategory.addPermission(new Permission('chatflows:duplicate', 'Duplicate', true, true, true)) - chatflowsCategory.addPermission(new Permission('chatflows:delete', 'Delete', true, true, true)) - chatflowsCategory.addPermission(new Permission('chatflows:export', 'Export', true, true, true)) - chatflowsCategory.addPermission(new Permission('chatflows:import', 'Import', true, true, true)) - chatflowsCategory.addPermission(new Permission('chatflows:config', 'Edit Configuration', true, true, true)) - chatflowsCategory.addPermission(new Permission('chatflows:domains', 'Allowed Domains', true, true, true)) - this.categories.push(chatflowsCategory) + const agentsCategory = new PermissionCategory('agents') + agentsCategory.addPermission(new Permission('agents:view', 'View', true, true, true)) + agentsCategory.addPermission(new Permission('agents:create', 'Create', true, true, true)) + agentsCategory.addPermission(new Permission('agents:update', 'Update', true, true, true)) + agentsCategory.addPermission(new Permission('agents:duplicate', 'Duplicate', true, true, true)) + agentsCategory.addPermission(new Permission('agents:delete', 'Delete', true, true, true)) + agentsCategory.addPermission(new Permission('agents:export', 'Export', true, true, true)) + agentsCategory.addPermission(new Permission('agents:import', 'Import', true, true, true)) + agentsCategory.addPermission(new Permission('agents:config', 'Edit Configuration', true, true, true)) + agentsCategory.addPermission(new Permission('agents:domains', 'Allowed Domains', true, true, true)) + this.categories.push(agentsCategory) const agentflowsCategory = new PermissionCategory('agentflows') agentflowsCategory.addPermission(new Permission('agentflows:view', 'View', true, true, true)) @@ -29,6 +29,23 @@ export class Permissions { agentflowsCategory.addPermission(new Permission('agentflows:domains', 'Allowed Domains', true, true, true)) this.categories.push(agentflowsCategory) + const executionsCategory = new PermissionCategory('executions') + executionsCategory.addPermission(new Permission('executions:view', 'View', true, true, true)) + executionsCategory.addPermission(new Permission('executions:delete', 'Delete', true, true, true)) + this.categories.push(executionsCategory) + + const chatflowsCategory = new PermissionCategory('chatflows') + chatflowsCategory.addPermission(new Permission('chatflows:view', 'View', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:create', 'Create', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:update', 'Update', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:duplicate', 'Duplicate', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:delete', 'Delete', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:export', 'Export', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:import', 'Import', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:config', 'Edit Configuration', true, true, true)) + chatflowsCategory.addPermission(new Permission('chatflows:domains', 'Allowed Domains', true, true, true)) + this.categories.push(chatflowsCategory) + const toolsCategory = new PermissionCategory('tools') toolsCategory.addPermission(new Permission('tools:view', 'View', true, true, true)) toolsCategory.addPermission(new Permission('tools:create', 'Create', true, true, true)) @@ -86,11 +103,6 @@ export class Permissions { datasetsCategory.addPermission(new Permission('datasets:delete', 'Delete', false, true, true)) this.categories.push(datasetsCategory) - const executionsCategory = new PermissionCategory('executions') - executionsCategory.addPermission(new Permission('executions:view', 'View', true, true, true)) - executionsCategory.addPermission(new Permission('executions:delete', 'Delete', true, true, true)) - this.categories.push(executionsCategory) - const evaluatorsCategory = new PermissionCategory('evaluators') evaluatorsCategory.addPermission(new Permission('evaluators:view', 'View', false, true, true)) evaluatorsCategory.addPermission(new Permission('evaluators:create', 'Create', false, true, true)) diff --git a/packages/server/src/routes/chatflows/index.ts b/packages/server/src/routes/chatflows/index.ts index 5d2ec2609ec..d8dcef3c0a3 100644 --- a/packages/server/src/routes/chatflows/index.ts +++ b/packages/server/src/routes/chatflows/index.ts @@ -6,19 +6,21 @@ const router = express.Router() // CREATE router.post( '/', - checkAnyPermission('chatflows:create,chatflows:update,agentflows:create,agentflows:update'), + checkAnyPermission('chatflows:create,chatflows:update,agentflows:create,agentflows:update,agents:create,agents:update'), chatflowsController.saveChatflow ) // READ router.get( '/', - checkAnyPermission('chatflows:view,chatflows:update,agentflows:view,agentflows:update'), + checkAnyPermission('chatflows:view,chatflows:update,agentflows:view,agentflows:update,agents:view,agents:update'), chatflowsController.getAllChatflows ) router.get( ['/', '/:id'], - checkAnyPermission('chatflows:view,chatflows:update,chatflows:delete,agentflows:view,agentflows:update,agentflows:delete'), + checkAnyPermission( + 'chatflows:view,chatflows:update,chatflows:delete,agentflows:view,agentflows:update,agentflows:delete,agents:view,agents:update,agents:delete' + ), chatflowsController.getChatflowById ) router.get(['/apikey/', '/apikey/:apikey'], chatflowsController.getChatflowByApiKey) @@ -26,17 +28,17 @@ router.get(['/apikey/', '/apikey/:apikey'], chatflowsController.getChatflowByApi // UPDATE router.put( ['/', '/:id'], - checkAnyPermission('chatflows:create,chatflows:update,agentflows:create,agentflows:update'), + checkAnyPermission('chatflows:create,chatflows:update,agentflows:create,agentflows:update,agents:create,agents:update'), chatflowsController.updateChatflow ) // DELETE -router.delete(['/', '/:id'], checkAnyPermission('chatflows:delete,agentflows:delete'), chatflowsController.deleteChatflow) +router.delete(['/', '/:id'], checkAnyPermission('chatflows:delete,agentflows:delete,agents:delete'), chatflowsController.deleteChatflow) // CHECK FOR CHANGE router.get( '/has-changed/:id/:lastUpdatedDateTime', - checkAnyPermission('chatflows:update,agentflows:update'), + checkAnyPermission('chatflows:update,agentflows:update,agents:update'), chatflowsController.checkIfChatflowHasChanged ) diff --git a/packages/server/src/services/chatflows/index.ts b/packages/server/src/services/chatflows/index.ts index 697b757d6c7..844288d0848 100644 --- a/packages/server/src/services/chatflows/index.ts +++ b/packages/server/src/services/chatflows/index.ts @@ -54,7 +54,7 @@ const checkIfChatflowIsValidForStreaming = async (chatflowId: string): Promise workspace.id) - const chatflowsCount = await appServer.AppDataSource.getRepository(ChatFlow).countBy({ - type, - workspaceId: In(workspaceIds) - }) + let chatflowsCount: number + if (type === 'AGENT') { + // Count both ASSISTANT (legacy) and AGENT (new) types + chatflowsCount = await appServer.AppDataSource.getRepository(ChatFlow).countBy([ + { type: 'ASSISTANT' as ChatflowType, workspaceId: In(workspaceIds) }, + { type: 'AGENT' as ChatflowType, workspaceId: In(workspaceIds) } + ]) + } else { + chatflowsCount = await appServer.AppDataSource.getRepository(ChatFlow).countBy({ + type, + workspaceId: In(workspaceIds) + }) + } return chatflowsCount } catch (error) { @@ -204,6 +216,14 @@ const getAllChatflowsCount = async (type?: ChatflowType, workspaceId?: string): try { const appServer = getRunningExpressApp() if (type) { + if (type === 'AGENT') { + // Count both ASSISTANT (legacy) and AGENT (new) types + const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).countBy([ + { type: 'ASSISTANT' as ChatflowType, ...getWorkspaceSearchOptions(workspaceId) }, + { type: 'AGENT' as ChatflowType, ...getWorkspaceSearchOptions(workspaceId) } + ]) + return dbResponse + } const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).countBy({ type, ...getWorkspaceSearchOptions(workspaceId) diff --git a/packages/server/src/services/evaluations/index.ts b/packages/server/src/services/evaluations/index.ts index 1acbfc3b894..907a4e101e6 100644 --- a/packages/server/src/services/evaluations/index.ts +++ b/packages/server/src/services/evaluations/index.ts @@ -509,7 +509,7 @@ const isOutdated = async (id: string, workspaceId: string) => { // check for backward compatibility, as previous versions did not the types in additionalConfig if (chatflowTypes && chatflowTypes.length >= 0) { if (chatflowTypes[i] === 'Custom Assistant') { - // if the chatflow type is custom assistant, then we should NOT check in the chatflows table + // Legacy custom assistant records are in the assistant table, skip chatflow table check continue } } @@ -530,7 +530,12 @@ const isOutdated = async (id: string, workspaceId: string) => { returnObj.chatflows.push({ chatflowName: chatflowNames[i], chatflowId: chatflowIds[i], - chatflowType: chatflow.type === 'AGENTFLOW' ? 'Agentflow v2' : 'Chatflow', + chatflowType: + chatflow.type === 'AGENTFLOW' + ? 'Agentflow v2' + : chatflow.type === 'AGENT' || chatflow.type === 'ASSISTANT' + ? 'Agent' + : 'Chatflow', isOutdated: true }) } @@ -539,7 +544,7 @@ const isOutdated = async (id: string, workspaceId: string) => { if (chatflowTypes && chatflowTypes.length > 0) { for (let i = 0; i < chatflowIds.length; i++) { if (chatflowTypes[i] !== 'Custom Assistant') { - // if the chatflow type is NOT custom assistant, then bail out for this item + // Only check assistant table for legacy Custom Assistant records continue } const assistant = await appServer.AppDataSource.getRepository(Assistant).findOneBy({ @@ -548,7 +553,7 @@ const isOutdated = async (id: string, workspaceId: string) => { }) if (!assistant) { returnObj.errors.push({ - message: `Custom Assistant ${chatflowNames[i]} not found`, + message: `Agent ${chatflowNames[i]} not found`, id: chatflowIds[i] }) isOutdated = true @@ -559,7 +564,7 @@ const isOutdated = async (id: string, workspaceId: string) => { returnObj.chatflows.push({ chatflowName: chatflowNames[i], chatflowId: chatflowIds[i], - chatflowType: 'Custom Assistant', + chatflowType: 'Agent', isOutdated: true }) } diff --git a/packages/server/src/services/export-import/index.ts b/packages/server/src/services/export-import/index.ts index 3e729bb5fa6..6eddd7d717b 100644 --- a/packages/server/src/services/export-import/index.ts +++ b/packages/server/src/services/export-import/index.ts @@ -105,7 +105,7 @@ const exportData = async (exportInput: ExportInput, activeWorkspaceId: string): exportInput.assistantCustom === true ? await assistantsService.getAllAssistants(activeWorkspaceId, 'CUSTOM') : [] let AssistantFlow: ChatFlow[] | { data: ChatFlow[]; total: number } = - exportInput.assistantCustom === true ? await chatflowService.getAllChatflows('ASSISTANT', activeWorkspaceId) : [] + exportInput.assistantCustom === true ? await chatflowService.getAllChatflows('AGENT', activeWorkspaceId) : [] AssistantFlow = 'data' in AssistantFlow ? AssistantFlow.data : AssistantFlow let AssistantOpenAI: Assistant[] = @@ -683,7 +683,7 @@ const importData = async (importData: ExportData, orgId: string, activeWorkspace if (importData.AssistantFlow.length > 0) { importData.AssistantFlow = reduceSpaceForChatflowFlowData(importData.AssistantFlow) importData.AssistantFlow = insertWorkspaceId(importData.AssistantFlow, activeWorkspaceId) - const existingChatflowCount = await chatflowService.getAllChatflowsCountByOrganization('ASSISTANT', orgId) + const existingChatflowCount = await chatflowService.getAllChatflowsCountByOrganization('AGENT', orgId) const newChatflowCount = importData.AssistantFlow.length await checkUsageLimit( 'flows', diff --git a/packages/server/src/services/marketplaces/index.ts b/packages/server/src/services/marketplaces/index.ts index ef50ad687a9..0cd774721ff 100644 --- a/packages/server/src/services/marketplaces/index.ts +++ b/packages/server/src/services/marketplaces/index.ts @@ -110,21 +110,35 @@ const getAllTemplates = async () => { } templates.push(template) }) + // Scan Agent templates + const agentsDir = path.join(__dirname, '..', '..', '..', 'marketplaces', 'agents') + if (fs.existsSync(agentsDir)) { + const agentJsons = fs.readdirSync(agentsDir).filter((file) => path.extname(file) === '.json') + agentJsons.forEach((file) => { + const filePath = path.join(agentsDir, file) + const fileData = fs.readFileSync(filePath) + const fileDataObj = JSON.parse(fileData.toString()) + const template = { + id: uuidv4(), + templateName: file.split('.json')[0], + flowData: fileData.toString(), + badge: fileDataObj?.badge, + framework: fileDataObj?.framework, + usecases: fileDataObj?.usecases, + categories: getCategories(fileDataObj), + type: 'Agent', + description: fileDataObj?.description || '' + } + templates.push(template) + }) + } + const sortedTemplates = templates.sort((a, b) => { - // Prioritize AgentflowV2 templates first - if (a.type === 'AgentflowV2' && b.type !== 'AgentflowV2') { - return -1 - } - if (b.type === 'AgentflowV2' && a.type !== 'AgentflowV2') { - return 1 - } - // Put Tool templates last - if (a.type === 'Tool' && b.type !== 'Tool') { - return 1 - } - if (b.type === 'Tool' && a.type !== 'Tool') { - return -1 - } + // Prioritize Agent and AgentflowV2 templates first + const priority: Record = { Agent: 0, AgentflowV2: 1, Chatflow: 2, Agentflow: 3, Tool: 4 } + const aPriority = priority[a.type] ?? 3 + const bPriority = priority[b.type] ?? 3 + if (aPriority !== bPriority) return aPriority - bPriority // For same types, sort alphabetically by templateName return a.templateName.localeCompare(b.templateName) }) diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts index c54c009c43a..b72f842b03e 100644 --- a/packages/server/src/utils/buildChatflow.ts +++ b/packages/server/src/utils/buildChatflow.ts @@ -477,7 +477,7 @@ export const executeFlow = async ({ } } - const isAgentFlowV2 = chatflow.type === 'AGENTFLOW' + const isAgentFlowV2 = chatflow.type === 'AGENTFLOW' || chatflow.type === 'AGENT' if (isAgentFlowV2) { return executeAgentFlow({ componentNodes, diff --git a/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx b/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx index 10445554bd0..3cd99194578 100644 --- a/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx +++ b/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types' import { forwardRef, useEffect } from 'react' -import { Link } from 'react-router-dom' +import { Link, useLocation } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' // material-ui @@ -19,6 +19,7 @@ import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord' const NavItem = ({ item, level, navType, onClick, onUploadFile }) => { const theme = useTheme() const dispatch = useDispatch() + const location = useLocation() const customization = useSelector((state) => state.customization) const matchesSM = useMediaQuery(theme.breakpoints.down('lg')) @@ -77,23 +78,23 @@ const NavItem = ({ item, level, navType, onClick, onUploadFile }) => { } } - // active menu item on page load + // active menu item on page load and route change useEffect(() => { if (navType === 'MENU') { - const currentIndex = document.location.pathname + const currentIndex = location.pathname .toString() .split('/') .findIndex((id) => id === item.id) if (currentIndex > -1) { dispatch({ type: MENU_OPEN, id: item.id }) } - if (!document.location.pathname.toString().split('/')[1]) { - itemHandler('chatflows') + if (!location.pathname.toString().split('/')[1]) { + itemHandler('agents') } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [navType]) + }, [navType, location.pathname]) return ( { // Define the order of routes to check (based on the menu order in dashboard.js) const routesToCheck = [ + { component: Agents, permission: 'agents:view' }, { component: Chatflows, permission: 'chatflows:view' }, { component: Agentflows, permission: 'agentflows:view' }, { component: Executions, permission: 'executions:view' }, @@ -68,14 +70,14 @@ export const DefaultRedirect = () => { return } - // For open source, show chatflows (no permission checks) + // For open source, show agents (no permission checks) if (isOpenSource) { - return + return } - // For global admins, show chatflows (they have access to everything) + // For global admins, show agents (they have access to everything) if (isGlobal) { - return + return } // Check each route in order and return the first accessible component diff --git a/packages/ui/src/routes/MainRoutes.jsx b/packages/ui/src/routes/MainRoutes.jsx index 544433db603..679340652ab 100644 --- a/packages/ui/src/routes/MainRoutes.jsx +++ b/packages/ui/src/routes/MainRoutes.jsx @@ -6,6 +6,7 @@ import Loadable from '@/ui-component/loading/Loadable' import { RequireAuth } from '@/routes/RequireAuth' import { DefaultRedirect } from '@/routes/DefaultRedirect' +import { Navigate } from 'react-router-dom' // chatflows routing const Chatflows = Loadable(lazy(() => import('@/views/chatflows'))) @@ -22,11 +23,15 @@ const APIKey = Loadable(lazy(() => import('@/views/apikey'))) // tools routing const Tools = Loadable(lazy(() => import('@/views/tools'))) +// agents routing (custom assistants rebranded) +const Agents = Loadable(lazy(() => import('@/views/agents'))) +const AgentConfigurePreview = Loadable(lazy(() => import('@/views/agents/AgentConfigurePreview'))) + // assistants routing const Assistants = Loadable(lazy(() => import('@/views/assistants'))) const OpenAIAssistantLayout = Loadable(lazy(() => import('@/views/assistants/openai/OpenAIAssistantLayout'))) const CustomAssistantLayout = Loadable(lazy(() => import('@/views/assistants/custom/CustomAssistantLayout'))) -const CustomAssistantConfigurePreview = Loadable(lazy(() => import('@/views/assistants/custom/CustomAssistantConfigurePreview'))) +// CustomAssistantConfigurePreview is now used via AgentConfigurePreview wrapper at /agents/:id // credentials routing const Credentials = Loadable(lazy(() => import('@/views/credentials'))) @@ -128,6 +133,30 @@ const MainRoutes = { ) }, + { + path: '/agents', + element: ( + + + + ) + }, + { + path: '/agents/:id', + element: ( + + + + ) + }, + { + path: '/marketplace/agents/:id', + element: ( + + + + ) + }, { path: '/assistants', element: ( @@ -146,11 +175,7 @@ const MainRoutes = { }, { path: '/assistants/custom/:id', - element: ( - - - - ) + element: }, { path: '/assistants/openai', diff --git a/packages/ui/src/ui-component/button/AgentListMenu.jsx b/packages/ui/src/ui-component/button/AgentListMenu.jsx new file mode 100644 index 00000000000..0f3d9b07ab1 --- /dev/null +++ b/packages/ui/src/ui-component/button/AgentListMenu.jsx @@ -0,0 +1,416 @@ +import { useState } from 'react' +import { useDispatch } from 'react-redux' +import PropTypes from 'prop-types' + +import { styled, alpha } from '@mui/material/styles' +import Menu from '@mui/material/Menu' +import { PermissionMenuItem } from '@/ui-component/button/RBACButtons' +import EditIcon from '@mui/icons-material/Edit' +import Divider from '@mui/material/Divider' +import FileCopyIcon from '@mui/icons-material/FileCopy' +import FileDownloadIcon from '@mui/icons-material/Downloading' +import FileDeleteIcon from '@mui/icons-material/Delete' +import FileCategoryIcon from '@mui/icons-material/Category' +import PictureInPictureAltIcon from '@mui/icons-material/PictureInPictureAlt' +import ThumbsUpDownOutlinedIcon from '@mui/icons-material/ThumbsUpDownOutlined' +import VpnLockOutlinedIcon from '@mui/icons-material/VpnLockOutlined' +import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined' +import ExportTemplateOutlinedIcon from '@mui/icons-material/BookmarksOutlined' +import Button from '@mui/material/Button' +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' +import { IconX } from '@tabler/icons-react' + +import chatflowsApi from '@/api/chatflows' + +import useApi from '@/hooks/useApi' +import useConfirm from '@/hooks/useConfirm' +import { uiBaseURL } from '@/store/constant' +import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' + +import SaveChatflowDialog from '@/ui-component/dialog/SaveChatflowDialog' +import TagDialog from '@/ui-component/dialog/TagDialog' +import StarterPromptsDialog from '@/ui-component/dialog/StarterPromptsDialog' +import ChatFeedbackDialog from '@/ui-component/dialog/ChatFeedbackDialog' +import AllowedDomainsDialog from '@/ui-component/dialog/AllowedDomainsDialog' +import SpeechToTextDialog from '@/ui-component/dialog/SpeechToTextDialog' +import ExportAsTemplateDialog from '@/ui-component/dialog/ExportAsTemplateDialog' + +import { generateExportFlowData } from '@/utils/genericHelper' +import useNotifier from '@/utils/useNotifier' + +const StyledMenu = styled((props) => ( + +))(({ theme }) => ({ + '& .MuiPaper-root': { + borderRadius: 6, + marginTop: theme.spacing(1), + minWidth: 180, + boxShadow: + 'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px', + '& .MuiMenu-list': { + padding: '4px 0' + }, + '& .MuiMenuItem-root': { + '& .MuiSvgIcon-root': { + fontSize: 18, + color: theme.palette.text.secondary, + marginRight: theme.spacing(1.5) + }, + '&:active': { + backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity) + } + } + } +})) + +export default function AgentListMenu({ agent, setError, onRefresh }) { + const { confirm } = useConfirm() + const dispatch = useDispatch() + const updateChatflowApi = useApi(chatflowsApi.updateChatflow) + + useNotifier() + const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) + const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + + const [anchorEl, setAnchorEl] = useState(null) + const open = Boolean(anchorEl) + const [flowDialogOpen, setFlowDialogOpen] = useState(false) + const [exportTemplateDialogOpen, setExportTemplateDialogOpen] = useState(false) + const [exportTemplateDialogProps, setExportTemplateDialogProps] = useState({}) + const [categoryDialogOpen, setCategoryDialogOpen] = useState(false) + const [categoryDialogProps, setCategoryDialogProps] = useState({}) + const [conversationStartersDialogOpen, setConversationStartersDialogOpen] = useState(false) + const [conversationStartersDialogProps, setConversationStartersDialogProps] = useState({}) + const [chatFeedbackDialogOpen, setChatFeedbackDialogOpen] = useState(false) + const [chatFeedbackDialogProps, setChatFeedbackDialogProps] = useState({}) + const [allowedDomainsDialogOpen, setAllowedDomainsDialogOpen] = useState(false) + const [allowedDomainsDialogProps, setAllowedDomainsDialogProps] = useState({}) + const [speechToTextDialogOpen, setSpeechToTextDialogOpen] = useState(false) + const [speechToTextDialogProps, setSpeechToTextDialogProps] = useState({}) + + const handleClick = (event) => { + event.stopPropagation() + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + const refreshAgents = () => { + if (onRefresh) onRefresh() + } + + const handleRename = () => { + setAnchorEl(null) + setFlowDialogOpen(true) + } + + const saveRename = async (newName) => { + try { + await updateChatflowApi.request(agent.id, { name: newName }) + refreshAgents() + } catch (error) { + if (setError) setError(error) + enqueueSnackbar({ + message: typeof error.response?.data === 'object' ? error.response.data.message : error.response?.data, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + const getFlowData = () => { + return agent.flowData || null + } + + const handleDuplicate = async () => { + setAnchorEl(null) + try { + const flowData = getFlowData() + if (!flowData) return + const saveObj = { + name: `${agent.name} (Copy)`, + flowData: flowData, + type: 'AGENT' + } + const createResp = await chatflowsApi.createNewChatflow(saveObj) + if (createResp.data) { + window.open(`${uiBaseURL}/agents/${createResp.data.id}`, '_blank') + } + } catch (e) { + console.error(e) + enqueueSnackbar({ + message: `Failed to duplicate agent: ${e.message || 'Unknown error'}`, + options: { key: new Date().getTime() + Math.random(), variant: 'error' } + }) + } + } + + const handleExport = () => { + setAnchorEl(null) + try { + const flowDataStr = getFlowData() + if (!flowDataStr) return + const flowData = JSON.parse(flowDataStr) + const dataStr = JSON.stringify(generateExportFlowData(flowData, 'AGENT'), null, 2) + const blob = new Blob([dataStr], { type: 'application/json' }) + const dataUri = URL.createObjectURL(blob) + const exportFileDefaultName = `${agent.name || 'Agent'} Agent.json` + const linkElement = document.createElement('a') + linkElement.setAttribute('href', dataUri) + linkElement.setAttribute('download', exportFileDefaultName) + linkElement.click() + } catch (e) { + console.error(e) + } + } + + const handleExportTemplate = () => { + setAnchorEl(null) + setExportTemplateDialogProps({ chatflow: agent }) + setExportTemplateDialogOpen(true) + } + + const handleStarterPrompts = () => { + setAnchorEl(null) + setConversationStartersDialogProps({ + title: 'Starter Prompts - ' + agent.name, + chatflow: agent + }) + setConversationStartersDialogOpen(true) + } + + const handleChatFeedback = () => { + setAnchorEl(null) + setChatFeedbackDialogProps({ + title: 'Chat Feedback - ' + agent.name, + chatflow: agent + }) + setChatFeedbackDialogOpen(true) + } + + const handleAllowedDomains = () => { + setAnchorEl(null) + setAllowedDomainsDialogProps({ + title: 'Allowed Domains - ' + agent.name, + chatflow: agent + }) + setAllowedDomainsDialogOpen(true) + } + + const handleSpeechToText = () => { + setAnchorEl(null) + setSpeechToTextDialogProps({ + title: 'Speech To Text - ' + agent.name, + chatflow: agent + }) + setSpeechToTextDialogOpen(true) + } + + const handleCategory = () => { + setAnchorEl(null) + if (agent.category) { + setCategoryDialogProps({ + category: agent.category.split(';') + }) + } + setCategoryDialogOpen(true) + } + + const saveCategory = async (categories) => { + setCategoryDialogOpen(false) + const categoryTags = categories.join(';') + try { + await updateChatflowApi.request(agent.id, { category: categoryTags }) + await refreshAgents() + } catch (error) { + if (setError) setError(error) + enqueueSnackbar({ + message: typeof error.response?.data === 'object' ? error.response.data.message : error.response?.data, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + + const handleDelete = async () => { + setAnchorEl(null) + const confirmPayload = { + title: `Delete`, + description: `Delete Agent ${agent.name}?`, + confirmButtonName: 'Delete', + cancelButtonName: 'Cancel' + } + const isConfirmed = await confirm(confirmPayload) + + if (isConfirmed) { + try { + await chatflowsApi.deleteChatflow(agent.id) + refreshAgents() + } catch (error) { + if (setError) setError(error) + enqueueSnackbar({ + message: typeof error.response?.data === 'object' ? error.response.data.message : error.response?.data, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + } + } + + return ( +
+ + + + + Rename + + + + Duplicate + + + + Export + + + + Save As Template + + + + + Starter Prompts + + + + Chat Feedback + + + + Allowed Domains + + + + Speech To Text + + + + Update Category + + + + + Delete + + + setFlowDialogOpen(false)} + onConfirm={saveRename} + /> + setCategoryDialogOpen(false)} + onSubmit={saveCategory} + /> + setConversationStartersDialogOpen(false)} + onConfirm={refreshAgents} + /> + setChatFeedbackDialogOpen(false)} + onConfirm={refreshAgents} + /> + setAllowedDomainsDialogOpen(false)} + onConfirm={refreshAgents} + /> + setSpeechToTextDialogOpen(false)} + onConfirm={refreshAgents} + /> + {exportTemplateDialogOpen && ( + setExportTemplateDialogOpen(false)} + /> + )} +
+ ) +} + +AgentListMenu.propTypes = { + agent: PropTypes.object, + setError: PropTypes.func, + onRefresh: PropTypes.func +} diff --git a/packages/ui/src/ui-component/button/FlowListMenu.jsx b/packages/ui/src/ui-component/button/FlowListMenu.jsx index 198b7f8476f..1b221640b82 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.jsx +++ b/packages/ui/src/ui-component/button/FlowListMenu.jsx @@ -323,7 +323,7 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, isAgentflowV2, s setAnchorEl(null) try { const flowData = JSON.parse(chatflow.flowData) - let dataStr = JSON.stringify(generateExportFlowData(flowData), null, 2) + let dataStr = JSON.stringify(generateExportFlowData(flowData, chatflow.type), null, 2) //let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) const blob = new Blob([dataStr], { type: 'application/json' }) const dataUri = URL.createObjectURL(blob) diff --git a/packages/ui/src/ui-component/dialog/ExpandTextDialog.jsx b/packages/ui/src/ui-component/dialog/ExpandTextDialog.jsx index 934eb5e6f6c..ba512dd5b24 100644 --- a/packages/ui/src/ui-component/dialog/ExpandTextDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ExpandTextDialog.jsx @@ -118,7 +118,7 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onInputHintDialogClicke borderColor: theme.palette.grey['500'], borderRadius: '12px', height: '100%', - maxHeight: languageType === 'js' ? 'calc(100vh - 250px)' : 'calc(100vh - 220px)', + maxHeight: inputParam.type === 'code' ? 'calc(100vh - 250px)' : 'calc(100vh - 220px)', overflowX: 'hidden', backgroundColor: 'white' }} @@ -126,19 +126,19 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onInputHintDialogClicke setInputValue(code)} /> diff --git a/packages/ui/src/ui-component/dialog/ExportAsTemplateDialog.jsx b/packages/ui/src/ui-component/dialog/ExportAsTemplateDialog.jsx index 53951768237..ba23962e752 100644 --- a/packages/ui/src/ui-component/dialog/ExportAsTemplateDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ExportAsTemplateDialog.jsx @@ -50,6 +50,8 @@ const ExportAsTemplateDialog = ({ show, dialogProps, onCancel }) => { setFlowType('Agentflow') } else if (dialogProps.chatflow.type === 'CHATFLOW') { setFlowType('Chatflow') + } else if (dialogProps.chatflow.type === 'AGENT' || dialogProps.chatflow.type === 'ASSISTANT') { + setFlowType('Agent') } } diff --git a/packages/ui/src/ui-component/table/ExecutionsListTable.jsx b/packages/ui/src/ui-component/table/ExecutionsListTable.jsx index 1b7dd68f996..e2592df7197 100644 --- a/packages/ui/src/ui-component/table/ExecutionsListTable.jsx +++ b/packages/ui/src/ui-component/table/ExecutionsListTable.jsx @@ -185,7 +185,7 @@ export const ExecutionsListTable = ({ data, isLoading, onExecutionRowClick, onSe handleRequestSort('name')}> - Agentflow + Agents Session diff --git a/packages/ui/src/utils/exportImport.js b/packages/ui/src/utils/exportImport.js index a9361a7faac..c3fb4120af7 100644 --- a/packages/ui/src/utils/exportImport.js +++ b/packages/ui/src/utils/exportImport.js @@ -22,7 +22,7 @@ const sanitizeTool = (Tool) => { const sanitizeChatflow = (ChatFlow) => { try { return ChatFlow.map((chatFlow) => { - const sanitizeFlowData = generateExportFlowData(JSON.parse(chatFlow.flowData)) + const sanitizeFlowData = generateExportFlowData(JSON.parse(chatFlow.flowData), chatFlow.type) return { id: chatFlow.id, name: chatFlow.name, diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index ac834c77f19..b90f83eddc3 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -567,12 +567,13 @@ const _removeCredentialId = (obj) => { const newObj = {} for (const [key, value] of Object.entries(obj)) { if (key === 'FLOWISE_CREDENTIAL_ID') continue + if (key === 'credential') continue newObj[key] = _removeCredentialId(value) } return newObj } -export const generateExportFlowData = (flowData) => { +export const generateExportFlowData = (flowData, type) => { const nodes = flowData.nodes const edges = flowData.edges @@ -620,6 +621,7 @@ export const generateExportFlowData = (flowData) => { nodes, edges } + if (type) exportJson.type = type return exportJson } diff --git a/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx b/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx index c89a1130f01..0ae7f1414fe 100644 --- a/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx +++ b/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx @@ -746,7 +746,15 @@ export const ExecutionDetails = ({ open, isPublic, execution, metadata, onClose, variant='outlined' label={localMetadata?.agentflow?.name || localMetadata?.agentflow?.id || 'Go to AgentFlow'} className={'button'} - onClick={() => window.open(`/v2/agentcanvas/${localMetadata?.agentflow?.id}`, '_blank')} + onClick={() => { + const agentflowType = localMetadata?.agentflow?.type + const agentflowId = localMetadata?.agentflow?.id + if (agentflowType === 'AGENT' || agentflowType === 'ASSISTANT') { + window.open(`/agents/${agentflowId}`, '_blank') + } else { + window.open(`/v2/agentcanvas/${agentflowId}`, '_blank') + } + }} /> )} diff --git a/packages/ui/src/views/agentexecutions/index.jsx b/packages/ui/src/views/agentexecutions/index.jsx index 2a6e366509b..fab4bfc928f 100644 --- a/packages/ui/src/views/agentexecutions/index.jsx +++ b/packages/ui/src/views/agentexecutions/index.jsx @@ -236,7 +236,7 @@ const AgentExecutions = () => { ) : ( - + {/* Filter Section */} @@ -319,7 +319,7 @@ const AgentExecutions = () => { handleFilterChange('agentflowName', e.target.value)} size='small' diff --git a/packages/ui/src/views/agentflowsv2/Canvas.jsx b/packages/ui/src/views/agentflowsv2/Canvas.jsx index 07bf57df51b..32177efa0d0 100644 --- a/packages/ui/src/views/agentflowsv2/Canvas.jsx +++ b/packages/ui/src/views/agentflowsv2/Canvas.jsx @@ -160,6 +160,26 @@ const AgentflowCanvas = () => { const handleLoadFlow = (file) => { try { const flowData = JSON.parse(file) + if (flowData.type && flowData.type !== 'AGENTFLOW') { + enqueueSnackbar({ + message: `Invalid file: expected AGENTFLOW type but got ${flowData.type}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + return + } + delete flowData.type const nodes = flowData.nodes || [] setNodes(nodes) diff --git a/packages/ui/src/views/agents/AgentConfigurePreview.jsx b/packages/ui/src/views/agents/AgentConfigurePreview.jsx new file mode 100644 index 00000000000..2a46f0f52a9 --- /dev/null +++ b/packages/ui/src/views/agents/AgentConfigurePreview.jsx @@ -0,0 +1,9 @@ +import CustomAssistantConfigurePreview from '@/views/assistants/custom/CustomAssistantConfigurePreview' + +// ==============================|| AGENT CONFIGURE PREVIEW ||============================== // + +const AgentConfigurePreview = () => { + return +} + +export default AgentConfigurePreview diff --git a/packages/ui/src/views/agents/index.jsx b/packages/ui/src/views/agents/index.jsx new file mode 100644 index 00000000000..3405e3d5ab0 --- /dev/null +++ b/packages/ui/src/views/agents/index.jsx @@ -0,0 +1,429 @@ +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import moment from 'moment' + +// material-ui +import { + Box, + Paper, + Skeleton, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TableSortLabel, + ToggleButton, + ToggleButtonGroup, + Tooltip, + Typography +} from '@mui/material' +import { useTheme, styled } from '@mui/material/styles' +import { tableCellClasses } from '@mui/material/TableCell' +import { useSelector } from 'react-redux' + +// project imports +import ViewHeader from '@/layout/MainLayout/ViewHeader' +import MainCard from '@/ui-component/cards/MainCard' +import ItemCard from '@/ui-component/cards/ItemCard' +import { baseURL, gridSpacing } from '@/store/constant' +import AssistantEmptySVG from '@/assets/images/assistant_empty.svg' +import ErrorBoundary from '@/ErrorBoundary' +import { StyledPermissionButton } from '@/ui-component/button/RBACButtons' +import AgentListMenu from '@/ui-component/button/AgentListMenu' +import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' + +// API +import chatflowsApi from '@/api/chatflows' + +// Hooks +import useApi from '@/hooks/useApi' + +// icons +import { IconPlus, IconLayoutGrid, IconList } from '@tabler/icons-react' + +// ==============================|| HELPERS ||============================== // + +// Extract agent info from chatflow's flowData +const parseAgentFromFlowData = (agent) => { + try { + if (!agent.flowData) return { name: agent.name, instruction: '', modelName: '' } + const flowData = JSON.parse(agent.flowData) + const agentNode = flowData.nodes?.find((n) => n.data?.name === 'agentAgentflow') + if (agentNode) { + const inputs = agentNode.data?.inputs || {} + const instruction = inputs.agentMessages?.[0]?.content || '' + const modelName = inputs.agentModel || '' + return { name: agent.name, instruction, modelName } + } + // Old format: try toolAgent node + const toolAgentNode = flowData.nodes?.find((n) => n.data?.name === 'toolAgent') + if (toolAgentNode) { + const instruction = toolAgentNode.data?.inputs?.systemMessage || '' + const chatModelNode = flowData.nodes?.find((n) => n.data?.category === 'Chat Models') + const modelName = chatModelNode?.data?.name || '' + return { name: agent.name, instruction, modelName } + } + return { name: agent.name, instruction: '', modelName: '' } + } catch { + return { name: agent.name || 'Untitled', instruction: '', modelName: '' } + } +} + +// ==============================|| STYLED TABLE COMPONENTS ||============================== // + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + borderColor: theme.palette.grey[900] + 25, + + [`&.${tableCellClasses.head}`]: { + color: theme.palette.grey[900] + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14, + height: 64 + } +})) + +const StyledTableRow = styled(TableRow)(() => ({ + '&:last-child td, &:last-child th': { + border: 0 + } +})) + +// ==============================|| AGENTS ||============================== // + +const Agents = () => { + const navigate = useNavigate() + const theme = useTheme() + const customization = useSelector((state) => state.customization) + + const getAllAgentsApi = useApi(chatflowsApi.getAllAgentflows) + + const [isLoading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [agents, setAgents] = useState([]) + + const [search, setSearch] = useState('') + const [view, setView] = useState(localStorage.getItem('agentDisplayStyle') || 'card') + const [order, setOrder] = useState(localStorage.getItem('agent_order') || 'desc') + const [orderBy, setOrderBy] = useState(localStorage.getItem('agent_orderBy') || 'updatedDate') + + const onSearchChange = (event) => { + setSearch(event.target.value) + } + + const handleChange = (event, nextView) => { + if (nextView === null) return + localStorage.setItem('agentDisplayStyle', nextView) + setView(nextView) + } + + const handleRequestSort = (property) => { + const isAsc = orderBy === property && order === 'asc' + const newOrder = isAsc ? 'desc' : 'asc' + setOrder(newOrder) + setOrderBy(property) + localStorage.setItem('agent_order', newOrder) + localStorage.setItem('agent_orderBy', property) + } + + const addNew = () => { + navigate('/agents/new') + } + + function filterAgents(agent) { + if (!search) return true + return agent.name && agent.name.toLowerCase().indexOf(search.toLowerCase()) > -1 + } + + const getImages = (agent) => { + const images = [] + const parsed = parseAgentFromFlowData(agent) + if (parsed.modelName) { + images.push({ imageSrc: `${baseURL}/api/v1/node-icon/${parsed.modelName}` }) + } + return images + } + + const getInstruction = (agent) => { + return parseAgentFromFlowData(agent).instruction + } + + const getModelName = (agent) => { + return parseAgentFromFlowData(agent).modelName + } + + const getSortedData = (data) => { + if (!data) return [] + return [...data].filter(filterAgents).sort((a, b) => { + if (orderBy === 'name') { + return order === 'asc' ? (a.name || '').localeCompare(b.name || '') : (b.name || '').localeCompare(a.name || '') + } else if (orderBy === 'updatedDate') { + return order === 'asc' + ? new Date(a.updatedDate) - new Date(b.updatedDate) + : new Date(b.updatedDate) - new Date(a.updatedDate) + } + return 0 + }) + } + + const refreshAgents = () => { + getAllAgentsApi.request('AGENT') + } + + useEffect(() => { + refreshAgents() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + setLoading(getAllAgentsApi.loading) + }, [getAllAgentsApi.loading]) + + useEffect(() => { + if (getAllAgentsApi.error) setError(getAllAgentsApi.error) + }, [getAllAgentsApi.error]) + + // Set agents from chatflows API (returns both ASSISTANT and AGENT types) + useEffect(() => { + const agentList = getAllAgentsApi.data?.data || getAllAgentsApi.data || [] + setAgents(agentList) + }, [getAllAgentsApi.data]) + + const total = agents.length + + return ( + <> + + {error ? ( + + ) : ( + + + + + + + + + + + } + > + Add New + + + + {isLoading && ( + + + + + + )} + {!isLoading && total > 0 && ( + <> + {!view || view === 'card' ? ( + + {agents.filter(filterAgents).map((agent, index) => ( + navigate(`/agents/${agent.id}`)} + /> + ))} + + ) : ( + + + + + + handleRequestSort('name')} + > + Name + + + Model + Instruction + + handleRequestSort('updatedDate')} + > + Last Modified + + + Actions + + + + {getSortedData(agents).map((agent, index) => { + const images = getImages(agent) + return ( + navigate(`/agents/${agent.id}`)} + > + + + + {agent.name || 'Untitled'} + + + + + {images.length > 0 && ( + + + + + + )} + + + + {getInstruction(agent) || ''} + + + + + {moment(agent.updatedDate).format('MMMM D, YYYY')} + + + e.stopPropagation()}> + + + + ) + })} + +
+
+ )} + + )} + {!isLoading && total === 0 && ( + + + AssistantEmptySVG + +
No Agents Added Yet
+
+ )} +
+ )} +
+ + + ) +} + +export default Agents diff --git a/packages/ui/src/views/assistants/custom/AddCustomAssistantDialog.jsx b/packages/ui/src/views/assistants/custom/AddCustomAssistantDialog.jsx index 20c1b79f28f..10062682c0e 100644 --- a/packages/ui/src/views/assistants/custom/AddCustomAssistantDialog.jsx +++ b/packages/ui/src/views/assistants/custom/AddCustomAssistantDialog.jsx @@ -18,7 +18,7 @@ import { StyledButton } from '@/ui-component/button/StyledButton' import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' // Icons -import { IconX, IconFiles } from '@tabler/icons-react' +import { IconX, IconRobot } from '@tabler/icons-react' // API import assistantsApi from '@/api/assistants' @@ -58,7 +58,7 @@ const AddCustomAssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => const createResp = await assistantsApi.createNewAssistant(obj) if (createResp.data) { enqueueSnackbar({ - message: 'New Custom Assistant created.', + message: 'New Agent created.', options: { key: new Date().getTime() + Math.random(), variant: 'success', @@ -73,7 +73,7 @@ const AddCustomAssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => } } catch (err) { enqueueSnackbar({ - message: `Failed to add new Custom Assistant: ${ + message: `Failed to add new Agent: ${ typeof err.response.data === 'object' ? err.response.data.message : err.response.data }`, options: { @@ -102,7 +102,7 @@ const AddCustomAssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => >
- + {dialogProps.title}
diff --git a/packages/ui/src/views/assistants/custom/CustomAssistantConfigurePreview.jsx b/packages/ui/src/views/assistants/custom/CustomAssistantConfigurePreview.jsx index 057241529ac..63f148b4ed9 100644 --- a/packages/ui/src/views/assistants/custom/CustomAssistantConfigurePreview.jsx +++ b/packages/ui/src/views/assistants/custom/CustomAssistantConfigurePreview.jsx @@ -1,7 +1,7 @@ import { cloneDeep, set } from 'lodash' import { memo, useEffect, useState, useRef } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { useNavigate, useParams } from 'react-router-dom' +import { useNavigate, useParams, useLocation } from 'react-router-dom' import { FullPageChat } from 'flowise-embed-react' import PropTypes from 'prop-types' @@ -10,7 +10,26 @@ import useApi from '@/hooks/useApi' import useConfirm from '@/hooks/useConfirm' // Material-UI -import { IconButton, Avatar, ButtonBase, Toolbar, Box, Button, Grid, OutlinedInput, Stack, Typography } from '@mui/material' +import { + IconButton, + Avatar, + ButtonBase, + Toolbar, + Box, + Button, + Grid, + OutlinedInput, + Stack, + Typography, + Checkbox, + FormControlLabel, + FormGroup, + Chip, + Accordion, + AccordionSummary, + AccordionDetails +} from '@mui/material' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { useTheme } from '@mui/material/styles' import { IconCode, @@ -20,7 +39,11 @@ import { IconX, IconTrash, IconWand, - IconArrowsMaximize + IconArrowsMaximize, + IconEdit, + IconCheck, + IconUpload, + IconCopy } from '@tabler/icons-react' // Project import @@ -29,6 +52,7 @@ import { BackdropLoader } from '@/ui-component/loading/BackdropLoader' import DocStoreInputHandler from '@/views/docstore/DocStoreInputHandler' import { Dropdown } from '@/ui-component/dropdown/Dropdown' import { StyledFab } from '@/ui-component/button/StyledFab' +import { StyledButton } from '@/ui-component/button/StyledButton' import ErrorBoundary from '@/ErrorBoundary' import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser' import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown' @@ -38,9 +62,11 @@ import ChatflowConfigurationDialog from '@/ui-component/dialog/ChatflowConfigura import ViewLeadsDialog from '@/ui-component/dialog/ViewLeadsDialog' import Settings from '@/views/settings' import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' +import { CodeEditor } from '@/ui-component/editor/CodeEditor' import PromptGeneratorDialog from '@/ui-component/dialog/PromptGeneratorDialog' import { Available } from '@/ui-component/rbac/available' import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog' +import ExportAsTemplateDialog from '@/ui-component/dialog/ExportAsTemplateDialog' import { SwitchInput } from '@/ui-component/switch/Switch' // API @@ -50,13 +76,12 @@ import nodesApi from '@/api/nodes' import documentstoreApi from '@/api/documentstore' // Const -import { baseURL } from '@/store/constant' +import { baseURL, uiBaseURL } from '@/store/constant' import { SET_CHATFLOW, closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' // Utils -import { initNode, showHideInputParams } from '@/utils/genericHelper' +import { initNode, showHideInputParams, generateExportFlowData } from '@/utils/genericHelper' import useNotifier from '@/utils/useNotifier' -import { toolAgentFlow } from './toolAgentFlow' // ===========================|| CustomAssistantConfigurePreview ||=========================== // @@ -75,33 +100,92 @@ MemoizedFullPageChat.propTypes = { chatflow: PropTypes.object } -const CustomAssistantConfigurePreview = () => { +// Helper to extract options from agentNodeDef inputParams by param name +const getParamOptions = (agentNodeDef, paramName) => { + if (!agentNodeDef?.inputParams) return [] + const param = agentNodeDef.inputParams.find((p) => p.name === paramName) + return param?.options || [] +} + +// Helper to build the built-in tools map from agentNodeDef — keyed by the model name in the `show` condition +const getBuiltInToolsMap = (agentNodeDef) => { + if (!agentNodeDef?.inputParams) return {} + const map = {} + const builtInParams = agentNodeDef.inputParams.filter((p) => p.name?.startsWith('agentToolsBuiltIn') && p.type === 'multiOptions') + for (const param of builtInParams) { + // The `show` condition maps to the model component name, e.g. { agentModel: 'chatOpenAI' } + const modelName = param.show?.agentModel + if (modelName) { + map[modelName] = { + paramName: param.name, + options: param.options || [] + } + } + } + return map +} + +// Helper to extract structured output type options from the agentStructuredOutput array definition +const getStructuredOutputTypeOptions = (agentNodeDef) => { + if (!agentNodeDef?.inputParams) return [] + const param = agentNodeDef.inputParams.find((p) => p.name === 'agentStructuredOutput') + if (!param?.array) return [] + const typeField = param.array.find((a) => a.name === 'type') + return typeField?.options || [] +} + +const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSISTANT' }) => { const navigate = useNavigate() const theme = useTheme() const settingsRef = useRef() + const loadAgentInputRef = useRef() const canvas = useSelector((state) => state.canvas) const customization = useSelector((state) => state.customization) - const getSpecificAssistantApi = useApi(assistantsApi.getSpecificAssistant) const getChatModelsApi = useApi(assistantsApi.getChatModels) const getDocStoresApi = useApi(assistantsApi.getDocStores) const getToolsApi = useApi(assistantsApi.getTools) const getSpecificChatflowApi = useApi(chatflowsApi.getSpecificChatflow) - const { id: customAssistantId } = useParams() + const { id: routeId } = useParams() + const location = useLocation() + const isNewAgent = !routeId || routeId === 'new' + const isTemplatePreview = !!location.state?.templateData + const templateData = location.state?.templateData + + // chatflowId is always from the route param (chatflow table ID) + const [chatflowId, setChatflowId] = useState(isNewAgent ? null : routeId) const [chatModelsComponents, setChatModelsComponents] = useState([]) const [chatModelsOptions, setChatModelsOptions] = useState([]) const [selectedChatModel, setSelectedChatModel] = useState({}) - const [selectedCustomAssistant, setSelectedCustomAssistant] = useState({}) + const [agentName, setAgentName] = useState('New Agent') const [customAssistantInstruction, setCustomAssistantInstruction] = useState('You are helpful assistant') - const [customAssistantFlowId, setCustomAssistantFlowId] = useState() const [documentStoreOptions, setDocumentStoreOptions] = useState([]) const [selectedDocumentStores, setSelectedDocumentStores] = useState([]) const [toolComponents, setToolComponents] = useState([]) const [toolOptions, setToolOptions] = useState([]) const [selectedTools, setSelectedTools] = useState([]) + const [vectorStoreOptions, setVectorStoreOptions] = useState([]) + const [vectorStoreComponents, setVectorStoreComponents] = useState([]) + const [embeddingModelOptions, setEmbeddingModelOptions] = useState([]) + const [embeddingModelComponents, setEmbeddingModelComponents] = useState([]) + const [knowledgeVSEmbeddings, setKnowledgeVSEmbeddings] = useState([]) + + // Built-in tools — dynamic map keyed by param name (e.g. 'agentToolsBuiltInOpenAI': ['web_search_preview']) + const [builtInTools, setBuiltInTools] = useState({}) + + const [enableMemory, setEnableMemory] = useState(true) + const [memoryType, setMemoryType] = useState('allMessages') + const [memoryWindowSize, setMemoryWindowSize] = useState(20) + const [memoryMaxTokenLimit, setMemoryMaxTokenLimit] = useState(2000) + + const [structuredOutput, setStructuredOutput] = useState([]) + + const [agentNodeDef, setAgentNodeDef] = useState(null) + const [startNodeDef, setStartNodeDef] = useState(null) + const [apiDialogOpen, setAPIDialogOpen] = useState(false) const [apiDialogProps, setAPIDialogProps] = useState({}) const [viewMessagesDialogOpen, setViewMessagesDialogOpen] = useState(false) @@ -111,6 +195,8 @@ const CustomAssistantConfigurePreview = () => { const [chatflowConfigurationDialogOpen, setChatflowConfigurationDialogOpen] = useState(false) const [chatflowConfigurationDialogProps, setChatflowConfigurationDialogProps] = useState({}) const [isSettingsOpen, setSettingsOpen] = useState(false) + const [exportAsTemplateDialogOpen, setExportAsTemplateDialogOpen] = useState(false) + const [exportAsTemplateDialogProps, setExportAsTemplateDialogProps] = useState({}) const [assistantPromptGeneratorDialogOpen, setAssistantPromptGeneratorDialogOpen] = useState(false) const [assistantPromptGeneratorDialogProps, setAssistantPromptGeneratorDialogProps] = useState({}) const [showExpandDialog, setShowExpandDialog] = useState(false) @@ -118,6 +204,22 @@ const CustomAssistantConfigurePreview = () => { const [loading, setLoading] = useState(false) const [loadingAssistant, setLoadingAssistant] = useState(true) + const [isEditingName, setIsEditingName] = useState(false) + const [editingNameValue, setEditingNameValue] = useState('') + + const saveAgentName = async (newName) => { + if (!newName || !newName.trim()) return + setAgentName(newName) + setIsEditingName(false) + // Persist to database immediately if the agent already exists + if (!isNewAgent && chatflowId) { + try { + await chatflowsApi.updateChatflow(chatflowId, { name: newName }) + } catch (e) { + console.error('Failed to save agent name', e) + } + } + } const [error, setError] = useState(null) const dispatch = useDispatch() @@ -150,6 +252,52 @@ const CustomAssistantConfigurePreview = () => { }) } + const handleVSDataChange = + (itemIndex) => + ({ inputParam, newValue }) => { + setKnowledgeVSEmbeddings((prev) => { + const updated = [...prev] + const item = { ...updated[itemIndex] } + if (item.vectorStoreNode) { + item.vectorStoreNode = { ...item.vectorStoreNode } + item.vectorStoreNode.inputs = { ...item.vectorStoreNode.inputs, [inputParam.name]: newValue } + item.vectorStoreNode.inputParams = showHideInputParams(item.vectorStoreNode) + } + updated[itemIndex] = item + return updated + }) + } + + const handleEmbeddingDataChange = + (itemIndex) => + ({ inputParam, newValue }) => { + setKnowledgeVSEmbeddings((prev) => { + const updated = [...prev] + const item = { ...updated[itemIndex] } + if (item.embeddingNode) { + item.embeddingNode = { ...item.embeddingNode } + item.embeddingNode.inputs = { ...item.embeddingNode.inputs, [inputParam.name]: newValue } + item.embeddingNode.inputParams = showHideInputParams(item.embeddingNode) + } + updated[itemIndex] = item + return updated + }) + } + + const initVectorStoreNode = (componentName, index) => { + const foundComponent = vectorStoreComponents.find((c) => c.name === componentName) + if (!foundComponent) return null + const clonedComponent = cloneDeep(foundComponent) + return initNode(clonedComponent, `${componentName}_vs_${index}`) + } + + const initEmbeddingNode = (componentName, index) => { + const foundComponent = embeddingModelComponents.find((c) => c.name === componentName) + if (!foundComponent) return null + const clonedComponent = cloneDeep(foundComponent) + return initNode(clonedComponent, `${componentName}_emb_${index}`) + } + const displayWarning = () => { enqueueSnackbar({ message: 'Please fill in all mandatory fields.', @@ -208,11 +356,15 @@ const CustomAssistantConfigurePreview = () => { const checkMandatoryFields = () => { let canSubmit = true + if (!agentName || !agentName.trim()) { + canSubmit = false + } + if (!selectedChatModel || !selectedChatModel.name) { canSubmit = false } - canSubmit = checkInputParamsMandatory() + if (canSubmit) canSubmit = checkInputParamsMandatory() // check if any of the description is empty if (selectedDocumentStores.length > 0) { @@ -230,267 +382,163 @@ const CustomAssistantConfigurePreview = () => { return canSubmit } - const onSaveAndProcess = async () => { - if (checkMandatoryFields()) { - setLoading(true) - const flowData = await prepareConfig() - if (!flowData) return - const saveObj = { - id: customAssistantId, - name: selectedCustomAssistant.name, - flowData: JSON.stringify(flowData), - type: 'ASSISTANT' - } - try { - let saveResp - if (!customAssistantFlowId) { - saveResp = await chatflowsApi.createNewChatflow(saveObj) - } else { - saveResp = await chatflowsApi.updateChatflow(customAssistantFlowId, saveObj) - } - - if (saveResp.data) { - setCustomAssistantFlowId(saveResp.data.id) - dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) - - const assistantDetails = { - ...selectedCustomAssistant, - chatModel: selectedChatModel, - instruction: customAssistantInstruction, - flowId: saveResp.data.id, - documentStores: selectedDocumentStores, - tools: selectedTools - } - - const saveAssistantResp = await assistantsApi.updateAssistant(customAssistantId, { - details: JSON.stringify(assistantDetails) - }) + // ==============================|| Build Config ||============================== // - if (saveAssistantResp.data) { - setLoading(false) - enqueueSnackbar({ - message: 'Assistant saved successfully', - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) - } - }) - } - } - } catch (error) { - setLoading(false) - enqueueSnackbar({ - message: `Failed to save assistant: ${ - typeof error.response.data === 'object' ? error.response.data.message : error.response.data - }`, - options: { - key: new Date().getTime() + Math.random(), - variant: 'error', - action: (key) => ( - - ) - } - }) - } + const buildModelConfig = () => { + const config = { + credential: selectedChatModel.credential || '', + agentModel: selectedChatModel.name } + // Copy all model input params + if (selectedChatModel.inputs) { + Object.keys(selectedChatModel.inputs).forEach((key) => { + config[key] = selectedChatModel.inputs[key] + }) + } + return config } - const addTools = async (toolAgentId) => { - const nodes = [] - const edges = [] - - for (let i = 0; i < selectedTools.length; i++) { - try { - const tool = selectedTools[i] - const toolId = `${tool.name}_${i}` - const toolNodeData = cloneDeep(tool) - set(toolNodeData, 'inputs', tool.inputs) - - const toolNodeObj = { - id: toolId, - data: { - ...toolNodeData, - id: toolId - } + const buildToolsArray = () => { + return selectedTools + .filter((tool) => tool && Object.keys(tool).length > 0) + .map((tool) => { + const toolConfig = { + credential: tool.credential || '', + agentSelectedTool: tool.name } - nodes.push(toolNodeObj) - - const toolEdge = { - source: toolId, - sourceHandle: `${toolId}-output-${tool.name}-Tool`, - target: toolAgentId, - targetHandle: `${toolAgentId}-input-tools-Tool`, - type: 'buttonedge', - id: `${toolId}-${toolId}-output-${tool.name}-Tool-${toolAgentId}-${toolAgentId}-input-tools-Tool` + if (tool.inputs) { + Object.keys(tool.inputs).forEach((key) => { + toolConfig[key] = tool.inputs[key] + }) } - edges.push(toolEdge) - } catch (error) { - console.error('Error adding tool', error) - } - } - - return { nodes, edges } + return { + agentSelectedTool: tool.name, + agentSelectedToolConfig: toolConfig, + agentSelectedToolRequiresHumanInput: tool.requireHumanInput ?? false + } + }) } - const addDocStore = async (toolAgentId) => { - const docStoreVSNode = await nodesApi.getSpecificNode('documentStoreVS') - const retrieverToolNode = await nodesApi.getSpecificNode('retrieverTool') - - const nodes = [] - const edges = [] - - for (let i = 0; i < selectedDocumentStores.length; i++) { - try { - const docStoreVSId = `documentStoreVS_${i}` - const retrieverToolId = `retrieverTool_${i}` - - const docStoreVSNodeData = cloneDeep(initNode(docStoreVSNode.data, docStoreVSId)) - const retrieverToolNodeData = cloneDeep(initNode(retrieverToolNode.data, retrieverToolId)) - - set(docStoreVSNodeData, 'inputs.selectedStore', selectedDocumentStores[i].id) - set(docStoreVSNodeData, 'outputs.output', 'retriever') - - const docStoreOption = documentStoreOptions.find((ds) => ds.name === selectedDocumentStores[i].id) - // convert to small case and replace space with underscore - const name = (docStoreOption?.label || '') - .toLowerCase() - .replace(/ /g, '_') - .replace(/[^a-z0-9_-]/g, '') - const desc = selectedDocumentStores[i].description || docStoreOption?.description || '' - - set(retrieverToolNodeData, 'inputs', { - name, - description: desc, - retriever: `{{${docStoreVSId}.data.instance}}`, - returnSourceDocuments: selectedDocumentStores[i].returnSourceDocuments ?? false - }) + const buildDocStoresArray = () => { + return selectedDocumentStores.map((ds) => ({ + documentStore: `${ds.id}:${ds.name}`, + docStoreDescription: ds.description || '', + returnSourceDocuments: ds.returnSourceDocuments ?? false + })) + } - const docStoreVS = { - id: docStoreVSId, - data: { - ...docStoreVSNodeData, - id: docStoreVSId + const buildVSEmbeddingsArray = () => { + return knowledgeVSEmbeddings + .filter((item) => item.vectorStore && item.embeddingModel) + .map((item) => { + const vsConfig = { credential: '', agentSelectedTool: item.vectorStore } + if (item.vectorStoreNode) { + vsConfig.credential = item.vectorStoreNode.credential || '' + if (item.vectorStoreNode.inputs) { + Object.keys(item.vectorStoreNode.inputs).forEach((key) => { + vsConfig[key] = item.vectorStoreNode.inputs[key] + }) } } - nodes.push(docStoreVS) - - const retrieverTool = { - id: retrieverToolId, - data: { - ...retrieverToolNodeData, - id: retrieverToolId + const embConfig = { credential: '', agentSelectedTool: item.embeddingModel } + if (item.embeddingNode) { + embConfig.credential = item.embeddingNode.credential || '' + if (item.embeddingNode.inputs) { + Object.keys(item.embeddingNode.inputs).forEach((key) => { + embConfig[key] = item.embeddingNode.inputs[key] + }) } } - nodes.push(retrieverTool) - - const docStoreVSEdge = { - source: docStoreVSId, - sourceHandle: `${docStoreVSId}-output-retriever-BaseRetriever`, - target: retrieverToolId, - targetHandle: `${retrieverToolId}-input-retriever-BaseRetriever`, - type: 'buttonedge', - id: `${docStoreVSId}-${docStoreVSId}-output-retriever-BaseRetriever-${retrieverToolId}-${retrieverToolId}-input-retriever-BaseRetriever` - } - edges.push(docStoreVSEdge) - - const retrieverToolEdge = { - source: retrieverToolId, - sourceHandle: `${retrieverToolId}-output-retrieverTool-RetrieverTool|DynamicTool|Tool|StructuredTool|Runnable`, - target: toolAgentId, - targetHandle: `${toolAgentId}-input-tools-Tool`, - type: 'buttonedge', - id: `${retrieverToolId}-${retrieverToolId}-output-retrieverTool-RetrieverTool|DynamicTool|Tool|StructuredTool|Runnable-${toolAgentId}-${toolAgentId}-input-tools-Tool` + return { + vectorStore: item.vectorStore, + vectorStoreConfig: vsConfig, + embeddingModel: item.embeddingModel, + embeddingModelConfig: embConfig, + knowledgeName: item.knowledgeName || '', + knowledgeDescription: item.knowledgeDescription || '', + returnSourceDocuments: item.returnSourceDocuments ?? false } - edges.push(retrieverToolEdge) - } catch (error) { - console.error('Error adding doc store', error) - } - } - - return { nodes, edges } + }) } const prepareConfig = async () => { try { - const config = {} - - const nodes = toolAgentFlow.nodes - const edges = toolAgentFlow.edges - const chatModelId = `${selectedChatModel.name}_0` - const existingChatModelId = nodes.find((node) => node.data.category === 'Chat Models')?.id - - // Replace Chat Model - let filteredNodes = nodes.filter((node) => node.data.category !== 'Chat Models') - const toBeReplaceNode = { - id: chatModelId, - data: { - ...selectedChatModel, - id: chatModelId - } + if (!startNodeDef || !agentNodeDef) { + throw new Error('Node definitions not loaded yet') } - filteredNodes.push(toBeReplaceNode) - - // Replace Tool Agent inputs - const toolAgentNode = filteredNodes.find((node) => node.data.name === 'toolAgent') - const toolAgentId = toolAgentNode.id - set(toolAgentNode.data.inputs, 'model', `{{${chatModelId}}}`) - set(toolAgentNode.data.inputs, 'systemMessage', `${customAssistantInstruction}`) - - const agentTools = [] - if (selectedDocumentStores.length > 0) { - const retrieverTools = selectedDocumentStores.map((_, index) => `{{retrieverTool_${index}}}`) - agentTools.push(...retrieverTools) - } - if (selectedTools.length > 0) { - const tools = selectedTools.map((_, index) => `{{${selectedTools[index].id}}}`) - agentTools.push(...tools) - } - set(toolAgentNode.data.inputs, 'tools', agentTools) - filteredNodes = filteredNodes.map((node) => (node.id === toolAgentNode.id ? toolAgentNode : node)) + const startNode = cloneDeep(startNodeDef) + const agentNode = cloneDeep(agentNodeDef) - // Go through each edge and loop through each key. Check if the string value of each key includes/contains existingChatModelId, if yes replace with chatModelId - let filteredEdges = edges.map((edge) => { - const newEdge = { ...edge } - Object.keys(newEdge).forEach((key) => { - if (newEdge[key].includes(existingChatModelId)) { - newEdge[key] = newEdge[key].replaceAll(existingChatModelId, chatModelId) - } - }) - return newEdge - }) + // Set agent node inputs + set(agentNode, 'inputs.agentModel', selectedChatModel.name) + set(agentNode, 'inputs.agentModelConfig', buildModelConfig()) + set(agentNode, 'inputs.agentMessages', [{ role: 'system', content: customAssistantInstruction }]) - // Add Doc Store - if (selectedDocumentStores.length > 0) { - const { nodes: newNodes, edges: newEdges } = await addDocStore(toolAgentId) - filteredNodes = [...filteredNodes, ...newNodes] - filteredEdges = [...filteredEdges, ...newEdges] + // Built-in tools — save all dynamic param entries + const builtInToolsMap = getBuiltInToolsMap(agentNodeDef) + for (const modelKey of Object.keys(builtInToolsMap)) { + const paramName = builtInToolsMap[modelKey].paramName + const tools = builtInTools[paramName] || [] + set(agentNode, `inputs.${paramName}`, tools.length > 0 ? JSON.stringify(tools) : '') } - // Add Tools - if (selectedTools.length > 0) { - const { nodes: newNodes, edges: newEdges } = await addTools(toolAgentId) - filteredNodes = [...filteredNodes, ...newNodes] - filteredEdges = [...filteredEdges, ...newEdges] + // Custom tools + set(agentNode, 'inputs.agentTools', buildToolsArray()) + + // Knowledge + set(agentNode, 'inputs.agentKnowledgeDocumentStores', buildDocStoresArray()) + set(agentNode, 'inputs.agentKnowledgeVSEmbeddings', buildVSEmbeddingsArray()) + + // Memory + set(agentNode, 'inputs.agentEnableMemory', enableMemory) + set(agentNode, 'inputs.agentMemoryType', memoryType) + set(agentNode, 'inputs.agentMemoryWindowSize', String(memoryWindowSize)) + set(agentNode, 'inputs.agentMemoryMaxTokenLimit', String(memoryMaxTokenLimit)) + + // Structured output + set(agentNode, 'inputs.agentStructuredOutput', structuredOutput) + + // Return response as user message + set(agentNode, 'inputs.agentReturnResponseAs', 'userMessage') + + const config = { + nodes: [ + { + id: startNode.id, + type: 'agentFlow', + position: { x: 70.5, y: 107 }, + data: startNode, + width: 103, + height: 66 + }, + { + id: agentNode.id, + type: 'agentFlow', + position: { x: 231, y: 105 }, + data: agentNode, + width: 175, + height: 72 + } + ], + edges: [ + { + source: 'startAgentflow_0', + sourceHandle: 'startAgentflow_0-output-startAgentflow', + target: 'agentAgentflow_0', + targetHandle: 'agentAgentflow_0', + data: { sourceColor: '#7EE787', targetColor: '#4DD0E1', isHumanInput: false }, + type: 'agentFlow', + id: 'startAgentflow_0-startAgentflow_0-output-startAgentflow-agentAgentflow_0-agentAgentflow_0' + } + ] } - config.nodes = filteredNodes - config.edges = filteredEdges - return config } catch (error) { console.error('Error preparing config', error) enqueueSnackbar({ - message: `Failed to save assistant: ${ - typeof error.response.data === 'object' ? error.response.data.message : error.response.data - }`, + message: `Failed to save agent: ${typeof error === 'string' ? error : error.message || 'Unknown error'}`, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -505,6 +553,75 @@ const CustomAssistantConfigurePreview = () => { } } + // ==============================|| Save & Process ||============================== // + + const onSaveAndProcess = async () => { + if (checkMandatoryFields()) { + setLoading(true) + const flowData = await prepareConfig() + if (!flowData) { + setLoading(false) + return + } + const saveObj = { + name: agentName, + flowData: JSON.stringify(flowData), + type: 'AGENT' + } + try { + let saveResp + if (isNewAgent || !chatflowId) { + // Create new chatflow (new agent or legacy assistant without linked chatflow) + saveResp = await chatflowsApi.createNewChatflow(saveObj) + } else { + saveResp = await chatflowsApi.updateChatflow(chatflowId, saveObj) + } + + if (saveResp.data) { + const newChatflowId = saveResp.data.id + setChatflowId(newChatflowId) + dispatch({ type: SET_CHATFLOW, chatflow: saveResp.data }) + + setLoading(false) + enqueueSnackbar({ + message: 'Agent saved successfully', + options: { + key: new Date().getTime() + Math.random(), + variant: 'success', + action: (key) => ( + + ) + } + }) + // If new agent, navigate to the saved agent's page + if (isNewAgent && newChatflowId) { + navigate(`/agents/${newChatflowId}`, { replace: true }) + } + } + } catch (error) { + setLoading(false) + enqueueSnackbar({ + message: `Failed to save agent: ${ + typeof error.response?.data === 'object' ? error.response.data.message : error.response?.data + }`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + action: (key) => ( + + ) + } + }) + } + } + } + + // ==============================|| Settings & Actions ||============================== // + const onSettingsItemClick = (setting) => { setSettingsOpen(false) @@ -525,32 +642,137 @@ const CustomAssistantConfigurePreview = () => { setViewLeadsDialogOpen(true) } else if (setting === 'chatflowConfiguration') { setChatflowConfigurationDialogProps({ - title: `Assistant Configuration`, + title: `Agent Configuration`, chatflow: canvas.chatflow }) setChatflowConfigurationDialogOpen(true) + } else if (setting === 'saveAsTemplate') { + if (isNewAgent) { + enqueueSnackbar({ + message: 'Please save the agent before exporting as template', + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + return + } + setExportAsTemplateDialogProps({ + title: 'Export As Template', + chatflow: canvas.chatflow + }) + setExportAsTemplateDialogOpen(true) + } else if (setting === 'duplicateAgent') { + handleDuplicateAgent() + } else if (setting === 'exportAgent') { + handleExportAgent() + } + } + + const onUploadFile = (file) => { + setSettingsOpen(false) + handleLoadAgent(file) + } + + const handleDuplicateAgent = async () => { + try { + // Build flowData from current UI state and create a new chatflow directly + const flowData = await prepareConfig() + if (!flowData) return + const saveObj = { + name: `${agentName || 'Agent'} (Copy)`, + flowData: JSON.stringify(flowData), + type: 'AGENT' + } + const createResp = await chatflowsApi.createNewChatflow(saveObj) + if (createResp.data) { + window.open(`${uiBaseURL}/agents/${createResp.data.id}`, '_blank') + } + } catch (e) { + console.error(e) + enqueueSnackbar({ + message: `Failed to duplicate agent: ${e.message || 'Unknown error'}`, + options: { key: new Date().getTime() + Math.random(), variant: 'error' } + }) + } + } + + const handleExportAgent = () => { + try { + if (!canvas.chatflow?.flowData) return + const flowData = JSON.parse(canvas.chatflow.flowData) + const dataStr = JSON.stringify(generateExportFlowData(flowData, 'AGENT'), null, 2) + const blob = new Blob([dataStr], { type: 'application/json' }) + const dataUri = URL.createObjectURL(blob) + const exportFileDefaultName = `${agentName || 'Agent'} Agent.json` + const linkElement = document.createElement('a') + linkElement.setAttribute('href', dataUri) + linkElement.setAttribute('download', exportFileDefaultName) + linkElement.click() + } catch (e) { + console.error(e) + } + } + + const handleLoadAgent = (file) => { + try { + const flowData = JSON.parse(file) + if (flowData.type && flowData.type !== 'AGENT') { + enqueueSnackbar({ + message: `Invalid file: expected AGENT type but got ${flowData.type}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + return + } + delete flowData.type + // Render the loaded agent in the UI without saving to DB — user must click Save + loadAgentFromFlowData(JSON.stringify(flowData)) + enqueueSnackbar({ + message: 'Agent loaded. Click Save to persist.', + options: { key: new Date().getTime() + Math.random(), variant: 'success' } + }) + } catch (e) { + console.error(e) + enqueueSnackbar({ + message: `Failed to load agent: ${e.message || 'Invalid file'}`, + options: { key: new Date().getTime() + Math.random(), variant: 'error' } + }) } } const handleDeleteFlow = async () => { const confirmPayload = { title: `Delete`, - description: `Delete ${selectedCustomAssistant.name}?`, + description: `Delete ${agentName}?`, confirmButtonName: 'Delete', cancelButtonName: 'Cancel' } const isConfirmed = await confirm(confirmPayload) - if (isConfirmed && customAssistantId) { + if (isConfirmed && !isNewAgent) { try { - const resp = await assistantsApi.deleteAssistant(customAssistantId) - if (resp.data && customAssistantFlowId) { - await chatflowsApi.deleteChatflow(customAssistantFlowId) + if (chatflowId) { + await chatflowsApi.deleteChatflow(chatflowId) } - navigate(-1) + navigate('/agents') } catch (error) { enqueueSnackbar({ - message: typeof error.response.data === 'object' ? error.response.data.message : error.response.data, + message: typeof error.response?.data === 'object' ? error.response.data.message : error.response?.data, options: { key: new Date().getTime() + Math.random(), variant: 'error', @@ -566,17 +788,21 @@ const CustomAssistantConfigurePreview = () => { } } - const onExpandDialogClicked = (value) => { + const [expandDialogTarget, setExpandDialogTarget] = useState(null) + + const onExpandDialogClicked = (value, target = 'instruction', options = {}) => { const dialogProps = { value, inputParam: { - label: 'Instructions', - name: 'instructions', - type: 'string' + label: options.label || 'Instructions', + name: options.name || 'instructions', + type: options.type || 'string' }, + languageType: options.languageType, confirmButtonName: 'Save', cancelButtonName: 'Cancel' } + setExpandDialogTarget(target) setExpandDialogProps(dialogProps) setShowExpandDialog(true) } @@ -599,13 +825,9 @@ const CustomAssistantConfigurePreview = () => { if (resp.data) { setLoading(false) const content = resp.data?.content || resp.data.kwargs?.content - // replace the description of the selected document store const newSelectedDocumentStores = selectedDocumentStores.map((ds) => { if (ds.id === storeId) { - return { - ...ds, - description: content - } + return { ...ds, description: content } } return ds }) @@ -660,9 +882,10 @@ const CustomAssistantConfigurePreview = () => { const onAPIDialogClick = () => { setAPIDialogProps({ title: 'Embed in website or use as API', - chatflowid: customAssistantFlowId, + chatflowid: chatflowId, chatflowApiKeyId: canvas.chatflow.apikeyid, - isSessionMemory: true + isSessionMemory: true, + isAgentCanvas: true }) setAPIDialogOpen(true) } @@ -691,17 +914,110 @@ const CustomAssistantConfigurePreview = () => { setSelectedDocumentStores(newSelectedDocumentStores) } + // ==============================|| Built-in Tools Handlers ||============================== // + + const getBuiltInToolsForParam = (paramName) => { + return builtInTools[paramName] || [] + } + + const handleBuiltInToolToggle = (paramName, toolName) => { + setBuiltInTools((prev) => { + const currentTools = prev[paramName] || [] + const updated = currentTools.includes(toolName) ? currentTools.filter((t) => t !== toolName) : [...currentTools, toolName] + return { ...prev, [paramName]: updated } + }) + } + + // ==============================|| Structured Output Handlers ||============================== // + + const handleStructuredOutputChange = (index, field, value) => { + const updated = [...structuredOutput] + updated[index] = { ...updated[index], [field]: value } + setStructuredOutput(updated) + } + + const addStructuredOutputField = () => { + setStructuredOutput([...structuredOutput, { key: '', type: 'string', description: '' }]) + } + + const removeStructuredOutputField = (index) => { + setStructuredOutput(structuredOutput.filter((_, i) => i !== index)) + } + + // ==============================|| Effects ||============================== // + useEffect(() => { getChatModelsApi.request() getDocStoresApi.request() getToolsApi.request() + // Fetch agentflow node definitions dynamically from server + const fetchNodeDefs = async () => { + try { + const [startResp, agentResp] = await Promise.all([ + nodesApi.getSpecificNode('startAgentflow'), + nodesApi.getSpecificNode('agentAgentflow') + ]) + if (startResp.data) { + const startData = initNode(startResp.data, 'startAgentflow_0') + setStartNodeDef(startData) + } + if (agentResp.data) { + const agentData = initNode(agentResp.data, 'agentAgentflow_0') + setAgentNodeDef(agentData) + } + } catch (err) { + console.error('Error fetching node definitions', err) + } + } + fetchNodeDefs() + + // Fetch vector store and embedding model options + full component definitions + const fetchVSEmbeddingOptions = async () => { + try { + const [vsResp, embResp, vsComponentsResp, embComponentsResp] = await Promise.all([ + nodesApi.executeNodeLoadMethod('agentAgentflow', { loadMethod: 'listVectorStores' }), + nodesApi.executeNodeLoadMethod('agentAgentflow', { loadMethod: 'listEmbeddings' }), + nodesApi.getNodesByCategory('Vector Stores'), + nodesApi.getNodesByCategory('Embeddings') + ]) + if (vsResp.data) { + setVectorStoreOptions( + vsResp.data.map((vs) => ({ + label: vs.label, + name: vs.name, + description: vs.description || '', + imageSrc: `${baseURL}/api/v1/node-icon/${vs.name}` + })) + ) + } + if (embResp.data) { + setEmbeddingModelOptions( + embResp.data.map((em) => ({ + label: em.label, + name: em.name, + description: em.description || '', + imageSrc: `${baseURL}/api/v1/node-icon/${em.name}` + })) + ) + } + if (vsComponentsResp.data) { + setVectorStoreComponents(vsComponentsResp.data.filter((c) => !c.tags?.includes('LlamaIndex'))) + } + if (embComponentsResp.data) { + setEmbeddingModelComponents(embComponentsResp.data.filter((c) => !c.tags?.includes('LlamaIndex'))) + } + } catch (err) { + console.error('Error fetching vector store / embedding options', err) + } + } + fetchVSEmbeddingOptions() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { if (getDocStoresApi.data) { - // Set options const options = getDocStoresApi.data.map((ds) => ({ label: ds.label, name: ds.name, @@ -717,11 +1033,11 @@ const CustomAssistantConfigurePreview = () => { if (getToolsApi.data) { setToolComponents(getToolsApi.data) - // Set options const options = getToolsApi.data.map((ds) => ({ label: ds.label, name: ds.name, - description: ds.description + description: ds.description, + imageSrc: `${baseURL}/api/v1/node-icon/${ds.name}` })) setToolOptions(options) } @@ -729,81 +1045,419 @@ const CustomAssistantConfigurePreview = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [getToolsApi.data]) + // Once toolComponents are loaded, initialize any selectedTools that are bare objects (missing inputParams). + // This handles both orderings: toolComponents loaded before or after selectedTools are set. + const initializeTools = (tools, components) => { + if (components.length === 0 || tools.length === 0) return null + const hasUninit = tools.some((t) => t.name && !t.inputParams) + if (!hasUninit) return null + + return tools.map((tool, index) => { + if (!tool.name || tool.inputParams) return tool + const foundComponent = components.find((c) => c.name === tool.name) + if (!foundComponent) return tool + const toolId = `${foundComponent.name}_${index}` + const clonedComponent = cloneDeep(foundComponent) + const freshTool = initNode(clonedComponent, toolId) + // Restore saved inputs + Object.keys(tool).forEach((key) => { + if (key === 'name' || key === 'toolId') return + if (key === 'credential') { + freshTool.credential = tool[key] + if (freshTool.inputs) { + freshTool.inputs.credential = tool[key] + freshTool.inputs.FLOWISE_CREDENTIAL_ID = tool[key] + } + } else if (freshTool.inputs && key in freshTool.inputs) { + freshTool.inputs[key] = tool[key] + } + }) + return freshTool + }) + } + useEffect(() => { - if (getChatModelsApi.data) { - setChatModelsComponents(getChatModelsApi.data) + const result = initializeTools(selectedTools, toolComponents) + if (result) setSelectedTools(result) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [toolComponents]) - // Set options - const options = getChatModelsApi.data.map((chatModel) => ({ - label: chatModel.label, - name: chatModel.name, - imageSrc: `${baseURL}/api/v1/node-icon/${chatModel.name}` - })) - setChatModelsOptions(options) + // Also run when selectedTools changes, but only if toolComponents is ready and tools need init + const prevToolsLenRef = useRef(0) + useEffect(() => { + // Only trigger when selectedTools length changes (new tools loaded from flowData) + if (selectedTools.length !== prevToolsLenRef.current) { + prevToolsLenRef.current = selectedTools.length + const result = initializeTools(selectedTools, toolComponents) + if (result) setSelectedTools(result) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedTools]) - if (customAssistantId) { - setLoadingAssistant(true) - getSpecificAssistantApi.request(customAssistantId) + // Initialize VS/embedding nodes when components are loaded and items need initialization + useEffect(() => { + if (vectorStoreComponents.length === 0 && embeddingModelComponents.length === 0) return + if (knowledgeVSEmbeddings.length === 0) return + const hasUninit = knowledgeVSEmbeddings.some( + (item) => (item.vectorStore && !item.vectorStoreNode) || (item.embeddingModel && !item.embeddingNode) + ) + if (!hasUninit) return + + const updated = knowledgeVSEmbeddings.map((item, index) => { + const newItem = { ...item } + if (item.vectorStore && !item.vectorStoreNode) { + const vsNode = initVectorStoreNode(item.vectorStore, index) + if (vsNode && item.vectorStoreConfig) { + // Restore saved inputs + Object.keys(item.vectorStoreConfig).forEach((key) => { + if (key === 'credential') { + vsNode.credential = item.vectorStoreConfig[key] + if (vsNode.inputs) { + vsNode.inputs.credential = item.vectorStoreConfig[key] + vsNode.inputs.FLOWISE_CREDENTIAL_ID = item.vectorStoreConfig[key] + } + } else if (key === 'agentSelectedTool') { + // skip + } else if (vsNode.inputs && key in vsNode.inputs) { + vsNode.inputs[key] = item.vectorStoreConfig[key] + } + }) + } + newItem.vectorStoreNode = vsNode } + if (item.embeddingModel && !item.embeddingNode) { + const embNode = initEmbeddingNode(item.embeddingModel, index) + if (embNode && item.embeddingModelConfig) { + Object.keys(item.embeddingModelConfig).forEach((key) => { + if (key === 'credential') { + embNode.credential = item.embeddingModelConfig[key] + if (embNode.inputs) { + embNode.inputs.credential = item.embeddingModelConfig[key] + embNode.inputs.FLOWISE_CREDENTIAL_ID = item.embeddingModelConfig[key] + } + } else if (key === 'agentSelectedTool') { + // skip + } else if (embNode.inputs && key in embNode.inputs) { + embNode.inputs[key] = item.embeddingModelConfig[key] + } + }) + } + newItem.embeddingNode = embNode + } + return newItem + }) + setKnowledgeVSEmbeddings(updated) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [vectorStoreComponents, embeddingModelComponents, knowledgeVSEmbeddings]) + + // Helper to restore a chat model from saved data + const restoreChatModel = (savedModel) => { + if (!savedModel) return + const latestComponent = chatModelsComponents.find((c) => c.name === savedModel.name) + if (latestComponent) { + const chatModelId = `${latestComponent.name}_0` + const clonedComponent = cloneDeep(latestComponent) + const freshModel = initNode(clonedComponent, chatModelId) + freshModel.credential = savedModel.credential || '' + if (savedModel.inputs) { + Object.keys(savedModel.inputs).forEach((key) => { + if (freshModel.inputs && key in freshModel.inputs) { + freshModel.inputs[key] = savedModel.inputs[key] + } + }) + } + // Ensure credential is set in inputs for DocStoreInputHandler/CredentialInputHandler to read + if (freshModel.credential && freshModel.inputs) { + freshModel.inputs.credential = freshModel.credential + freshModel.inputs.FLOWISE_CREDENTIAL_ID = freshModel.credential + } + setSelectedChatModel(freshModel) + } else { + setSelectedChatModel(savedModel) } + } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getChatModelsApi.data]) + // Helper to extract agent config from flowData + const loadAgentFromFlowData = (flowDataStr) => { + try { + const flowData = JSON.parse(flowDataStr) + + // ---- New agentflow format (agentAgentflow node) ---- + const agentNode = flowData.nodes?.find((n) => n.data?.name === 'agentAgentflow') + if (agentNode) { + const inputs = agentNode.data?.inputs || {} + const modelConfig = inputs.agentModelConfig || {} + + // Restore chat model + if (inputs.agentModel && modelConfig) { + const savedModel = { + name: inputs.agentModel, + credential: modelConfig.credential || '', + inputs: { ...modelConfig } + } + delete savedModel.inputs.credential + delete savedModel.inputs.agentModel + restoreChatModel(savedModel) + } - useEffect(() => { - if (getSpecificAssistantApi.data) { - setLoadingAssistant(false) - try { - const assistantDetails = JSON.parse(getSpecificAssistantApi.data.details) - setSelectedCustomAssistant(assistantDetails) + // Instruction from messages + if (inputs.agentMessages?.length > 0) { + const systemMsg = inputs.agentMessages.find((m) => m.role === 'system') + if (systemMsg?.content) setCustomAssistantInstruction(systemMsg.content) + } - if (assistantDetails.chatModel) { - setSelectedChatModel(assistantDetails.chatModel) + // Built-in tools — handle both stringified and raw array formats + const parseBuiltInTools = (val) => { + if (!val) return [] + if (typeof val === 'string') { + try { + return JSON.parse(val) + } catch { + return [] + } + } + return Array.isArray(val) ? val : [] + } + // Dynamically load all built-in tool params + const loadedBuiltInTools = {} + for (const key of Object.keys(inputs)) { + if (key.startsWith('agentToolsBuiltIn') && inputs[key]) { + loadedBuiltInTools[key] = parseBuiltInTools(inputs[key]) + } + } + if (Object.keys(loadedBuiltInTools).length > 0) setBuiltInTools(loadedBuiltInTools) + + // Tools + if (inputs.agentTools?.length > 0) { + const tools = inputs.agentTools.map((t) => ({ + name: t.agentSelectedTool, + toolId: t.agentSelectedTool, + requireHumanInput: t.agentSelectedToolRequiresHumanInput ?? false, + ...(t.agentSelectedToolConfig || {}) + })) + setSelectedTools(tools) } - if (assistantDetails.instruction) { - setCustomAssistantInstruction(assistantDetails.instruction) + // Knowledge - Document Stores + if (inputs.agentKnowledgeDocumentStores?.length > 0) { + const docStores = inputs.agentKnowledgeDocumentStores.map((ds) => { + // documentStore format is "storeId:storeName" + // Extract storeId (UUID) for dropdown matching and storeName for display + const compositeId = ds.documentStore || '' + const colonIdx = compositeId.indexOf(':') + const storeId = colonIdx > -1 ? compositeId.substring(0, colonIdx) : compositeId + const displayName = colonIdx > -1 ? compositeId.substring(colonIdx + 1) : compositeId + return { + id: storeId, + name: displayName, + description: ds.docStoreDescription || '', + returnSourceDocuments: ds.returnSourceDocuments || false + } + }) + setSelectedDocumentStores(docStores) } - if (assistantDetails.flowId) { - setCustomAssistantFlowId(assistantDetails.flowId) - getSpecificChatflowApi.request(assistantDetails.flowId) + // Knowledge - Vector Embeddings + if (inputs.agentKnowledgeVSEmbeddings?.length > 0) { + setKnowledgeVSEmbeddings(inputs.agentKnowledgeVSEmbeddings) } - if (assistantDetails.documentStores) { - setSelectedDocumentStores(assistantDetails.documentStores) + // Memory + if (inputs.agentEnableMemory !== undefined) setEnableMemory(inputs.agentEnableMemory) + if (inputs.agentMemoryType) setMemoryType(inputs.agentMemoryType) + if (inputs.agentMemoryWindowSize !== undefined) setMemoryWindowSize(Number(inputs.agentMemoryWindowSize)) + if (inputs.agentMemoryMaxTokenLimit !== undefined) setMemoryMaxTokenLimit(Number(inputs.agentMemoryMaxTokenLimit)) + + // Structured Output + if (inputs.agentStructuredOutput?.length > 0) setStructuredOutput(inputs.agentStructuredOutput) + + return true + } + + // ---- Old format (toolAgent node) — backward compatibility ---- + const toolAgentNode = flowData.nodes?.find((n) => n.data?.name === 'toolAgent') + if (toolAgentNode) { + const toolAgentInputs = toolAgentNode.data?.inputs || {} + + // Instruction from systemMessage + if (toolAgentInputs.systemMessage) { + setCustomAssistantInstruction(toolAgentInputs.systemMessage) } - if (assistantDetails.tools) { - setSelectedTools(assistantDetails.tools) + // Chat model — find the node connected to toolAgent's model input + const chatModelNode = flowData.nodes?.find((n) => n.data?.category === 'Chat Models') + if (chatModelNode) { + const credentialId = + chatModelNode.data.credential || + chatModelNode.data.inputs?.credential || + chatModelNode.data.inputs?.FLOWISE_CREDENTIAL_ID || + '' + const savedModel = { + name: chatModelNode.data.name, + credential: credentialId, + inputs: { ...(chatModelNode.data.inputs || {}) } + } + // Keep credential in inputs so it gets copied to freshModel.inputs + // but remove FLOWISE_CREDENTIAL_ID as it's redundant + if (credentialId) savedModel.inputs.credential = credentialId + delete savedModel.inputs.FLOWISE_CREDENTIAL_ID + restoreChatModel(savedModel) } - } catch (error) { - console.error('Error parsing assistant details', error) + + // Memory — if a memory node exists, enable memory + const memoryNode = flowData.nodes?.find((n) => n.data?.category === 'Memory') + if (memoryNode) { + setEnableMemory(true) + // Map old memory node types to new memory types + const memoryName = memoryNode.data?.name || '' + if (memoryName.includes('windowMemory') || memoryName.includes('BufferWindowMemory')) { + setMemoryType('windowSize') + const windowSize = memoryNode.data?.inputs?.k || memoryNode.data?.inputs?.size + if (windowSize) setMemoryWindowSize(Number(windowSize)) + } else if (memoryName.includes('conversationSummaryMemory')) { + setMemoryType('conversationSummary') + } else { + setMemoryType('allMessages') + } + } + + // Tools — parse from edges and nodes + // Old format: toolAgent has inputs.tools = ["{{retrieverTool_0}}", "{{calculator_0}}"] + // We need to find tool nodes that connect to toolAgent + const toolNodeIds = new Set() + const retrieverToolNodes = [] + const directToolNodes = [] + + // Find all nodes connected to toolAgent's tools input via edges + if (flowData.edges) { + for (const edge of flowData.edges) { + if (edge.target === toolAgentNode.data.id && edge.targetHandle?.includes('tools')) { + const sourceNode = flowData.nodes?.find((n) => n.data?.id === edge.source || n.id === edge.source) + if (sourceNode) { + toolNodeIds.add(sourceNode.data?.id || sourceNode.id) + if (sourceNode.data?.name === 'retrieverTool') { + retrieverToolNodes.push(sourceNode) + } else if (sourceNode.data?.category === 'Tools' || sourceNode.data?.baseClasses?.includes('Tool')) { + directToolNodes.push(sourceNode) + } + } + } + } + } + + // Map direct tool nodes (calculator, etc.) to selectedTools format + // Include credential and saved inputs so they can be restored by initializeTools + const parsedTools = directToolNodes.map((toolNode) => { + const toolData = toolNode.data || {} + const result = { + name: toolData.name, + toolId: toolData.name + } + // Carry over credential + const credentialId = toolData.credential || toolData.inputs?.credential || toolData.inputs?.FLOWISE_CREDENTIAL_ID || '' + if (credentialId) result.credential = credentialId + // Carry over saved inputs + if (toolData.inputs) { + Object.keys(toolData.inputs).forEach((key) => { + if (key === 'credential' || key === 'FLOWISE_CREDENTIAL_ID') return + result[key] = toolData.inputs[key] + }) + } + return result + }) + if (parsedTools.length > 0) { + setSelectedTools(parsedTools) + } + + // Map retrieverTool nodes to document stores + // retrieverTool connects to a vectorStore (documentStoreVS) which has inputs.selectedStore + const parsedDocStores = [] + for (const rt of retrieverToolNodes) { + const rtInputs = rt.data?.inputs || {} + // Find the document store node connected to this retriever tool + const dsEdge = flowData.edges?.find((e) => e.target === (rt.data?.id || rt.id) && e.targetHandle?.includes('retriever')) + if (dsEdge) { + const dsNode = flowData.nodes?.find((n) => (n.data?.id || n.id) === dsEdge.source) + if (dsNode?.data?.inputs?.selectedStore) { + const storeId = dsNode.data.inputs.selectedStore + const storeName = rtInputs.name || storeId + parsedDocStores.push({ + id: storeId, + name: storeName, + description: rtInputs.description || '', + returnSourceDocuments: rtInputs.returnSourceDocuments || false + }) + } + } + } + if (parsedDocStores.length > 0) { + setSelectedDocumentStores(parsedDocStores) + } + + return true + } + + return false + } catch (e) { + console.error('Error loading agent from flowData', e) + return false + } + } + + useEffect(() => { + if (getChatModelsApi.data) { + setChatModelsComponents(getChatModelsApi.data) + + const options = getChatModelsApi.data.map((chatModel) => ({ + label: chatModel.label, + name: chatModel.name, + imageSrc: `${baseURL}/api/v1/node-icon/${chatModel.name}` + })) + setChatModelsOptions(options) + + if (!isNewAgent && !isTemplatePreview) { + setLoadingAssistant(true) + getSpecificChatflowApi.request(chatflowId) + } else { + setLoadingAssistant(false) } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getSpecificAssistantApi.data]) + }, [getChatModelsApi.data]) useEffect(() => { if (getSpecificChatflowApi.data) { const chatflow = getSpecificChatflowApi.data dispatch({ type: SET_CHATFLOW, chatflow }) - } else if (getSpecificChatflowApi.error) { - setError(`Failed to retrieve: ${getSpecificChatflowApi.error.response.data.message}`) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getSpecificChatflowApi.data, getSpecificChatflowApi.error]) + // Set agent name from chatflow + let name = chatflow.name + if (!name || name === 'Untitled') { + try { + const fd = chatflow.flowData ? JSON.parse(chatflow.flowData) : null + const agentNode = fd?.nodes?.find((n) => n.data?.name === 'agentAgentflow') + if (agentNode?.data?.label && agentNode.data.label !== 'Agent 0') { + name = agentNode.data.label + } + } catch { + // ignore + } + } + setAgentName(name || 'Untitled Agent') + setLoadingAssistant(false) - useEffect(() => { - if (getSpecificAssistantApi.error) { + // Load agent config from flowData (handles both old toolAgent and new agentAgentflow formats) + if (chatflow.flowData) { + loadAgentFromFlowData(chatflow.flowData) + } + } else if (getSpecificChatflowApi.error) { setLoadingAssistant(false) - setError(getSpecificAssistantApi.error) + setError(`Failed to retrieve: ${getSpecificChatflowApi.error?.response?.data?.message || 'Unknown error'}`) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getSpecificAssistantApi.error]) + }, [getSpecificChatflowApi.data, getSpecificChatflowApi.error]) useEffect(() => { if (getChatModelsApi.error) { @@ -821,8 +1475,23 @@ const CustomAssistantConfigurePreview = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [getDocStoresApi.error]) + // Load agent from marketplace template data passed via navigation state + useEffect(() => { + if (isNewAgent && location.state?.templateFlowData && getChatModelsApi.data) { + loadAgentFromFlowData(location.state.templateFlowData) + } + // Template preview mode — load from templateData + if (isTemplatePreview && templateData?.flowData && getChatModelsApi.data) { + loadAgentFromFlowData(templateData.flowData) + setAgentName(templateData.templateName || templateData.name || 'Template Agent') + setLoadingAssistant(false) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [location.state, getChatModelsApi.data]) + const defaultWidth = () => { - if (customAssistantFlowId && !loadingAssistant) { + if (isTemplatePreview) return 12 + if (!isNewAgent && !loadingAssistant) { return 6 } return 12 @@ -832,6 +1501,261 @@ const CustomAssistantConfigurePreview = () => { return window.innerHeight - 130 } + // ==============================|| Render Helpers ||============================== // + + const renderBuiltInToolsSection = () => { + const modelName = selectedChatModel?.name + const builtInToolsMap = getBuiltInToolsMap(agentNodeDef) + const builtInConfig = builtInToolsMap[modelName] + if (!builtInConfig) return null + + const currentTools = getBuiltInToolsForParam(builtInConfig.paramName) + + return ( + + + Built-in Tools + + + + {builtInConfig.options.map((tool) => ( + handleBuiltInToolToggle(builtInConfig.paramName, tool.name)} + /> + } + label={ + + {tool.label} + + - {tool.description} + + + } + /> + ))} + + + ) + } + + const renderMemorySection = () => { + return ( + + + Enable Memory + + + setEnableMemory(newValue)} /> + {enableMemory && ( + <> + + Memory Type + + setMemoryType(newValue || 'allMessages')} + value={memoryType} + /> + {memoryType === 'windowSize' && ( + <> + + Window Size + + + setMemoryWindowSize(Number(e.target.value))} + /> + + )} + {memoryType === 'conversationSummaryBuffer' && ( + <> + + Max Token Limit + + + setMemoryMaxTokenLimit(Number(e.target.value))} + /> + + )} + + )} + + ) + } + + const renderStructuredOutputSection = () => { + return ( + + + JSON Structured Output + + + {structuredOutput.map((item, index) => ( + + + + removeStructuredOutputField(index)}> + + + + +
+ + Key * + + handleStructuredOutputChange(index, 'key', e.target.value)} + fullWidth + /> +
+
+ + Type * + + handleStructuredOutputChange(index, 'type', newValue || 'string')} + value={item.type || 'string'} + /> +
+ {item.type === 'enum' && ( +
+ + Enum Values + + handleStructuredOutputChange(index, 'enumValues', e.target.value)} + fullWidth + /> +
+ )} + {item.type === 'jsonArray' && ( +
+ + + JSON Schema + + + + onExpandDialogClicked(item.jsonSchema || '', `jsonSchema_${index}`, { + label: 'JSON Schema', + name: 'jsonSchema', + type: 'code', + languageType: 'json' + }) + } + > + + + +
+ handleStructuredOutputChange(index, 'jsonSchema', code)} + basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }} + /> +
+
+ )} +
+ + Description * + + handleStructuredOutputChange(index, 'description', e.target.value)} + fullWidth + /> +
+
+
+ ))} + +
+ ) + } + + // ==============================|| Render ||============================== // + return ( <> @@ -869,56 +1793,150 @@ const CustomAssistantConfigurePreview = () => { > - - {selectedCustomAssistant?.name ?? ''} - + {isEditingName ? ( + + setEditingNameValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + saveAgentName(editingNameValue) + } else if (e.key === 'Escape') { + setIsEditingName(false) + } + }} + placeholder='Agent Name' + /> + saveAgentName(editingNameValue)} + > + + + setIsEditingName(false)} + > + + + + ) : ( + + {agentName || 'Untitled'} + {!isTemplatePreview && ( + { + setEditingNameValue(agentName) + setIsEditingName(true) + }} + > + + + )} + + )}
- {customAssistantFlowId && !loadingAssistant && ( - - + + navigate('/agents/new', { + state: { templateFlowData: templateData.flowData } + }) + } + startIcon={} > - - - + Use Template + + + ) : ( + <> + {!isNewAgent && !loadingAssistant && ( + + + + + + )} + {isNewAgent && ( + + + loadAgentInputRef.current?.click()} + > + + + + + )} + + + + + + + + )} - - - - - - - - {customAssistantFlowId && !loadingAssistant && ( + {!isNewAgent && !loadingAssistant && !isTemplatePreview && ( { )} - {!customAssistantFlowId && !loadingAssistant && ( - - - - - - - - )} - -
- - Select Model * - -
- { - if (!newValue) { - setSelectedChatModel({}) - } else { - const foundChatComponent = chatModelsComponents.find( - (chatModel) => chatModel.name === newValue - ) - if (foundChatComponent) { - const chatModelId = `${foundChatComponent.name}_0` - const clonedComponent = cloneDeep(foundChatComponent) - const initChatModelData = initNode(clonedComponent, chatModelId) - setSelectedChatModel(initChatModelData) - } - } + + {/* Form content — disabled in template preview mode */} + + {/* Select Model */} + - - - - - Instructions * - -
- + + + Select Model * + + + { + if (!newValue) { + setSelectedChatModel({}) + } else { + const foundChatComponent = chatModelsComponents.find( + (chatModel) => chatModel.name === newValue + ) + if (foundChatComponent) { + const chatModelId = `${foundChatComponent.name}_0` + const clonedComponent = cloneDeep(foundChatComponent) + const initChatModelData = initNode(clonedComponent, chatModelId) + setSelectedChatModel(initChatModelData) + } + } }} - title='Expand' - color='secondary' - onClick={() => onExpandDialogClicked(customAssistantInstruction)} - > - - - {selectedChatModel?.name && ( - + }> + + + + {selectedChatModel?.label || selectedChatModel?.name} Parameters + + + + + {showHideInputParams(selectedChatModel) + .filter((inputParam) => !inputParam.hidden && inputParam.display !== false) + .map((inputParam, index) => ( + + ))} + + )} -
- setCustomAssistantInstruction(event.target.value)} - /> -
- - - Knowledge (Document Stores) - - - { - if (!newValue) { - setSelectedDocumentStores([]) - } else { - onDocStoreItemSelected(newValue) - } + + + {/* Instructions */} + ds.id) ?? 'choose an option'} - /> - {selectedDocumentStores.length > 0 && ( - + > + - Describe Knowledge * + Instructions * - +
+ onExpandDialogClicked(customAssistantInstruction)} + > + + + {selectedChatModel?.name && ( + + )}
- )} - {selectedDocumentStores.map((ds, index) => { - return ( - - -
- {ds.name} - onDocStoreItemDelete(ds.id)} - > - - -
-
- {selectedChatModel?.name && ( - - )} -
- { - const newSelectedDocumentStores = [...selectedDocumentStores] - newSelectedDocumentStores[index].description = event.target.value - setSelectedDocumentStores(newSelectedDocumentStores) - }} - /> - - Return Source Documents - - - { - const newSelectedDocumentStores = [...selectedDocumentStores] - newSelectedDocumentStores[index].returnSourceDocuments = newValue - setSelectedDocumentStores(newSelectedDocumentStores) + setCustomAssistantInstruction(event.target.value)} + /> +
+ + {/* Built-in Tools (conditional on model) */} + {selectedChatModel?.name && renderBuiltInToolsSection()} + + {/* Tools */} + + + Tools + + + {selectedTools.map((tool, index) => { + return ( + - - ) - })} - - {selectedChatModel && Object.keys(selectedChatModel).length > 0 && ( + key={index} + > + +
+ + Tool * + +
+ { + const newSelectedTools = selectedTools.filter( + (t, i) => i !== index + ) + setSelectedTools(newSelectedTools) + }} + > + + +
+ { + if (!newValue) { + const newSelectedTools = [...selectedTools] + newSelectedTools[index] = {} + setSelectedTools(newSelectedTools) + } else { + const foundToolComponent = toolComponents.find( + (tool) => tool.name === newValue + ) + if (foundToolComponent) { + const toolId = `${foundToolComponent.name}_${index}` + const clonedComponent = cloneDeep(foundToolComponent) + const initToolData = initNode(clonedComponent, toolId) + const newSelectedTools = [...selectedTools] + newSelectedTools[index] = initToolData + setSelectedTools(newSelectedTools) + } + } + }} + value={tool?.name || 'choose an option'} + /> +
+ {tool && Object.keys(tool).length === 0 && ( + + )} + {tool && Object.keys(tool).length > 0 && ( + + {showHideInputParams(tool) + .filter( + (inputParam) => + !inputParam.hidden && inputParam.display !== false + ) + .map((inputParam, inputIndex) => ( + + ))} + + Require Human Input + { + const newSelectedTools = [...selectedTools] + newSelectedTools[index] = { + ...newSelectedTools[index], + requireHumanInput: newValue + } + setSelectedTools(newSelectedTools) + }} + /> + + + )} + + ) + })} + +
+ + {/* Knowledge (Document Stores) */} { borderRadius: 2 }} > - {showHideInputParams(selectedChatModel) - .filter((inputParam) => !inputParam.hidden && inputParam.display !== false) - .map((inputParam, index) => ( - + Knowledge (Document Stores) + +
+ { + if (!newValue) { + setSelectedDocumentStores([]) + } else { + onDocStoreItemSelected(newValue) + } + }} + value={selectedDocumentStores.map((ds) => ds.id) ?? 'choose an option'} + /> + {selectedDocumentStores.map((ds, index) => { + return ( + - ))} + sx={{ + p: 2, + mt: 1, + mb: 1, + border: 1, + borderColor: theme.palette.grey[900] + 25, + borderRadius: 2 + }} + > + + + {ds.name} + + + + onDocStoreItemDelete(ds.id)} + > + + + + + +
+ + + Describe Knowledge * + + {selectedChatModel?.name && ( + + )} + + { + const newSelectedDocumentStores = [...selectedDocumentStores] + newSelectedDocumentStores[index].description = + event.target.value + setSelectedDocumentStores(newSelectedDocumentStores) + }} + /> +
+
+ Return Source Documents + { + const newSelectedDocumentStores = [...selectedDocumentStores] + newSelectedDocumentStores[index].returnSourceDocuments = + newValue + setSelectedDocumentStores(newSelectedDocumentStores) + }} + /> +
+
+
+ ) + })} - )} - - - Tools - - - {selectedTools.map((tool, index) => { - return ( + + {/* Knowledge (Vector Embeddings) */} + + + Knowledge (Vector Embeddings) + + + {knowledgeVSEmbeddings.map((item, index) => ( - -
- - Tool * + + + { + setKnowledgeVSEmbeddings( + knowledgeVSEmbeddings.filter((_, i) => i !== index) + ) + }} + > + + + + +
+ + Vector Store * -
- { - const newSelectedTools = selectedTools.filter((t, i) => i !== index) - setSelectedTools(newSelectedTools) + { + const updated = [...knowledgeVSEmbeddings] + const vsNode = newValue + ? initVectorStoreNode(newValue, index) + : null + updated[index] = { + ...updated[index], + vectorStore: newValue || '', + vectorStoreNode: vsNode + } + setKnowledgeVSEmbeddings(updated) }} - > - - + value={item.vectorStore || 'choose an option'} + /> + {item.vectorStoreNode && ( + + }> + + + + {item.vectorStoreNode.label || + item.vectorStoreNode.name}{' '} + Parameters + + + + + {showHideInputParams(item.vectorStoreNode) + .filter( + (inputParam) => + !inputParam.hidden && inputParam.display !== false + ) + .map((inputParam, paramIndex) => ( + + ))} + + + )}
- { - if (!newValue) { - const newSelectedTools = [...selectedTools] - newSelectedTools[index] = {} - setSelectedTools(newSelectedTools) - } else { - const foundToolComponent = toolComponents.find( - (tool) => tool.name === newValue - ) - if (foundToolComponent) { - const toolId = `${foundToolComponent.name}_${index}` - const clonedComponent = cloneDeep(foundToolComponent) - const initToolData = initNode(clonedComponent, toolId) - const newSelectedTools = [...selectedTools] - newSelectedTools[index] = initToolData - setSelectedTools(newSelectedTools) +
+ + Embedding Model * + + { + const updated = [...knowledgeVSEmbeddings] + const embNode = newValue ? initEmbeddingNode(newValue, index) : null + updated[index] = { + ...updated[index], + embeddingModel: newValue || '', + embeddingNode: embNode } - } - }} - value={tool?.name || 'choose an option'} - /> - - {tool && Object.keys(tool).length === 0 && ( - - )} - {tool && Object.keys(tool).length > 0 && ( - - {showHideInputParams(tool) - .filter( - (inputParam) => !inputParam.hidden && inputParam.display !== false - ) - .map((inputParam, inputIndex) => ( - - ))} - - )} + setKnowledgeVSEmbeddings(updated) + }} + value={item.embeddingModel || 'choose an option'} + /> + {item.embeddingNode && ( + + }> + + + + {item.embeddingNode.label || item.embeddingNode.name}{' '} + Parameters + + + + + {showHideInputParams(item.embeddingNode) + .filter( + (inputParam) => + !inputParam.hidden && inputParam.display !== false + ) + .map((inputParam, paramIndex) => ( + + ))} + + + )} +
+
+ + Knowledge Name * + + { + const updated = [...knowledgeVSEmbeddings] + updated[index] = { + ...updated[index], + knowledgeName: e.target.value + } + setKnowledgeVSEmbeddings(updated) + }} + /> +
+
+ + Describe Knowledge * + + { + const updated = [...knowledgeVSEmbeddings] + updated[index] = { + ...updated[index], + knowledgeDescription: e.target.value + } + setKnowledgeVSEmbeddings(updated) + }} + /> +
+
+ Return Source Documents + { + const updated = [...knowledgeVSEmbeddings] + updated[index] = { + ...updated[index], + returnSourceDocuments: newValue + } + setKnowledgeVSEmbeddings(updated) + }} + /> +
+
- ) - })} - - - {selectedChatModel && Object.keys(selectedChatModel).length > 0 && ( - + ))} + + + {/* Chat Model Input Parameters - moved to accordion below Select Model */} + + {/* Memory */} + {renderMemorySection()} + + {/* Structured Output */} + {renderStructuredOutputSection()} + + {/* End form content wrapper */} + + {/* Save & Load Buttons — hidden in template preview */} + {!isTemplatePreview && ( + + + {isNewAgent && ( + <> + { + if (!e.target.files?.[0]) return + const reader = new FileReader() + reader.onload = (evt) => { + if (evt?.target?.result) handleLoadAgent(evt.target.result) + } + reader.readAsText(e.target.files[0]) + e.target.value = null + }} + /> + + + )} + + )}
- {customAssistantFlowId && !loadingAssistant && ( + {!isNewAgent && !loadingAssistant && !isTemplatePreview && ( {customization.isDarkMode && ( { )} {!customization.isDarkMode && ( { anchorEl={settingsRef.current} onClose={() => setSettingsOpen(false)} onSettingsItemClick={onSettingsItemClick} + onUploadFile={onUploadFile} isCustomAssistant={true} /> )} @@ -1425,13 +2738,27 @@ const CustomAssistantConfigurePreview = () => { dialogProps={expandDialogProps} onCancel={() => setShowExpandDialog(false)} onConfirm={(newValue) => { - setCustomAssistantInstruction(newValue) + if (expandDialogTarget === 'instruction') { + setCustomAssistantInstruction(newValue) + } else if (expandDialogTarget?.startsWith('jsonSchema_')) { + const idx = parseInt(expandDialogTarget.split('_')[1]) + handleStructuredOutputChange(idx, 'jsonSchema', newValue) + } setShowExpandDialog(false) }} > + setExportAsTemplateDialogOpen(false)} + /> ) } +CustomAssistantConfigurePreview.propTypes = { + chatflowType: PropTypes.string +} + export default CustomAssistantConfigurePreview diff --git a/packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx b/packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx index 6597bfeb6e7..2c85d156d52 100644 --- a/packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx +++ b/packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx @@ -1,8 +1,25 @@ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' +import moment from 'moment' // material-ui -import { Box, Stack, Skeleton } from '@mui/material' +import { + Box, + Stack, + Skeleton, + ToggleButton, + ToggleButtonGroup, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Typography +} from '@mui/material' +import { useTheme, styled } from '@mui/material/styles' +import { tableCellClasses } from '@mui/material/TableCell' // project imports import ViewHeader from '@/layout/MainLayout/ViewHeader' @@ -13,6 +30,8 @@ import AssistantEmptySVG from '@/assets/images/assistant_empty.svg' import AddCustomAssistantDialog from './AddCustomAssistantDialog' import ErrorBoundary from '@/ErrorBoundary' import { StyledPermissionButton } from '@/ui-component/button/RBACButtons' +import AgentListMenu from '@/ui-component/button/AgentListMenu' +import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' // API import assistantsApi from '@/api/assistants' @@ -21,12 +40,30 @@ import assistantsApi from '@/api/assistants' import useApi from '@/hooks/useApi' // icons -import { IconPlus } from '@tabler/icons-react' +import { IconPlus, IconLayoutGrid, IconList } from '@tabler/icons-react' + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + borderColor: theme.palette.grey[900] + 25, + [`&.${tableCellClasses.head}`]: { + color: theme.palette.grey[900] + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14, + height: 64 + } +})) + +const StyledTableRow = styled(TableRow)(() => ({ + '&:last-child td, &:last-child th': { + border: 0 + } +})) // ==============================|| CustomAssistantLayout ||============================== // const CustomAssistantLayout = () => { const navigate = useNavigate() + const theme = useTheme() const getAllAssistantsApi = useApi(assistantsApi.getAllAssistants) @@ -34,15 +71,22 @@ const CustomAssistantLayout = () => { const [error, setError] = useState(null) const [showDialog, setShowDialog] = useState(false) const [dialogProps, setDialogProps] = useState({}) + const [view, setView] = useState(localStorage.getItem('agentDisplayStyle') || 'card') const [search, setSearch] = useState('') const onSearchChange = (event) => { setSearch(event.target.value) } + const handleChange = (event, nextView) => { + if (nextView === null) return + localStorage.setItem('agentDisplayStyle', nextView) + setView(nextView) + } + const addNew = () => { const dialogProp = { - title: 'Add New Custom Assistant', + title: 'Add New Agent', type: 'ADD', cancelButtonName: 'Cancel', confirmButtonName: 'Add' @@ -53,7 +97,7 @@ const CustomAssistantLayout = () => { const onConfirm = (assistantId) => { setShowDialog(false) - navigate(`/assistants/custom/${assistantId}`) + navigate(`/agents/${assistantId}`) } function filterAssistants(data) { @@ -73,7 +117,6 @@ const CustomAssistantLayout = () => { useEffect(() => { getAllAssistantsApi.request('CUSTOM') - // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -87,6 +130,8 @@ const CustomAssistantLayout = () => { } }, [getAllAssistantsApi.error]) + const totalAgents = getAllAssistantsApi.data?.length || 0 + return ( <> @@ -95,14 +140,45 @@ const CustomAssistantLayout = () => { ) : ( navigate(-1)} + searchPlaceholder='Search Agents' + title='Agents' + description='Build single-agent system for chat' > + + + + + + + + { onClick={addNew} startIcon={} > - Add + Add New - {isLoading ? ( + + {isLoading && ( - ) : ( - - {getAllAssistantsApi.data && - getAllAssistantsApi.data?.filter(filterAssistants).map((data, index) => ( - navigate('/assistants/custom/' + data.id)} - /> - ))} - )} - {!isLoading && (!getAllAssistantsApi.data || getAllAssistantsApi.data.length === 0) && ( + + {!isLoading && totalAgents > 0 && ( + <> + {!view || view === 'card' ? ( + + {getAllAssistantsApi.data?.filter(filterAssistants).map((data, index) => ( + navigate('/agents/' + data.id)} + /> + ))} + + ) : ( + + + + + Name + Model + Last Updated + Actions + + + + {getAllAssistantsApi.data?.filter(filterAssistants).map((data, index) => { + const details = JSON.parse(data.details) + return ( + navigate('/agents/' + data.id)} + > + + + {details.chatModel?.name && ( + + )} + {details.name} + + + + + {details.chatModel?.label || details.chatModel?.name || '-'} + + + + + {data.updatedDate + ? moment(data.updatedDate).format('MMM D, YYYY') + : '-'} + + + e.stopPropagation()} + > + + + + ) + })} + +
+
+ )} + + )} + + {!isLoading && totalAgents === 0 && ( { alt='AssistantEmptySVG' /> -
No Custom Assistants Added Yet
+
No Agents Added Yet
)}
@@ -156,7 +303,8 @@ const CustomAssistantLayout = () => { onCancel={() => setShowDialog(false)} onConfirm={onConfirm} setError={setError} - > + /> + ) } diff --git a/packages/ui/src/views/assistants/custom/toolAgentFlow.js b/packages/ui/src/views/assistants/custom/toolAgentFlow.js deleted file mode 100644 index a39bf47bde1..00000000000 --- a/packages/ui/src/views/assistants/custom/toolAgentFlow.js +++ /dev/null @@ -1,336 +0,0 @@ -export const toolAgentFlow = { - nodes: [ - { - id: 'bufferMemory_0', - data: { - id: 'bufferMemory_0', - label: 'Buffer Memory', - version: 2, - name: 'bufferMemory', - type: 'BufferMemory', - baseClasses: ['BufferMemory', 'BaseChatMemory', 'BaseMemory'], - category: 'Memory', - description: 'Retrieve chat messages stored in database', - inputParams: [ - { - label: 'Session Id', - name: 'sessionId', - type: 'string', - description: - 'If not specified, a random id will be used. Learn
more', - default: '', - additionalParams: true, - optional: true, - id: 'bufferMemory_0-input-sessionId-string' - }, - { - label: 'Memory Key', - name: 'memoryKey', - type: 'string', - default: 'chat_history', - additionalParams: true, - id: 'bufferMemory_0-input-memoryKey-string' - } - ], - inputAnchors: [], - inputs: { - sessionId: '', - memoryKey: 'chat_history' - }, - outputAnchors: [ - { - id: 'bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory', - name: 'bufferMemory', - label: 'BufferMemory', - description: 'Retrieve chat messages stored in database', - type: 'BufferMemory | BaseChatMemory | BaseMemory' - } - ], - outputs: {} - } - }, - { - id: 'chatOpenAI_0', - data: { - id: 'chatOpenAI_0', - label: 'ChatOpenAI', - version: 8, - name: 'chatOpenAI', - type: 'ChatOpenAI', - baseClasses: ['ChatOpenAI', 'BaseChatModel', 'BaseLanguageModel', 'Runnable'], - category: 'Chat Models', - description: 'Wrapper around OpenAI large language models that use the Chat endpoint', - inputParams: [ - { - label: 'Connect Credential', - name: 'credential', - type: 'credential', - credentialNames: ['openAIApi'], - id: 'chatOpenAI_0-input-credential-credential' - }, - { - label: 'Model Name', - name: 'modelName', - type: 'asyncOptions', - loadMethod: 'listModels', - default: 'gpt-4o-mini', - id: 'chatOpenAI_0-input-modelName-asyncOptions' - }, - { - label: 'Temperature', - name: 'temperature', - type: 'number', - step: 0.1, - default: 0.9, - optional: true, - id: 'chatOpenAI_0-input-temperature-number' - }, - { - label: 'Streaming', - name: 'streaming', - type: 'boolean', - default: true, - optional: true, - additionalParams: true, - id: 'chatOpenAI_0-input-streaming-boolean' - }, - { - label: 'Max Tokens', - name: 'maxTokens', - type: 'number', - step: 1, - optional: true, - additionalParams: true, - id: 'chatOpenAI_0-input-maxTokens-number' - }, - { - label: 'Top Probability', - name: 'topP', - type: 'number', - step: 0.1, - optional: true, - additionalParams: true, - id: 'chatOpenAI_0-input-topP-number' - }, - { - label: 'Frequency Penalty', - name: 'frequencyPenalty', - type: 'number', - step: 0.1, - optional: true, - additionalParams: true, - id: 'chatOpenAI_0-input-frequencyPenalty-number' - }, - { - label: 'Presence Penalty', - name: 'presencePenalty', - type: 'number', - step: 0.1, - optional: true, - additionalParams: true, - id: 'chatOpenAI_0-input-presencePenalty-number' - }, - { - label: 'Timeout', - name: 'timeout', - type: 'number', - step: 1, - optional: true, - additionalParams: true, - id: 'chatOpenAI_0-input-timeout-number' - }, - { - label: 'BasePath', - name: 'basepath', - type: 'string', - optional: true, - additionalParams: true, - id: 'chatOpenAI_0-input-basepath-string' - }, - { - label: 'Proxy Url', - name: 'proxyUrl', - type: 'string', - optional: true, - additionalParams: true, - id: 'chatOpenAI_0-input-proxyUrl-string' - }, - { - label: 'Stop Sequence', - name: 'stopSequence', - type: 'string', - rows: 4, - optional: true, - description: 'List of stop words to use when generating. Use comma to separate multiple stop words.', - additionalParams: true, - id: 'chatOpenAI_0-input-stopSequence-string' - }, - { - label: 'Base Options', - name: 'baseOptions', - type: 'json', - optional: true, - additionalParams: true, - id: 'chatOpenAI_0-input-baseOptions-json' - }, - { - label: 'Allow Image Uploads', - name: 'allowImageUploads', - type: 'boolean', - description: - 'Allow image input. Refer to the docs for more details.', - default: false, - optional: true, - id: 'chatOpenAI_0-input-allowImageUploads-boolean' - } - ], - inputAnchors: [ - { - label: 'Cache', - name: 'cache', - type: 'BaseCache', - optional: true, - id: 'chatOpenAI_0-input-cache-BaseCache' - } - ], - inputs: { - cache: '', - modelName: 'gpt-4o-mini', - temperature: 0.9, - streaming: true, - maxTokens: '', - topP: '', - frequencyPenalty: '', - presencePenalty: '', - timeout: '', - basepath: '', - proxyUrl: '', - stopSequence: '', - baseOptions: '', - allowImageUploads: '' - }, - outputAnchors: [ - { - id: 'chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable', - name: 'chatOpenAI', - label: 'ChatOpenAI', - description: 'Wrapper around OpenAI large language models that use the Chat endpoint', - type: 'ChatOpenAI | BaseChatModel | BaseLanguageModel | Runnable' - } - ], - outputs: {} - } - }, - { - id: 'toolAgent_0', - data: { - id: 'toolAgent_0', - label: 'Tool Agent', - version: 2, - name: 'toolAgent', - type: 'AgentExecutor', - baseClasses: ['AgentExecutor', 'BaseChain', 'Runnable'], - category: 'Agents', - description: 'Agent that uses Function Calling to pick the tools and args to call', - inputParams: [ - { - label: 'System Message', - name: 'systemMessage', - type: 'string', - default: 'You are a helpful AI assistant.', - description: 'If Chat Prompt Template is provided, this will be ignored', - rows: 4, - optional: true, - additionalParams: true, - id: 'toolAgent_0-input-systemMessage-string' - }, - { - label: 'Max Iterations', - name: 'maxIterations', - type: 'number', - optional: true, - additionalParams: true, - id: 'toolAgent_0-input-maxIterations-number' - } - ], - inputAnchors: [ - { - label: 'Tools', - name: 'tools', - type: 'Tool', - list: true, - id: 'toolAgent_0-input-tools-Tool' - }, - { - label: 'Memory', - name: 'memory', - type: 'BaseChatMemory', - id: 'toolAgent_0-input-memory-BaseChatMemory' - }, - { - label: 'Tool Calling Chat Model', - name: 'model', - type: 'BaseChatModel', - description: - 'Only compatible with models that are capable of function calling: ChatOpenAI, ChatMistral, ChatAnthropic, ChatGoogleGenerativeAI, ChatVertexAI, GroqChat', - id: 'toolAgent_0-input-model-BaseChatModel' - }, - { - label: 'Chat Prompt Template', - name: 'chatPromptTemplate', - type: 'ChatPromptTemplate', - description: 'Override existing prompt with Chat Prompt Template. Human Message must includes {input} variable', - optional: true, - id: 'toolAgent_0-input-chatPromptTemplate-ChatPromptTemplate' - }, - { - label: 'Input Moderation', - description: 'Detect text that could generate harmful output and prevent it from being sent to the language model', - name: 'inputModeration', - type: 'Moderation', - optional: true, - list: true, - id: 'toolAgent_0-input-inputModeration-Moderation' - } - ], - inputs: { - tools: [], - memory: '{{bufferMemory_0.data.instance}}', - model: '{{chatOpenAI_0.data.instance}}', - chatPromptTemplate: '', - systemMessage: 'You are helpful assistant', - inputModeration: '', - maxIterations: '' - }, - outputAnchors: [ - { - id: 'toolAgent_0-output-toolAgent-AgentExecutor|BaseChain|Runnable', - name: 'toolAgent', - label: 'AgentExecutor', - description: 'Agent that uses Function Calling to pick the tools and args to call', - type: 'AgentExecutor | BaseChain | Runnable' - } - ], - outputs: {} - } - } - ], - edges: [ - { - source: 'bufferMemory_0', - sourceHandle: 'bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory', - target: 'toolAgent_0', - targetHandle: 'toolAgent_0-input-memory-BaseChatMemory', - type: 'buttonedge', - id: 'bufferMemory_0-bufferMemory_0-output-bufferMemory-BufferMemory|BaseChatMemory|BaseMemory-toolAgent_0-toolAgent_0-input-memory-BaseChatMemory' - }, - { - source: 'chatOpenAI_0', - sourceHandle: 'chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable', - target: 'toolAgent_0', - targetHandle: 'toolAgent_0-input-model-BaseChatModel', - type: 'buttonedge', - id: 'chatOpenAI_0-chatOpenAI_0-output-chatOpenAI-ChatOpenAI|BaseChatModel|BaseLanguageModel|Runnable-toolAgent_0-toolAgent_0-input-model-BaseChatModel' - } - ] -} diff --git a/packages/ui/src/views/assistants/index.jsx b/packages/ui/src/views/assistants/index.jsx index bd7af08a56c..6590e71ce25 100644 --- a/packages/ui/src/views/assistants/index.jsx +++ b/packages/ui/src/views/assistants/index.jsx @@ -14,16 +14,17 @@ import { IconRobotFace, IconBrandOpenai } from '@tabler/icons-react' const cards = [ { - title: 'Custom Assistant', - description: 'Create custom assistant using your choice of LLMs', + title: 'Agent', + description: 'Custom Assistant has been moved to Agents. You can now find it under the Agents section in the sidebar.', icon: , iconText: 'Custom', - gradient: 'linear-gradient(135deg, #fff8e14e 0%, #ffcc802f 100%)' + gradient: 'linear-gradient(135deg, #fff8e14e 0%, #ffcc802f 100%)', + deprecated: true, + deprecatedLabel: 'Moved to Agents' }, { title: 'OpenAI Assistant', - description: - 'Create assistant using OpenAI Assistant API. This option is being deprecated; consider using Custom Assistant instead.', + description: 'Create assistant using OpenAI Assistant API. This option is being deprecated; consider using Agent instead.', icon: , iconText: 'OpenAI', gradient: 'linear-gradient(135deg, #c9ffd85f 0%, #a0f0b567 100%)', @@ -59,7 +60,7 @@ const FeatureCards = () => { const customization = useSelector((state) => state.customization) const onCardClick = (index) => { - if (index === 0) navigate('/assistants/custom') + if (index === 0) navigate('/agents') if (index === 1) navigate('/assistants/openai') } @@ -100,6 +101,7 @@ const FeatureCards = () => { {card.icon} {card.iconText} + {card.deprecated && } {card.deprecating && }

{card.title}

diff --git a/packages/ui/src/views/canvas/CanvasHeader.jsx b/packages/ui/src/views/canvas/CanvasHeader.jsx index 85dccf45134..f3a028083fd 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.jsx +++ b/packages/ui/src/views/canvas/CanvasHeader.jsx @@ -139,7 +139,7 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, isAgentflowV2, handleSaveFlow, } else if (setting === 'exportChatflow') { try { const flowData = JSON.parse(chatflow.flowData) - let dataStr = JSON.stringify(generateExportFlowData(flowData), null, 2) + let dataStr = JSON.stringify(generateExportFlowData(flowData, chatflow.type), null, 2) //let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) const blob = new Blob([dataStr], { type: 'application/json' }) const dataUri = URL.createObjectURL(blob) diff --git a/packages/ui/src/views/canvas/index.jsx b/packages/ui/src/views/canvas/index.jsx index 8835706ecf3..602160d796e 100644 --- a/packages/ui/src/views/canvas/index.jsx +++ b/packages/ui/src/views/canvas/index.jsx @@ -166,6 +166,27 @@ const Canvas = () => { const handleLoadFlow = (file) => { try { const flowData = JSON.parse(file) + const expectedType = isAgentCanvas ? 'MULTIAGENT' : 'CHATFLOW' + if (flowData.type && flowData.type !== expectedType) { + enqueueSnackbar({ + message: `Invalid file: expected ${expectedType} type but got ${flowData.type}`, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + return + } + delete flowData.type const nodes = flowData.nodes || [] setNodes(nodes) diff --git a/packages/ui/src/views/chatbot/index.jsx b/packages/ui/src/views/chatbot/index.jsx index 1bf8b723d0b..0a92134b6ef 100644 --- a/packages/ui/src/views/chatbot/index.jsx +++ b/packages/ui/src/views/chatbot/index.jsx @@ -44,7 +44,7 @@ const ChatbotFull = () => { const chatflowType = chatflowData.type if (chatflowData.chatbotConfig) { let parsedConfig = {} - if (chatflowType === 'MULTIAGENT' || chatflowType === 'AGENTFLOW') { + if (chatflowType === 'MULTIAGENT' || chatflowType === 'AGENTFLOW' || chatflowType === 'AGENT') { parsedConfig.showAgentMessages = true } @@ -63,7 +63,7 @@ const ChatbotFull = () => { setChatbotTheme(parsedConfig) setChatbotOverrideConfig({}) } - } else if (chatflowType === 'MULTIAGENT' || chatflowType === 'AGENTFLOW') { + } else if (chatflowType === 'MULTIAGENT' || chatflowType === 'AGENTFLOW' || chatflowType === 'AGENT') { setChatbotTheme({ showAgentMessages: true }) } } diff --git a/packages/ui/src/views/chatflows/index.jsx b/packages/ui/src/views/chatflows/index.jsx index 3a094b54e4f..5764c54d958 100644 --- a/packages/ui/src/views/chatflows/index.jsx +++ b/packages/ui/src/views/chatflows/index.jsx @@ -140,7 +140,7 @@ const Chatflows = () => { search={true} searchPlaceholder='Search Name or Category' title='Chatflows' - description='Build single-agent systems, chatbots and simple LLM flows' + description='Build flows with prebuilt agents and chains' > { search={hasDocStores} searchPlaceholder='Search Name' title='Document Store' - description='Store and upsert documents for LLM retrieval (RAG)' + description='Manage and upsert documents for Retrieval-Augmented Generation (RAG)' > {hasDocStores && ( { const getAllEvaluatorsApi = useApi(evaluatorsApi.getAllEvaluators) const getNodesByCategoryApi = useApi(nodesApi.getNodesByCategory) const getModelsApi = useApi(nodesApi.executeNodeLoadMethod) - const getAssistantsApi = useApi(assistantsApi.getAllAssistants) + const getAllAgentsApi = useApi(chatflowsApi.getAllAgentflows) const [chatflow, setChatflow] = useState([]) const [dataset, setDataset] = useState('') @@ -227,7 +226,7 @@ const CreateEvaluationDialog = ({ show, dialogProps, onCancel, onConfirm }) => { getNodesByCategoryApi.request('Chat Models') if (flows.length === 0) { getAllChatflowsApi.request() - getAssistantsApi.request('CUSTOM') + getAllAgentsApi.request('AGENT') getAllAgentflowsApi.request('AGENTFLOW') } if (datasets.length === 0) { @@ -238,18 +237,18 @@ const CreateEvaluationDialog = ({ show, dialogProps, onCancel, onConfirm }) => { }, []) useEffect(() => { - if (getAllAgentflowsApi.data && getAllChatflowsApi.data && getAssistantsApi.data) { + if (getAllAgentflowsApi.data && getAllChatflowsApi.data && getAllAgentsApi.data) { try { const agentFlows = populateFlowNames(getAllAgentflowsApi.data, 'Agentflow v2') const chatFlows = populateFlowNames(getAllChatflowsApi.data, 'Chatflow') - const assistants = populateAssistants(getAssistantsApi.data) - setFlows([...agentFlows, ...chatFlows, ...assistants]) - setFlowTypes(['Agentflow v2', 'Chatflow', 'Custom Assistant']) + const agents = populateFlowNames(getAllAgentsApi.data, 'Agent') + setFlows([...agentFlows, ...chatFlows, ...agents]) + setFlowTypes(['Agentflow v2', 'Chatflow', 'Agent']) } catch (e) { console.error(e) } } - }, [getAllAgentflowsApi.data, getAllChatflowsApi.data, getAssistantsApi.data]) + }, [getAllAgentflowsApi.data, getAllChatflowsApi.data, getAllAgentsApi.data]) useEffect(() => { if (getNodesByCategoryApi.data) { @@ -369,20 +368,6 @@ const CreateEvaluationDialog = ({ show, dialogProps, onCancel, onConfirm }) => { return flowNames } - const populateAssistants = (assistants) => { - let assistantNames = [] - for (let i = 0; i < assistants.length; i += 1) { - const assistant = assistants[i] - assistantNames.push({ - label: JSON.parse(assistant.details).name || '', - name: assistant.id, - type: 'Custom Assistant', - description: 'Custom Assistant' - }) - } - return assistantNames - } - const component = show ? ( { onChange={onChangeFlowType} />{' '} Agentflows (v2) - {' '} - Custom Assistants + {' '} + Agents } label={ item.metrics[index]?.chain - ? 'Chain Latency: ' + item.metrics[index]?.chain - : 'Chain Latency: N/A' + ? 'Flow Latency: ' + item.metrics[index]?.chain + : 'Flow Latency: N/A' } sx={{ mr: 1, mb: 1 }} /> diff --git a/packages/ui/src/views/evaluations/EvaluationResult.jsx b/packages/ui/src/views/evaluations/EvaluationResult.jsx index 6fdde95b759..ae3ad861dab 100644 --- a/packages/ui/src/views/evaluations/EvaluationResult.jsx +++ b/packages/ui/src/views/evaluations/EvaluationResult.jsx @@ -336,7 +336,8 @@ const EvalEvaluationRows = () => { case 'Chatflow': return '/canvas/' + evaluation.chatflowId[index] case 'Custom Assistant': - return '/assistants/custom/' + evaluation.chatflowId[index] + case 'Agent': + return '/agents/' + evaluation.chatflowId[index] case 'Agentflow v2': return '/v2/agentcanvas/' + evaluation.chatflowId[index] } @@ -360,6 +361,7 @@ const EvalEvaluationRows = () => { case 'Chatflow': return case 'Custom Assistant': + case 'Agent': return case 'Agentflow v2': return @@ -486,8 +488,9 @@ const EvalEvaluationRows = () => { window.open( chatflow.chatflowType === 'Chatflow' ? '/canvas/' + chatflow.chatflowId - : chatflow.chatflowType === 'Custom Assistant' - ? '/assistants/custom/' + chatflow.chatflowId + : chatflow.chatflowType === 'Custom Assistant' || + chatflow.chatflowType === 'Agent' + ? '/agents/' + chatflow.chatflowId : '/v2/agentcanvas/' + chatflow.chatflowId, '_blank' ) @@ -869,9 +872,9 @@ const EvalEvaluationRows = () => { icon={} label={ item.metrics[index]?.chain - ? 'Chain Latency: ' + + ? 'Flow Latency: ' + item.metrics[index]?.chain - : 'Chain Latency: N/A' + : 'Flow Latency: N/A' } sx={{ mr: 1, mb: 1 }} /> diff --git a/packages/ui/src/views/evaluations/EvaluationResultSideDrawer.jsx b/packages/ui/src/views/evaluations/EvaluationResultSideDrawer.jsx index c415fb983a5..a5a95b1092a 100644 --- a/packages/ui/src/views/evaluations/EvaluationResultSideDrawer.jsx +++ b/packages/ui/src/views/evaluations/EvaluationResultSideDrawer.jsx @@ -45,6 +45,7 @@ const EvaluationResultSideDrawer = ({ show, dialogProps, onClickFunction }) => { case 'Chatflow': return case 'Custom Assistant': + case 'Agent': return case 'Agentflow v2': return @@ -164,7 +165,7 @@ const EvaluationResultSideDrawer = ({ show, dialogProps, onClickFunction }) => { )} {dialogProps.data.metrics[index]?.retriever && ( diff --git a/packages/ui/src/views/evaluators/evaluatorConstant.js b/packages/ui/src/views/evaluators/evaluatorConstant.js index 79272cf0f05..31d1c3f1d4e 100644 --- a/packages/ui/src/views/evaluators/evaluatorConstant.js +++ b/packages/ui/src/views/evaluators/evaluatorConstant.js @@ -80,9 +80,9 @@ export const evaluators = [ }, { type: 'numeric', - label: 'Chatflow Latency', + label: 'Flow Latency', name: 'chain', - description: 'Actual time spent in executing the chatflow (milliseconds).' + description: 'Actual time spent in executing the flow (milliseconds).' }, { type: 'numeric', diff --git a/packages/ui/src/views/marketplaces/index.jsx b/packages/ui/src/views/marketplaces/index.jsx index 1f364761414..1dece8da713 100644 --- a/packages/ui/src/views/marketplaces/index.jsx +++ b/packages/ui/src/views/marketplaces/index.jsx @@ -61,7 +61,7 @@ import { gridSpacing } from '@/store/constant' import { useError } from '@/store/context/ErrorContext' const badges = ['POPULAR', 'NEW'] -const types = ['Chatflow', 'AgentflowV2', 'Tool'] +const types = ['Chatflow', 'AgentflowV2', 'Agent', 'Tool'] const framework = ['Langchain', 'LlamaIndex'] const MenuProps = { PaperProps: { @@ -342,7 +342,9 @@ const Marketplace = () => { } const goToCanvas = (selectedChatflow) => { - if (selectedChatflow.type === 'AgentflowV2') { + if (selectedChatflow.type === 'Agent') { + navigate(`/marketplace/agents/${selectedChatflow.id}`, { state: { templateData: selectedChatflow } }) + } else if (selectedChatflow.type === 'AgentflowV2') { navigate(`/v2/marketplace/${selectedChatflow.id}`, { state: selectedChatflow }) } else { navigate(`/marketplace/${selectedChatflow.id}`, { state: selectedChatflow }) @@ -737,7 +739,8 @@ const Marketplace = () => { > {(data.type === 'Chatflow' || data.type === 'Agentflow' || - data.type === 'AgentflowV2') && ( + data.type === 'AgentflowV2' || + data.type === 'Agent') && ( goToCanvas(data)} data={data} @@ -753,7 +756,8 @@ const Marketplace = () => { {!data.badge && (data.type === 'Chatflow' || data.type === 'Agentflow' || - data.type === 'AgentflowV2') && ( + data.type === 'AgentflowV2' || + data.type === 'Agent') && ( goToCanvas(data)} data={data} @@ -801,33 +805,35 @@ const Marketplace = () => { - - {templateUsecases.map((usecase, index) => ( - { - setSelectedTemplateUsecases( - event.target.checked - ? [...selectedTemplateUsecases, usecase] - : selectedTemplateUsecases.filter((item) => item !== usecase) - ) - }} - /> - } - label={usecase} - /> - ))} - + {templateUsecases.length > 0 && ( + + {templateUsecases.map((usecase, index) => ( + { + setSelectedTemplateUsecases( + event.target.checked + ? [...selectedTemplateUsecases, usecase] + : selectedTemplateUsecases.filter((item) => item !== usecase) + ) + }} + /> + } + label={usecase} + /> + ))} + + )} {selectedTemplateUsecases.length > 0 && ( + ) + } + }) + } + + const showError = (message) => { + enqueueSnackbar({ + message, + options: { + key: new Date().getTime() + Math.random(), + variant: 'error', + persist: true, + action: (key) => ( + + ) + } + }) + } + + const refreshChatflowStore = async () => { + try { + const resp = await chatflowsApi.getSpecificChatflow(dialogProps.chatflow.id) + if (resp.data) { + dispatch({ type: SET_CHATFLOW, chatflow: resp.data }) + } + } catch { + // silent fail — the store will refresh on next navigation + } + } + + const handleToggle = (enabled) => { + setMcpEnabled(enabled) + } + + const onSave = async () => { + if (!dialogProps.chatflow?.id) return + if (mcpEnabled && (toolNameError || !toolName.trim() || !description.trim())) return + + setLoading(true) + try { + if (mcpEnabled) { + if (hasExistingConfig) { + const resp = await mcpServerApi.updateMcpServerConfig(dialogProps.chatflow.id, { + enabled: true, + toolName: toolName || undefined, + description: description || undefined + }) + if (resp.data) { + setMcpEnabled(resp.data.enabled) + setToken(resp.data.token || '') + setToolName(resp.data.toolName || '') + setDescription(resp.data.description || '') + showSuccess('MCP Server settings saved') + } + } else { + const resp = await mcpServerApi.createMcpServerConfig(dialogProps.chatflow.id, { + toolName: toolName || undefined, + description: description || undefined + }) + if (resp.data) { + setMcpEnabled(resp.data.enabled) + setToken(resp.data.token || '') + setToolName(resp.data.toolName || '') + setDescription(resp.data.description || '') + setHasExistingConfig(true) + showSuccess('MCP Server settings saved') + } + } + } else { + await mcpServerApi.deleteMcpServerConfig(dialogProps.chatflow.id) + setMcpEnabled(false) + showSuccess('MCP Server disabled') + } + await refreshChatflowStore() + } catch (error) { + showError( + `Failed to save MCP Server settings: ${ + typeof error.response?.data === 'object' ? error.response.data.message : error.response?.data || error.message + }` + ) + } finally { + setLoading(false) + } + } + + const handleCopyUrl = (url) => { + if (!url) return + navigator.clipboard.writeText(url) + showSuccess('URL copied to clipboard') + } + + const handleRefreshCode = async () => { + const confirmPayload = { + title: 'Rotate Token', + description: + 'This will invalidate the existing token. Any clients using the old token will need to be updated with the new one. Are you sure?', + confirmButtonName: 'Rotate', + cancelButtonName: 'Cancel' + } + const isConfirmed = await confirm(confirmPayload) + if (!isConfirmed) return + if (!dialogProps.chatflow?.id) return + + setLoading(true) + try { + const resp = await mcpServerApi.refreshMcpToken(dialogProps.chatflow.id) + if (resp.data) { + setToken(resp.data.token || '') + showSuccess('Token rotated successfully') + } + await refreshChatflowStore() + } catch (error) { + showError( + `Failed to rotate token: ${ + typeof error.response?.data === 'object' ? error.response.data.message : error.response?.data || error.message + }` + ) + } finally { + setLoading(false) + } + } + + useEffect(() => { + if (dialogProps.chatflow?.id) { + getMcpServerConfigApi.request(dialogProps.chatflow.id) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dialogProps]) + + useEffect(() => { + if (getMcpServerConfigApi.error) { + showError( + `Failed to load MCP Server configuration: ${ + typeof getMcpServerConfigApi.error.response?.data === 'object' + ? getMcpServerConfigApi.error.response.data.message + : getMcpServerConfigApi.error.response?.data || getMcpServerConfigApi.error.message + }` + ) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getMcpServerConfigApi.error]) + + useEffect(() => { + if (getMcpServerConfigApi.data) { + setMcpEnabled(getMcpServerConfigApi.data.enabled || false) + setToolName(getMcpServerConfigApi.data.toolName || '') + setDescription(getMcpServerConfigApi.data.description || '') + setToken(getMcpServerConfigApi.data.token || '') + setHasExistingConfig(!!getMcpServerConfigApi.data.token) + } + }, [getMcpServerConfigApi.data]) + + if (getMcpServerConfigApi.loading) { + return ( + + Loading MCP Server configuration... + + ) + } + + return ( + <> + + + + + {mcpEnabled && ( + + {/* Tool Name (required) */} + + + Tool Name * + + handleToolNameChange(e.target.value)} + placeholder='e.g. product_qa' + error={!!toolNameError} + disabled={loading} + /> + {toolNameError && ( + + {toolNameError} + + )} + + Used as the MCP tool identifier by LLM clients. + + + + {/* Description (required) */} + + + Description * + + setDescription(e.target.value)} + placeholder='e.g. Answers product catalog questions' + disabled={loading} + /> + + Helps LLMs understand when to route queries to this tool. Good descriptions improve tool selection accuracy. + + + + {/* MCP Endpoint URL — visible only when has token */} + {token && ( + + Streamable HTTP Endpoint + + handleCopyUrl(endpointUrl)} + title='Copy URL to clipboard' + sx={{ color: customization.isDarkMode ? theme.palette.grey[300] : 'inherit' }} + > + + + + } + /> + + For clients that support the Streamable HTTP transport + + + Token (Bearer Token) + + { + navigator.clipboard.writeText(token) + showSuccess('Token copied to clipboard') + }} + title='Copy token' + sx={{ color: customization.isDarkMode ? theme.palette.grey[300] : 'inherit' }} + > + + + + + + + } + /> + + Use the URL above as the MCP endpoint and pass the token as a Bearer token in the Authorization header. + Configure your MCP client with:{' '} + + Authorization: Bearer {''} + + + + )} + + )} + + + {loading ? 'Saving...' : 'Save'} + + + ) +} + +McpServer.propTypes = { + dialogProps: PropTypes.object +} + +export default McpServer diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d16d9648e97..b1397be3198 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -822,6 +822,9 @@ importers: '@keyv/redis': specifier: ^4.2.0 version: 4.3.3 + '@modelcontextprotocol/sdk': + specifier: ^1.10.1 + version: 1.12.0 '@oclif/core': specifier: 4.0.7 version: 4.0.7 @@ -1074,6 +1077,9 @@ importers: winston-daily-rotate-file: specifier: ^5.0.0 version: 5.0.0(winston@3.12.0) + zod: + specifier: ^3.25.76 || ^4 + version: 4.3.6 devDependencies: '@types/content-disposition': specifier: 0.5.8 @@ -12275,10 +12281,6 @@ packages: resolution: {integrity: sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==} engines: {node: '>=14.18'} - eventsource-parser@3.0.0: - resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==} - engines: {node: '>=18.0.0'} - eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -15945,9 +15947,6 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - object-inspect@1.13.1: - resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -18313,10 +18312,6 @@ packages: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} engines: {node: '>= 0.4'} - side-channel@1.0.6: - resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} - engines: {node: '>= 0.4'} - side-channel@1.1.0: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} @@ -34391,7 +34386,7 @@ snapshots: object-keys: 1.1.1 object.assign: 4.1.5 regexp.prototype.flags: 1.5.2 - side-channel: 1.0.6 + side-channel: 1.1.0 which-boxed-primitive: 1.0.2 which-collection: 1.0.2 which-typed-array: 1.1.20 @@ -34805,7 +34800,7 @@ snapshots: is-string: 1.0.7 is-typed-array: 1.1.13 is-weakref: 1.0.2 - object-inspect: 1.13.1 + object-inspect: 1.13.4 object-keys: 1.1.1 object.assign: 4.1.5 regexp.prototype.flags: 1.5.2 @@ -35431,13 +35426,11 @@ snapshots: eventsource-parser@1.1.2: {} - eventsource-parser@3.0.0: {} - eventsource-parser@3.0.6: {} eventsource@3.0.5: dependencies: - eventsource-parser: 3.0.0 + eventsource-parser: 3.0.6 exa-js@1.0.12(encoding@0.1.13): dependencies: @@ -37298,7 +37291,7 @@ snapshots: dependencies: es-errors: 1.3.0 hasown: 2.0.2 - side-channel: 1.0.6 + side-channel: 1.1.0 internal-slot@1.1.0: dependencies: @@ -40507,8 +40500,6 @@ snapshots: object-hash@3.0.0: {} - object-inspect@1.13.1: {} - object-inspect@1.13.4: {} object-is@1.1.6: @@ -42061,23 +42052,23 @@ snapshots: qs@6.10.4: dependencies: - side-channel: 1.0.6 + side-channel: 1.1.0 qs@6.11.0: dependencies: - side-channel: 1.0.6 + side-channel: 1.1.0 qs@6.11.2: dependencies: - side-channel: 1.0.6 + side-channel: 1.1.0 qs@6.12.1: dependencies: - side-channel: 1.0.6 + side-channel: 1.1.0 qs@6.13.0: dependencies: - side-channel: 1.0.6 + side-channel: 1.1.0 qs@6.5.5: {} @@ -43410,13 +43401,6 @@ snapshots: object-inspect: 1.13.4 side-channel-map: 1.0.1 - side-channel@1.0.6: - dependencies: - call-bind: 1.0.8 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.1 - side-channel@1.1.0: dependencies: es-errors: 1.3.0 @@ -43864,7 +43848,7 @@ snapshots: internal-slot: 1.0.7 regexp.prototype.flags: 1.5.2 set-function-name: 2.0.2 - side-channel: 1.0.6 + side-channel: 1.1.0 string.prototype.trim@1.2.10: dependencies: From a77194c8c265eb88cfd5a3de4058771ef6feed81 Mon Sep 17 00:00:00 2001 From: Jocelyn Lin Date: Wed, 15 Apr 2026 12:54:54 -0700 Subject: [PATCH 17/21] feat(agentflow): optional cavasActions to allow additional buttons next to validate (#6224) * feat(agentflow): optional cavasActions to allow additional buttons next to validate - Updated Agentflow component to support canvasActions prop for rendering additional action buttons in the canvas overlay. - Enhanced type definitions to include canvasActions in AgentflowProps. - Added tests to verify the rendering of canvasActions in the Agentflow component. - Added a new example demonstrating custom FABs alongside the validation button. * update jsdoc to match actual implementation --- packages/agentflow/examples/src/App.tsx | 10 ++- .../src/demos/ValidationActionsExample.tsx | 71 +++++++++++++++++++ .../agentflow/examples/src/demos/index.ts | 1 + packages/agentflow/src/Agentflow.test.tsx | 24 +++++++ packages/agentflow/src/Agentflow.tsx | 24 ++++--- .../agentflow/src/core/types/agentflow.ts | 15 ++++ .../canvas/components/ValidationFeedback.tsx | 2 +- 7 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 packages/agentflow/examples/src/demos/ValidationActionsExample.tsx diff --git a/packages/agentflow/examples/src/App.tsx b/packages/agentflow/examples/src/App.tsx index 834e3afac93..9dac1d2fa5d 100644 --- a/packages/agentflow/examples/src/App.tsx +++ b/packages/agentflow/examples/src/App.tsx @@ -15,7 +15,8 @@ import { DarkModeExampleProps, FilteredComponentsExampleProps, MultiNodeFlowProps, - StatusIndicatorsExampleProps + StatusIndicatorsExampleProps, + ValidationActionsExampleProps } from './demos' import { PropsDisplay } from './PropsDisplay' @@ -81,6 +82,13 @@ const examples: Array<{ description: 'Restrict available nodes with presets', props: FilteredComponentsExampleProps, component: lazy(() => import('./demos/FilteredComponentsExample').then((m) => ({ default: m.FilteredComponentsExample }))) + }, + { + id: 'canvas-actions', + name: 'Canvas Actions', + description: 'Custom FABs alongside the validation button via canvasActions', + props: ValidationActionsExampleProps, + component: lazy(() => import('./demos/ValidationActionsExample').then((m) => ({ default: m.ValidationActionsExample }))) } ] diff --git a/packages/agentflow/examples/src/demos/ValidationActionsExample.tsx b/packages/agentflow/examples/src/demos/ValidationActionsExample.tsx new file mode 100644 index 00000000000..c57034d7d51 --- /dev/null +++ b/packages/agentflow/examples/src/demos/ValidationActionsExample.tsx @@ -0,0 +1,71 @@ +/** + * Canvas Actions Example + * + * Demonstrates how to add custom FAB buttons alongside the built-in validation + * button in the top-right canvas overlay using the `canvasActions` prop. + * + * Mirrors the legacy v2 pattern where a chat FAB and validation FAB sit side-by-side. + */ + +import { useState } from 'react' + +import { Agentflow } from '@flowiseai/agentflow' +import { Box, Dialog, DialogContent, DialogTitle, Fab, IconButton, Typography } from '@mui/material' +import { IconMessage, IconX } from '@tabler/icons-react' + +import { apiBaseUrl, token } from '../config' + +function ChatFab() { + const [open, setOpen] = useState(false) + + return ( + <> + setOpen(true)} + sx={{ + color: 'white', + backgroundColor: 'secondary.main', + '&:hover': { + backgroundColor: 'secondary.main', + backgroundImage: 'linear-gradient(rgb(0 0 0/10%) 0 0)' + } + }} + > + + + + setOpen(false)} maxWidth='sm' fullWidth> + + Chat + setOpen(false)}> + + + + + + + Your chat UI goes here. Full control — bring any component. + + + + + + ) +} + +export function ValidationActionsExample() { + return ( +
+ } /> +
+ ) +} + +export const ValidationActionsExampleProps = { + apiBaseUrl: '{from environment variables}', + token: '{from environment variables}', + canvasActions: '' +} diff --git a/packages/agentflow/examples/src/demos/index.ts b/packages/agentflow/examples/src/demos/index.ts index 5a71e28913f..f5fa5d83e39 100644 --- a/packages/agentflow/examples/src/demos/index.ts +++ b/packages/agentflow/examples/src/demos/index.ts @@ -6,3 +6,4 @@ export * from './DarkModeExample' export * from './FilteredComponentsExample' export * from './MultiNodeFlow' export * from './StatusIndicatorsExample' +export * from './ValidationActionsExample' diff --git a/packages/agentflow/src/Agentflow.test.tsx b/packages/agentflow/src/Agentflow.test.tsx index b3bb1995e3a..542ee89e526 100644 --- a/packages/agentflow/src/Agentflow.test.tsx +++ b/packages/agentflow/src/Agentflow.test.tsx @@ -428,6 +428,30 @@ describe('Agentflow Component', () => { }) }) + describe('Canvas Actions', () => { + it('should render canvasActions content in the canvas', async () => { + const { getByTestId } = render( + My Button} /> + ) + + await waitFor(() => { + expect(getByTestId('custom-action')).toBeInTheDocument() + }) + }) + + it('should not render canvasActions in read-only mode', async () => { + const { container, queryByTestId } = render( + My Button} /> + ) + + await waitFor(() => { + expect(container.querySelector('.agentflow-container')).toBeInTheDocument() + }) + + expect(queryByTestId('custom-action')).not.toBeInTheDocument() + }) + }) + describe('Imperative Ref', () => { it('should expose agentflow instance via ref', async () => { const ref = createRef() diff --git a/packages/agentflow/src/Agentflow.tsx b/packages/agentflow/src/Agentflow.tsx index 1626e14b5d6..df1e71a2243 100644 --- a/packages/agentflow/src/Agentflow.tsx +++ b/packages/agentflow/src/Agentflow.tsx @@ -41,6 +41,7 @@ function AgentflowCanvas({ showDefaultHeader = true, enableGenerator = true, showDefaultPalette = true, + canvasActions, renderHeader, renderNodePalette }: { @@ -52,6 +53,7 @@ function AgentflowCanvas({ showDefaultHeader?: boolean showDefaultPalette?: boolean enableGenerator?: boolean + canvasActions?: AgentflowProps['canvasActions'] renderHeader?: AgentflowProps['renderHeader'] renderNodePalette?: AgentflowProps['renderNodePalette'] }) { @@ -272,14 +274,17 @@ function AgentflowCanvas({ )} - {/* Validation Feedback - positioned at top right */} + {/* Canvas action buttons - positioned at top right */} {!readOnly && ( - >} - /> +
+ >} + /> + {canvasActions} +
)} (function renderHeader, renderNodePalette, showDefaultHeader = true, - showDefaultPalette = true + showDefaultPalette = true, + canvasActions } = props return ( @@ -394,6 +400,7 @@ export const Agentflow = forwardRef(function showDefaultPalette={showDefaultPalette} renderHeader={renderHeader} renderNodePalette={renderNodePalette} + canvasActions={canvasActions} /> @@ -416,6 +423,7 @@ const AgentflowCanvasWithRef = forwardRef< enableGenerator?: boolean renderHeader?: AgentflowProps['renderHeader'] renderNodePalette?: AgentflowProps['renderNodePalette'] + canvasActions?: AgentflowProps['canvasActions'] } >(function AgentflowCanvasWithRef(props, ref) { const agentflow = useAgentflow() diff --git a/packages/agentflow/src/core/types/agentflow.ts b/packages/agentflow/src/core/types/agentflow.ts index 6acee224567..522b6b69be9 100644 --- a/packages/agentflow/src/core/types/agentflow.ts +++ b/packages/agentflow/src/core/types/agentflow.ts @@ -59,6 +59,21 @@ export interface AgentflowProps { /** Whether the canvas is read-only */ readOnly?: boolean + /** + * Additional buttons rendered in the top-right canvas overlay, to the right of the built-in + * validation FAB. Consumers have full control over content — pass any ReactNode (FABs, icon + * buttons, popovers, dialogs, etc.). Hidden when `readOnly` is true. + * + * @example + * // Add a chat button alongside the validation FAB (mirrors legacy v2 pattern) + * canvasActions={ + * setShowChat(true)}> + * + * + * } + */ + canvasActions?: ReactNode + /** Custom header renderer - receives save/export handlers */ renderHeader?: (props: HeaderRenderProps) => ReactNode diff --git a/packages/agentflow/src/features/canvas/components/ValidationFeedback.tsx b/packages/agentflow/src/features/canvas/components/ValidationFeedback.tsx index beebb4f0f4b..29e3b96a14f 100644 --- a/packages/agentflow/src/features/canvas/components/ValidationFeedback.tsx +++ b/packages/agentflow/src/features/canvas/components/ValidationFeedback.tsx @@ -144,7 +144,7 @@ function ValidationFeedbackComponent({ nodes, edges, availableNodes, setNodes }: return ( <> -
+
Date: Wed, 15 Apr 2026 13:06:11 -0700 Subject: [PATCH 18/21] chore: bump @flowiseai/agentflow to 0.0.0-dev.11 (#6216) Co-authored-by: github-actions[bot] --- packages/agentflow/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agentflow/package.json b/packages/agentflow/package.json index 13fed731b53..1e8eee043da 100644 --- a/packages/agentflow/package.json +++ b/packages/agentflow/package.json @@ -1,6 +1,6 @@ { "name": "@flowiseai/agentflow", - "version": "0.0.0-dev.10", + "version": "0.0.0-dev.11", "description": "Embeddable React component for building and visualizing AI agent workflows", "license": "Apache-2.0", "repository": { From 73d57db9d2bdb3584bb923f1964a96ca4beb5f15 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:29:00 -0700 Subject: [PATCH 19/21] chore: bump @flowiseai/agentflow to 0.0.0-dev.12 (#6225) Co-authored-by: github-actions[bot] --- packages/agentflow/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agentflow/package.json b/packages/agentflow/package.json index 1e8eee043da..5360f3e8a9a 100644 --- a/packages/agentflow/package.json +++ b/packages/agentflow/package.json @@ -1,6 +1,6 @@ { "name": "@flowiseai/agentflow", - "version": "0.0.0-dev.11", + "version": "0.0.0-dev.12", "description": "Embeddable React component for building and visualizing AI agent workflows", "license": "Apache-2.0", "repository": { From 85df44bca4f5f6cde83098c41bf0afa209e3ef9a Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Thu, 16 Apr 2026 14:57:43 +0100 Subject: [PATCH 20/21] Feat/FlowConfigDialog UI Redesign (#6229) * enhance ChatflowConfigurationDialog with new configuration sections and improved UI * fix: remove background color override for MuiSvgIcon in dark mode --- packages/ui/src/assets/images/langchain.png | Bin 58913 -> 5357 bytes packages/ui/src/themes/compStyleOverride.js | 3 +- .../dialog/ChatflowConfigurationDialog.jsx | 554 ++++++++++++++---- .../ui-component/extended/AllowedDomains.jsx | 31 +- .../src/ui-component/extended/AnalyseFlow.jsx | 17 +- .../ui-component/extended/ChatFeedback.jsx | 16 +- .../src/ui-component/extended/FileUpload.jsx | 149 ++--- .../ui-component/extended/FollowUpPrompts.jsx | 8 +- .../ui/src/ui-component/extended/Leads.jsx | 18 +- .../src/ui-component/extended/McpServer.jsx | 32 +- .../ui-component/extended/OverrideConfig.jsx | 120 +++- .../ui-component/extended/PostProcessing.jsx | 93 ++- .../src/ui-component/extended/RateLimit.jsx | 55 +- .../ui-component/extended/SpeechToText.jsx | 18 +- .../ui-component/extended/StarterPrompts.jsx | 44 +- .../ui-component/extended/TextToSpeech.jsx | 18 +- 16 files changed, 826 insertions(+), 350 deletions(-) diff --git a/packages/ui/src/assets/images/langchain.png b/packages/ui/src/assets/images/langchain.png index 587dd314071048b3b8d892e0ba7f409c12da2712..d8ee337ff1fd51121412cf7a67f772e1f669167f 100644 GIT binary patch literal 5357 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 literal 58913 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 @Bok*SLeD@x@yGivkx9M1+2R)2) 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°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 S_sACEb*U(5$k)o6h5Qczr~XI) diff --git a/packages/ui/src/themes/compStyleOverride.js b/packages/ui/src/themes/compStyleOverride.js index c04cc3f17f4..18014c70572 100644 --- a/packages/ui/src/themes/compStyleOverride.js +++ b/packages/ui/src/themes/compStyleOverride.js @@ -45,8 +45,7 @@ export default function componentStyleOverrides(theme) { MuiSvgIcon: { styleOverrides: { root: { - color: theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit', - background: theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryLight : 'inherit' + color: theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit' } } }, diff --git a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx index f657b96ad14..c5b117c11c3 100644 --- a/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ChatflowConfigurationDialog.jsx @@ -1,11 +1,33 @@ import PropTypes from 'prop-types' -import { useState } from 'react' +import { useState, useMemo } from 'react' import { createPortal } from 'react-dom' -import { Box, Dialog, DialogContent, DialogTitle, Tabs, Tab } from '@mui/material' -import { tabsClasses } from '@mui/material/Tabs' +import { useSelector } from 'react-redux' +import { Box, Dialog, DialogContent, DialogTitle, Typography, IconButton } from '@mui/material' +import { useTheme } from '@mui/material/styles' +import { + IconX, + IconShieldLock, + IconWorldWww, + IconUserPlus, + IconMessageChatbot, + IconArrowForwardUp, + IconThumbUp, + IconMicrophone, + IconVolume, + IconUpload, + IconChartBar, + IconCode, + IconServer, + IconAdjustments +} from '@tabler/icons-react' +import PerfectScrollbar from 'react-perfect-scrollbar' + +// Section components import SpeechToText from '@/ui-component/extended/SpeechToText' import TextToSpeech from '@/ui-component/extended/TextToSpeech' -import Security from '@/ui-component/extended/Security' +import RateLimit from '@/ui-component/extended/RateLimit' +import AllowedDomains from '@/ui-component/extended/AllowedDomains' +import OverrideConfig from '@/ui-component/extended/OverrideConfig' import ChatFeedback from '@/ui-component/extended/ChatFeedback' import AnalyseFlow from '@/ui-component/extended/AnalyseFlow' import StarterPrompts from '@/ui-component/extended/StarterPrompts' @@ -15,146 +37,448 @@ import FileUpload from '@/ui-component/extended/FileUpload' import PostProcessing from '@/ui-component/extended/PostProcessing' import McpServer from '@/ui-component/extended/McpServer' -const CHATFLOW_CONFIGURATION_TABS = [ - { - label: 'Security', - id: 'security' - }, - { - label: 'Starter Prompts', - id: 'conversationStarters' - }, - { - label: 'Follow-up Prompts', - id: 'followUpPrompts' - }, - { - label: 'Speech to Text', - id: 'speechToText' - }, - { - label: 'Text to Speech', - id: 'textToSpeech' - }, - { - label: 'Chat Feedback', - id: 'chatFeedback' - }, - { - label: 'Analyse Chatflow', - id: 'analyseChatflow' - }, +const CONFIGURATION_GROUPS = [ { - label: 'Leads', - id: 'leads' + label: 'General', + sections: [ + { + label: 'Rate Limit', + id: 'rateLimit', + icon: IconShieldLock, + description: 'Limit API requests per time window' + }, + { + label: 'Allowed Domains', + id: 'allowedDomains', + icon: IconWorldWww, + description: 'Restrict chatbot to specific domains' + }, + { + label: 'Leads', + id: 'leads', + icon: IconUserPlus, + description: 'Capture visitor contact information' + } + ] }, { - label: 'File Upload', - id: 'fileUpload' + label: 'Chat', + sections: [ + { + label: 'Starter Prompts', + id: 'conversationStarters', + icon: IconMessageChatbot, + description: 'Suggested prompts for new conversations' + }, + { + label: 'Follow-up Prompts', + id: 'followUpPrompts', + icon: IconArrowForwardUp, + description: 'Auto-generate follow-up questions' + }, + { + label: 'Chat Feedback', + id: 'chatFeedback', + icon: IconThumbUp, + description: 'Allow users to rate responses' + } + ] }, { - label: 'Post Processing', - id: 'postProcessing' + label: 'Media & Files', + sections: [ + { + label: 'Speech to Text', + id: 'speechToText', + icon: IconMicrophone, + description: 'Voice input transcription' + }, + { + label: 'Text to Speech', + id: 'textToSpeech', + icon: IconVolume, + description: 'Audio response playback' + }, + { + label: 'File Upload', + id: 'fileUpload', + icon: IconUpload, + description: 'Allow file uploads in chat' + } + ] }, { - label: 'MCP Server', - id: 'mcpServer' + label: 'Advanced', + sections: [ + { + label: 'Analytics', + id: 'analyseChatflow', + icon: IconChartBar, + description: 'Connect analytics providers' + }, + { + label: 'Post Processing', + id: 'postProcessing', + icon: IconCode, + description: 'Custom JavaScript post-processing' + }, + { + label: 'MCP Server', + id: 'mcpServer', + icon: IconServer, + description: 'Model Context Protocol server' + }, + { + label: 'Override Config', + id: 'overrideConfig', + icon: IconAdjustments, + description: 'Override flow configuration via API' + } + ] } ] -function TabPanel(props) { - const { children, value, index, ...other } = props - return ( - - ) -} +function getSectionStatus(sectionId, chatflow) { + if (!chatflow) return false -TabPanel.propTypes = { - children: PropTypes.node, - index: PropTypes.number.isRequired, - value: PropTypes.number.isRequired -} + let chatbotConfig = {} + let apiConfig = {} + try { + chatbotConfig = chatflow.chatbotConfig ? JSON.parse(chatflow.chatbotConfig) : {} + apiConfig = chatflow.apiConfig ? JSON.parse(chatflow.apiConfig) : {} + } catch { + return false + } -function a11yProps(index) { - return { - id: `chatflow-config-tab-${index}`, - 'aria-controls': `chatflow-config-tabpanel-${index}` + switch (sectionId) { + case 'rateLimit': + return apiConfig?.rateLimit?.status === true + case 'allowedDomains': + return Array.isArray(chatbotConfig?.allowedOrigins) && chatbotConfig.allowedOrigins.some((o) => o && o.trim() !== '') + case 'leads': + return chatbotConfig?.leads?.status === true + case 'conversationStarters': { + const sp = chatbotConfig?.starterPrompts + if (!sp) return false + return Object.values(sp).some((entry) => entry?.prompt && entry.prompt.trim() !== '') + } + case 'followUpPrompts': + return chatbotConfig?.followUpPrompts?.status === true + case 'chatFeedback': + return chatbotConfig?.chatFeedback?.status === true + case 'speechToText': { + const stt = chatbotConfig?.speechToText + if (!stt) return false + return Object.values(stt).some((provider) => provider?.status === true) + } + case 'textToSpeech': { + const tts = chatbotConfig?.textToSpeech + if (!tts) return false + return Object.values(tts).some((provider) => provider?.status === true) + } + case 'fileUpload': + return chatbotConfig?.fullFileUpload?.status === true + case 'analyseChatflow': { + const ap = chatbotConfig?.analyticsProviders + if (!ap) return false + return Object.values(ap).some((provider) => provider?.status === true) + } + case 'postProcessing': + return chatbotConfig?.postProcessing?.status === true + case 'mcpServer': + return chatbotConfig?.mcpServer?.status === true + case 'overrideConfig': + return apiConfig?.overrideConfig?.status === true + default: + return false } } +// Flatten all sections for quick lookup +const ALL_SECTIONS = CONFIGURATION_GROUPS.flatMap((g) => g.sections) + +const SIDEBAR_WIDTH = 220 + const ChatflowConfigurationDialog = ({ show, isAgentCanvas, dialogProps, onCancel }) => { const portalElement = document.getElementById('portal') - const [tabValue, setTabValue] = useState(0) + const theme = useTheme() + const chatflow = useSelector((state) => state.canvas.chatflow) + const customization = useSelector((state) => state.customization) + + const [activeSection, setActiveSection] = useState('rateLimit') + const [mcpServerEnabled, setMcpServerEnabled] = useState(false) + + const isDark = theme.palette.mode === 'dark' || customization?.isDarkMode + + // Filter groups/sections based on agent canvas + const filteredGroups = useMemo(() => { + return CONFIGURATION_GROUPS.map((group) => ({ + ...group, + sections: group.sections.filter((section) => !isAgentCanvas || !section.hideInAgentFlow) + })).filter((group) => group.sections.length > 0) + }, [isAgentCanvas]) + + // Get all section IDs for validation + const allSectionIds = useMemo(() => { + return filteredGroups.flatMap((g) => g.sections.map((s) => s.id)) + }, [filteredGroups]) - const filteredTabs = CHATFLOW_CONFIGURATION_TABS.filter((tab) => !isAgentCanvas || !tab.hideInAgentFlow) + // Reset activeSection if current one is filtered out + const currentSection = allSectionIds.includes(activeSection) ? activeSection : allSectionIds[0] || 'rateLimit' + const currentSectionData = ALL_SECTIONS.find((s) => s.id === currentSection) + + const renderContent = () => { + const props = { dialogProps } + switch (currentSection) { + case 'rateLimit': + return + case 'allowedDomains': + return + case 'leads': + return + case 'conversationStarters': + return + case 'followUpPrompts': + return + case 'chatFeedback': + return + case 'speechToText': + return + case 'textToSpeech': + return + case 'fileUpload': + return + case 'analyseChatflow': + return + case 'postProcessing': + return + case 'mcpServer': + return setMcpServerEnabled(enabled)} /> + case 'overrideConfig': + return + default: + return null + } + } const component = show ? ( - - {dialogProps.title} + {/* Header */} + + {dialogProps.title} + + + - - + {/* Sidebar */} + + + + {filteredGroups.map((group, groupIndex) => ( + + {/* Group label */} + 0 ? 2 : 0.5, + pb: 0.75 + }} + > + {group.label} + + + {/* Section items */} + {group.sections.map((section) => { + const isActive = currentSection === section.id + const isEnabled = + section.id === 'mcpServer' ? mcpServerEnabled : getSectionStatus(section.id, chatflow) + const SectionIcon = section.icon + + return ( + setActiveSection(section.id)} + sx={{ + display: 'flex', + alignItems: 'center', + gap: 1.25, + px: 1.5, + py: 0.875, + borderRadius: '8px', + cursor: 'pointer', + position: 'relative', + color: isActive ? 'primary.main' : isDark ? 'grey.300' : 'grey.700', + bgcolor: isActive + ? isDark + ? 'rgba(33, 150, 243, 0.12)' + : 'rgba(33, 150, 243, 0.06)' + : 'transparent', + transition: 'all 0.15s ease', + '&:hover': { + bgcolor: isActive + ? isDark + ? 'rgba(33, 150, 243, 0.16)' + : 'rgba(33, 150, 243, 0.08)' + : isDark + ? 'rgba(255,255,255,0.04)' + : 'rgba(0,0,0,0.03)' + }, + userSelect: 'none' + }} + > + {/* Icon */} + + + {/* Label */} + + {section.label} + + + {/* Status badge - only show when enabled */} + {isEnabled && ( + + ON + + )} + + ) + })} + + ))} + + + + + {/* Content area */} + setTabValue(value)} - aria-label='tabs' - variant='scrollable' - scrollButtons='auto' > - {filteredTabs.map((item, index) => ( - - ))} - - {filteredTabs.map((item, index) => ( - - {item.id === 'security' && } - {item.id === 'conversationStarters' ? : null} - {item.id === 'followUpPrompts' ? : null} - {item.id === 'speechToText' ? : null} - {item.id === 'textToSpeech' ? : null} - {item.id === 'chatFeedback' ? : null} - {item.id === 'analyseChatflow' ? : null} - {item.id === 'leads' ? : null} - {item.id === 'fileUpload' ? : null} - {item.id === 'postProcessing' ? : null} - {item.id === 'mcpServer' ? : null} - - ))} + + + {/* Section header */} + + + {currentSectionData?.label || ''} + + {currentSectionData?.description && ( + + {currentSectionData.description} + + )} + + + {/* Section content */} + {renderContent()} + + + ) : null diff --git a/packages/ui/src/ui-component/extended/AllowedDomains.jsx b/packages/ui/src/ui-component/extended/AllowedDomains.jsx index 75ae32a6334..29a44a5792e 100644 --- a/packages/ui/src/ui-component/extended/AllowedDomains.jsx +++ b/packages/ui/src/ui-component/extended/AllowedDomains.jsx @@ -17,7 +17,7 @@ import useNotifier from '@/utils/useNotifier' // API import chatflowsApi from '@/api/chatflows' -const AllowedDomains = ({ dialogProps, onConfirm }) => { +const AllowedDomains = ({ dialogProps, onConfirm, hideTitle = false }) => { const dispatch = useDispatch() useNotifier() @@ -119,14 +119,16 @@ const AllowedDomains = ({ dialogProps, onConfirm }) => { }, [dialogProps]) return ( - - - Allowed Domains - - + + {!hideTitle && ( + + Allowed Domains + + + )} Domains @@ -193,16 +195,19 @@ const AllowedDomains = ({ dialogProps, onConfirm }) => { /> - - Save - + + + Save + + ) } AllowedDomains.propTypes = { dialogProps: PropTypes.object, - onConfirm: PropTypes.func + onConfirm: PropTypes.func, + hideTitle: PropTypes.bool } export default AllowedDomains diff --git a/packages/ui/src/ui-component/extended/AnalyseFlow.jsx b/packages/ui/src/ui-component/extended/AnalyseFlow.jsx index de162e51a34..84038f4c69c 100644 --- a/packages/ui/src/ui-component/extended/AnalyseFlow.jsx +++ b/packages/ui/src/ui-component/extended/AnalyseFlow.jsx @@ -15,6 +15,7 @@ import { ListItemAvatar, ListItemText } from '@mui/material' +import { useTheme } from '@mui/material/styles' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { IconX } from '@tabler/icons-react' @@ -221,6 +222,7 @@ const analyticProviders = [ const AnalyseFlow = ({ dialogProps }) => { const dispatch = useDispatch() + const theme = useTheme() useNotifier() @@ -339,7 +341,12 @@ const AnalyseFlow = ({ dialogProps }) => { sx={{ ml: 1 }} primary={provider.label} secondary={ - + {provider.url} } @@ -416,9 +423,11 @@ const AnalyseFlow = ({ dialogProps }) => { ))} - - Save - + + + Save + + ) } diff --git a/packages/ui/src/ui-component/extended/ChatFeedback.jsx b/packages/ui/src/ui-component/extended/ChatFeedback.jsx index 132d073e2be..ca55dc5b718 100644 --- a/packages/ui/src/ui-component/extended/ChatFeedback.jsx +++ b/packages/ui/src/ui-component/extended/ChatFeedback.jsx @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react' import PropTypes from 'prop-types' // material-ui -import { Button, Box } from '@mui/material' +import { Button, Box, Stack } from '@mui/material' import { IconX } from '@tabler/icons-react' // Project import @@ -91,14 +91,14 @@ const ChatFeedback = ({ dialogProps, onConfirm }) => { }, [dialogProps]) return ( - <> - - + + + + + Save + - - Save - - + ) } diff --git a/packages/ui/src/ui-component/extended/FileUpload.jsx b/packages/ui/src/ui-component/extended/FileUpload.jsx index d688cb75460..fc561359088 100644 --- a/packages/ui/src/ui-component/extended/FileUpload.jsx +++ b/packages/ui/src/ui-component/extended/FileUpload.jsx @@ -5,7 +5,19 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba import parser from 'html-react-parser' // material-ui -import { Button, Box, Typography, FormControl, RadioGroup, FormControlLabel, Radio } from '@mui/material' +import { + Button, + Box, + Typography, + FormControl, + RadioGroup, + FormControlLabel, + Radio, + Accordion, + AccordionSummary, + AccordionDetails +} from '@mui/material' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { IconX, IconBulb } from '@tabler/icons-react' // Project import @@ -20,7 +32,7 @@ import chatflowsApi from '@/api/chatflows' const message = `The full contents of uploaded files will be converted to text and sent to the Agent.
-Refer docs for more details.` +Refer docs for more details.` const availableFileTypes = [ { name: 'CSS', ext: 'text/css', extension: '.css' }, @@ -52,8 +64,6 @@ const FileUpload = ({ dialogProps }) => { const [allowedFileTypes, setAllowedFileTypes] = useState([]) const [chatbotConfig, setChatbotConfig] = useState({}) const [pdfUsage, setPdfUsage] = useState('perPage') - const [pdfLegacyBuild, setPdfLegacyBuild] = useState(false) - const handleChange = (value) => { setFullFileUpload(value) } @@ -71,18 +81,13 @@ const FileUpload = ({ dialogProps }) => { setPdfUsage(event.target.value) } - const handleLegacyBuildChange = (value) => { - setPdfLegacyBuild(value) - } - const onSave = async () => { try { const value = { status: fullFileUpload, allowedUploadFileTypes: allowedFileTypes.join(','), pdfFile: { - usage: pdfUsage, - legacyBuild: pdfLegacyBuild + usage: pdfUsage } } chatbotConfig.fullFileUpload = value @@ -140,13 +145,8 @@ const FileUpload = ({ dialogProps }) => { const allowedFileTypes = chatbotConfig.fullFileUpload.allowedUploadFileTypes.split(',') setAllowedFileTypes(allowedFileTypes) } - if (chatbotConfig.fullFileUpload?.pdfFile) { - if (chatbotConfig.fullFileUpload.pdfFile.usage) { - setPdfUsage(chatbotConfig.fullFileUpload.pdfFile.usage) - } - if (chatbotConfig.fullFileUpload.pdfFile.legacyBuild !== undefined) { - setPdfLegacyBuild(chatbotConfig.fullFileUpload.pdfFile.legacyBuild) - } + if (chatbotConfig.fullFileUpload?.pdfFile?.usage) { + setPdfUsage(chatbotConfig.fullFileUpload.pdfFile.usage) } } catch (e) { setChatbotConfig({}) @@ -170,31 +170,27 @@ const FileUpload = ({ dialogProps }) => { mb: 2 }} > -
-
- - {parser(message)} -
-
+ + {parser(message)} +
- Allow Uploads of Type + Allow Uploads of Type
{ disabled={!fullFileUpload} onChange={handleAllowedFileTypesChange} /> -
))}

- {allowedFileTypes.includes('application/pdf') && fullFileUpload && ( - - } + sx={{ minHeight: 40, px: 2, '& .MuiAccordionSummary-content': { my: 0.75 } }} > - PDF Configuration - - - - PDF Usage - - - } label='One document per page' /> - } label='One document per file' /> - - - - - - - - + Advanced Settings + + + {/* PDF Processing */} + {allowedFileTypes.includes('application/pdf') && ( + + + PDF Processing + + + + } + label={One document per page} + /> + } + label={One document per file} + /> + + + + )} + + )} - - Save - + + + Save + + ) } diff --git a/packages/ui/src/ui-component/extended/FollowUpPrompts.jsx b/packages/ui/src/ui-component/extended/FollowUpPrompts.jsx index bc0d95bc1a5..8a7a711b88e 100644 --- a/packages/ui/src/ui-component/extended/FollowUpPrompts.jsx +++ b/packages/ui/src/ui-component/extended/FollowUpPrompts.jsx @@ -601,9 +601,11 @@ const FollowUpPrompts = ({ dialogProps }) => { )} - - Save - + + + Save + + ) } diff --git a/packages/ui/src/ui-component/extended/Leads.jsx b/packages/ui/src/ui-component/extended/Leads.jsx index 395902986c8..58fa8169bb1 100644 --- a/packages/ui/src/ui-component/extended/Leads.jsx +++ b/packages/ui/src/ui-component/extended/Leads.jsx @@ -160,14 +160,16 @@ const Leads = ({ dialogProps }) => { )} - - Save - + + + Save + + ) } diff --git a/packages/ui/src/ui-component/extended/McpServer.jsx b/packages/ui/src/ui-component/extended/McpServer.jsx index 892c0dba837..b712130a9d7 100644 --- a/packages/ui/src/ui-component/extended/McpServer.jsx +++ b/packages/ui/src/ui-component/extended/McpServer.jsx @@ -23,7 +23,7 @@ import useNotifier from '@/utils/useNotifier' import mcpServerApi from '@/api/mcpserver' import chatflowsApi from '@/api/chatflows' -const McpServer = ({ dialogProps }) => { +const McpServer = ({ dialogProps, onStatusChange }) => { const dispatch = useDispatch() const theme = useTheme() const customization = useSelector((state) => state.customization) @@ -123,6 +123,7 @@ const McpServer = ({ dialogProps }) => { setToken(resp.data.token || '') setToolName(resp.data.toolName || '') setDescription(resp.data.description || '') + onStatusChange?.(resp.data.enabled) showSuccess('MCP Server settings saved') } } else { @@ -136,12 +137,14 @@ const McpServer = ({ dialogProps }) => { setToolName(resp.data.toolName || '') setDescription(resp.data.description || '') setHasExistingConfig(true) + onStatusChange?.(resp.data.enabled) showSuccess('MCP Server settings saved') } } } else { await mcpServerApi.deleteMcpServerConfig(dialogProps.chatflow.id) setMcpEnabled(false) + onStatusChange?.(false) showSuccess('MCP Server disabled') } await refreshChatflowStore() @@ -215,13 +218,15 @@ const McpServer = ({ dialogProps }) => { useEffect(() => { if (getMcpServerConfigApi.data) { - setMcpEnabled(getMcpServerConfigApi.data.enabled || false) + const enabled = getMcpServerConfigApi.data.enabled || false + setMcpEnabled(enabled) setToolName(getMcpServerConfigApi.data.toolName || '') setDescription(getMcpServerConfigApi.data.description || '') setToken(getMcpServerConfigApi.data.token || '') setHasExistingConfig(!!getMcpServerConfigApi.data.token) + onStatusChange?.(enabled) } - }, [getMcpServerConfigApi.data]) + }, [getMcpServerConfigApi.data]) // eslint-disable-line react-hooks/exhaustive-deps if (getMcpServerConfigApi.loading) { return ( @@ -391,20 +396,23 @@ const McpServer = ({ dialogProps }) => { )} - - {loading ? 'Saving...' : 'Save'} - + + + {loading ? 'Saving...' : 'Save'} + + ) } McpServer.propTypes = { - dialogProps: PropTypes.object + dialogProps: PropTypes.object, + onStatusChange: PropTypes.func } export default McpServer diff --git a/packages/ui/src/ui-component/extended/OverrideConfig.jsx b/packages/ui/src/ui-component/extended/OverrideConfig.jsx index 32397b4b973..0ca1b4c85fd 100644 --- a/packages/ui/src/ui-component/extended/OverrideConfig.jsx +++ b/packages/ui/src/ui-component/extended/OverrideConfig.jsx @@ -5,6 +5,7 @@ import { Accordion, AccordionDetails, AccordionSummary, + Box, Button, Paper, Stack, @@ -17,7 +18,6 @@ import { Typography, Card } from '@mui/material' -import { useTheme } from '@mui/material/styles' // Project import import { StyledButton } from '@/ui-component/button/StyledButton' @@ -39,6 +39,9 @@ import variablesApi from '@/api/variables' // utils const OverrideConfigTable = ({ columns, onToggle, rows, sx }) => { + const customization = useSelector((state) => state.customization) + const isDark = customization?.isDarkMode + const handleChange = (enabled, row) => { onToggle(row, enabled) } @@ -47,10 +50,8 @@ const OverrideConfigTable = ({ columns, onToggle, rows, sx }) => { if (key === 'enabled') { return handleChange(enabled, row)} value={row.enabled} /> } else if (key === 'type' && row.schema) { - // If there's schema information, add a tooltip let schemaContent if (Array.isArray(row.schema)) { - // Handle array format: [{ name: "field", type: "string" }, ...] schemaContent = '[
' + row.schema @@ -67,30 +68,53 @@ const OverrideConfigTable = ({ columns, onToggle, rows, sx }) => { .join(',
') + '
]' } else if (typeof row.schema === 'object' && row.schema !== null) { - // Handle object format: { "field": "string", "field2": "number", ... } schemaContent = JSON.stringify(row.schema, null, 2).replace(/\n/g, '
').replace(/ /g, ' ') } else { schemaContent = 'No schema available' } return ( - - {row[key]} + + {row[key]} Schema:
${schemaContent}
`} /> ) } else { - return row[key] + return {row[key]} } } + const columnLabels = { label: 'Label', name: 'Name', type: 'Type', enabled: 'On' } + return ( - - + +
{columns.map((col, index) => ( - {col.charAt(0).toUpperCase() + col.slice(1)} + + {columnLabels[col] || col.charAt(0).toUpperCase() + col.slice(1)} + ))} @@ -99,7 +123,19 @@ const OverrideConfigTable = ({ columns, onToggle, rows, sx }) => { {Object.keys(row).map((key, index) => { if (key !== 'id' && key !== 'schema') { - return {renderCellContent(key, row)} + return ( + + {renderCellContent(key, row)} + + ) } })} @@ -117,14 +153,14 @@ OverrideConfigTable.propTypes = { onToggle: PropTypes.func } -const OverrideConfig = ({ dialogProps }) => { +const OverrideConfig = ({ dialogProps, hideTitle = false }) => { const dispatch = useDispatch() + const customization = useSelector((state) => state.customization) const chatflow = useSelector((state) => state.canvas.chatflow) const chatflowid = chatflow.id const apiConfig = chatflow.apiConfig ? JSON.parse(chatflow.apiConfig) : {} useNotifier() - const theme = useTheme() const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) @@ -359,22 +395,32 @@ const OverrideConfig = ({ dialogProps }) => { }, [getAllVariablesApi.data]) return ( - - - Override Configuration - documentation for more information.' - } - /> - + + {!hideTitle && ( + + Override Configuration + documentation for more information.' + } + /> + + )} {overrideConfigStatus && ( <> {nodeOverrides && nodeConfig && ( - + Nodes @@ -388,6 +434,11 @@ const OverrideConfig = ({ dialogProps }) => { onChange={handleAccordionChange(nodeLabel)} key={nodeLabel} disableGutters + sx={{ + '&:before': { + bgcolor: customization.isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.06)' + } + }} > } @@ -444,7 +495,15 @@ const OverrideConfig = ({ dialogProps }) => { )} {variableOverrides && variableOverrides.length > 0 && ( - + Variables @@ -459,15 +518,18 @@ const OverrideConfig = ({ dialogProps }) => { )} - - Save - + + + Save + + ) } OverrideConfig.propTypes = { - dialogProps: PropTypes.object + dialogProps: PropTypes.object, + hideTitle: PropTypes.bool } export default OverrideConfig diff --git a/packages/ui/src/ui-component/extended/PostProcessing.jsx b/packages/ui/src/ui-component/extended/PostProcessing.jsx index fb9543a55d5..e6cab9400df 100644 --- a/packages/ui/src/ui-component/extended/PostProcessing.jsx +++ b/packages/ui/src/ui-component/extended/PostProcessing.jsx @@ -179,7 +179,7 @@ const PostProcessing = ({ dialogProps }) => { style={{ marginTop: '10px', border: '1px solid', - borderColor: theme.palette.grey['300'], + borderColor: customization.isDarkMode ? 'rgba(255,255,255,0.12)' : theme.palette.grey['300'], borderRadius: '6px', height: '200px', width: '100%' @@ -196,7 +196,16 @@ const PostProcessing = ({ dialogProps }) => { /> - + { }} > }> - Available Variables + Available Variables - -
+ +
- Variable - Type - Description + + Variable + + + Type + + + Description + - + $flow.rawOutput @@ -283,11 +334,11 @@ const PostProcessing = ({ dialogProps }) => { List of artifacts generated during execution - + $flow.fileAnnotations - array - File annotations associated with the response + array + File annotations associated with the response
@@ -295,14 +346,16 @@ const PostProcessing = ({ dialogProps }) => { - - Save - + + + Save + + { +const RateLimit = ({ dialogProps, hideTitle = false }) => { const dispatch = useDispatch() const chatflow = useSelector((state) => state.canvas.chatflow) const chatflowid = chatflow.id @@ -147,36 +147,39 @@ const RateLimit = ({ dialogProps }) => { } return ( - - - Rate Limit{' '} - Rate Limit Setup Guide to set up Rate Limit correctly in your hosting environment.' - } - /> - - - - {rateLimitStatus && ( - - {textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number', '5')} - {textField(limitDuration, 'limitDuration', 'Duration in Second', 'number', '60')} - {textField(limitMsg, 'limitMsg', 'Limit Message', 'string', 'You have reached the quota')} - - )} - - onSave()} sx={{ width: 'auto' }}> - Save - + + {!hideTitle && ( + + Rate Limit{' '} + Rate Limit Setup Guide to set up Rate Limit correctly in your hosting environment.' + } + /> + + )} + + {rateLimitStatus && ( + + {textField(limitMax, 'limitMax', 'Message Limit per Duration', 'number', '5')} + {textField(limitDuration, 'limitDuration', 'Duration in Second', 'number', '60')} + {textField(limitMsg, 'limitMsg', 'Limit Message', 'string', 'You have reached the quota')} + + )} + + onSave()} sx={{ minWidth: 100 }}> + Save + + ) } RateLimit.propTypes = { isSessionMemory: PropTypes.bool, - dialogProps: PropTypes.object + dialogProps: PropTypes.object, + hideTitle: PropTypes.bool } export default RateLimit diff --git a/packages/ui/src/ui-component/extended/SpeechToText.jsx b/packages/ui/src/ui-component/extended/SpeechToText.jsx index 2c732b2d02b..2ca7fd95c28 100644 --- a/packages/ui/src/ui-component/extended/SpeechToText.jsx +++ b/packages/ui/src/ui-component/extended/SpeechToText.jsx @@ -478,14 +478,16 @@ const SpeechToText = ({ dialogProps, onConfirm }) => { ))} )} - - Save - + + + Save + + ) } diff --git a/packages/ui/src/ui-component/extended/StarterPrompts.jsx b/packages/ui/src/ui-component/extended/StarterPrompts.jsx index 31504fc59e6..861bea1e125 100644 --- a/packages/ui/src/ui-component/extended/StarterPrompts.jsx +++ b/packages/ui/src/ui-component/extended/StarterPrompts.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction, SET_CHATFLOW } from '@/store/actions' // material-ui -import { Button, IconButton, OutlinedInput, Box, List, InputAdornment } from '@mui/material' +import { Button, IconButton, OutlinedInput, Box, List, InputAdornment, Typography } from '@mui/material' import { IconX, IconTrash, IconPlus, IconBulb } from '@tabler/icons-react' // Project import @@ -133,28 +133,24 @@ const StarterPrompts = ({ dialogProps, onConfirm }) => { return ( <> -
-
- - - Starter prompts will only be shown when there is no messages on the chat - -
-
+ + + Starter prompts will only be shown when there are no messages on the chat + + :not(style)': { m: 1 }, pt: 2 }}> {inputFields.map((data, index) => { @@ -199,9 +195,11 @@ const StarterPrompts = ({ dialogProps, onConfirm }) => { })} - - Save - + + + Save + + ) } diff --git a/packages/ui/src/ui-component/extended/TextToSpeech.jsx b/packages/ui/src/ui-component/extended/TextToSpeech.jsx index 226d45a1e17..8ce171615a4 100644 --- a/packages/ui/src/ui-component/extended/TextToSpeech.jsx +++ b/packages/ui/src/ui-component/extended/TextToSpeech.jsx @@ -641,14 +641,16 @@ const TextToSpeech = ({ dialogProps }) => { )} - - Save - + + + Save + + ) } From b0cb4b0856605e65c384bb7fdf2cad71e159ef24 Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 18 Apr 2026 14:50:10 +0100 Subject: [PATCH 21/21] - revamp agents and agentflows view pages - smartagents --- .gitignore | 3 +- .../components/nodes/agentflow/Agent/Agent.ts | 20 +- .../nodes/agentflow/SmartAgent/SmartAgent.ts | 2993 +++++++++++++++++ .../SmartAgent/context/SystemPromptBuilder.ts | 169 + .../SmartAgent/planning/PlanningTool.ts | 142 + packages/components/nodes/agentflow/utils.ts | 43 + .../services/organization.service.ts | 38 +- packages/ui/package.json | 1 + packages/ui/src/api/user.js | 4 +- .../src/assets/scss/_themes-vars.module.scss | 2 + .../Sidebar/MenuList/NavGroup/index.jsx | 24 +- .../Sidebar/MenuList/NavItem/index.jsx | 77 +- .../src/layout/MainLayout/Sidebar/index.jsx | 19 +- .../ui/src/layout/MainLayout/ViewHeader.jsx | 13 +- packages/ui/src/layout/MainLayout/index.jsx | 6 +- packages/ui/src/menu-items/agentsettings.js | 2 +- packages/ui/src/store/constant.js | 1 + packages/ui/src/themes/index.js | 2 +- .../agentexecutions/ExecutionDetails.jsx | 8 +- .../agentexecutions/NodeExecutionDetails.jsx | 9 +- .../ui/src/views/agentexecutions/index.jsx | 423 +-- packages/ui/src/views/agentflows/index.jsx | 606 +++- packages/ui/src/views/agents/index.jsx | 613 ++-- packages/ui/src/views/apikey/index.jsx | 283 +- .../CustomAssistantConfigurePreview.jsx | 420 ++- .../custom/CustomAssistantLayout.jsx | 293 +- .../views/assistants/custom/DescribeMode.jsx | 697 ++++ .../openai/OpenAIAssistantLayout.jsx | 128 +- packages/ui/src/views/canvas/AddNodes.jsx | 5 + packages/ui/src/views/chatflows/index.jsx | 186 +- packages/ui/src/views/credentials/index.jsx | 378 ++- packages/ui/src/views/datasets/index.jsx | 298 +- packages/ui/src/views/docstore/index.jsx | 250 +- packages/ui/src/views/evaluations/index.jsx | 375 ++- packages/ui/src/views/evaluators/index.jsx | 770 ++--- packages/ui/src/views/files/index.jsx | 36 +- packages/ui/src/views/marketplaces/index.jsx | 866 ++--- packages/ui/src/views/roles/index.jsx | 211 +- packages/ui/src/views/serverlogs/index.jsx | 200 +- packages/ui/src/views/tools/index.jsx | 218 +- packages/ui/src/views/users/index.jsx | 228 +- packages/ui/src/views/variables/index.jsx | 349 +- .../ui/src/views/workspace/WorkspaceUsers.jsx | 407 +-- packages/ui/src/views/workspace/index.jsx | 219 +- pnpm-lock.yaml | 14 + 45 files changed, 8463 insertions(+), 3586 deletions(-) create mode 100644 packages/components/nodes/agentflow/SmartAgent/SmartAgent.ts create mode 100644 packages/components/nodes/agentflow/SmartAgent/context/SystemPromptBuilder.ts create mode 100644 packages/components/nodes/agentflow/SmartAgent/planning/PlanningTool.ts create mode 100644 packages/ui/src/views/assistants/custom/DescribeMode.jsx diff --git a/.gitignore b/.gitignore index 4f4a2cf85fe..4acfb57966e 100644 --- a/.gitignore +++ b/.gitignore @@ -118,4 +118,5 @@ apps/*/ # Claude - session/user specific files .claude/plans/ .claude/settings.local.json -.claude/agent-memory/* \ No newline at end of file +.claude/agent-memory/* +.claude/launch.json \ No newline at end of file diff --git a/packages/components/nodes/agentflow/Agent/Agent.ts b/packages/components/nodes/agentflow/Agent/Agent.ts index 63730c2d15b..6bc1d9cd09c 100644 --- a/packages/components/nodes/agentflow/Agent/Agent.ts +++ b/packages/components/nodes/agentflow/Agent/Agent.ts @@ -30,6 +30,7 @@ import { getUniqueImageMessages, processMessagesWithImages, revertBase64ImagesToFileRefs, + normalizeMessagesForStorage, replaceInlineDataWithFileReferences, updateFlowState } from '../utils' @@ -1469,7 +1470,8 @@ class Agent_Agentflow implements INode { * This is to avoid storing the actual base64 data into database */ const messagesToStore = messages.filter((msg: any) => !msg._isTemporaryImageMessage) - const messagesWithFileReferences = revertBase64ImagesToFileRefs(messagesToStore) + const normalizedMessagesToStore = normalizeMessagesForStorage(messagesToStore) + const messagesWithFileReferences = revertBase64ImagesToFileRefs(normalizedMessagesToStore) // Only add to runtime chat history if this is the first node const inputMessages = [] @@ -2233,13 +2235,7 @@ class Agent_Agentflow implements INode { } // Add LLM response with tool calls to messages - messages.push({ - id: response.id, - role: 'assistant', - content: response.content, - tool_calls: response.tool_calls, - usage_metadata: response.usage_metadata - }) + messages.push(response) // Process each tool call for (let i = 0; i < response.tool_calls.length; i++) { @@ -2620,13 +2616,7 @@ class Agent_Agentflow implements INode { } // Add LLM response with tool calls to messages - messages.push({ - id: response.id, - role: 'assistant', - content: response.content, - tool_calls: response.tool_calls, - usage_metadata: response.usage_metadata - }) + messages.push(response) // Process each tool call for (let i = 0; i < response.tool_calls.length; i++) { diff --git a/packages/components/nodes/agentflow/SmartAgent/SmartAgent.ts b/packages/components/nodes/agentflow/SmartAgent/SmartAgent.ts new file mode 100644 index 00000000000..39f6f75152d --- /dev/null +++ b/packages/components/nodes/agentflow/SmartAgent/SmartAgent.ts @@ -0,0 +1,2993 @@ +import { BaseChatModel } from '@langchain/core/language_models/chat_models' +import { + ICommonObject, + IDatabaseEntity, + IHumanInput, + IMessage, + INode, + INodeData, + INodeOptionsValue, + INodeParams, + IServerSideEventStreamer, + IUsedTool +} from '../../../src/Interface' +import { ContentBlock } from 'langchain' +import { AIMessageChunk, BaseMessageLike } from '@langchain/core/messages' +import { AnalyticHandler } from '../../../src/handler' +import { DEFAULT_SUMMARIZER_TEMPLATE } from '../prompt' +import { ILLMMessage, IResponseMetadata } from '../Interface.Agentflow' +import { DynamicStructuredTool, Tool } from '@langchain/core/tools' +import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX, TOOL_ARGS_PREFIX } from '../../../src/agents' +import { flatten } from 'lodash' +import zodToJsonSchema from 'zod-to-json-schema' +import { z } from 'zod' +import { PlanningTool, Todo } from './planning/PlanningTool' +import { buildSystemPrompt } from './context/SystemPromptBuilder' +import { getErrorMessage } from '../../../src/error' +import { DataSource } from 'typeorm' +import { randomBytes } from 'crypto' +import { + addImageArtifactsToMessages, + extractArtifactsFromResponse, + getPastChatHistoryImageMessages, + getUniqueImageMessages, + processMessagesWithImages, + revertBase64ImagesToFileRefs, + normalizeMessagesForStorage, + replaceInlineDataWithFileReferences, + updateFlowState +} from '../utils' +import { + convertMultiOptionsToStringArray, + processTemplateVariables, + configureStructuredOutput, + extractResponseContent +} from '../../../src/utils' +import { sanitizeFileName } from '../../../src/validator' +import { getModelConfigByModelName, MODEL_TYPE } from '../../../src/modelLoader' + +interface ITool { + agentSelectedTool: string + agentSelectedToolConfig: ICommonObject + agentSelectedToolRequiresHumanInput: boolean +} + +interface IKnowledgeBase { + documentStore: string + docStoreDescription: string + returnSourceDocuments: boolean +} + +interface IKnowledgeBaseVSEmbeddings { + vectorStore: string + vectorStoreConfig: ICommonObject + embeddingModel: string + embeddingModelConfig: ICommonObject + knowledgeName: string + knowledgeDescription: string + returnSourceDocuments: boolean +} + +interface ISimpliefiedTool { + name: string + description: string + schema: any + toolNode: { + label: string + name: string + } +} + +/** + * Sanitizes a string to be used as a tool name. + * Restricts to ASCII characters [a-z0-9_-] for LLM API compatibility (OpenAI, Anthropic, Gemini). + * Non-ASCII titles (Korean, Chinese, Japanese, etc.) will use auto-generated fallback names. + * This prevents 'Invalid tools[0].function.name: empty string' errors. + */ +const sanitizeToolName = (name: string): string => { + const sanitized = name + .toLowerCase() + .replace(/ /g, '_') + .replace(/[^a-z0-9_-]/g, '') // ASCII only for LLM API compatibility + + // If the result is empty (e.g., non-ASCII only input), generate a unique fallback name + if (!sanitized) { + return `tool_${Date.now()}_${randomBytes(4).toString('hex').slice(0, 5)}` + } + + // Enforce 64 character limit common for tool names + return sanitized.slice(0, 64) +} + +class SmartAgent_Agentflow implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + color: string + baseClasses: string[] + documentation?: string + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Smart Agent' + this.name = 'smartAgentAgentflow' + this.version = 1.0 + this.type = 'Smart Agent' + this.category = 'Agent Flows' + this.description = 'Built in harness for building smart agents' + this.color = '#AB47BC' + this.baseClasses = [this.type] + this.inputs = [ + { + label: 'Model', + name: 'agentModel', + type: 'asyncOptions', + loadMethod: 'listModels', + loadConfig: true + }, + { + label: 'Messages', + name: 'agentMessages', + type: 'array', + optional: true, + acceptVariable: true, + array: [ + { + label: 'Role', + name: 'role', + type: 'options', + options: [ + { + label: 'System', + name: 'system' + }, + { + label: 'Assistant', + name: 'assistant' + }, + { + label: 'Developer', + name: 'developer' + }, + { + label: 'User', + name: 'user' + } + ] + }, + { + label: 'Content', + name: 'content', + type: 'string', + acceptVariable: true, + generateInstruction: true, + rows: 4 + } + ] + }, + { + label: 'OpenAI Built-in Tools', + name: 'agentToolsBuiltInOpenAI', + type: 'multiOptions', + optional: true, + options: [ + { + label: 'Web Search', + name: 'web_search_preview', + description: 'Search the web for the latest information' + }, + { + label: 'Code Interpreter', + name: 'code_interpreter', + description: 'Write and run Python code in a sandboxed environment' + }, + { + label: 'Image Generation', + name: 'image_generation', + description: 'Generate images based on a text prompt' + } + ], + show: { + agentModel: 'chatOpenAI' + } + }, + { + label: 'Gemini Built-in Tools', + name: 'agentToolsBuiltInGemini', + type: 'multiOptions', + optional: true, + options: [ + { + label: 'URL Context', + name: 'urlContext', + description: 'Extract content from given URLs' + }, + { + label: 'Google Search', + name: 'googleSearch', + description: 'Search real-time web content' + }, + { + label: 'Code Execution', + name: 'codeExecution', + description: 'Write and run Python code in a sandboxed environment' + } + ], + show: { + agentModel: 'chatGoogleGenerativeAI' + } + }, + { + label: 'Anthropic Built-in Tools', + name: 'agentToolsBuiltInAnthropic', + type: 'multiOptions', + optional: true, + options: [ + { + label: 'Web Search', + name: 'web_search_20250305', + description: 'Search the web for the latest information' + }, + { + label: 'Web Fetch', + name: 'web_fetch_20250910', + description: 'Retrieve full content from specified web pages' + } + /* + * Not supported yet as we need to get bash_code_execution_tool_result from content: + https://docs.claude.com/en/docs/agents-and-tools/tool-use/code-execution-tool#retrieve-generated-files + { + label: 'Code Interpreter', + name: 'code_execution_20250825', + description: 'Write and run Python code in a sandboxed environment' + }*/ + ], + show: { + agentModel: 'chatAnthropic' + } + }, + { + label: 'Tools', + name: 'agentTools', + type: 'array', + optional: true, + array: [ + { + label: 'Tool', + name: 'agentSelectedTool', + type: 'asyncOptions', + loadMethod: 'listTools', + loadConfig: true + }, + { + label: 'Require Human Input', + name: 'agentSelectedToolRequiresHumanInput', + type: 'boolean', + optional: true + } + ] + }, + { + label: 'Knowledge (Document Stores)', + name: 'agentKnowledgeDocumentStores', + type: 'array', + description: 'Give your agent context about different document sources. Document stores must be upserted in advance.', + array: [ + { + label: 'Document Store', + name: 'documentStore', + type: 'asyncOptions', + loadMethod: 'listStores' + }, + { + label: 'Describe Knowledge', + name: 'docStoreDescription', + type: 'string', + generateDocStoreDescription: true, + placeholder: + 'Describe what the knowledge base is about, this is useful for the AI to know when and how to search for correct information', + rows: 4 + }, + { + label: 'Return Source Documents', + name: 'returnSourceDocuments', + type: 'boolean', + optional: true + } + ], + optional: true + }, + { + label: 'Knowledge (Vector Embeddings)', + name: 'agentKnowledgeVSEmbeddings', + type: 'array', + description: 'Give your agent context about different document sources from existing vector stores and embeddings', + array: [ + { + label: 'Vector Store', + name: 'vectorStore', + type: 'asyncOptions', + loadMethod: 'listVectorStores', + loadConfig: true + }, + { + label: 'Embedding Model', + name: 'embeddingModel', + type: 'asyncOptions', + loadMethod: 'listEmbeddings', + loadConfig: true + }, + { + label: 'Knowledge Name', + name: 'knowledgeName', + type: 'string', + placeholder: + 'A short name for the knowledge base, this is useful for the AI to know when and how to search for correct information' + }, + { + label: 'Describe Knowledge', + name: 'knowledgeDescription', + type: 'string', + placeholder: + 'Describe what the knowledge base is about, this is useful for the AI to know when and how to search for correct information', + rows: 4 + }, + { + label: 'Return Source Documents', + name: 'returnSourceDocuments', + type: 'boolean', + optional: true + } + ], + optional: true + }, + { + label: 'Enable Memory', + name: 'agentEnableMemory', + type: 'boolean', + description: 'Enable memory for the conversation thread', + default: true, + optional: true + }, + { + label: 'Memory Type', + name: 'agentMemoryType', + type: 'options', + options: [ + { + label: 'All Messages', + name: 'allMessages', + description: 'Retrieve all messages from the conversation' + }, + { + label: 'Window Size', + name: 'windowSize', + description: 'Uses a fixed window size to surface the last N messages' + }, + { + label: 'Conversation Summary', + name: 'conversationSummary', + description: 'Summarizes the whole conversation' + }, + { + label: 'Conversation Summary Buffer', + name: 'conversationSummaryBuffer', + description: 'Summarize conversations once token limit is reached. Default to 2000' + } + ], + optional: true, + default: 'allMessages', + show: { + agentEnableMemory: true + } + }, + { + label: 'Window Size', + name: 'agentMemoryWindowSize', + type: 'number', + default: '20', + description: 'Uses a fixed window size to surface the last N messages', + show: { + agentMemoryType: 'windowSize' + } + }, + { + label: 'Max Token Limit', + name: 'agentMemoryMaxTokenLimit', + type: 'number', + default: '2000', + description: 'Summarize conversations once token limit is reached. Default to 2000', + show: { + agentMemoryType: 'conversationSummaryBuffer' + } + }, + { + label: 'Input Message', + name: 'agentUserMessage', + type: 'string', + description: 'Add an input message as user message at the end of the conversation', + rows: 4, + optional: true, + acceptVariable: true, + show: { + agentEnableMemory: true + } + }, + { + label: 'Return Response As', + name: 'agentReturnResponseAs', + type: 'options', + options: [ + { + label: 'User Message', + name: 'userMessage' + }, + { + label: 'Assistant Message', + name: 'assistantMessage' + } + ], + default: 'userMessage' + }, + { + label: 'JSON Structured Output', + name: 'agentStructuredOutput', + description: 'Instruct the Agent to give output in a JSON structured schema', + type: 'array', + optional: true, + acceptVariable: true, + array: [ + { + label: 'Key', + name: 'key', + type: 'string' + }, + { + label: 'Type', + name: 'type', + type: 'options', + options: [ + { + label: 'String', + name: 'string' + }, + { + label: 'String Array', + name: 'stringArray' + }, + { + label: 'Number', + name: 'number' + }, + { + label: 'Boolean', + name: 'boolean' + }, + { + label: 'Enum', + name: 'enum' + }, + { + label: 'JSON Array', + name: 'jsonArray' + } + ] + }, + { + label: 'Enum Values', + name: 'enumValues', + type: 'string', + placeholder: 'value1, value2, value3', + description: 'Enum values. Separated by comma', + optional: true, + show: { + 'agentStructuredOutput[$index].type': 'enum' + } + }, + { + label: 'JSON Schema', + name: 'jsonSchema', + type: 'code', + placeholder: `{ + "answer": { + "type": "string", + "description": "Value of the answer" + }, + "reason": { + "type": "string", + "description": "Reason for the answer" + }, + "optional": { + "type": "boolean" + }, + "count": { + "type": "number" + }, + "children": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Value of the children's answer" + } + } + } + } +}`, + description: 'JSON schema for the structured output', + optional: true, + hideCodeExecute: true, + show: { + 'agentStructuredOutput[$index].type': 'jsonArray' + } + }, + { + label: 'Description', + name: 'description', + type: 'string', + placeholder: 'Description of the key' + } + ] + }, + { + label: 'Update Flow State', + name: 'agentUpdateState', + description: 'Update runtime state during the execution of the workflow', + type: 'array', + optional: true, + acceptVariable: true, + array: [ + { + label: 'Key', + name: 'key', + type: 'asyncOptions', + loadMethod: 'listRuntimeStateKeys' + }, + { + label: 'Value', + name: 'value', + type: 'string', + acceptVariable: true, + acceptNodeOutputAsVariable: true + } + ] + } + ] + } + + //@ts-ignore + loadMethods = { + async listModels(_: INodeData, options: ICommonObject): Promise { + const componentNodes = options.componentNodes as { + [key: string]: INode + } + + const returnOptions: INodeOptionsValue[] = [] + for (const nodeName in componentNodes) { + const componentNode = componentNodes[nodeName] + if (componentNode.category === 'Chat Models') { + if (componentNode.tags?.includes('LlamaIndex')) { + continue + } + returnOptions.push({ + label: componentNode.label, + name: nodeName, + imageSrc: componentNode.icon + }) + } + } + return returnOptions + }, + async listEmbeddings(_: INodeData, options: ICommonObject): Promise { + const componentNodes = options.componentNodes as { + [key: string]: INode + } + + const returnOptions: INodeOptionsValue[] = [] + for (const nodeName in componentNodes) { + const componentNode = componentNodes[nodeName] + if (componentNode.category === 'Embeddings') { + if (componentNode.tags?.includes('LlamaIndex')) { + continue + } + returnOptions.push({ + label: componentNode.label, + name: nodeName, + imageSrc: componentNode.icon + }) + } + } + return returnOptions + }, + async listTools(_: INodeData, options: ICommonObject): Promise { + const componentNodes = options.componentNodes as { + [key: string]: INode + } + + const removeTools = ['chainTool', 'retrieverTool', 'webBrowser'] + + const returnOptions: INodeOptionsValue[] = [] + for (const nodeName in componentNodes) { + const componentNode = componentNodes[nodeName] + if (componentNode.category === 'Tools' || componentNode.category === 'Tools (MCP)') { + if (componentNode.tags?.includes('LlamaIndex')) { + continue + } + if (removeTools.includes(nodeName)) { + continue + } + returnOptions.push({ + label: componentNode.label, + name: nodeName, + imageSrc: componentNode.icon + }) + } + } + return returnOptions + }, + async listRuntimeStateKeys(_: INodeData, options: ICommonObject): Promise { + const previousNodes = options.previousNodes as ICommonObject[] + const startAgentflowNode = previousNodes.find((node) => node.name === 'startAgentflow') + const state = startAgentflowNode?.inputs?.startState as ICommonObject[] + return state.map((item) => ({ label: item.key, name: item.key })) + }, + async listStores(_: INodeData, options: ICommonObject): Promise { + const returnData: INodeOptionsValue[] = [] + + const appDataSource = options.appDataSource as DataSource + const databaseEntities = options.databaseEntities as IDatabaseEntity + + if (appDataSource === undefined || !appDataSource) { + return returnData + } + + const searchOptions = options.searchOptions || {} + const stores = await appDataSource.getRepository(databaseEntities['DocumentStore']).findBy(searchOptions) + for (const store of stores) { + if (store.status === 'UPSERTED') { + const obj = { + name: `${store.id}:${store.name}`, + label: store.name, + description: store.description + } + returnData.push(obj) + } + } + return returnData + }, + async listVectorStores(_: INodeData, options: ICommonObject): Promise { + const componentNodes = options.componentNodes as { + [key: string]: INode + } + + const returnOptions: INodeOptionsValue[] = [] + for (const nodeName in componentNodes) { + const componentNode = componentNodes[nodeName] + if (componentNode.category === 'Vector Stores') { + if (componentNode.tags?.includes('LlamaIndex')) { + continue + } + returnOptions.push({ + label: componentNode.label, + name: nodeName, + imageSrc: componentNode.icon + }) + } + } + return returnOptions + } + } + + async run(nodeData: INodeData, input: string | Record, options: ICommonObject): Promise { + let llmIds: ICommonObject | undefined + let analyticHandlers = options.analyticHandlers as AnalyticHandler + + try { + const abortController = options.abortController as AbortController + + // Extract input parameters + const model = nodeData.inputs?.agentModel as string + const modelConfig = nodeData.inputs?.agentModelConfig as ICommonObject + if (!model) { + throw new Error('Model is required') + } + const modelName = modelConfig?.model ?? modelConfig?.modelName + + // Extract tools + const tools = nodeData.inputs?.agentTools as ITool[] + + const toolsInstance: Tool[] = [] + for (const tool of tools) { + const toolConfig = tool.agentSelectedToolConfig + const nodeInstanceFilePath = options.componentNodes[tool.agentSelectedTool].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const newToolNodeInstance = new nodeModule.nodeClass() + const newNodeData = { + ...nodeData, + credential: toolConfig['FLOWISE_CREDENTIAL_ID'], + inputs: { + ...nodeData.inputs, + ...toolConfig + } + } + const toolInstance = await newToolNodeInstance.init(newNodeData, '', options) + + // toolInstance might returns a list of tools like MCP tools + if (Array.isArray(toolInstance)) { + for (const subTool of toolInstance) { + const subToolInstance = subTool as Tool + ;(subToolInstance as any).agentSelectedTool = tool.agentSelectedTool + if (tool.agentSelectedToolRequiresHumanInput) { + ;(subToolInstance as any).requiresHumanInput = true + } + toolsInstance.push(subToolInstance) + } + } else { + if (tool.agentSelectedToolRequiresHumanInput) { + toolInstance.requiresHumanInput = true + } + toolsInstance.push(toolInstance as Tool) + } + } + + const availableTools: ISimpliefiedTool[] = toolsInstance.map((tool, index) => { + const originalTool = tools[index] + let agentSelectedTool = (tool as any)?.agentSelectedTool + if (!agentSelectedTool) { + agentSelectedTool = originalTool?.agentSelectedTool + } + const componentNode = options.componentNodes[agentSelectedTool] + + const jsonSchema = zodToJsonSchema(tool.schema as any) + if (jsonSchema.$schema) { + delete jsonSchema.$schema + } + + return { + name: tool.name, + description: tool.description, + schema: jsonSchema, + toolNode: { + label: componentNode?.label || tool.name, + name: componentNode?.name || tool.name + } + } + }) + + // Extract knowledge + const knowledgeBases = nodeData.inputs?.agentKnowledgeDocumentStores as IKnowledgeBase[] + if (knowledgeBases && knowledgeBases.length > 0) { + for (const knowledgeBase of knowledgeBases) { + const nodeInstanceFilePath = options.componentNodes['retrieverTool'].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const newRetrieverToolNodeInstance = new nodeModule.nodeClass() + const [storeId, storeName] = knowledgeBase.documentStore.split(':') + + const docStoreVectorInstanceFilePath = options.componentNodes['documentStoreVS'].filePath as string + const docStoreVectorModule = await import(docStoreVectorInstanceFilePath) + const newDocStoreVectorInstance = new docStoreVectorModule.nodeClass() + const docStoreVectorInstance = await newDocStoreVectorInstance.init( + { + ...nodeData, + inputs: { + ...nodeData.inputs, + selectedStore: storeId + }, + outputs: { + output: 'retriever' + } + }, + '', + options + ) + + const newRetrieverToolNodeData = { + ...nodeData, + inputs: { + ...nodeData.inputs, + name: sanitizeToolName(storeName), + description: knowledgeBase.docStoreDescription, + retriever: docStoreVectorInstance, + returnSourceDocuments: knowledgeBase.returnSourceDocuments + } + } + const retrieverToolInstance = await newRetrieverToolNodeInstance.init(newRetrieverToolNodeData, '', options) + + toolsInstance.push(retrieverToolInstance as Tool) + + const jsonSchema = zodToJsonSchema(retrieverToolInstance.schema) + if (jsonSchema.$schema) { + delete jsonSchema.$schema + } + const componentNode = options.componentNodes['retrieverTool'] + + availableTools.push({ + name: sanitizeToolName(storeName), + description: knowledgeBase.docStoreDescription, + schema: jsonSchema, + toolNode: { + label: componentNode?.label || retrieverToolInstance.name, + name: componentNode?.name || retrieverToolInstance.name + } + }) + } + } + + const knowledgeBasesForVSEmbeddings = nodeData.inputs?.agentKnowledgeVSEmbeddings as IKnowledgeBaseVSEmbeddings[] + if (knowledgeBasesForVSEmbeddings && knowledgeBasesForVSEmbeddings.length > 0) { + for (const knowledgeBase of knowledgeBasesForVSEmbeddings) { + const nodeInstanceFilePath = options.componentNodes['retrieverTool'].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const newRetrieverToolNodeInstance = new nodeModule.nodeClass() + + const selectedEmbeddingModel = knowledgeBase.embeddingModel + const selectedEmbeddingModelConfig = knowledgeBase.embeddingModelConfig + const embeddingInstanceFilePath = options.componentNodes[selectedEmbeddingModel].filePath as string + const embeddingModule = await import(embeddingInstanceFilePath) + const newEmbeddingInstance = new embeddingModule.nodeClass() + const newEmbeddingNodeData = { + ...nodeData, + credential: selectedEmbeddingModelConfig['FLOWISE_CREDENTIAL_ID'], + inputs: { + ...nodeData.inputs, + ...selectedEmbeddingModelConfig + } + } + const embeddingInstance = await newEmbeddingInstance.init(newEmbeddingNodeData, '', options) + + const selectedVectorStore = knowledgeBase.vectorStore + const selectedVectorStoreConfig = knowledgeBase.vectorStoreConfig + const vectorStoreInstanceFilePath = options.componentNodes[selectedVectorStore].filePath as string + const vectorStoreModule = await import(vectorStoreInstanceFilePath) + const newVectorStoreInstance = new vectorStoreModule.nodeClass() + const newVSNodeData = { + ...nodeData, + credential: selectedVectorStoreConfig['FLOWISE_CREDENTIAL_ID'], + inputs: { + ...nodeData.inputs, + ...selectedVectorStoreConfig, + embeddings: embeddingInstance + }, + outputs: { + output: 'retriever' + } + } + const vectorStoreInstance = await newVectorStoreInstance.init(newVSNodeData, '', options) + + const knowledgeName = knowledgeBase.knowledgeName || '' + + const newRetrieverToolNodeData = { + ...nodeData, + inputs: { + ...nodeData.inputs, + name: sanitizeToolName(knowledgeName), + description: knowledgeBase.knowledgeDescription, + retriever: vectorStoreInstance, + returnSourceDocuments: knowledgeBase.returnSourceDocuments + } + } + const retrieverToolInstance = await newRetrieverToolNodeInstance.init(newRetrieverToolNodeData, '', options) + + toolsInstance.push(retrieverToolInstance as Tool) + + const jsonSchema = zodToJsonSchema(retrieverToolInstance.schema) + if (jsonSchema.$schema) { + delete jsonSchema.$schema + } + const componentNode = options.componentNodes['retrieverTool'] + + availableTools.push({ + name: sanitizeToolName(knowledgeName), + description: knowledgeBase.knowledgeDescription, + schema: jsonSchema, + toolNode: { + label: componentNode?.label || retrieverToolInstance.name, + name: componentNode?.name || retrieverToolInstance.name + } + }) + } + } + + // Extract memory and configuration options + const enableMemory = nodeData.inputs?.agentEnableMemory as boolean + const memoryType = nodeData.inputs?.agentMemoryType as string + const userMessage = nodeData.inputs?.agentUserMessage as string + const _agentUpdateState = nodeData.inputs?.agentUpdateState + const _agentStructuredOutput = nodeData.inputs?.agentStructuredOutput + const agentMessages = (nodeData.inputs?.agentMessages as unknown as ILLMMessage[]) ?? [] + + // Extract runtime state and history + const state = options.agentflowRuntime?.state as ICommonObject + const pastChatHistory = (options.pastChatHistory as BaseMessageLike[]) ?? [] + const runtimeChatHistory = (options.agentflowRuntime?.chatHistory as BaseMessageLike[]) ?? [] + const prependedChatHistory = options.prependedChatHistory as IMessage[] + const chatId = options.chatId as string + + // Initialize the LLM model instance + const nodeInstanceFilePath = options.componentNodes[model].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const newLLMNodeInstance = new nodeModule.nodeClass() + const newNodeData = { + ...nodeData, + credential: modelConfig['FLOWISE_CREDENTIAL_ID'], + inputs: { + ...nodeData.inputs, + ...modelConfig + } + } + + const llmWithoutToolsBind = (await newLLMNodeInstance.init(newNodeData, '', options)) as BaseChatModel + let llmNodeInstance = llmWithoutToolsBind // save the original LLM instance for later use in withStructuredOutput, getNumTokens + + const isStructuredOutput = _agentStructuredOutput && Array.isArray(_agentStructuredOutput) && _agentStructuredOutput.length > 0 + + const agentToolsBuiltInOpenAI = convertMultiOptionsToStringArray(nodeData.inputs?.agentToolsBuiltInOpenAI) + if (agentToolsBuiltInOpenAI && agentToolsBuiltInOpenAI.length > 0) { + for (const tool of agentToolsBuiltInOpenAI) { + const builtInTool: ICommonObject = { + type: tool + } + if (tool === 'code_interpreter') { + builtInTool.container = { type: 'auto' } + } + ;(toolsInstance as any).push(builtInTool) + ;(availableTools as any).push({ + name: tool, + toolNode: { + label: tool, + name: tool + } + }) + } + } + + const agentToolsBuiltInGemini = convertMultiOptionsToStringArray(nodeData.inputs?.agentToolsBuiltInGemini) + if (agentToolsBuiltInGemini && agentToolsBuiltInGemini.length > 0) { + for (const tool of agentToolsBuiltInGemini) { + const builtInTool: ICommonObject = { + [tool]: {} + } + ;(toolsInstance as any).push(builtInTool) + ;(availableTools as any).push({ + name: tool, + toolNode: { + label: tool, + name: tool + } + }) + } + } + + const agentToolsBuiltInAnthropic = convertMultiOptionsToStringArray(nodeData.inputs?.agentToolsBuiltInAnthropic) + if (agentToolsBuiltInAnthropic && agentToolsBuiltInAnthropic.length > 0) { + for (const tool of agentToolsBuiltInAnthropic) { + // split _ to get the tool name by removing the last part (date) + const toolName = tool.split('_').slice(0, -1).join('_') + + if (tool === 'code_execution_20250825') { + ;(llmNodeInstance as any).clientOptions = { + defaultHeaders: { + 'anthropic-beta': ['code-execution-2025-08-25', 'files-api-2025-04-14'] + } + } + } + + if (tool === 'web_fetch_20250910') { + ;(llmNodeInstance as any).clientOptions = { + defaultHeaders: { + 'anthropic-beta': ['web-fetch-2025-09-10'] + } + } + } + + const builtInTool: ICommonObject = { + type: tool, + name: toolName + } + ;(toolsInstance as any).push(builtInTool) + ;(availableTools as any).push({ + name: tool, + toolNode: { + label: tool, + name: tool + } + }) + } + } + + // Create PlanningTool (write_todos) + const planner = new PlanningTool({ + onUpdate: (todos) => { + const streamer = options.sseStreamer as IServerSideEventStreamer | undefined + // TODO: update UI to consume this event + streamer?.streamCustomEvent(chatId, 'builtin_todos', todos) + } + }) + + const writeTodosTool = new DynamicStructuredTool({ + name: 'write_todos', + description: planner.toolDefinition.description, + schema: z.object({ + todos: z + .array( + z.object({ + content: z.string().describe('Content of the todo item'), + status: z.enum(['pending', 'in_progress', 'completed']).describe('Status of the todo') + }) + ) + .describe('List of todo items to update') + }), + func: async () => '' // Never called — intercepted in handleToolCalls + }) + + toolsInstance.push(writeTodosTool as unknown as Tool) + availableTools.push({ + name: 'write_todos', + description: planner.toolDefinition.description, + schema: planner.toolDefinition.parameters, + toolNode: { label: 'Planning', name: 'write_todos' } + }) + + if (llmNodeInstance && toolsInstance.length > 0) { + if (llmNodeInstance.bindTools === undefined) { + throw new Error(`Agent needs to have a function calling capable models.`) + } + + // @ts-ignore + llmNodeInstance = llmNodeInstance.bindTools(toolsInstance) + } + + // Prepare messages array + const messages: BaseMessageLike[] = [] + + // Prepend history ONLY if it is the first node + if (prependedChatHistory.length > 0 && !runtimeChatHistory.length) { + for (const msg of prependedChatHistory) { + const role: string = msg.role === 'apiMessage' ? 'assistant' : 'user' + const content: string = msg.content ?? '' + messages.push({ + role, + content + }) + } + } + + const userSystemParts: string[] = [] + for (const msg of agentMessages) { + const role = msg.role + const content = msg.content + if (role && content) { + if (role === 'system') { + userSystemParts.push(content) + } else { + messages.push({ role, content }) + } + } + } + + // Build unified system prompt in fixed assembly order + const systemPrompt = buildSystemPrompt({ + todoListPrompt: planner.getSystemPrompt(), + skillsEnabled: false, // TODO: wire to node input + filesystemEnabled: false, // TODO: wire to node input + subagentEnabled: false, // TODO: wire to node input + asyncSubagentEnabled: false, // TODO: wire to node input + userSystemPrompt: userSystemParts.join('\n\n') || undefined + }) + + messages.unshift({ role: 'system', content: systemPrompt }) + + // Handle memory management if enabled + if (enableMemory) { + await this.handleMemory({ + messages, + memoryType, + pastChatHistory, + runtimeChatHistory, + llmWithoutToolsBind, + nodeData, + userMessage, + input, + abortController, + options, + modelConfig + }) + } else if (!runtimeChatHistory.length) { + /* + * If this is the first node: + * - Add images to messages if exist + * - Add user message if it does not exist in the agentMessages array + */ + if (options.uploads) { + const imageContents = await getUniqueImageMessages(options, messages, modelConfig) + if (imageContents) { + messages.push(imageContents.imageMessageWithBase64) + } + } + + if (input && typeof input === 'string' && !agentMessages.some((msg) => msg.role === 'user')) { + messages.push({ + role: 'user', + content: input + }) + } + } + delete nodeData.inputs?.agentMessages + + // Initialize response and determine if streaming is possible + let response: AIMessageChunk = new AIMessageChunk('') + const isLastNode = options.isLastNode as boolean + const streamingConfig = modelConfig?.streaming + const useDefault = streamingConfig == null || streamingConfig === '' + const effectiveStreaming = useDefault + ? newLLMNodeInstance.inputs?.find((i: INodeParams) => i.name === 'streaming')?.default ?? true + : streamingConfig + const isStreamable = isLastNode && options.sseStreamer !== undefined && effectiveStreaming !== false && !isStructuredOutput + + // Start analytics + if (analyticHandlers && options.parentTraceIds) { + const llmLabel = options?.componentNodes?.[model]?.label || model + llmIds = await analyticHandlers.onLLMStart(llmLabel, messages, options.parentTraceIds) + } + + // Handle tool calls with support for recursion + let usedTools: IUsedTool[] = [] + let sourceDocuments: Array = [] + let artifacts: any[] = [] + let fileAnnotations: any[] = [] + let additionalTokens = 0 + let isWaitingForHumanInput = false + let reasonContent = '' + let thinkingDuration: number | undefined + + // Store the current messages length to track which messages are added during tool calls + const messagesBeforeToolCalls = [...messages] + let _toolCallMessages: BaseMessageLike[] = [] + + /** + * Add image artifacts from previous assistant responses as user messages. + * Only the inserted temporary messages contain base64 — other messages are untouched. + */ + await addImageArtifactsToMessages(messages, options) + + // Check if this is hummanInput for tool calls + const _humanInput = nodeData.inputs?.humanInput + const humanInput: IHumanInput = typeof _humanInput === 'string' ? JSON.parse(_humanInput) : _humanInput + const humanInputAction = options.humanInputAction + const iterationContext = options.iterationContext + + // Track execution time + const startTime = Date.now() + + // Get initial response from LLM + const sseStreamer: IServerSideEventStreamer | undefined = options.sseStreamer + + if (humanInput) { + if (humanInput.type !== 'proceed' && humanInput.type !== 'reject') { + throw new Error(`Invalid human input type. Expected 'proceed' or 'reject', but got '${humanInput.type}'`) + } + const result = await this.handleResumedToolCalls({ + humanInput, + humanInputAction, + messages, + toolsInstance, + sseStreamer, + chatId, + input, + options, + abortController, + llmWithoutToolsBind, + isStreamable, + isLastNode, + iterationContext, + isStructuredOutput, + planningTool: planner + }) + + response = result.response + usedTools = result.usedTools + sourceDocuments = result.sourceDocuments + artifacts = result.artifacts + additionalTokens = result.totalTokens + isWaitingForHumanInput = result.isWaitingForHumanInput || false + if (result.accumulatedReasonContent !== undefined) { + reasonContent = result.accumulatedReasonContent + } + if (result.accumulatedReasoningDuration !== undefined) { + thinkingDuration = result.accumulatedReasoningDuration + } + + // Calculate which messages were added during tool calls + _toolCallMessages = messages.slice(messagesBeforeToolCalls.length) + + // Stream additional data if this is the last node + if (isLastNode && sseStreamer) { + if (usedTools.length > 0) { + sseStreamer.streamUsedToolsEvent(chatId, flatten(usedTools)) + } + + if (sourceDocuments.length > 0) { + sseStreamer.streamSourceDocumentsEvent(chatId, flatten(sourceDocuments)) + } + + if (artifacts.length > 0) { + sseStreamer.streamArtifactsEvent(chatId, flatten(artifacts)) + } + } + } else { + if (isStreamable) { + response = await this.handleStreamingResponse( + sseStreamer, + llmNodeInstance, + messages, + chatId, + abortController, + isStructuredOutput, + isLastNode + ) + } else { + response = await llmNodeInstance.invoke(messages, { signal: abortController?.signal }) + } + } + + // Capture reasoning and duration from first LLM response so they can be accumulated across tool-call turns + if (response.additional_kwargs?.reasoning_content) { + reasonContent = (response.additional_kwargs.reasoning_content as string) || '' + } + if (typeof response.additional_kwargs?.reasoning_duration === 'number') { + thinkingDuration = response.additional_kwargs.reasoning_duration + } + + // Address built in tools (after artifacts are processed) + const builtInUsedTools: IUsedTool[] = await this.extractBuiltInUsedTools(response, []) + + if (!humanInput && response.tool_calls && response.tool_calls.length > 0) { + const result = await this.handleToolCalls({ + response, + messages, + toolsInstance, + sseStreamer, + chatId, + input, + options, + abortController, + llmNodeInstance, + isStreamable, + isLastNode, + iterationContext, + isStructuredOutput, + accumulatedReasonContent: reasonContent, + accumulatedReasoningDuration: thinkingDuration, + planningTool: planner + }) + + response = result.response + usedTools = result.usedTools + sourceDocuments = result.sourceDocuments + artifacts = result.artifacts + additionalTokens = result.totalTokens + isWaitingForHumanInput = result.isWaitingForHumanInput || false + if (result.accumulatedReasonContent !== undefined) { + reasonContent = result.accumulatedReasonContent + } + if (result.accumulatedReasoningDuration !== undefined) { + thinkingDuration = result.accumulatedReasoningDuration + } + + // Calculate which messages were added during tool calls + _toolCallMessages = messages.slice(messagesBeforeToolCalls.length) + + // Stream additional data if this is the last node + if (isLastNode && sseStreamer) { + if (usedTools.length > 0) { + sseStreamer.streamUsedToolsEvent(chatId, flatten(usedTools)) + } + + if (sourceDocuments.length > 0) { + sseStreamer.streamSourceDocumentsEvent(chatId, flatten(sourceDocuments)) + } + + if (artifacts.length > 0) { + sseStreamer.streamArtifactsEvent(chatId, flatten(artifacts)) + } + } + } else if (!humanInput && !isStreamable && isLastNode && sseStreamer && !isStructuredOutput) { + // Stream whole response back to UI if not streaming and no tool calls + // Skip this if structured output is enabled - it will be streamed after conversion + + // Stream thinking content if available + if (response.contentBlocks?.length) { + for (const block of response.contentBlocks) { + if (block.type === 'reasoning' && (block as { reasoning?: string }).reasoning) { + reasonContent += (block as { reasoning: string }).reasoning + } + if ((block as any).type === 'thinking' && block.thinking) { + reasonContent += block.thinking + } + } + + sseStreamer.streamThinkingEvent(chatId, reasonContent) + // Send end of thinking event with duration from token details if available + const reasoningTokens = response.usage_metadata?.output_token_details?.reasoning || 0 + // Estimate duration based on reasoning tokens (rough estimate: ~50 tokens/sec) + thinkingDuration = reasoningTokens > 0 ? Math.round(reasoningTokens / 50) : 2 + sseStreamer.streamThinkingEvent(chatId, '', thinkingDuration) + } + + sseStreamer.streamTokenEvent(chatId, extractResponseContent(response)) + } + + // Calculate execution time + const endTime = Date.now() + const timeDelta = endTime - startTime + + // Update flow state if needed + let newState = { ...state } + if (_agentUpdateState && Array.isArray(_agentUpdateState) && _agentUpdateState.length > 0) { + newState = updateFlowState(state, _agentUpdateState) + } + + // Clean up empty inputs + for (const key in nodeData.inputs) { + if (nodeData.inputs[key] === '') { + delete nodeData.inputs[key] + } + } + + // Prepare final response and output object + let finalResponse = '' + if (response.content && Array.isArray(response.content)) { + // Process items and concatenate consecutive text items + const processedParts: string[] = [] + let currentTextBuffer = '' + + for (const item of response.content) { + const itemAny = item as any + const isTextItem = (itemAny.text && !itemAny.type) || (itemAny.type === 'text' && itemAny.text) + + if (isTextItem) { + // Accumulate consecutive text items + currentTextBuffer += itemAny.text + } else { + // Flush accumulated text before processing other types + if (currentTextBuffer) { + processedParts.push(currentTextBuffer) + currentTextBuffer = '' + } + + // Process non-text items + if (itemAny.type === 'executableCode' && itemAny.executableCode) { + // Format executable code as a code block + const language = itemAny.executableCode.language?.toLowerCase() || 'python' + processedParts.push(`\n\`\`\`${language}\n${itemAny.executableCode.code}\n\`\`\`\n`) + } else if (itemAny.type === 'codeExecutionResult' && itemAny.codeExecutionResult) { + // Format code execution result + const outcome = itemAny.codeExecutionResult.outcome || 'OUTCOME_OK' + const output = itemAny.codeExecutionResult.output || '' + if (outcome === 'OUTCOME_OK' && output) { + processedParts.push(`**Code Output:**\n\`\`\`\n${output}\n\`\`\`\n`) + } else if (outcome !== 'OUTCOME_OK') { + processedParts.push(`**Code Execution Error:**\n\`\`\`\n${output}\n\`\`\`\n`) + } + } + } + } + + // Flush any remaining text + if (currentTextBuffer) { + processedParts.push(currentTextBuffer) + } + + finalResponse = processedParts.filter((text) => text).join('\n') + } else if (response.content && typeof response.content === 'string') { + finalResponse = response.content + } else if (response.content === '') { + // Empty response content, this could happen when there is only image data + finalResponse = '' + } else { + finalResponse = JSON.stringify(response, null, 2) + } + + // Address built in tools + const additionalBuiltInUsedTools: IUsedTool[] = await this.extractBuiltInUsedTools(response, builtInUsedTools) + if (additionalBuiltInUsedTools.length > 0) { + usedTools = [...new Set([...usedTools, ...additionalBuiltInUsedTools])] + + // Stream used tools if this is the last node + if (isLastNode && sseStreamer) { + sseStreamer.streamUsedToolsEvent(chatId, flatten(usedTools)) + } + } + + // Extract artifacts from annotations in response metadata and replace inline data + if (response.response_metadata) { + const { + artifacts: extractedArtifacts, + fileAnnotations: extractedFileAnnotations, + savedInlineImages + } = await extractArtifactsFromResponse(response.response_metadata as IResponseMetadata, newNodeData, options) + if (extractedArtifacts.length > 0) { + artifacts = [...artifacts, ...extractedArtifacts] + + // Stream artifacts if this is the last node + if (isLastNode && sseStreamer) { + sseStreamer.streamArtifactsEvent(chatId, extractedArtifacts) + } + } + + if (extractedFileAnnotations.length > 0) { + fileAnnotations = [...fileAnnotations, ...extractedFileAnnotations] + + // Stream file annotations if this is the last node + if (isLastNode && sseStreamer) { + sseStreamer.streamFileAnnotationsEvent(chatId, fileAnnotations) + } + } + + // Replace inlineData base64 with file references in the response + if (savedInlineImages && savedInlineImages.length > 0) { + replaceInlineDataWithFileReferences(response, savedInlineImages) + } + } + + // Replace sandbox links with proper download URLs. Example: [Download the script](sandbox:/mnt/data/dummy_bar_graph.py) + if (finalResponse.includes('sandbox:/')) { + finalResponse = await this.processSandboxLinks(finalResponse, options.baseURL, options.chatflowid, chatId) + } + + // If is structured output, then invoke LLM again with structured output at the very end after all tool calls + if (isStructuredOutput) { + const structuredllmNodeInstance = configureStructuredOutput(llmWithoutToolsBind, _agentStructuredOutput) + const prompt = 'Convert the following response to the structured output format: ' + finalResponse + response = await structuredllmNodeInstance.invoke(prompt, { signal: abortController?.signal }) + + // Prefix the response with ```json and suffix with ``` to render as a code block + if (typeof response === 'object') { + finalResponse = '```json\n' + JSON.stringify(response, null, 2) + '\n```' + } else { + finalResponse = response + } + + if (isLastNode && sseStreamer) { + sseStreamer.streamTokenEvent(chatId, finalResponse) + } + } + + // Add reasoning content + if (!reasonContent && response.additional_kwargs?.reasoning_content) { + reasonContent = response.additional_kwargs.reasoning_content as string + } + if (reasonContent && response.additional_kwargs?.reasoning_duration != null) { + thinkingDuration = response.additional_kwargs.reasoning_duration as number + } + const reasonContentObj = + reasonContent !== undefined && reasonContent !== '' ? { thinking: reasonContent, thinkingDuration } : undefined + + const costMetadata = await this.calculateUsageCost( + model, + modelConfig?.modelName as string | undefined, + response.usage_metadata, + additionalTokens + ) + + const output = this.prepareOutputObject( + response, + availableTools, + finalResponse, + startTime, + endTime, + timeDelta, + usedTools, + sourceDocuments, + artifacts, + additionalTokens, + isWaitingForHumanInput, + fileAnnotations, + isStructuredOutput, + reasonContentObj, + costMetadata + ) + + // End analytics tracking + if (analyticHandlers && llmIds) { + await analyticHandlers.onLLMEnd(llmIds, output, { model: modelName, provider: model }) + } + + // Send additional streaming events if needed + if (isStreamable) { + this.sendStreamingEvents(options, chatId, response) + } + + // Stream file annotations if any were extracted + if (fileAnnotations.length > 0 && isLastNode && sseStreamer) { + sseStreamer.streamFileAnnotationsEvent(chatId, fileAnnotations) + } + + // Process template variables in state + const outputForStateProcessing = + isStructuredOutput && typeof response === 'object' ? JSON.stringify(response, null, 2) : finalResponse + newState = processTemplateVariables(newState, outputForStateProcessing) + + /** + * Remove temporary artifact image messages (they were only needed for the model invoke). + * Then revert all remaining tagged base64 image_url items back to stored-file format. + * This is to avoid storing the actual base64 data into database + */ + const messagesToStore = messages.filter((msg: any) => !msg._isTemporaryImageMessage) + const normalizedMessagesToStore = normalizeMessagesForStorage(messagesToStore) + const messagesWithFileReferences = revertBase64ImagesToFileRefs(normalizedMessagesToStore) + + // Only add to runtime chat history if this is the first node + const inputMessages = [] + if (!runtimeChatHistory.length) { + // Include any image file reference messages from uploads in the chat history + const imageInputMessages = messagesWithFileReferences.filter( + (msg: any) => + msg.role === 'user' && + Array.isArray(msg.content) && + msg.content.some((item: any) => item.type === 'stored-file' && item.mime?.startsWith('image/')) + ) + if (imageInputMessages.length) { + inputMessages.push(...imageInputMessages) + } + if (input && typeof input === 'string') { + if (!enableMemory) { + if (!agentMessages.some((msg) => msg.role === 'user')) { + inputMessages.push({ role: 'user', content: input }) + } else { + agentMessages.map((msg) => { + if (msg.role === 'user') { + inputMessages.push({ role: 'user', content: msg.content }) + } + }) + } + } else { + inputMessages.push({ role: 'user', content: input }) + } + } + } + + const returnResponseAs = nodeData.inputs?.agentReturnResponseAs as string + let returnRole = 'user' + if (returnResponseAs === 'assistantMessage') { + returnRole = 'assistant' + } + + // Prepare and return the final output + return { + id: nodeData.id, + name: this.name, + input: { + messages: messagesWithFileReferences, + ...nodeData.inputs + }, + output, + state: newState, + chatHistory: [ + ...inputMessages, + + // Add the messages that were specifically added during tool calls, this enable other nodes to see the full tool call history, temporaraily disabled + // ...toolCallMessages, + + // End with the final assistant response + { + role: returnRole, + content: finalResponse, + name: nodeData?.label ? nodeData?.label.toLowerCase().replace(/\s/g, '_').trim() : nodeData?.id, + ...(((artifacts && artifacts.length > 0) || + (fileAnnotations && fileAnnotations.length > 0) || + (usedTools && usedTools.length > 0)) && { + additional_kwargs: { + ...(artifacts && artifacts.length > 0 && { artifacts }), + ...(fileAnnotations && fileAnnotations.length > 0 && { fileAnnotations }), + ...(usedTools && usedTools.length > 0 && { usedTools }) + } + }) + } + ] + } + } catch (error) { + if (options.analyticHandlers && llmIds) { + await options.analyticHandlers.onLLMError(llmIds, error instanceof Error ? error.message : String(error)) + } + + if (error instanceof Error && error.message === 'Aborted') { + throw error + } + throw new Error(`Error in Agent node: ${error instanceof Error ? error.message : String(error)}`) + } + } + + /** + * Extracts built-in used tools from response metadata and processes image generation results + */ + private async extractBuiltInUsedTools(response: AIMessageChunk, builtInUsedTools: IUsedTool[] = []): Promise { + if (!response.response_metadata) { + return builtInUsedTools + } + + const { output, tools, groundingMetadata, urlContextMetadata } = response.response_metadata as { + output?: any[] + tools?: any[] + groundingMetadata?: { webSearchQueries?: string[] } + urlContextMetadata?: { urlMetadata?: any[] } + } + + // Handle OpenAI built-in tools + if (output && Array.isArray(output) && output.length > 0 && tools && Array.isArray(tools) && tools.length > 0) { + for (const outputItem of output) { + if (outputItem.type && outputItem.type.endsWith('_call')) { + let toolInput = outputItem.action ?? outputItem.code + let toolOutput = outputItem.status === 'completed' ? 'Success' : outputItem.status + + // Handle image generation calls specially + if (outputItem.type === 'image_generation_call') { + // Create input summary for image generation + toolInput = { + prompt: outputItem.revised_prompt || 'Image generation request', + size: outputItem.size || '1024x1024', + quality: outputItem.quality || 'standard', + output_format: outputItem.output_format || 'png' + } + + // Check if image has been processed (base64 replaced with file path) + if (outputItem.result && !outputItem.result.startsWith('data:') && !outputItem.result.includes('base64')) { + toolOutput = `Image generated and saved` + } else { + toolOutput = `Image generated (base64)` + } + } + + // Remove "_call" suffix to get the base tool name + const baseToolName = outputItem.type.replace('_call', '') + + // Find matching tool that includes the base name in its type + const matchingTool = tools.find((tool) => tool.type && tool.type.includes(baseToolName)) + + if (matchingTool) { + // Check for duplicates + if (builtInUsedTools.find((tool) => tool.tool === matchingTool.type)) { + continue + } + + builtInUsedTools.push({ + tool: matchingTool.type, + toolInput, + toolOutput + }) + } + } + } + } + + // Handle Gemini googleSearch tool + if (groundingMetadata && groundingMetadata.webSearchQueries && Array.isArray(groundingMetadata.webSearchQueries)) { + // Check for duplicates + const isDuplicate = builtInUsedTools.find( + (tool) => + tool.tool === 'googleSearch' && + JSON.stringify((tool.toolInput as any)?.queries) === JSON.stringify(groundingMetadata.webSearchQueries) + ) + if (!isDuplicate) { + builtInUsedTools.push({ + tool: 'googleSearch', + toolInput: { + queries: groundingMetadata.webSearchQueries + }, + toolOutput: `Searched for: ${groundingMetadata.webSearchQueries.join(', ')}` + }) + } + } + + // Handle Gemini urlContext tool + if (urlContextMetadata && urlContextMetadata.urlMetadata && Array.isArray(urlContextMetadata.urlMetadata)) { + // Check for duplicates + const isDuplicate = builtInUsedTools.find( + (tool) => + tool.tool === 'urlContext' && + JSON.stringify((tool.toolInput as any)?.urlMetadata) === JSON.stringify(urlContextMetadata.urlMetadata) + ) + if (!isDuplicate) { + builtInUsedTools.push({ + tool: 'urlContext', + toolInput: { + urlMetadata: urlContextMetadata.urlMetadata + }, + toolOutput: `Processed ${urlContextMetadata.urlMetadata.length} URL(s)` + }) + } + } + + // Handle Gemini codeExecution tool + if (response.content && Array.isArray(response.content)) { + for (let i = 0; i < response.content.length; i++) { + const item = response.content[i] + + if (item.type === 'executableCode' && item.executableCode) { + const executableCode = item.executableCode as { language?: string; code?: string } + const language = executableCode.language || 'PYTHON' + const code = executableCode.code || '' + let toolOutput = '' + + // Check for duplicates + const isDuplicate = builtInUsedTools.find( + (tool) => + tool.tool === 'codeExecution' && + (tool.toolInput as any)?.language === language && + (tool.toolInput as any)?.code === code + ) + if (isDuplicate) { + continue + } + + // Check the next item for the output + const nextItem = i + 1 < response.content.length ? response.content[i + 1] : null + + if (nextItem) { + if (nextItem.type === 'codeExecutionResult' && nextItem.codeExecutionResult) { + const codeExecutionResult = nextItem.codeExecutionResult as { outcome?: string; output?: string } + const outcome = codeExecutionResult.outcome + const output = codeExecutionResult.output || '' + toolOutput = outcome === 'OUTCOME_OK' ? output : `Error: ${output}` + } else if (nextItem.type === 'inlineData') { + toolOutput = 'Generated image data' + } + } + + builtInUsedTools.push({ + tool: 'codeExecution', + toolInput: { + language, + code + }, + toolOutput + }) + } + } + } + + return builtInUsedTools + } + + /** + * Handles memory management based on the specified memory type + */ + private async handleMemory({ + messages, + memoryType, + pastChatHistory, + runtimeChatHistory, + llmWithoutToolsBind, + nodeData, + userMessage, + input, + abortController, + options, + modelConfig + }: { + messages: BaseMessageLike[] + memoryType: string + pastChatHistory: BaseMessageLike[] + runtimeChatHistory: BaseMessageLike[] + llmWithoutToolsBind: BaseChatModel + nodeData: INodeData + userMessage: string + input: string | Record + abortController: AbortController + options: ICommonObject + modelConfig: ICommonObject + }): Promise { + const { updatedPastMessages } = await getPastChatHistoryImageMessages(pastChatHistory, options) + pastChatHistory = updatedPastMessages + + let pastMessages = [...pastChatHistory, ...runtimeChatHistory] + if (!runtimeChatHistory.length && input && typeof input === 'string') { + /* + * If this is the first node: + * - Add images to messages if exist + * - Add user message + */ + if (options.uploads) { + const imageContents = await getUniqueImageMessages(options, messages, modelConfig) + if (imageContents) { + pastMessages.push(imageContents.imageMessageWithBase64) + } + } + pastMessages.push({ + role: 'user', + content: input + }) + } + const { updatedMessages } = await processMessagesWithImages(pastMessages, options) + pastMessages = updatedMessages + + if (pastMessages.length > 0) { + if (memoryType === 'windowSize') { + // Window memory: Keep the last N messages + const windowSize = nodeData.inputs?.agentMemoryWindowSize as number + const windowedMessages = pastMessages.slice(-windowSize * 2) + messages.push(...windowedMessages) + } else if (memoryType === 'conversationSummary') { + // Summary memory: Summarize all past messages + const summary = await llmWithoutToolsBind.invoke( + [ + { + role: 'user', + content: DEFAULT_SUMMARIZER_TEMPLATE.replace( + '{conversation}', + pastMessages.map((msg: any) => `${msg.role}: ${msg.content}`).join('\n') + ) + } + ], + { signal: abortController?.signal } + ) + messages.push({ role: 'assistant', content: extractResponseContent(summary) }) + if (!userMessage && input && typeof input === 'string') { + messages.push({ + role: 'user', + content: input + }) + } + } else if (memoryType === 'conversationSummaryBuffer') { + // Summary buffer: Summarize messages that exceed token limit + await this.handleSummaryBuffer(messages, pastMessages, llmWithoutToolsBind, nodeData, abortController) + } else { + // Default: Use all messages + messages.push(...pastMessages) + } + } + + // Add user message + if (userMessage) { + messages.push({ + role: 'user', + content: userMessage + }) + } + } + + /** + * Handles conversation summary buffer memory type + */ + private async handleSummaryBuffer( + messages: BaseMessageLike[], + pastMessages: BaseMessageLike[], + llmWithoutToolsBind: BaseChatModel, + nodeData: INodeData, + abortController: AbortController + ): Promise { + const maxTokenLimit = (nodeData.inputs?.agentMemoryMaxTokenLimit as number) || 2000 + + // Convert past messages to a format suitable for token counting + const messagesString = pastMessages.map((msg: any) => `${msg.role}: ${msg.content}`).join('\n') + const tokenCount = await llmWithoutToolsBind.getNumTokens(messagesString) + + if (tokenCount > maxTokenLimit) { + // Calculate how many messages to summarize (messages that exceed the token limit) + let currBufferLength = tokenCount + const messagesToSummarize = [] + const remainingMessages = [...pastMessages] + + // Remove messages from the beginning until we're under the token limit + while (currBufferLength > maxTokenLimit && remainingMessages.length > 0) { + const poppedMessage = remainingMessages.shift() + if (poppedMessage) { + messagesToSummarize.push(poppedMessage) + // Recalculate token count for remaining messages + const remainingMessagesString = remainingMessages.map((msg: any) => `${msg.role}: ${msg.content}`).join('\n') + currBufferLength = await llmWithoutToolsBind.getNumTokens(remainingMessagesString) + } + } + + // Summarize the messages that were removed + const messagesToSummarizeString = messagesToSummarize.map((msg: any) => `${msg.role}: ${msg.content}`).join('\n') + + const summary = await llmWithoutToolsBind.invoke( + [ + { + role: 'user', + content: DEFAULT_SUMMARIZER_TEMPLATE.replace('{conversation}', messagesToSummarizeString) + } + ], + { signal: abortController?.signal } + ) + + // Add summary as a system message at the beginning, then add remaining messages + let summaryRole = 'system' + if (messages.some((msg) => typeof msg === 'object' && !Array.isArray(msg) && 'role' in msg && msg.role === 'system')) { + summaryRole = 'user' // some model doesn't allow multiple system messages + } + messages.push({ role: summaryRole, content: `Previous conversation summary: ${extractResponseContent(summary)}` }) + messages.push(...remainingMessages) + } else { + // If under token limit, use all messages + messages.push(...pastMessages) + } + } + + /** + * Handles streaming response from the LLM + */ + private async handleStreamingResponse( + sseStreamer: IServerSideEventStreamer | undefined, + llmNodeInstance: BaseChatModel, + messages: BaseMessageLike[], + chatId: string, + abortController: AbortController, + isStructuredOutput: boolean = false, + isLastNode: boolean = false + ): Promise { + let response = new AIMessageChunk('') + let reasonContent = '' + let thinkingDuration: number | undefined + let thinkingStartTime: number | null = null + let wasThinking = false + let sentLastThinkingEvent = false + + try { + for await (const chunk of await llmNodeInstance.stream(messages, { signal: abortController?.signal })) { + if (sseStreamer && !isStructuredOutput) { + let content = '' + + if (chunk.contentBlocks?.length) { + for (const block of chunk.contentBlocks) { + if (isLastNode) { + // As soon as we see the first non-reasoning block, send last thinking event with duration + if (block.type !== 'reasoning' && wasThinking && !sentLastThinkingEvent && thinkingStartTime != null) { + thinkingDuration = Math.round((Date.now() - thinkingStartTime) / 1000) + sseStreamer.streamThinkingEvent(chatId, '', thinkingDuration) + sentLastThinkingEvent = true + } + if (block.type === 'reasoning' && (block as { reasoning?: string }).reasoning) { + if (!thinkingStartTime) { + thinkingStartTime = Date.now() + } + wasThinking = true + const reasoningContent = (block as { reasoning: string }).reasoning + sseStreamer.streamThinkingEvent(chatId, reasoningContent) + reasonContent += reasoningContent + } + } + } + } + + if (typeof chunk === 'string') { + content = chunk + } else if (Array.isArray(chunk.content) && chunk.content.length > 0) { + content = chunk.content + .map((item: any) => { + if ((item.text && !item.type) || (item.type === 'text' && item.text)) { + return item.text + } else if (item.type === 'executableCode' && item.executableCode) { + const language = item.executableCode.language?.toLowerCase() || 'python' + return `\n\`\`\`${language}\n${item.executableCode.code}\n\`\`\`\n` + } else if (item.type === 'codeExecutionResult' && item.codeExecutionResult) { + const outcome = item.codeExecutionResult.outcome || 'OUTCOME_OK' + const output = item.codeExecutionResult.output || '' + if (outcome === 'OUTCOME_OK' && output) { + return `**Code Output:**\n\`\`\`\n${output}\n\`\`\`\n` + } else if (outcome !== 'OUTCOME_OK') { + return `**Code Execution Error:**\n\`\`\`\n${output}\n\`\`\`\n` + } + } + return '' + }) + .filter((text: string) => text) + .join('') + } else if (chunk.content) { + content = chunk.content.toString() + } + sseStreamer.streamTokenEvent(chatId, content) + } + + const messageChunk = typeof chunk === 'string' ? new AIMessageChunk(chunk) : chunk + response = response.concat(messageChunk) + } + } catch (error) { + console.error('Error during streaming:', error) + throw error + } + + // Only convert to string if all content items are text (no inlineData or other special types) + if (Array.isArray(response.content) && response.content.length > 0) { + const hasNonTextContent = response.content.some( + (item: any) => item.type === 'inlineData' || item.type === 'executableCode' || item.type === 'codeExecutionResult' + ) + if (!hasNonTextContent) { + const responseContents = response.content as ContentBlock.Text[] + response.content = responseContents.map((item) => item.text).join('') + } + } + + if (reasonContent.length > 0) { + response.additional_kwargs = { + ...response.additional_kwargs, + reasoning_content: reasonContent, + reasoning_duration: thinkingDuration + } + } + + return response + } + + /** + * Calculates input/output and total cost from usage metadata using model pricing from models.json. + * Also returns the model's base (per-token) input and output costs. + */ + private async calculateUsageCost( + provider: string | undefined, + modelName: string | undefined, + usageMetadata: Record | undefined, + additionalTokens: number = 0 + ): Promise< + | { + input_cost: number + output_cost: number + total_cost: number + base_input_cost: number + base_output_cost: number + } + | undefined + > { + if (!provider || !modelName) return undefined + const inputTokens = (usageMetadata?.input_tokens ?? 0) as number + const outputTokens = ((usageMetadata?.output_tokens ?? 0) as number) + additionalTokens + try { + const modelConfig = await getModelConfigByModelName(MODEL_TYPE.CHAT, provider, modelName) + if (!modelConfig) return undefined + const baseInputCost = Number(modelConfig.input_cost) || 0 + const baseOutputCost = Number(modelConfig.output_cost) || 0 + const inputCost = inputTokens * baseInputCost + const outputCost = outputTokens * baseOutputCost + const totalCost = inputCost + outputCost + if (inputCost === 0 && outputCost === 0) return undefined + return { + input_cost: inputCost, + output_cost: outputCost, + total_cost: totalCost, + base_input_cost: baseInputCost, + base_output_cost: baseOutputCost + } + } catch { + return undefined + } + } + + /** + * Prepares the output object with response and metadata + */ + private prepareOutputObject( + response: AIMessageChunk, + availableTools: ISimpliefiedTool[], + finalResponse: string, + startTime: number, + endTime: number, + timeDelta: number, + usedTools: IUsedTool[], + sourceDocuments: Array, + artifacts: any[], + additionalTokens: number = 0, + isWaitingForHumanInput: boolean = false, + fileAnnotations: any[] = [], + isStructuredOutput: boolean = false, + reasonContent?: { thinking: string; thinkingDuration?: number }, + costMetadata?: { + input_cost: number + output_cost: number + total_cost: number + base_input_cost: number + base_output_cost: number + } + ): any { + const output: any = { + content: finalResponse, + timeMetadata: { + start: startTime, + end: endTime, + delta: timeDelta + } + } + + if (response.tool_calls) { + output.calledTools = response.tool_calls + } + + // Include token usage metadata with accumulated tokens from tool calls + if (response.usage_metadata) { + const originalTokens = response.usage_metadata.total_tokens || 0 + output.usageMetadata = { + ...response.usage_metadata, + total_tokens: originalTokens + additionalTokens, + tool_call_tokens: additionalTokens + } + } else if (additionalTokens > 0) { + // If no original usage metadata but we have tool tokens + output.usageMetadata = { + total_tokens: additionalTokens, + tool_call_tokens: additionalTokens + } + } + + if (costMetadata && output.usageMetadata) { + output.usageMetadata.input_cost = costMetadata.input_cost + output.usageMetadata.output_cost = costMetadata.output_cost + output.usageMetadata.total_cost = costMetadata.total_cost + output.usageMetadata.base_input_cost = costMetadata.base_input_cost + output.usageMetadata.base_output_cost = costMetadata.base_output_cost + } + + if (response.response_metadata) { + output.responseMetadata = response.response_metadata + } + + if (isStructuredOutput && typeof response === 'object') { + const structuredOutput = response as Record + for (const key in structuredOutput) { + if (structuredOutput[key] !== undefined && structuredOutput[key] !== null) { + output[key] = structuredOutput[key] + } + } + } + + // Add used tools, source documents and artifacts to output + if (usedTools && usedTools.length > 0) { + output.usedTools = flatten(usedTools) + } + + if (sourceDocuments && sourceDocuments.length > 0) { + output.sourceDocuments = flatten(sourceDocuments) + } + + if (artifacts && artifacts.length > 0) { + output.artifacts = flatten(artifacts) + } + + if (availableTools && availableTools.length > 0) { + output.availableTools = availableTools + } + + if (isWaitingForHumanInput) { + output.isWaitingForHumanInput = isWaitingForHumanInput + } + + if (fileAnnotations && fileAnnotations.length > 0) { + output.fileAnnotations = fileAnnotations + } + + if (reasonContent) { + output.reasonContent = reasonContent + } + + return output + } + + /** + * Sends additional streaming events for tool calls and metadata + */ + private sendStreamingEvents(options: ICommonObject, chatId: string, response: AIMessageChunk): void { + const sseStreamer: IServerSideEventStreamer = options.sseStreamer as IServerSideEventStreamer + + if (response.tool_calls) { + const formattedToolCalls = response.tool_calls.map((toolCall: any) => ({ + tool: toolCall.name || 'tool', + toolInput: toolCall.args, + toolOutput: '' + })) + sseStreamer.streamCalledToolsEvent(chatId, flatten(formattedToolCalls)) + } + + if (response.usage_metadata) { + sseStreamer.streamUsageMetadataEvent(chatId, response.usage_metadata) + } + + sseStreamer.streamEndEvent(chatId) + } + + /** + * Handles tool calls and their responses, with support for recursive tool calling + */ + private async handleToolCalls({ + response, + messages, + toolsInstance, + sseStreamer, + chatId, + input, + options, + abortController, + llmNodeInstance, + isStreamable, + isLastNode, + iterationContext, + isStructuredOutput = false, + accumulatedReasonContent: initialAccumulatedReasonContent, + accumulatedReasoningDuration: initialAccumulatedReasoningDuration, + planningTool + }: { + response: AIMessageChunk + messages: BaseMessageLike[] + toolsInstance: Tool[] + sseStreamer: IServerSideEventStreamer | undefined + chatId: string + input: string | Record + options: ICommonObject + abortController: AbortController + llmNodeInstance: BaseChatModel + isStreamable: boolean + isLastNode: boolean + iterationContext: ICommonObject + isStructuredOutput?: boolean + accumulatedReasonContent?: string + accumulatedReasoningDuration?: number + planningTool?: PlanningTool + }): Promise<{ + response: AIMessageChunk + usedTools: IUsedTool[] + sourceDocuments: Array + artifacts: any[] + totalTokens: number + isWaitingForHumanInput?: boolean + accumulatedReasonContent?: string + accumulatedReasoningDuration?: number + }> { + // Track total tokens used throughout this process + let totalTokens = response.usage_metadata?.total_tokens || 0 + const usedTools: IUsedTool[] = [] + let sourceDocuments: Array = [] + let artifacts: any[] = [] + let isWaitingForHumanInput: boolean | undefined + // Use reasoning from caller (first turn); subsequent turns are added when we get newResponse + let accumulatedReasonContent = initialAccumulatedReasonContent ?? '' + let accumulatedReasoningDuration = initialAccumulatedReasoningDuration ?? 0 + + if (!response.tool_calls || response.tool_calls.length === 0) { + return { + response, + usedTools: [], + sourceDocuments: [], + artifacts: [], + totalTokens, + accumulatedReasonContent: accumulatedReasonContent || undefined, + accumulatedReasoningDuration: accumulatedReasoningDuration || undefined + } + } + + // Stream tool calls if available + if (sseStreamer) { + const formattedToolCalls = response.tool_calls.map((toolCall: any) => ({ + tool: toolCall.name || 'tool', + toolInput: toolCall.args, + toolOutput: '' + })) + sseStreamer.streamCalledToolsEvent(chatId, flatten(formattedToolCalls)) + } + + // Remove tool calls with no id + const toBeRemovedToolCalls = [] + for (let i = 0; i < response.tool_calls.length; i++) { + const toolCall = response.tool_calls[i] + if (!toolCall.id) { + toBeRemovedToolCalls.push(toolCall) + usedTools.push({ + tool: toolCall.name || 'tool', + toolInput: toolCall.args, + toolOutput: response.content + }) + } + } + + for (const toolCall of toBeRemovedToolCalls) { + response.tool_calls.splice(response.tool_calls.indexOf(toolCall), 1) + } + + // Add LLM response with tool calls to messages + messages.push(response) + + // Process each tool call + for (let i = 0; i < response.tool_calls.length; i++) { + const toolCall = response.tool_calls[i] + + // Intercept write_todos — handled by PlanningTool, not the LangChain tool + if (toolCall.name === 'write_todos' && planningTool) { + const toolOutput = planningTool.handleToolCall(toolCall.args as { todos: Todo[] }) + messages.push({ role: 'tool', content: toolOutput, tool_call_id: toolCall.id, name: toolCall.name }) + usedTools.push({ tool: 'write_todos', toolInput: toolCall.args, toolOutput }) + continue + } + + const selectedTool = toolsInstance.find((tool) => tool.name === toolCall.name) + if (selectedTool) { + let parsedDocs + let parsedArtifacts + let isToolRequireHumanInput = + (selectedTool as any).requiresHumanInput && (!iterationContext || Object.keys(iterationContext).length === 0) + + const flowConfig = { + chatflowId: options.chatflowid, + sessionId: options.sessionId, + chatId: options.chatId, + input: input, + state: options.agentflowRuntime?.state + } + + if (isToolRequireHumanInput) { + const toolCallDetails = '```json\n' + JSON.stringify(toolCall, null, 2) + '\n```' + const responseContent = response.content + `\nAttempting to use tool:\n${toolCallDetails}` + response.content = responseContent + if (!isStructuredOutput) { + sseStreamer?.streamTokenEvent(chatId, responseContent) + } + return { + response, + usedTools, + sourceDocuments, + artifacts, + totalTokens, + isWaitingForHumanInput: true, + accumulatedReasonContent: accumulatedReasonContent || undefined, + accumulatedReasoningDuration: accumulatedReasoningDuration || undefined + } + } + + let toolIds: ICommonObject | undefined + if (options.analyticHandlers) { + toolIds = await options.analyticHandlers.onToolStart(toolCall.name, toolCall.args, options.parentTraceIds) + } + + try { + //@ts-ignore + let toolOutput = await selectedTool.call(toolCall.args, { signal: abortController?.signal }, undefined, flowConfig) + + if (options.analyticHandlers && toolIds) { + await options.analyticHandlers.onToolEnd(toolIds, toolOutput) + } + + // Extract source documents if present + if (typeof toolOutput === 'string' && toolOutput.includes(SOURCE_DOCUMENTS_PREFIX)) { + const [output, docs] = toolOutput.split(SOURCE_DOCUMENTS_PREFIX) + toolOutput = output + try { + parsedDocs = JSON.parse(docs) + sourceDocuments.push(parsedDocs) + } catch (e) { + console.error('Error parsing source documents from tool:', e) + } + } + + // Extract artifacts if present + if (typeof toolOutput === 'string' && toolOutput.includes(ARTIFACTS_PREFIX)) { + const [output, artifact] = toolOutput.split(ARTIFACTS_PREFIX) + toolOutput = output + try { + parsedArtifacts = JSON.parse(artifact) + artifacts.push(parsedArtifacts) + } catch (e) { + console.error('Error parsing artifacts from tool:', e) + } + } + + let toolInput + if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) { + const [output, args] = toolOutput.split(TOOL_ARGS_PREFIX) + toolOutput = output + try { + toolInput = JSON.parse(args) + } catch (e) { + console.error('Error parsing tool input from tool:', e) + } + } + + // Add tool message to conversation + messages.push({ + role: 'tool', + content: toolOutput, + tool_call_id: toolCall.id, + name: toolCall.name, + additional_kwargs: { + artifacts: parsedArtifacts, + sourceDocuments: parsedDocs + } + }) + + // Track used tools + usedTools.push({ + tool: toolCall.name, + toolInput: toolInput ?? toolCall.args, + toolOutput + }) + } catch (e) { + if (options.analyticHandlers && toolIds) { + await options.analyticHandlers.onToolEnd(toolIds, e) + } + + console.error('Error invoking tool:', e) + const errMsg = getErrorMessage(e) + let toolInput = toolCall.args + if (typeof errMsg === 'string' && errMsg.includes(TOOL_ARGS_PREFIX)) { + const [_, args] = errMsg.split(TOOL_ARGS_PREFIX) + try { + toolInput = JSON.parse(args) + } catch (e) { + console.error('Error parsing tool input from tool:', e) + } + } + + usedTools.push({ + tool: selectedTool.name, + toolInput, + toolOutput: '', + error: getErrorMessage(e) + }) + sseStreamer?.streamUsedToolsEvent(chatId, flatten(usedTools)) + throw new Error(getErrorMessage(e)) + } + } + } + + // Return direct tool output if there's exactly one tool with returnDirect + if (response.tool_calls.length === 1) { + const selectedTool = toolsInstance.find((tool) => tool.name === response.tool_calls?.[0]?.name) + if (selectedTool && selectedTool.returnDirect) { + const lastToolOutput = usedTools[0]?.toolOutput || '' + const lastToolOutputString = typeof lastToolOutput === 'string' ? lastToolOutput : JSON.stringify(lastToolOutput, null, 2) + + if (sseStreamer && !isStructuredOutput) { + sseStreamer.streamTokenEvent(chatId, lastToolOutputString) + } + + return { + response: new AIMessageChunk(lastToolOutputString), + usedTools, + sourceDocuments, + artifacts, + totalTokens, + accumulatedReasonContent: accumulatedReasonContent || undefined, + accumulatedReasoningDuration: accumulatedReasoningDuration || undefined + } + } + } + + if (response.tool_calls.length === 0) { + const responseContent = extractResponseContent(response) + return { + response: new AIMessageChunk(responseContent), + usedTools, + sourceDocuments, + artifacts, + totalTokens, + accumulatedReasonContent: accumulatedReasonContent || undefined, + accumulatedReasoningDuration: accumulatedReasoningDuration || undefined + } + } + + // Get LLM response after tool calls + let newResponse: AIMessageChunk + + if (isStreamable) { + newResponse = await this.handleStreamingResponse( + sseStreamer, + llmNodeInstance, + messages, + chatId, + abortController, + isStructuredOutput, + isLastNode + ) + } else { + newResponse = await llmNodeInstance.invoke(messages, { signal: abortController?.signal }) + + // Stream non-streaming response if this is the last node + if (isLastNode && sseStreamer && !isStructuredOutput) { + sseStreamer.streamTokenEvent(chatId, extractResponseContent(newResponse)) + } + } + + // Add tokens from this response + if (newResponse.usage_metadata?.total_tokens) { + totalTokens += newResponse.usage_metadata.total_tokens + } + + // Accumulate this turn's reasoning content and duration + if (newResponse.additional_kwargs?.reasoning_content) { + const chunkReason = newResponse.additional_kwargs.reasoning_content as string + accumulatedReasonContent += (accumulatedReasonContent ? '\n\n' : '') + chunkReason + } + if (typeof newResponse.additional_kwargs?.reasoning_duration === 'number') { + accumulatedReasoningDuration += newResponse.additional_kwargs.reasoning_duration + } + + // Check for recursive tool calls and handle them + if (newResponse.tool_calls && newResponse.tool_calls.length > 0) { + const { + response: recursiveResponse, + usedTools: recursiveUsedTools, + sourceDocuments: recursiveSourceDocuments, + artifacts: recursiveArtifacts, + totalTokens: recursiveTokens, + isWaitingForHumanInput: recursiveIsWaitingForHumanInput, + accumulatedReasonContent: recursiveAccumulatedReasonContent, + accumulatedReasoningDuration: recursiveAccumulatedReasoningDuration + } = await this.handleToolCalls({ + response: newResponse, + messages, + toolsInstance, + sseStreamer, + chatId, + input, + options, + abortController, + llmNodeInstance, + isStreamable, + isLastNode, + iterationContext, + isStructuredOutput, + accumulatedReasonContent, + accumulatedReasoningDuration, + planningTool + }) + + // Merge results from recursive tool calls + newResponse = recursiveResponse + usedTools.push(...recursiveUsedTools) + sourceDocuments = [...sourceDocuments, ...recursiveSourceDocuments] + artifacts = [...artifacts, ...recursiveArtifacts] + totalTokens += recursiveTokens + isWaitingForHumanInput = recursiveIsWaitingForHumanInput + if (recursiveAccumulatedReasonContent !== undefined) { + accumulatedReasonContent = recursiveAccumulatedReasonContent + } + if (recursiveAccumulatedReasoningDuration !== undefined) { + accumulatedReasoningDuration = recursiveAccumulatedReasoningDuration + } + } + + return { + response: newResponse, + usedTools, + sourceDocuments, + artifacts, + totalTokens, + isWaitingForHumanInput, + accumulatedReasonContent: accumulatedReasonContent || undefined, + accumulatedReasoningDuration: accumulatedReasoningDuration || undefined + } + } + + /** + * Handles tool calls and their responses, with support for recursive tool calling + */ + private async handleResumedToolCalls({ + humanInput, + humanInputAction, + messages, + toolsInstance, + sseStreamer, + chatId, + input, + options, + abortController, + llmWithoutToolsBind, + isStreamable, + isLastNode, + iterationContext, + isStructuredOutput = false, + planningTool + }: { + humanInput: IHumanInput + humanInputAction: Record | undefined + messages: BaseMessageLike[] + toolsInstance: Tool[] + sseStreamer: IServerSideEventStreamer | undefined + chatId: string + input: string | Record + options: ICommonObject + abortController: AbortController + llmWithoutToolsBind: BaseChatModel + isStreamable: boolean + isLastNode: boolean + iterationContext: ICommonObject + isStructuredOutput?: boolean + planningTool?: PlanningTool + }): Promise<{ + response: AIMessageChunk + usedTools: IUsedTool[] + sourceDocuments: Array + artifacts: any[] + totalTokens: number + isWaitingForHumanInput?: boolean + accumulatedReasonContent?: string + accumulatedReasoningDuration?: number + }> { + let llmNodeInstance = llmWithoutToolsBind + const usedTools: IUsedTool[] = [] + let sourceDocuments: Array = [] + let artifacts: any[] = [] + let isWaitingForHumanInput: boolean | undefined + + const lastCheckpointMessages = humanInputAction?.data?.input?.messages ?? [] + if (!lastCheckpointMessages.length) { + return { + response: new AIMessageChunk(''), + usedTools: [], + sourceDocuments: [], + artifacts: [], + totalTokens: 0, + accumulatedReasonContent: undefined, + accumulatedReasoningDuration: undefined + } + } + + // Use the last message as the response + const response = lastCheckpointMessages[lastCheckpointMessages.length - 1] as AIMessageChunk + + // Replace messages array + messages.length = 0 + messages.push(...lastCheckpointMessages.slice(0, lastCheckpointMessages.length - 1)) + + // Track total tokens used throughout this process + let totalTokens = response.usage_metadata?.total_tokens || 0 + + if (!response.tool_calls || response.tool_calls.length === 0) { + const acc = (response.additional_kwargs?.reasoning_content as string) || undefined + const dur = + typeof response.additional_kwargs?.reasoning_duration === 'number' + ? response.additional_kwargs.reasoning_duration + : undefined + return { + response, + usedTools: [], + sourceDocuments: [], + artifacts: [], + totalTokens, + accumulatedReasonContent: acc, + accumulatedReasoningDuration: dur + } + } + + // Stream tool calls if available + if (sseStreamer) { + const formattedToolCalls = response.tool_calls.map((toolCall: any) => ({ + tool: toolCall.name || 'tool', + toolInput: toolCall.args, + toolOutput: '' + })) + sseStreamer.streamCalledToolsEvent(chatId, flatten(formattedToolCalls)) + } + + // Remove tool calls with no id + const toBeRemovedToolCalls = [] + for (let i = 0; i < response.tool_calls.length; i++) { + const toolCall = response.tool_calls[i] + if (!toolCall.id) { + toBeRemovedToolCalls.push(toolCall) + usedTools.push({ + tool: toolCall.name || 'tool', + toolInput: toolCall.args, + toolOutput: response.content + }) + } + } + + for (const toolCall of toBeRemovedToolCalls) { + response.tool_calls.splice(response.tool_calls.indexOf(toolCall), 1) + } + + // Add LLM response with tool calls to messages + messages.push(response) + + // Process each tool call + for (let i = 0; i < response.tool_calls.length; i++) { + const toolCall = response.tool_calls[i] + + const selectedTool = toolsInstance.find((tool) => tool.name === toolCall.name) + if (selectedTool) { + let parsedDocs + let parsedArtifacts + + const flowConfig = { + chatflowId: options.chatflowid, + sessionId: options.sessionId, + chatId: options.chatId, + input: input, + state: options.agentflowRuntime?.state + } + + if (humanInput.type === 'reject') { + messages.pop() + const toBeRemovedTool = toolsInstance.find((tool) => tool.name === toolCall.name) + if (toBeRemovedTool) { + toolsInstance = toolsInstance.filter((tool) => tool.name !== toolCall.name) + // Remove other tools with the same agentSelectedTool such as MCP tools + toolsInstance = toolsInstance.filter( + (tool) => (tool as any).agentSelectedTool !== (toBeRemovedTool as any).agentSelectedTool + ) + } + } + if (humanInput.type === 'proceed') { + // Intercept write_todos — handled by PlanningTool, not the LangChain tool + if (toolCall.name === 'write_todos' && planningTool) { + const toolOutput = planningTool.handleToolCall(toolCall.args as { todos: Todo[] }) + messages.push({ role: 'tool', content: toolOutput, tool_call_id: toolCall.id, name: toolCall.name }) + usedTools.push({ tool: 'write_todos', toolInput: toolCall.args, toolOutput }) + } else { + let toolIds: ICommonObject | undefined + if (options.analyticHandlers) { + toolIds = await options.analyticHandlers.onToolStart(toolCall.name, toolCall.args, options.parentTraceIds) + } + + try { + let toolOutput = await selectedTool.call( + toolCall.args, + { signal: abortController?.signal }, + //@ts-ignore + undefined, + //@ts-ignore + flowConfig + ) + + if (options.analyticHandlers && toolIds) { + await options.analyticHandlers.onToolEnd(toolIds, toolOutput) + } + + // Extract source documents if present + if (typeof toolOutput === 'string' && toolOutput.includes(SOURCE_DOCUMENTS_PREFIX)) { + const [output, docs] = toolOutput.split(SOURCE_DOCUMENTS_PREFIX) + toolOutput = output + try { + parsedDocs = JSON.parse(docs) + sourceDocuments.push(parsedDocs) + } catch (e) { + console.error('Error parsing source documents from tool:', e) + } + } + + // Extract artifacts if present + if (typeof toolOutput === 'string' && toolOutput.includes(ARTIFACTS_PREFIX)) { + const [output, artifact] = toolOutput.split(ARTIFACTS_PREFIX) + toolOutput = output + try { + parsedArtifacts = JSON.parse(artifact) + artifacts.push(parsedArtifacts) + } catch (e) { + console.error('Error parsing artifacts from tool:', e) + } + } + + let toolInput + if (typeof toolOutput === 'string' && toolOutput.includes(TOOL_ARGS_PREFIX)) { + const [output, args] = toolOutput.split(TOOL_ARGS_PREFIX) + toolOutput = output + try { + toolInput = JSON.parse(args) + } catch (e) { + console.error('Error parsing tool input from tool:', e) + } + } + + // Add tool message to conversation + messages.push({ + role: 'tool', + content: toolOutput, + tool_call_id: toolCall.id, + name: toolCall.name, + additional_kwargs: { + artifacts: parsedArtifacts, + sourceDocuments: parsedDocs + } + }) + + // Track used tools + usedTools.push({ + tool: toolCall.name, + toolInput: toolInput ?? toolCall.args, + toolOutput + }) + } catch (e) { + if (options.analyticHandlers && toolIds) { + await options.analyticHandlers.onToolEnd(toolIds, e) + } + + console.error('Error invoking tool:', e) + const errMsg = getErrorMessage(e) + let toolInput = toolCall.args + if (typeof errMsg === 'string' && errMsg.includes(TOOL_ARGS_PREFIX)) { + const [_, args] = errMsg.split(TOOL_ARGS_PREFIX) + try { + toolInput = JSON.parse(args) + } catch (e) { + console.error('Error parsing tool input from tool:', e) + } + } + + usedTools.push({ + tool: selectedTool.name, + toolInput, + toolOutput: '', + error: getErrorMessage(e) + }) + sseStreamer?.streamUsedToolsEvent(chatId, flatten(usedTools)) + throw new Error(getErrorMessage(e)) + } + } // close else (non-write_todos tools) + } + } + } + + // Return direct tool output if there's exactly one tool with returnDirect + if (response.tool_calls.length === 1) { + const selectedTool = toolsInstance.find((tool) => tool.name === response.tool_calls?.[0]?.name) + if (selectedTool && selectedTool.returnDirect) { + const lastToolOutput = usedTools[0]?.toolOutput || '' + const lastToolOutputString = typeof lastToolOutput === 'string' ? lastToolOutput : JSON.stringify(lastToolOutput, null, 2) + + if (sseStreamer && !isStructuredOutput) { + sseStreamer.streamTokenEvent(chatId, lastToolOutputString) + } + + const acc = (response.additional_kwargs?.reasoning_content as string) || undefined + const dur = + typeof response.additional_kwargs?.reasoning_duration === 'number' + ? response.additional_kwargs.reasoning_duration + : undefined + return { + response: new AIMessageChunk(lastToolOutputString), + usedTools, + sourceDocuments, + artifacts, + totalTokens, + accumulatedReasonContent: acc, + accumulatedReasoningDuration: dur + } + } + } + + // Get LLM response after tool calls + let newResponse: AIMessageChunk + + if (llmNodeInstance && (llmNodeInstance as any).builtInTools && (llmNodeInstance as any).builtInTools.length > 0) { + toolsInstance.push(...(llmNodeInstance as any).builtInTools) + } + + if (llmNodeInstance && toolsInstance.length > 0) { + if (llmNodeInstance.bindTools === undefined) { + throw new Error(`Agent needs to have a function calling capable models.`) + } + + // @ts-ignore + llmNodeInstance = llmNodeInstance.bindTools(toolsInstance) + } + + if (isStreamable) { + newResponse = await this.handleStreamingResponse( + sseStreamer, + llmNodeInstance, + messages, + chatId, + abortController, + isStructuredOutput, + isLastNode + ) + } else { + newResponse = await llmNodeInstance.invoke(messages, { signal: abortController?.signal }) + + // Stream non-streaming response if this is the last node + if (isLastNode && sseStreamer && !isStructuredOutput) { + sseStreamer.streamTokenEvent(chatId, extractResponseContent(newResponse)) + } + } + + // Add tokens from this response + if (newResponse.usage_metadata?.total_tokens) { + totalTokens += newResponse.usage_metadata.total_tokens + } + + // Accumulate reasoning and duration from checkpoint response and this turn + let accumulatedReasonContent = (response.additional_kwargs?.reasoning_content as string) || '' + if (newResponse.additional_kwargs?.reasoning_content) { + accumulatedReasonContent += + (accumulatedReasonContent ? '\n\n' : '') + (newResponse.additional_kwargs.reasoning_content as string) + } + let accumulatedReasoningDuration = + (typeof response.additional_kwargs?.reasoning_duration === 'number' ? response.additional_kwargs.reasoning_duration : 0) + + (typeof newResponse.additional_kwargs?.reasoning_duration === 'number' ? newResponse.additional_kwargs.reasoning_duration : 0) + + // Check for recursive tool calls and handle them + if (newResponse.tool_calls && newResponse.tool_calls.length > 0) { + const { + response: recursiveResponse, + usedTools: recursiveUsedTools, + sourceDocuments: recursiveSourceDocuments, + artifacts: recursiveArtifacts, + totalTokens: recursiveTokens, + isWaitingForHumanInput: recursiveIsWaitingForHumanInput, + accumulatedReasonContent: recursiveAccumulatedReasonContent, + accumulatedReasoningDuration: recursiveAccumulatedReasoningDuration + } = await this.handleToolCalls({ + response: newResponse, + messages, + toolsInstance, + sseStreamer, + chatId, + input, + options, + abortController, + llmNodeInstance, + isStreamable, + isLastNode, + iterationContext, + isStructuredOutput, + accumulatedReasonContent, + accumulatedReasoningDuration, + planningTool + }) + + // Merge results from recursive tool calls + newResponse = recursiveResponse + usedTools.push(...recursiveUsedTools) + sourceDocuments = [...sourceDocuments, ...recursiveSourceDocuments] + artifacts = [...artifacts, ...recursiveArtifacts] + totalTokens += recursiveTokens + isWaitingForHumanInput = recursiveIsWaitingForHumanInput + if (recursiveAccumulatedReasonContent !== undefined) { + accumulatedReasonContent = recursiveAccumulatedReasonContent + } + if (recursiveAccumulatedReasoningDuration !== undefined) { + accumulatedReasoningDuration = recursiveAccumulatedReasoningDuration + } + } + + return { + response: newResponse, + usedTools, + sourceDocuments, + artifacts, + totalTokens, + isWaitingForHumanInput, + accumulatedReasonContent: accumulatedReasonContent || undefined, + accumulatedReasoningDuration: accumulatedReasoningDuration || undefined + } + } + + /** + * Processes sandbox links in the response text and converts them to file annotations + */ + private async processSandboxLinks(text: string, baseURL: string, chatflowId: string, chatId: string): Promise { + let processedResponse = text + + // Regex to match sandbox links: [text](sandbox:/path/to/file) + const sandboxLinkRegex = /\[([^\]]+)\]\(sandbox:\/([^)]+)\)/g + const matches = Array.from(text.matchAll(sandboxLinkRegex)) + + for (const match of matches) { + const fullMatch = match[0] + const linkText = match[1] + const filePath = match[2] + + try { + // Extract and sanitize filename from the file path (LLM-generated, untrusted) + const fileName = sanitizeFileName(filePath) + + // Replace sandbox link with proper download URL + const downloadUrl = `${baseURL}/api/v1/get-upload-file?chatflowId=${chatflowId}&chatId=${chatId}&fileName=${fileName}&download=true` + const newLink = `[${linkText}](${downloadUrl})` + + processedResponse = processedResponse.replace(fullMatch, newLink) + } catch (error) { + console.error('Error processing sandbox link:', error) + // If there's an error, remove the sandbox link as fallback + processedResponse = processedResponse.replace(fullMatch, linkText) + } + } + + return processedResponse + } +} + +module.exports = { nodeClass: SmartAgent_Agentflow } diff --git a/packages/components/nodes/agentflow/SmartAgent/context/SystemPromptBuilder.ts b/packages/components/nodes/agentflow/SmartAgent/context/SystemPromptBuilder.ts new file mode 100644 index 00000000000..8de58d51e23 --- /dev/null +++ b/packages/components/nodes/agentflow/SmartAgent/context/SystemPromptBuilder.ts @@ -0,0 +1,169 @@ +// Part 1: Base agent prompt +const BASE_AGENT_PROMPT = `You are a smart and powerful AI Agent that helps users accomplish tasks using tools. You respond with text and tool calls. The user can see your responses and tool outputs in real time. + +## Core Behavior + +- Be concise and direct. Don't over-explain unless asked. +- NEVER add unnecessary preamble ("Sure!", "Great question!", "I'll now..."). +- Don't say "I'll now do X" — just do it. +- If the request is ambiguous, ask questions before acting. +- If asked how to approach something, explain first, then act. + +## Professional Objectivity + +- Prioritize accuracy over validating the user's beliefs +- Disagree respectfully when the user is incorrect +- Avoid unnecessary superlatives, praise, or emotional validation + +## Doing Tasks + +When the user asks you to do something: + +1. **Understand first** — read relevant files, check existing patterns. Quick but thorough — gather enough evidence to start, then iterate. +2. **Act** — implement the solution. Work quickly but accurately. +3. **Verify** — check your work against what was asked, not against your own output. Your first attempt is rarely correct — iterate. + +Keep working until the task is fully complete. Don't stop partway and explain what you would do — just do it. Only yield back to the user when the task is done or you're genuinely blocked. + +**When things go wrong:** +- If something fails repeatedly, stop and analyze *why* — don't keep retrying the same approach. +- If you're blocked, tell the user what's wrong and ask for guidance. + +## Progress Updates + +For longer tasks, provide brief progress updates at reasonable intervals — a concise sentence recapping what you've done and what's next.` + +// Part 3: Skills prompt +const SKILLS_PROMPT = `## Skills +// TODO: skills prompt — frontmatter list + load instructions` + +// Part 4: Filesystem tool prompt +const FILESYSTEM_TOOL_PROMPT = `## Filesystem Tools +// TODO: ls, read_file, write_file usage guidance` + +// Part 5: Subagent prompt +const SUBAGENT_PROMPT = `## Subagent Delegation +// TODO: task delegation guidance` + +// Part 6: Async subagent prompt +const ASYNC_SUBAGENT_PROMPT = `## Async Subagent +// TODO: async subagent delegation guidance` + +// Part 7: Wrap user system message / memory content with agent_memory + memory_guidelines tags +function buildMemoryPrompt(content: string): string { + return ` +${content} + + + +The above was loaded in from files in your filesystem. As you learn from your interactions with the user, you can save new knowledge by calling the \`edit_file\` tool. + +**Learning from feedback:** +- One of your MAIN PRIORITIES is to learn from your interactions with the user. These learnings can be implicit or explicit. This means that in the future, you will remember this important information. +- When you need to remember something, updating memory must be your FIRST, IMMEDIATE action - before responding to the user, before calling other tools, before doing anything else. Just update memory immediately. +- When user says something is better/worse, capture WHY and encode it as a pattern. +- Each correction is a chance to improve permanently - don't just fix the immediate issue, update your instructions. +- A great opportunity to update your memories is when the user interrupts a tool call and provides feedback. You should update your memories immediately before revising the tool call. +- Look for the underlying principle behind corrections, not just the specific mistake. +- The user might not explicitly ask you to remember something, but if they provide information that is useful for future use, you should update your memories immediately. + +**Asking for information:** +- If you lack context to perform an action (e.g. send a Slack DM, requires a user ID/email) you should explicitly ask the user for this information. +- It is preferred for you to ask for information, don't assume anything that you do not know! +- When the user provides information that is useful for future use, you should update your memories immediately. + +**When to update memories:** +- When the user explicitly asks you to remember something (e.g., "remember my email", "save this preference") +- When the user describes your role or how you should behave (e.g., "you are a web researcher", "always do X") +- When the user gives feedback on your work - capture what was wrong and how to improve +- When the user provides information required for tool use (e.g., slack channel ID, email addresses) +- When the user provides context useful for future tasks, such as how to use tools, or which actions to take in a particular situation +- When you discover new patterns or preferences (coding styles, conventions, workflows) + +**When to NOT update memories:** +- When the information is temporary or transient (e.g., "I'm running late", "I'm on my phone right now") +- When the information is a one-time task request (e.g., "Find me a recipe", "What's 25 * 4?") +- When the information is a simple question that doesn't reveal lasting preferences (e.g., "What day is it?", "Can you explain X?") +- When the information is an acknowledgment or small talk (e.g., "Sounds good!", "Hello", "Thanks for that") +- When the information is stale or irrelevant in future conversations +- Never store API keys, access tokens, passwords, or any other credentials in any file, memory, or system prompt. +- If the user asks where to put API keys or provides an API key, do NOT echo or save it. + +**Examples:** +Example 1 (remembering user information): +User: Can you connect to my google account? +Agent: Sure, I'll connect to your google account, what's your google account email? +User: john@example.com +Agent: Let me save this to my memory. +Tool Call: edit_file(...) -> remembers that the user's google account email is john@example.com + +Example 2 (remembering implicit user preferences): +User: Can you write me an example for creating a deep agent in LangChain? +Agent: Sure, I'll write you an example for creating a deep agent in LangChain +User: Can you do this in JavaScript +Agent: Let me save this to my memory. +Tool Call: edit_file(...) -> remembers that the user prefers to get LangChain code examples in JavaScript +Agent: Sure, here is the JavaScript example + +Example 3 (do not remember transient information): +User: I'm going to play basketball tonight so I will be offline for a few hours. +Agent: Okay I'll add a block to your calendar. +Tool Call: create_calendar_event(...) -> just calls a tool, does not commit anything to memory, as it is transient information +` +} + +export interface SystemPromptOptions { + todoListPrompt: string // Part 2: from PlanningTool + skillsEnabled?: boolean // Part 3 + filesystemEnabled?: boolean // Part 4 + subagentEnabled?: boolean // Part 5 + asyncSubagentEnabled?: boolean // Part 6 + userSystemPrompt?: string // Part 7: user-specified system message / memory (AGENTS.md) +} + +/** + * Assembles the system prompt in a fixed order: + * 1. BASE_AGENT_PROMPT (always) + * 2. Todo list prompt (always) + * 3. Skills prompt (if configured) + * 4. Filesystem tool prompt + * 5. Subagent prompt + * 6. Async subagent prompt (if configured) + * 7. User specified system message / Memory prompt ( + ) + */ +export function buildSystemPrompt(opts: SystemPromptOptions): string { + const parts: string[] = [] + + // Part 1: Base agent prompt (always) + parts.push(BASE_AGENT_PROMPT) + + // Part 2: Todo list prompt (always) + parts.push(opts.todoListPrompt) + + // Part 3: Skills — if configured + if (opts.skillsEnabled) { + parts.push(SKILLS_PROMPT) + } + + // Part 4: Filesystem tool prompt + if (opts.filesystemEnabled) { + parts.push(FILESYSTEM_TOOL_PROMPT) + } + + // Part 5: Subagent prompt + if (opts.subagentEnabled) { + parts.push(SUBAGENT_PROMPT) + } + + // Part 6: Async subagent prompt — if configured + if (opts.asyncSubagentEnabled) { + parts.push(ASYNC_SUBAGENT_PROMPT) + } + + // Part 7: User specified system message / Memory prompt / AGENT.md — if content exists + if (opts.userSystemPrompt) { + parts.push(buildMemoryPrompt(opts.userSystemPrompt)) + } + + return parts.join('\n\n') +} diff --git a/packages/components/nodes/agentflow/SmartAgent/planning/PlanningTool.ts b/packages/components/nodes/agentflow/SmartAgent/planning/PlanningTool.ts new file mode 100644 index 00000000000..d249e94a662 --- /dev/null +++ b/packages/components/nodes/agentflow/SmartAgent/planning/PlanningTool.ts @@ -0,0 +1,142 @@ +export type TodoStatus = 'pending' | 'in_progress' | 'completed' + +export interface Todo { + content: string + status: TodoStatus +} + +const WRITE_TODOS_DESCRIPTION = `Use this tool to create and manage a structured task list for your current work session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user. +It also helps the user understand the progress of the task and overall progress of their requests. +Only use this tool if you think it will be helpful in staying organized. If the user's request is trivial and takes less than 3 steps, it is better to NOT use this tool and just do the task directly. + +## When to Use This Tool +Use this tool in these scenarios: + +1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions +2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations +3. User explicitly requests todo list - When the user directly asks you to use the todo list +4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated) +5. The plan may need future revisions or updates based on results from the first few steps. Keeping track of this in a list is helpful. + +## How to Use This Tool +1. When you start working on a task - Mark it as in_progress BEFORE beginning work. +2. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation. +3. You can also update future tasks, such as deleting them if they are no longer necessary, or adding new tasks that are necessary. Don't change previously completed tasks. +4. You can make several updates to the todo list at once. For example, when you complete a task, you can mark the next task you need to start as in_progress. + +## When NOT to Use This Tool +It is important to skip using this tool when: +1. There is only a single, straightforward task +2. The task is trivial and tracking it provides no benefit +3. The task can be completed in less than 3 trivial steps +4. The task is purely conversational or informational + +## Task States and Management + +1. **Task States**: Use these states to track progress: + - pending: Task not yet started + - in_progress: Currently working on (you can have multiple tasks in_progress at a time if they are not related to each other and can be run in parallel) + - completed: Task finished successfully + +2. **Task Management**: + - Update task status in real-time as you work + - Mark tasks complete IMMEDIATELY after finishing (don't batch completions) + - Complete current tasks before starting new ones + - Remove tasks that are no longer relevant from the list entirely + - IMPORTANT: When you write this todo list, you should mark your first task (or tasks) as in_progress immediately!. + - IMPORTANT: Unless all tasks are completed, you should always have at least one task in_progress to show the user that you are working on something. + +3. **Task Completion Requirements**: + - ONLY mark a task as completed when you have FULLY accomplished it + - If you encounter errors, blockers, or cannot finish, keep the task as in_progress + - When blocked, create a new task describing what needs to be resolved + - Never mark a task as completed if: + - There are unresolved issues or errors + - Work is partial or incomplete + - You encountered blockers that prevent completion + - You couldn't find necessary resources or dependencies + - Quality standards haven't been met + +4. **Task Breakdown**: + - Create specific, actionable items + - Break complex tasks into smaller, manageable steps + - Use clear, descriptive task names + +Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully +Remember: If you only need to make a few tool calls to complete a task, and it is clear what you need to do, it is better to just do the task directly and NOT call this tool at all.` + +const TODO_SYSTEM_PROMPT = `## \`write_todos\` + +You have access to the \`write_todos\` tool to help you manage and plan complex objectives. +Use this tool for complex objectives to ensure that you are tracking each necessary step and giving the user visibility into your progress. +This tool is very helpful for planning complex objectives, and for breaking down these larger complex objectives into smaller steps. + +It is critical that you mark todos as completed as soon as you are done with a step. Do not batch up multiple steps before marking them as completed. +For simple objectives that only require a few steps, it is better to just complete the objective directly and NOT use this tool. +Writing todos takes time and tokens, use it when it is helpful for managing complex many-step problems! But not for simple few-step requests. + +## Important To-Do List Usage Notes to Remember +- The \`write_todos\` tool should never be called multiple times in parallel. +- Don't be afraid to revise the To-Do list as you go. New information may reveal new tasks that need to be done, or old tasks that are irrelevant.` + +export interface PlanningToolOptions { + onUpdate?: (todos: Todo[]) => void +} + +export class PlanningTool { + #todos: Todo[] = [] + onUpdate?: (todos: Todo[]) => void + + constructor(options?: PlanningToolOptions) { + this.onUpdate = options?.onUpdate + } + + get todos(): Todo[] { + return this.#todos + } + + get toolDefinition(): { name: string; description: string; parameters: Record } { + return { + name: 'write_todos', + description: WRITE_TODOS_DESCRIPTION, + parameters: { + type: 'object', + properties: { + todos: { + type: 'array', + description: 'List of todo items to update', + items: { + type: 'object', + properties: { + content: { type: 'string', description: 'Content of the todo item' }, + status: { + type: 'string', + enum: ['pending', 'in_progress', 'completed'], + description: 'Status of the todo' + } + }, + required: ['content', 'status'] + } + } + }, + required: ['todos'] + } + } + } + + getSystemPrompt(): string { + return TODO_SYSTEM_PROMPT + } + + handleToolCall(args: { todos: Todo[] }): string { + this.#todos = args.todos + if (this.onUpdate) { + this.onUpdate(this.#todos) + } + return `Updated todo list to ${JSON.stringify(this.#todos)}` + } + + load(todos: Todo[]): void { + this.#todos = todos + } +} diff --git a/packages/components/nodes/agentflow/utils.ts b/packages/components/nodes/agentflow/utils.ts index 59e3fb569df..4a80677af1e 100644 --- a/packages/components/nodes/agentflow/utils.ts +++ b/packages/components/nodes/agentflow/utils.ts @@ -197,6 +197,49 @@ export const revertBase64ImagesToFileRefs = (messages: BaseMessageLike[]): BaseM return updatedMessages } +// ─── Normalizing messages for DB storage/UI rendering ──────────────────────── + +/** + * Converts LangChain message/chunk instances into plain JSON objects for clean DB storage. + * This avoids persisting large `{ lc, type, kwargs }` blobs and keeps execution-details UI readable. + */ +export const normalizeMessagesForStorage = (messages: BaseMessageLike[]): BaseMessageLike[] => { + return (messages || []).map((msg: any) => { + if (msg?.lc_namespace || typeof msg?._getType === 'function') { + const rawType = typeof msg?._getType === 'function' ? msg._getType() : msg?.type + const role = + rawType === 'ai' + ? 'assistant' + : rawType === 'human' + ? 'user' + : rawType === 'system' + ? 'system' + : rawType === 'tool' + ? 'tool' + : msg?.role || 'assistant' + + const plain: Record = { + role, + content: msg?.content ?? '' + } + + if (msg?.name) plain.name = msg.name + if (msg?.tool_call_id) plain.tool_call_id = msg.tool_call_id + if (Array.isArray(msg?.tool_calls) && msg.tool_calls.length > 0) plain.tool_calls = msg.tool_calls + + if (msg?.additional_kwargs && Object.keys(msg.additional_kwargs).length > 0) { + plain.additional_kwargs = msg.additional_kwargs + } + + if (msg?.usage_metadata) plain.usage_metadata = msg.usage_metadata + if (msg?.id) plain.id = msg.id + + return plain + } + return msg + }) +} + // ─── Handling new image uploads ────────────────────────────────────────────── /** diff --git a/packages/server/src/enterprise/services/organization.service.ts b/packages/server/src/enterprise/services/organization.service.ts index 2b181856574..1c0de448ea2 100644 --- a/packages/server/src/enterprise/services/organization.service.ts +++ b/packages/server/src/enterprise/services/organization.service.ts @@ -4,6 +4,7 @@ import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { generateId } from '../../utils' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { Telemetry } from '../../utils/telemetry' +import { Credential } from '../../database/entities/Credential' import { Organization, OrganizationName } from '../database/entities/organization.entity' import { isInvalidName, isInvalidUUID } from '../utils/validation.util' import { UserErrorMessage, UserService } from './user.service' @@ -35,7 +36,42 @@ export class OrganizationService { public async readOrganizationById(id: string | undefined, queryRunner: QueryRunner) { this.validateOrganizationId(id) - return await queryRunner.manager.findOneBy(Organization, { id }) + const organization = await queryRunner.manager.findOneBy(Organization, { id }) + if (!organization) return null + + // TODO: Replace mocked defaultConfig with actual governance settings from database + const credential = await queryRunner.manager.findOneBy(Credential, { credentialName: 'openAIApi' }) + const credentialId = credential?.id || '' + const defaultConfig = { + chatModel: { + name: 'chatOpenAI', + label: 'OpenAI', + inputs: { + cache: '', + modelName: 'gpt-5.4', + temperature: 0.9, + streaming: true, + allowImageUploads: '', + reasoning: '', + reasoningEffort: '', + reasoningSummary: '', + maxTokens: '', + topP: '', + frequencyPenalty: '', + presencePenalty: '', + timeout: '', + strictToolCalling: '', + stopSequence: '', + basepath: '', + baseOptions: '', + FLOWISE_CREDENTIAL_ID: credentialId, + credential: credentialId + }, + credential: credentialId + } + } + + return { ...organization, defaultConfig: JSON.stringify(defaultConfig) } } public validateOrganizationName(name: string | undefined, isRegister: boolean = false) { diff --git a/packages/ui/package.json b/packages/ui/package.json index b04a3194f49..249dbded054 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -38,6 +38,7 @@ "@uiw/codemirror-theme-vscode": "^4.21.21", "@uiw/react-codemirror": "^4.21.21", "axios": "1.12.0", + "boring-avatars": "^2.0.4", "clsx": "^1.1.1", "dompurify": "^3.2.6", "dotenv": "^16.0.0", diff --git a/packages/ui/src/api/user.js b/packages/ui/src/api/user.js index 86165ec9ca7..b47ff4851f5 100644 --- a/packages/ui/src/api/user.js +++ b/packages/ui/src/api/user.js @@ -25,6 +25,7 @@ const getPlanProration = (subscriptionId, newPlanId) => const updateSubscriptionPlan = (subscriptionId, newPlanId, prorationDate) => client.post(`/organization/update-subscription-plan`, { subscriptionId, newPlanId, prorationDate }) const getCurrentUsage = () => client.get(`/organization/get-current-usage`) +const getOrganizationById = (id) => client.get(`/organization?id=${id}`) // workspace users const getAllUsersByWorkspaceId = (workspaceId) => client.get(`/workspaceuser?workspaceId=${workspaceId}`) @@ -55,5 +56,6 @@ export default { getPlanProration, updateSubscriptionPlan, getCurrentUsage, - deleteOrganizationUser + deleteOrganizationUser, + getOrganizationById } diff --git a/packages/ui/src/assets/scss/_themes-vars.module.scss b/packages/ui/src/assets/scss/_themes-vars.module.scss index 7235b84474a..9ef55509a6b 100644 --- a/packages/ui/src/assets/scss/_themes-vars.module.scss +++ b/packages/ui/src/assets/scss/_themes-vars.module.scss @@ -50,6 +50,7 @@ $grey400: #c4c4c4; $grey500: #9e9e9e; $grey600: #757575; $grey700: #616161; +$grey800: #424242; $grey900: #212121; // transparent @@ -139,6 +140,7 @@ $darkTextSecondary: #8492c4; grey500: $grey500; grey600: $grey600; grey700: $grey700; + grey800: $grey800; grey900: $grey900; // ==============================|| DARK THEME VARIANTS ||============================== // diff --git a/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx b/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx index 054f409c9e7..3817ab1b1d0 100644 --- a/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx +++ b/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx @@ -1,8 +1,9 @@ import PropTypes from 'prop-types' +import { useSelector } from 'react-redux' // material-ui import { useTheme } from '@mui/material/styles' -import { Divider, List, Typography } from '@mui/material' +import { Divider, List, Typography, useMediaQuery } from '@mui/material' // project imports import NavItem from '../NavItem' @@ -15,6 +16,9 @@ import { Available } from '@/ui-component/rbac/available' const NavGroup = ({ item }) => { const theme = useTheme() const { hasPermission, hasDisplay } = useAuth() + const drawerOpened = useSelector((state) => state.customization.opened) + const matchUpMd = useMediaQuery(theme.breakpoints.up('md')) + const collapsed = matchUpMd && !drawerOpened const listItems = (menu, level = 1) => { // Filter based on display and permission @@ -72,6 +76,7 @@ const NavGroup = ({ item }) => { <> {item.title} @@ -83,7 +88,7 @@ const NavGroup = ({ item }) => { ) } - sx={{ p: '16px', py: 2, display: 'flex', flexDirection: 'column', gap: 1 }} + sx={{ px: collapsed ? '8px' : '16px', py: 2, display: 'flex', flexDirection: 'column', gap: 1 }} > {renderPrimaryItems().map((menu) => listItems(menu))} @@ -96,11 +101,18 @@ const NavGroup = ({ item }) => { - {group.title} - + !collapsed && ( + + {group.title} + + ) } - sx={{ p: '16px', py: 2, display: 'flex', flexDirection: 'column', gap: 1 }} + sx={{ px: collapsed ? '8px' : '16px', py: 2, display: 'flex', flexDirection: 'column', gap: 1 }} > {group.children.map((menu) => listItems(menu))} diff --git a/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx b/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx index 3cd99194578..3ca99c290e5 100644 --- a/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx +++ b/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx @@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux' // material-ui import { useTheme } from '@mui/material/styles' -import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography, useMediaQuery } from '@mui/material' +import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Tooltip, Typography, useMediaQuery } from '@mui/material' // project imports import { MENU_OPEN, SET_MENU } from '@/store/actions' @@ -96,42 +96,61 @@ const NavItem = ({ item, level, navType, onClick, onUploadFile }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [navType, location.pathname]) - return ( + const drawerOpened = customization.opened + const matchUpMd = useMediaQuery(theme.breakpoints.up('md')) + const collapsed = matchUpMd && !drawerOpened + + const button = ( 1 ? 'transparent !important' : 'inherit', py: level > 1 ? 1 : 1.25, - pl: `${level * 24}px` + ...(collapsed ? { pl: 0, pr: 0, justifyContent: 'center' } : { pl: `${level * 24}px`, justifyContent: 'flex-start' }) }} selected={customization.isOpen.findIndex((id) => id === item.id) > -1} onClick={() => itemHandler(item.id)} > {item.id === 'loadChatflow' && handleFileUpload(e)} />} - {itemIcon} - id === item.id) > -1 ? 'h5' : 'body1'} - color='inherit' - sx={{ my: 0.5 }} - > - {item.title} - - } - secondary={ - item.caption && ( - - {item.caption} + + {itemIcon} + + {!collapsed && ( + id === item.id) > -1 ? 'h5' : 'body1'} + color='inherit' + sx={{ my: 0.5 }} + > + {item.title} - ) - } - sx={{ my: 'auto' }} - /> - {item.chip && ( + } + secondary={ + item.caption && ( + + {item.caption} + + ) + } + sx={{ my: 'auto' }} + /> + )} + {!collapsed && item.chip && ( { avatar={item.chip.avatar && {item.chip.avatar}} /> )} - {item.isBeta && ( + {!collapsed && item.isBeta && ( { )} ) + + return collapsed ? ( + + {button} + + ) : ( + button + ) } NavItem.propTypes = { diff --git a/packages/ui/src/layout/MainLayout/Sidebar/index.jsx b/packages/ui/src/layout/MainLayout/Sidebar/index.jsx index b05d6669ee8..427b2107f68 100644 --- a/packages/ui/src/layout/MainLayout/Sidebar/index.jsx +++ b/packages/ui/src/layout/MainLayout/Sidebar/index.jsx @@ -15,7 +15,7 @@ import LogoSection from '../LogoSection' import CloudMenuList from '@/layout/MainLayout/Sidebar/CloudMenuList' // store -import { drawerWidth, headerHeight } from '@/store/constant' +import { drawerWidth, miniDrawerWidth, headerHeight } from '@/store/constant' // ==============================|| SIDEBAR DRAWER ||============================== // @@ -60,32 +60,37 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => { const container = window !== undefined ? () => window.document.body : undefined + const desktopWidth = drawerOpen ? drawerWidth : miniDrawerWidth + return ( t.transitions.create('width', { duration: t.transitions.duration.shortest }) }} aria-label='mailbox folders' > {isAuthenticated && ( t.transitions.create('width', { duration: t.transitions.duration.shortest }) } }} ModalProps={{ keepMounted: true }} diff --git a/packages/ui/src/layout/MainLayout/ViewHeader.jsx b/packages/ui/src/layout/MainLayout/ViewHeader.jsx index 2be32d8a5af..6654a843038 100644 --- a/packages/ui/src/layout/MainLayout/ViewHeader.jsx +++ b/packages/ui/src/layout/MainLayout/ViewHeader.jsx @@ -54,7 +54,7 @@ const ViewHeader = ({ {title} {description && ( prop !== 'open' })(({ }), marginRight: 0, [theme.breakpoints.up('md')]: { - marginLeft: -drawerWidth, - width: `calc(100% - ${drawerWidth}px)` + marginLeft: 0, + width: `calc(100% - ${miniDrawerWidth}px)` }, [theme.breakpoints.down('md')]: { marginLeft: '20px', diff --git a/packages/ui/src/menu-items/agentsettings.js b/packages/ui/src/menu-items/agentsettings.js index 91986f337b6..a58067dd7c1 100644 --- a/packages/ui/src/menu-items/agentsettings.js +++ b/packages/ui/src/menu-items/agentsettings.js @@ -46,7 +46,7 @@ const agentSettings = { icon: icons.IconUsers }, { - id: 'chatflowConfiguration', + id: 'agentConfiguration', title: 'Configuration', type: 'item', url: '', diff --git a/packages/ui/src/store/constant.js b/packages/ui/src/store/constant.js index 627959fa1f2..043a98af815 100644 --- a/packages/ui/src/store/constant.js +++ b/packages/ui/src/store/constant.js @@ -19,6 +19,7 @@ import { export const gridSpacing = 3 export const drawerWidth = 260 +export const miniDrawerWidth = 72 export const appDrawerWidth = 320 export const headerHeight = 80 export const maxScroll = 100000 diff --git a/packages/ui/src/themes/index.js b/packages/ui/src/themes/index.js index b1067ab354a..b75949be575 100644 --- a/packages/ui/src/themes/index.js +++ b/packages/ui/src/themes/index.js @@ -37,7 +37,7 @@ export const theme = (customization) => { paper: color.paper, backgroundDefault: color.paper, background: color.primaryLight, - darkTextPrimary: color.grey700, + darkTextPrimary: color.grey800, darkTextSecondary: color.grey500, textDark: color.grey900, menuSelected: color.secondaryDark, diff --git a/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx b/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx index 0ae7f1414fe..32c1c6fbc02 100644 --- a/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx +++ b/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx @@ -31,6 +31,7 @@ import { IconLoader, IconCircleXFilled, IconRelationOneToManyFilled, + IconRobot, IconShare, IconWorld, IconX @@ -147,7 +148,10 @@ function CustomLabel({ icon: Icon, itemStatus, children, name, ...other }) { } // Otherwise display the node icon - const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === name) + const foundIcon = + name === 'smartAgentAgentflow' + ? { icon: IconRobot, color: '#9575CD' } + : AGENTFLOW_ICONS.find((icon) => icon.name === name) if (foundIcon) { return ( ({ id: node.uniqueNodeId, - label: node.nodeLabel, + label: node.data?.name === 'smartAgentAgentflow' ? 'Agent' : node.nodeLabel, name: node.data?.name, status: node.status, data: node.data, diff --git a/packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx b/packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx index 60d0d732508..3a54dc4bf24 100644 --- a/packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx +++ b/packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx @@ -25,7 +25,7 @@ import { } from '@mui/material' import { useTheme, darken } from '@mui/material/styles' import { useSnackbar } from 'notistack' -import { IconCoins, IconCoin, IconClock, IconChevronDown, IconDownload, IconTool } from '@tabler/icons-react' +import { IconCoins, IconCoin, IconClock, IconChevronDown, IconDownload, IconTool, IconRobot } from '@tabler/icons-react' import toolSVG from '@/assets/images/tool.svg' // Project imports @@ -261,7 +261,10 @@ export const NodeExecutionDetails = ({ data, label, status, metadata, isPublic, {(() => { const nodeName = data?.name || data?.id?.split('_')[0] - const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === nodeName) + const isSmartAgent = nodeName === 'smartAgentAgentflow' + const foundIcon = isSmartAgent + ? { icon: IconRobot, color: '#9575CD' } + : AGENTFLOW_ICONS.find((icon) => icon.name === nodeName) if (foundIcon) { return ( @@ -303,7 +306,7 @@ export const NodeExecutionDetails = ({ data, label, status, metadata, isPublic, })()} - {label} + {data?.name === 'smartAgentAgentflow' ? 'Agent' : label}
{data.output && data.output.timeMetadata && data.output.timeMetadata.delta && ( diff --git a/packages/ui/src/views/agentexecutions/index.jsx b/packages/ui/src/views/agentexecutions/index.jsx index fab4bfc928f..0518d9c93f2 100644 --- a/packages/ui/src/views/agentexecutions/index.jsx +++ b/packages/ui/src/views/agentexecutions/index.jsx @@ -11,6 +11,7 @@ import { DialogContent, DialogContentText, DialogTitle, + Fade, FormControl, Grid, IconButton, @@ -235,228 +236,230 @@ const AgentExecutions = () => { {error ? ( ) : ( - - - - {/* Filter Section */} - - - - - State - - - - - onDateChange('startDate', date)} - selectsStart - startDate={filters.startDate} - className='form-control' - wrapperClassName='datePicker' - maxDate={new Date()} - customInput={ - + + + + {/* Filter Section */} + + + + + State + + + + + onDateChange('startDate', date)} + selectsStart + startDate={filters.startDate} + className='form-control' + wrapperClassName='datePicker' + maxDate={new Date()} + customInput={ + + } + /> + + + onDateChange('endDate', date)} + selectsEnd + endDate={filters.endDate} + className='form-control' + wrapperClassName='datePicker' + minDate={filters.startDate} + maxDate={new Date()} + customInput={ + + } + /> + + + handleFilterChange('agentflowName', e.target.value)} + size='small' + sx={{ + '& .MuiOutlinedInput-notchedOutline': { + borderColor: borderColor + } + }} + /> + + + handleFilterChange('sessionId', e.target.value)} + size='small' + sx={{ + '& .MuiOutlinedInput-notchedOutline': { + borderColor: borderColor + } + }} + /> + + + + + + + + + + + + + + + + - - handleFilterChange('agentflowName', e.target.value)} - size='small' - sx={{ - '& .MuiOutlinedInput-notchedOutline': { - borderColor: borderColor - } + + + {executions?.length > 0 && ( + <> + { + setOpenDrawer(true) + const executionDetails = + typeof execution.executionData === 'string' + ? JSON.parse(execution.executionData) + : execution.executionData + setSelectedExecutionData(executionDetails) + setSelectedMetadata(omit(execution, ['executionData'])) }} /> - - - handleFilterChange('sessionId', e.target.value)} - size='small' - sx={{ - '& .MuiOutlinedInput-notchedOutline': { - borderColor: borderColor - } + + {/* Pagination and Page Size Controls */} + {!isLoading && total > 0 && ( + + )} + + setOpenDrawer(false)} + onProceedSuccess={() => { + setOpenDrawer(false) + getAllExecutions.request() + }} + onUpdateSharing={() => { + getAllExecutions.request() + }} + onRefresh={(executionId) => { + getAllExecutions.request() + getExecutionByIdApi.request(executionId) }} /> - - - - - - - - - - - - - - - - - - - - {executions?.length > 0 && ( - <> - { - setOpenDrawer(true) - const executionDetails = - typeof execution.executionData === 'string' - ? JSON.parse(execution.executionData) - : execution.executionData - setSelectedExecutionData(executionDetails) - setSelectedMetadata(omit(execution, ['executionData'])) - }} - /> - - {/* Pagination and Page Size Controls */} - {!isLoading && total > 0 && ( - - )} - - setOpenDrawer(false)} - onProceedSuccess={() => { - setOpenDrawer(false) - getAllExecutions.request() - }} - onUpdateSharing={() => { - getAllExecutions.request() - }} - onRefresh={(executionId) => { - getAllExecutions.request() - getExecutionByIdApi.request(executionId) - }} - /> - - )} - - {/* Delete Confirmation Dialog */} - - Confirm Deletion - - - Are you sure you want to delete {selectedExecutionIds.length} execution - {selectedExecutionIds.length !== 1 ? 's' : ''}? This action cannot be undone. - - - - - - - - - {!isLoading && (!executions || executions.length === 0) && ( - - - execution_empty - -
No Executions Yet
-
- )} -
+ + )} + + {/* Delete Confirmation Dialog */} + + Confirm Deletion + + + Are you sure you want to delete {selectedExecutionIds.length} execution + {selectedExecutionIds.length !== 1 ? 's' : ''}? This action cannot be undone. + + + + + + + + + {!isLoading && (!executions || executions.length === 0) && ( + + + execution_empty + +
No Executions Yet
+
+ )} +
+ )} ) diff --git a/packages/ui/src/views/agentflows/index.jsx b/packages/ui/src/views/agentflows/index.jsx index 9ee745c16fc..0a6589fcf12 100644 --- a/packages/ui/src/views/agentflows/index.jsx +++ b/packages/ui/src/views/agentflows/index.jsx @@ -1,18 +1,30 @@ import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' +import moment from 'moment' +import Avatar from 'boring-avatars' // material-ui -import { Box, Chip, IconButton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material' -import { useTheme } from '@mui/material/styles' +import { + Box, + Chip, + Fade, + IconButton, + InputAdornment, + OutlinedInput, + Skeleton, + Stack, + ToggleButton, + ToggleButtonGroup, + Tooltip, + Typography +} from '@mui/material' +import { useTheme, darken } from '@mui/material/styles' // project imports -import AgentsEmptySVG from '@/assets/images/agents_empty.svg' import ErrorBoundary from '@/ErrorBoundary' -import ViewHeader from '@/layout/MainLayout/ViewHeader' import { gridSpacing } from '@/store/constant' import { StyledPermissionButton } from '@/ui-component/button/RBACButtons' -import ItemCard from '@/ui-component/cards/ItemCard' import MainCard from '@/ui-component/cards/MainCard' import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination' @@ -29,9 +41,9 @@ import { AGENTFLOW_ICONS, baseURL } from '@/store/constant' import { useError } from '@/store/context/ErrorContext' // icons -import { IconAlertTriangle, IconLayoutGrid, IconList, IconPlus, IconX } from '@tabler/icons-react' +import { IconAlertTriangle, IconLayoutGrid, IconList, IconSearch, IconX } from '@tabler/icons-react' -// ==============================|| AGENTS ||============================== // +// ==============================|| AGENTFLOWS ||============================== // const Agentflows = () => { const navigate = useNavigate() @@ -170,187 +182,421 @@ const Agentflows = () => { }, [getAllAgentflows.data]) return ( - - {error ? ( - - ) : ( - - - - + + {error ? ( + + ) : ( + + + {/* ==================== Hero Section ==================== */} + - - V2 - - - V1 - - - - - - - - - - - } - sx={{ borderRadius: 2, height: 40 }} - > - Add New - - - - {/* Deprecation Notice For V1 */} - {agentflowVersion === 'v1' && showDeprecationNotice && ( - - - - V1 Agentflows are deprecated. We recommend migrating to V2 for improved performance and - continued support. + + Create an agentflow + + + + Multi-agent systems and agentic workflow orchestration + + + + + Create + + {!isLoading && total === 0 && ( + navigate('/marketplaces', { state: { typeFilter: ['AgentflowV2'] } })} + sx={{ + borderRadius: '24px', + px: 3, + height: 44, + textTransform: 'none', + fontSize: '0.95rem', + fontWeight: 600, + border: `1px solid ${theme.palette.grey[900] + 40}`, + backgroundColor: 'transparent', + color: theme.palette.text.primary, + '&:hover': { + backgroundColor: theme.palette.action.hover, + borderColor: theme.palette.grey[900] + 60 + } + }} + > + View Templates + + )} + - - - - - )} - {!isLoading && total > 0 && ( - <> - {!view || view === 'card' ? ( - - {getAllAgentflows.data?.data.filter(filterFlows).map((data, index) => ( - goToCanvas(data)} - data={data} - images={images[data.id]} - icons={icons[data.id]} + + {/* ==================== Agentflows Listing Section ==================== */} + {total > 0 && ( + + + Agentflows + + + + + + } + sx={{ + width: 250, + borderRadius: 2, + '& .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.grey[900] + 25 + } + }} /> - ))} + + + + V2 + + + V1 + + + + + + + + + + + - ) : ( - )} - {/* Pagination and Page Size Controls */} - - - )} - - {!isLoading && total === 0 && ( - - - AgentsEmptySVG - -
No Agents Yet
+ + {/* Deprecation Notice For V1 */} + {agentflowVersion === 'v1' && showDeprecationNotice && ( + + + + V1 Agentflows are deprecated. We recommend migrating to V2 for improved performance + and continued support. + + + + + + )} + + {isLoading && ( + + + + + + )} + + {!isLoading && total > 0 && ( + <> + {!view || view === 'card' ? ( + + {getAllAgentflows.data?.data.filter(filterFlows).map((data, index) => { + const flowImages = images[data.id] || [] + const flowIcons = icons[data.id] || [] + const combined = [ + ...flowIcons.map((ic) => ({ + type: 'icon', + icon: ic.icon, + color: ic.color, + label: ic.name + })), + ...flowImages.map((img) => ({ type: 'image', src: img.imageSrc, label: img.label })) + ] + const visible = combined.slice(0, 4) + const remaining = combined.length - visible.length + return ( + goToCanvas(data)} + sx={{ + display: 'flex', + alignItems: 'center', + gap: 1.5, + p: 2, + borderRadius: 3, + border: `1px solid ${theme.palette.grey[900]}15`, + cursor: 'pointer', + backgroundColor: theme.palette.card?.main || theme.palette.background.paper, + boxShadow: '0 2px 12px rgba(0,0,0,0.08)', + transition: 'background-color 0.2s, box-shadow 0.2s', + '&:hover': { + backgroundColor: theme.palette.card?.hover || theme.palette.action.hover, + boxShadow: '0 4px 20px rgba(0,0,0,0.12)' + } + }} + > + + + + + + {data.name || 'Untitled'} + + + {moment(data.updatedDate).format('MMM D, hh:mm A')} + + + {combined.length > 0 && ( + + {visible.map((item, i) => ( + + {item.type === 'image' ? ( + + + + ) : ( + + + + )} + + ))} + {remaining > 0 && ( + + +{remaining} + + )} + + )} + + ) + })} + + ) : ( + + )} + + + )}
- )} -
- )} - -
+ + )} + + + ) } diff --git a/packages/ui/src/views/agents/index.jsx b/packages/ui/src/views/agents/index.jsx index 1ff12453f32..337cd81c3f9 100644 --- a/packages/ui/src/views/agents/index.jsx +++ b/packages/ui/src/views/agents/index.jsx @@ -1,10 +1,16 @@ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import moment from 'moment' +import Avatar from 'boring-avatars' // material-ui import { Box, + Chip, + Fade, + IconButton, + InputAdornment, + OutlinedInput, Paper, Skeleton, Stack, @@ -20,16 +26,13 @@ import { Tooltip, Typography } from '@mui/material' -import { useTheme, styled } from '@mui/material/styles' +import { useTheme, styled, darken } from '@mui/material/styles' import { tableCellClasses } from '@mui/material/TableCell' import { useSelector } from 'react-redux' // project imports -import ViewHeader from '@/layout/MainLayout/ViewHeader' import MainCard from '@/ui-component/cards/MainCard' -import ItemCard from '@/ui-component/cards/ItemCard' import { baseURL, gridSpacing } from '@/store/constant' -import AssistantEmptySVG from '@/assets/images/assistant_empty.svg' import ErrorBoundary from '@/ErrorBoundary' import { StyledPermissionButton } from '@/ui-component/button/RBACButtons' import AgentListMenu from '@/ui-component/button/AgentListMenu' @@ -43,11 +46,14 @@ import chatflowsApi from '@/api/chatflows' import useApi from '@/hooks/useApi' // icons -import { IconPlus, IconLayoutGrid, IconList } from '@tabler/icons-react' +import { IconArrowUp, IconLayoutGrid, IconList, IconSearch } from '@tabler/icons-react' + +// ==============================|| CONSTANTS ||============================== // + +const SUGGESTION_CHIPS = ['Trip planner', 'Image generator', 'Code debugger', 'Research assistant', 'Decision helper'] // ==============================|| HELPERS ||============================== // -// Extract agent info from chatflow's flowData const parseAgentFromFlowData = (agent) => { try { if (!agent.flowData) return { name: agent.name, instruction: '', modelName: '' } @@ -59,7 +65,6 @@ const parseAgentFromFlowData = (agent) => { const modelName = inputs.agentModel || '' return { name: agent.name, instruction, modelName } } - // Old format: try toolAgent node const toolAgentNode = flowData.nodes?.find((n) => n.data?.name === 'toolAgent') if (toolAgentNode) { const instruction = toolAgentNode.data?.inputs?.systemMessage || '' @@ -73,11 +78,10 @@ const parseAgentFromFlowData = (agent) => { } } -// ==============================|| STYLED TABLE COMPONENTS ||============================== // +// ==============================|| STYLED COMPONENTS ||============================== // const StyledTableCell = styled(TableCell)(({ theme }) => ({ borderColor: theme.palette.grey[900] + 25, - [`&.${tableCellClasses.head}`]: { color: theme.palette.grey[900] }, @@ -108,6 +112,7 @@ const Agents = () => { const [total, setTotal] = useState(0) const [search, setSearch] = useState('') + const [generateInput, setGenerateInput] = useState('') const [view, setView] = useState(localStorage.getItem('agentDisplayStyle') || 'card') const [order, setOrder] = useState(localStorage.getItem('agent_order') || 'desc') const [orderBy, setOrderBy] = useState(localStorage.getItem('agent_orderBy') || 'updatedDate') @@ -137,6 +142,19 @@ const Agents = () => { navigate('/agents/new') } + const handleGenerate = (taskText) => { + const task = taskText || generateInput + if (!task.trim()) return + navigate('/agents/new', { state: { generateTask: task.trim() } }) + } + + const handleGenerateKeyDown = (e) => { + if (e.key === 'Enter') { + e.preventDefault() + handleGenerate() + } + } + function filterAgents(agent) { if (!search) return true return agent.name && agent.name.toLowerCase().indexOf(search.toLowerCase()) > -1 @@ -198,7 +216,6 @@ const Agents = () => { if (getAllAgentsApi.error) setError(getAllAgentsApi.error) }, [getAllAgentsApi.error]) - // Set agents from chatflows API (returns both ASSISTANT and AGENT types) useEffect(() => { if (getAllAgentsApi.data) { const agentList = getAllAgentsApi.data?.data || getAllAgentsApi.data || [] @@ -213,226 +230,410 @@ const Agents = () => { {error ? ( ) : ( - - - + + {/* ==================== Hero Section ==================== */} + - - - - - - - - } - > - Add New - - - - {isLoading && ( - - - - + Create an agent + + + + + Create Manually + + + setGenerateInput(e.target.value)} + onKeyDown={handleGenerateKeyDown} + endAdornment={ + + handleGenerate()} + disabled={!generateInput.trim()} + sx={{ + backgroundColor: generateInput.trim() + ? customization.isDarkMode + ? theme.palette.common.white + : theme.palette.common.black + : 'transparent', + color: generateInput.trim() + ? customization.isDarkMode + ? theme.palette.common.black + : theme.palette.common.white + : theme.palette.text.secondary, + borderRadius: '50%', + width: 28, + height: 28, + '&:hover': { + backgroundColor: generateInput.trim() + ? customization.isDarkMode + ? theme.palette.grey[200] + : theme.palette.grey[800] + : theme.palette.action.hover + } + }} + > + + + + } + sx={{ + borderRadius: '24px', + height: 44, + width: 200, + overflow: 'hidden', + '& .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.grey[900] + 25, + borderRadius: '24px' + } + }} + /> + + + + {SUGGESTION_CHIPS.map((label) => ( + handleGenerate(label)} + sx={{ + borderRadius: '16px', + cursor: 'pointer', + fontWeight: 500, + borderColor: theme.palette.grey[900] + 25, + color: theme.palette.text.primary, + '&:hover': { + backgroundColor: customization.isDarkMode + ? 'rgba(255,255,255,0.08)' + : 'rgba(0,0,0,0.04)' + } + }} + /> + ))} + - )} - {!isLoading && total > 0 && ( - <> - {!view || view === 'card' ? ( - - {getSortedData(agents).map((agent, index) => ( - 0 && ( + + + Agents + + + + + + } + sx={{ + width: 250, + borderRadius: 2, + '& .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.grey[900] + 25 + } + }} + /> + + navigate(`/agents/${agent.id}`)} - /> - ))} - - ) : ( - - - + + + - - - handleRequestSort('name')} - > - Name - - - Model - Instruction - - handleRequestSort('updatedDate')} + + + + + + )} + + {isLoading && ( + + + + + + )} + + {!isLoading && total > 0 && ( + <> + {!view || view === 'card' ? ( + + {getSortedData(agents).map((agent, index) => ( + navigate(`/agents/${agent.id}`)} + sx={{ + display: 'flex', + alignItems: 'center', + gap: 1.5, + p: 2, + borderRadius: 3, + border: `1px solid ${theme.palette.grey[900]}15`, + cursor: 'pointer', + backgroundColor: theme.palette.card?.main || theme.palette.background.paper, + boxShadow: '0 2px 12px rgba(0,0,0,0.08)', + transition: 'background-color 0.2s, box-shadow 0.2s', + '&:hover': { + backgroundColor: theme.palette.card?.hover || theme.palette.action.hover, + boxShadow: '0 4px 20px rgba(0,0,0,0.12)' + } + }} + > + + + + + - Last Modified - - - Actions - - - - {getSortedData(agents).map((agent, index) => { - const images = getImages(agent) - return ( - + navigate(`/agents/${agent.id}`)} > - - + {moment(agent.updatedDate).format('MMM D, hh:mm A')} + + + + ))} + + ) : ( + +
+ + + + handleRequestSort('name')} + > + Name + + + Model + Instruction + + handleRequestSort('updatedDate')} + > + Last Modified + + + Actions + + + + {getSortedData(agents).map((agent, index) => { + const images = getImages(agent) + return ( + navigate(`/agents/${agent.id}`)} + > + + + + {agent.name || 'Untitled'} + + + + + {images.length > 0 && ( + + + + + + )} + + - {agent.name || 'Untitled'} + {getInstruction(agent) || ''} - - - - {images.length > 0 && ( - - - - - - )} - - - - {getInstruction(agent) || ''} - - - - - {moment(agent.updatedDate).format('MMMM D, YYYY')} - - - e.stopPropagation()}> - - - - ) - })} - -
-
- )} - - - )} - {!isLoading && total === 0 && ( - - - AssistantEmptySVG - -
No Agents Added Yet
-
- )} -
+ + + + {moment(agent.updatedDate).format('MMMM D, YYYY')} + + + e.stopPropagation()}> + + + + ) + })} + + +
+ )} + + + )} + + )} diff --git a/packages/ui/src/views/apikey/index.jsx b/packages/ui/src/views/apikey/index.jsx index 190431876f5..32ab7f71a94 100644 --- a/packages/ui/src/views/apikey/index.jsx +++ b/packages/ui/src/views/apikey/index.jsx @@ -10,6 +10,7 @@ import { Button, Chip, Collapse, + Fade, IconButton, Paper, Popover, @@ -385,148 +386,150 @@ const APIKey = () => { {error ? ( ) : ( - - - } - id='btn_createApiKey' + + + - Create Key - - - {!isLoading && apiKeys?.length <= 0 ? ( - - - APIEmptySVG - -
No API Keys Yet
-
- ) : ( - <> - } + id='btn_createApiKey' > - - - - Key Name - API Key - Permissions - Usage - Updated - - - - - - - - - - {isLoading ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - <> - {apiKeys?.filter(filterKeys).map((key, index) => ( - { - navigator.clipboard.writeText(key.apiKey) - setAnchorEl(event.currentTarget) - setTimeout(() => { - handleClosePopOver() - }, 1500) - }} - onShowAPIClick={() => onShowApiKeyClick(key.apiKey)} - open={openPopOver} - anchorEl={anchorEl} - onClose={handleClosePopOver} - theme={theme} - onEditClick={() => edit(key)} - onDeleteClick={() => deleteKey(key)} - /> - ))} - - )} - -
-
- {/* Pagination and Page Size Controls */} - - - )} -
+ Create Key + + + {!isLoading && apiKeys?.length <= 0 ? ( + + + APIEmptySVG + +
No API Keys Yet
+
+ ) : ( + <> + + + + + Key Name + API Key + Permissions + Usage + Updated + + + + + + + + + + {isLoading ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {apiKeys?.filter(filterKeys).map((key, index) => ( + { + navigator.clipboard.writeText(key.apiKey) + setAnchorEl(event.currentTarget) + setTimeout(() => { + handleClosePopOver() + }, 1500) + }} + onShowAPIClick={() => onShowApiKeyClick(key.apiKey)} + open={openPopOver} + anchorEl={anchorEl} + onClose={handleClosePopOver} + theme={theme} + onEditClick={() => edit(key)} + onDeleteClick={() => deleteKey(key)} + /> + ))} + + )} + +
+
+ {/* Pagination and Page Size Controls */} + + + )} + + )} { - if (!agentNodeDef?.inputParams) return [] - const param = agentNodeDef.inputParams.find((p) => p.name === paramName) - return param?.options || [] -} - // Helper to build the built-in tools map from agentNodeDef — keyed by the model name in the `show` condition const getBuiltInToolsMap = (agentNodeDef) => { if (!agentNodeDef?.inputParams) return {} @@ -141,11 +142,13 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST const loadAgentInputRef = useRef() const canvas = useSelector((state) => state.canvas) const customization = useSelector((state) => state.customization) + const currentUser = useSelector((state) => state.auth.user) const getChatModelsApi = useApi(assistantsApi.getChatModels) const getDocStoresApi = useApi(assistantsApi.getDocStores) const getToolsApi = useApi(assistantsApi.getTools) const getSpecificChatflowApi = useApi(chatflowsApi.getSpecificChatflow) + const getOrganizationApi = useApi(userApi.getOrganizationById) const { id: routeId } = useParams() const location = useLocation() @@ -159,7 +162,12 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST const [chatModelsComponents, setChatModelsComponents] = useState([]) const [chatModelsOptions, setChatModelsOptions] = useState([]) const [selectedChatModel, setSelectedChatModel] = useState({}) + const [modelConfigDialogOpen, setModelConfigDialogOpen] = useState(false) + const previousChatModelRef = useRef(null) const [agentName, setAgentName] = useState('New Agent') + const [creationMode, setCreationMode] = useState(location.state?.generateTask ? 'describe' : 'manual') + const [defaultCheckComplete, setDefaultCheckComplete] = useState(false) + const [modelConfirmed, setModelConfirmed] = useState(false) const [customAssistantInstruction, setCustomAssistantInstruction] = useState('You are helpful assistant') const [documentStoreOptions, setDocumentStoreOptions] = useState([]) const [selectedDocumentStores, setSelectedDocumentStores] = useState([]) @@ -525,11 +533,11 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST { source: 'startAgentflow_0', sourceHandle: 'startAgentflow_0-output-startAgentflow', - target: 'agentAgentflow_0', - targetHandle: 'agentAgentflow_0', + target: 'smartAgentAgentflow_0', + targetHandle: 'smartAgentAgentflow_0', data: { sourceColor: '#7EE787', targetColor: '#4DD0E1', isHumanInput: false }, type: 'agentFlow', - id: 'startAgentflow_0-startAgentflow_0-output-startAgentflow-agentAgentflow_0-agentAgentflow_0' + id: 'startAgentflow_0-startAgentflow_0-output-startAgentflow-smartAgentAgentflow_0-smartAgentAgentflow_0' } ] } @@ -625,7 +633,7 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST const onSettingsItemClick = (setting) => { setSettingsOpen(false) - if (setting === 'deleteAssistant') { + if (setting === 'deleteAgent') { handleDeleteFlow() } else if (setting === 'viewMessages') { setViewMessagesDialogProps({ @@ -640,7 +648,7 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST chatflow: canvas.chatflow }) setViewLeadsDialogOpen(true) - } else if (setting === 'chatflowConfiguration') { + } else if (setting === 'agentConfiguration') { setChatflowConfigurationDialogProps({ title: `Agent Configuration`, chatflow: canvas.chatflow @@ -951,19 +959,24 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST getDocStoresApi.request() getToolsApi.request() + // Fetch org defaultConfig for new agents + if (isNewAgent && currentUser?.activeOrganizationId) { + getOrganizationApi.request(currentUser.activeOrganizationId) + } + // Fetch agentflow node definitions dynamically from server const fetchNodeDefs = async () => { try { const [startResp, agentResp] = await Promise.all([ nodesApi.getSpecificNode('startAgentflow'), - nodesApi.getSpecificNode('agentAgentflow') + nodesApi.getSpecificNode('smartAgentAgentflow') ]) if (startResp.data) { const startData = initNode(startResp.data, 'startAgentflow_0') setStartNodeDef(startData) } if (agentResp.data) { - const agentData = initNode(agentResp.data, 'agentAgentflow_0') + const agentData = initNode(agentResp.data, 'smartAgentAgentflow_0') setAgentNodeDef(agentData) } } catch (err) { @@ -976,8 +989,8 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST const fetchVSEmbeddingOptions = async () => { try { const [vsResp, embResp, vsComponentsResp, embComponentsResp] = await Promise.all([ - nodesApi.executeNodeLoadMethod('agentAgentflow', { loadMethod: 'listVectorStores' }), - nodesApi.executeNodeLoadMethod('agentAgentflow', { loadMethod: 'listEmbeddings' }), + nodesApi.executeNodeLoadMethod('smartAgentAgentflow', { loadMethod: 'listVectorStores' }), + nodesApi.executeNodeLoadMethod('smartAgentAgentflow', { loadMethod: 'listEmbeddings' }), nodesApi.getNodesByCategory('Vector Stores'), nodesApi.getNodesByCategory('Embeddings') ]) @@ -1182,8 +1195,8 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST try { const flowData = JSON.parse(flowDataStr) - // ---- New agentflow format (agentAgentflow node) ---- - const agentNode = flowData.nodes?.find((n) => n.data?.name === 'agentAgentflow') + // ---- New agentflow format (smartAgentAgentflow node) ---- + const agentNode = flowData.nodes?.find((n) => n.data?.name === 'smartAgentAgentflow') if (agentNode) { const inputs = agentNode.data?.inputs || {} const modelConfig = inputs.agentModelConfig || {} @@ -1426,6 +1439,49 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST // eslint-disable-next-line react-hooks/exhaustive-deps }, [getChatModelsApi.data]) + // TODO: Replace mocked defaultConfig with actual governance settings from database + useEffect(() => { + if (defaultCheckComplete) return + // Not a new agent or no org to check — nothing to wait for + if (!isNewAgent || !currentUser?.activeOrganizationId) { + setDefaultCheckComplete(true) + return + } + // Need both chat models and org response before we can decide + if (!getChatModelsApi.data || getOrganizationApi.data === null) return + + try { + if (getOrganizationApi.data?.defaultConfig) { + const config = JSON.parse(getOrganizationApi.data.defaultConfig) + if (config.chatModel) { + const saved = config.chatModel + const foundComponent = getChatModelsApi.data.find((c) => c.name === saved.name) + if (foundComponent) { + const chatModelId = `${foundComponent.name}_0` + const clonedComponent = cloneDeep(foundComponent) + const restored = initNode(clonedComponent, chatModelId) + if (saved.inputs) { + restored.inputs = { ...restored.inputs, ...saved.inputs } + } + // Restore credential reference + if (saved.credentialId || saved.credential) { + restored.credential = saved.credentialId || saved.credential + restored.inputs.FLOWISE_CREDENTIAL_ID = restored.credential + } + restored.inputParams = showHideInputParams(restored) + setSelectedChatModel(restored) + setModelConfirmed(true) + } + } + } + } catch (e) { + console.error('Error parsing defaultConfig', e) + } finally { + setDefaultCheckComplete(true) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getChatModelsApi.data, getOrganizationApi.data, isNewAgent, currentUser?.activeOrganizationId]) + useEffect(() => { if (getSpecificChatflowApi.data) { const chatflow = getSpecificChatflowApi.data @@ -1436,7 +1492,7 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST if (!name || name === 'Untitled') { try { const fd = chatflow.flowData ? JSON.parse(chatflow.flowData) : null - const agentNode = fd?.nodes?.find((n) => n.data?.name === 'agentAgentflow') + const agentNode = fd?.nodes?.find((n) => n.data?.name === 'smartAgentAgentflow') if (agentNode?.data?.label && agentNode.data.label !== 'Agent 0') { name = agentNode.data.label } @@ -1447,7 +1503,7 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST setAgentName(name || 'Untitled Agent') setLoadingAssistant(false) - // Load agent config from flowData (handles both old toolAgent and new agentAgentflow formats) + // Load agent config from flowData (handles both old toolAgent and new smartAgentAgentflow formats) if (chatflow.flowData) { loadAgentFromFlowData(chatflow.flowData) } @@ -1551,68 +1607,6 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST ) } - const renderMemorySection = () => { - return ( - - - Enable Memory - - - setEnableMemory(newValue)} /> - {enableMemory && ( - <> - - Memory Type - - setMemoryType(newValue || 'allMessages')} - value={memoryType} - /> - {memoryType === 'windowSize' && ( - <> - - Window Size - - - setMemoryWindowSize(Number(e.target.value))} - /> - - )} - {memoryType === 'conversationSummaryBuffer' && ( - <> - - Max Token Limit - - - setMemoryMaxTokenLimit(Number(e.target.value))} - /> - - )} - - )} - - ) - } - const renderStructuredOutputSection = () => { return ( + + { + if (!newValue) { + setSelectedChatModel({}) + } else if (newValue !== selectedChatModel?.name) { + const found = chatModelsComponents.find( + (c) => c.name === newValue + ) + if (found) { + previousChatModelRef.current = selectedChatModel + const id = `${found.name}_0` + const cloned = cloneDeep(found) + setSelectedChatModel(initNode(cloned, id)) + setModelConfigDialogOpen(true) + } + } + }} + value={selectedChatModel?.name || 'choose an option'} + disableClearable + /> + + setModelConfigDialogOpen(true)} + sx={{ + color: customization.isDarkMode + ? theme.palette.common.white + : theme.palette.text.primary + }} + > + + + + )} {isTemplatePreview ? ( - - - - - - + {!(isNewAgent && creationMode === 'describe') && ( + + + + + + + + )} )} {!isNewAgent && !loadingAssistant && !isTemplatePreview && ( @@ -1960,8 +2008,76 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST - {/* Form content — disabled in template preview mode */} - + {/* Mode toggle for new agents */} + {isNewAgent && !isTemplatePreview && ( + + { + if (newMode !== null) setCreationMode(newMode) + }} + sx={{ + borderRadius: '24px', + backgroundColor: theme.palette.grey[100], + ...(customization.isDarkMode && { + backgroundColor: theme.palette.grey[800] + }), + '& .MuiToggleButtonGroup-grouped': { + border: 'none', + borderRadius: '24px !important', + px: 3, + py: 0.75, + textTransform: 'none', + fontWeight: 600, + fontSize: '0.875rem', + color: theme.palette.text.secondary, + '&.Mui-selected': { + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, + boxShadow: '0 1px 3px rgba(0,0,0,0.12)', + '&:hover': { + backgroundColor: theme.palette.background.paper + } + }, + '&:hover': { + backgroundColor: 'transparent' + } + } + }} + > + Describe + Manual + + + )} + + {/* Describe mode */} + {isNewAgent && !isTemplatePreview && creationMode === 'describe' && ( + + )} + {/* Form content — disabled in template preview mode, hidden in describe mode */} + {/* Select Model */} {isNewAgent && ( @@ -2719,7 +2828,7 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST /> setViewLeadsDialogOpen(false)} /> setChatflowConfigurationDialogOpen(false)} @@ -2752,6 +2861,81 @@ const CustomAssistantConfigurePreview = ({ chatflowType: _chatflowType = 'ASSIST dialogProps={exportAsTemplateDialogProps} onCancel={() => setExportAsTemplateDialogOpen(false)} /> + { + if (previousChatModelRef.current) { + setSelectedChatModel(previousChatModelRef.current) + previousChatModelRef.current = null + } + setModelConfigDialogOpen(false) + }} + fullWidth + maxWidth='sm' + > + + + {selectedChatModel?.name && ( + + )} + {selectedChatModel?.label || selectedChatModel?.name} Configuration + + + + {selectedChatModel && + Object.keys(selectedChatModel).length > 0 && + showHideInputParams(selectedChatModel) + .filter( + (ip) => + !ip.hidden && + ip.display !== false && + ['credential', 'model', 'modelName', 'customModel', 'customModelName'].includes(ip.name) + ) + .map((ip, idx) => ( + + ))} + + + + { + previousChatModelRef.current = null + setModelConfigDialogOpen(false) + }} + > + Confirm + + + ) diff --git a/packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx b/packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx index 45ff7bd27d4..76fcda80ae9 100644 --- a/packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx +++ b/packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx @@ -5,17 +5,18 @@ import moment from 'moment' // material-ui import { Box, - Stack, + Fade, + Paper, Skeleton, - ToggleButton, - ToggleButtonGroup, + Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, - Paper, + ToggleButton, + ToggleButtonGroup, Typography } from '@mui/material' import { useTheme, styled } from '@mui/material/styles' @@ -137,154 +138,156 @@ const CustomAssistantLayout = () => { {error ? ( ) : ( - - - + + - - - - + + + + + + + } > - - -
- } - > - Add New - - + Add New + + + + {isLoading && ( + + + + + + )} - {isLoading && ( - - - - - - )} + {!isLoading && totalAgents > 0 && ( + <> + {!view || view === 'card' ? ( + + {getAllAssistantsApi.data?.filter(filterAssistants).map((data, index) => ( + navigate('/agents/' + data.id)} + /> + ))} + + ) : ( + + + + + Name + Model + Last Updated + Actions + + + + {getAllAssistantsApi.data?.filter(filterAssistants).map((data, index) => { + const details = JSON.parse(data.details) + return ( + navigate('/agents/' + data.id)} + > + + + {details.chatModel?.name && ( + + )} + {details.name} + + + + + {details.chatModel?.label || details.chatModel?.name || '-'} + + + + + {data.updatedDate + ? moment(data.updatedDate).format('MMM D, YYYY') + : '-'} + + + + + ) + })} + +
+
+ )} + + )} - {!isLoading && totalAgents > 0 && ( - <> - {!view || view === 'card' ? ( - - {getAllAssistantsApi.data?.filter(filterAssistants).map((data, index) => ( - navigate('/agents/' + data.id)} - /> - ))} + {!isLoading && totalAgents === 0 && ( + + + AssistantEmptySVG - ) : ( - - - - - Name - Model - Last Updated - Actions - - - - {getAllAssistantsApi.data?.filter(filterAssistants).map((data, index) => { - const details = JSON.parse(data.details) - return ( - navigate('/agents/' + data.id)} - > - - - {details.chatModel?.name && ( - - )} - {details.name} - - - - - {details.chatModel?.label || details.chatModel?.name || '-'} - - - - - {data.updatedDate - ? moment(data.updatedDate).format('MMM D, YYYY') - : '-'} - - - - - ) - })} - -
-
- )} - - )} - - {!isLoading && totalAgents === 0 && ( - - - AssistantEmptySVG - -
No Agents Added Yet
-
- )} -
+
No Agents Added Yet
+ + )} + + )} { + const theme = useTheme() + const customization = useSelector((state) => state.customization) + + const [describeInput, setDescribeInput] = useState('') + const [chatMessages, setChatMessages] = useState([]) + const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0) + const [isTyping, setIsTyping] = useState(false) + const [showScrollButton, setShowScrollButton] = useState(false) + + const chatEndRef = useRef(null) + const chatContainerRef = useRef(null) + const isUserNearBottom = useRef(true) + const isFirstMessage = useRef(true) + + // ==============================|| Scroll Helpers ||============================== // + + const scrollToBottom = (force = false) => { + setTimeout(() => { + if (force || isUserNearBottom.current || isFirstMessage.current) { + chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + isFirstMessage.current = false + } + }, 100) + } + + const handleChatScroll = () => { + const el = chatContainerRef.current + if (!el) return + const threshold = 80 + const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold + isUserNearBottom.current = atBottom + setShowScrollButton(!atBottom) + } + + const handleScrollToBottomClick = () => { + chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + setShowScrollButton(false) + isUserNearBottom.current = true + } + + // ==============================|| Chat Handlers ||============================== // + + const addBotMessage = (content, type = 'text', questionIndex) => { + setChatMessages((prev) => [...prev, { role: 'bot', content, type, questionIndex }]) + scrollToBottom() + } + + const handleDescribeSubmit = () => { + if (!describeInput.trim() || !modelConfirmed) return + const userMsg = describeInput.trim() + setChatMessages((prev) => [...prev, { role: 'user', content: userMsg }]) + setDescribeInput('') + setIsTyping(true) + scrollToBottom(true) + + const hasAskedQuestions = chatMessages.some((m) => m.type === 'question') + + if (!hasAskedQuestions) { + setTimeout(() => { + setIsTyping(false) + addBotMessage(`Great idea! I'll help you build that. I have a couple of questions to make sure I get it right.`) + setTimeout(() => { + setCurrentQuestionIndex(0) + addBotMessage(MOCK_QUESTIONS[0].text, 'question', 0) + scrollToBottom() + }, 600) + }, 1200) + } else { + setTimeout(() => { + setIsTyping(false) + const nextIdx = currentQuestionIndex + 1 + if (nextIdx < MOCK_QUESTIONS.length) { + setCurrentQuestionIndex(nextIdx) + addBotMessage(MOCK_QUESTIONS[nextIdx].text, 'question', nextIdx) + } else { + handleDescribeFinish(userMsg) + } + }, 800) + } + } + + const handleOptionSelect = (option) => { + setChatMessages((prev) => [...prev, { role: 'user', content: option }]) + setIsTyping(true) + scrollToBottom(true) + + setTimeout(() => { + setIsTyping(false) + const nextIdx = currentQuestionIndex + 1 + if (nextIdx < MOCK_QUESTIONS.length) { + setCurrentQuestionIndex(nextIdx) + addBotMessage(MOCK_QUESTIONS[nextIdx].text, 'question', nextIdx) + } else { + handleDescribeFinish(option) + } + }, 800) + } + + const handleSkipQuestion = () => { + setIsTyping(true) + scrollToBottom() + + setTimeout(() => { + setIsTyping(false) + const nextIdx = currentQuestionIndex + 1 + if (nextIdx < MOCK_QUESTIONS.length) { + setCurrentQuestionIndex(nextIdx) + addBotMessage(MOCK_QUESTIONS[nextIdx].text, 'question', nextIdx) + } else { + handleDescribeFinish() + } + }, 600) + } + + const handleDescribeFinish = () => { + addBotMessage("I've got everything I need. Setting up your agent now...") + const firstUserMsg = chatMessages.find((m) => m.role === 'user')?.content || 'New Agent' + setTimeout(() => { + setAgentName(firstUserMsg.length > 50 ? firstUserMsg.slice(0, 50) : firstUserMsg) + setCustomAssistantInstruction(firstUserMsg) + setCreationMode('manual') + }, 1500) + } + + // ==============================|| Effects ||============================== // + + // Auto-submit generate task when model is confirmed + useEffect(() => { + if (generateTask && chatMessages.length === 0) { + if (modelConfirmed) { + setTimeout(() => { + setChatMessages([{ role: 'user', content: generateTask }]) + setDescribeInput('') + setIsTyping(true) + setTimeout(() => { + setIsTyping(false) + setChatMessages((prev) => [ + ...prev, + { + role: 'bot', + content: `Great idea! I'll help you build that. I have a couple of questions to make sure I get it right.` + } + ]) + setTimeout(() => { + setCurrentQuestionIndex(0) + setChatMessages((prev) => [ + ...prev, + { role: 'bot', content: MOCK_QUESTIONS[0].text, type: 'question', questionIndex: 0 } + ]) + }, 600) + }, 1200) + }, 300) + } else { + setDescribeInput(generateTask) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [generateTask, modelConfirmed]) + + // Hide page scrollbar in describe mode + useEffect(() => { + const style = document.createElement('style') + style.id = 'hide-describe-scrollbar' + if (modelConfirmed) { + style.textContent = 'html, body { overflow: hidden !important; }' + } else { + style.textContent = ` + html, body { scrollbar-width: none !important; -ms-overflow-style: none !important; } + html::-webkit-scrollbar, body::-webkit-scrollbar { display: none !important; } + ` + } + document.head.appendChild(style) + return () => { + const el = document.getElementById('hide-describe-scrollbar') + if (el) el.remove() + } + }, [modelConfirmed]) + + // ==============================|| Model Selection Handler ||============================== // + + const handleModelSelect = (newValue) => { + if (!newValue) { + setSelectedChatModel({}) + } else { + const found = chatModelsComponents.find((c) => c.name === newValue) + if (found) { + const id = `${found.name}_0` + const cloned = cloneDeep(found) + const data = initNode(cloned, id) + setSelectedChatModel(data) + scrollToBottom(true) + } + } + } + + const handleConfirm = () => { + setModelConfirmed(true) + scrollToBottom(true) + } + + // ==============================|| Render ||============================== // + + return ( + + {/* Chat messages area — scrollable */} + + {/* Empty state — centered content with model selector */} + {chatMessages.length === 0 && ( + + + What do you want to build? + + + Describe your agent or start with a template. + + + {/* Model selector — hidden once confirmed or while default config is still resolving */} + {!modelConfirmed && defaultConfigResolved && ( + + + {!selectedChatModel?.name && ( + + + + Select a model to get started + + + )} + + + + + {/* Credential and model name fields */} + {selectedChatModel && + Object.keys(selectedChatModel).length > 0 && + showHideInputParams(selectedChatModel) + .filter( + (ip) => !ip.hidden && ip.display !== false && MODEL_PARAM_WHITELIST.includes(ip.name) + ) + .map((ip, idx) => ( + + ))} + + + For best results, use larger models such as Claude Opus 4.6, GPT-5.4, or Gemini 3.1 + + {selectedChatModel?.name && ( + + )} + + + )} + + )} + + {/* Messages */} + {chatMessages.map((msg, idx) => ( + + {msg.role === 'user' ? ( + + + {msg.content} + + + ) : msg.type === 'question' ? ( + + + + {msg.content} + + + {MOCK_QUESTIONS[msg.questionIndex]?.options.map((option, optIdx) => { + const isLastQuestion = idx === chatMessages.length - 1 + return ( + handleOptionSelect(option) : undefined} + sx={{ + display: 'flex', + alignItems: 'center', + gap: 1.5, + py: 1.5, + px: 2, + borderRadius: 2, + cursor: isLastQuestion ? 'pointer' : 'default', + '&:hover': isLastQuestion + ? { + backgroundColor: customization.isDarkMode + ? 'rgba(255,255,255,0.05)' + : 'rgba(0,0,0,0.03)' + } + : {} + }} + > + + + {optIdx + 1} + + + {option} + + ) + })} + + {idx === chatMessages.length - 1 && ( + + + {currentQuestionIndex + 1} of {MOCK_QUESTIONS.length} + + + + )} + + + ) : ( + + + {msg.content} + + + )} + + ))} + + {/* Typing indicator */} + {isTyping && ( + + + {[0, 1, 2].map((i) => ( + + ))} + + + )} + +
+ + + {/* Scroll to bottom button */} + {showScrollButton && modelConfirmed && ( + + + + + + )} + + {/* Input area */} + + + setDescribeInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleDescribeSubmit() + } + }} + sx={{ + pr: 7, + '& .MuiOutlinedInput-notchedOutline': { + border: 'none' + } + }} + /> + + + + + + + ) +} + +DescribeMode.propTypes = { + selectedChatModel: PropTypes.object, + setSelectedChatModel: PropTypes.func, + chatModelsComponents: PropTypes.array, + chatModelsOptions: PropTypes.array, + handleChatModelDataChange: PropTypes.func, + setAgentName: PropTypes.func, + setCustomAssistantInstruction: PropTypes.func, + setCreationMode: PropTypes.func, + modelConfirmed: PropTypes.bool, + setModelConfirmed: PropTypes.func, + generateTask: PropTypes.string, + defaultConfigResolved: PropTypes.bool +} + +export default DescribeMode diff --git a/packages/ui/src/views/assistants/openai/OpenAIAssistantLayout.jsx b/packages/ui/src/views/assistants/openai/OpenAIAssistantLayout.jsx index 865245c6bc2..bee0a6b8a26 100644 --- a/packages/ui/src/views/assistants/openai/OpenAIAssistantLayout.jsx +++ b/packages/ui/src/views/assistants/openai/OpenAIAssistantLayout.jsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' // material-ui -import { Box, Stack, Skeleton } from '@mui/material' +import { Box, Fade, Skeleton, Stack } from '@mui/material' // project imports import MainCard from '@/ui-component/cards/MainCard' @@ -113,70 +113,72 @@ const OpenAIAssistantLayout = () => { {error ? ( ) : ( - - navigate(-1)} - > - } - sx={{ borderRadius: 2, height: 40 }} + + + navigate(-1)} > - Load - - } - > - Add - - - {isLoading ? ( - - - - - - ) : ( - - {getAllAssistantsApi.data && - getAllAssistantsApi.data?.filter(filterAssistants).map((data, index) => ( - edit(data)} - /> - ))} - - )} - {!isLoading && (!getAllAssistantsApi.data || getAllAssistantsApi.data.length === 0) && ( - - - AssistantEmptySVG + } + sx={{ borderRadius: 2, height: 40 }} + > + Load + + } + > + Add + + + {isLoading ? ( + + + + -
No OpenAI Assistants Added Yet
-
- )} -
+ ) : ( + + {getAllAssistantsApi.data && + getAllAssistantsApi.data?.filter(filterAssistants).map((data, index) => ( + edit(data)} + /> + ))} + + )} + {!isLoading && (!getAllAssistantsApi.data || getAllAssistantsApi.data.length === 0) && ( + + + AssistantEmptySVG + +
No OpenAI Assistants Added Yet
+
+ )} + + )} { + // Strip globally-hidden nodes before any category grouping + nodes = nodes.filter((nd) => !blacklistNodeNames.includes(nd.name)) if (isAgentCanvas) { const accordianCategories = {} const result = nodes.reduce(function (r, a) { diff --git a/packages/ui/src/views/chatflows/index.jsx b/packages/ui/src/views/chatflows/index.jsx index 5764c54d958..ec9bbb428db 100644 --- a/packages/ui/src/views/chatflows/index.jsx +++ b/packages/ui/src/views/chatflows/index.jsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' // material-ui -import { Box, Skeleton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material' +import { Box, Fade, Skeleton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material' import { useTheme } from '@mui/material/styles' // project imports @@ -134,102 +134,104 @@ const Chatflows = () => { {error ? ( ) : ( - - - + + - - - - + + + + + + + } + sx={{ borderRadius: 2, height: 40 }} > - - - - } - sx={{ borderRadius: 2, height: 40 }} - > - Add New - - - - {isLoading && ( - - - - - - )} - {!isLoading && total > 0 && ( - <> - {!view || view === 'card' ? ( - - {getAllChatflowsApi.data?.data?.filter(filterFlows).map((data, index) => ( - goToCanvas(data)} data={data} images={images[data.id]} /> - ))} - - ) : ( - - )} - {/* Pagination and Page Size Controls */} - - - )} - {!isLoading && (!getAllChatflowsApi.data?.data || getAllChatflowsApi.data?.data.length === 0) && ( - - - WorkflowEmptySVG + Add New + + + + {isLoading && ( + + + + -
No Chatflows Yet
-
- )} -
+ )} + {!isLoading && total > 0 && ( + <> + {!view || view === 'card' ? ( + + {getAllChatflowsApi.data?.data?.filter(filterFlows).map((data, index) => ( + goToCanvas(data)} data={data} images={images[data.id]} /> + ))} + + ) : ( + + )} + {/* Pagination and Page Size Controls */} + + + )} + {!isLoading && (!getAllChatflowsApi.data?.data || getAllChatflowsApi.data?.data.length === 0) && ( + + + WorkflowEmptySVG + +
No Chatflows Yet
+
+ )} + + )} diff --git a/packages/ui/src/views/credentials/index.jsx b/packages/ui/src/views/credentials/index.jsx index 3ab68e34bc9..57fda740da8 100644 --- a/packages/ui/src/views/credentials/index.jsx +++ b/packages/ui/src/views/credentials/index.jsx @@ -9,6 +9,7 @@ import { tableCellClasses } from '@mui/material/TableCell' import { Button, Box, + Fade, Skeleton, Stack, Table, @@ -241,199 +242,204 @@ const Credentials = () => { {error ? ( ) : ( - - - } + + + - Add Credential - - - {!isLoading && credentials.length <= 0 ? ( - - - CredentialEmptySVG - -
No Credentials Yet
-
- ) : ( - - - - - Name - Last Updated - Created - - - - - - - {isLoading ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - <> - {credentials.filter(filterCredentials).map((credential, index) => ( - - - - - {credential.credentialName} { - e.target.onerror = null - e.target.style.padding = '5px' - e.target.src = keySVG - }} - /> - - {credential.name} - + } + > + Add Credential + + + {!isLoading && credentials.length <= 0 ? ( + + + CredentialEmptySVG + +
No Credentials Yet
+
+ ) : ( + +
+ + + Name + Last Updated + Created + + + + + + + {isLoading ? ( + <> + + + - {moment(credential.updatedDate).format('MMMM Do, YYYY HH:mm:ss')} + - {moment(credential.createdDate).format('MMMM Do, YYYY HH:mm:ss')} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - {!credential.shared && ( - <> - - share(credential)} - > - - - - - edit(credential)} - > - - - - - deleteCredential(credential)} - > - - - - - )} - {credential.shared && ( - <> - Shared Credential - - )} - ))} - - )} - -
-
- )} -
+ + ) : ( + <> + {credentials.filter(filterCredentials).map((credential, index) => ( + + + + + {credential.credentialName} { + e.target.onerror = null + e.target.style.padding = '5px' + e.target.src = keySVG + }} + /> + + {credential.name} + + + + {moment(credential.updatedDate).format('MMMM Do, YYYY HH:mm:ss')} + + + {moment(credential.createdDate).format('MMMM Do, YYYY HH:mm:ss')} + + {!credential.shared && ( + <> + + share(credential)} + > + + + + + edit(credential)} + > + + + + + deleteCredential(credential)} + > + + + + + )} + {credential.shared && ( + <> + Shared Credential + + )} + + ))} + + )} + + + + )} + + )} { {error ? ( ) : ( - - - } + + + - Add New - - - {!isLoading && datasets.length <= 0 ? ( - - - empty_datasetSVG - -
No Datasets Yet
-
- ) : ( - <> - } > - - - - Name - Description - Rows - Last Updated - - - - - - - - - - {isLoading ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - <> - {datasets.filter(filterDatasets).map((ds, index) => ( - - goToRows(ds)} component='th' scope='row'> - {ds.name} - - goToRows(ds)} - style={{ wordWrap: 'break-word', flexWrap: 'wrap', width: '40%' }} - > - {truncateString(ds?.description, 200)} - - goToRows(ds)}>{ds?.rowCount} - goToRows(ds)}> - {moment(ds.updatedDate).format('MMMM Do YYYY, hh:mm A')} - + Add New + + + {!isLoading && datasets.length <= 0 ? ( + + + empty_datasetSVG + +
No Datasets Yet
+
+ ) : ( + <> + +
+ + + Name + Description + Rows + Last Updated + + + + + + + + + + {isLoading ? ( + <> + + + + + + + + + + + + + - - edit(ds)}> - - - + - - deleteDataset(ds)} - > - - - + + + + + + + + + + + + + + + + + + + + + - ))} - - )} - -
-
- {/* Pagination and Page Size Controls */} - - - )} -
+ + ) : ( + <> + {datasets.filter(filterDatasets).map((ds, index) => ( + + goToRows(ds)} component='th' scope='row'> + {ds.name} + + goToRows(ds)} + style={{ wordWrap: 'break-word', flexWrap: 'wrap', width: '40%' }} + > + {truncateString(ds?.description, 200)} + + goToRows(ds)}>{ds?.rowCount} + goToRows(ds)}> + {moment(ds.updatedDate).format('MMMM Do YYYY, hh:mm A')} + + + + edit(ds)}> + + + + + + + deleteDataset(ds)} + > + + + + + + ))} + + )} + + + + {/* Pagination and Page Size Controls */} + + + )} + + )} { {error ? ( ) : ( - - - {hasDocStores && ( - - - - - - - - - )} - } - id='btn_createVariable' + + + - Add New - - - {!hasDocStores ? ( - - - doc_store_empty - -
No Document Stores Created Yet
-
- ) : ( - - {!view || view === 'card' ? ( - - {docStores?.filter(filterDocStores).map((data) => ( - - goToDocumentStore(data.id)} - /> - {canManageDocumentStore && ( - handleActionMenuOpen(event, data)} - > - - - )} - - ))} - - ) : ( - goToDocumentStore(row.id)} - showActions={canManageDocumentStore} - onActionMenuClick={handleActionMenuOpen} - actionButtonSx={getDocStoreActionButtonSx(theme)} - /> + {hasDocStores && ( + + + + + + + + )} - {/* Pagination and Page Size Controls */} - - - )} -
+ } + id='btn_createVariable' + > + Add New + + + {!hasDocStores ? ( + + + doc_store_empty + +
No Document Stores Created Yet
+
+ ) : ( + + {!view || view === 'card' ? ( + + {docStores?.filter(filterDocStores).map((data) => ( + + goToDocumentStore(data.id)} + /> + {canManageDocumentStore && ( + handleActionMenuOpen(event, data)} + > + + + )} + + ))} + + ) : ( + goToDocumentStore(row.id)} + showActions={canManageDocumentStore} + onActionMenuClick={handleActionMenuOpen} + actionButtonSx={getDocStoreActionButtonSx(theme)} + /> + )} + {/* Pagination and Page Size Controls */} + + + )} + + )} {showDialog && ( { {error ? ( ) : ( - - - + + + - {autoRefresh ? : } - - - - - } - > - New Evaluation - - - {selected.length > 0 && ( - } - > - Delete {selected.length} {selected.length === 1 ? 'evaluation' : 'evaluations'} - - )} - {!isTableLoading && rows.length <= 0 ? ( - - - empty_evalSVG - -
No Evaluations Yet
-
- ) : ( - <> - - - - - - item?.latestEval) || []).length} - onChange={onSelectAllClick} - inputProps={{ - 'aria-label': 'select all' - }} - /> - - - Name - Latest Version - Average Metrics - Last Evaluated - Flow(s) - Dataset - - - - - {isTableLoading ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - <> - {rows - .filter((item) => item?.latestEval) - .map((item, index) => ( - row.name === item.name)} - item={item} - key={index} - theme={theme} - selected={selected} - customization={customization} - onRefresh={onRefresh} - handleSelect={handleSelect} - /> - ))} - - )} - -
-
- {/* Pagination and Page Size Controls */} - - - )} -
+ {autoRefresh ? : } +
+ + + + } + > + New Evaluation + +
+ {selected.length > 0 && ( + } + > + Delete {selected.length} {selected.length === 1 ? 'evaluation' : 'evaluations'} + + )} + {!isTableLoading && rows.length <= 0 ? ( + + + empty_evalSVG + +
No Evaluations Yet
+
+ ) : ( + <> + + + + + + item?.latestEval) || []).length + } + onChange={onSelectAllClick} + inputProps={{ + 'aria-label': 'select all' + }} + /> + + + Name + Latest Version + Average Metrics + Last Evaluated + Flow(s) + Dataset + + + + + {isTableLoading ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {rows + .filter((item) => item?.latestEval) + .map((item, index) => ( + row.name === item.name)} + item={item} + key={index} + theme={theme} + selected={selected} + customization={customization} + onRefresh={onRefresh} + handleSelect={handleSelect} + /> + ))} + + )} + +
+
+ {/* Pagination and Page Size Controls */} + + + )} +
+ )} {showNewEvaluationDialog && ( diff --git a/packages/ui/src/views/evaluators/index.jsx b/packages/ui/src/views/evaluators/index.jsx index f1738f92303..c8901e39915 100644 --- a/packages/ui/src/views/evaluators/index.jsx +++ b/packages/ui/src/views/evaluators/index.jsx @@ -2,7 +2,21 @@ import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' // material-ui -import { Chip, Skeleton, Box, Stack, TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, Button } from '@mui/material' +import { + Box, + Button, + Chip, + Fade, + Paper, + Skeleton, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow +} from '@mui/material' import { useTheme } from '@mui/material/styles' // project imports @@ -178,385 +192,387 @@ const Evaluators = () => { {error ? ( ) : ( - - - } + + + - New Evaluator - - - {!isLoading && evaluators.length <= 0 ? ( - - - empty_evaluatorSVG - -
No Evaluators Yet
-
- ) : ( - <> - } > - - - - Type - Name - Details - Last Updated - - - - - {isLoading ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - <> - {evaluators.filter(filterDatasets).map((ds, index) => ( - <> - - edit(ds)}> - {ds?.type === 'numeric' && ( - - } - label='Numeric' - variant='outlined' - /> - - )} - {ds?.type === 'text' && ( - - } - label='Text Based' - variant='outlined' - /> - - )} - {ds?.type === 'json' && ( - - } - label='JSON Based' - variant='outlined' - /> - - )} - {ds?.type === 'llm' && ( - - } - label='LLM Based' - variant='outlined' - /> - - )} - - edit(ds)} component='th' scope='row'> - {ds.name} - - edit(ds)}> - {ds?.type === 'numeric' && ( - + + {!isLoading && evaluators.length <= 0 ? ( + + + empty_evaluatorSVG + +
No Evaluators Yet
+
+ ) : ( + <> + +
+ + + Type + Name + Details + Last Updated + + + + + {isLoading ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {evaluators.filter(filterDatasets).map((ds, index) => ( + <> + + edit(ds)}> + {ds?.type === 'numeric' && ( + + } + label='Numeric' + variant='outlined' + /> + + )} + {ds?.type === 'text' && ( + + } + label='Text Based' + variant='outlined' + /> + + )} + {ds?.type === 'json' && ( + + } + label='JSON Based' + variant='outlined' + /> + + )} + {ds?.type === 'llm' && ( + + } + label='LLM Based' + variant='outlined' + /> + + )} + + edit(ds)} component='th' scope='row'> + {ds.name} + + edit(ds)}> + {ds?.type === 'numeric' && ( + + + Measure:{' '} + { + [ + ...evaluatorsOptions, + ...numericOperators + ].find((item) => item.name === ds?.measure) + ?.label + } + + } + /> + + Operator:{' '} + { + [ + ...evaluatorsOptions, + ...numericOperators + ].find((item) => item.name === ds?.operator) + ?.label + } + + } + /> + + Value: {ds?.value} + + } + /> + + )} + {ds?.type === 'text' && ( + + + Operator:{' '} + { + [ + ...evaluatorsOptions, + ...numericOperators + ].find((item) => item.name === ds?.operator) + ?.label + } + + } + /> + + Value: {ds?.value} + + } + /> + + )} + {ds?.type === 'json' && ( + + + Operator:{' '} + { + [...evaluatorsOptions].find( + (item) => item.name === ds?.operator + )?.label + } + + } + /> + + )} + {ds?.type === 'llm' && ( + + + Prompt: {truncateString(ds?.prompt, 100)} + + } + /> + + Output Schema Elements:{' '} + {ds?.outputSchema.length > 0 + ? ds?.outputSchema + .map((item) => item.property) + .join(', ') + : 'None'} + + } + /> + + )} + + edit(ds)}> + {moment(ds.updatedDate).format('MMMM Do YYYY, hh:mm A')} + + + deleteEvaluator(ds)} > - - Measure:{' '} - { - [ - ...evaluatorsOptions, - ...numericOperators - ].find((item) => item.name === ds?.measure) - ?.label - } - - } - /> - - Operator:{' '} - { - [ - ...evaluatorsOptions, - ...numericOperators - ].find((item) => item.name === ds?.operator) - ?.label - } - - } - /> - - Value: {ds?.value} - - } - /> - - )} - {ds?.type === 'text' && ( - - - Operator:{' '} - { - [ - ...evaluatorsOptions, - ...numericOperators - ].find((item) => item.name === ds?.operator) - ?.label - } - - } - /> - - Value: {ds?.value} - - } - /> - - )} - {ds?.type === 'json' && ( - - - Operator:{' '} - { - [...evaluatorsOptions].find( - (item) => item.name === ds?.operator - )?.label - } - - } - /> - - )} - {ds?.type === 'llm' && ( - - - Prompt: {truncateString(ds?.prompt, 100)} - - } - /> - - Output Schema Elements:{' '} - {ds?.outputSchema.length > 0 - ? ds?.outputSchema - .map((item) => item.property) - .join(', ') - : 'None'} - - } - /> - - )} - - edit(ds)}> - {moment(ds.updatedDate).format('MMMM Do YYYY, hh:mm A')} - - - deleteEvaluator(ds)} - > - - - - - - ))} - - )} - -
-
- {/* Pagination and Page Size Controls */} - - - )} -
+ + + + + + ))} + + )} + + + + {/* Pagination and Page Size Controls */} + + + )} + + )} {showEvaluatorDialog && ( diff --git a/packages/ui/src/views/files/index.jsx b/packages/ui/src/views/files/index.jsx index e5b952c838a..0e87c0d6f40 100644 --- a/packages/ui/src/views/files/index.jsx +++ b/packages/ui/src/views/files/index.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' // material-ui -import { Box, Button, Stack } from '@mui/material' +import { Box, Button, Fade, Stack } from '@mui/material' // project imports import MainCard from '@/ui-component/cards/MainCard' @@ -126,22 +126,24 @@ const Files = () => { {error ? ( ) : ( - - - - {!isLoading && (!getAllFilesApi.data || getAllFilesApi.data.length === 0) && ( - - - WorkflowEmptySVG - -
No Files Yet
-
- )} -
+ + + + + {!isLoading && (!getAllFilesApi.data || getAllFilesApi.data.length === 0) && ( + + + WorkflowEmptySVG + +
No Files Yet
+
+ )} +
+
)} diff --git a/packages/ui/src/views/marketplaces/index.jsx b/packages/ui/src/views/marketplaces/index.jsx index 1dece8da713..884ad533e95 100644 --- a/packages/ui/src/views/marketplaces/index.jsx +++ b/packages/ui/src/views/marketplaces/index.jsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useEffect, useState } from 'react' -import { useNavigate } from 'react-router-dom' +import { useLocation, useNavigate } from 'react-router-dom' import { useDispatch } from 'react-redux' // material-ui @@ -8,6 +8,7 @@ import { Box, Stack, Badge, + Fade, ToggleButton, InputLabel, FormControl, @@ -75,6 +76,7 @@ const MenuProps = { const Marketplace = () => { const navigate = useNavigate() + const location = useLocation() const dispatch = useDispatch() useNotifier() @@ -96,7 +98,7 @@ const Marketplace = () => { const [view, setView] = React.useState(localStorage.getItem('mpDisplayStyle') || 'card') const [search, setSearch] = useState('') const [badgeFilter, setBadgeFilter] = useState([]) - const [typeFilter, setTypeFilter] = useState([]) + const [typeFilter, setTypeFilter] = useState(location.state?.typeFilter || []) const [frameworkFilter, setFrameworkFilter] = useState([]) const getAllCustomTemplatesApi = useApi(marketplacesApi.getAllCustomTemplates) @@ -473,271 +475,289 @@ const Marketplace = () => { {error ? ( ) : ( - - - + + + + + Tag + + + + + + Type + + + + + + Framework + + + + + } + onSearchChange={onSearchChange} + search={true} + searchPlaceholder='Search Name/Description/Node' + title='Marketplace' + description='Explore and use pre-built templates' + > + + - - Tag - - - - + + - - Type - - - - + + + + {hasPermission('templates:marketplace') && hasPermission('templates:custom') && ( + + + + + + setSelectedUsecases(newValue)} + disableCloseOnSelect + getOptionLabel={(option) => option} + isOptionEqualToValue={(option, value) => option === value} + renderOption={(props, option, { selected }) => { + const isDisabled = eligibleUsecases.length > 0 && !eligibleUsecases.includes(option) + + return ( +
  • + + +
  • + ) + }} + renderInput={(params) => } sx={{ - borderRadius: 2, - display: 'flex', - flexDirection: 'column', - justifyContent: 'end', - minWidth: 120 + width: 300 }} - > - - Framework - - - - - } - onSearchChange={onSearchChange} - search={true} - searchPlaceholder='Search Name/Description/Node' - title='Marketplace' - description='Explore and use pre-built templates' - > - - - - - - - - - - {hasPermission('templates:marketplace') && hasPermission('templates:custom') && ( - - - - - - setSelectedUsecases(newValue)} - disableCloseOnSelect - getOptionLabel={(option) => option} - isOptionEqualToValue={(option, value) => option === value} - renderOption={(props, option, { selected }) => { - const isDisabled = eligibleUsecases.length > 0 && !eligibleUsecases.includes(option) - - return ( -
  • - - -
  • - ) - }} - renderInput={(params) => } - sx={{ - width: 300 - }} - limitTags={2} - renderTags={(value, getTagProps) => { - const totalTags = value.length - const limitTags = 2 - - return ( - <> - {value.slice(0, limitTags).map((option, index) => ( - - ))} + limitTags={2} + renderTags={(value, getTagProps) => { + const totalTags = value.length + const limitTags = 2 + + return ( + <> + {value.slice(0, limitTags).map((option, index) => ( + + ))} - {totalTags > limitTags && ( - - {value.slice(limitTags).map((item, i) => ( -
  • {item}
  • - ))} - - } - placement='top' - > - +{totalTags - limitTags} -
    - )} - - ) - }} - slotProps={{ - paper: { - sx: { - boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)' + {totalTags > limitTags && ( + + {value.slice(limitTags).map((item, i) => ( +
  • {item}
  • + ))} + + } + placement='top' + > + +{totalTags - limitTags} +
    + )} + + ) + }} + slotProps={{ + paper: { + sx: { + boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)' + } } - } - }} - /> -
    - )} - - - {!view || view === 'card' ? ( - <> - {isLoading ? ( - - - - - - ) : ( - - {getAllTemplatesMarketplacesApi.data - ?.filter(filterByBadge) - .filter(filterByType) - .filter(filterFlows) - .filter(filterByFramework) - .filter(filterByUsecases) - .map((data, index) => ( - - {data.badge && ( - - {(data.type === 'Chatflow' || + }} + /> +
    + )} + + + {!view || view === 'card' ? ( + <> + {isLoading ? ( + + + + + + ) : ( + + {getAllTemplatesMarketplacesApi.data + ?.filter(filterByBadge) + .filter(filterByType) + .filter(filterFlows) + .filter(filterByFramework) + .filter(filterByUsecases) + .map((data, index) => ( + + {data.badge && ( + + {(data.type === 'Chatflow' || + data.type === 'Agentflow' || + data.type === 'AgentflowV2' || + data.type === 'Agent') && ( + goToCanvas(data)} + data={data} + images={images[data.id]} + icons={icons[data.id]} + /> + )} + {data.type === 'Tool' && ( + goToTool(data)} /> + )} + + )} + {!data.badge && + (data.type === 'Chatflow' || data.type === 'Agentflow' || data.type === 'AgentflowV2' || data.type === 'Agent') && ( @@ -748,133 +768,133 @@ const Marketplace = () => { icons={icons[data.id]} /> )} - {data.type === 'Tool' && ( - goToTool(data)} /> - )} - - )} - {!data.badge && - (data.type === 'Chatflow' || - data.type === 'Agentflow' || - data.type === 'AgentflowV2' || - data.type === 'Agent') && ( - goToCanvas(data)} - data={data} - images={images[data.id]} - icons={icons[data.id]} - /> + {!data.badge && data.type === 'Tool' && ( + goToTool(data)} /> )} - {!data.badge && data.type === 'Tool' && ( - goToTool(data)} /> - )} - - ))} - - )} - - ) : ( - - )} + + ))} + + )} + + ) : ( + + )} - {!isLoading && - (!getAllTemplatesMarketplacesApi.data || getAllTemplatesMarketplacesApi.data.length === 0) && ( - - - WorkflowEmptySVG + + WorkflowEmptySVG + +
    No Marketplace Yet
    +
    + )} +
    +
    + + + {templateUsecases.length > 0 && ( + + {templateUsecases.map((usecase, index) => ( + { + setSelectedTemplateUsecases( + event.target.checked + ? [...selectedTemplateUsecases, usecase] + : selectedTemplateUsecases.filter((item) => item !== usecase) + ) + }} + /> + } + label={usecase} /> - -
    No Marketplace Yet
    + ))}
    )} -
    -
    - - - {templateUsecases.length > 0 && ( - - {templateUsecases.map((usecase, index) => ( - { - setSelectedTemplateUsecases( - event.target.checked - ? [...selectedTemplateUsecases, usecase] - : selectedTemplateUsecases.filter((item) => item !== usecase) - ) - }} - /> - } - label={usecase} - /> - ))} - - )} - {selectedTemplateUsecases.length > 0 && ( - - )} - {!view || view === 'card' ? ( - <> - {isLoading ? ( - - - - - - ) : ( - - {getAllCustomTemplatesApi.data - ?.filter(filterByBadge) - .filter(filterByType) - .filter(filterFlows) - .filter(filterByFramework) - .filter(filterByUsecases) - .map((data, index) => ( - - {data.badge && ( - - {(data.type === 'Chatflow' || + {selectedTemplateUsecases.length > 0 && ( + + )} + {!view || view === 'card' ? ( + <> + {isLoading ? ( + + + + + + ) : ( + + {getAllCustomTemplatesApi.data + ?.filter(filterByBadge) + .filter(filterByType) + .filter(filterFlows) + .filter(filterByFramework) + .filter(filterByUsecases) + .map((data, index) => ( + + {data.badge && ( + + {(data.type === 'Chatflow' || + data.type === 'Agentflow' || + data.type === 'AgentflowV2' || + data.type === 'Agent') && ( + goToCanvas(data)} + data={data} + images={templateImages[data.id]} + icons={templateIcons[data.id]} + /> + )} + {data.type === 'Tool' && ( + goToTool(data)} /> + )} + + )} + {!data.badge && + (data.type === 'Chatflow' || data.type === 'Agentflow' || data.type === 'AgentflowV2' || data.type === 'Agent') && ( @@ -885,62 +905,46 @@ const Marketplace = () => { icons={templateIcons[data.id]} /> )} - {data.type === 'Tool' && ( - goToTool(data)} /> - )} - - )} - {!data.badge && - (data.type === 'Chatflow' || - data.type === 'Agentflow' || - data.type === 'AgentflowV2' || - data.type === 'Agent') && ( - goToCanvas(data)} - data={data} - images={templateImages[data.id]} - icons={templateIcons[data.id]} - /> + {!data.badge && data.type === 'Tool' && ( + goToTool(data)} /> )} - {!data.badge && data.type === 'Tool' && ( - goToTool(data)} /> - )} - - ))} + + ))} + + )} + + ) : ( + + )} + {!isLoading && (!getAllCustomTemplatesApi.data || getAllCustomTemplatesApi.data.length === 0) && ( + + + WorkflowEmptySVG - )} - - ) : ( - - )} - {!isLoading && (!getAllCustomTemplatesApi.data || getAllCustomTemplatesApi.data.length === 0) && ( - - - WorkflowEmptySVG - -
    No Saved Custom Templates
    -
    - )} -
    -
    -
    +
    No Saved Custom Templates
    + + )} + + + + )} { {error ? ( ) : ( - - - } - id='btn_createUser' - > - Add Role - - - {!isLoading && roles.length === 0 ? ( - - - roles_emptySVG - -
    No Roles Yet
    -
    - ) : ( - <> - - - - - - - Name - Description - Permissions - Assigned Users - - - - - {isLoading ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - <> - {roles.filter(filterUsers).map((role, index) => ( - - ))} - - )} - -
    -
    + + + + } + id='btn_createUser' + > + Add Role + + + {!isLoading && roles.length === 0 ? ( + + + roles_emptySVG +
    No Roles Yet
    - - )} -
    + ) : ( + <> + + + + + + + Name + Description + Permissions + Assigned Users + + + + + {isLoading ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {roles.filter(filterUsers).map((role, index) => ( + + ))} + + )} + +
    +
    +
    +
    + + )} +
    + )} {showCreateEditDialog && ( diff --git a/packages/ui/src/views/serverlogs/index.jsx b/packages/ui/src/views/serverlogs/index.jsx index a0b935a12ce..31a6d5c23ff 100644 --- a/packages/ui/src/views/serverlogs/index.jsx +++ b/packages/ui/src/views/serverlogs/index.jsx @@ -11,7 +11,7 @@ import { markdown } from '@codemirror/lang-markdown' import { sublime } from '@uiw/codemirror-theme-sublime' // material-ui -import { Box, Skeleton, Stack, Select, MenuItem, ListItemButton } from '@mui/material' +import { Box, Fade, ListItemButton, MenuItem, Select, Skeleton, Stack } from '@mui/material' import { useTheme } from '@mui/material/styles' // ui @@ -206,107 +206,109 @@ const Logs = () => { {error ? ( ) : ( - - - {isLoading ? ( - - - - - + + + + {isLoading ? ( + + + + + + + + + + + + + + - - - - - - - - - - ) : ( - <> - - - {selectedTimeSearch === 'Custom' && ( - <> - - From - onStartDateSelected(date)} - selectsStart - startDate={startDate} - endDate={endDate} - maxDate={endDate} - showTimeSelect - timeFormat='HH:mm' - timeIntervals={60} - dateFormat='yyyy MMMM d, h aa' - customInput={} - /> - - - To - onEndDateSelected(date)} - selectsEnd - showTimeSelect - timeFormat='HH:mm' - timeIntervals={60} - startDate={startDate} - endDate={endDate} - minDate={startDate} - maxDate={new Date()} - dateFormat='yyyy MMMM d, h aa' - customInput={} + ) : ( + <> + + + {selectedTimeSearch === 'Custom' && ( + <> + + From + onStartDateSelected(date)} + selectsStart + startDate={startDate} + endDate={endDate} + maxDate={endDate} + showTimeSelect + timeFormat='HH:mm' + timeIntervals={60} + dateFormat='yyyy MMMM d, h aa' + customInput={} + /> + + + To + onEndDateSelected(date)} + selectsEnd + showTimeSelect + timeFormat='HH:mm' + timeIntervals={60} + startDate={startDate} + endDate={endDate} + minDate={startDate} + maxDate={new Date()} + dateFormat='yyyy MMMM d, h aa' + customInput={} + /> + + + )} + + {logData ? ( + + ) : ( + + + LogsEmptySVG - - + +
    No Logs Yet
    +
    )} -
    - {logData ? ( - - ) : ( - - - LogsEmptySVG - -
    No Logs Yet
    -
    - )} - - )} - + + )} + + )} ) diff --git a/packages/ui/src/views/tools/index.jsx b/packages/ui/src/views/tools/index.jsx index 10dff435140..099aa2f3349 100644 --- a/packages/ui/src/views/tools/index.jsx +++ b/packages/ui/src/views/tools/index.jsx @@ -1,7 +1,7 @@ import { useEffect, useState, useRef } from 'react' // material-ui -import { Box, Stack, ButtonGroup, Skeleton, ToggleButtonGroup, ToggleButton } from '@mui/material' +import { Box, ButtonGroup, Fade, Skeleton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material' import { useTheme } from '@mui/material/styles' // project imports @@ -157,117 +157,119 @@ const Tools = () => { {error ? ( ) : ( - - - + + - - - - - - - - - inputRef.current.click()} - startIcon={} - sx={{ borderRadius: 2, height: 40 }} - > - Load - - handleFileUpload(e)} - /> - - - } - sx={{ borderRadius: 2, height: 40 }} - > - Create - - - - {isLoading && ( - - - - - - )} - {!isLoading && total > 0 && ( - <> - {!view || view === 'card' ? ( - - {getAllToolsApi.data?.data?.filter(filterTools).map((data, index) => ( - edit(data)} /> - ))} - - ) : ( - - )} - {/* Pagination and Page Size Controls */} - - - )} - {!isLoading && total === 0 && ( - - - ToolEmptySVG + + + + + + + + inputRef.current.click()} + startIcon={} + sx={{ borderRadius: 2, height: 40 }} + > + Load + + handleFileUpload(e)} /> -
    No Tools Created Yet
    -
    - )} -
    + + } + sx={{ borderRadius: 2, height: 40 }} + > + Create + + + + {isLoading && ( + + + + + + )} + {!isLoading && total > 0 && ( + <> + {!view || view === 'card' ? ( + + {getAllToolsApi.data?.data?.filter(filterTools).map((data, index) => ( + edit(data)} /> + ))} + + ) : ( + + )} + {/* Pagination and Page Size Controls */} + + + )} + {!isLoading && total === 0 && ( + + + ToolEmptySVG + +
    No Tools Created Yet
    +
    + )} + + )} { {error ? ( ) : ( - - - } - id='btn_createUser' + + + - Invite User - - - {!isLoading && users.length === 0 ? ( - - - users_emptySVG - -
    No Users Yet
    -
    - ) : ( - <> - - - - - - -   - Email/Name - Assigned Roles - Status - Last Login - - - - - {isLoading ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - <> - {users.filter(filterUsers).map((item, index) => ( - - ))} - - )} - -
    -
    + } + id='btn_createUser' + > + Invite User + + + {!isLoading && users.length === 0 ? ( + + + users_emptySVG +
    No Users Yet
    - - )} -
    + ) : ( + <> + + + + + + +   + Email/Name + Assigned Roles + Status + Last Login + + + + + {isLoading ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {users.filter(filterUsers).map((item, index) => ( + + ))} + + )} + +
    +
    +
    +
    + + )} +
    + )} {showInviteDialog && ( diff --git a/packages/ui/src/views/variables/index.jsx b/packages/ui/src/views/variables/index.jsx index 01e1d3225e4..4057e3466eb 100644 --- a/packages/ui/src/views/variables/index.jsx +++ b/packages/ui/src/views/variables/index.jsx @@ -9,6 +9,7 @@ import { tableCellClasses } from '@mui/material/TableCell' import { Button, Box, + Fade, Skeleton, Stack, Table, @@ -218,205 +219,215 @@ const Variables = () => { {error ? ( ) : ( - - - - } - id='btn_createVariable' + + + - Add Variable - - - {!isLoading && variables.length === 0 ? ( - - - VariablesEmptySVG - -
    No Variables Yet
    -
    - ) : ( - <> - setShowHowToDialog(true)} + > + How To Use + + } + id='btn_createVariable' > - - - - Name - Value - Type - Last Updated - Created - - - - - - - - - - {isLoading ? ( - <> - - - - - - - - - - - - - - - - - + Add Variable + + + {!isLoading && variables.length === 0 ? ( + + + VariablesEmptySVG + +
    No Variables Yet
    +
    + ) : ( + <> + +
    + + + Name + Value + Type + Last Updated + Created + + + + + + + + + + {isLoading ? ( + <> + - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - <> - {variables.filter(filterVariables).map((variable, index) => ( - - -
    -
    - -
    - {variable.name} -
    + + - {variable.value} + + + + + + + + + + +
    + - + - {moment(variable.updatedDate).format('MMMM Do, YYYY HH:mm:ss')} + - {moment(variable.createdDate).format('MMMM Do, YYYY HH:mm:ss')} + + + + + + + - edit(variable)}> - - + - deleteVariable(variable)} - > - - + - ))} - - )} -
    -
    -
    - {/* Pagination and Page Size Controls */} - - - )} -
    + + ) : ( + <> + {variables.filter(filterVariables).map((variable, index) => ( + + +
    +
    + +
    + {variable.name} +
    +
    + {variable.value} + + + + + {moment(variable.updatedDate).format('MMMM Do, YYYY HH:mm:ss')} + + + {moment(variable.createdDate).format('MMMM Do, YYYY HH:mm:ss')} + + + + edit(variable)} + > + + + + + + + deleteVariable(variable)} + > + + + + +
    + ))} + + )} + + + + {/* Pagination and Page Size Controls */} + + + )} + + )} { {error ? ( ) : ( - - window.history.back()} - search={workspaceUsers.length > 0} - onSearchChange={onSearchChange} - searchPlaceholder={'Search Users'} - title={(workspace?.name || '') + ': Workspace Users'} - description={'Manage workspace users and permissions.'} - > - {workspaceUsers.length > 0 && ( - <> - } - > - Remove Users - + + + window.history.back()} + search={workspaceUsers.length > 0} + onSearchChange={onSearchChange} + searchPlaceholder={'Search Users'} + title={(workspace?.name || '') + ': Workspace Users'} + description={'Manage workspace users and permissions.'} + > + {workspaceUsers.length > 0 && ( + <> + } + > + Remove Users + + } + > + Add User + + + )} + + {!isLoading && workspaceUsers?.length <= 0 ? ( + + + empty_datasetSVG + +
    No Assigned Users Yet
    } + onClick={addUser} > Add User - - )} -
    - {!isLoading && workspaceUsers?.length <= 0 ? ( - - - empty_datasetSVG - -
    No Assigned Users Yet
    - } - onClick={addUser} - > - Add User - -
    - ) : ( - <> - - - - - - - - Email/Name - Role - Status - Last Login - - - - - {isLoading ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - <> - {(workspaceUsers || []).filter(filterUsers).map((item, index) => ( - - - {item.isOrgOwner ? null : ( - handleUserSelect(event, item)} - inputProps={{ - 'aria-labelledby': item.userId - }} - /> - )} + + ) : ( + <> + +
    + + + + + + Email/Name + Role + Status + Last Login + + + + + {isLoading ? ( + <> + + + - {item.user.name && ( - <> - {item.user.name} -
    - - )} - {item.user.email} +
    - {item.isOrgOwner ? ( - - ) : ( - item.role.name - )} + - {item.isOrgOwner ? ( - <> - ) : ( - <> - {'ACTIVE' === item.status.toUpperCase() && ( - - )} - {'INVITED' === item.status.toUpperCase() && ( - - )} - {'INACTIVE' === item.status.toUpperCase() && ( - - )} - - )} + - {!item.lastLogin - ? 'Never' - : moment(item.lastLogin).format('DD/MM/YYYY HH:mm')} + - {!item.isOrgOwner && item.status.toUpperCase() === 'INVITED' && ( - onEditClick(item)} - > - - - )} - {!item.isOrgOwner && item.status.toUpperCase() === 'ACTIVE' && ( - onEditClick(item)} - > - - - )} +
    - ))} - - )} -
    -
    -
    - - )} -
    + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {(workspaceUsers || []).filter(filterUsers).map((item, index) => ( + + + {item.isOrgOwner ? null : ( + handleUserSelect(event, item)} + inputProps={{ + 'aria-labelledby': item.userId + }} + /> + )} + + + {item.user.name && ( + <> + {item.user.name} +
    + + )} + {item.user.email} +
    + + {item.isOrgOwner ? ( + + ) : ( + item.role.name + )} + + + {item.isOrgOwner ? ( + <> + ) : ( + <> + {'ACTIVE' === item.status.toUpperCase() && ( + + )} + {'INVITED' === item.status.toUpperCase() && ( + + )} + {'INACTIVE' === item.status.toUpperCase() && ( + + )} + + )} + + + {!item.lastLogin + ? 'Never' + : moment(item.lastLogin).format('DD/MM/YYYY HH:mm')} + + + {!item.isOrgOwner && item.status.toUpperCase() === 'INVITED' && ( + onEditClick(item)} + > + + + )} + {!item.isOrgOwner && item.status.toUpperCase() === 'ACTIVE' && ( + onEditClick(item)} + > + + + )} + +
    + ))} + + )} + + + + + )} + + )} {showAddUserDialog && ( diff --git a/packages/ui/src/views/workspace/index.jsx b/packages/ui/src/views/workspace/index.jsx index 3c12764dda3..626a5ef2d78 100644 --- a/packages/ui/src/views/workspace/index.jsx +++ b/packages/ui/src/views/workspace/index.jsx @@ -10,6 +10,7 @@ import { Button, Chip, Drawer, + Fade, IconButton, Paper, Skeleton, @@ -401,115 +402,117 @@ const Workspaces = () => { {error ? ( ) : ( - - - } - > - Add New - - - {!isLoading && workspaces.length <= 0 ? ( - - - workspaces_emptySVG - -
    No Workspaces Yet
    -
    - ) : ( - + + - - - - Name - Description - Users - Last Updated - - - - - {isLoading ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - <> - {workspaces.filter(filterWorkspaces).map((ds, index) => ( - - ))} - - )} - -
    -
    - )} -
    + } + > + Add New + + + {!isLoading && workspaces.length <= 0 ? ( + + + workspaces_emptySVG + +
    No Workspaces Yet
    +
    + ) : ( + + + + + Name + Description + Users + Last Updated + + + + + {isLoading ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {workspaces.filter(filterWorkspaces).map((ds, index) => ( + + ))} + + )} + +
    +
    + )} + + )} {showWorkspaceDialog && ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1397be3198..4a8099355ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1267,6 +1267,9 @@ importers: axios: specifier: 1.12.0 version: 1.12.0(debug@4.3.4) + boring-avatars: + specifier: ^2.0.4 + version: 2.0.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) clsx: specifier: ^1.1.1 version: 1.2.1 @@ -10274,6 +10277,12 @@ packages: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + boring-avatars@2.0.4: + resolution: {integrity: sha512-xhZO/w/6aFmRfkaWohcl2NfyIy87gK5SBbys8kctZeTGF1Apjpv/10pfUuv+YEfVPkESU/h2Y6tt/Dwp+bIZPw==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} @@ -32986,6 +32995,11 @@ snapshots: boolean@3.2.0: {} + boring-avatars@2.0.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + bottleneck@2.19.5: {} bowser@2.11.0: {}