feat: 全新智能体功能
- 基于先进智能体框架,增加智能体编排功能 - 增加智能体聊天,并对接持久化
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
import type {ChatTimelineMessageItem} from '@easyflow/common-ui';
|
||||
|
||||
import {
|
||||
applyAgentSseEnvelope,
|
||||
parseAgentSseMessage,
|
||||
recordsToTimelineItems,
|
||||
} from './agentTimelineAdapter';
|
||||
|
||||
describe('agentTimelineAdapter', () => {
|
||||
it('projects history records to chat timeline items', () => {
|
||||
const items = recordsToTimelineItems([
|
||||
{
|
||||
id: '1',
|
||||
senderRole: 'user',
|
||||
contentText: '帮我查一下',
|
||||
roundId: 'r1',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
senderRole: 'assistant',
|
||||
contentText: '查到了',
|
||||
roundId: 'r1',
|
||||
contentPayload: {
|
||||
agentResult: {
|
||||
reasoning: '先检索',
|
||||
text: '查到了',
|
||||
knowledgeReferences: [
|
||||
{
|
||||
documentName: '手册',
|
||||
chunkContent: '内容片段',
|
||||
},
|
||||
],
|
||||
},
|
||||
chains: [
|
||||
{
|
||||
id: 'tool-1',
|
||||
name: 'search',
|
||||
status: 'TOOL_RESULT',
|
||||
arguments: { q: 'EasyFlow' },
|
||||
result: 'ok',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
items.some((item) => item.type === 'message' && item.role === 'user'),
|
||||
).toBe(true);
|
||||
const assistant = items.find(
|
||||
(item): item is ChatTimelineMessageItem =>
|
||||
item.type === 'message' && item.role === 'assistant',
|
||||
);
|
||||
expect(assistant?.parts.some((part) => part.type === 'thinking')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(assistant?.parts.some((part) => part.type === 'text')).toBe(true);
|
||||
expect(items.some((item) => item.type === 'tool')).toBe(true);
|
||||
expect(assistant?.knowledgeItems?.[0]?.documentName).toBe('手册');
|
||||
});
|
||||
|
||||
it('keeps stable ids when history has reasoning, tools and final text', () => {
|
||||
const items = recordsToTimelineItems([
|
||||
{
|
||||
id: '42',
|
||||
senderRole: 'assistant',
|
||||
contentText: '最终回答',
|
||||
roundId: 'r2',
|
||||
contentPayload: {
|
||||
agentResult: {
|
||||
text: '最终回答',
|
||||
},
|
||||
chains: [
|
||||
{
|
||||
reasoning_content: '先思考',
|
||||
},
|
||||
{
|
||||
id: 'tool-2',
|
||||
name: 'search',
|
||||
status: 'TOOL_RESULT',
|
||||
result: 'ok',
|
||||
},
|
||||
],
|
||||
messageChain: [
|
||||
{
|
||||
role: 'assistant',
|
||||
reasoningContent: '中间思考',
|
||||
toolCalls: [{ id: 'tool-2', name: 'search', arguments: '{}' }],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
toolCallId: 'tool-2',
|
||||
content: 'ok',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const ids = items.map((item) => item.id);
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
expect(
|
||||
items.filter(
|
||||
(item) => item.type === 'message' && item.role === 'assistant',
|
||||
),
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
items.some((item) => item.type === 'tool' && item.status === 'success'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('parses raw SSE text as message delta', () => {
|
||||
const envelope = parseAgentSseMessage({
|
||||
data: 'hello',
|
||||
event: '',
|
||||
id: '',
|
||||
retry: undefined,
|
||||
});
|
||||
|
||||
expect(envelope).toMatchObject({
|
||||
domain: 'LLM',
|
||||
type: 'MESSAGE',
|
||||
payload: { delta: 'hello' },
|
||||
});
|
||||
});
|
||||
|
||||
it('applies streaming text, HITL approval and error envelopes', () => {
|
||||
const items: any[] = [];
|
||||
|
||||
applyAgentSseEnvelope(items, {
|
||||
domain: 'LLM',
|
||||
type: 'MESSAGE',
|
||||
payload: { delta: '你好' },
|
||||
});
|
||||
applyAgentSseEnvelope(items, {
|
||||
domain: 'TOOL',
|
||||
type: 'FORM_REQUEST',
|
||||
payload: {
|
||||
requestId: 'req-1',
|
||||
resumeToken: 'token-1',
|
||||
toolCallId: 'tool-1',
|
||||
toolName: 'workflow',
|
||||
input: { name: 'demo' },
|
||||
},
|
||||
});
|
||||
applyAgentSseEnvelope(items, {
|
||||
domain: 'ERROR',
|
||||
type: 'ERROR',
|
||||
payload: { message: '失败' },
|
||||
});
|
||||
|
||||
const assistant = items.find(
|
||||
(item): item is ChatTimelineMessageItem =>
|
||||
item.type === 'message' && item.role === 'assistant',
|
||||
);
|
||||
const tool = items.find((item) => item.type === 'tool');
|
||||
const error = items.find((item) => item.type === 'error');
|
||||
|
||||
expect(assistant?.parts[0]?.content).toBe('你好');
|
||||
expect(tool?.status).toBe('pending_approval');
|
||||
expect(tool?.approval?.resumeToken).toBe('token-1');
|
||||
expect(error?.message).toBe('失败');
|
||||
});
|
||||
|
||||
it('keeps assistant text and approval card when a tool request is rejected', () => {
|
||||
const items: any[] = [];
|
||||
|
||||
applyAgentSseEnvelope(items, {
|
||||
domain: 'LLM',
|
||||
type: 'MESSAGE',
|
||||
payload: { delta: '正在处理' },
|
||||
});
|
||||
applyAgentSseEnvelope(items, {
|
||||
domain: 'TOOL',
|
||||
type: 'FORM_REQUEST',
|
||||
payload: {
|
||||
requestId: 'req-2',
|
||||
resumeToken: 'token-2',
|
||||
toolCallId: 'tool-2',
|
||||
toolName: '审批工具',
|
||||
input: { name: 'demo' },
|
||||
},
|
||||
});
|
||||
applyAgentSseEnvelope(items, {
|
||||
domain: 'TOOL',
|
||||
type: 'FORM_REJECTED',
|
||||
payload: {
|
||||
requestId: 'req-2',
|
||||
resumeToken: 'token-2',
|
||||
toolCallId: 'tool-2',
|
||||
reason: '用户拒绝执行',
|
||||
},
|
||||
});
|
||||
|
||||
const assistant = items.find(
|
||||
(item): item is ChatTimelineMessageItem =>
|
||||
item.type === 'message' && item.role === 'assistant',
|
||||
);
|
||||
const tool = items.find((item) => item.type === 'tool');
|
||||
|
||||
expect(assistant?.parts[0]?.content).toBe('正在处理');
|
||||
expect(items).toHaveLength(2);
|
||||
expect(tool?.status).toBe('rejected');
|
||||
expect(tool?.rejectReason).toBe('用户拒绝执行');
|
||||
});
|
||||
|
||||
it('applies streaming round metadata to assistant messages for action toolbar anchoring', () => {
|
||||
const items: any[] = [];
|
||||
|
||||
applyAgentSseEnvelope(
|
||||
items,
|
||||
{
|
||||
domain: 'LLM',
|
||||
type: 'MESSAGE',
|
||||
payload: { delta: '准备调用工具' },
|
||||
},
|
||||
{ roundId: 'runtime-round-1' },
|
||||
);
|
||||
|
||||
const assistant = items.find(
|
||||
(item): item is ChatTimelineMessageItem =>
|
||||
item.type === 'message' && item.role === 'assistant',
|
||||
);
|
||||
|
||||
expect(assistant?.roundId).toBe('runtime-round-1');
|
||||
expect(assistant?.parts[0]?.content).toBe('准备调用工具');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user