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>
This commit is contained in:
Archer
2026-03-20 18:07:29 +08:00
committed by GitHub
parent ec7a8beba5
commit 7a6601394d
45 changed files with 1353 additions and 850 deletions
+7 -9
View File
@@ -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);
});
});
@@ -1,450 +0,0 @@
import type { CompletionFinishReason } from '@fastgpt/global/core/ai/type';
import { parseLLMStreamResponse } from '@fastgpt/service/core/ai/utils';
import { describe, expect, it } from 'vitest';
describe('Parse reasoning stream content test', async () => {
const partList = [
{
data: [{ content: '你好1' }, { content: '你好2' }, { content: '你好3' }],
correct: { answer: '你好1你好2你好3', reasoning: '' }
},
{
data: [
{ reasoning_content: '这是' },
{ reasoning_content: '思考' },
{ reasoning_content: '过程' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<t' },
{ content: 'hink>' },
{ content: '这是' },
{ content: '思考' },
{ content: '过程' },
{ content: '</think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>' },
{ content: '这是' },
{ content: '思考' },
{ content: '过程' },
{ content: '</think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程' },
{ content: '</think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</' },
{ content: 'think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</think>你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</th' },
{ content: '假的' },
{ content: '你好2' },
{ content: '你好3' },
{ content: '过程</think>你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程</th假的你好2你好3过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</th' },
{ content: '假的' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '', reasoning: '这是思考过程</th假的你好2你好3' }
}
];
// Remove think
partList.forEach((part, index) => {
it(`Reasoning test:${index}`, () => {
const { parsePart } = parseLLMStreamResponse();
let answer = '';
let reasoning = '';
part.data.forEach((item) => {
const formatPart = {
choices: [
{
delta: {
role: 'assistant',
content: item.content,
reasoning_content: item.reasoning_content
}
}
]
};
const { reasoningContent, content } = parsePart({
part: formatPart,
parseThinkTag: true,
retainDatasetCite: false
});
answer += content;
reasoning += reasoningContent;
});
expect(answer).toBe(part.correct.answer);
expect(reasoning).toBe(part.correct.reasoning);
});
});
});
describe('Parse dataset cite content test', async () => {
const partList = [
{
// 完整的
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e74767063e882d6861](CITE)' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](CITE)',
responseContent: '知识库问答系统'
}
},
{
// 只要 objectId
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861]' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861]',
responseContent: '知识库问答系统'
}
},
{
// 满足替换条件的
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](',
responseContent: '知识库问答系统'
}
},
{
// 满足替换条件的
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](C' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](C',
responseContent: '知识库问答系统'
}
},
{
// 满足替换条件的
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CI' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](CI',
responseContent: '知识库问答系统'
}
},
{
// 满足替换条件的
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CIT' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](CIT',
responseContent: '知识库问答系统'
}
},
{
// 满足替换条件的
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](CITE',
responseContent: '知识库问答系统'
}
},
{
// 缺失结尾
data: [
{ content: '知识库问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](CITE',
responseContent: '知识库问答系统'
}
},
{
// ObjectId 不正确
data: [
{ content: '知识库问答系统' },
{ content: '[67e517e747' },
{ content: '67882d' },
{ content: '6861](CITE)' }
],
correct: {
content: '知识库问答系统[67e517e74767882d6861](CITE)',
responseContent: '知识库问答系统[67e517e74767882d6861](CITE)'
}
},
{
// 其他链接
data: [{ content: '知识库' }, { content: '问答系统' }, { content: '[](https://fastgpt.cn)' }],
correct: {
content: '知识库问答系统[](https://fastgpt.cn)',
responseContent: '知识库问答系统[](https://fastgpt.cn)'
}
},
{
// 不完整的其他链接
data: [{ content: '知识库' }, { content: '问答系统' }, { content: '[](https://fastgp' }],
correct: {
content: '知识库问答系统[](https://fastgp',
responseContent: '知识库问答系统[](https://fastgp'
}
},
{
// 开头
data: [{ content: '[知识库' }, { content: '问答系统' }, { content: '[](https://fastgp' }],
correct: {
content: '[知识库问答系统[](https://fastgp',
responseContent: '[知识库问答系统[](https://fastgp'
}
},
{
// 结尾
data: [{ content: '知识库' }, { content: '问答系统' }, { content: '[' }],
correct: {
content: '知识库问答系统[',
responseContent: '知识库问答系统['
}
},
{
// 中间
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[' },
{ content: '问答系统]' }
],
correct: {
content: '知识库问答系统[问答系统]',
responseContent: '知识库问答系统[问答系统]'
}
},
{
// 双链接
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[](https://fastgpt.cn)' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE)' }
],
correct: {
content: '知识库问答系统[](https://fastgpt.cn)[67e517e74767063e882d6861](CITE)',
responseContent: '知识库问答系统[](https://fastgpt.cn)'
}
},
{
// 双链接缺失部分
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[](https://fastgpt.cn)' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CIT' }
],
correct: {
content: '知识库问答系统[](https://fastgpt.cn)[67e517e74767063e882d6861](CIT',
responseContent: '知识库问答系统[](https://fastgpt.cn)'
}
},
{
// 双Cite
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE)' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE)' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](CITE)[67e517e74767063e882d6861](CITE)',
responseContent: '知识库问答系统'
}
},
{
// 双Cite-第一个假Cite
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '6861](CITE)' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE)' }
],
correct: {
content: '知识库问答系统[67e517e7476861](CITE)[67e517e74767063e882d6861](CITE)',
responseContent: '知识库问答系统[67e517e7476861](CITE)'
}
},
{
// [id](CITE)
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[i' },
{ content: 'd](CITE)' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE)' }
],
correct: {
content: '知识库问答系统[id](CITE)[67e517e74767063e882d6861](CITE)',
responseContent: '知识库问答系统'
}
},
{
// [id](CITE)
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[i' },
{ content: 'd](CITE)' }
],
correct: {
content: '知识库问答系统[id](CITE)',
responseContent: '知识库问答系统'
}
}
];
partList.forEach((part, index) => {
it(`Dataset cite test: ${index}`, () => {
const { parsePart } = parseLLMStreamResponse();
let answer = '';
let responseContent = '';
const list = [...part.data, { content: '' }];
list.forEach((item, index) => {
const formatPart = {
choices: [
{
delta: {
role: 'assistant',
content: item.content,
reasoning_content: ''
},
finish_reason: (index === list.length - 2 ? 'stop' : null) as CompletionFinishReason
}
]
};
const { content, responseContent: newResponseContent } = parsePart({
part: formatPart,
parseThinkTag: false,
retainDatasetCite: false
});
answer += content;
responseContent += newResponseContent;
});
expect(answer).toEqual(part.correct.content);
expect(responseContent).toEqual(part.correct.responseContent);
});
});
});
+603
View File
@@ -0,0 +1,603 @@
import { describe, expect, it } from 'vitest';
import {
parseJsonArgs,
parseLLMStreamResponse,
computedMaxToken,
computedTemperature,
parseReasoningContent
} from '@fastgpt/service/core/ai/utils';
import type { CompletionFinishReason } from '@fastgpt/global/core/ai/type';
import type { LLMModelItemType } from '@fastgpt/global/core/ai/model.schema';
const mockModel = (maxResponse: number, maxTemperature?: number) =>
({ maxResponse, maxTemperature }) as LLMModelItemType;
describe('computedMaxToken', () => {
it('should return undefined when maxToken is undefined', () => {
expect(computedMaxToken({ maxToken: undefined, model: mockModel(4096) })).toBeUndefined();
});
it('should cap maxToken to model.maxResponse', () => {
expect(computedMaxToken({ maxToken: 8000, model: mockModel(4096) })).toBe(4096);
});
it('should return maxToken when within model.maxResponse', () => {
expect(computedMaxToken({ maxToken: 1000, model: mockModel(4096) })).toBe(1000);
});
it('should enforce minimum of 1 by default', () => {
expect(computedMaxToken({ maxToken: 0, model: mockModel(4096) })).toBe(1);
});
it('should enforce custom min value', () => {
expect(computedMaxToken({ maxToken: 5, model: mockModel(4096), min: 10 })).toBe(10);
});
it('should use maxToken when it exceeds min', () => {
expect(computedMaxToken({ maxToken: 100, model: mockModel(4096), min: 10 })).toBe(100);
});
});
describe('computedTemperature', () => {
it('should return undefined when model has no maxTemperature', () => {
expect(computedTemperature({ model: mockModel(4096), temperature: 5 })).toBeUndefined();
});
it('should scale temperature proportionally', () => {
// maxTemperature=2, temperature=5 => 2*(5/10)=1.0
expect(computedTemperature({ model: mockModel(4096, 2), temperature: 5 })).toBe(1.0);
});
it('should return maxTemperature when temperature=10', () => {
expect(computedTemperature({ model: mockModel(4096, 2), temperature: 10 })).toBe(2.0);
});
it('should enforce minimum of 0.01', () => {
expect(computedTemperature({ model: mockModel(4096, 2), temperature: 0 })).toBe(0.01);
});
it('should round to 2 decimal places', () => {
// maxTemperature=1, temperature=3 => 1*(3/10)=0.30
expect(computedTemperature({ model: mockModel(4096, 1), temperature: 3 })).toBe(0.3);
});
});
describe('parseReasoningContent', () => {
it('should return empty reasoning and full text when no think tag', () => {
expect(parseReasoningContent('hello world')).toEqual(['', 'hello world']);
});
it('should extract think content and remaining answer', () => {
expect(parseReasoningContent('<think>reasoning</think>answer')).toEqual([
'reasoning',
'answer'
]);
});
it('should trim whitespace from think content', () => {
expect(parseReasoningContent('<think> reasoning </think>answer')).toEqual([
'reasoning',
'answer'
]);
});
it('should return empty answer when nothing after think tag', () => {
expect(parseReasoningContent('<think>reasoning</think>')).toEqual(['reasoning', '']);
});
it('should handle multiline think content', () => {
expect(parseReasoningContent('<think>line1\nline2</think>answer')).toEqual([
'line1\nline2',
'answer'
]);
});
it('should only match first think tag', () => {
expect(parseReasoningContent('<think>first</think>mid<think>second</think>end')).toEqual([
'first',
'mid<think>second</think>end'
]);
});
});
describe('parseLLMStreamResponse', () => {
describe('Parse reasoning stream content test', async () => {
const partList = [
{
data: [{ content: '你好1' }, { content: '你好2' }, { content: '你好3' }],
correct: { answer: '你好1你好2你好3', reasoning: '' }
},
{
data: [
{ reasoning_content: '这是' },
{ reasoning_content: '思考' },
{ reasoning_content: '过程' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<t' },
{ content: 'hink>' },
{ content: '这是' },
{ content: '思考' },
{ content: '过程' },
{ content: '</think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>' },
{ content: '这是' },
{ content: '思考' },
{ content: '过程' },
{ content: '</think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程' },
{ content: '</think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</' },
{ content: 'think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</think>' },
{ content: '你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</think>你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</th' },
{ content: '假的' },
{ content: '你好2' },
{ content: '你好3' },
{ content: '过程</think>你好1' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程</th假的你好2你好3过程' }
},
{
data: [
{ content: '<think>这是' },
{ content: '思考' },
{ content: '过程</th' },
{ content: '假的' },
{ content: '你好2' },
{ content: '你好3' }
],
correct: { answer: '', reasoning: '这是思考过程</th假的你好2你好3' }
}
];
// Remove think
partList.forEach((part, index) => {
it(`Reasoning test:${index}`, () => {
const { parsePart } = parseLLMStreamResponse();
let answer = '';
let reasoning = '';
part.data.forEach((item) => {
const formatPart = {
choices: [
{
delta: {
role: 'assistant',
content: item.content,
reasoning_content: item.reasoning_content
}
}
]
};
const { reasoningContent, content } = parsePart({
part: formatPart,
parseThinkTag: true,
retainDatasetCite: false
});
answer += content;
reasoning += reasoningContent;
});
expect(answer).toBe(part.correct.answer);
expect(reasoning).toBe(part.correct.reasoning);
});
});
});
describe('Parse dataset cite content test', async () => {
const partList = [
{
// 完整的
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e74767063e882d6861](CITE)' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](CITE)',
responseContent: '知识库问答系统'
}
},
{
// 只要 objectId
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861]' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861]',
responseContent: '知识库问答系统'
}
},
{
// 满足替换条件的
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](',
responseContent: '知识库问答系统'
}
},
{
// 满足替换条件的
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](C' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](C',
responseContent: '知识库问答系统'
}
},
{
// 满足替换条件的
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CI' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](CI',
responseContent: '知识库问答系统'
}
},
{
// 满足替换条件的
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CIT' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](CIT',
responseContent: '知识库问答系统'
}
},
{
// 满足替换条件的
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](CITE',
responseContent: '知识库问答系统'
}
},
{
// 缺失结尾
data: [
{ content: '知识库问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](CITE',
responseContent: '知识库问答系统'
}
},
{
// ObjectId 不正确
data: [
{ content: '知识库问答系统' },
{ content: '[67e517e747' },
{ content: '67882d' },
{ content: '6861](CITE)' }
],
correct: {
content: '知识库问答系统[67e517e74767882d6861](CITE)',
responseContent: '知识库问答系统[67e517e74767882d6861](CITE)'
}
},
{
// 其他链接
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[](https://fastgpt.cn)' }
],
correct: {
content: '知识库问答系统[](https://fastgpt.cn)',
responseContent: '知识库问答系统[](https://fastgpt.cn)'
}
},
{
// 不完整的其他链接
data: [{ content: '知识库' }, { content: '问答系统' }, { content: '[](https://fastgp' }],
correct: {
content: '知识库问答系统[](https://fastgp',
responseContent: '知识库问答系统[](https://fastgp'
}
},
{
// 开头
data: [{ content: '[知识库' }, { content: '问答系统' }, { content: '[](https://fastgp' }],
correct: {
content: '[知识库问答系统[](https://fastgp',
responseContent: '[知识库问答系统[](https://fastgp'
}
},
{
// 结尾
data: [{ content: '知识库' }, { content: '问答系统' }, { content: '[' }],
correct: {
content: '知识库问答系统[',
responseContent: '知识库问答系统['
}
},
{
// 中间
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[' },
{ content: '问答系统]' }
],
correct: {
content: '知识库问答系统[问答系统]',
responseContent: '知识库问答系统[问答系统]'
}
},
{
// 双链接
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[](https://fastgpt.cn)' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE)' }
],
correct: {
content: '知识库问答系统[](https://fastgpt.cn)[67e517e74767063e882d6861](CITE)',
responseContent: '知识库问答系统[](https://fastgpt.cn)'
}
},
{
// 双链接缺失部分
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[](https://fastgpt.cn)' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CIT' }
],
correct: {
content: '知识库问答系统[](https://fastgpt.cn)[67e517e74767063e882d6861](CIT',
responseContent: '知识库问答系统[](https://fastgpt.cn)'
}
},
{
// 双Cite
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE)' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE)' }
],
correct: {
content: '知识库问答系统[67e517e74767063e882d6861](CITE)[67e517e74767063e882d6861](CITE)',
responseContent: '知识库问答系统'
}
},
{
// 双Cite-第一个假Cite
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[67e517e747' },
{ content: '6861](CITE)' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE)' }
],
correct: {
content: '知识库问答系统[67e517e7476861](CITE)[67e517e74767063e882d6861](CITE)',
responseContent: '知识库问答系统[67e517e7476861](CITE)'
}
},
{
// [id](CITE)
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[i' },
{ content: 'd](CITE)' },
{ content: '[67e517e747' },
{ content: '67063e882d' },
{ content: '6861](CITE)' }
],
correct: {
content: '知识库问答系统[id](CITE)[67e517e74767063e882d6861](CITE)',
responseContent: '知识库问答系统'
}
},
{
// [id](CITE)
data: [
{ content: '知识库' },
{ content: '问答系统' },
{ content: '[i' },
{ content: 'd](CITE)' }
],
correct: {
content: '知识库问答系统[id](CITE)',
responseContent: '知识库问答系统'
}
}
];
partList.forEach((part, index) => {
it(`Dataset cite test: ${index}`, () => {
const { parsePart } = parseLLMStreamResponse();
let answer = '';
let responseContent = '';
const list = [...part.data, { content: '' }];
list.forEach((item, index) => {
const formatPart = {
choices: [
{
delta: {
role: 'assistant',
content: item.content,
reasoning_content: ''
},
finish_reason: (index === list.length - 2 ? 'stop' : null) as CompletionFinishReason
}
]
};
const { content, responseContent: newResponseContent } = parsePart({
part: formatPart,
parseThinkTag: false,
retainDatasetCite: false
});
answer += content;
responseContent += newResponseContent;
});
expect(answer).toEqual(part.correct.content);
expect(responseContent).toEqual(part.correct.responseContent);
});
});
});
});
describe('parseJsonArgs', () => {
it('should parse valid JSON string', () => {
const result = parseJsonArgs<{ a: number }>('{"a": 1}');
expect(result).toEqual({ a: 1 });
});
it('should parse JSON5 (unquoted keys)', () => {
const result = parseJsonArgs<{ a: number }>('{a: 1}');
expect(result).toEqual({ a: 1 });
});
it('should parse JSON with trailing commas', () => {
const result = parseJsonArgs<{ a: number; b: string }>('{a: 1, b: "hello",}');
expect(result).toEqual({ a: 1, b: 'hello' });
});
it('should repair and parse broken JSON (missing closing brace)', () => {
const result = parseJsonArgs<{ a: number }>('{a: 1');
expect(result).toEqual({ a: 1 });
});
it('should extract JSON from surrounding text', () => {
const result = parseJsonArgs<{ key: string }>('prefix {"key": "value"} suffix');
expect(result).toEqual({ key: 'value' });
});
it('should parse array JSON', () => {
const result = parseJsonArgs<number[]>('[1, 2, 3]');
expect(result).toEqual([1, 2, 3]);
});
it('should return undefined for completely invalid input', () => {
// jsonrepair returns the string as-is, json5 parses it as a string — not an object
// Only truly unparseable input (e.g. unmatched braces with garbage) returns undefined
const result = parseJsonArgs('{{{invalid');
expect(result).toBeUndefined();
});
it('should return undefined for empty string', () => {
const result = parseJsonArgs('');
expect(result).toBeUndefined();
});
it('should parse nested objects', () => {
const result = parseJsonArgs<{ a: { b: number } }>('{"a": {"b": 2}}');
expect(result).toEqual({ a: { b: 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({