V4.14.9 dev (#6555)

* feat: encapsulate logger (#6535)

* feat: encapsulate logger

* update engines

---------

Co-authored-by: archer <545436317@qq.com>

* next config

* dev shell

* Agent sandbox (#6532)

* docs: switch to docs layout and apply black theme (#6533)

* feat: add Gemini 3.1 models

- Add gemini-3.1-pro-preview (released February 19, 2026)
- Add gemini-3.1-flash-lite-preview (released March 3, 2026)

Both models support:
- 1M context window
- 64k max response
- Vision
- Tool choice

* docs: switch to docs layout and apply black theme

- Change layout from notebook to docs
- Update logo to icon + text format
- Apply fumadocs black theme
- Simplify global.css (keep only navbar and TOC styles)
- Fix icon components to properly accept className props
- Add mobile text overflow handling
- Update Node engine requirement to >=20.x

* doc

* doc

* lock

* fix: ts

* doc

* doc

---------

Co-authored-by: archer <archer@archerdeMac-mini.local>
Co-authored-by: archer <545436317@qq.com>

* Doc (#6493)

* cloud doc

* doc refactor

* doc move

* seo

* remove doc

* yml

* doc

* fix: tsconfig

* fix: tsconfig

* sandbox version (#6497)

* sandbox version

* add sandbox log

* update lock

* fix

* fix: sandbox

* doc

* add console

* i18n

* sandbxo in agent

* feat: agent sandbox

* lock

* feat: sandbox ui

* sandbox check exists

* env tempalte

* doc

* lock

* sandbox in chat window

* sandbox entry

* fix: test

* rename var

* sandbox config tip

* update sandbox lifecircle

* update prompt

* rename provider test

* sandbox logger

* yml

---------

Co-authored-by: Archer <archer@fastgpt.io>
Co-authored-by: archer <archer@archerdeMac-mini.local>

* perf: sandbox error tip

* Add sandbox limit and fix some issue (#6550)

* sandbox in plan

* fix: some issue

* fix: test

* editor default path

* fix: comment

* perf: sandbox worksapce

* doc

* perf: del sandbox

* sandbox build

* fix: test

* fix: pr comment

---------

Co-authored-by: Ryo <whoeverimf5@gmail.com>
Co-authored-by: Archer <archer@fastgpt.io>
Co-authored-by: archer <archer@archerdeMac-mini.local>
This commit is contained in:
Archer
2026-03-16 17:09:25 +08:00
committed by GitHub
parent 21b3f8549a
commit aaa7d17ef1
258 changed files with 6844 additions and 6162 deletions
@@ -275,6 +275,7 @@ export const runAgentCall = async ({
throwError: false,
body: {
...body,
max_tokens,
model,
messages: requestMessages,
tool_choice: consecutiveRequestToolTimes > 5 ? 'none' : 'auto',
@@ -442,16 +442,6 @@ export const compressToolResponse = async ({
// 取静态阈值和动态可用空间的较小值
const maxTokens = Math.min(staticMaxTokens, availableSpace);
logger.info('Tool Response Compression', {
responseTokens: await countGptMessagesTokens([{ role: 'user', content: response }]),
currentMessagesTokens,
maxContext: model.maxContext,
reservedTokens,
availableSpace,
staticMaxTokens,
finalMaxTokens: maxTokens
});
// 调用通用压缩函数
return compressLargeContent({
content: response,
+16 -1
View File
@@ -16,7 +16,7 @@ import {
parseLLMStreamResponse,
parseReasoningContent
} from '../utils';
import { removeDatasetCiteText } from '@fastgpt/global/core/ai/llm/utils';
import { getLLMSupportParams, removeDatasetCiteText } from '@fastgpt/global/core/ai/llm/utils';
import { getAIApi } from '../config';
import type { OpenaiAccountType } from '@fastgpt/global/support/user/team/type';
import { customNanoid, getNanoid } from '@fastgpt/global/common/string/tools';
@@ -772,6 +772,21 @@ const llmCompletionsBodyFormat = async <T extends CompletionsBodyType>({
Object.entries(requestBody).filter(([_, value]) => value !== null && value !== undefined)
) as T;
const supportParams = getLLMSupportParams(modelData);
if (!supportParams.temperature) {
delete requestBody.temperature;
}
if (!supportParams.topP) {
delete requestBody.top_p;
}
if (!supportParams.stop) {
delete requestBody.stop;
}
if (!supportParams.responseFormat) {
delete requestBody.response_format;
}
// field map
if (modelData.fieldMap) {
Object.entries(modelData.fieldMap).forEach(([sourceKey, targetKey]) => {
@@ -0,0 +1,197 @@
import {
generateSandboxId,
SandboxStatusEnum,
SANDBOX_SUSPEND_MINUTES
} from '@fastgpt/global/core/ai/sandbox/constants';
import { env } from '../../../env';
import { MongoSandboxInstance } from './schema';
import {
createSandbox,
type ExecuteResult,
type ISandbox,
type ResourceLimits
} from '@fastgpt-sdk/sandbox-adapter';
import { getLogger, LogCategories } from '../../../common/logger';
import { setCron } from '../../../common/system/cron';
import { subMinutes } from 'date-fns';
import { batchRun } from '@fastgpt/global/common/system/utils';
import { getErrText } from '@fastgpt/global/common/error/utils';
const logger = getLogger(LogCategories.MODULE.AI.SANDBOX);
type UnionIdType = {
appId: string;
userId: string;
chatId: string;
};
export class SandboxClient {
private appId?: string;
private userId?: string;
private chatId?: string;
private sandboxId: string;
readonly provider: ISandbox;
constructor(
props:
| {
sandboxId: string;
}
| UnionIdType,
opts: {
resourceLimits?: ResourceLimits;
} = {}
) {
if ('sandboxId' in props) {
this.sandboxId = props.sandboxId;
} else {
this.appId = props.appId;
this.userId = props.userId;
this.chatId = props.chatId;
this.sandboxId = generateSandboxId(this.appId, this.userId, this.chatId);
}
const providerName = env.AGENT_SANDBOX_PROVIDER;
const params = (() => {
if (providerName === 'sealosdevbox') {
if (!env.AGENT_SANDBOX_SEALOS_BASEURL || !env.AGENT_SANDBOX_SEALOS_TOKEN) {
throw new Error('AGENT_SANDBOX_SEALOS_BASEURL / AGENT_SANDBOX_SEALOS_TOKEN required');
}
return {
provider: 'sealosdevbox' as const,
config: {
baseUrl: env.AGENT_SANDBOX_SEALOS_BASEURL,
token: env.AGENT_SANDBOX_SEALOS_TOKEN,
sandboxId: this.sandboxId
},
createConfig: undefined
};
} else if (!providerName) {
throw new Error(
'AGENT_SANDBOX_PROVIDER is not configured. Please set it in your environment variables.'
);
} else {
throw new Error(`Unsupported sandbox provider: ${env.AGENT_SANDBOX_PROVIDER}`);
}
})();
this.provider = createSandbox(params.provider, params.config, params.createConfig);
}
async ensureAvailable() {
await MongoSandboxInstance.findOneAndUpdate(
{ provider: this.provider.provider, sandboxId: this.sandboxId },
{
$set: {
status: SandboxStatusEnum.running,
lastActiveAt: new Date()
},
$setOnInsert: {
...(this.appId ? { appId: this.appId } : {}),
...(this.userId ? { userId: this.userId } : {}),
...(this.chatId ? { chatId: this.chatId } : {}),
createdAt: new Date()
}
},
{ upsert: true }
);
await this.provider.ensureRunning();
}
async exec(command: string, timeout?: number): Promise<ExecuteResult> {
try {
await this.ensureAvailable();
} catch (err) {
logger.error('Failed to ensure sandbox available', { sandboxId: this.sandboxId, error: err });
return {
stdout: '',
stderr: `Sandbox service is not available: ${getErrText(err)}`,
exitCode: -1
};
}
return await this.provider
.execute(command, {
timeoutMs: timeout ? timeout * 1000 : undefined
})
.catch((err) => {
logger.error('Failed to execute sandbox', { sandboxId: this.sandboxId, error: err });
return {
stdout: '',
stderr: `Failed to execute sandbox: ${getErrText(err)}`,
exitCode: -1
};
});
}
async delete() {
await this.provider.delete();
await MongoSandboxInstance.deleteOne({ sandboxId: this.sandboxId });
}
async stop() {
await this.provider.stop();
await MongoSandboxInstance.updateOne(
{ sandboxId: this.sandboxId },
{ $set: { status: SandboxStatusEnum.stoped } }
);
}
}
// ==== Delete Sandboxes ====
export const deleteSandboxesByChatIds = async ({
appId,
chatIds
}: {
appId: string;
chatIds: string[];
}) => {
const instances = await MongoSandboxInstance.find({ appId, chatId: { $in: chatIds } }).lean();
if (!instances.length) return;
await Promise.allSettled(
instances.map((doc) =>
new SandboxClient({
sandboxId: doc.sandboxId
})
.delete()
.catch((err) => {
logger.error('Failed to delete sandbox', { sandboxId: doc.sandboxId, error: err });
})
)
);
};
export const deleteSandboxesByAppId = async (appId: string) => {
const instances = await MongoSandboxInstance.find({ appId }).lean();
if (!instances.length) return;
await Promise.allSettled(
instances.map((doc) =>
new SandboxClient({
sandboxId: doc.sandboxId
}).delete()
)
);
};
// 5 分钟检查一遍,暂停
export const cronJob = async () => {
setCron('*/5 * * * *', async () => {
const instances = await MongoSandboxInstance.find({
status: SandboxStatusEnum.running,
lastActiveAt: { $lt: subMinutes(new Date(), SANDBOX_SUSPEND_MINUTES) }
}).lean();
if (!instances.length) return;
logger.info('Found running sandboxes inactive > 5 min', { count: instances.length });
await batchRun(instances, (doc) =>
new SandboxClient({
sandboxId: doc.sandboxId
})
.stop()
.catch((error) => {
logger.error('Failed to stop sandbox', { sandboxId: doc.sandboxId, error });
})
);
});
};
@@ -0,0 +1 @@
export type { ISandbox } from '@fastgpt-sdk/sandbox-adapter';
@@ -0,0 +1,67 @@
import { connectionMongo, getMongoModel } from '../../../common/mongo';
const { Schema } = connectionMongo;
import type { SandboxInstanceSchemaType } from './type';
import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants';
import { AppCollectionName } from '../../app/schema';
import { SandboxLimitSchema, SandboxProviderSchema } from './type';
export const collectionName = 'agent_sandbox_instances';
const SandboxInstanceSchema = new Schema({
provider: {
type: String,
enum: SandboxProviderSchema.options,
required: true
},
// 唯一 idchat 模式下,由 3 个 id hash 获取。
sandboxId: {
type: String,
required: true
},
// Chat 模式下会关联会话。Skill editor 不需要 appId,userId,chatId
appId: {
type: Schema.Types.ObjectId,
ref: AppCollectionName
},
userId: String,
chatId: String,
status: {
type: String,
enum: Object.values(SandboxStatusEnum),
default: SandboxStatusEnum.running,
required: true
},
lastActiveAt: {
type: Date,
default: () => new Date(),
required: true
},
createdAt: {
type: Date,
default: () => new Date(),
required: true
},
limit: {
type: SandboxLimitSchema.shape
}
});
SandboxInstanceSchema.index(
{ appId: 1, userId: 1, chatId: 1 },
{
unique: true,
partialFilterExpression: {
appId: { $exists: true, $ne: null },
userId: { $exists: true, $ne: null },
chatId: { $exists: true, $ne: null }
}
}
);
SandboxInstanceSchema.index({ status: 1, lastActiveAt: 1 });
SandboxInstanceSchema.index({ provider: 1, sandboxId: 1 }, { unique: true });
export const MongoSandboxInstance = getMongoModel<SandboxInstanceSchemaType>(
collectionName,
SandboxInstanceSchema
);
+26
View File
@@ -0,0 +1,26 @@
import z from 'zod';
import { SandboxStatusEnum } from '@fastgpt/global/core/ai/sandbox/constants';
// ---- 沙盒实例 DB 类型 ----
export const SandboxProviderSchema = z.enum(['sealosdevbox']);
export type SandboxProviderType = z.infer<typeof SandboxProviderSchema>;
export const SandboxLimitSchema = z.object({
cpuCount: z.number(),
memoryMiB: z.number(),
diskGiB: z.number()
});
export const SandboxInstanceZodSchema = z.object({
_id: z.string(),
sandboxId: z.string(),
appId: z.string().nullish(),
userId: z.string().nullish(),
chatId: z.string().nullish(),
status: z.enum(SandboxStatusEnum),
lastActiveAt: z.date(),
createdAt: z.date(),
limit: SandboxLimitSchema.nullish(),
provider: SandboxProviderSchema
});
export type SandboxInstanceSchemaType = z.infer<typeof SandboxInstanceZodSchema>;
+4
View File
@@ -30,6 +30,7 @@ import { MongoMcpKey } from '../../support/mcp/schema';
import { MongoAppRecord } from './record/schema';
import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { getLogger, LogCategories } from '../../common/logger';
import { deleteSandboxesByAppId } from '../ai/sandbox/controller';
const logger = getLogger(LogCategories.MODULE.APP.FOLDER);
@@ -152,7 +153,10 @@ export const deleteAppDataProcessor = async ({
// 1. 删除应用头像
await removeImageByPath(app.avatar);
// 2. 删除聊天记录和S3文件
// 删除沙盒实例
await deleteSandboxesByAppId(appId);
await getS3ChatSource().deleteChatFilesByPrefix({ appId });
await MongoAppChatLog.deleteMany({ teamId, appId });
await MongoChatItemResponse.deleteMany({ appId });
@@ -0,0 +1,93 @@
import { getWorker, QueueNames, getQueue, type Job } from '../../../common/bullmq';
import { MongoDatasetCollection } from './schema';
import { getLogger, LogCategories } from '../../../common/logger';
const logger = getLogger(LogCategories.MODULE.DATASET.COLLECTION);
export type CollectionUpdateJobData = {
teamId: string;
datasetId: string;
collectionId: string;
};
/**
* Initialize Collection Update Worker
* This worker handles collection updates (updateTime, etc.) with debounce mechanism
*/
export const initCollectionUpdateWorker = () => {
const worker = getWorker<CollectionUpdateJobData>(
QueueNames.collectionUpdate,
async (job: Job<CollectionUpdateJobData>) => {
const { collectionId } = job.data;
try {
// Update collection updateTime and other operations
await MongoDatasetCollection.updateOne(
{
_id: collectionId
},
{
$set: {
updateTime: new Date()
// TODO: 更新统计数据
}
}
);
logger.debug('Collection updated', {
collectionId
});
} catch (error) {
logger.error('Failed to update collection', {
collectionId,
error
});
throw error;
}
},
{
concurrency: 3, // Process 3 jobs concurrently
removeOnComplete: {
count: 0 // Remove completed jobs immediately
},
removeOnFail: {
count: 1000, // Keep last 1000 failed jobs
age: 30 * 24 * 60 * 60 // Keep failed jobs for 30 days (in seconds)
}
}
);
logger.info('Collection Update worker initialized');
return worker;
};
/**
* Push collection update job to queue with debounce
* @param collectionId - Collection ID
* @param datasetId - Dataset ID
* @param teamId - Team ID
* @param delay - Delay in milliseconds (default: 5000ms = 5s)
*/
export const pushCollectionUpdateJob = async (data: CollectionUpdateJobData) => {
const queue = getQueue<CollectionUpdateJobData>(QueueNames.collectionUpdate);
// Use jobId to ensure only one job per collection (debounce mechanism)
// If a job with the same jobId already exists, it will be replaced
const jobId = `collection-update-${data.collectionId}`;
try {
await queue.add('updateCollection', data, {
jobId, // Unique job ID for debounce
delay: 5000 // Delay execution by 5 seconds
});
logger.debug('Collection update job pushed', {
collectionId: data.collectionId
});
} catch (error) {
logger.error('Failed to push collection update job', {
collectionId: data.collectionId,
error
});
}
};
@@ -49,6 +49,9 @@ export type DispatchAgentModuleProps = ModuleDispatchProps<{
// Knowledge base search configuration
[NodeInputKeyEnum.datasetParams]?: AppFormEditFormType['dataset'];
// Sandbox (Computer Use)
[NodeInputKeyEnum.useAgentSandbox]?: boolean;
}>;
type Response = DispatchNodeResultType<{
@@ -85,7 +88,9 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
fileUrlList: fileLinks,
agent_selectedTools: selectedTools = [],
// Dataset search configuration
agent_datasetParams: datasetParams
agent_datasetParams: datasetParams,
// Sandbox (Computer Use)
useAgentSandbox = false
}
} = props;
const chatHistories = getHistories(history, histories);
@@ -162,7 +167,8 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
lang,
getPlanTool: true,
hasDataset: datasetParams && datasetParams.datasets.length > 0,
hasFiles: !!chatConfig?.fileSelectConfig?.canSelectFile
hasFiles: !!chatConfig?.fileSelectConfig?.canSelectFile,
useAgentSandbox
}
);
@@ -14,6 +14,7 @@ import { dispatchTool } from '../sub/tool';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { DatasetSearchToolSchema } from '../sub/dataset/utils';
import { dispatchAgentDatasetSearch } from '../sub/dataset';
import { dispatchSandboxShell } from '../sub/sandbox';
import type { DispatchAgentModuleProps } from '..';
import { getLLMModel } from '../../../../../ai/model';
import { getStepCallQuery, getStepDependon } from './dependon';
@@ -31,6 +32,10 @@ import { PlanAgentParamsSchema } from '../sub/plan/constants';
import { filterMemoryMessages } from '../../utils';
import { dispatchApp, dispatchPlugin } from '../sub/app';
import { getLogger, LogCategories } from '../../../../../../common/logger';
import {
SandboxShellToolSchema,
SANDBOX_TOOL_NAME
} from '@fastgpt/global/core/ai/sandbox/constants';
type Response = {
stepResponse?: {
@@ -85,7 +90,9 @@ export const masterCall = async ({
params: {
model,
// Dataset search configuration
agent_datasetParams: datasetParams
agent_datasetParams: datasetParams,
// Sandbox (Computer Use)
useAgentSandbox = false
}
} = props;
@@ -177,7 +184,11 @@ export const masterCall = async ({
const messages: ChatCompletionMessageParam[] = [
{
role: 'system' as const,
content: getMasterSystemPrompt(systemPrompt, hasUserTools)
content: getMasterSystemPrompt({
systemPrompt,
hasUserTools,
useAgentSandbox
})
},
...masterMessages
];
@@ -365,6 +376,33 @@ export const masterCall = async ({
usages: result.usages
};
}
if (toolId === SANDBOX_TOOL_NAME) {
const toolParams = SandboxShellToolSchema.safeParse(
parseJsonArgs(call.function.arguments)
);
if (!toolParams.success) {
return {
response: toolParams.error.message,
usages: []
};
}
const result = await dispatchSandboxShell({
command: toolParams.data.command,
timeout: toolParams.data.timeout,
appId: runningAppInfo.id,
userId: props.uid,
chatId,
lang: props.lang
});
childrenResponses.push(result.nodeResponse);
return {
response: result.response,
usages: result.usages
};
}
if (toolId === SubAppIds.plan) {
try {
const toolArgs = await PlanAgentParamsSchema.safeParseAsync(
@@ -1,6 +1,15 @@
import { SubAppIds } from '@fastgpt/global/core/workflow/node/agent/constants';
import { SANDBOX_SYSTEM_PROMPT } from '@fastgpt/global/core/ai/sandbox/constants';
export const getMasterSystemPrompt = (systemPrompt?: string, hasUserTools: boolean = true) => {
export const getMasterSystemPrompt = ({
systemPrompt,
hasUserTools,
useAgentSandbox
}: {
systemPrompt?: string;
hasUserTools: boolean;
useAgentSandbox: boolean;
}) => {
return `<!-- Master Agent 决策系统 -->
<role>
@@ -17,6 +26,15 @@ ${systemPrompt}
: ''
}
${
useAgentSandbox
? `
<sandbox_environment>
${SANDBOX_SYSTEM_PROMPT}
</sandbox_environment>
`
: ''
}
<decision_paths>
三种执行路径:
@@ -0,0 +1,103 @@
import { SandboxClient } from '../../../../../../ai/sandbox/controller';
import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { getLogger, LogCategories } from '../../../../../../../common/logger';
import {
SANDBOX_ICON,
SANDBOX_NAME,
SANDBOX_TOOL_NAME
} from '@fastgpt/global/core/ai/sandbox/constants';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import type { localeType } from '@fastgpt/global/common/i18n/type';
type SandboxShellParams = {
command: string;
timeout?: number;
appId: string;
userId: string;
chatId: string;
lang?: localeType;
};
export const dispatchSandboxShell = async ({
command,
timeout,
appId,
userId,
chatId,
lang
}: SandboxShellParams): Promise<{
response: string;
usages: ChatNodeUsageType[];
nodeResponse: ChatHistoryItemResType;
}> => {
const startTime = Date.now();
const nodeId = getNanoid(6);
const moduleName = parseI18nString(SANDBOX_NAME, lang);
try {
const sandboxInstance = new SandboxClient({
appId,
userId,
chatId
});
const result = await sandboxInstance.exec(command, timeout);
const response = JSON.stringify({
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode
});
getLogger(LogCategories.MODULE.AI.AGENT).info('[Sandbox Shell] Command executed', {
command,
exitCode: result.exitCode,
stdoutLength: result.stdout?.length || 0,
stderrLength: result.stderr?.length || 0
});
return {
response,
usages: [],
nodeResponse: {
nodeId,
id: nodeId,
moduleType: FlowNodeTypeEnum.tool,
moduleName,
moduleLogo: SANDBOX_ICON,
toolId: SANDBOX_TOOL_NAME,
toolInput: { command, timeout },
toolRes: response,
totalPoints: 0,
runningTime: +((Date.now() - startTime) / 1000).toFixed(2)
}
};
} catch (error) {
getLogger(LogCategories.MODULE.AI.AGENT).error('[Sandbox Shell] Execution failed', { error });
const errorResponse = JSON.stringify({
stdout: '',
stderr: getErrText(error),
exitCode: -1
});
return {
response: errorResponse,
usages: [],
nodeResponse: {
nodeId,
id: nodeId,
moduleType: FlowNodeTypeEnum.tool,
moduleName,
moduleLogo: SANDBOX_ICON,
toolInput: { command, timeout },
toolRes: errorResponse,
totalPoints: 0,
runningTime: +((Date.now() - startTime) / 1000).toFixed(2)
}
};
}
};
@@ -1,11 +1,12 @@
import type { localeType } from '@fastgpt/global/common/i18n/type';
import type { SkillToolType } from '@fastgpt/global/core/ai/skill/type';
import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type';
import type { SubAppRuntimeType } from './type';
import { getAgentRuntimeTools } from './sub/tool/utils';
import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type';
import { readFileTool } from './sub/file/utils';
import { PlanAgentTool } from './sub/plan/constants';
import { datasetSearchTool } from './sub/dataset/utils';
import { SANDBOX_TOOLS } from '@fastgpt/global/core/ai/sandbox/constants';
export const getSubapps = async ({
tmbId,
@@ -13,7 +14,8 @@ export const getSubapps = async ({
lang,
getPlanTool,
hasDataset,
hasFiles
hasFiles,
useAgentSandbox
}: {
tmbId: string;
tools: SkillToolType[];
@@ -21,6 +23,7 @@ export const getSubapps = async ({
getPlanTool?: Boolean;
hasDataset?: boolean;
hasFiles: boolean;
useAgentSandbox: boolean;
}): Promise<{
completionTools: ChatCompletionTool[];
subAppsMap: Map<string, SubAppRuntimeType>;
@@ -42,6 +45,11 @@ export const getSubapps = async ({
completionTools.push(datasetSearchTool);
}
/* Sandbox Shell */
if (useAgentSandbox) {
completionTools.push(...SANDBOX_TOOLS);
}
/* System tool */
const formatTools = await getAgentRuntimeTools({
tools,
@@ -1,4 +1,8 @@
import { replaceVariable } from '@fastgpt/global/common/string/tools';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import type { ChildResponseItemType } from './type';
import { SANDBOX_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/constants';
export const getMultiplePrompt = (obj: {
fileCount: number;
@@ -12,3 +16,36 @@ Image{{imgCount}}
{{question}}`;
return replaceVariable(prompt, obj);
};
export const getSandboxToolWorkflowResponse = ({
name,
logo,
input,
response,
durationSeconds
}: {
name: string;
logo: string;
input: Record<string, any>;
response: string;
durationSeconds: number;
}): ChildResponseItemType => {
return {
flowResponses: [
{
moduleName: name,
moduleType: FlowNodeTypeEnum.tool,
moduleLogo: logo,
toolId: SANDBOX_TOOL_NAME,
toolInput: input,
toolRes: response,
totalPoints: 0,
id: getNanoid(),
nodeId: getNanoid(),
runningTime: durationSeconds
}
],
flowUsages: [],
runTimes: 0
};
};
@@ -62,7 +62,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
fileUrlList: fileLinks,
aiChatVision,
aiChatReasoning,
isResponseAnswerText = true
isResponseAnswerText = true,
useAgentSandbox = false
}
} = props;
@@ -220,7 +221,8 @@ export const dispatchRunTools = async (props: DispatchToolModuleProps): Promise<
toolModel,
messages: adaptMessages,
childrenInteractiveParams:
lastInteractive?.type === 'toolChildrenInteractive' ? lastInteractive.params : undefined
lastInteractive?.type === 'toolChildrenInteractive' ? lastInteractive.params : undefined,
useAgentSandbox
});
})();
@@ -6,8 +6,7 @@ import type {
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils';
import { runWorkflow } from '../../index';
import type { DispatchToolModuleProps, ToolNodeItemType } from './type';
import type { DispatchFlowResponse } from '../../type';
import type { ChildResponseItemType, DispatchToolModuleProps, ToolNodeItemType } from './type';
import { chats2GPTMessages, GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import type { AIChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { formatToolResponse, initToolCallEdges, initToolNodes } from './utils';
@@ -18,11 +17,22 @@ import { toolValueTypeList, valueTypeJsonSchemaMap } from '@fastgpt/global/core/
import { runAgentCall } from '../../../../ai/llm/agentCall';
import type { ToolCallChildrenInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import type { JsonSchemaPropertiesItemType } from '@fastgpt/global/core/app/jsonschema';
import {
SANDBOX_SHELL_TOOL,
SandboxShellToolSchema,
SANDBOX_SYSTEM_PROMPT,
SANDBOX_ICON,
SANDBOX_NAME,
SANDBOX_TOOL_NAME
} from '@fastgpt/global/core/ai/sandbox/constants';
import { SandboxClient } from '../../../../ai/sandbox/controller';
import { getSandboxToolWorkflowResponse } from './constants';
import { getErrText } from '@fastgpt/global/common/error/utils';
type ResponseType = {
requestIds: string[];
error?: string;
toolDispatchFlowResponses: DispatchFlowResponse[];
toolDispatchFlowResponses: ChildResponseItemType[];
toolCallInputTokens: number;
toolCallOutputTokens: number;
completeMessages: ChatCompletionMessageParam[];
@@ -31,8 +41,17 @@ type ResponseType = {
toolWorkflowInteractiveResponse?: ToolCallChildrenInteractive;
};
export const runToolCall = async (props: DispatchToolModuleProps): Promise<ResponseType> => {
const { messages, toolNodes, toolModel, childrenInteractiveParams, ...workflowProps } = props;
export const runToolCall = async (
props: DispatchToolModuleProps & { useAgentSandbox?: boolean }
): Promise<ResponseType> => {
const {
messages,
toolNodes,
toolModel,
childrenInteractiveParams,
useAgentSandbox,
...workflowProps
} = props;
const {
res,
checkIsStopping,
@@ -97,16 +116,40 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
}
};
});
// 注入 sandbox_shell 工具和提示词
let finalMessages = messages;
if (useAgentSandbox) {
tools.push(SANDBOX_SHELL_TOOL);
const systemMessage = messages.find((m) => m.role === 'system');
if (systemMessage) {
finalMessages = messages.map((m) =>
m.role === 'system' ? { ...m, content: `${m.content}\n\n${SANDBOX_SYSTEM_PROMPT}` } : m
);
} else {
finalMessages = [{ role: 'system', content: SANDBOX_SYSTEM_PROMPT }, ...messages];
}
}
const getToolInfo = (name: string) => {
if (name === SANDBOX_TOOL_NAME) {
return {
name: SANDBOX_NAME[workflowProps.lang || 'zh-CN'] || SANDBOX_TOOL_NAME,
avatar: SANDBOX_ICON
};
}
const toolNode = toolNodesMap.get(name);
return {
name: toolNode?.name || '',
avatar: toolNode?.avatar || ''
avatar: toolNode?.avatar || '',
rawData: toolNode
};
};
// 工具响应原始值
const toolRunResponses: DispatchFlowResponse[] = [];
const toolRunResponses: ChildResponseItemType[] = [];
const {
inputTokens,
@@ -120,7 +163,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
} = await runAgentCall({
maxRunAgentTimes: 50,
body: {
messages,
messages: finalMessages,
tools,
model: toolModel.model,
max_tokens: maxToken,
@@ -158,7 +201,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
},
onToolCall({ call }) {
if (!isResponseAnswerText) return;
const toolNode = toolNodesMap.get(call.function.name);
const toolNode = getToolInfo(call.function.name);
if (toolNode) {
workflowStreamResponse?.({
id: call.id,
@@ -169,8 +212,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
toolName: toolNode.name,
toolAvatar: toolNode.avatar,
functionName: call.function.name,
params: call.function.arguments ?? '',
response: ''
params: call.function.arguments ?? ''
}
}
});
@@ -186,38 +228,101 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
id: tool.id,
toolName: '',
toolAvatar: '',
params,
response: ''
params
}
}
});
},
handleToolResponse: async ({ call, messages }) => {
const toolNode = toolNodesMap.get(call.function?.name);
const tool = getToolInfo(call.function?.name);
const startTime = Date.now();
if (!toolNode) {
return {
response: 'Call tool not found',
assistantMessages: [],
usages: [],
interactive: undefined
};
}
const {
response,
flowResponse,
assistantMessages = [],
usages = [],
interactive,
stop
} = await (async () => {
// 拦截 sandbox_shell 调用
if (call.function?.name === SANDBOX_TOOL_NAME) {
try {
const params = SandboxShellToolSchema.parse(parseJsonArgs(call.function.arguments));
// Init tool params and run
const startParams = parseJsonArgs(call.function.arguments);
initToolNodes(runtimeNodes, [toolNode.nodeId], startParams);
initToolCallEdges(runtimeEdges, [toolNode.nodeId]);
const instance = new SandboxClient({
appId: String(workflowProps.runningAppInfo.id),
userId: String(workflowProps.uid),
chatId: workflowProps.chatId
});
const toolRunResponse = await runWorkflow({
...workflowProps,
runtimeNodes,
usageId: undefined,
isToolCall: true
});
const result = await instance.exec(params.command, params.timeout);
// Format tool response
const stringToolResponse = formatToolResponse(toolRunResponse.toolResponses);
const stringToolResponse = JSON.stringify({
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode
});
const flowResponse = getSandboxToolWorkflowResponse({
name: tool.name,
logo: SANDBOX_ICON,
input: params,
response: stringToolResponse,
durationSeconds: +((Date.now() - startTime) / 1000).toFixed(2)
});
return {
response: stringToolResponse,
flowResponse
};
} catch (error) {
return {
response: `Sandbox execution error: ${getErrText(error)}`
};
}
} else {
const toolNode = tool?.rawData;
if (!toolNode) {
return {
response: 'Call tool not found'
};
}
// Init tool params and run
const startParams = parseJsonArgs(call.function.arguments);
initToolNodes(runtimeNodes, [toolNode.nodeId], startParams);
initToolCallEdges(runtimeEdges, [toolNode.nodeId]);
const toolRunResponse = await runWorkflow({
...workflowProps,
runtimeNodes,
usageId: undefined,
isToolCall: true
});
// Format tool response
const stringToolResponse = formatToolResponse(toolRunResponse.toolResponses);
return {
response: stringToolResponse,
flowResponse: toolRunResponse,
assistantMessages: chats2GPTMessages({
messages: [
{
obj: ChatRoleEnum.AI,
value: toolRunResponse.assistantResponses
}
],
reserveId: false
}),
usages: toolRunResponse.flowUsages,
interactive: toolRunResponse.workflowInteractiveResponse,
stop: toolRunResponse.flowResponses?.some((item) => item.toolStop)
};
}
})();
if (isResponseAnswerText) {
workflowStreamResponse?.({
@@ -229,30 +334,22 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
toolName: '',
toolAvatar: '',
params: '',
response: sliceStrStartEnd(stringToolResponse, 5000, 5000)
response: sliceStrStartEnd(response, 5000, 5000)
}
}
});
}
toolRunResponses.push(toolRunResponse);
const assistantMessages = chats2GPTMessages({
messages: [
{
obj: ChatRoleEnum.AI,
value: toolRunResponse.assistantResponses
}
],
reserveId: false
});
if (flowResponse) {
toolRunResponses.push(flowResponse);
}
return {
response: stringToolResponse,
response,
assistantMessages,
usages: toolRunResponse.flowUsages,
interactive: toolRunResponse.workflowInteractiveResponse,
stop: toolRunResponse.flowResponses?.some((item) => item.toolStop)
usages,
interactive,
stop
};
},
childrenInteractiveParams,
@@ -27,6 +27,7 @@ export type DispatchToolModuleProps = ModuleDispatchProps<{
[NodeInputKeyEnum.aiChatStopSign]?: string;
[NodeInputKeyEnum.aiChatResponseFormat]?: string;
[NodeInputKeyEnum.aiChatJsonSchema]?: string;
[NodeInputKeyEnum.useAgentSandbox]?: boolean;
}> & {
messages: ChatCompletionMessageParam[];
toolNodes: ToolNodeItemType[];
@@ -38,3 +39,9 @@ export type ToolNodeItemType = RuntimeNodeItemType & {
toolParams: RuntimeNodeItemType['inputs'];
jsonSchema?: JSONSchemaInputType;
};
export type ChildResponseItemType = {
flowResponses: DispatchFlowResponse['flowResponses'];
runTimes: DispatchFlowResponse['runTimes'];
flowUsages: DispatchFlowResponse['flowUsages'];
};
@@ -78,6 +78,6 @@ export const callbackMap: Record<FlowNodeTypeEnum, Function> = {
[FlowNodeTypeEnum.comment]: () => Promise.resolve(),
[FlowNodeTypeEnum.toolSet]: () => Promise.resolve(),
// @deprecated
/** @deprecated */
[FlowNodeTypeEnum.runApp]: dispatchAppRequest
};
@@ -29,13 +29,13 @@ export const dispatchCodeSandbox = async (props: RunCodeType): Promise<RunCodeRe
params: { codeType, code, [NodeInputKeyEnum.addInputParam]: customVariables }
} = props;
if (!process.env.SANDBOX_URL) {
if (!process.env.CODE_SANDBOX_URL) {
return {
error: {
[NodeOutputKeyEnum.error]: 'Can not find SANDBOX_URL in env'
[NodeOutputKeyEnum.error]: 'Can not find CODE_SANDBOX_URL in env'
},
[DispatchNodeResponseKeyEnum.nodeResponse]: {
errorText: 'Can not find SANDBOX_URL in env',
errorText: 'Can not find CODE_SANDBOX_URL in env',
customInputs: customVariables
}
};