feat: 全新智能体功能
- 基于先进智能体框架,增加智能体编排功能 - 增加智能体聊天,并对接持久化
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
import type {ServerSentEventMessage} from 'fetch-event-stream';
|
||||
|
||||
import type {
|
||||
ChatTimelineItem as ChatTimelineItemType,
|
||||
ChatTimelineMessageItem,
|
||||
} from '@easyflow/common-ui';
|
||||
import {ChatTimelineBuilder} from '@easyflow/common-ui';
|
||||
|
||||
import type {AgentInfo, AgentKnowledgeBinding, AgentToolBinding,} from '../types';
|
||||
|
||||
import {ref} from 'vue';
|
||||
|
||||
import {sseClient} from '#/api/request';
|
||||
|
||||
import {clearAgentDraftSession} from '../api';
|
||||
import {useAgentTryoutRawRounds} from './useAgentTryoutRawRounds';
|
||||
|
||||
function resolveDraftSessionId(agent: AgentInfo) {
|
||||
return `agent-draft-${agent.id || agent.localId || 'unsaved'}`;
|
||||
}
|
||||
|
||||
function parseEventData(message: ServerSentEventMessage) {
|
||||
const raw = message.data || '';
|
||||
if (!raw) return {};
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return { payload: { delta: raw } };
|
||||
}
|
||||
}
|
||||
|
||||
function resolveEnvelope(data: any) {
|
||||
return {
|
||||
domain: data.domain || data.eventDomain || data.typeDomain,
|
||||
type: data.type || data.eventType || data.chatType || data.event,
|
||||
payload: data.payload ?? data.data ?? data,
|
||||
};
|
||||
}
|
||||
|
||||
function asText(value: unknown) {
|
||||
return value === null || value === undefined ? '' : String(value);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
}
|
||||
|
||||
function isEndOfRoundEvent(domain: string, type: string) {
|
||||
return domain === 'SYSTEM' && type === 'DONE';
|
||||
}
|
||||
|
||||
interface DraftRuntimeContext {
|
||||
agent: AgentInfo;
|
||||
knowledgeBindings: AgentKnowledgeBinding[];
|
||||
toolBindings: AgentToolBinding[];
|
||||
}
|
||||
|
||||
export function useAgentTryoutStream() {
|
||||
const timelineItems = ref<ChatTimelineItemType[]>([]);
|
||||
const loading = ref(false);
|
||||
let rawRounds: ReturnType<typeof useAgentTryoutRawRounds> | undefined;
|
||||
let activeRoundId = '';
|
||||
let activeSessionId = '';
|
||||
let userStopped = false;
|
||||
|
||||
function errorMessageOf(error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
return `${error.name} ${error.message}`.trim();
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
const value = asRecord(error);
|
||||
const nested = asRecord(value.cause);
|
||||
return [value.name, value.message, value.error, nested.name, nested.message]
|
||||
.map((item) => asText(item).trim())
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function isAbortError(error: unknown) {
|
||||
const message = errorMessageOf(error).toLowerCase();
|
||||
return message.includes('abort');
|
||||
}
|
||||
|
||||
function shouldIgnoreStoppedError(error: unknown) {
|
||||
return userStopped && isAbortError(error);
|
||||
}
|
||||
|
||||
function finishStoppedRun() {
|
||||
finishAssistant();
|
||||
rawRounds?.flush();
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
function rebuildTimeline() {
|
||||
timelineItems.value = rawRounds?.buildTimelineItems() || [];
|
||||
}
|
||||
|
||||
function syncDraftContext(payload: DraftRuntimeContext, restore = false) {
|
||||
const sessionId = resolveDraftSessionId(payload.agent);
|
||||
const sessionChanged = activeSessionId !== sessionId;
|
||||
activeSessionId = sessionId;
|
||||
if (!rawRounds || sessionChanged) {
|
||||
rawRounds = useAgentTryoutRawRounds({
|
||||
mode: 'draft',
|
||||
sessionId,
|
||||
});
|
||||
activeRoundId = '';
|
||||
}
|
||||
if (restore && sessionChanged && !loading.value) {
|
||||
rebuildTimeline();
|
||||
}
|
||||
}
|
||||
|
||||
function markRoundCompleted(roundId: string) {
|
||||
if (!roundId) {
|
||||
return;
|
||||
}
|
||||
rawRounds?.completeRound(roundId);
|
||||
rebuildTimeline();
|
||||
}
|
||||
|
||||
function finishAssistant() {
|
||||
ChatTimelineBuilder.finalize(timelineItems.value);
|
||||
}
|
||||
|
||||
function markToolApproving(payload: {
|
||||
requestId?: string;
|
||||
resumeToken?: string;
|
||||
toolCallId?: string;
|
||||
}) {
|
||||
rawRounds?.recordEvent(activeRoundId, {
|
||||
domain: 'TOOL',
|
||||
payload,
|
||||
type: 'FORM_APPROVING',
|
||||
});
|
||||
rawRounds?.flush();
|
||||
rebuildTimeline();
|
||||
}
|
||||
|
||||
function markToolRejected(payload: {
|
||||
reason?: string;
|
||||
requestId?: string;
|
||||
resumeToken?: string;
|
||||
toolCallId?: string;
|
||||
}) {
|
||||
rawRounds?.recordEvent(activeRoundId, {
|
||||
domain: 'TOOL',
|
||||
payload,
|
||||
type: 'FORM_REJECTED',
|
||||
});
|
||||
rawRounds?.flush();
|
||||
rebuildTimeline();
|
||||
}
|
||||
|
||||
function handleMessage(message: ServerSentEventMessage) {
|
||||
const data = parseEventData(message);
|
||||
const envelope = resolveEnvelope(data);
|
||||
const domain = String(envelope.domain || '').toUpperCase();
|
||||
const type = String(envelope.type || '').toUpperCase();
|
||||
const payload = envelope.payload || {};
|
||||
|
||||
if (activeRoundId) {
|
||||
rawRounds?.recordEvent(activeRoundId, {
|
||||
domain,
|
||||
payload,
|
||||
type,
|
||||
});
|
||||
rebuildTimeline();
|
||||
}
|
||||
|
||||
if (domain === 'LLM' && type === 'MESSAGE') {
|
||||
return;
|
||||
}
|
||||
if (domain === 'LLM' && type === 'THINKING') {
|
||||
const text = asText(payload.reasoning ?? payload.delta ?? payload.text);
|
||||
if (!text) return;
|
||||
return;
|
||||
}
|
||||
if (domain === 'TOOL' && type === 'FORM_REQUEST') {
|
||||
return;
|
||||
}
|
||||
if (domain === 'TOOL' && (type === 'TOOL_CALL' || type === 'TOOL_RESULT')) {
|
||||
return;
|
||||
}
|
||||
if (domain === 'BUSINESS' && type === 'CITATIONS') {
|
||||
return;
|
||||
}
|
||||
if (domain === 'BUSINESS' && type === 'STATUS') {
|
||||
return;
|
||||
}
|
||||
if (isEndOfRoundEvent(domain, type)) {
|
||||
markRoundCompleted(activeRoundId);
|
||||
return;
|
||||
}
|
||||
if (type === 'ERROR' || domain === 'ERROR') {
|
||||
const message = payload.message ?? payload.error ?? '试运行失败';
|
||||
if (shouldIgnoreStoppedError(message)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runDraft(payload: {
|
||||
agent: AgentInfo;
|
||||
knowledgeBindings: AgentKnowledgeBinding[];
|
||||
prompt: string;
|
||||
toolBindings: AgentToolBinding[];
|
||||
}) {
|
||||
syncDraftContext(payload);
|
||||
if (!rawRounds) {
|
||||
return;
|
||||
}
|
||||
activeRoundId = rawRounds.createRound(payload.prompt);
|
||||
rebuildTimeline();
|
||||
loading.value = true;
|
||||
userStopped = false;
|
||||
await sseClient.post(
|
||||
'/api/v1/agent/chat/draft',
|
||||
{
|
||||
...payload,
|
||||
sessionId: activeSessionId,
|
||||
},
|
||||
{
|
||||
onMessage: handleMessage,
|
||||
onError: (error) => {
|
||||
if (shouldIgnoreStoppedError(error)) {
|
||||
return;
|
||||
}
|
||||
rawRounds?.recordEvent(activeRoundId, {
|
||||
domain: 'SYSTEM',
|
||||
payload: {
|
||||
message: error?.message ?? '试运行失败,请稍后再试',
|
||||
},
|
||||
type: 'ERROR',
|
||||
});
|
||||
rebuildTimeline();
|
||||
finishAssistant();
|
||||
rawRounds?.flush();
|
||||
loading.value = false;
|
||||
},
|
||||
onFinished: () => {
|
||||
if (userStopped) {
|
||||
return;
|
||||
}
|
||||
finishAssistant();
|
||||
markRoundCompleted(activeRoundId);
|
||||
rawRounds?.flush();
|
||||
loading.value = false;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function sendDraft(payload: {
|
||||
agent: AgentInfo;
|
||||
knowledgeBindings: AgentKnowledgeBinding[];
|
||||
prompt: string;
|
||||
toolBindings: AgentToolBinding[];
|
||||
}) {
|
||||
await runDraft(payload);
|
||||
}
|
||||
|
||||
async function regenerateDraft(item: ChatTimelineMessageItem) {
|
||||
// 有状态 runtime 的历史重生成需要后端 session fork/rollback,第一版先禁用入口。
|
||||
void item;
|
||||
}
|
||||
|
||||
function selectVariant(
|
||||
item: ChatTimelineMessageItem,
|
||||
direction: 'next' | 'previous',
|
||||
) {
|
||||
if (!item.roundId || !rawRounds?.canSwitch(item, direction)) {
|
||||
return;
|
||||
}
|
||||
rawRounds.selectVariant(item.roundId, direction);
|
||||
rebuildTimeline();
|
||||
}
|
||||
|
||||
function copyMessageText(item: ChatTimelineMessageItem) {
|
||||
return rawRounds?.copyText(item) || '';
|
||||
}
|
||||
|
||||
async function clearDraftSession() {
|
||||
if (loading.value) {
|
||||
userStopped = true;
|
||||
sseClient.abort();
|
||||
loading.value = false;
|
||||
}
|
||||
const sessionId = activeSessionId;
|
||||
rawRounds?.clear();
|
||||
timelineItems.value = [];
|
||||
activeRoundId = '';
|
||||
if (sessionId) {
|
||||
await clearAgentDraftSession(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (!loading.value) {
|
||||
return;
|
||||
}
|
||||
userStopped = true;
|
||||
sseClient.abort();
|
||||
finishStoppedRun();
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
clearDraftSession,
|
||||
markToolApproving,
|
||||
markToolRejected,
|
||||
copyMessageText,
|
||||
regenerateDraft,
|
||||
selectVariant,
|
||||
syncDraftContext,
|
||||
timelineItems,
|
||||
sendDraft,
|
||||
stop,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user