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