Commit 7a660139 by Archer Committed by GitHub

perf: agent pause (#6588)

* doc

* feat: Pause Recovery (#6494)

* feat: Pause Recovery

* agent pause

* agent pause

* fix:agent pause

* fix:agent pause

* perf: pause agent call

* fix: test

---------

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

* fix: image read and json error (Agent) (#6502)

* fix:
1.image read
2.JSON parsing error

* dataset cite and pause

* perf: plancall second parse

* add test

---------

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

* master message

* remove invalid code

* fix: pause agent (#6595)

* fix: ask and step result

* delete console

* udpate pnpm version

* prettier

---------

Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com>
parent ec7a8beb
......@@ -20,7 +20,7 @@ jobs:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- uses: pnpm/action-setup@v4
with:
version: 10
version: 9
- name: 'Install Deps'
run: pnpm install
- name: 'Test'
......
......@@ -20,7 +20,7 @@ jobs:
- uses: pnpm/action-setup@v4
with:
version: 10
version: 9
- uses: actions/setup-node@v4
with:
......
'use client';
import { Check, Link as LinkIcon } from 'lucide-react';
import { ComponentProps, type ReactNode, useEffect, useRef, useState } from 'react';
import type { ComponentProps} from 'react';
import { type ReactNode, useEffect, useRef, useState } from 'react';
import { cn } from '../lib/cn';
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';
import { buttonVariants } from './ui/button';
......
......@@ -27,7 +27,7 @@ AGENT_SANDBOX_SEALOS_TOKEN=
1. 新增 AI 虚拟机功能,可以给 AI 挂载一个虚拟机工具进行更丰富的操作。
2. 封装 logger sdk。
3. 更新知识库数据时候,同步更新 collection 更新时间。
3. 更新知识库单个数据时,同步更新 collection 更新时间。
4. 表单输入文件时,支持打开文件进行预览。
## ⚙️ 优化
......
......@@ -147,10 +147,10 @@
"document/content/docs/openapi/share.mdx": "2026-02-12T18:45:30+08:00",
"document/content/docs/self-host/config/json.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/config/json.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/config/model/intro.en.mdx": "2026-03-19T13:55:19+08:00",
"document/content/docs/self-host/config/model/intro.mdx": "2026-03-19T13:55:19+08:00",
"document/content/docs/self-host/config/model/siliconCloud.en.mdx": "2026-03-19T13:55:19+08:00",
"document/content/docs/self-host/config/model/siliconCloud.mdx": "2026-03-19T13:55:19+08:00",
"document/content/docs/self-host/config/model/intro.en.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/self-host/config/model/intro.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/self-host/config/model/siliconCloud.en.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/self-host/config/model/siliconCloud.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/self-host/config/object-storage.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/config/object-storage.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/config/signoz.en.mdx": "2026-03-03T17:39:47+08:00",
......@@ -171,8 +171,8 @@
"document/content/docs/self-host/custom-models/ollama.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/custom-models/xinference.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/custom-models/xinference.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/deploy/docker.en.mdx": "2026-03-19T13:55:19+08:00",
"document/content/docs/self-host/deploy/docker.mdx": "2026-03-19T13:55:19+08:00",
"document/content/docs/self-host/deploy/docker.en.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/self-host/deploy/docker.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/self-host/deploy/sealos.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/deploy/sealos.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/design/dataset.en.mdx": "2026-03-03T17:39:47+08:00",
......@@ -180,7 +180,7 @@
"document/content/docs/self-host/design/design_plugin.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/design/design_plugin.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/dev.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/dev.mdx": "2026-03-19T11:32:26+08:00",
"document/content/docs/self-host/dev.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/self-host/faq.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/index.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/index.mdx": "2026-03-03T17:39:47+08:00",
......@@ -197,7 +197,7 @@
"document/content/docs/self-host/troubleshooting/model-errors.en.mdx": "2026-03-19T11:32:14+08:00",
"document/content/docs/self-host/troubleshooting/model-errors.mdx": "2026-03-19T11:32:14+08:00",
"document/content/docs/self-host/troubleshooting/s3-issues.en.mdx": "2026-03-17T14:44:54+08:00",
"document/content/docs/self-host/troubleshooting/s3-issues.mdx": "2026-03-19T13:55:19+08:00",
"document/content/docs/self-host/troubleshooting/s3-issues.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/self-host/upgrading/4-12/4120.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-12/4120.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-12/4121.en.mdx": "2026-03-03T17:39:47+08:00",
......@@ -236,7 +236,7 @@
"document/content/docs/self-host/upgrading/4-14/4148.mdx": "2026-03-09T17:39:53+08:00",
"document/content/docs/self-host/upgrading/4-14/41481.en.mdx": "2026-03-09T12:02:02+08:00",
"document/content/docs/self-host/upgrading/4-14/41481.mdx": "2026-03-09T17:39:53+08:00",
"document/content/docs/self-host/upgrading/4-14/4149.mdx": "2026-03-19T11:21:58+08:00",
"document/content/docs/self-host/upgrading/4-14/4149.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/self-host/upgrading/outdated/40.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/40.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/41.en.mdx": "2026-03-03T17:39:47+08:00",
......@@ -377,8 +377,8 @@
"document/content/docs/self-host/upgrading/outdated/499.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/upgrade-intruction.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/upgrade-intruction.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/toc.en.mdx": "2026-03-19T13:55:19+08:00",
"document/content/docs/toc.mdx": "2026-03-19T13:55:19+08:00",
"document/content/docs/toc.en.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/toc.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/use-cases/app-cases/dalle3.en.mdx": "2026-02-26T22:14:30+08:00",
"document/content/docs/use-cases/app-cases/dalle3.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/use-cases/app-cases/english_essay_correction_bot.en.mdx": "2026-02-26T22:14:30+08:00",
......
......@@ -53,6 +53,6 @@
},
"engines": {
"node": ">=20",
"pnpm": "10.x"
"pnpm": "9.x"
}
}
......@@ -19,7 +19,7 @@ import type {
ChatCompletionToolMessageParam
} from '../../core/ai/type';
import { ChatCompletionRequestMessageRoleEnum } from '../../core/ai/constants';
import { getNanoid } from '../../common/string/tools';
import { getPlanCallResponseText } from './utils';
export const GPT2Chat = {
[ChatCompletionRequestMessageRoleEnum.System]: ChatRoleEnum.System,
......@@ -52,7 +52,7 @@ export const chats2GPTMessages = ({
}): ChatCompletionMessageParam[] => {
let results: ChatCompletionMessageParam[] = [];
messages.forEach((item) => {
messages.forEach((item, index) => {
const dataId = reserveId ? item.dataId : undefined;
if (item.obj === ChatRoleEnum.System) {
const content = item.value?.[0]?.text?.content;
......@@ -65,6 +65,8 @@ export const chats2GPTMessages = ({
}
} else if (item.obj === ChatRoleEnum.Human) {
const value = item.value
// 有 planId 的过滤掉,会被 planTool 一起处理
.filter((item) => !item.planId)
.map((item) => {
if (item.text) {
return {
......@@ -93,22 +95,28 @@ export const chats2GPTMessages = ({
})
.filter(Boolean) as ChatCompletionContentPart[];
results.push({
dataId,
hideInUI: item.hideInUI,
role: ChatCompletionRequestMessageRoleEnum.User,
content: simpleUserContentPart(value)
});
if (value.length) {
results.push({
dataId,
hideInUI: item.hideInUI,
role: ChatCompletionRequestMessageRoleEnum.User,
content: simpleUserContentPart(value)
});
}
} else {
const aiResults: ChatCompletionMessageParam[] = [];
const existsPlanId = new Set<string>();
item.value.forEach((value, i) => {
// 只需要把根节点转化即可
if (value.stepId) return;
/* Plan agent 产生的上下文都需要合并到一个 toolCall 里。
Plan agent 产生的上下文都会携带 planId
value.plan 代表的是 plan 的具体内容,根据这个值去转化成 toolcall
*/
if (value.planId && !value.plan) return;
if ((value.tools || value.tool) && reserveTool) {
const tools = value.tools || [value.tool!];
const hasTools = Array.isArray(value.tools) && value.tools.length > 0;
if (reserveTool && (hasTools || value.tool)) {
const tools = hasTools ? value.tools! : [value.tool!];
const tool_calls: ChatCompletionMessageToolCall[] = [];
const toolResponse: ChatCompletionToolMessageParam[] = [];
tools.forEach((tool) => {
......@@ -150,33 +158,25 @@ export const chats2GPTMessages = ({
content: value.text.content
});
}
} else if (value.plan && reserveTool) {
} else if (value.plan) {
// 查找该 Plan 产生的上下文,组成一个 toolcall
// 需要跨所有历史消息收集同 planId 的 values(ask 信息可能在之前的 AI 消息中)
const planId = value.plan.planId;
if (existsPlanId.has(planId)) {
return;
}
existsPlanId.add(planId);
const steps = item.value
.filter((item) => item.plan?.planId === planId)
.flatMap((item) => item.plan?.steps || [])
.map((step) => {
const stepResponse = item.value
.filter((item) => item.stepId === step.id)
?.map((item) => item.text?.content)
.join('\n');
return {
title: step.title,
response: stepResponse
};
});
const toolId = getNanoid(6);
const allPlanValues = messages
.filter((msg) => msg.obj === ChatRoleEnum.AI)
.flatMap((msg) =>
(msg.value as AIChatItemValueItemType[]).filter((v) => v.planId === planId)
);
const planResponseText = getPlanCallResponseText({
plan: value.plan,
assistantResponses: allPlanValues
});
aiResults.push({
dataId,
role: ChatCompletionRequestMessageRoleEnum.Assistant,
tool_calls: [
{
id: toolId,
id: planId,
type: 'function',
function: {
name: 'plan_agent',
......@@ -192,25 +192,11 @@ export const chats2GPTMessages = ({
aiResults.push({
dataId,
role: ChatCompletionRequestMessageRoleEnum.Tool,
tool_call_id: toolId,
content: JSON.stringify(steps)
tool_call_id: planId,
content: planResponseText
});
} else if (value.interactive) {
if (value.interactive.type === 'agentPlanAskQuery') {
aiResults.push({
dataId,
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: value.interactive.params.content
});
} else if (value.interactive.type === 'agentPlanAskUserForm') {
aiResults.push({
dataId,
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: `${value.interactive.params.description}
Answer: ${value.interactive.params.inputForm.map((item) => `- ${item.label}: ${item.value}`).join('\n')}`
});
}
// 目前只有 plan 里会有交互,所以这里暂时不需要处理
}
});
......
......@@ -85,6 +85,7 @@ export const UserChatItemFileItemSchema = z.object({
export type UserChatItemFileItemType = z.infer<typeof UserChatItemFileItemSchema>;
export const UserChatItemValueItemSchema = z.object({
planId: z.string().nullish(),
text: z
.object({
content: z.string()
......@@ -130,6 +131,7 @@ export type AdminFbkType = z.infer<typeof AdminFbkSchema>;
export const AIChatItemValueSchema = z.object({
id: z.string().nullish(),
stepId: z.string().nullish(),
planId: z.string().nullish(),
text: z
.object({
content: z.string()
......
......@@ -12,6 +12,88 @@ import { PublishChannelEnum } from '../../support/outLink/constant';
import { removeDatasetCiteText } from '../ai/llm/utils';
import type { WorkflowInteractiveResponseType } from '../workflow/template/system/interactive/type';
import { ConfirmPlanAgentText } from '../workflow/runtime/constants';
import type { AgentPlanType } from '../ai/agent/type';
export type PlanAskInfo = {
question: string;
answer: string;
};
export const getPlanCallResponseText = ({
plan,
assistantResponses
}: {
plan: AgentPlanType;
assistantResponses: AIChatItemValueItemType[];
}): string => {
// 1. 获取 ask 信息
const askText = (() => {
const asks = assistantResponses
.map((item) => {
const interactive = item.interactive;
if (!interactive) return;
if (interactive.type === 'agentPlanAskQuery') {
const question = interactive.params?.content?.trim();
if (!question) return;
const answer = interactive.params?.answer?.trim() || undefined;
return JSON.stringify({ question, answer });
}
if (interactive.type === 'agentPlanAskUserForm') {
const question = interactive.params?.description?.trim();
const answer =
interactive.params?.inputForm
?.map((item) => {
if (!item?.label) return '';
const val =
typeof item.value === 'object' ? JSON.stringify(item.value) : String(item.value);
return `${item.label}: ${val}`;
})
.filter(Boolean)
.join('; ') || undefined;
if (!question && !answer) return;
return JSON.stringify({ question, answer });
}
return undefined;
})
.filter(Boolean) as string[];
return asks.join('\n');
})();
// 2. 获取 step 信息; 如果是中途暂停,则需要提示用户暂停
const { stepText, isPause } = (() => {
const stepValues = assistantResponses.filter((item) => item.stepId);
let isPause = false;
const stepResults = plan.steps.map((step, index) => {
const result = stepValues
.filter((item) => item.stepId === step.id)
.map((item) => item.text?.content?.trim() || '')
.filter(Boolean)
.join('\n');
const executed = !!result;
if (!executed) {
isPause = true;
}
return `(${index + 1}) [${executed ? `executed` : `pending`}] id=${step.id}; title=${step.title || ''}; description=${step.description || ''}${result ? `; result: ${result}` : ''}`;
});
return {
stepText: stepResults.join('\n'),
isPause
};
})();
return `${isPause ? 'PLAN_PAUSE_HANDOFF' : ''}
COLLECTED INFO:
${askText}
STEPS:
${stepText}`;
};
// Concat 2 -> 1, and sort by role
export const concatHistories = (histories1: ChatItemType[], histories2: ChatItemType[]) => {
......
......@@ -88,7 +88,8 @@ export type AgentPlanCheckInteractive = z.infer<typeof AgentPlanCheckInteractive
export const AgentPlanAskQueryInteractiveSchema = z.object({
type: z.literal('agentPlanAskQuery'),
params: z.object({
content: z.string()
content: z.string(),
answer: z.string().optional()
})
});
export type AgentPlanAskQueryInteractive = z.infer<typeof AgentPlanAskQueryInteractiveSchema>;
......@@ -147,16 +148,21 @@ export const PaymentPauseInteractiveSchema = z.object({
});
export type PaymentPauseInteractive = z.infer<typeof PaymentPauseInteractiveSchema>;
export const InteractiveNodeResponseTypeSchema = z.discriminatedUnion('type', [
UserSelectInteractiveSchema,
UserInputInteractiveSchema,
ChildrenInteractiveSchema,
ToolCallChildrenInteractiveSchema,
LoopInteractiveSchema,
PaymentPauseInteractiveSchema,
AgentPlanCheckInteractiveSchema,
AgentPlanAskQueryInteractiveSchema
]);
export const InteractiveNodeResponseTypeSchema = z.intersection(
z.discriminatedUnion('type', [
UserSelectInteractiveSchema,
UserInputInteractiveSchema,
ChildrenInteractiveSchema,
ToolCallChildrenInteractiveSchema,
LoopInteractiveSchema,
PaymentPauseInteractiveSchema,
AgentPlanCheckInteractiveSchema,
AgentPlanAskQueryInteractiveSchema
]),
z.object({
planId: z.string().optional()
})
);
export type InteractiveNodeResponseType = z.infer<typeof InteractiveNodeResponseTypeSchema>;
export const WorkflowInteractiveResponseTypeSchema = z.intersection(
......
......@@ -3,7 +3,7 @@
"version": "1.0.0",
"engines": {
"node": ">=20",
"pnpm": "10.x"
"pnpm": "9.x"
},
"dependencies": {
"@fastgpt-sdk/plugin": "0.3.8",
......
......@@ -85,11 +85,12 @@ export const createLLMResponse = async <T extends CompletionsBodyType>(
const { throwError = true, body, custonHeaders, userKey, maxContinuations = 1 } = args;
const { messages, useVision, requestOrigin, tools, toolCallMode } = body;
const model = getLLMModel(body.model);
// Messages process
const requestMessages = await loadRequestMessages({
messages,
useVision,
useVision: useVision && model.vision,
origin: requestOrigin
});
// Message process
......
......@@ -4,6 +4,7 @@ import { getLLMDefaultUsage } from '@fastgpt/global/core/ai/constants';
import { removeDatasetCiteText } from '@fastgpt/global/core/ai/llm/utils';
import json5 from 'json5';
import { sliceJsonStr } from '@fastgpt/global/common/string/tools';
import { jsonrepair } from 'jsonrepair';
/*
Count response max token
......@@ -332,7 +333,7 @@ export const parseLLMStreamResponse = () => {
export const parseJsonArgs = <T = Record<string, any>>(str: string) => {
try {
return json5.parse(sliceJsonStr(str)) as T;
return json5.parse(jsonrepair(sliceJsonStr(str))) as T;
} catch {
return;
}
......
......@@ -458,7 +458,25 @@ export const updateInteractiveChat = async ({
interactive,
input: userInteractiveVal
});
// 提取嵌套在子流程里的交互节点
const finalInteractive = extractDeepestInteractive(interactive);
if (status === 'query') {
// 特殊处理:
{
// 1. AskQuery 需要把用户答案回填到上一条 interactive,避免后续多轮恢复时丢失 answer。
if (finalInteractive.type === 'agentPlanAskQuery') {
finalInteractive.params.answer = userInteractiveVal;
chatItem.value[chatItem.value.length - 1].interactive = interactive;
chatItem.markModified('value');
await chatItem.save();
// 追加 PlanId 给 userItem(便于适配器会跳过转化该条消息)
props.userContent.value.forEach((item) => {
item.planId = finalInteractive.planId;
});
}
}
return await pushChatRecords(props);
}
......@@ -483,9 +501,6 @@ export const updateInteractiveChat = async ({
*/
// Update interactive value
{
// 提取嵌套在子流程里的交互节点
const finalInteractive = extractDeepestInteractive(interactive);
if (
finalInteractive.type === 'userSelect' ||
finalInteractive.type === 'agentPlanAskUserSelect'
......
......@@ -18,8 +18,10 @@ import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import {
chats2GPTMessages,
chatValue2RuntimePrompt,
runtimePrompt2ChatsValue,
GPTMessages2Chats
} from '@fastgpt/global/core/chat/adapt';
import { getPlanCallResponseText } from '@fastgpt/global/core/chat/utils';
import { filterMemoryMessages } from '../utils';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import { systemSubInfo } from '@fastgpt/global/core/workflow/node/agent/constants';
......@@ -41,6 +43,7 @@ export type DispatchAgentModuleProps = ModuleDispatchProps<{
[NodeInputKeyEnum.history]?: ChatItemType[];
[NodeInputKeyEnum.userChatInput]: string;
[NodeInputKeyEnum.aiChatVision]?: boolean;
[NodeInputKeyEnum.fileUrlList]?: string[];
[NodeInputKeyEnum.aiModel]: string;
[NodeInputKeyEnum.aiSystemPrompt]: string;
......@@ -94,6 +97,9 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
}
} = props;
const chatHistories = getHistories(history, histories);
const aiHistoryValues = chatHistories
.filter((item) => item.obj === ChatRoleEnum.AI)
.flatMap((item) => item.value);
const historiesMessages = chats2GPTMessages({
messages: chatHistories,
reserveId: false,
......@@ -126,16 +132,25 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
});
// 交互模式进来的话,这个值才是交互输入的值
const queryInput = chatValue2RuntimePrompt(query).text;
const { text: queryInput, files: queryFiles } = chatValue2RuntimePrompt(query);
const formatUserChatInput = fileInputPrompt
? `${fileInputPrompt}\n\n${userChatInput}`
: userChatInput;
const currentUserMessage = chats2GPTMessages({
messages: [
{
obj: ChatRoleEnum.Human,
value: runtimePrompt2ChatsValue({
text: formatUserChatInput,
files: queryFiles
})
}
],
reserveId: false
})[0];
let {
masterMessages = historiesMessages.concat({
role: 'user',
content: formatUserChatInput
}),
masterMessages: restoredMasterMessages,
planHistoryMessages,
agentPlan,
planBuffer
......@@ -159,6 +174,18 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
};
})();
let masterMessages = (() => {
if (!restoredMasterMessages) {
return historiesMessages.concat(currentUserMessage ? [currentUserMessage] : []);
} else if (planHistoryMessages?.length) {
return restoredMasterMessages ?? historiesMessages;
} else {
return currentUserMessage
? restoredMasterMessages.concat(currentUserMessage)
: restoredMasterMessages;
}
})();
// Get sub apps
const { completionTools: agentCompletionTools, subAppsMap: agentSubAppsMap } = await getSubapps(
{
......@@ -234,11 +261,15 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
});
}
if (askInteractive) {
// 这里存储一份冗余的 planId,便于告诉 AI value 也存储一份 planId
askInteractive.planId = planBuffer?.planId;
}
return {
completeMessages,
askInteractive,
plan,
planBuffer
plan
};
};
const planCallFn = async () => {
......@@ -286,15 +317,12 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
getLogger(LogCategories.MODULE.AI.AGENT).debug(
`All steps completed, check if need continue planning`
);
const stepsResponse = agentPlan.steps.map((step) => {
const stepResponse = assistantResponses
.filter((item) => item.stepId === step.id)
?.map((item) => item.text?.content)
.join('\n');
return {
title: step.title,
response: stepResponse
};
const planResponseText = getPlanCallResponseText({
plan: agentPlan,
assistantResponses: [
...aiHistoryValues.filter((v) => v.planId === agentPlan!.planId),
...assistantResponses
]
});
try {
......@@ -310,11 +338,9 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
task: agentPlan.task,
description: agentPlan.description,
background: agentPlan.background,
response: JSON.stringify(stepsResponse)
response: planResponseText
}),
task: agentPlan.task,
description: agentPlan.description,
background: agentPlan.background
...agentPlan
});
const { plan: continuePlan } = parsePlanCallResult(result);
......@@ -350,6 +376,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
break;
}
// Step calls
if (agentPlan) {
while (!checkIsStopping() && agentPlan.steps.filter((item) => !item.response).length) {
for await (const step of agentPlan.steps) {
......@@ -357,8 +384,10 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
break;
}
if (step.response) continue;
getLogger(LogCategories.MODULE.AI.AGENT).debug(`Step call: ${step.id}`, step);
assistantResponses.push({
planId: agentPlan.planId,
stepTitle: {
stepId: step.id,
title: step.title
......@@ -399,6 +428,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
.flat()
.map((item) => ({
...item,
planId: agentPlan!.planId,
stepId: step.id
}));
assistantResponses.push(...assistantResponse);
......@@ -408,25 +438,23 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
}
}
// 用户主动暂停(相当于强制结束本轮 task,会清空所有状态)
if (checkIsStopping()) {
break;
}
// 所有步骤执行完后,固定调用 Plan Agent(继续规划模式)
const stepsResponse = agentPlan.steps.map((step) => {
const stepResponse = assistantResponses
.filter((item) => item.stepId === step.id)
?.map((item) => item.text?.content)
.join('\n');
return {
title: step.title,
response: stepResponse
};
const planResponseText = getPlanCallResponseText({
plan: agentPlan,
assistantResponses: [
...aiHistoryValues.filter((v) => v.planId === agentPlan!.planId),
...assistantResponses
]
});
// 拼接 plan response 到 masterMessages 的 plan tool call 里(肯定在最后一个)
const lastToolIndex = masterMessages.findLastIndex((item) => item.role === 'tool');
if (lastToolIndex !== -1) {
masterMessages[lastToolIndex].content = JSON.stringify(stepsResponse);
masterMessages[lastToolIndex].content = planResponseText;
}
planIterationCount++;
......@@ -452,7 +480,6 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
}
} else {
getLogger(LogCategories.MODULE.AI.AGENT).debug(`Start master agent`);
const result = await masterCall({
...props,
masterMessages,
......@@ -478,7 +505,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
// 触发了 plan
if (result.planResponse) {
const { completeMessages, askInteractive, plan, planBuffer } = parsePlanCallResult(
const { completeMessages, askInteractive, plan } = parsePlanCallResult(
result.planResponse
);
......@@ -490,7 +517,7 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
[masterMessagesKey]: masterMessages,
[planMessagesKey]: filterMemoryMessages(completeMessages),
[agentPlanKey]: plan,
[planBufferKey]: planBuffer
[planBufferKey]: result.planResponse.planBuffer
},
[DispatchNodeResponseKeyEnum.interactive]: askInteractive,
[DispatchNodeResponseKeyEnum.nodeResponses]: nodeResponses
......
......@@ -92,7 +92,8 @@ export const masterCall = async ({
// Dataset search configuration
agent_datasetParams: datasetParams,
// Sandbox (Computer Use)
useAgentSandbox = false
useAgentSandbox = false,
aiChatVision
}
} = props;
......@@ -215,6 +216,7 @@ export const masterCall = async ({
messages: requestMessages,
model: getLLMModel(model),
stream: true,
useVision: aiChatVision,
tools: isStepCall
? completionTools.filter((item) => item.function.name !== SubAppIds.plan)
: completionTools
......@@ -425,9 +427,8 @@ export const masterCall = async ({
model,
stream,
mode: 'initial',
task: toolArgs.data.task,
description: toolArgs.data.description,
background: toolArgs.data.background
...toolArgs.data,
planId: call.id
});
return {
......@@ -605,7 +606,7 @@ export const masterCall = async ({
response,
assistantMessages: [], // TODO
usages,
stop
stop: stop || checkIsStopping()
};
},
onToolCompress: ({ call, response, usage }) => {
......
......@@ -122,6 +122,50 @@ ${
| 明显需要工具的任务 | **提示模式**(引导用户选择工具或进入辅助生成) |
</decision_matrix>
<goal_alignment_policy>
## 第五步:总结模式目标对齐策略
进入总结模式前,先锁定当前任务目标:
1. 当前输入优先
- 优先使用本轮最新用户输入来确定当前任务目标
2. 短指令回退规则
- 若用户仅输入“总结一下”“汇总下进度”等短指令,且未给出新任务目标
- 默认总结最近任务目标
3. 最近任务目标定义
- 优先提取当前上下文处在最后面的任务目标
4. 目标冲突处理
- 若当前输入与旧目标冲突,旧目标仅可作为背景证据
- 总结结论必须围绕当前任务目标组织
</goal_alignment_policy>
<pause_resume_policy>
## 第六步:Pause 后软续跑策略
仅当满足下面条件时,才启用 pause 续跑策略:
- 最近一条工具消息内容以 "PLAN_PAUSE_HANDOFF" 开头
- 该工具消息位于当前用户输入之前的最近上下文位置
- 若只是更早历史里的 handoff 记录,不视为本轮 pause 恢复信号
1. 仅当用户输入明显表达“继续”意图时,优先延续旧 plan
- 继续意图示例:继续、接着、按上次、继续执行、往下做、resume、continue
- 命中后优先调用 ${SubAppIds.plan}
2. 命中继续意图时,调用 ${SubAppIds.plan}
- 复用 handoff 里的 信息
- 明确继承 ask 的 question 和 answer(若存在)
- 优先推进 pending steps,避免重复 executed steps
3. 若用户新输入与旧 plan 目标明显冲突
- 直接按新输入重规划任务,不强行续旧 plan
4. 若没有继续意图
- 按 <decision_matrix> 正常决策,不自动续跑旧 plan
</pause_resume_policy>
<examples>
<example name="无工具 + 通用对话 → 聊天助手">
用户:"什么是人工智能?"
......@@ -167,6 +211,34 @@ ${
判断:信息不够完整
决策:调用 ${SubAppIds.plan} 补充规划
</example>
<example name="Pause 上下文 + 继续意图 → 优先续旧计划">
用户:"继续执行上次的计划"
上下文:包含 PLAN_PAUSE_HANDOFF,且存在 pending steps
判断:用户明确要继续,且旧计划可延续
决策:优先调用 ${SubAppIds.plan},把 handoff 的任务和步骤进度整理进 background,优先执行 pending steps
</example>
<example name="Pause 上下文 + 新任务冲突 → 直接重规划">
用户:"不要继续上次了,改成帮我做竞品调研"
上下文:包含 PLAN_PAUSE_HANDOFF,但用户目标已明显变化
判断:新输入与旧计划冲突
决策:按新输入直接重规划,不强行沿用旧 pending steps
</example>
<example name="Pause 上下文 + 短指令总结 → 对齐最近任务目标">
用户:"总结一下"
上下文:最近存在 PLAN_PAUSE_HANDOFF,同时更早历史里还有其他任务记录
判断:用户未给新目标,属于短指令总结
决策:按最近任务目标总结,不扩散到更早历史任务
</example>
<example name="Pause 上下文 + 新目标冲突 + 总结 → 当前输入优先">
用户:"别按上次方向了,直接总结这次关于成本优化的结论"
上下文:存在旧任务 handoff,但与本轮目标冲突
判断:当前输入给出了明确新目标
决策:按当前输入目标总结,旧目标只作为背景证据
</example>
</examples>
<security>
......@@ -205,8 +277,10 @@ ${
- 需要工具的任务 → 明确告知用户缺少工具,引导选择工具或进入辅助生成模式,例如:"当前没有可用的工具来完成此任务,请先选择相关工具或进入辅助生成模式自动化生成任务规划"
**有工具时的执行**:
- 总结模式:整合已有信息,生成结构化输出,需要基于之前的上下文所有信息来输出一个完整且详细的总结,
不是简单回答,需要直接把详细的总结信息给输出出来,把上下执行的信息都整合进去,这一步越详细越好
- 总结模式:先在内部确认当前任务目标(优先本轮用户输入;短指令则回退最近任务目标),
再结合当前历史记录中与目标相关的全部信息输出详细总结,不要只给简短结论,把上下执行的信息都整合进去,这一步越详细越好,如果不详细很可能会重做
这是不可允许的,一旦进入总结模式就需要根据上下文并结合目标来完成报告。
若当前目标信息不足,不要硬总结,转工具模式或规划模式补充信息
- 工具模式:调用最合适的工具
- 规划模式:调用 ${SubAppIds.plan},让规划系统接管,同时需要对当前的上下文信息进行总结,尤其是之前向用户询问的一些问题和用户的回答(避免重复的询问同一个问题),
以及已经执行的步骤和结果,未执行的步骤信息,帮助规划系统更好地理解当前状态,最后将上诉的信息放到 background 的参数里
......
import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type';
import { SubAppIds, systemSubInfo } from '@fastgpt/global/core/workflow/node/agent/constants';
import type { InteractiveNodeResponseType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import z from 'zod';
export const PlanCheckInteractive: InteractiveNodeResponseType = {
......@@ -13,7 +14,8 @@ export const PlanCheckInteractive: InteractiveNodeResponseType = {
export const PlanAgentParamsSchema = z.object({
task: z.string(),
description: z.string(),
background: z.string().nullish()
background: z.string().nullish(),
planId: z.string().default(() => getNanoid(6))
});
export type PlanAgentParamsType = z.infer<typeof PlanAgentParamsSchema>;
export const PlanAgentTool: ChatCompletionTool = {
......
......@@ -4,13 +4,17 @@ import type {
ChatCompletionTool
} from '@fastgpt/global/core/ai/type';
import { createLLMResponse } from '../../../../../../ai/llm/request';
import { getInitialPlanPrompt, getContinuePlanPrompt, getInitialPlanQuery } from './prompt';
import {
getInitialPlanPrompt,
getContinuePlanPrompt,
getInitialPlanQuery,
reTryPlanPrompt
} from './prompt';
import { getLLMModel } from '../../../../../../ai/model';
import { formatModelChars2Points } from '../../../../../../../support/wallet/usage/utils';
import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
import type {
AgentPlanAskQueryInteractive,
UserInputInteractive,
InteractiveNodeResponseType,
WorkflowInteractiveResponseType
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { parseJsonArgs } from '../../../../../../ai/utils';
......@@ -29,6 +33,8 @@ import type { PlanAgentParamsType } from './constants';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { getLogger, LogCategories } from '../../../../../../../common/logger';
const agentLogger = getLogger(LogCategories.MODULE.AI.AGENT);
type PlanAgentConfig = {
systemPrompt?: string;
model: string;
......@@ -61,7 +67,7 @@ type DispatchPlanAgentProps = PlanAgentConfig &
} & (InitialParams | ContinueParams | InteractiveParams);
export type DispatchPlanAgentResponse = {
askInteractive?: UserInputInteractive | AgentPlanAskQueryInteractive;
askInteractive?: InteractiveNodeResponseType;
plan?: AgentPlanType;
planBuffer: PlanAgentParamsType;
completeMessages: ChatCompletionMessageParam[];
......@@ -71,6 +77,7 @@ export type DispatchPlanAgentResponse = {
const parsePlan = async ({
text,
planId,
task,
description,
background
......@@ -81,19 +88,22 @@ const parsePlan = async ({
return;
}
const result = parseJsonArgs(text);
const result = parseJsonArgs<{ steps: AgentPlanType['steps'] }>(text);
if (!result) {
return;
return result;
}
const params = await AgentPlanSchema.safeParseAsync({
...result,
planId,
task,
description,
background
});
if (!params.success) {
getLogger(LogCategories.MODULE.AI.AGENT).warn(`[Plan Agent] Not plan`, { text });
agentLogger.warn(`[Plan Agent] Not plan`, { text });
return;
}
......@@ -101,7 +111,7 @@ const parsePlan = async ({
};
const parseAskInteractive = async (
toolCalls: ChatCompletionMessageToolCall[]
): Promise<UserInputInteractive | AgentPlanAskQueryInteractive | undefined> => {
): Promise<InteractiveNodeResponseType | undefined> => {
const tooCall = toolCalls[0];
if (!tooCall) return;
const params = await AIAskAnswerSchema.safeParseAsync(parseJsonArgs(tooCall.function.arguments));
......@@ -141,7 +151,7 @@ const parseAskInteractive = async (
}
};
} else {
getLogger(LogCategories.MODULE.AI.AGENT).warn(`[Plan Agent] Ask tool params is not valid`, {
agentLogger.warn(`[Plan Agent] Ask tool params is not valid`, {
tooCall
});
return;
......@@ -161,6 +171,7 @@ export const dispatchPlanAgent = async ({
getSubAppInfo,
systemPrompt,
model,
planId,
task,
description,
background,
......@@ -202,7 +213,7 @@ export const dispatchPlanAgent = async ({
content: props.queryInput
});
} else {
getLogger(LogCategories.MODULE.AI.AGENT).error('Plan interactive mode error', {
agentLogger.error('Plan interactive mode error', {
planMessages: props.planMessages
});
return Promise.reject('Plan interactive mode error');
......@@ -226,6 +237,14 @@ export const dispatchPlanAgent = async ({
// console.dir({ requestMessages }, { depth: null });
// console.log('userInput:', userInput, 'mode:', mode, 'interactive?.type:', interactive?.type);
const requestParams = {
model: modelData.model,
stream: true,
tools: props.mode === 'continue' ? undefined : [AIAskTool],
tool_choice: 'auto' as const,
toolCallMode: modelData.toolChoice ? ('toolChoice' as const) : ('prompt' as const),
parallel_tool_calls: false
};
let {
answerText,
toolCalls = [],
......@@ -236,13 +255,8 @@ export const dispatchPlanAgent = async ({
} = await createLLMResponse({
isAborted: checkIsStopping,
body: {
model: modelData.model,
messages: requestMessages,
stream: true,
tools: props.mode === 'continue' ? undefined : [AIAskTool],
tool_choice: 'auto',
toolCallMode: modelData.toolChoice ? 'toolChoice' : 'prompt',
parallel_tool_calls: false
...requestParams
}
});
......@@ -250,21 +264,93 @@ export const dispatchPlanAgent = async ({
return Promise.reject(responseEmptyTip);
}
const llmRequestIds: string[] = [requestId];
/*
正常输出情况:
1. text: 正常生成plan
2. toolCall: 调用ask工具
3. text + confirm: 成功生成工具 + 确认操作
*/
// 获取生成的 plan
const plan = await parsePlan({
text: answerText,
task,
description,
background
});
// 获取交互结果
const askInteractive = await parseAskInteractive(toolCalls);
// 1. 首次获取交互结果
const { askInteractive, plan } = await (async () => {
// 1. 首次获取交互结果
let [askInteractive, plan] = await Promise.all([
parseAskInteractive(toolCalls),
parsePlan({
text: answerText,
planId,
task,
description,
background
})
]);
if (plan || askInteractive) {
return {
askInteractive,
plan
};
}
// 2. 二次尝试生成 plan
agentLogger.warn('[Plan Agent] parse failed, try regenerate plan once', {
requestId,
mode: props.mode,
answerText: answerText.slice(0, 2000)
});
const regenerateResponse = await createLLMResponse({
isAborted: checkIsStopping,
body: {
messages: [
...completeMessages,
{
role: 'user',
content: reTryPlanPrompt
}
],
...requestParams
}
});
usage.inputTokens += regenerateResponse.usage.inputTokens;
usage.outputTokens += regenerateResponse.usage.outputTokens;
llmRequestIds.push(regenerateResponse.requestId);
completeMessages = regenerateResponse.completeMessages;
[askInteractive, plan] = await Promise.all([
parseAskInteractive(regenerateResponse.toolCalls || []),
parsePlan({
text: regenerateResponse.answerText,
planId,
task,
description,
background
})
]);
if (plan || askInteractive) {
return {
askInteractive,
plan
};
}
// 真的失败了
agentLogger.warn('[Plan Agent] plan regenerate failed', {
requestId,
regenerateRequestId: regenerateResponse.requestId,
mode: props.mode,
answerText: regenerateResponse.answerText.slice(0, 2000)
});
askInteractive = {
type: 'agentPlanAskQuery',
params: {
content: i18nT('chat:agent_plan_parse_retry_tip')
}
};
return {
askInteractive
};
})();
const { totalPoints, modelName } = formatModelChars2Points({
model: modelData.model,
......@@ -285,13 +371,14 @@ export const dispatchPlanAgent = async ({
totalPoints,
model: modelName,
runningTime: +((Date.now() - startTime) / 1000).toFixed(2),
llmRequestIds: [requestId]
llmRequestIds
};
return {
askInteractive,
plan,
planBuffer: {
planId,
task,
description,
background
......
......@@ -1092,7 +1092,11 @@ ${bestPractices}
`;
};
export const getInitialPlanQuery = ({ task, description, background }: PlanAgentParamsType) => {
export const getInitialPlanQuery = ({
task,
description,
background
}: Omit<PlanAgentParamsType, 'planId'>) => {
return `## 任务目标
${task}
......@@ -1108,7 +1112,7 @@ export const getContinuePlanQuery = ({
description,
background,
response
}: PlanAgentParamsType & {
}: Omit<PlanAgentParamsType, 'planId'> & {
response: string;
}) => {
return `${getInitialPlanQuery({ task, description, background })}
......@@ -1121,3 +1125,29 @@ ${response}
## 下一步任务
请基于已执行步骤及结果,根据系统提示词来判断是否需要继续规划、生成总结报告步骤、还是任务已完成,或者遇到问题直接返回`;
};
export const reTryPlanPrompt = `上一轮 plan 输出不是合法 JSON,无法解析。
请基于原始任务重新生成完整 plan,严格按 JSON 输出。
要求:
- 仅返回 JSON
- 包含 task 和 steps 字段
- 每个 step 必须包含 id/title/description
JSON 格式示例(只参考格式,不要照抄内容):
{
"task": "深入了解 Rust 编程语言(系统编程方向)",
"steps": [
{
"id": "step1",
"title": "了解 Rust 的核心特性",
"description": "使用 @webSearch 搜索 Rust 的所有权、借用检查与并发安全机制"
},
{
"id": "step2",
"title": "调研 Rust 在系统编程的应用",
"description": "使用 @webSearch 搜索 Rust 在操作系统、网络编程、嵌入式中的典型项目"
}
]
}`;
......@@ -211,10 +211,15 @@ export async function dispatchWorkFlow({
const checkStoppingTimer =
apiVersion === 'v2'
? setInterval(async () => {
stopping = await shouldWorkflowStop({
if (stopping) return;
const shouldStop = await shouldWorkflowStop({
appId: runningAppInfo.id,
chatId
});
if (shouldStop) {
stopping = true;
}
}, 100)
: undefined;
......@@ -1209,11 +1214,11 @@ export class WorkflowQueue {
}
/*
特殊情况:
通过 skipEdges 可以判断是运行了分支节点。
由于分支节点,可能会实现递归调用(skip 连线往前递归)
需要把分支节点也加入到已跳过的记录里,可以保证递归 skip 运行时,至多只会传递到当前分支节点,不会影响分支后的内容。
*/
特殊情况:
通过 skipEdges 可以判断是运行了分支节点。
由于分支节点,可能会实现递归调用(skip 连线往前递归)
需要把分支节点也加入到已跳过的记录里,可以保证递归 skip 运行时,至多只会传递到当前分支节点,不会影响分支后的内容。
*/
const skipEdges = (nodeRunResult.result[DispatchNodeResponseKeyEnum.skipHandleId] ||
[]) as string[];
if (skipEdges && skipEdges?.length > 0) {
......@@ -1310,6 +1315,7 @@ export class WorkflowQueue {
}
return {
planId: interactiveResult.planId,
interactive: interactiveResult
};
}
......
......@@ -4,11 +4,11 @@
"type": "module",
"engines": {
"node": ">=20",
"pnpm": "10.x"
"pnpm": "9.x"
},
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.7.2",
"@fastgpt-sdk/sandbox-adapter": "^0.0.21",
"@fastgpt-sdk/sandbox-adapter": "^0.0.22",
"@fastgpt-sdk/storage": "catalog:",
"@fastgpt-sdk/logger": "catalog:",
"@fastgpt/global": "workspace:*",
......@@ -36,6 +36,7 @@
"ioredis": "^5.6.0",
"joplin-turndown-plugin-gfm": "^1.0.12",
"json5": "catalog:",
"jsonrepair": "^3.0.0",
"jsonpath-plus": "^10.3.0",
"jsonwebtoken": "^9.0.2",
"lodash": "catalog:",
......
......@@ -5,6 +5,7 @@
"Next": "Next",
"Previous": "Previous",
"agent_plan_continue": "Continue planning",
"agent_plan_parse_retry_tip": "The plan format was invalid. Add one more requirement and I will regenerate the plan.",
"ai_reasoning": "Thinking process",
"back_to_text": "Text input",
"balance_not_enough_pause": "Workflow paused due to insufficient AI points",
......
......@@ -5,6 +5,7 @@
"Next": "下一个",
"Previous": "上一个",
"agent_plan_continue": "继续规划",
"agent_plan_parse_retry_tip": "规划结果格式异常,请补充一句需求后我重新生成计划。",
"ai_reasoning": "思考过程",
"back_to_text": "返回输入",
"balance_not_enough_pause": "由于 AI 积分不足,暂停运行工作流",
......
......@@ -5,6 +5,7 @@
"Next": "下一個",
"Previous": "上一個",
"agent_plan_continue": "繼續規劃",
"agent_plan_parse_retry_tip": "規劃結果格式異常,請補充一句需求後我重新生成計畫。",
"ai_reasoning": "思考過程",
"back_to_text": "返回輸入",
"balance_not_enough_pause": "由於 AI 積分不足,暫停運行工作流",
......
......@@ -3,7 +3,7 @@
"version": "1.0.0",
"engines": {
"node": ">=20",
"pnpm": "10.x"
"pnpm": "9.x"
},
"dependencies": {
"@chakra-ui/anatomy": "catalog:",
......
......@@ -5,7 +5,7 @@ WORKDIR /app
ARG proxy
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat && npm install -g pnpm@10
RUN apk add --no-cache libc6-compat && npm install -g pnpm@9
# copy packages and one project
COPY pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
......@@ -39,7 +39,7 @@ COPY --from=maindeps /app/projects/app/node_modules ./projects/app/node_modules
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat && npm install -g pnpm@10
RUN apk add --no-cache libc6-compat && npm install -g pnpm@9
ENV NODE_OPTIONS="--max-old-space-size=4096"
ENV NEXT_PUBLIC_BASE_URL=$base_url
......
......@@ -12,7 +12,7 @@
},
"engines": {
"node": ">=20",
"pnpm": "10.x"
"pnpm": "9.x"
},
"dependencies": {
"@chakra-ui/anatomy": "catalog:",
......
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
import Head from 'next/head';
import React, { useMemo } from 'react';
const NextHead = ({ title, icon, desc }: { title?: string; icon?: string; desc?: string }) => {
const formatIcon = useMemo(() => {
if (!icon) return LOGO_ICON;
if (!icon) return '/favicon.ico';
if (icon.startsWith('http') || icon.startsWith('/')) {
return icon;
}
return LOGO_ICON;
return '/favicon.ico';
}, [icon]);
return (
......
......@@ -60,11 +60,13 @@ const accordionButtonStyle = {
const RenderResoningContent = React.memo(function RenderResoningContent({
content,
isChatting,
isLastResponseValue
isLastResponseValue,
isDisabled
}: {
content: string;
isChatting: boolean;
isLastResponseValue: boolean;
isDisabled?: boolean;
}) {
const { t } = useTranslation();
const showAnimation = isChatting && isLastResponseValue;
......@@ -90,7 +92,7 @@ const RenderResoningContent = React.memo(function RenderResoningContent({
borderColor={'myGray.300'}
color={'myGray.500'}
>
<Markdown source={content} showAnimation={showAnimation} />
<Markdown source={content} showAnimation={showAnimation} isDisabled={isDisabled} />
</AccordionPanel>
</AccordionItem>
</Accordion>
......@@ -100,12 +102,14 @@ const RenderText = React.memo(function RenderText({
showAnimation,
text,
chatItemDataId,
onOpenCiteModal
onOpenCiteModal,
isDisabled
}: {
showAnimation: boolean;
text: string;
chatItemDataId: string;
onOpenCiteModal?: (e?: OnOpenCiteModalProps) => void;
isDisabled?: boolean;
}) {
const appId = useContextSelector(WorkflowRuntimeContext, (v) => v.appId);
const chatId = useContextSelector(WorkflowRuntimeContext, (v) => v.chatId);
......@@ -131,6 +135,7 @@ const RenderText = React.memo(function RenderText({
showAnimation={showAnimation}
chatAuthData={chatAuthData}
onOpenCiteModal={onOpenCiteModal}
isDisabled={isDisabled}
/>
);
});
......@@ -424,6 +429,7 @@ const AIResponseBox = ({
}) => {
const showRunningStatus = useContextSelector(ChatItemContext, (v) => v.showRunningStatus);
const tools = value.tool ? [value.tool] : value.tools;
const disableStreamingInteraction = isChatting && isLastChild;
if ('text' in value && value.text) {
return (
......@@ -432,6 +438,7 @@ const AIResponseBox = ({
showAnimation={isChatting && isLastResponseValue}
text={value.text.content}
onOpenCiteModal={onOpenCiteModal}
isDisabled={disableStreamingInteraction}
/>
);
}
......@@ -441,6 +448,7 @@ const AIResponseBox = ({
isChatting={isChatting}
isLastResponseValue={isLastResponseValue}
content={value.reasoning.content}
isDisabled={disableStreamingInteraction}
/>
);
}
......
......@@ -165,6 +165,13 @@ export function agentForm2AppWorkflow(
value: [[workflowStartNodeId, NodeOutputKeyEnum.userFiles]]
},
{
key: NodeInputKeyEnum.aiChatVision,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
label: '',
valueType: WorkflowIOValueTypeEnum.boolean,
value: true
},
{
key: NodeInputKeyEnum.history,
renderTypeList: [FlowNodeInputTypeEnum.numberInput, FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.chatHistory,
......
......@@ -24,8 +24,7 @@ type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
// 哪些路由有自定义 Head
const routesWithCustomHead = ['/chat', '/chat/share', '/app/detail/', '/dataset/detail'];
const routesWithCustomHead = ['/chat', '/chat/share', '/app/detail', '/dataset/detail'];
// 哪些路由不需要 Layout
const routesWithoutLayout = ['/openapi'];
......
......@@ -5,7 +5,7 @@ WORKDIR /app
ARG proxy
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat && npm install -g pnpm@10
RUN apk add --no-cache libc6-compat && npm install -g pnpm@9
# copy packages and one project
COPY pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
......@@ -37,7 +37,7 @@ COPY --from=maindeps /app/projects/marketplace/node_modules ./projects/marketpla
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat && npm install -g pnpm@10
RUN apk add --no-cache libc6-compat && npm install -g pnpm@9
ENV NODE_OPTIONS="--max-old-space-size=4096"
ENV NEXT_PUBLIC_BASE_URL=$base_url
......
......@@ -10,7 +10,7 @@
},
"engines": {
"node": ">=20",
"pnpm": "10.x"
"pnpm": "9.x"
},
"dependencies": {
"@chakra-ui/anatomy": "catalog:",
......
......@@ -4,7 +4,7 @@ WORKDIR /app
ARG proxy
RUN npm install -g pnpm@10
RUN npm install -g pnpm@9
# 复制package.json
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
......@@ -40,7 +40,7 @@ COPY --from=install /app/node_modules /app/node_modules
COPY --from=install /app/projects/mcp_server/node_modules /app/projects/mcp_server/node_modules
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat curl bash && npm install -g pnpm@10
RUN apk add --no-cache libc6-compat curl bash && npm install -g pnpm@9
# Install curl and bash, then install bun
RUN curl -fsSL https://bun.sh/install | bash
......
......@@ -15,7 +15,7 @@
},
"engines": {
"node": ">=20",
"pnpm": "10.x"
"pnpm": "9.x"
},
"dependencies": {
"@fastgpt/global": "workspace:*",
......
......@@ -5,7 +5,7 @@ WORKDIR /app
ARG proxy
# 安装 pnpm
RUN apk add --no-cache nodejs npm && npm install -g pnpm@10
RUN apk add --no-cache nodejs npm && npm install -g pnpm@9
# 复制 workspace 配置和依赖包
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
......
......@@ -14,7 +14,7 @@
},
"engines": {
"node": ">=20",
"pnpm": "10.x"
"pnpm": "9.x"
},
"dependencies": {
"@fastgpt-sdk/logger": "catalog:",
......
......@@ -40,7 +40,7 @@
},
"engines": {
"node": ">=20",
"pnpm": "10.x"
"pnpm": "9.x"
},
"license": "Apache-2.0",
"dependencies": {
......
......@@ -21,7 +21,7 @@
},
"engines": {
"node": ">=20",
"pnpm": "10.x"
"pnpm": "9.x"
},
"exports": {
".": {
......
......@@ -319,8 +319,7 @@ describe('chats2GPTMessages', () => {
const result = chats2GPTMessages({ messages, reserveId: false });
expect(result).toHaveLength(1);
expect(result[0].content).toBe('What would you like to know?');
expect(result).toHaveLength(0);
});
it('should handle interactive agentPlanAskUserForm', () => {
......@@ -346,10 +345,7 @@ describe('chats2GPTMessages', () => {
const result = chats2GPTMessages({ messages, reserveId: false });
expect(result).toHaveLength(1);
expect(result[0].content).toContain('Please fill in the form');
expect(result[0].content).toContain('- Name: John');
expect(result[0].content).toContain('- Age: 25');
expect(result).toHaveLength(0);
});
it('should handle plan with reserveTool true', () => {
......@@ -370,10 +366,12 @@ describe('chats2GPTMessages', () => {
}
} as any,
{
planId: 'plan-1',
stepId: 'step-1',
text: { content: 'Search results here' }
} as any,
{
planId: 'plan-1',
stepId: 'step-2',
text: { content: 'Analysis complete' }
} as any
......@@ -406,7 +404,7 @@ describe('chats2GPTMessages', () => {
} as any,
{
plan: {
planId: 'plan-1',
planId: 'plan-2',
task: 'Task 1 duplicate',
description: 'Description 1 duplicate',
background: 'Background 1 duplicate',
......@@ -420,7 +418,7 @@ describe('chats2GPTMessages', () => {
const result = chats2GPTMessages({ messages, reserveId: false, reserveTool: true });
// Should only have 2 messages (1 assistant + 1 tool) for the first plan
expect(result).toHaveLength(2);
expect(result).toHaveLength(4);
});
it('should not process plan when reserveTool is false', () => {
......@@ -444,7 +442,7 @@ describe('chats2GPTMessages', () => {
const result = chats2GPTMessages({ messages, reserveId: false, reserveTool: false });
// Plan should be skipped when reserveTool is false
expect(result).toHaveLength(0);
expect(result).toHaveLength(2);
});
});
......
......@@ -762,6 +762,72 @@ describe('pushChatRecords', () => {
}
});
it('should persist agentPlanAskQuery answer before pushing new records', async () => {
await MongoChatItem.create({
chatId: 'test-chat-id',
teamId: testTeamId,
tmbId: testTmbId,
appId: testAppId,
obj: ChatRoleEnum.AI,
dataId: 'plan-ask-data-id',
value: [
{
interactive: {
type: 'agentPlanAskQuery',
planId: 'plan_1',
params: {
content: '请补充目标'
}
}
}
]
});
const props = createMockProps(
{
userContent: {
obj: ChatRoleEnum.Human,
value: [
{
text: { content: '深入了解 Rust 系统编程方向' }
}
]
}
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
const interactive = {
type: 'agentPlanAskQuery' as const,
planId: 'plan_1',
params: {
content: '请补充目标'
},
entryNodeIds: [],
memoryEdges: [],
nodeOutputs: []
};
await updateInteractiveChat({ interactive, ...props });
const chatItem = await MongoChatItem.findOne({
appId: testAppId,
chatId: props.chatId,
obj: ChatRoleEnum.AI,
dataId: 'plan-ask-data-id'
});
if (chatItem?.obj !== ChatRoleEnum.AI) {
throw new Error('chatItem does not have AI interactive value');
}
const lastValue = chatItem.value[chatItem.value.length - 1];
if (lastValue.interactive?.type !== 'agentPlanAskQuery') {
throw new Error('chatItem does not have agentPlanAskQuery interactive');
}
expect(lastValue.interactive.params.answer).toBe('深入了解 Rust 系统编程方向');
});
it('should remove paymentPause interactive value', async () => {
// Create an AI chat item with paymentPause interactive
await MongoChatItem.create({
......
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