refactor(pilot-chat): stream native ai sdk reasoning parts
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
|||||||
MeQueryDocument,
|
MeQueryDocument,
|
||||||
} from "~~/graphql/generated";
|
} from "~~/graphql/generated";
|
||||||
import { Chat as AiChat } from "@ai-sdk/vue";
|
import { Chat as AiChat } from "@ai-sdk/vue";
|
||||||
import { DefaultChatTransport, isTextUIPart, type DataUIPart, type UIMessage } from "ai";
|
import { DefaultChatTransport, isReasoningUIPart, isTextUIPart, type UIMessage } from "ai";
|
||||||
import { isVoiceCaptureSupported, transcribeAudioBlob } from "~/composables/useVoiceTranscription";
|
import { isVoiceCaptureSupported, transcribeAudioBlob } from "~/composables/useVoiceTranscription";
|
||||||
|
|
||||||
import type { Contact } from "~/composables/useContacts";
|
import type { Contact } from "~/composables/useContacts";
|
||||||
@@ -90,30 +90,8 @@ export type ChatConversation = {
|
|||||||
lastMessageText?: string | null;
|
lastMessageText?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PilotDataTypes = {
|
|
||||||
"agent-log": {
|
|
||||||
requestId: string;
|
|
||||||
at: string;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type PilotUiMessage = UIMessage<unknown, PilotDataTypes>;
|
|
||||||
|
|
||||||
function safeTrim(value: unknown) { return String(value ?? "").trim(); }
|
function safeTrim(value: unknown) { return String(value ?? "").trim(); }
|
||||||
|
type PilotUiMessage = UIMessage;
|
||||||
function parsePilotAgentLog(part: DataUIPart<PilotDataTypes>) {
|
|
||||||
if (part.type !== "data-agent-log") return null;
|
|
||||||
|
|
||||||
const data = part.data as Partial<PilotDataTypes["agent-log"]> | null | undefined;
|
|
||||||
const text = safeTrim(data?.text);
|
|
||||||
if (!text) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
text,
|
|
||||||
at: safeTrim(data?.at) || new Date().toISOString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePilotChat(opts: {
|
export function usePilotChat(opts: {
|
||||||
apolloAuthReady: ComputedRef<boolean>;
|
apolloAuthReady: ComputedRef<boolean>;
|
||||||
@@ -218,11 +196,6 @@ export function usePilotChat(opts: {
|
|||||||
transport: new DefaultChatTransport({
|
transport: new DefaultChatTransport({
|
||||||
api: "/api/pilot-chat",
|
api: "/api/pilot-chat",
|
||||||
}),
|
}),
|
||||||
onData: (part) => {
|
|
||||||
const log = parsePilotAgentLog(part);
|
|
||||||
if (!log) return;
|
|
||||||
pilotLiveLogs.value = [...pilotLiveLogs.value, { id: `${Date.now()}-${Math.random()}`, text: log.text, at: log.at }];
|
|
||||||
},
|
|
||||||
onFinish: async () => {
|
onFinish: async () => {
|
||||||
pilotSending.value = false;
|
pilotSending.value = false;
|
||||||
livePilotUserText.value = "";
|
livePilotUserText.value = "";
|
||||||
@@ -274,6 +247,16 @@ export function usePilotChat(opts: {
|
|||||||
|
|
||||||
const textPart = latestAssistant.parts.find(isTextUIPart);
|
const textPart = latestAssistant.parts.find(isTextUIPart);
|
||||||
livePilotAssistantText.value = textPart?.text ?? "";
|
livePilotAssistantText.value = textPart?.text ?? "";
|
||||||
|
|
||||||
|
// Use native AI SDK reasoning parts for live "thinking" output.
|
||||||
|
pilotLiveLogs.value = latestAssistant.parts
|
||||||
|
.filter(isReasoningUIPart)
|
||||||
|
.map((part, index) => ({
|
||||||
|
id: `${latestAssistant.id}-reasoning-${index}`,
|
||||||
|
text: safeTrim(part.text),
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
}))
|
||||||
|
.filter((log) => Boolean(log.text));
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -309,10 +292,15 @@ export function usePilotChat(opts: {
|
|||||||
// Pilot ↔ UIMessage bridge
|
// Pilot ↔ UIMessage bridge
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
function pilotToUiMessage(message: PilotMessage): PilotUiMessage {
|
function pilotToUiMessage(message: PilotMessage): PilotUiMessage {
|
||||||
|
const reasoningParts = (message.thinking ?? [])
|
||||||
|
.map((item) => safeTrim(item))
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((text) => ({ type: "reasoning" as const, text, state: "done" as const }));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
role: message.role,
|
role: message.role,
|
||||||
parts: [{ type: "text", text: message.text }],
|
parts: [...reasoningParts, { type: "text", text: message.text }],
|
||||||
metadata: {
|
metadata: {
|
||||||
createdAt: message.createdAt ?? null,
|
createdAt: message.createdAt ?? null,
|
||||||
},
|
},
|
||||||
@@ -702,6 +690,7 @@ export function usePilotChat(opts: {
|
|||||||
refetchChatConversations,
|
refetchChatConversations,
|
||||||
// realtime pilot trace handlers (called from useCrmRealtime)
|
// realtime pilot trace handlers (called from useCrmRealtime)
|
||||||
handleRealtimePilotTrace(log: { text: string; at: string }) {
|
handleRealtimePilotTrace(log: { text: string; at: string }) {
|
||||||
|
if (pilotSending.value) return;
|
||||||
const text = String(log.text ?? "").trim();
|
const text = String(log.text ?? "").trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
// Mark as sending so the UI shows live-log panel
|
// Mark as sending so the UI shows live-log panel
|
||||||
|
|||||||
@@ -15,15 +15,7 @@ import type { ChangeSet } from "../utils/changeSet";
|
|||||||
import { startPilotRun, addPilotTrace, finishPilotRun } from "../utils/pilotRunStore";
|
import { startPilotRun, addPilotTrace, finishPilotRun } from "../utils/pilotRunStore";
|
||||||
import { broadcastToConversation } from "../routes/ws/crm-updates";
|
import { broadcastToConversation } from "../routes/ws/crm-updates";
|
||||||
|
|
||||||
type PilotDataTypes = {
|
type PilotUiMessage = UIMessage;
|
||||||
"agent-log": {
|
|
||||||
requestId: string;
|
|
||||||
at: string;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type PilotUiMessage = UIMessage<unknown, PilotDataTypes>;
|
|
||||||
|
|
||||||
type PilotChatRequestBody = {
|
type PilotChatRequestBody = {
|
||||||
messages?: unknown;
|
messages?: unknown;
|
||||||
@@ -41,7 +33,7 @@ function extractMessageText(message: PilotUiMessage): string {
|
|||||||
function getLastUserText(messages: PilotUiMessage[]): string {
|
function getLastUserText(messages: PilotUiMessage[]): string {
|
||||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||||
const message = messages[i];
|
const message = messages[i];
|
||||||
if (message.role !== "user") continue;
|
if (!message || message.role !== "user") continue;
|
||||||
const text = extractMessageText(message);
|
const text = extractMessageText(message);
|
||||||
if (text) return text;
|
if (text) return text;
|
||||||
}
|
}
|
||||||
@@ -156,11 +148,10 @@ function renderChangeSetSummary(changeSet: ChangeSet): string {
|
|||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function writePilotLog(writer: UIMessageStreamWriter<PilotUiMessage>, payload: PilotDataTypes["agent-log"]) {
|
function writeAssistantReasoning(writer: UIMessageStreamWriter<PilotUiMessage>, reasoningId: string, text: string) {
|
||||||
writer.write({
|
writer.write({ type: "reasoning-start", id: reasoningId });
|
||||||
type: "data-agent-log",
|
writer.write({ type: "reasoning-delta", id: reasoningId, delta: text });
|
||||||
data: payload,
|
writer.write({ type: "reasoning-end", id: reasoningId });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeAssistantText(writer: UIMessageStreamWriter<PilotUiMessage>, textId: string, text: string) {
|
function writeAssistantText(writer: UIMessageStreamWriter<PilotUiMessage>, textId: string, text: string) {
|
||||||
@@ -229,12 +220,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
onTrace: async (trace: AgentTraceEvent) => {
|
onTrace: async (trace: AgentTraceEvent) => {
|
||||||
const traceText = humanizeTraceText(trace);
|
const traceText = humanizeTraceText(trace);
|
||||||
const traceAt = new Date().toISOString();
|
const traceAt = new Date().toISOString();
|
||||||
|
const reasoningId = `reasoning-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`;
|
||||||
|
|
||||||
writePilotLog(writer, {
|
writeAssistantReasoning(writer, reasoningId, traceText);
|
||||||
requestId,
|
|
||||||
at: traceAt,
|
|
||||||
text: traceText,
|
|
||||||
});
|
|
||||||
|
|
||||||
addPilotTrace(auth.conversationId, traceText);
|
addPilotTrace(auth.conversationId, traceText);
|
||||||
broadcastToConversation(auth.conversationId, {
|
broadcastToConversation(auth.conversationId, {
|
||||||
@@ -245,6 +233,16 @@ export default defineEventHandler(async (event) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const thought of reply.thinking ?? []) {
|
||||||
|
const text = String(thought ?? "").trim();
|
||||||
|
if (!text) continue;
|
||||||
|
writeAssistantReasoning(
|
||||||
|
writer,
|
||||||
|
`reasoning-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`,
|
||||||
|
text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const snapshotAfter = await captureSnapshot(prisma, auth.teamId);
|
const snapshotAfter = await captureSnapshot(prisma, auth.teamId);
|
||||||
const changeSet = buildChangeSet(snapshotBefore, snapshotAfter);
|
const changeSet = buildChangeSet(snapshotBefore, snapshotAfter);
|
||||||
|
|
||||||
@@ -297,11 +295,11 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
finalizePilotExecution(auth.conversationId, "error");
|
finalizePilotExecution(auth.conversationId, "error");
|
||||||
|
|
||||||
writePilotLog(writer, {
|
writeAssistantReasoning(
|
||||||
requestId,
|
writer,
|
||||||
at: new Date().toISOString(),
|
`reasoning-${Date.now()}-${Math.floor(Math.random() * 1_000_000)}`,
|
||||||
text: "Ошибка выполнения агентского цикла.",
|
"Ошибка выполнения агентского цикла.",
|
||||||
});
|
);
|
||||||
|
|
||||||
writeAssistantText(writer, textId, errorText);
|
writeAssistantText(writer, textId, errorText);
|
||||||
writer.write({ type: "finish", finishReason: "error" });
|
writer.write({ type: "finish", finishReason: "error" });
|
||||||
|
|||||||
Reference in New Issue
Block a user