incognitolm commited on
Commit
0cb48f9
·
1 Parent(s): 54277fc

Update chatStream.js

Browse files
Files changed (1) hide show
  1. 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
- ) + "/ws/chat";
63
- await new Promise(resolve => {
 
 
 
64
  ws.on("open", resolve);
 
65
  });
66
 
67
- ws.send(JSON.stringify({
68
- key: process.env.WEBSOCKET_KEY
69
- }));
 
 
70
 
71
- await new Promise(resolve => {
72
- ws.onmessage = e => {
73
- const msg = JSON.parse(e.data);
74
- if (msg.type === "auth") resolve();
75
- };
 
 
 
76
  });
77
 
78
- ws.send(JSON.stringify({
79
- body,
80
- headers
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
- const payload = JSON.parse(line.slice(5));
 
 
 
 
 
 
94
 
95
  const delta = payload.choices?.[0]?.delta;
 
96
 
97
- if (delta?.content) {
98
  assistantText += delta.content;
99
  onToken(delta.content);
100
  }
101
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  if (payload.choices?.[0]?.finish_reason) {
 
103
  ws.close();
104
- resolve({ assistantText });
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
  });
107
 
@@ -128,7 +180,7 @@ export function extractSessionName(text) {
128
  return name;
129
  }
130
 
131
- export async function streamChat(ws, {
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({ role: "user", content: normalizedUserMessage });
 
 
 
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
- ws,
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"]);