refactor(pilot-chat): stream native ai sdk reasoning parts

This commit is contained in:
Ruslan Bakiev
2026-03-09 09:47:32 +07:00
parent c2cf4e6dd8
commit e96b57a55f
2 changed files with 42 additions and 55 deletions

View File

@@ -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

View File

@@ -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" });