feat: 全新智能体功能
- 基于先进智能体框架,增加智能体编排功能 - 增加智能体聊天,并对接持久化
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
import type {ServerSentEventMessage} from 'fetch-event-stream';
|
||||
|
||||
import type {
|
||||
ChatTimelineItem,
|
||||
ChatTimelineKnowledgeHit,
|
||||
ChatTimelineMessageItem,
|
||||
ChatTimelineToolApprovalPayload,
|
||||
} from '@easyflow/common-ui';
|
||||
import {ChatTimelineBuilder} from '@easyflow/common-ui';
|
||||
|
||||
import type {AgentChatMessageRecord} from '../api';
|
||||
|
||||
export interface AgentSseEnvelope {
|
||||
domain: string;
|
||||
payload: Record<string, any>;
|
||||
type: string;
|
||||
}
|
||||
|
||||
function asText(value: unknown) {
|
||||
return value === null || value === undefined ? '' : String(value);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, any> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, any>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function asArray(value: unknown): any[] {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
function asTimestamp(value: unknown) {
|
||||
if (!value) {
|
||||
return Date.now();
|
||||
}
|
||||
const timestamp = new Date(String(value)).getTime();
|
||||
return Number.isFinite(timestamp) ? timestamp : Date.now();
|
||||
}
|
||||
|
||||
function normalizeRole(value: unknown): 'assistant' | 'system' | 'user' {
|
||||
const role = asText(value).toLowerCase();
|
||||
if (role === 'assistant' || role === 'system' || role === 'user') {
|
||||
return role;
|
||||
}
|
||||
return 'assistant';
|
||||
}
|
||||
|
||||
function normalizeToolName(value: unknown) {
|
||||
return asText(value).trim();
|
||||
}
|
||||
|
||||
function normalizeToolCallId(payload: Record<string, any>) {
|
||||
return asText(payload.toolCallId ?? payload.tool_call_id ?? payload.id);
|
||||
}
|
||||
|
||||
function normalizeMetadata(record: AgentChatMessageRecord) {
|
||||
return {
|
||||
createdAt: asTimestamp(record.created),
|
||||
id: `history-${record.id || record.roundId || Date.now()}`,
|
||||
roundId: asText(record.roundId),
|
||||
roundNo: record.roundNo,
|
||||
selectedVariantIndex: record.selectedVariantIndex,
|
||||
switchable: false,
|
||||
variantCount: record.variantCount,
|
||||
variantIndex: record.variantIndex,
|
||||
} satisfies Partial<ChatTimelineMessageItem>;
|
||||
}
|
||||
|
||||
function assistantMetadata(
|
||||
record: AgentChatMessageRecord,
|
||||
suffix?: string,
|
||||
): Partial<ChatTimelineMessageItem> {
|
||||
const metadata = normalizeMetadata(record);
|
||||
return suffix ? { ...metadata, id: `${metadata.id}-${suffix}` } : metadata;
|
||||
}
|
||||
|
||||
function normalizeKnowledgeItems(payload: Record<string, any>) {
|
||||
const rawItems =
|
||||
asArray(payload.items).length > 0
|
||||
? asArray(payload.items)
|
||||
: asArray(payload.knowledgeReferences).length > 0
|
||||
? asArray(payload.knowledgeReferences)
|
||||
: asArray(payload.knowledgeCitations);
|
||||
return rawItems
|
||||
.map((item, index): ChatTimelineKnowledgeHit => {
|
||||
const source = asRecord(item);
|
||||
const metadata = asRecord(source.metadata);
|
||||
const documentName = asText(
|
||||
source.documentName ?? source.title ?? metadata.documentName,
|
||||
);
|
||||
const sourceFileName = asText(
|
||||
source.sourceFileName ?? metadata.sourceFileName,
|
||||
);
|
||||
const chunkContent = asText(
|
||||
source.chunkContent ?? source.content ?? source.text ?? source.summary,
|
||||
);
|
||||
return {
|
||||
...source,
|
||||
id: asText(source.id ?? source.chunkId ?? index),
|
||||
chunkContent,
|
||||
content: asText(source.content ?? source.text ?? source.summary),
|
||||
documentId: asText(source.documentId ?? metadata.documentId),
|
||||
documentName,
|
||||
knowledgeId: asText(source.knowledgeId ?? payload.knowledgeId),
|
||||
knowledgeName: asText(source.knowledgeName ?? payload.knowledgeName),
|
||||
metadata,
|
||||
score: source.score ?? source.similarity,
|
||||
sourceFileName,
|
||||
sourceUri: asText(source.sourceUri ?? metadata.sourceUri),
|
||||
title: documentName || sourceFileName || asText(source.source),
|
||||
};
|
||||
})
|
||||
.filter((item) => item.chunkContent || item.title || item.documentName);
|
||||
}
|
||||
|
||||
function buildApprovalPayload(payload: Record<string, any>) {
|
||||
return {
|
||||
expiresAt: asText(payload.expiresAt),
|
||||
input: payload.input,
|
||||
metadata: payload.metadata,
|
||||
requestId: asText(payload.requestId),
|
||||
resumeToken: asText(payload.resumeToken),
|
||||
toolCallId: normalizeToolCallId(payload),
|
||||
toolDisplayName: asText(payload.toolDisplayName),
|
||||
toolName: normalizeToolName(payload.toolName ?? payload.name) || '工具调用',
|
||||
toolType: asText(payload.toolType),
|
||||
} satisfies ChatTimelineToolApprovalPayload;
|
||||
}
|
||||
|
||||
function appendAssistantText(
|
||||
items: ChatTimelineItem[],
|
||||
record: AgentChatMessageRecord,
|
||||
content: unknown,
|
||||
suffix?: string,
|
||||
metadata?: Partial<ChatTimelineMessageItem>,
|
||||
) {
|
||||
const text = asText(content);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
ChatTimelineBuilder.appendMessageDelta(
|
||||
items,
|
||||
text,
|
||||
{
|
||||
...assistantMetadata(record, suffix),
|
||||
...metadata,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function appendAssistantThinking(
|
||||
items: ChatTimelineItem[],
|
||||
record: AgentChatMessageRecord,
|
||||
content: unknown,
|
||||
suffix?: string,
|
||||
metadata?: Partial<ChatTimelineMessageItem>,
|
||||
) {
|
||||
const text = asText(content);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
ChatTimelineBuilder.appendThinkingDelta(
|
||||
items,
|
||||
text,
|
||||
{
|
||||
...assistantMetadata(record, suffix),
|
||||
...metadata,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function projectHistoryChain(
|
||||
items: ChatTimelineItem[],
|
||||
record: AgentChatMessageRecord,
|
||||
) {
|
||||
const payload = asRecord(record.contentPayload);
|
||||
let hasAssistantText = false;
|
||||
let hasAssistantThinking = false;
|
||||
const displayChains = asArray(payload.displayChains ?? payload.chains);
|
||||
for (const chain of displayChains) {
|
||||
const item = asRecord(chain);
|
||||
const reasoning = item.reasoningContent ?? item.reasoning_content;
|
||||
if (reasoning) {
|
||||
appendAssistantThinking(items, record, reasoning, 'thinking');
|
||||
hasAssistantThinking = true;
|
||||
continue;
|
||||
}
|
||||
const toolName = normalizeToolName(item.name ?? item.toolName);
|
||||
if (toolName) {
|
||||
ChatTimelineBuilder.upsertToolCall(items, {
|
||||
input: item.arguments ?? item.input,
|
||||
output: item.result ?? item.output,
|
||||
status: asText(item.status) === 'TOOL_RESULT' ? 'success' : 'running',
|
||||
toolCallId: asText(item.id ?? item.toolCallId),
|
||||
toolName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const messageChain = asArray(payload.messageChain);
|
||||
for (const chain of messageChain) {
|
||||
const item = asRecord(chain);
|
||||
const role = asText(item.role).toLowerCase();
|
||||
if (role === 'assistant') {
|
||||
appendAssistantThinking(items, record, item.reasoningContent, 'thinking');
|
||||
if (item.reasoningContent) {
|
||||
hasAssistantThinking = true;
|
||||
}
|
||||
if (!payload.agentResult && item.content) {
|
||||
appendAssistantText(items, record, item.content, 'text');
|
||||
hasAssistantText = true;
|
||||
}
|
||||
for (const toolCall of asArray(item.toolCalls)) {
|
||||
const tool = asRecord(toolCall);
|
||||
ChatTimelineBuilder.upsertToolCall(items, {
|
||||
input: tool.arguments ?? tool.input,
|
||||
status: 'running',
|
||||
toolCallId: asText(tool.id ?? tool.toolCallId),
|
||||
toolName: normalizeToolName(tool.name ?? tool.toolName),
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (role === 'tool') {
|
||||
ChatTimelineBuilder.upsertToolCall(items, {
|
||||
output: item.content ?? item.result,
|
||||
status: 'success',
|
||||
toolCallId: asText(item.toolCallId ?? item.id),
|
||||
toolName: normalizeToolName(item.name ?? item.toolName) || '工具调用',
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
hasAssistantText,
|
||||
hasAssistantThinking,
|
||||
};
|
||||
}
|
||||
|
||||
function appendHistoryRecord(
|
||||
items: ChatTimelineItem[],
|
||||
record: AgentChatMessageRecord,
|
||||
) {
|
||||
const role = normalizeRole(record.senderRole);
|
||||
const metadata = normalizeMetadata(record);
|
||||
if (role === 'user') {
|
||||
ChatTimelineBuilder.appendUserMessage(items, record.contentText, metadata);
|
||||
return;
|
||||
}
|
||||
if (role === 'system') {
|
||||
ChatTimelineBuilder.appendError(items, record.contentText || '系统消息');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = asRecord(record.contentPayload);
|
||||
const agentResult = asRecord(payload.agentResult);
|
||||
const chainProjection = projectHistoryChain(items, record);
|
||||
if (!chainProjection.hasAssistantThinking) {
|
||||
appendAssistantThinking(
|
||||
items,
|
||||
record,
|
||||
payload.reasoningContent ?? agentResult.reasoning,
|
||||
'thinking',
|
||||
);
|
||||
}
|
||||
if (!chainProjection.hasAssistantText) {
|
||||
appendAssistantText(
|
||||
items,
|
||||
record,
|
||||
agentResult.text ?? payload.content ?? record.contentText,
|
||||
chainProjection.hasAssistantThinking ? 'text' : undefined,
|
||||
);
|
||||
}
|
||||
const knowledgeItems = normalizeKnowledgeItems({
|
||||
...payload,
|
||||
items:
|
||||
payload.knowledgeCitations ??
|
||||
agentResult.knowledgeReferences ??
|
||||
payload.knowledgeReferences,
|
||||
});
|
||||
if (knowledgeItems.length > 0) {
|
||||
ChatTimelineBuilder.appendKnowledge(items, knowledgeItems);
|
||||
}
|
||||
ChatTimelineBuilder.finalize(items);
|
||||
}
|
||||
|
||||
export function recordsToTimelineItems(records: AgentChatMessageRecord[] = []) {
|
||||
const items: ChatTimelineItem[] = [];
|
||||
for (const record of records) {
|
||||
appendHistoryRecord(items, record);
|
||||
}
|
||||
ChatTimelineBuilder.finalize(items);
|
||||
return items;
|
||||
}
|
||||
|
||||
export function parseAgentSseMessage(message: ServerSentEventMessage) {
|
||||
const raw = message.data || '';
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(raw);
|
||||
return {
|
||||
domain: asText(
|
||||
data.domain ?? data.eventDomain ?? data.typeDomain,
|
||||
).toUpperCase(),
|
||||
payload: asRecord(data.payload ?? data.data ?? data),
|
||||
type: asText(
|
||||
data.type ?? data.eventType ?? data.chatType ?? data.event,
|
||||
).toUpperCase(),
|
||||
} satisfies AgentSseEnvelope;
|
||||
} catch {
|
||||
return {
|
||||
domain: 'LLM',
|
||||
payload: { delta: raw },
|
||||
type: 'MESSAGE',
|
||||
} satisfies AgentSseEnvelope;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyAgentSseEnvelope(
|
||||
items: ChatTimelineItem[],
|
||||
envelope: AgentSseEnvelope,
|
||||
metadata?: Partial<ChatTimelineMessageItem>,
|
||||
) {
|
||||
const { domain, payload, type } = envelope;
|
||||
if (domain === 'LLM' && type === 'MESSAGE') {
|
||||
ChatTimelineBuilder.appendMessageDelta(
|
||||
items,
|
||||
payload.delta ?? payload.text,
|
||||
metadata,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (domain === 'LLM' && type === 'THINKING') {
|
||||
ChatTimelineBuilder.appendThinkingDelta(
|
||||
items,
|
||||
payload.reasoning ?? payload.delta ?? payload.text,
|
||||
metadata,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (domain === 'TOOL' && type === 'FORM_REQUEST') {
|
||||
ChatTimelineBuilder.appendToolApproval(
|
||||
items,
|
||||
buildApprovalPayload(payload),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (domain === 'TOOL' && type === 'FORM_APPROVING') {
|
||||
ChatTimelineBuilder.markToolApproving(items, {
|
||||
requestId: asText(payload.requestId),
|
||||
resumeToken: asText(payload.resumeToken),
|
||||
toolCallId: normalizeToolCallId(payload),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (domain === 'TOOL' && type === 'FORM_REJECTED') {
|
||||
ChatTimelineBuilder.markToolRejected(items, {
|
||||
reason: asText(payload.reason),
|
||||
requestId: asText(payload.requestId),
|
||||
resumeToken: asText(payload.resumeToken),
|
||||
toolCallId: normalizeToolCallId(payload),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (domain === 'TOOL' && (type === 'TOOL_CALL' || type === 'TOOL_RESULT')) {
|
||||
ChatTimelineBuilder.upsertToolCall(items, {
|
||||
input: payload.input ?? payload.toolInput,
|
||||
output: payload.output ?? payload.result ?? payload.text,
|
||||
status: type === 'TOOL_RESULT' ? 'success' : 'running',
|
||||
statusKey: asText(payload.statusKey) || undefined,
|
||||
toolCallId: normalizeToolCallId(payload),
|
||||
toolName: normalizeToolName(
|
||||
payload.toolDisplayName ?? payload.toolName ?? payload.name,
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (domain === 'BUSINESS' && type === 'CITATIONS') {
|
||||
ChatTimelineBuilder.appendKnowledge(
|
||||
items,
|
||||
normalizeKnowledgeItems(payload),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (domain === 'BUSINESS' && type === 'STATUS') {
|
||||
if (asText(payload.statusKey) === 'memory-compression') {
|
||||
ChatTimelineBuilder.upsertMemoryCompressionStatus(items, {
|
||||
compressed:
|
||||
typeof payload.compressed === 'boolean'
|
||||
? payload.compressed
|
||||
: undefined,
|
||||
label: asText(payload.label),
|
||||
phase: asText(payload.phase),
|
||||
status: asText(payload.status),
|
||||
statusKey: asText(payload.statusKey),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (asText(payload.statusKey) === 'knowledge-retrieval') {
|
||||
ChatTimelineBuilder.upsertKnowledgeRetrievalStatus(
|
||||
items,
|
||||
asText(payload.status) === 'running' ? 'running' : 'done',
|
||||
asText(payload.statusKey),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (domain === 'SYSTEM' && type === 'DONE') {
|
||||
ChatTimelineBuilder.finalize(items);
|
||||
return;
|
||||
}
|
||||
if (domain === 'ERROR' || type === 'ERROR') {
|
||||
ChatTimelineBuilder.appendError(
|
||||
items,
|
||||
payload.message ?? payload.error ?? '请求失败',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user