@jskit-ai/assistant 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.descriptor.mjs +284 -0
- package/package.json +31 -0
- package/src/client/components/AssistantClientElement.vue +1316 -0
- package/src/client/components/AssistantConsoleSettingsClientElement.vue +71 -0
- package/src/client/components/AssistantSettingsFormCard.vue +76 -0
- package/src/client/components/AssistantWorkspaceClientElement.vue +15 -0
- package/src/client/components/AssistantWorkspaceSettingsClientElement.vue +73 -0
- package/src/client/composables/useAssistantWorkspaceRuntime.js +789 -0
- package/src/client/index.js +12 -0
- package/src/client/lib/assistantApi.js +137 -0
- package/src/client/lib/assistantHttpClient.js +10 -0
- package/src/client/lib/markdownRenderer.js +31 -0
- package/src/client/providers/AssistantWebClientProvider.js +25 -0
- package/src/server/AssistantServiceProvider.js +179 -0
- package/src/server/actionIds.js +11 -0
- package/src/server/actions.js +191 -0
- package/src/server/diTokens.js +19 -0
- package/src/server/lib/aiClient.js +43 -0
- package/src/server/lib/ndjson.js +47 -0
- package/src/server/lib/providers/anthropicClient.js +375 -0
- package/src/server/lib/providers/common.js +158 -0
- package/src/server/lib/providers/deepSeekClient.js +22 -0
- package/src/server/lib/providers/openAiClient.js +13 -0
- package/src/server/lib/providers/openAiCompatibleClient.js +69 -0
- package/src/server/lib/resolveWorkspaceSlug.js +24 -0
- package/src/server/lib/serviceToolCatalog.js +459 -0
- package/src/server/registerRoutes.js +384 -0
- package/src/server/repositories/assistantSettingsRepository.js +100 -0
- package/src/server/repositories/conversationsRepository.js +244 -0
- package/src/server/repositories/messagesRepository.js +154 -0
- package/src/server/repositories/repositoryPersistenceUtils.js +63 -0
- package/src/server/services/assistantSettingsService.js +153 -0
- package/src/server/services/chatService.js +987 -0
- package/src/server/services/transcriptService.js +334 -0
- package/src/shared/assistantPaths.js +50 -0
- package/src/shared/assistantResource.js +323 -0
- package/src/shared/assistantSettingsResource.js +214 -0
- package/src/shared/index.js +39 -0
- package/src/shared/queryKeys.js +69 -0
- package/src/shared/settingsEvents.js +7 -0
- package/src/shared/streamEvents.js +31 -0
- package/src/shared/support/positiveInteger.js +9 -0
- package/templates/migrations/assistant_settings_initial.cjs +39 -0
- package/templates/migrations/assistant_transcripts_initial.cjs +51 -0
- package/templates/src/pages/admin/workspace/assistant/index.vue +7 -0
- package/test/aiConfigValidation.test.js +15 -0
- package/test/assistantApiSurfaceHeader.test.js +64 -0
- package/test/assistantResource.test.js +53 -0
- package/test/assistantSettingsResource.test.js +48 -0
- package/test/assistantSettingsService.test.js +133 -0
- package/test/chatService.test.js +841 -0
- package/test/descriptorSurfaceOption.test.js +35 -0
- package/test/queryKeys.test.js +41 -0
- package/test/resolveWorkspaceSlug.test.js +83 -0
- package/test/routeInputContracts.test.js +287 -0
- package/test/serviceToolCatalog.test.js +1235 -0
- package/test/transcriptService.test.js +175 -0
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createChatService } from "../src/server/services/chatService.js";
|
|
4
|
+
|
|
5
|
+
function createAssistantSettingsServiceStub({ prompt = "" } = {}) {
|
|
6
|
+
return {
|
|
7
|
+
async resolveSystemPrompt() {
|
|
8
|
+
return String(prompt || "");
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test("chat service system prompt includes current workspace slug from resolved workspace context", async () => {
|
|
14
|
+
let capturedMessages = null;
|
|
15
|
+
|
|
16
|
+
const aiClient = {
|
|
17
|
+
enabled: true,
|
|
18
|
+
provider: "openai",
|
|
19
|
+
defaultModel: "gpt-test",
|
|
20
|
+
async createChatCompletionStream(payload = {}) {
|
|
21
|
+
capturedMessages = Array.isArray(payload.messages) ? payload.messages : null;
|
|
22
|
+
|
|
23
|
+
return (async function* generate() {
|
|
24
|
+
yield {
|
|
25
|
+
choices: [
|
|
26
|
+
{
|
|
27
|
+
delta: {
|
|
28
|
+
content: "Hello"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
};
|
|
33
|
+
})();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const transcriptService = {
|
|
38
|
+
async createConversationForTurn() {
|
|
39
|
+
return {
|
|
40
|
+
conversation: {
|
|
41
|
+
id: 42
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
async appendMessage() {
|
|
46
|
+
return null;
|
|
47
|
+
},
|
|
48
|
+
async completeConversation() {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const serviceToolCatalog = {
|
|
54
|
+
resolveToolSet() {
|
|
55
|
+
return {
|
|
56
|
+
tools: [],
|
|
57
|
+
byName: new Map()
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
toOpenAiToolSchema() {
|
|
61
|
+
return {
|
|
62
|
+
type: "function",
|
|
63
|
+
function: {
|
|
64
|
+
name: "noop",
|
|
65
|
+
description: "noop",
|
|
66
|
+
parameters: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {},
|
|
69
|
+
additionalProperties: false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const streamWriter = {
|
|
77
|
+
sendMeta() {},
|
|
78
|
+
sendAssistantDelta() {},
|
|
79
|
+
sendAssistantMessage() {},
|
|
80
|
+
sendToolCall() {},
|
|
81
|
+
sendToolResult() {},
|
|
82
|
+
sendError() {},
|
|
83
|
+
sendDone() {}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const chatService = createChatService({
|
|
87
|
+
aiClient,
|
|
88
|
+
transcriptService,
|
|
89
|
+
serviceToolCatalog,
|
|
90
|
+
assistantSettingsService: createAssistantSettingsServiceStub({
|
|
91
|
+
prompt: "Always answer in short bullet points."
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await chatService.streamChat(
|
|
96
|
+
{
|
|
97
|
+
messageId: "msg_1",
|
|
98
|
+
input: "Hi assistant"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
context: {
|
|
102
|
+
actor: {
|
|
103
|
+
id: 7
|
|
104
|
+
},
|
|
105
|
+
workspace: {
|
|
106
|
+
id: 1,
|
|
107
|
+
slug: "tonymobily3"
|
|
108
|
+
},
|
|
109
|
+
requestMeta: {
|
|
110
|
+
resolvedWorkspaceContext: {
|
|
111
|
+
workspace: {
|
|
112
|
+
slug: "tonymobily3"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
streamWriter
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
assert.ok(Array.isArray(capturedMessages));
|
|
122
|
+
assert.equal(capturedMessages[0]?.role, "system");
|
|
123
|
+
assert.match(String(capturedMessages[0]?.content || ""), /Current workspace slug: tonymobily3\./);
|
|
124
|
+
assert.match(String(capturedMessages[0]?.content || ""), /Always answer in short bullet points\./);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("chat service recovers with a final assistant answer when a tool call fails", async () => {
|
|
128
|
+
const completionPayloads = [];
|
|
129
|
+
const appendedMessages = [];
|
|
130
|
+
const completedStatuses = [];
|
|
131
|
+
const emittedAssistantMessages = [];
|
|
132
|
+
const emittedDone = [];
|
|
133
|
+
let streamCall = 0;
|
|
134
|
+
|
|
135
|
+
const aiClient = {
|
|
136
|
+
enabled: true,
|
|
137
|
+
provider: "openai",
|
|
138
|
+
defaultModel: "gpt-test",
|
|
139
|
+
async createChatCompletionStream(payload = {}) {
|
|
140
|
+
completionPayloads.push(payload);
|
|
141
|
+
streamCall += 1;
|
|
142
|
+
|
|
143
|
+
if (streamCall === 1) {
|
|
144
|
+
return (async function* generateToolCall() {
|
|
145
|
+
yield {
|
|
146
|
+
choices: [
|
|
147
|
+
{
|
|
148
|
+
delta: {
|
|
149
|
+
tool_calls: [
|
|
150
|
+
{
|
|
151
|
+
index: 0,
|
|
152
|
+
id: "tool_1",
|
|
153
|
+
function: {
|
|
154
|
+
name: "users_accountprofile_service_getforuser",
|
|
155
|
+
arguments: "{}"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
};
|
|
163
|
+
})();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return (async function* generateRecoveryAnswer() {
|
|
167
|
+
yield {
|
|
168
|
+
choices: [
|
|
169
|
+
{
|
|
170
|
+
delta: {
|
|
171
|
+
content: "Recovered answer."
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
};
|
|
176
|
+
})();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const transcriptService = {
|
|
181
|
+
async createConversationForTurn() {
|
|
182
|
+
return {
|
|
183
|
+
conversation: {
|
|
184
|
+
id: 77
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
async appendMessage(_conversationId, payload = {}) {
|
|
189
|
+
appendedMessages.push(payload);
|
|
190
|
+
return null;
|
|
191
|
+
},
|
|
192
|
+
async completeConversation(_conversationId, payload = {}) {
|
|
193
|
+
completedStatuses.push(String(payload.status || ""));
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const serviceToolCatalog = {
|
|
199
|
+
resolveToolSet() {
|
|
200
|
+
const descriptor = {
|
|
201
|
+
name: "users_accountprofile_service_getforuser",
|
|
202
|
+
actionId: "settings.read",
|
|
203
|
+
actionVersion: 1,
|
|
204
|
+
description: "Read account profile.",
|
|
205
|
+
parameters: {
|
|
206
|
+
type: "object",
|
|
207
|
+
properties: {},
|
|
208
|
+
additionalProperties: false
|
|
209
|
+
},
|
|
210
|
+
outputSchema: {
|
|
211
|
+
type: "object",
|
|
212
|
+
properties: {},
|
|
213
|
+
additionalProperties: true
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
tools: [descriptor],
|
|
219
|
+
byName: new Map([[descriptor.name, descriptor]])
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
toOpenAiToolSchema(tool = {}) {
|
|
223
|
+
return {
|
|
224
|
+
type: "function",
|
|
225
|
+
function: {
|
|
226
|
+
name: tool.name,
|
|
227
|
+
description: tool.description,
|
|
228
|
+
parameters: tool.parameters
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
async executeToolCall() {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
error: {
|
|
236
|
+
code: "ACTION_SURFACE_FORBIDDEN",
|
|
237
|
+
message: "Forbidden.",
|
|
238
|
+
status: 403
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const streamWriter = {
|
|
245
|
+
sendMeta() {},
|
|
246
|
+
sendAssistantDelta() {},
|
|
247
|
+
sendAssistantMessage(payload = {}) {
|
|
248
|
+
emittedAssistantMessages.push(String(payload.text || ""));
|
|
249
|
+
},
|
|
250
|
+
sendToolCall() {},
|
|
251
|
+
sendToolResult() {},
|
|
252
|
+
sendError() {},
|
|
253
|
+
sendDone(payload = {}) {
|
|
254
|
+
emittedDone.push(String(payload.status || ""));
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const chatService = createChatService({
|
|
259
|
+
aiClient,
|
|
260
|
+
transcriptService,
|
|
261
|
+
serviceToolCatalog,
|
|
262
|
+
assistantSettingsService: createAssistantSettingsServiceStub()
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const result = await chatService.streamChat(
|
|
266
|
+
{
|
|
267
|
+
messageId: "msg_recovery",
|
|
268
|
+
input: "Know everything."
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
context: {
|
|
272
|
+
actor: {
|
|
273
|
+
id: 7
|
|
274
|
+
},
|
|
275
|
+
workspace: {
|
|
276
|
+
id: 1,
|
|
277
|
+
slug: "tonymobily3"
|
|
278
|
+
},
|
|
279
|
+
surface: "admin"
|
|
280
|
+
},
|
|
281
|
+
streamWriter
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
assert.equal(result.status, "completed");
|
|
286
|
+
assert.equal(streamCall, 2);
|
|
287
|
+
assert.equal(Array.isArray(completionPayloads[1]?.tools), true);
|
|
288
|
+
assert.equal(completionPayloads[1].tools.length, 0);
|
|
289
|
+
assert.ok(
|
|
290
|
+
appendedMessages.some((entry) => entry.role === "assistant" && entry.kind === "chat" && entry.contentText === "Recovered answer.")
|
|
291
|
+
);
|
|
292
|
+
assert.deepEqual(completedStatuses, ["completed"]);
|
|
293
|
+
assert.deepEqual(emittedAssistantMessages, ["Recovered answer."]);
|
|
294
|
+
assert.deepEqual(emittedDone, ["completed"]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("chat service strips raw tool-call markup from recovery assistant output", async () => {
|
|
298
|
+
const emittedAssistantMessages = [];
|
|
299
|
+
let streamCall = 0;
|
|
300
|
+
|
|
301
|
+
const aiClient = {
|
|
302
|
+
enabled: true,
|
|
303
|
+
provider: "openai",
|
|
304
|
+
defaultModel: "gpt-test",
|
|
305
|
+
async createChatCompletionStream() {
|
|
306
|
+
streamCall += 1;
|
|
307
|
+
|
|
308
|
+
if (streamCall === 1) {
|
|
309
|
+
return (async function* generateToolCall() {
|
|
310
|
+
yield {
|
|
311
|
+
choices: [
|
|
312
|
+
{
|
|
313
|
+
delta: {
|
|
314
|
+
tool_calls: [
|
|
315
|
+
{
|
|
316
|
+
index: 0,
|
|
317
|
+
id: "tool_1",
|
|
318
|
+
function: {
|
|
319
|
+
name: "users_workspace_service_listworkspacesforauthenticatedus",
|
|
320
|
+
arguments: "{}"
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
]
|
|
327
|
+
};
|
|
328
|
+
})();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return (async function* generateRecoveryAnswer() {
|
|
332
|
+
yield {
|
|
333
|
+
choices: [
|
|
334
|
+
{
|
|
335
|
+
delta: {
|
|
336
|
+
content:
|
|
337
|
+
"<|DSML|function_calls>\n<|DSML|invoke name=\"users_console_settings_service_getsettings\"></|DSML|invoke>\n</|DSML|function_calls>"
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
]
|
|
341
|
+
};
|
|
342
|
+
})();
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const transcriptService = {
|
|
347
|
+
async createConversationForTurn() {
|
|
348
|
+
return {
|
|
349
|
+
conversation: {
|
|
350
|
+
id: 99
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
async appendMessage() {
|
|
355
|
+
return null;
|
|
356
|
+
},
|
|
357
|
+
async completeConversation() {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const serviceToolCatalog = {
|
|
363
|
+
resolveToolSet() {
|
|
364
|
+
const descriptor = {
|
|
365
|
+
name: "users_workspace_service_listworkspacesforauthenticatedus",
|
|
366
|
+
actionId: "workspace.list",
|
|
367
|
+
actionVersion: 1,
|
|
368
|
+
description: "List workspaces for actor.",
|
|
369
|
+
parameters: {
|
|
370
|
+
type: "object",
|
|
371
|
+
properties: {},
|
|
372
|
+
additionalProperties: false
|
|
373
|
+
},
|
|
374
|
+
outputSchema: {
|
|
375
|
+
type: "object",
|
|
376
|
+
properties: {},
|
|
377
|
+
additionalProperties: true
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
tools: [descriptor],
|
|
383
|
+
byName: new Map([[descriptor.name, descriptor]])
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
toOpenAiToolSchema(tool = {}) {
|
|
387
|
+
return {
|
|
388
|
+
type: "function",
|
|
389
|
+
function: {
|
|
390
|
+
name: tool.name,
|
|
391
|
+
description: tool.description,
|
|
392
|
+
parameters: tool.parameters
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
async executeToolCall() {
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
error: {
|
|
400
|
+
code: "assistant_tool_failed",
|
|
401
|
+
message: "Tool failed.",
|
|
402
|
+
status: 500
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const streamWriter = {
|
|
409
|
+
sendMeta() {},
|
|
410
|
+
sendAssistantDelta() {},
|
|
411
|
+
sendAssistantMessage(payload = {}) {
|
|
412
|
+
emittedAssistantMessages.push(String(payload.text || ""));
|
|
413
|
+
},
|
|
414
|
+
sendToolCall() {},
|
|
415
|
+
sendToolResult() {},
|
|
416
|
+
sendError() {},
|
|
417
|
+
sendDone() {}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
const chatService = createChatService({
|
|
421
|
+
aiClient,
|
|
422
|
+
transcriptService,
|
|
423
|
+
serviceToolCatalog,
|
|
424
|
+
assistantSettingsService: createAssistantSettingsServiceStub()
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await chatService.streamChat(
|
|
428
|
+
{
|
|
429
|
+
messageId: "msg_markup",
|
|
430
|
+
input: "Tell me about this workspace."
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
context: {
|
|
434
|
+
actor: {
|
|
435
|
+
id: 7
|
|
436
|
+
},
|
|
437
|
+
workspace: {
|
|
438
|
+
id: 1,
|
|
439
|
+
slug: "tonymobily3"
|
|
440
|
+
},
|
|
441
|
+
surface: "admin"
|
|
442
|
+
},
|
|
443
|
+
streamWriter
|
|
444
|
+
}
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
assert.equal(emittedAssistantMessages.length, 1);
|
|
448
|
+
assert.equal(
|
|
449
|
+
emittedAssistantMessages[0],
|
|
450
|
+
"I could not gather additional information from successful operations."
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test("chat service retries plain-language recovery before fallback when post-failure completion text is empty", async () => {
|
|
455
|
+
const completionPayloads = [];
|
|
456
|
+
const emittedAssistantMessages = [];
|
|
457
|
+
let streamCall = 0;
|
|
458
|
+
|
|
459
|
+
const aiClient = {
|
|
460
|
+
enabled: true,
|
|
461
|
+
provider: "openai",
|
|
462
|
+
defaultModel: "gpt-test",
|
|
463
|
+
async createChatCompletionStream(payload = {}) {
|
|
464
|
+
completionPayloads.push(payload);
|
|
465
|
+
streamCall += 1;
|
|
466
|
+
|
|
467
|
+
if (streamCall === 1) {
|
|
468
|
+
return (async function* generateToolCall() {
|
|
469
|
+
yield {
|
|
470
|
+
choices: [
|
|
471
|
+
{
|
|
472
|
+
delta: {
|
|
473
|
+
tool_calls: [
|
|
474
|
+
{
|
|
475
|
+
index: 0,
|
|
476
|
+
id: "tool_1",
|
|
477
|
+
function: {
|
|
478
|
+
name: "users_workspace_service_listworkspacesforauthenticatedus",
|
|
479
|
+
arguments: "{}"
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
]
|
|
486
|
+
};
|
|
487
|
+
})();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (streamCall === 2) {
|
|
491
|
+
return (async function* generateEmptyAssistantText() {
|
|
492
|
+
yield {
|
|
493
|
+
choices: [
|
|
494
|
+
{
|
|
495
|
+
delta: {
|
|
496
|
+
content: ""
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
]
|
|
500
|
+
};
|
|
501
|
+
})();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return (async function* generateRecoveryAnswer() {
|
|
505
|
+
yield {
|
|
506
|
+
choices: [
|
|
507
|
+
{
|
|
508
|
+
delta: {
|
|
509
|
+
content: "I could not run one workspace operation, but I can continue with the others."
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
]
|
|
513
|
+
};
|
|
514
|
+
})();
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const transcriptService = {
|
|
519
|
+
async createConversationForTurn() {
|
|
520
|
+
return {
|
|
521
|
+
conversation: {
|
|
522
|
+
id: 122
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
async appendMessage() {
|
|
527
|
+
return null;
|
|
528
|
+
},
|
|
529
|
+
async completeConversation() {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const serviceToolCatalog = {
|
|
535
|
+
resolveToolSet() {
|
|
536
|
+
const descriptor = {
|
|
537
|
+
name: "users_workspace_service_listworkspacesforauthenticatedus",
|
|
538
|
+
actionId: "workspace.list",
|
|
539
|
+
actionVersion: 1,
|
|
540
|
+
description: "List workspaces for actor.",
|
|
541
|
+
parameters: {
|
|
542
|
+
type: "object",
|
|
543
|
+
properties: {},
|
|
544
|
+
additionalProperties: false
|
|
545
|
+
},
|
|
546
|
+
outputSchema: {
|
|
547
|
+
type: "object",
|
|
548
|
+
properties: {},
|
|
549
|
+
additionalProperties: true
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
tools: [descriptor],
|
|
555
|
+
byName: new Map([[descriptor.name, descriptor]])
|
|
556
|
+
};
|
|
557
|
+
},
|
|
558
|
+
toOpenAiToolSchema(tool = {}) {
|
|
559
|
+
return {
|
|
560
|
+
type: "function",
|
|
561
|
+
function: {
|
|
562
|
+
name: tool.name,
|
|
563
|
+
description: tool.description,
|
|
564
|
+
parameters: tool.parameters
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
},
|
|
568
|
+
async executeToolCall() {
|
|
569
|
+
return {
|
|
570
|
+
ok: false,
|
|
571
|
+
error: {
|
|
572
|
+
code: "assistant_tool_failed",
|
|
573
|
+
message: "Tool failed.",
|
|
574
|
+
status: 500
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const streamWriter = {
|
|
581
|
+
sendMeta() {},
|
|
582
|
+
sendAssistantDelta() {},
|
|
583
|
+
sendAssistantMessage(payload = {}) {
|
|
584
|
+
emittedAssistantMessages.push(String(payload.text || ""));
|
|
585
|
+
},
|
|
586
|
+
sendToolCall() {},
|
|
587
|
+
sendToolResult() {},
|
|
588
|
+
sendError() {},
|
|
589
|
+
sendDone() {}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const chatService = createChatService({
|
|
593
|
+
aiClient,
|
|
594
|
+
transcriptService,
|
|
595
|
+
serviceToolCatalog,
|
|
596
|
+
assistantSettingsService: createAssistantSettingsServiceStub()
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
await chatService.streamChat(
|
|
600
|
+
{
|
|
601
|
+
messageId: "msg_empty_after_failure",
|
|
602
|
+
input: "Tell me everything."
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
context: {
|
|
606
|
+
actor: {
|
|
607
|
+
id: 7
|
|
608
|
+
},
|
|
609
|
+
workspace: {
|
|
610
|
+
id: 1,
|
|
611
|
+
slug: "tonymobily3"
|
|
612
|
+
},
|
|
613
|
+
surface: "admin"
|
|
614
|
+
},
|
|
615
|
+
streamWriter
|
|
616
|
+
}
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
assert.equal(streamCall, 3);
|
|
620
|
+
assert.equal(Array.isArray(completionPayloads[2]?.tools), true);
|
|
621
|
+
assert.equal(completionPayloads[2].tools.length, 0);
|
|
622
|
+
assert.equal(
|
|
623
|
+
emittedAssistantMessages[0],
|
|
624
|
+
"I could not run one workspace operation, but I can continue with the others."
|
|
625
|
+
);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("chat service recovery streams sanitized text without DSML tag leakage", async () => {
|
|
629
|
+
const emittedAssistantMessages = [];
|
|
630
|
+
const streamedDeltas = [];
|
|
631
|
+
let streamCall = 0;
|
|
632
|
+
|
|
633
|
+
const aiClient = {
|
|
634
|
+
enabled: true,
|
|
635
|
+
provider: "openai",
|
|
636
|
+
defaultModel: "gpt-test",
|
|
637
|
+
async createChatCompletionStream() {
|
|
638
|
+
streamCall += 1;
|
|
639
|
+
|
|
640
|
+
if (streamCall === 1) {
|
|
641
|
+
return (async function* generateInitialToolCall() {
|
|
642
|
+
yield {
|
|
643
|
+
choices: [
|
|
644
|
+
{
|
|
645
|
+
delta: {
|
|
646
|
+
tool_calls: [
|
|
647
|
+
{
|
|
648
|
+
index: 0,
|
|
649
|
+
id: "tool_1",
|
|
650
|
+
function: {
|
|
651
|
+
name: "users_workspace_service_listworkspacesforauthenticatedus",
|
|
652
|
+
arguments: "{}"
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
]
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
]
|
|
659
|
+
};
|
|
660
|
+
})();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (streamCall === 2) {
|
|
664
|
+
return (async function* generateRecoveryDsmlAndText() {
|
|
665
|
+
yield {
|
|
666
|
+
choices: [
|
|
667
|
+
{
|
|
668
|
+
delta: {
|
|
669
|
+
content:
|
|
670
|
+
"<|DSML|function_calls>\n<|DSML|invoke name=\"users_console_settings_service_getsettings\"></|DSML|invoke>\n</|DSML|function_calls>Interim notes from successful operations."
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
]
|
|
674
|
+
};
|
|
675
|
+
})();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return (async function* generateFinalRecoveryAnswer() {
|
|
679
|
+
yield {
|
|
680
|
+
choices: [
|
|
681
|
+
{
|
|
682
|
+
delta: {
|
|
683
|
+
content: "Final response from available data."
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
]
|
|
687
|
+
};
|
|
688
|
+
})();
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
const transcriptService = {
|
|
693
|
+
async createConversationForTurn() {
|
|
694
|
+
return {
|
|
695
|
+
conversation: {
|
|
696
|
+
id: 200
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
},
|
|
700
|
+
async appendMessage() {
|
|
701
|
+
return null;
|
|
702
|
+
},
|
|
703
|
+
async completeConversation() {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const serviceToolCatalog = {
|
|
709
|
+
resolveToolSet() {
|
|
710
|
+
const first = {
|
|
711
|
+
name: "users_workspace_service_listworkspacesforauthenticatedus",
|
|
712
|
+
actionId: "workspace.list",
|
|
713
|
+
actionVersion: 1,
|
|
714
|
+
description: "List workspaces for actor.",
|
|
715
|
+
parameters: {
|
|
716
|
+
type: "object",
|
|
717
|
+
properties: {},
|
|
718
|
+
additionalProperties: false
|
|
719
|
+
},
|
|
720
|
+
outputSchema: {
|
|
721
|
+
type: "object",
|
|
722
|
+
properties: {},
|
|
723
|
+
additionalProperties: true
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
const second = {
|
|
727
|
+
name: "users_console_settings_service_getsettings",
|
|
728
|
+
actionId: "console.settings.get",
|
|
729
|
+
actionVersion: 1,
|
|
730
|
+
description: "Read console settings.",
|
|
731
|
+
parameters: {
|
|
732
|
+
type: "object",
|
|
733
|
+
properties: {},
|
|
734
|
+
additionalProperties: false
|
|
735
|
+
},
|
|
736
|
+
outputSchema: {
|
|
737
|
+
type: "object",
|
|
738
|
+
properties: {},
|
|
739
|
+
additionalProperties: true
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
return {
|
|
744
|
+
tools: [first, second],
|
|
745
|
+
byName: new Map([
|
|
746
|
+
[first.name, first],
|
|
747
|
+
[second.name, second]
|
|
748
|
+
])
|
|
749
|
+
};
|
|
750
|
+
},
|
|
751
|
+
toOpenAiToolSchema(tool = {}) {
|
|
752
|
+
return {
|
|
753
|
+
type: "function",
|
|
754
|
+
function: {
|
|
755
|
+
name: tool.name,
|
|
756
|
+
description: tool.description,
|
|
757
|
+
parameters: tool.parameters
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
},
|
|
761
|
+
async executeToolCall({ toolName = "" } = {}) {
|
|
762
|
+
if (toolName === "users_workspace_service_listworkspacesforauthenticatedus") {
|
|
763
|
+
return {
|
|
764
|
+
ok: false,
|
|
765
|
+
error: {
|
|
766
|
+
code: "assistant_tool_failed",
|
|
767
|
+
message: "Workspace listing failed.",
|
|
768
|
+
status: 500
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (toolName === "users_console_settings_service_getsettings") {
|
|
774
|
+
return {
|
|
775
|
+
ok: true,
|
|
776
|
+
result: {
|
|
777
|
+
locale: "en"
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
ok: false,
|
|
784
|
+
error: {
|
|
785
|
+
code: "assistant_tool_unknown",
|
|
786
|
+
message: "Unknown tool.",
|
|
787
|
+
status: 400
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const streamWriter = {
|
|
794
|
+
sendMeta() {},
|
|
795
|
+
sendAssistantDelta(payload = {}) {
|
|
796
|
+
streamedDeltas.push(String(payload.delta || ""));
|
|
797
|
+
},
|
|
798
|
+
sendAssistantMessage(payload = {}) {
|
|
799
|
+
emittedAssistantMessages.push(String(payload.text || ""));
|
|
800
|
+
},
|
|
801
|
+
sendToolCall() {},
|
|
802
|
+
sendToolResult() {},
|
|
803
|
+
sendError() {},
|
|
804
|
+
sendDone() {}
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
const chatService = createChatService({
|
|
808
|
+
aiClient,
|
|
809
|
+
transcriptService,
|
|
810
|
+
serviceToolCatalog,
|
|
811
|
+
assistantSettingsService: createAssistantSettingsServiceStub()
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
await chatService.streamChat(
|
|
815
|
+
{
|
|
816
|
+
messageId: "msg_stream_sanitized",
|
|
817
|
+
input: "Tell me everything."
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
context: {
|
|
821
|
+
actor: {
|
|
822
|
+
id: 7
|
|
823
|
+
},
|
|
824
|
+
workspace: {
|
|
825
|
+
id: 1,
|
|
826
|
+
slug: "tonymobily3"
|
|
827
|
+
},
|
|
828
|
+
surface: "admin"
|
|
829
|
+
},
|
|
830
|
+
streamWriter
|
|
831
|
+
}
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
const streamedText = streamedDeltas.join("");
|
|
835
|
+
assert.equal(streamedText.includes("DSML"), false, streamedText);
|
|
836
|
+
assert.equal(streamedText.includes("Interim notes from successful operations."), true);
|
|
837
|
+
assert.equal(emittedAssistantMessages.length, 1);
|
|
838
|
+
assert.equal(emittedAssistantMessages[0].includes("Interim notes from successful operations."), true);
|
|
839
|
+
assert.equal(emittedAssistantMessages[0].includes("Final response from available data."), true);
|
|
840
|
+
assert.equal(emittedAssistantMessages[0].includes("DSML"), false);
|
|
841
|
+
});
|