mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-01 02:01:34 +08:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user