Commit ee979434 by YeYuheng Committed by GitHub

fix: stabilize agent sandbox mentions (#7027)

parent 2272899b
......@@ -7,3 +7,5 @@ export const SANDBOX_NAME: I18nStringType = {
};
export const SANDBOX_ICON = 'core/app/sandbox/sandbox' as const;
export const AGENT_SANDBOX_TOOLSET_ID = 'agent_sandbox';
......@@ -24,7 +24,7 @@ import {
SANDBOX_WRITE_FILE_TOOL_NAME
} from './writeFile';
export { SANDBOX_ICON, SANDBOX_NAME } from './common';
export { AGENT_SANDBOX_TOOLSET_ID, SANDBOX_ICON, SANDBOX_NAME } from './common';
export {
SANDBOX_EDIT_FILE_NAME,
SANDBOX_EDIT_FILE_TOOL,
......
import type { I18nStringType, localeType } from '../../../../common/i18n/type';
import { sandboxToolMap } from '../../../ai/sandbox/tools';
import {
AGENT_SANDBOX_TOOLSET_ID,
SANDBOX_ICON,
SANDBOX_NAME,
sandboxToolMap
} from '../../../ai/sandbox/tools';
import { parseI18nString } from '../../../../common/i18n/utils';
export enum SubAppIds {
......@@ -50,6 +55,12 @@ export const systemSubInfo: Record<
avatar: 'core/workflow/template/agent',
toolDescription: '调用 LLM 模型完成一些通用任务。'
},
[AGENT_SANDBOX_TOOLSET_ID]: {
name: SANDBOX_NAME,
avatar: SANDBOX_ICON,
toolDescription:
'提供完整虚拟机能力,包括命令执行、文件读写、文件编辑、文件搜索和文件链接生成。'
},
...sandboxToolMap
};
export const getSystemToolInfo = (id: string, lang: localeType = 'en') => {
......
......@@ -23,6 +23,7 @@ import { MongoDataset } from '../../../../dataset/schema';
import { ObjectIdSchema } from '@fastgpt/global/common/type/mongo';
import type { SelectedDatasetType } from '@fastgpt/global/core/workflow/type/io';
import { getLogger, LogCategories } from '../../../../../common/logger';
import { AGENT_SANDBOX_TOOLSET_ID } from '@fastgpt/global/core/ai/sandbox/tools';
export const dispatchTopAgent = async (
props: HelperBotDispatchParamsType<TopAgentParamsType>
......@@ -155,12 +156,15 @@ export const dispatchTopAgent = async (
teamId: user.teamId,
datasetIds: knowledges
});
const enableSandboxEnabled =
responseJson.resources?.system_features?.sandbox?.enabled ||
tools.includes(AGENT_SANDBOX_TOOLSET_ID);
const formData = TopAgentFormDataSchema.parse({
systemPrompt: buildSystemPrompt(responseJson), // 构建 system prompt
tools, // 从 execution_plan 提取
datasets: filterDatasets,
fileUploadEnabled: responseJson.resources?.system_features?.file_upload?.enabled || false,
enableSandboxEnabled: responseJson.resources?.system_features?.sandbox?.enabled || false,
enableSandboxEnabled,
executionPlan: responseJson.execution_plan // 保存原始 execution_plan
});
......
......@@ -7,9 +7,9 @@ import { MongoResourcePermission } from '../../../../../support/permission/schem
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { getGroupsByTmbId } from '../../../../../support/permission/memberGroup/controllers';
import { getOrgIdSetWithParentByTmbId } from '../../../../../support/permission/org/controllers';
import { SANDBOX_SHELL_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools';
import { getUserAvaliableWorkflowTools } from '../../../../app/tool/workflowTool';
import { SystemToolRepo } from '../../../../app/tool/systemTool/systemTool.repo';
import { AGENT_SANDBOX_TOOLSET_ID } from '@fastgpt/global/core/ai/sandbox/tools';
const getAccessibleDatasets = async ({ teamId, tmbId }: { teamId: string; tmbId: string }) => {
const [roleList, myGroupMap, myOrgSet] = await Promise.all([
......@@ -109,7 +109,7 @@ ${dataset}
})
]);
const builtinTools = [SubAppIds.readFiles, SANDBOX_SHELL_TOOL_NAME].map((id) => {
const builtinTools = [SubAppIds.readFiles, AGENT_SANDBOX_TOOLSET_ID].map((id) => {
const info = systemSubInfo[id];
return `- **${id}** [工具]: ${parseI18nString(info.name, lang)} - ${info.toolDescription}`;
});
......
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AGENT_SANDBOX_TOOLSET_ID } from '@fastgpt/global/core/ai/sandbox/tools';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
const { createLLMResponseMock } = vi.hoisted(() => ({
createLLMResponseMock: vi.fn()
}));
vi.mock('@fastgpt/service/core/ai/llm/request', () => ({
createLLMResponse: createLLMResponseMock
}));
vi.mock('@fastgpt/service/core/ai/model', () => ({
getDefaultHelperBotModel: vi.fn(() => ({
model: 'helper-model'
}))
}));
vi.mock('@fastgpt/service/core/app/tool/controller', () => ({
getSystemToolsWithInstalled: vi.fn(async () => []),
getMyTools: vi.fn(async () => [])
}));
vi.mock('@fastgpt/service/core/dataset/schema', () => ({
MongoDataset: {
find: vi.fn(() => ({
select: vi.fn(() => ({
sort: vi.fn(() => ({
lean: vi.fn(async () => [])
}))
})),
lean: vi.fn(async () => [])
}))
}
}));
vi.mock('@fastgpt/service/support/permission/schema', () => ({
MongoResourcePermission: {
find: vi.fn(() => ({
lean: vi.fn(async () => [])
}))
}
}));
vi.mock('@fastgpt/service/support/permission/memberGroup/controllers', () => ({
getGroupsByTmbId: vi.fn(async () => [])
}));
vi.mock('@fastgpt/service/support/permission/org/controllers', () => ({
getOrgIdSetWithParentByTmbId: vi.fn(async () => new Set())
}));
import { dispatchTopAgent } from '@fastgpt/service/core/chat/HelperBot/dispatch/topAgent';
describe('dispatchTopAgent', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('enables sandbox when generated plan selects the agent sandbox toolset', async () => {
createLLMResponseMock.mockResolvedValue({
answerText: JSON.stringify({
phase: 'generation',
reasoning: 'need sandbox',
task_analysis: {
goal: 'run code',
role: 'coding assistant',
key_features: 'execute shell commands'
},
execution_plan: {
total_steps: 1,
steps: [
{
id: 'step_1',
title: 'Execute command',
description: `Use @${AGENT_SANDBOX_TOOLSET_ID} to inspect files`,
expectedTools: [
{
id: AGENT_SANDBOX_TOOLSET_ID,
type: 'tool'
}
]
}
]
},
resources: {
system_features: {
file_upload: {
enabled: false
},
sandbox: {
enabled: false
}
}
}
}),
reasoningText: '',
usage: {
inputTokens: 10,
outputTokens: 5
}
});
const workflowResponseWrite = vi.fn();
await dispatchTopAgent({
query: 'build an agent that can run commands',
files: [],
data: {},
histories: [],
workflowResponseWrite,
user: {
teamId: 'team_1',
tmbId: 'tmb_1',
userId: 'user_1',
isRoot: false,
lang: 'zh-CN'
}
});
expect(workflowResponseWrite).toHaveBeenCalledWith({
event: SseResponseEventEnum.topAgentConfig,
data: expect.objectContaining({
tools: [AGENT_SANDBOX_TOOLSET_ID],
enableSandboxEnabled: true
})
});
});
});
import { describe, expect, it, vi } from 'vitest';
import {
AGENT_SANDBOX_TOOLSET_ID,
SANDBOX_SHELL_TOOL_NAME
} from '@fastgpt/global/core/ai/sandbox/tools';
vi.mock('@fastgpt/service/core/app/tool/controller', () => ({
getSystemToolsWithInstalled: vi.fn(async () => []),
getMyTools: vi.fn(async () => [])
}));
vi.mock('@fastgpt/service/core/dataset/schema', () => ({
MongoDataset: {
find: vi.fn(() => ({
select: vi.fn(() => ({
sort: vi.fn(() => ({
lean: vi.fn(async () => [])
}))
}))
}))
}
}));
vi.mock('@fastgpt/service/support/permission/schema', () => ({
MongoResourcePermission: {
find: vi.fn(() => ({
lean: vi.fn(async () => [])
}))
}
}));
vi.mock('@fastgpt/service/support/permission/memberGroup/controllers', () => ({
getGroupsByTmbId: vi.fn(async () => [])
}));
vi.mock('@fastgpt/service/support/permission/org/controllers', () => ({
getOrgIdSetWithParentByTmbId: vi.fn(async () => new Set())
}));
import { generateResourceList } from '@fastgpt/service/core/chat/HelperBot/dispatch/topAgent/utils';
describe('topAgent utils', () => {
it('lists sandbox as an agent sandbox capability group instead of the shell tool', async () => {
const { resourceList } = await generateResourceList({
teamId: 'team_1',
tmbId: 'tmb_1',
isRoot: false,
lang: 'zh-CN'
});
expect(resourceList).toContain(`**${AGENT_SANDBOX_TOOLSET_ID}**`);
expect(resourceList).not.toContain(`**${SANDBOX_SHELL_TOOL_NAME}**`);
});
});
......@@ -7,7 +7,7 @@
*/
import type { CSSProperties } from 'react';
import { useEffect, useMemo, useState, useTransition, useRef } from 'react';
import { useMemo, useState, useTransition, useRef } from 'react';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
......@@ -27,7 +27,7 @@ import { Box, Flex } from '@chakra-ui/react';
import styles from './index.module.scss';
import VariablePlugin from './plugins/VariablePlugin';
import { VariableNode } from './plugins/VariablePlugin/node';
import type { EditorState, LexicalEditor } from 'lexical';
import type { LexicalEditor } from 'lexical';
import OnBlurPlugin from './plugins/OnBlurPlugin';
import type { FormPropsType } from './type';
import { type EditorVariableLabelPickerType, type EditorVariablePickerType } from './type';
......@@ -142,7 +142,7 @@ export default function Editor({
onBlur?: (editor: LexicalEditor) => void;
}) {
const [key, setKey] = useState(getNanoid(6));
const [_, startSts] = useTransition();
const [, startSts] = useTransition();
const [focus, setFocus] = useState(false);
const [scrollHeight, setScrollHeight] = useState(0);
const editorOutputRef = useRef(value);
......@@ -164,10 +164,12 @@ export default function Editor({
}
};
// 技能菜单和标签状态由插件内部同步;不能因 selectedSkills 变化重建编辑器,
// 否则 @ 插入瞬间会销毁 SkillNode,并误触发工具移除监听。
useDeepCompareEffect(() => {
if (focus && value === editorOutputRef.current) return;
setKey(getNanoid(6));
}, [value, variables, variableLabels, skillOption, selectedSkills]);
}, [value, variables, variableLabels]);
const showFullScreenIcon = useMemo(() => {
return showOpenModal && scrollHeight > maxH;
......
......@@ -97,8 +97,6 @@ function SkillLabelPlugin({
// Update existing SkillNode properties when selectedSkills change
// Sync tool name, avatar, status and configure handler for each skill node
useEffect(() => {
if (selectedSkills.length === 0) return;
// Wrapped click handler: delete if invalid, otherwise call original onClickSkill
const handleClick = (id: string, status: SkillLabelItemType['configStatus']) => {
if (status === 'invalid') {
......@@ -126,14 +124,21 @@ function SkillLabelPlugin({
if (node instanceof SkillNode) {
const id = node.getSkillKey();
const tool = selectedSkills.find((t) => t.id === id);
const writableNode = node.getWritable();
if (tool) {
const writableNode = node.getWritable();
writableNode.__id = tool.id;
writableNode.__name = tool.name;
writableNode.__icon = tool.avatar;
writableNode.__skillType = tool.flowNodeType;
writableNode.__status = tool.configStatus;
writableNode.__onClick = (id) => handleClick(id, tool.configStatus);
} else {
writableNode.__name = id;
writableNode.__icon = undefined;
writableNode.__skillType = FlowNodeTypeEnum.tool;
writableNode.__status = 'invalid';
writableNode.__onClick = (id) => handleClick(id, 'invalid');
}
}
});
......
Subproject commit 0e3a010511341a538100a3ca7291bb2c6ccb5118
Subproject commit 1ebfaac6a4e6faef2c01d4b6785add3debf7285f
......@@ -28,7 +28,7 @@ import { SmallAddIcon } from '@chakra-ui/icons';
import MyIconButton, { MyDeleteIconButton } from '@fastgpt/web/components/common/Icon/button';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useSkillManager } from './hooks/useSkillManager';
import { SANDBOX_ICON } from '@fastgpt/global/core/ai/sandbox/tools';
import { AGENT_SANDBOX_TOOLSET_ID, SANDBOX_ICON } from '@fastgpt/global/core/ai/sandbox/tools';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import SandboxTipTag from '../../components/SandboxTipTag';
import { useSystemStore } from '@/web/common/system/useSystemStore';
......@@ -134,6 +134,30 @@ const EditForm = ({
enableSandbox,
setAppForm
});
const promptSkillOption = useMemo(
() => ({
...skillOption,
onSelect: async (id: string) => {
const option = await skillOption.onSelect?.(id);
if (!option?.onClick) return option;
return {
...option,
onClick: async (toolId: string) => {
const skillId = await option.onClick?.(toolId);
// AgentV2 提示词 @虚拟机 时,同步打开下方虚拟机开关。
if (skillId === AGENT_SANDBOX_TOOLSET_ID && !appForm.aiSettings.useAgentSandbox) {
onChangeAgentSandbox(true);
}
return skillId;
}
};
}
}),
[appForm.aiSettings.useAgentSandbox, onChangeAgentSandbox, skillOption]
);
const tokenLimit = useMemo(() => {
return selectedModel.quoteMaxToken || 3000;
}, [selectedModel.quoteMaxToken]);
......@@ -217,7 +241,7 @@ const EditForm = ({
bg={'myGray.50'}
title={t('common:core.ai.Prompt')}
isRichText={true}
skillOption={skillOption}
skillOption={promptSkillOption}
selectedSkills={selectedSkills}
onClickSkill={onClickSkill}
onRemoveSkill={onRemoveSkill}
......
......@@ -31,7 +31,7 @@ import {
import { useLatest } from 'ahooks';
import { SubAppIds, systemSubInfo } from '@fastgpt/global/core/workflow/node/agent/constants';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import { SANDBOX_SHELL_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools';
import { AGENT_SANDBOX_TOOLSET_ID } from '@fastgpt/global/core/ai/sandbox/tools';
const ConfigToolModal = dynamic(() => import('../../component/ConfigToolModal'));
......@@ -100,10 +100,10 @@ export const useSkillManager = ({
});
}
const sandboxToolInfo = systemSubInfo[SANDBOX_SHELL_TOOL_NAME];
const sandboxToolInfo = systemSubInfo[AGENT_SANDBOX_TOOLSET_ID];
if (sandboxToolInfo) {
apiTools.unshift({
id: SANDBOX_SHELL_TOOL_NAME,
id: AGENT_SANDBOX_TOOLSET_ID,
label: parseI18nString(sandboxToolInfo.name, i18n.language),
icon: sandboxToolInfo.avatar,
description: sandboxToolInfo.toolDescription,
......@@ -306,11 +306,11 @@ export const useSkillManager = ({
}
// Merge sandbox tool
const sandboxToolInfo = systemSubInfo[SANDBOX_SHELL_TOOL_NAME];
const sandboxToolInfo = systemSubInfo[AGENT_SANDBOX_TOOLSET_ID];
if (sandboxToolInfo) {
tools.push({
id: SANDBOX_SHELL_TOOL_NAME,
pluginId: SANDBOX_SHELL_TOOL_NAME,
id: AGENT_SANDBOX_TOOLSET_ID,
pluginId: AGENT_SANDBOX_TOOLSET_ID,
name: parseI18nString(sandboxToolInfo.name, i18n.language),
avatar: sandboxToolInfo.avatar,
intro: sandboxToolInfo.toolDescription,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment