mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
feat(web): add Agent V2 frontend — app creation, node editor, sandbox settings
P0 — Agent App can be created and routed: - Add AppModeEnum.AGENT to types/app.ts - Add Agent card to create-app-modal (primary row, with RiRobot2Fill icon) - Route Agent apps to /workflow editor (same as workflow/advanced-chat) - Update layout-main.tsx mode guards P1 — Agent V2 workflow node: - Add BlockEnum.AgentV2 = 'agent-v2' to workflow types - Create agent-v2/node.tsx: displays model, strategy, tool count - Create agent-v2/panel.tsx: model selector, strategy picker, tool list, max iterations, memory config, vision toggle - Register in NodeComponentMap and PanelComponentMap P2 — Sandbox Provider settings: - Create sandbox-provider-page: list/configure/activate/delete providers (Docker, E2B, SSH, AWS CodeInterpreter) - Create service/sandbox.ts: API client for sandbox provider endpoints - Add "Sandbox Providers" to settings menu i18n: Add en-US and zh-Hans translations for agent V2 description. Made-with: Cursor
This commit is contained in:
parent
59b9221501
commit
f4e04fc872
@ -140,10 +140,10 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||||||
router.replace(`/app/${appId}/overview`)
|
router.replace(`/app/${appId}/overview`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ((res.mode === AppModeEnum.WORKFLOW || res.mode === AppModeEnum.ADVANCED_CHAT) && (pathname).endsWith('configuration')) {
|
if ((res.mode === AppModeEnum.WORKFLOW || res.mode === AppModeEnum.ADVANCED_CHAT || res.mode === AppModeEnum.AGENT) && (pathname).endsWith('configuration')) {
|
||||||
router.replace(`/app/${appId}/workflow`)
|
router.replace(`/app/${appId}/workflow`)
|
||||||
}
|
}
|
||||||
else if ((res.mode !== AppModeEnum.WORKFLOW && res.mode !== AppModeEnum.ADVANCED_CHAT) && (pathname).endsWith('workflow')) {
|
else if ((res.mode !== AppModeEnum.WORKFLOW && res.mode !== AppModeEnum.ADVANCED_CHAT && res.mode !== AppModeEnum.AGENT) && (pathname).endsWith('workflow')) {
|
||||||
router.replace(`/app/${appId}/configuration`)
|
router.replace(`/app/${appId}/configuration`)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { AppIconSelection } from '../../base/app-icon-picker'
|
import type { AppIconSelection } from '../../base/app-icon-picker'
|
||||||
import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon/react'
|
import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill, RiRobot2Fill } from '@remixicon/react'
|
||||||
|
|
||||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
@ -145,6 +145,19 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||||||
setAppMode(AppModeEnum.ADVANCED_CHAT)
|
setAppMode(AppModeEnum.ADVANCED_CHAT)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<AppTypeCard
|
||||||
|
active={appMode === AppModeEnum.AGENT}
|
||||||
|
title={t('types.agent', { ns: 'app' })}
|
||||||
|
description={t('newApp.agentV2ShortDescription', { ns: 'app' })}
|
||||||
|
icon={(
|
||||||
|
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-violet-solid">
|
||||||
|
<RiRobot2Fill className="h-4 w-4 text-components-avatar-shape-fill-stop-100" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setAppMode(AppModeEnum.AGENT)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export const ACCOUNT_SETTING_TAB = {
|
|||||||
API_BASED_EXTENSION: 'api-based-extension',
|
API_BASED_EXTENSION: 'api-based-extension',
|
||||||
CUSTOM: 'custom',
|
CUSTOM: 'custom',
|
||||||
LANGUAGE: 'language',
|
LANGUAGE: 'language',
|
||||||
|
SANDBOX_PROVIDER: 'sandbox-provider',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type AccountSettingTab = typeof ACCOUNT_SETTING_TAB[keyof typeof ACCOUNT_SETTING_TAB]
|
export type AccountSettingTab = typeof ACCOUNT_SETTING_TAB[keyof typeof ACCOUNT_SETTING_TAB]
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import DataSourcePage from './data-source-page-new'
|
|||||||
import LanguagePage from './language-page'
|
import LanguagePage from './language-page'
|
||||||
import MembersPage from './members-page'
|
import MembersPage from './members-page'
|
||||||
import ModelProviderPage from './model-provider-page'
|
import ModelProviderPage from './model-provider-page'
|
||||||
|
import SandboxProviderPage from './sandbox-provider-page'
|
||||||
import { useResetModelProviderListExpanded } from './model-provider-page/atoms'
|
import { useResetModelProviderListExpanded } from './model-provider-page/atoms'
|
||||||
|
|
||||||
const iconClassName = `
|
const iconClassName = `
|
||||||
@ -94,6 +95,12 @@ export default function AccountSetting({
|
|||||||
icon: <span className={cn('i-ri-puzzle-2-line', iconClassName)} />,
|
icon: <span className={cn('i-ri-puzzle-2-line', iconClassName)} />,
|
||||||
activeIcon: <span className={cn('i-ri-puzzle-2-fill', iconClassName)} />,
|
activeIcon: <span className={cn('i-ri-puzzle-2-fill', iconClassName)} />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: ACCOUNT_SETTING_TAB.SANDBOX_PROVIDER,
|
||||||
|
name: 'Sandbox Providers',
|
||||||
|
icon: <span className={cn('i-ri-shield-keyhole-line', iconClassName)} />,
|
||||||
|
activeIcon: <span className={cn('i-ri-shield-keyhole-fill', iconClassName)} />,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if (enableReplaceWebAppLogo || enableBilling) {
|
if (enableReplaceWebAppLogo || enableBilling) {
|
||||||
@ -233,6 +240,7 @@ export default function AccountSetting({
|
|||||||
{activeMenu === ACCOUNT_SETTING_TAB.API_BASED_EXTENSION && <ApiBasedExtensionPage />}
|
{activeMenu === ACCOUNT_SETTING_TAB.API_BASED_EXTENSION && <ApiBasedExtensionPage />}
|
||||||
{activeMenu === ACCOUNT_SETTING_TAB.CUSTOM && <CustomPage />}
|
{activeMenu === ACCOUNT_SETTING_TAB.CUSTOM && <CustomPage />}
|
||||||
{activeMenu === ACCOUNT_SETTING_TAB.LANGUAGE && <LanguagePage />}
|
{activeMenu === ACCOUNT_SETTING_TAB.LANGUAGE && <LanguagePage />}
|
||||||
|
{activeMenu === ACCOUNT_SETTING_TAB.SANDBOX_PROVIDER && <SandboxProviderPage />}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,192 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiCheckLine, RiDeleteBin7Line, RiSettings3Line } from '@remixicon/react'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Input from '@/app/components/base/input'
|
||||||
|
import { toast } from '@/app/components/base/ui/toast'
|
||||||
|
import {
|
||||||
|
activateSandboxProvider,
|
||||||
|
deleteSandboxProviderConfig,
|
||||||
|
listSandboxProviders,
|
||||||
|
saveSandboxProviderConfig,
|
||||||
|
} from '@/service/sandbox'
|
||||||
|
import type { SandboxProvider } from '@/service/sandbox'
|
||||||
|
|
||||||
|
const providerConfigs: Record<string, { label: string, description: string, fields: Array<{ key: string, label: string, secret?: boolean }> }> = {
|
||||||
|
docker: {
|
||||||
|
label: 'Docker',
|
||||||
|
description: 'Run agent tools in Docker containers on the local machine.',
|
||||||
|
fields: [
|
||||||
|
{ key: 'docker_sock', label: 'Docker Socket Path' },
|
||||||
|
{ key: 'docker_image', label: 'Docker Image' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
e2b: {
|
||||||
|
label: 'E2B Cloud',
|
||||||
|
description: 'Run agent tools in E2B cloud sandboxes.',
|
||||||
|
fields: [
|
||||||
|
{ key: 'api_key', label: 'E2B API Key', secret: true },
|
||||||
|
{ key: 'e2b_default_template', label: 'Template' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
ssh: {
|
||||||
|
label: 'SSH Remote',
|
||||||
|
description: 'Run agent tools on a remote server via SSH.',
|
||||||
|
fields: [
|
||||||
|
{ key: 'ssh_host', label: 'Host' },
|
||||||
|
{ key: 'ssh_port', label: 'Port' },
|
||||||
|
{ key: 'ssh_username', label: 'Username' },
|
||||||
|
{ key: 'ssh_password', label: 'Password / Private Key', secret: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
aws_code_interpreter: {
|
||||||
|
label: 'AWS CodeInterpreter',
|
||||||
|
description: 'Run agent tools in Amazon Bedrock AgentCore Code Interpreter.',
|
||||||
|
fields: [
|
||||||
|
{ key: 'aws_access_key_id', label: 'Access Key ID', secret: true },
|
||||||
|
{ key: 'aws_secret_access_key', label: 'Secret Access Key', secret: true },
|
||||||
|
{ key: 'aws_region', label: 'Region' },
|
||||||
|
{ key: 'code_interpreter_id', label: 'Code Interpreter ID' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SandboxProviderPage() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [providers, setProviders] = useState<SandboxProvider[]>([])
|
||||||
|
const [editingType, setEditingType] = useState<string | null>(null)
|
||||||
|
const [editConfig, setEditConfig] = useState<Record<string, string>>({})
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const fetchProviders = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const data = await listSandboxProviders()
|
||||||
|
setProviders(Array.isArray(data) ? data : [])
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
setProviders([])
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => { fetchProviders() }, [fetchProviders])
|
||||||
|
|
||||||
|
const handleSave = async (providerType: string) => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
await saveSandboxProviderConfig(providerType, editConfig, true)
|
||||||
|
toast({ type: 'success', message: 'Saved and activated' })
|
||||||
|
setEditingType(null)
|
||||||
|
fetchProviders()
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
toast({ type: 'error', message: e.message || 'Failed to save' })
|
||||||
|
}
|
||||||
|
finally { setLoading(false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (providerType: string) => {
|
||||||
|
try {
|
||||||
|
await deleteSandboxProviderConfig(providerType)
|
||||||
|
toast({ type: 'success', message: 'Deleted' })
|
||||||
|
fetchProviders()
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
toast({ type: 'error', message: e.message || 'Failed to delete' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleActivate = async (providerType: string) => {
|
||||||
|
try {
|
||||||
|
await activateSandboxProvider(providerType)
|
||||||
|
toast({ type: 'success', message: 'Activated' })
|
||||||
|
fetchProviders()
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
toast({ type: 'error', message: e.message || 'Failed to activate' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeProvider = providers.find(p => p.is_active)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-2 pb-7">
|
||||||
|
<div className="mb-4 flex items-center gap-2">
|
||||||
|
<RiSettings3Line className="h-5 w-5 text-text-secondary" />
|
||||||
|
<h2 className="text-text-primary title-xl-semi-bold">Sandbox Providers</h2>
|
||||||
|
</div>
|
||||||
|
<p className="mb-6 text-text-tertiary system-sm-regular">
|
||||||
|
Configure where agent tools execute in isolated environments.
|
||||||
|
{activeProvider && (
|
||||||
|
<span className="ml-2 text-text-accent">
|
||||||
|
Active: <strong>{providerConfigs[activeProvider.provider_type]?.label || activeProvider.provider_type}</strong>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{Object.entries(providerConfigs).map(([type, cfg]) => {
|
||||||
|
const existing = providers.find(p => p.provider_type === type)
|
||||||
|
const isActive = existing?.is_active
|
||||||
|
const isEditing = editingType === type
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={type} className={`rounded-xl border ${isActive ? 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg' : 'border-divider-subtle'} p-4`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-text-primary system-md-semibold">{cfg.label}</h3>
|
||||||
|
<p className="text-text-tertiary system-xs-regular mt-0.5">{cfg.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isActive && (
|
||||||
|
<span className="flex items-center gap-1 rounded-full bg-util-colors-green-green-50 px-2 py-0.5 text-[11px] text-util-colors-green-green-600">
|
||||||
|
<RiCheckLine className="h-3 w-3" /> Active
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{existing && !isActive && (
|
||||||
|
<Button size="small" onClick={() => handleActivate(type)}>Activate</Button>
|
||||||
|
)}
|
||||||
|
<Button size="small" variant="secondary" onClick={() => {
|
||||||
|
setEditingType(isEditing ? null : type)
|
||||||
|
setEditConfig(existing?.config || {})
|
||||||
|
}}>
|
||||||
|
{isEditing ? 'Cancel' : 'Configure'}
|
||||||
|
</Button>
|
||||||
|
{existing && (
|
||||||
|
<Button size="small" variant="ghost" onClick={() => handleDelete(type)}>
|
||||||
|
<RiDeleteBin7Line className="h-4 w-4 text-text-tertiary" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEditing && (
|
||||||
|
<div className="mt-4 space-y-3 border-t border-divider-subtle pt-4">
|
||||||
|
{cfg.fields.map(field => (
|
||||||
|
<div key={field.key}>
|
||||||
|
<label className="mb-1 block text-text-secondary system-xs-semibold">{field.label}</label>
|
||||||
|
<Input
|
||||||
|
type={field.secret ? 'password' : 'text'}
|
||||||
|
value={editConfig[field.key] || ''}
|
||||||
|
onChange={e => setEditConfig(prev => ({ ...prev, [field.key]: e.target.value }))}
|
||||||
|
placeholder={field.label}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => handleSave(type)}
|
||||||
|
>
|
||||||
|
{loading ? 'Saving...' : 'Save & Activate'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
61
web/app/components/workflow/nodes/agent-v2/node.tsx
Normal file
61
web/app/components/workflow/nodes/agent-v2/node.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import type { FC } from 'react'
|
||||||
|
import type { NodeProps } from '../../types'
|
||||||
|
import type { AgentV2NodeType } from './types'
|
||||||
|
import { memo, useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiRobot2Line, RiToolsFill } from '@remixicon/react'
|
||||||
|
import { Group, GroupLabel } from '../_base/components/group'
|
||||||
|
import { SettingItem } from '../_base/components/setting-item'
|
||||||
|
|
||||||
|
const strategyLabels: Record<string, string> = {
|
||||||
|
auto: 'Auto',
|
||||||
|
'function-calling': 'Function Calling',
|
||||||
|
'chain-of-thought': 'ReAct (Chain of Thought)',
|
||||||
|
}
|
||||||
|
|
||||||
|
const AgentV2Node: FC<NodeProps<AgentV2NodeType>> = ({ id, data }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const modelName = data.model?.name || ''
|
||||||
|
const modelProvider = data.model?.provider || ''
|
||||||
|
const strategy = data.agent_strategy || 'auto'
|
||||||
|
const enabledTools = useMemo(() => (data.tools || []).filter(t => t.enabled), [data.tools])
|
||||||
|
const maxIter = data.max_iterations || 10
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-1 space-y-1 px-3">
|
||||||
|
<SettingItem label={t('workflow.nodes.llm.model')}>
|
||||||
|
<span className="system-xs-medium text-text-secondary truncate">
|
||||||
|
{modelName || 'Not configured'}
|
||||||
|
</span>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem label="Strategy">
|
||||||
|
<span className="system-xs-medium text-text-secondary">
|
||||||
|
{strategyLabels[strategy] || strategy}
|
||||||
|
</span>
|
||||||
|
</SettingItem>
|
||||||
|
{enabledTools.length > 0 && (
|
||||||
|
<Group label={<GroupLabel className="mt-1"><RiToolsFill className="mr-1 inline h-3 w-3" />Tools ({enabledTools.length})</GroupLabel>}>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{enabledTools.slice(0, 6).map((tool, i) => (
|
||||||
|
<span key={i} className="inline-flex items-center rounded bg-components-badge-bg-gray px-1.5 py-0.5 text-[11px] text-text-tertiary">
|
||||||
|
{tool.tool_name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{enabledTools.length > 6 && (
|
||||||
|
<span className="text-[11px] text-text-quaternary">+{enabledTools.length - 6}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
{maxIter !== 10 && (
|
||||||
|
<SettingItem label="Max Iterations">
|
||||||
|
<span className="system-xs-medium text-text-secondary">{maxIter}</span>
|
||||||
|
</SettingItem>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AgentV2Node.displayName = 'AgentV2Node'
|
||||||
|
export default memo(AgentV2Node)
|
||||||
145
web/app/components/workflow/nodes/agent-v2/panel.tsx
Normal file
145
web/app/components/workflow/nodes/agent-v2/panel.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import type { FC } from 'react'
|
||||||
|
import type { AgentV2NodeType } from './types'
|
||||||
|
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||||
|
import { memo, useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiAddLine, RiDeleteBin7Line } from '@remixicon/react'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Select from '@/app/components/base/select'
|
||||||
|
import Switch from '@/app/components/base/switch'
|
||||||
|
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||||
|
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||||
|
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||||
|
import ConfigVision from '../_base/components/config-vision'
|
||||||
|
import MemoryConfig from '../_base/components/memory-config'
|
||||||
|
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
|
||||||
|
import ConfigPrompt from '../llm/components/config-prompt'
|
||||||
|
import { useProviderContextSelector } from '@/context/provider-context'
|
||||||
|
import { useNodeDataUpdate } from '../../hooks/use-node-data-update'
|
||||||
|
|
||||||
|
const strategyOptions = [
|
||||||
|
{ value: 'auto', name: 'Auto (based on model capability)' },
|
||||||
|
{ value: 'function-calling', name: 'Function Calling' },
|
||||||
|
{ value: 'chain-of-thought', name: 'ReAct (Chain of Thought)' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const Panel: FC<NodePanelProps<AgentV2NodeType>> = ({ id, data }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||||
|
|
||||||
|
const updateData = useCallback((patch: Partial<AgentV2NodeType>) => {
|
||||||
|
handleNodeDataUpdate({ id, data: patch as any })
|
||||||
|
}, [id, handleNodeDataUpdate])
|
||||||
|
|
||||||
|
const inputs = data as AgentV2NodeType
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 px-4 pb-4 pt-2">
|
||||||
|
{/* Model Selection */}
|
||||||
|
<Field title={t('workflow.nodes.llm.model')}>
|
||||||
|
<ModelParameterModal
|
||||||
|
popupProps={{ disabled: false }}
|
||||||
|
isInWorkflow
|
||||||
|
isAdvancedMode
|
||||||
|
mode={inputs.model?.mode || 'chat'}
|
||||||
|
provider={inputs.model?.provider || ''}
|
||||||
|
completionParams={inputs.model?.completion_params || {}}
|
||||||
|
modelId={inputs.model?.name || ''}
|
||||||
|
setModel={(model) => {
|
||||||
|
updateData({
|
||||||
|
model: {
|
||||||
|
...inputs.model,
|
||||||
|
provider: model.provider,
|
||||||
|
name: model.modelId,
|
||||||
|
mode: model.mode || 'chat',
|
||||||
|
completion_params: model.completionParams || {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onCompletionParamsChange={(params) => {
|
||||||
|
updateData({
|
||||||
|
model: { ...inputs.model, completion_params: params },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Split />
|
||||||
|
|
||||||
|
{/* Agent Strategy */}
|
||||||
|
<Field title="Agent Strategy">
|
||||||
|
<Select
|
||||||
|
items={strategyOptions}
|
||||||
|
defaultValue={inputs.agent_strategy || 'auto'}
|
||||||
|
onSelect={(item) => updateData({ agent_strategy: item.value as any })}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{/* Max Iterations */}
|
||||||
|
<Field title="Max Iterations">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={99}
|
||||||
|
className="w-full rounded-lg border border-components-input-border-active px-3 py-1.5 text-[13px]"
|
||||||
|
value={inputs.max_iterations || 10}
|
||||||
|
onChange={(e) => updateData({ max_iterations: parseInt(e.target.value) || 10 })}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Split />
|
||||||
|
|
||||||
|
{/* Tools */}
|
||||||
|
<Field title={`Tools (${(inputs.tools || []).filter(t => t.enabled).length})`}>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(inputs.tools || []).map((tool, idx) => (
|
||||||
|
<div key={idx} className="flex items-center justify-between rounded-lg border border-divider-subtle px-3 py-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
defaultValue={tool.enabled}
|
||||||
|
onChange={(v) => {
|
||||||
|
const tools = [...(inputs.tools || [])]
|
||||||
|
tools[idx] = { ...tools[idx], enabled: v }
|
||||||
|
updateData({ tools })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-[13px] text-text-secondary">{tool.tool_name}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] text-text-quaternary">{tool.provider_name?.split('/').pop()}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(inputs.tools || []).length === 0 && (
|
||||||
|
<div className="py-3 text-center text-[13px] text-text-quaternary">
|
||||||
|
No tools configured. Add tools from the workflow toolbar.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Split />
|
||||||
|
|
||||||
|
{/* Memory */}
|
||||||
|
<Field title="Memory">
|
||||||
|
<MemoryConfig
|
||||||
|
readonly={false}
|
||||||
|
config={inputs.memory || { window: { enabled: true, size: 50 } }}
|
||||||
|
onChange={(memory) => updateData({ memory })}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Split />
|
||||||
|
|
||||||
|
{/* Vision */}
|
||||||
|
<Field title="Vision">
|
||||||
|
<ConfigVision
|
||||||
|
payload={inputs.vision}
|
||||||
|
onChange={(vision) => updateData({ vision })}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel.displayName = 'AgentV2Panel'
|
||||||
|
export default memo(Panel)
|
||||||
32
web/app/components/workflow/nodes/agent-v2/types.ts
Normal file
32
web/app/components/workflow/nodes/agent-v2/types.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type { CommonNodeType, Memory, ModelConfig, PromptItem, ValueSelector, VisionSetting } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
export type ToolMetadata = {
|
||||||
|
enabled: boolean
|
||||||
|
type: string
|
||||||
|
provider_name: string
|
||||||
|
tool_name: string
|
||||||
|
plugin_unique_identifier?: string
|
||||||
|
credential_id?: string
|
||||||
|
parameters: Record<string, any>
|
||||||
|
settings: Record<string, any>
|
||||||
|
extra: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AgentV2NodeType = CommonNodeType & {
|
||||||
|
model: ModelConfig
|
||||||
|
prompt_template: PromptItem[] | PromptItem
|
||||||
|
tools: ToolMetadata[]
|
||||||
|
max_iterations: number
|
||||||
|
agent_strategy: 'auto' | 'function-calling' | 'chain-of-thought'
|
||||||
|
memory?: Memory
|
||||||
|
context: {
|
||||||
|
enabled: boolean
|
||||||
|
variable_selector?: ValueSelector
|
||||||
|
}
|
||||||
|
vision: {
|
||||||
|
enabled: boolean
|
||||||
|
configs?: VisionSetting
|
||||||
|
}
|
||||||
|
structured_output_enabled?: boolean
|
||||||
|
structured_output?: Record<string, any>
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@ import type { ComponentType } from 'react'
|
|||||||
import { BlockEnum } from '../types'
|
import { BlockEnum } from '../types'
|
||||||
import AgentNode from './agent/node'
|
import AgentNode from './agent/node'
|
||||||
import AgentPanel from './agent/panel'
|
import AgentPanel from './agent/panel'
|
||||||
|
import AgentV2Node from './agent-v2/node'
|
||||||
|
import AgentV2Panel from './agent-v2/panel'
|
||||||
import AnswerNode from './answer/node'
|
import AnswerNode from './answer/node'
|
||||||
import AnswerPanel from './answer/panel'
|
import AnswerPanel from './answer/panel'
|
||||||
import AssignerNode from './assigner/node'
|
import AssignerNode from './assigner/node'
|
||||||
@ -72,6 +74,7 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
|
|||||||
[BlockEnum.DocExtractor]: DocExtractorNode,
|
[BlockEnum.DocExtractor]: DocExtractorNode,
|
||||||
[BlockEnum.ListFilter]: ListFilterNode,
|
[BlockEnum.ListFilter]: ListFilterNode,
|
||||||
[BlockEnum.Agent]: AgentNode,
|
[BlockEnum.Agent]: AgentNode,
|
||||||
|
[BlockEnum.AgentV2]: AgentV2Node,
|
||||||
[BlockEnum.DataSource]: DataSourceNode,
|
[BlockEnum.DataSource]: DataSourceNode,
|
||||||
[BlockEnum.KnowledgeBase]: KnowledgeBaseNode,
|
[BlockEnum.KnowledgeBase]: KnowledgeBaseNode,
|
||||||
[BlockEnum.HumanInput]: HumanInputNode,
|
[BlockEnum.HumanInput]: HumanInputNode,
|
||||||
@ -101,6 +104,7 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
|
|||||||
[BlockEnum.DocExtractor]: DocExtractorPanel,
|
[BlockEnum.DocExtractor]: DocExtractorPanel,
|
||||||
[BlockEnum.ListFilter]: ListFilterPanel,
|
[BlockEnum.ListFilter]: ListFilterPanel,
|
||||||
[BlockEnum.Agent]: AgentPanel,
|
[BlockEnum.Agent]: AgentPanel,
|
||||||
|
[BlockEnum.AgentV2]: AgentV2Panel,
|
||||||
[BlockEnum.DataSource]: DataSourcePanel,
|
[BlockEnum.DataSource]: DataSourcePanel,
|
||||||
[BlockEnum.KnowledgeBase]: KnowledgeBasePanel,
|
[BlockEnum.KnowledgeBase]: KnowledgeBasePanel,
|
||||||
[BlockEnum.HumanInput]: HumanInputPanel,
|
[BlockEnum.HumanInput]: HumanInputPanel,
|
||||||
|
|||||||
@ -46,6 +46,7 @@ export enum BlockEnum {
|
|||||||
IterationStart = 'iteration-start',
|
IterationStart = 'iteration-start',
|
||||||
Assigner = 'assigner', // is now named as VariableAssigner
|
Assigner = 'assigner', // is now named as VariableAssigner
|
||||||
Agent = 'agent',
|
Agent = 'agent',
|
||||||
|
AgentV2 = 'agent-v2',
|
||||||
Loop = 'loop',
|
Loop = 'loop',
|
||||||
LoopStart = 'loop-start',
|
LoopStart = 'loop-start',
|
||||||
LoopEnd = 'loop-end',
|
LoopEnd = 'loop-end',
|
||||||
|
|||||||
@ -135,6 +135,7 @@
|
|||||||
"newApp.advancedUserDescription": "Workflow with additional memory features and a chatbot interface.",
|
"newApp.advancedUserDescription": "Workflow with additional memory features and a chatbot interface.",
|
||||||
"newApp.agentAssistant": "New Agent Assistant",
|
"newApp.agentAssistant": "New Agent Assistant",
|
||||||
"newApp.agentShortDescription": "Intelligent agent with reasoning and autonomous tool use",
|
"newApp.agentShortDescription": "Intelligent agent with reasoning and autonomous tool use",
|
||||||
|
"newApp.agentV2ShortDescription": "Next-gen agent with tools, sandbox, and workflow integration",
|
||||||
"newApp.agentUserDescription": "An intelligent agent capable of iterative reasoning and autonomous tool use to achieve task goals.",
|
"newApp.agentUserDescription": "An intelligent agent capable of iterative reasoning and autonomous tool use to achieve task goals.",
|
||||||
"newApp.appCreateDSLErrorPart1": "A significant difference in DSL versions has been detected. Forcing the import may cause the application to malfunction.",
|
"newApp.appCreateDSLErrorPart1": "A significant difference in DSL versions has been detected. Forcing the import may cause the application to malfunction.",
|
||||||
"newApp.appCreateDSLErrorPart2": "Do you want to continue?",
|
"newApp.appCreateDSLErrorPart2": "Do you want to continue?",
|
||||||
|
|||||||
@ -135,6 +135,7 @@
|
|||||||
"newApp.advancedUserDescription": "基于工作流编排,适用于定义等复杂流程的多轮对话场景,具有记忆功能。",
|
"newApp.advancedUserDescription": "基于工作流编排,适用于定义等复杂流程的多轮对话场景,具有记忆功能。",
|
||||||
"newApp.agentAssistant": "新的智能助手",
|
"newApp.agentAssistant": "新的智能助手",
|
||||||
"newApp.agentShortDescription": "具备推理与自主工具调用的智能助手",
|
"newApp.agentShortDescription": "具备推理与自主工具调用的智能助手",
|
||||||
|
"newApp.agentV2ShortDescription": "新一代 Agent,支持工具调用、沙箱执行和 Workflow 集成",
|
||||||
"newApp.agentUserDescription": "能够迭代式的规划推理、自主工具调用,直至完成任务目标的智能助手。",
|
"newApp.agentUserDescription": "能够迭代式的规划推理、自主工具调用,直至完成任务目标的智能助手。",
|
||||||
"newApp.appCreateDSLErrorPart1": "检测到 DSL 版本差异较大,强制导入应用可能无法正常运行。",
|
"newApp.appCreateDSLErrorPart1": "检测到 DSL 版本差异较大,强制导入应用可能无法正常运行。",
|
||||||
"newApp.appCreateDSLErrorPart2": "是否继续?",
|
"newApp.appCreateDSLErrorPart2": "是否继续?",
|
||||||
|
|||||||
40
web/service/sandbox.ts
Normal file
40
web/service/sandbox.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { del, get, post } from './base'
|
||||||
|
|
||||||
|
export type SandboxProvider = {
|
||||||
|
provider_type: string
|
||||||
|
is_active: boolean
|
||||||
|
config?: Record<string, any>
|
||||||
|
config_schema?: Array<{
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listSandboxProviders = (): Promise<SandboxProvider[]> => {
|
||||||
|
return get<SandboxProvider[]>('workspaces/current/sandbox-providers')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveSandboxProviderConfig = (
|
||||||
|
providerType: string,
|
||||||
|
config: Record<string, any>,
|
||||||
|
activate = false,
|
||||||
|
): Promise<{ result: string }> => {
|
||||||
|
return post<{ result: string }>(`workspaces/current/sandbox-provider/${providerType}/config`, {
|
||||||
|
body: { config, activate },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const activateSandboxProvider = (
|
||||||
|
providerType: string,
|
||||||
|
type = 'user',
|
||||||
|
): Promise<{ result: string }> => {
|
||||||
|
return post<{ result: string }>(`workspaces/current/sandbox-provider/${providerType}/activate`, {
|
||||||
|
body: { type },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSandboxProviderConfig = (
|
||||||
|
providerType: string,
|
||||||
|
): Promise<{ result: string }> => {
|
||||||
|
return del<{ result: string }>(`workspaces/current/sandbox-provider/${providerType}/config`)
|
||||||
|
}
|
||||||
@ -44,8 +44,9 @@ export enum AppModeEnum {
|
|||||||
CHAT = 'chat',
|
CHAT = 'chat',
|
||||||
ADVANCED_CHAT = 'advanced-chat',
|
ADVANCED_CHAT = 'advanced-chat',
|
||||||
AGENT_CHAT = 'agent-chat',
|
AGENT_CHAT = 'agent-chat',
|
||||||
|
AGENT = 'agent',
|
||||||
}
|
}
|
||||||
export const AppModes = [AppModeEnum.COMPLETION, AppModeEnum.WORKFLOW, AppModeEnum.CHAT, AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT] as const
|
export const AppModes = [AppModeEnum.COMPLETION, AppModeEnum.WORKFLOW, AppModeEnum.CHAT, AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.AGENT] as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variable type
|
* Variable type
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export const getRedirectionPath = (
|
|||||||
return `/app/${app.id}/overview`
|
return `/app/${app.id}/overview`
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT)
|
if (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT || app.mode === AppModeEnum.AGENT)
|
||||||
return `/app/${app.id}/workflow`
|
return `/app/${app.id}/workflow`
|
||||||
else
|
else
|
||||||
return `/app/${app.id}/configuration`
|
return `/app/${app.id}/configuration`
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user