incognitolm commited on
Commit ·
0cb48f9
1
Parent(s): 54277fc
Update chatStream.js
Browse files- server/chatStream.js +83 -31
server/chatStream.js
CHANGED
|
@@ -53,55 +53,107 @@ function makeClient(accessToken, clientId) {
|
|
| 53 |
});
|
| 54 |
}
|
| 55 |
|
| 56 |
-
async function websocketChatStream(body, headers, onToken) {
|
| 57 |
|
| 58 |
const wsURL =
|
| 59 |
(LIGHTNING_BASE.startsWith("https")
|
| 60 |
? LIGHTNING_BASE.replace("https", "wss")
|
| 61 |
-
: LIGHTNING_BASE.replace("http", "ws")
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
| 64 |
ws.on("open", resolve);
|
|
|
|
| 65 |
});
|
| 66 |
|
| 67 |
-
ws.send(
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
| 70 |
|
| 71 |
-
await new Promise(resolve => {
|
| 72 |
-
ws.
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
| 76 |
});
|
| 77 |
|
| 78 |
-
ws.send(
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
|
|
|
|
|
|
| 82 |
|
| 83 |
let assistantText = "";
|
|
|
|
| 84 |
|
| 85 |
return new Promise((resolve, reject) => {
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
ws.on("message", (data) => {
|
| 88 |
|
| 89 |
const line = data.toString();
|
| 90 |
|
| 91 |
if (!line.startsWith("data:")) return;
|
| 92 |
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
const delta = payload.choices?.[0]?.delta;
|
|
|
|
| 96 |
|
| 97 |
-
if (delta
|
| 98 |
assistantText += delta.content;
|
| 99 |
onToken(delta.content);
|
| 100 |
}
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
if (payload.choices?.[0]?.finish_reason) {
|
|
|
|
| 103 |
ws.close();
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
}
|
| 106 |
});
|
| 107 |
|
|
@@ -128,7 +180,7 @@ export function extractSessionName(text) {
|
|
| 128 |
return name;
|
| 129 |
}
|
| 130 |
|
| 131 |
-
export async function streamChat(
|
| 132 |
sessionId,
|
| 133 |
model,
|
| 134 |
history,
|
|
@@ -175,7 +227,6 @@ export async function streamChat(ws, {
|
|
| 175 |
.trim() || "";
|
| 176 |
|
| 177 |
}
|
| 178 |
-
|
| 179 |
}
|
| 180 |
|
| 181 |
const hasUserMessage =
|
|
@@ -190,12 +241,15 @@ export async function streamChat(ws, {
|
|
| 190 |
];
|
| 191 |
|
| 192 |
if (hasUserMessage) {
|
| 193 |
-
messages.push({
|
|
|
|
|
|
|
|
|
|
| 194 |
}
|
| 195 |
|
| 196 |
const headers = {
|
| 197 |
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
| 198 |
-
...(clientId ? { "X-Client-ID": clientId } : {})
|
| 199 |
};
|
| 200 |
|
| 201 |
try {
|
|
@@ -204,7 +258,7 @@ export async function streamChat(ws, {
|
|
| 204 |
model: model || "lightning",
|
| 205 |
messages,
|
| 206 |
tools: enabledTools.length ? enabledTools : undefined,
|
| 207 |
-
stream: true
|
| 208 |
};
|
| 209 |
|
| 210 |
let { assistantText, toolCalls } =
|
|
@@ -213,7 +267,7 @@ export async function streamChat(ws, {
|
|
| 213 |
if (toolCalls.length > 0) {
|
| 214 |
|
| 215 |
const toolResults = await processToolCalls(
|
| 216 |
-
|
| 217 |
toolCalls,
|
| 218 |
tools,
|
| 219 |
accessToken,
|
|
@@ -231,7 +285,7 @@ export async function streamChat(ws, {
|
|
| 231 |
if (hasUserMessage) {
|
| 232 |
followUpMessages.push({
|
| 233 |
role: "user",
|
| 234 |
-
content: normalizedUserMessage
|
| 235 |
});
|
| 236 |
}
|
| 237 |
|
|
@@ -239,7 +293,7 @@ export async function streamChat(ws, {
|
|
| 239 |
{
|
| 240 |
role: "assistant",
|
| 241 |
content: assistantText || "",
|
| 242 |
-
tool_calls: toolCalls
|
| 243 |
},
|
| 244 |
...toolResults
|
| 245 |
);
|
|
@@ -247,14 +301,13 @@ export async function streamChat(ws, {
|
|
| 247 |
const followUpBody = {
|
| 248 |
model: model || "lightning",
|
| 249 |
messages: followUpMessages,
|
| 250 |
-
stream: true
|
| 251 |
};
|
| 252 |
|
| 253 |
const followUp =
|
| 254 |
await websocketChatStream(followUpBody, headers, onToken, abortSignal);
|
| 255 |
|
| 256 |
assistantText += followUp.assistantText;
|
| 257 |
-
|
| 258 |
}
|
| 259 |
|
| 260 |
const sessionName = extractSessionName(assistantText);
|
|
@@ -269,11 +322,10 @@ export async function streamChat(ws, {
|
|
| 269 |
) {
|
| 270 |
onDone(null, null, true, null);
|
| 271 |
} else {
|
|
|
|
| 272 |
onError(String(err));
|
| 273 |
}
|
| 274 |
-
|
| 275 |
}
|
| 276 |
-
|
| 277 |
}
|
| 278 |
|
| 279 |
const VALID_ROLES = new Set(["system", "user", "assistant", "tool"]);
|
|
|
|
| 53 |
});
|
| 54 |
}
|
| 55 |
|
| 56 |
+
async function websocketChatStream(body, headers, onToken, abortSignal) {
|
| 57 |
|
| 58 |
const wsURL =
|
| 59 |
(LIGHTNING_BASE.startsWith("https")
|
| 60 |
? LIGHTNING_BASE.replace("https", "wss")
|
| 61 |
+
: LIGHTNING_BASE.replace("http", "ws")) +
|
| 62 |
+
"/ws/chat";
|
| 63 |
+
|
| 64 |
+
const ws = new WebSocket(wsURL);
|
| 65 |
+
|
| 66 |
+
await new Promise((resolve, reject) => {
|
| 67 |
ws.on("open", resolve);
|
| 68 |
+
ws.on("error", reject);
|
| 69 |
});
|
| 70 |
|
| 71 |
+
ws.send(
|
| 72 |
+
JSON.stringify({
|
| 73 |
+
key: process.env.WEBSOCKET_KEY,
|
| 74 |
+
})
|
| 75 |
+
);
|
| 76 |
|
| 77 |
+
await new Promise((resolve, reject) => {
|
| 78 |
+
ws.on("message", (data) => {
|
| 79 |
+
try {
|
| 80 |
+
const msg = JSON.parse(data.toString());
|
| 81 |
+
if (msg.type === "auth" && msg.status === "ok") resolve();
|
| 82 |
+
} catch {}
|
| 83 |
+
});
|
| 84 |
+
ws.on("error", reject);
|
| 85 |
});
|
| 86 |
|
| 87 |
+
ws.send(
|
| 88 |
+
JSON.stringify({
|
| 89 |
+
body,
|
| 90 |
+
headers,
|
| 91 |
+
})
|
| 92 |
+
);
|
| 93 |
|
| 94 |
let assistantText = "";
|
| 95 |
+
const toolCallBuffer = new Map();
|
| 96 |
|
| 97 |
return new Promise((resolve, reject) => {
|
| 98 |
|
| 99 |
+
if (abortSignal) {
|
| 100 |
+
abortSignal.addEventListener("abort", () => {
|
| 101 |
+
ws.close();
|
| 102 |
+
reject(new Error("AbortError"));
|
| 103 |
+
});
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
ws.on("message", (data) => {
|
| 107 |
|
| 108 |
const line = data.toString();
|
| 109 |
|
| 110 |
if (!line.startsWith("data:")) return;
|
| 111 |
|
| 112 |
+
let payload;
|
| 113 |
+
|
| 114 |
+
try {
|
| 115 |
+
payload = JSON.parse(line.slice(5));
|
| 116 |
+
} catch {
|
| 117 |
+
return;
|
| 118 |
+
}
|
| 119 |
|
| 120 |
const delta = payload.choices?.[0]?.delta;
|
| 121 |
+
if (!delta) return;
|
| 122 |
|
| 123 |
+
if (delta.content) {
|
| 124 |
assistantText += delta.content;
|
| 125 |
onToken(delta.content);
|
| 126 |
}
|
| 127 |
|
| 128 |
+
if (delta.tool_calls) {
|
| 129 |
+
for (const call of delta.tool_calls) {
|
| 130 |
+
const entry = toolCallBuffer.get(call.index) ?? { arguments: "" };
|
| 131 |
+
|
| 132 |
+
if (call.id) entry.id = call.id;
|
| 133 |
+
if (call.function?.name) entry.name = call.function.name;
|
| 134 |
+
if (call.function?.arguments) entry.arguments += call.function.arguments;
|
| 135 |
+
|
| 136 |
+
toolCallBuffer.set(call.index, entry);
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
if (payload.choices?.[0]?.finish_reason) {
|
| 141 |
+
|
| 142 |
ws.close();
|
| 143 |
+
|
| 144 |
+
const toolCalls = [...toolCallBuffer.values()].map((t) => ({
|
| 145 |
+
id: t.id || `call_${crypto.randomUUID()}`,
|
| 146 |
+
type: "function",
|
| 147 |
+
function: {
|
| 148 |
+
name: t.name,
|
| 149 |
+
arguments: t.arguments,
|
| 150 |
+
},
|
| 151 |
+
}));
|
| 152 |
+
|
| 153 |
+
resolve({
|
| 154 |
+
assistantText,
|
| 155 |
+
toolCalls,
|
| 156 |
+
});
|
| 157 |
}
|
| 158 |
});
|
| 159 |
|
|
|
|
| 180 |
return name;
|
| 181 |
}
|
| 182 |
|
| 183 |
+
export async function streamChat({
|
| 184 |
sessionId,
|
| 185 |
model,
|
| 186 |
history,
|
|
|
|
| 227 |
.trim() || "";
|
| 228 |
|
| 229 |
}
|
|
|
|
| 230 |
}
|
| 231 |
|
| 232 |
const hasUserMessage =
|
|
|
|
| 241 |
];
|
| 242 |
|
| 243 |
if (hasUserMessage) {
|
| 244 |
+
messages.push({
|
| 245 |
+
role: "user",
|
| 246 |
+
content: normalizedUserMessage,
|
| 247 |
+
});
|
| 248 |
}
|
| 249 |
|
| 250 |
const headers = {
|
| 251 |
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
| 252 |
+
...(clientId ? { "X-Client-ID": clientId } : {}),
|
| 253 |
};
|
| 254 |
|
| 255 |
try {
|
|
|
|
| 258 |
model: model || "lightning",
|
| 259 |
messages,
|
| 260 |
tools: enabledTools.length ? enabledTools : undefined,
|
| 261 |
+
stream: true,
|
| 262 |
};
|
| 263 |
|
| 264 |
let { assistantText, toolCalls } =
|
|
|
|
| 267 |
if (toolCalls.length > 0) {
|
| 268 |
|
| 269 |
const toolResults = await processToolCalls(
|
| 270 |
+
null,
|
| 271 |
toolCalls,
|
| 272 |
tools,
|
| 273 |
accessToken,
|
|
|
|
| 285 |
if (hasUserMessage) {
|
| 286 |
followUpMessages.push({
|
| 287 |
role: "user",
|
| 288 |
+
content: normalizedUserMessage,
|
| 289 |
});
|
| 290 |
}
|
| 291 |
|
|
|
|
| 293 |
{
|
| 294 |
role: "assistant",
|
| 295 |
content: assistantText || "",
|
| 296 |
+
tool_calls: toolCalls,
|
| 297 |
},
|
| 298 |
...toolResults
|
| 299 |
);
|
|
|
|
| 301 |
const followUpBody = {
|
| 302 |
model: model || "lightning",
|
| 303 |
messages: followUpMessages,
|
| 304 |
+
stream: true,
|
| 305 |
};
|
| 306 |
|
| 307 |
const followUp =
|
| 308 |
await websocketChatStream(followUpBody, headers, onToken, abortSignal);
|
| 309 |
|
| 310 |
assistantText += followUp.assistantText;
|
|
|
|
| 311 |
}
|
| 312 |
|
| 313 |
const sessionName = extractSessionName(assistantText);
|
|
|
|
| 322 |
) {
|
| 323 |
onDone(null, null, true, null);
|
| 324 |
} else {
|
| 325 |
+
console.error("streamChat error:", err);
|
| 326 |
onError(String(err));
|
| 327 |
}
|
|
|
|
| 328 |
}
|
|
|
|
| 329 |
}
|
| 330 |
|
| 331 |
const VALID_ROLES = new Set(["system", "user", "assistant", "tool"]);
|