feat: 全新智能体功能
- 基于先进智能体框架,增加智能体编排功能 - 增加智能体聊天,并对接持久化
This commit is contained in:
@@ -0,0 +1,572 @@
|
||||
import type {
|
||||
ChatTimelineItem,
|
||||
ChatTimelineKnowledgeHit,
|
||||
ChatTimelineMessageItem,
|
||||
ChatTimelineMessagePart,
|
||||
ChatTimelineStatusItem,
|
||||
ChatTimelineStatusStatus,
|
||||
ChatTimelineStatusTone,
|
||||
ChatTimelineThinkingStatus,
|
||||
ChatTimelineToolApprovalPayload,
|
||||
ChatTimelineToolItem,
|
||||
ChatTimelineToolMode,
|
||||
ChatTimelineToolStatus,
|
||||
} from './types';
|
||||
|
||||
function createId(prefix: string) {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function normalizeText(value: unknown) {
|
||||
return value === null || value === undefined ? '' : String(value);
|
||||
}
|
||||
|
||||
function normalizePayloadValue(value: unknown) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeToolName(value?: string) {
|
||||
return normalizeText(value).trim().toLowerCase();
|
||||
}
|
||||
|
||||
function isHiddenToolName(toolName?: string) {
|
||||
const normalizedName = normalizeToolName(toolName);
|
||||
return (
|
||||
normalizedName === 'retrieve_knowledge' || normalizedName === '__fragment__'
|
||||
);
|
||||
}
|
||||
|
||||
function isKnowledgeRetrievalToolName(toolName?: string) {
|
||||
return normalizeToolName(toolName) === 'retrieve_knowledge';
|
||||
}
|
||||
|
||||
function isBlankToolName(toolName?: string) {
|
||||
return !normalizeToolName(toolName);
|
||||
}
|
||||
|
||||
function knowledgeRetrievalStatusKey(statusKey?: string) {
|
||||
return normalizeText(statusKey).trim() || 'knowledge-retrieval';
|
||||
}
|
||||
|
||||
function ensureMessageTail(
|
||||
items: ChatTimelineItem[],
|
||||
role: ChatTimelineMessageItem['role'],
|
||||
status: ChatTimelineMessageItem['status'] = 'streaming',
|
||||
metadata?: Partial<ChatTimelineMessageItem>,
|
||||
) {
|
||||
const last = items[items.length - 1];
|
||||
if (
|
||||
last?.type === 'message' &&
|
||||
last.role === role &&
|
||||
last.status !== 'done' &&
|
||||
(!metadata?.roundId || last.roundId === metadata.roundId)
|
||||
) {
|
||||
last.status = status;
|
||||
Object.assign(last, metadata);
|
||||
return last;
|
||||
}
|
||||
const item: ChatTimelineMessageItem = {
|
||||
id: createId(role),
|
||||
role,
|
||||
status,
|
||||
createdAt: Date.now(),
|
||||
parts: [],
|
||||
type: 'message',
|
||||
...metadata,
|
||||
};
|
||||
items.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
function appendMessagePart(
|
||||
message: ChatTimelineMessageItem,
|
||||
part: ChatTimelineMessagePart,
|
||||
) {
|
||||
const tail = message.parts[message.parts.length - 1];
|
||||
if (tail?.type === part.type) {
|
||||
tail.content += part.content;
|
||||
if (tail.type === 'thinking' && part.type === 'thinking') {
|
||||
tail.status = part.status;
|
||||
}
|
||||
return;
|
||||
}
|
||||
message.parts.push(part);
|
||||
}
|
||||
|
||||
function appendThinkingPart(
|
||||
message: ChatTimelineMessageItem,
|
||||
part: Extract<ChatTimelineMessagePart, { type: 'thinking' }>,
|
||||
) {
|
||||
appendMessagePart(message, part);
|
||||
}
|
||||
|
||||
function appendTextPart(message: ChatTimelineMessageItem, content: string) {
|
||||
appendMessagePart(message, {
|
||||
id: createId('text'),
|
||||
content,
|
||||
type: 'text',
|
||||
});
|
||||
}
|
||||
|
||||
function replaceTextPart(message: ChatTimelineMessageItem, content: string) {
|
||||
message.parts = [
|
||||
...message.parts.filter((part) => part.type !== 'text'),
|
||||
{
|
||||
id: createId('text'),
|
||||
content,
|
||||
type: 'text' as const,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function updateThinkingStatus(
|
||||
message: ChatTimelineMessageItem,
|
||||
status: ChatTimelineThinkingStatus,
|
||||
) {
|
||||
message.parts = message.parts.map((part) =>
|
||||
part.type === 'thinking' && part.status === 'thinking'
|
||||
? { ...part, expanded: status === 'thinking', status }
|
||||
: part,
|
||||
);
|
||||
}
|
||||
|
||||
function finishLastAssistantMessage(items: ChatTimelineItem[]) {
|
||||
finishAssistantMessage(items, true);
|
||||
}
|
||||
|
||||
function finishAssistantMessage(
|
||||
items: ChatTimelineItem[],
|
||||
roundCompleted: boolean,
|
||||
) {
|
||||
const lastMessage = [...items]
|
||||
.reverse()
|
||||
.find(
|
||||
(item): item is ChatTimelineMessageItem =>
|
||||
item.type === 'message' && item.role === 'assistant',
|
||||
);
|
||||
if (!lastMessage) {
|
||||
return;
|
||||
}
|
||||
updateThinkingStatus(lastMessage, 'end');
|
||||
lastMessage.status = lastMessage.status === 'error' ? 'error' : 'done';
|
||||
if (lastMessage.status === 'done' && roundCompleted) {
|
||||
lastMessage.roundCompleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
function findToolItem(items: ChatTimelineItem[], toolCallId?: string) {
|
||||
const identity = normalizeText(toolCallId).trim();
|
||||
if (!identity) {
|
||||
return undefined;
|
||||
}
|
||||
return items.find(
|
||||
(item): item is ChatTimelineToolItem =>
|
||||
item.type === 'tool' && item.toolCallId === identity,
|
||||
);
|
||||
}
|
||||
|
||||
function findStatusItem(items: ChatTimelineItem[], statusKey: string) {
|
||||
return items.find(
|
||||
(item): item is ChatTimelineStatusItem =>
|
||||
item.type === 'status' && item.statusKey === statusKey,
|
||||
);
|
||||
}
|
||||
|
||||
function doneStatusLabel(item: ChatTimelineStatusItem) {
|
||||
if (item.statusKey === 'knowledge-retrieval') {
|
||||
return '已检索知识库';
|
||||
}
|
||||
if (item.statusKey === 'memory-compression') {
|
||||
return '已整理上下文';
|
||||
}
|
||||
return item.label.replace(/^正在/, '已');
|
||||
}
|
||||
|
||||
function finishRunningStatusItems(items: ChatTimelineItem[]) {
|
||||
items.forEach((item) => {
|
||||
if (item.type !== 'status' || item.status !== 'running') {
|
||||
return;
|
||||
}
|
||||
item.status = 'done';
|
||||
item.label = doneStatusLabel(item);
|
||||
});
|
||||
}
|
||||
|
||||
function upsertStatus(
|
||||
items: ChatTimelineItem[],
|
||||
payload: {
|
||||
label: string;
|
||||
presentation?: ChatTimelineStatusItem['presentation'];
|
||||
status: ChatTimelineStatusStatus;
|
||||
statusKey: string;
|
||||
tone?: ChatTimelineStatusTone;
|
||||
},
|
||||
) {
|
||||
const found = findStatusItem(items, payload.statusKey);
|
||||
if (found) {
|
||||
found.label = payload.label;
|
||||
found.presentation = payload.presentation ?? found.presentation;
|
||||
found.status = payload.status;
|
||||
found.tone = payload.tone ?? found.tone;
|
||||
return found;
|
||||
}
|
||||
const item: ChatTimelineStatusItem = {
|
||||
id: payload.statusKey,
|
||||
createdAt: Date.now(),
|
||||
label: payload.label,
|
||||
presentation: payload.presentation,
|
||||
status: payload.status,
|
||||
statusKey: payload.statusKey,
|
||||
tone: payload.tone ?? 'muted',
|
||||
type: 'status',
|
||||
};
|
||||
items.push(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
function upsertTool(
|
||||
items: ChatTimelineItem[],
|
||||
payload: {
|
||||
approval?: ChatTimelineToolApprovalPayload;
|
||||
input?: unknown;
|
||||
mode?: ChatTimelineToolMode;
|
||||
output?: unknown;
|
||||
rejectReason?: string;
|
||||
requestId?: string;
|
||||
resumeToken?: string;
|
||||
status?: ChatTimelineToolStatus;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
},
|
||||
) {
|
||||
const toolCallId = normalizeText(
|
||||
payload.toolCallId ?? payload.approval?.toolCallId,
|
||||
).trim();
|
||||
const found = findToolItem(items, toolCallId);
|
||||
const approval = payload.approval ?? found?.approval;
|
||||
const mode =
|
||||
payload.mode === 'approval'
|
||||
? 'approval'
|
||||
: (found?.mode ?? payload.mode ?? (approval ? 'approval' : 'auto'));
|
||||
const toolName =
|
||||
payload.toolName ||
|
||||
approval?.toolDisplayName ||
|
||||
approval?.toolName ||
|
||||
found?.toolName;
|
||||
if (isHiddenToolName(toolName)) {
|
||||
return found;
|
||||
}
|
||||
if (!found && isBlankToolName(toolName)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
found.approval = approval;
|
||||
found.input =
|
||||
normalizePayloadValue(payload.input) ??
|
||||
normalizePayloadValue(approval?.input) ??
|
||||
found.input;
|
||||
found.mode = mode;
|
||||
found.output = normalizePayloadValue(payload.output) ?? found.output;
|
||||
found.rejectReason = payload.rejectReason ?? found.rejectReason;
|
||||
found.status = payload.status || found.status;
|
||||
found.toolCallId = toolCallId || found.toolCallId;
|
||||
found.toolName = toolName || found.toolName;
|
||||
return found;
|
||||
}
|
||||
|
||||
const toolItem: ChatTimelineToolItem = {
|
||||
id: toolCallId || createId('tool'),
|
||||
approval,
|
||||
createdAt: Date.now(),
|
||||
input:
|
||||
normalizePayloadValue(payload.input) ??
|
||||
normalizePayloadValue(approval?.input),
|
||||
mode,
|
||||
output: normalizePayloadValue(payload.output),
|
||||
rejectReason: payload.rejectReason,
|
||||
status:
|
||||
payload.status || (mode === 'approval' ? 'pending_approval' : 'running'),
|
||||
toolCallId,
|
||||
toolName: toolName || '工具调用',
|
||||
type: 'tool',
|
||||
};
|
||||
items.push(toolItem);
|
||||
return toolItem;
|
||||
}
|
||||
|
||||
export const ChatTimelineBuilder = {
|
||||
appendUserMessage(
|
||||
items: ChatTimelineItem[],
|
||||
content?: unknown,
|
||||
metadata?: Partial<ChatTimelineMessageItem>,
|
||||
) {
|
||||
const text = normalizeText(content);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
const item: ChatTimelineMessageItem = {
|
||||
id: createId('user'),
|
||||
role: 'user',
|
||||
status: 'done',
|
||||
createdAt: Date.now(),
|
||||
parts: [
|
||||
{
|
||||
id: createId('text'),
|
||||
content: text,
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
type: 'message',
|
||||
...metadata,
|
||||
};
|
||||
items.push(item);
|
||||
},
|
||||
|
||||
appendThinkingDelta(
|
||||
items: ChatTimelineItem[],
|
||||
delta?: unknown,
|
||||
metadata?: Partial<ChatTimelineMessageItem>,
|
||||
) {
|
||||
const text = normalizeText(delta);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
const message = ensureMessageTail(
|
||||
items,
|
||||
'assistant',
|
||||
'streaming',
|
||||
metadata,
|
||||
);
|
||||
if (message.parts.some((part) => part.type === 'text')) {
|
||||
return;
|
||||
}
|
||||
appendThinkingPart(message, {
|
||||
id: createId('thinking'),
|
||||
content: text,
|
||||
expanded: true,
|
||||
status: 'thinking',
|
||||
type: 'thinking',
|
||||
});
|
||||
},
|
||||
|
||||
appendMessageDelta(
|
||||
items: ChatTimelineItem[],
|
||||
delta?: unknown,
|
||||
metadata?: Partial<ChatTimelineMessageItem>,
|
||||
) {
|
||||
const text = normalizeText(delta);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
const message = ensureMessageTail(
|
||||
items,
|
||||
'assistant',
|
||||
'streaming',
|
||||
metadata,
|
||||
);
|
||||
updateThinkingStatus(message, 'end');
|
||||
appendTextPart(message, text);
|
||||
},
|
||||
|
||||
replaceMessageContent(items: ChatTimelineItem[], content?: unknown) {
|
||||
const text = normalizeText(content);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
const message = ensureMessageTail(items, 'assistant', 'done');
|
||||
updateThinkingStatus(message, 'end');
|
||||
replaceTextPart(message, text);
|
||||
},
|
||||
|
||||
appendToolApproval(
|
||||
items: ChatTimelineItem[],
|
||||
payload: ChatTimelineToolApprovalPayload,
|
||||
) {
|
||||
upsertTool(items, {
|
||||
approval: payload,
|
||||
input: payload.input,
|
||||
mode: 'approval',
|
||||
status: 'pending_approval',
|
||||
toolCallId: payload.toolCallId,
|
||||
toolName: payload.toolDisplayName || payload.toolName,
|
||||
});
|
||||
},
|
||||
|
||||
upsertToolCall(
|
||||
items: ChatTimelineItem[],
|
||||
payload: {
|
||||
input?: unknown;
|
||||
output?: unknown;
|
||||
status?: ChatTimelineToolStatus;
|
||||
statusKey?: string;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
},
|
||||
) {
|
||||
if (isKnowledgeRetrievalToolName(payload.toolName)) {
|
||||
ChatTimelineBuilder.upsertKnowledgeRetrievalStatus(
|
||||
items,
|
||||
payload.status === 'success' ? 'done' : 'running',
|
||||
payload.statusKey,
|
||||
);
|
||||
return;
|
||||
}
|
||||
upsertTool(items, {
|
||||
...payload,
|
||||
mode: 'auto',
|
||||
status: payload.status || 'running',
|
||||
});
|
||||
},
|
||||
|
||||
upsertKnowledgeRetrievalStatus(
|
||||
items: ChatTimelineItem[],
|
||||
status: ChatTimelineStatusStatus,
|
||||
statusKey?: string,
|
||||
) {
|
||||
finishAssistantMessage(items, false);
|
||||
upsertStatus(items, {
|
||||
label: status === 'running' ? '正在检索知识库' : '已检索知识库',
|
||||
status,
|
||||
statusKey: knowledgeRetrievalStatusKey(statusKey),
|
||||
tone: 'muted',
|
||||
});
|
||||
},
|
||||
|
||||
upsertMemoryCompressionStatus(
|
||||
items: ChatTimelineItem[],
|
||||
payload?: {
|
||||
compressed?: boolean;
|
||||
label?: string;
|
||||
phase?: string;
|
||||
status?: string;
|
||||
statusKey?: string;
|
||||
},
|
||||
) {
|
||||
const status =
|
||||
payload?.status === 'done' || payload?.phase === 'completed'
|
||||
? 'done'
|
||||
: 'running';
|
||||
finishAssistantMessage(items, false);
|
||||
const label =
|
||||
status === 'running'
|
||||
? payload?.label || '正在整理上下文'
|
||||
: payload?.compressed === false
|
||||
? '无需压缩上下文'
|
||||
: payload?.label || '已整理上下文';
|
||||
upsertStatus(items, {
|
||||
label,
|
||||
status,
|
||||
statusKey: payload?.statusKey || 'memory-compression',
|
||||
presentation: 'separator',
|
||||
tone: 'muted',
|
||||
});
|
||||
},
|
||||
|
||||
markToolApproving(
|
||||
items: ChatTimelineItem[],
|
||||
payload: {
|
||||
requestId?: string;
|
||||
resumeToken?: string;
|
||||
toolCallId?: string;
|
||||
},
|
||||
) {
|
||||
upsertTool(items, {
|
||||
...payload,
|
||||
mode: 'approval',
|
||||
status: 'approving',
|
||||
});
|
||||
},
|
||||
|
||||
markToolRejected(
|
||||
items: ChatTimelineItem[],
|
||||
payload: {
|
||||
reason?: string;
|
||||
requestId?: string;
|
||||
resumeToken?: string;
|
||||
toolCallId?: string;
|
||||
},
|
||||
) {
|
||||
upsertTool(items, {
|
||||
...payload,
|
||||
mode: 'approval',
|
||||
rejectReason: payload.reason,
|
||||
status: 'rejected',
|
||||
});
|
||||
},
|
||||
|
||||
appendKnowledge(
|
||||
items: ChatTimelineItem[],
|
||||
knowledgeItems: ChatTimelineKnowledgeHit[],
|
||||
) {
|
||||
if (knowledgeItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
const lastAssistantMessage = [...items]
|
||||
.reverse()
|
||||
.find(
|
||||
(item): item is ChatTimelineMessageItem =>
|
||||
item.type === 'message' && item.role === 'assistant',
|
||||
);
|
||||
if (lastAssistantMessage) {
|
||||
lastAssistantMessage.knowledgeItems = [
|
||||
...(lastAssistantMessage.knowledgeItems || []),
|
||||
...knowledgeItems,
|
||||
];
|
||||
return;
|
||||
}
|
||||
const last = items[items.length - 1];
|
||||
if (last?.type === 'knowledge') {
|
||||
last.items.push(...knowledgeItems);
|
||||
return;
|
||||
}
|
||||
items.push({
|
||||
id: createId('knowledge'),
|
||||
createdAt: Date.now(),
|
||||
items: knowledgeItems,
|
||||
type: 'knowledge',
|
||||
});
|
||||
},
|
||||
|
||||
appendError(items: ChatTimelineItem[], message?: unknown) {
|
||||
const text = normalizeText(message) || '请求失败';
|
||||
const last = items[items.length - 1];
|
||||
if (last?.type === 'message' && last.role === 'assistant') {
|
||||
updateThinkingStatus(last, 'error');
|
||||
last.status = 'error';
|
||||
}
|
||||
items.push({
|
||||
id: createId('error'),
|
||||
createdAt: Date.now(),
|
||||
message: text,
|
||||
type: 'error',
|
||||
});
|
||||
},
|
||||
|
||||
finalize(items: ChatTimelineItem[]) {
|
||||
finishRunningStatusItems(items);
|
||||
finishLastAssistantMessage(items);
|
||||
},
|
||||
|
||||
replaceRoundAssistant(
|
||||
items: ChatTimelineItem[],
|
||||
roundId: string,
|
||||
message: ChatTimelineMessageItem,
|
||||
) {
|
||||
const targetIndex = items.findIndex(
|
||||
(item): item is ChatTimelineMessageItem =>
|
||||
item.type === 'message' &&
|
||||
item.role === 'assistant' &&
|
||||
item.roundId === roundId,
|
||||
);
|
||||
if (targetIndex >= 0) {
|
||||
items.splice(targetIndex, 1, message);
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user