@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,987 @@
|
|
|
1
|
+
import { AppError, parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
|
|
2
|
+
import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
+
import { ASSISTANT_STREAM_EVENT_TYPES } from "../../shared/streamEvents.js";
|
|
4
|
+
import { mapStreamError } from "../lib/ndjson.js";
|
|
5
|
+
import { resolveWorkspaceSlug } from "../lib/resolveWorkspaceSlug.js";
|
|
6
|
+
|
|
7
|
+
const MAX_HISTORY_MESSAGES = 20;
|
|
8
|
+
const MAX_INPUT_CHARS = 8000;
|
|
9
|
+
const MAX_TOOL_ROUNDS = 4;
|
|
10
|
+
|
|
11
|
+
function normalizeConversationId(value) {
|
|
12
|
+
const parsed = parsePositiveInteger(value);
|
|
13
|
+
return parsed > 0 ? parsed : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeHistory(history = []) {
|
|
17
|
+
const source = Array.isArray(history) ? history : [];
|
|
18
|
+
return source
|
|
19
|
+
.slice(0, MAX_HISTORY_MESSAGES)
|
|
20
|
+
.map((entry) => {
|
|
21
|
+
const item = normalizeObject(entry);
|
|
22
|
+
const role = normalizeText(item.role).toLowerCase();
|
|
23
|
+
if (role !== "user" && role !== "assistant") {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const content = normalizeText(item.content).slice(0, MAX_INPUT_CHARS);
|
|
28
|
+
if (!content) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
role,
|
|
34
|
+
content
|
|
35
|
+
};
|
|
36
|
+
})
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeStreamInput(payload = {}) {
|
|
41
|
+
const source = normalizeObject(payload);
|
|
42
|
+
const messageId = normalizeText(source.messageId);
|
|
43
|
+
const input = normalizeText(source.input).slice(0, MAX_INPUT_CHARS);
|
|
44
|
+
if (!messageId) {
|
|
45
|
+
throw new AppError(400, "Validation failed.", {
|
|
46
|
+
details: {
|
|
47
|
+
fieldErrors: {
|
|
48
|
+
messageId: "messageId is required."
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!input) {
|
|
54
|
+
throw new AppError(400, "Validation failed.", {
|
|
55
|
+
details: {
|
|
56
|
+
fieldErrors: {
|
|
57
|
+
input: "input is required."
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
messageId,
|
|
65
|
+
conversationId: normalizeConversationId(source.conversationId),
|
|
66
|
+
input,
|
|
67
|
+
history: normalizeHistory(source.history)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function hasStreamWriter(streamWriter) {
|
|
72
|
+
return Boolean(
|
|
73
|
+
streamWriter &&
|
|
74
|
+
typeof streamWriter.sendMeta === "function" &&
|
|
75
|
+
typeof streamWriter.sendAssistantDelta === "function" &&
|
|
76
|
+
typeof streamWriter.sendAssistantMessage === "function" &&
|
|
77
|
+
typeof streamWriter.sendToolCall === "function" &&
|
|
78
|
+
typeof streamWriter.sendToolResult === "function" &&
|
|
79
|
+
typeof streamWriter.sendError === "function" &&
|
|
80
|
+
typeof streamWriter.sendDone === "function"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isAbortError(error) {
|
|
85
|
+
if (!error) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const errorName = String(error.name || "").trim();
|
|
90
|
+
if (errorName === "AbortError") {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function extractTextDelta(deltaContent) {
|
|
98
|
+
if (typeof deltaContent === "string") {
|
|
99
|
+
return deltaContent;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!Array.isArray(deltaContent)) {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return deltaContent
|
|
107
|
+
.map((entry) => {
|
|
108
|
+
if (typeof entry === "string") {
|
|
109
|
+
return entry;
|
|
110
|
+
}
|
|
111
|
+
if (!entry || typeof entry !== "object") {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
return String(entry.text || "");
|
|
115
|
+
})
|
|
116
|
+
.join("");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function toCompactJson(value, fallback = "{}") {
|
|
120
|
+
try {
|
|
121
|
+
if (!value || typeof value !== "object") {
|
|
122
|
+
return fallback;
|
|
123
|
+
}
|
|
124
|
+
return JSON.stringify(value);
|
|
125
|
+
} catch {
|
|
126
|
+
return fallback;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildToolContractLine(toolDescriptor = {}) {
|
|
131
|
+
const name = normalizeText(toolDescriptor.name);
|
|
132
|
+
if (!name) {
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const inputSchema = toolDescriptor.parameters;
|
|
137
|
+
const outputSchema = toolDescriptor.outputSchema;
|
|
138
|
+
return `${name}: input=${toCompactJson(inputSchema)} output=${toCompactJson(outputSchema, "null")}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildSystemPrompt({ toolDescriptors = [], workspaceSlug = "", customSystemPrompt = "" } = {}) {
|
|
142
|
+
const toolSummary = toolDescriptors.length > 0
|
|
143
|
+
? `Available tools: ${toolDescriptors.map((entry) => entry.name).join(", ")}.`
|
|
144
|
+
: "No tools are currently available for this user/session.";
|
|
145
|
+
const toolContracts = toolDescriptors.length > 0
|
|
146
|
+
? `Tool contracts: ${toolDescriptors.map((entry) => buildToolContractLine(entry)).filter(Boolean).join(" | ")}.`
|
|
147
|
+
: "Tool contracts: none.";
|
|
148
|
+
const normalizedWorkspaceSlug = normalizeText(workspaceSlug).toLowerCase();
|
|
149
|
+
const workspaceLine = normalizedWorkspaceSlug
|
|
150
|
+
? `Current workspace slug: ${normalizedWorkspaceSlug}.`
|
|
151
|
+
: "Current workspace slug is unavailable.";
|
|
152
|
+
const normalizedCustomSystemPrompt = String(customSystemPrompt || "").trim();
|
|
153
|
+
|
|
154
|
+
const promptSegments = [
|
|
155
|
+
"You are the workspace assistant.",
|
|
156
|
+
"Use tools when they are necessary and only when available.",
|
|
157
|
+
"Do not mention tools that are not available.",
|
|
158
|
+
"When answering schema questions, rely only on tool contracts and tool results.",
|
|
159
|
+
workspaceLine,
|
|
160
|
+
toolSummary,
|
|
161
|
+
toolContracts
|
|
162
|
+
];
|
|
163
|
+
if (normalizedCustomSystemPrompt) {
|
|
164
|
+
promptSegments.push(`Additional instructions for this surface: ${normalizedCustomSystemPrompt}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return promptSegments.join(" ");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildRecoveryPrompt({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
|
|
171
|
+
const normalizedReason = normalizeText(reason).toLowerCase();
|
|
172
|
+
const failureSummary = (Array.isArray(toolFailures) ? toolFailures : [])
|
|
173
|
+
.slice(0, 3)
|
|
174
|
+
.map((entry) => {
|
|
175
|
+
const toolName = normalizeText(entry?.name) || "unknown_tool";
|
|
176
|
+
const errorCode = normalizeText(entry?.error?.code) || "tool_failed";
|
|
177
|
+
return `${toolName}:${errorCode}`;
|
|
178
|
+
})
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
.join(", ");
|
|
181
|
+
const successSummary = (Array.isArray(toolSuccesses) ? toolSuccesses : [])
|
|
182
|
+
.slice(0, 3)
|
|
183
|
+
.map((entry) => normalizeText(entry?.name))
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.join(", ");
|
|
186
|
+
|
|
187
|
+
const failureSuffix = failureSummary ? ` Recent tool failures: ${failureSummary}.` : "";
|
|
188
|
+
const successSuffix = successSummary ? ` Successful tools: ${successSummary}.` : "";
|
|
189
|
+
if (normalizedReason === "tool_failure") {
|
|
190
|
+
return `One or more tool calls may fail. Continue with available successful results. Do not output function-call markup. Do not mention failed operations unless explicitly asked.${failureSuffix}${successSuffix}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return `Tool-call rounds were exhausted. Provide the best direct answer with available context and successful results only.${failureSuffix}${successSuffix}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildRecoveryFallbackAnswer({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
|
|
197
|
+
const normalizedReason = normalizeText(reason).toLowerCase();
|
|
198
|
+
if (normalizedReason === "tool_failure") {
|
|
199
|
+
return buildToolOutcomeFallbackAnswer({
|
|
200
|
+
toolFailures,
|
|
201
|
+
toolSuccesses
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return "I reached the tool-call limit for this request. Please narrow the request and I will continue.";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function toSafeToolResultText(value) {
|
|
209
|
+
if (value == null) {
|
|
210
|
+
return "null";
|
|
211
|
+
}
|
|
212
|
+
if (typeof value === "string") {
|
|
213
|
+
const normalized = normalizeText(value);
|
|
214
|
+
return normalized || "\"\"";
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
return JSON.stringify(value, null, 2);
|
|
218
|
+
} catch {
|
|
219
|
+
return "\"<unserializable>\"";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildToolOutcomeFallbackAnswer({ toolFailures = [], toolSuccesses = [] } = {}) {
|
|
224
|
+
const successNames = [...new Set(
|
|
225
|
+
(Array.isArray(toolSuccesses) ? toolSuccesses : [])
|
|
226
|
+
.map((entry) => normalizeText(entry?.name))
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
)];
|
|
229
|
+
const hasFailures = Array.isArray(toolFailures) && toolFailures.length > 0;
|
|
230
|
+
|
|
231
|
+
if (successNames.length > 0) {
|
|
232
|
+
const summaryLines = (Array.isArray(toolSuccesses) ? toolSuccesses : [])
|
|
233
|
+
.filter((entry) => normalizeText(entry?.name))
|
|
234
|
+
.map((entry) => {
|
|
235
|
+
const name = normalizeText(entry.name);
|
|
236
|
+
const payload = toSafeToolResultText(entry.result);
|
|
237
|
+
return `- ${name}:\n\`\`\`json\n${payload}\n\`\`\``;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return [
|
|
241
|
+
"I used the available successful results:",
|
|
242
|
+
...summaryLines
|
|
243
|
+
].join("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (hasFailures) {
|
|
247
|
+
return "I could not gather additional information from successful operations.";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return "I could not gather additional information from the available operations.";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function sanitizeAssistantMessageText(value) {
|
|
254
|
+
let source = String(value || "");
|
|
255
|
+
if (!source) {
|
|
256
|
+
return "";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const blockPatterns = [
|
|
260
|
+
/<[^>\n]*function_calls[^>\n]*>[\s\S]*?<\/[^>\n]*function_calls>/gi,
|
|
261
|
+
/<[^>\n]*tool_calls?[^>\n]*>[\s\S]*?<\/[^>\n]*tool_calls?[^>\n]*>/gi,
|
|
262
|
+
/<[^>\n]*invoke\b[^>\n]*>[\s\S]*?<\/[^>\n]*invoke>/gi
|
|
263
|
+
];
|
|
264
|
+
for (const pattern of blockPatterns) {
|
|
265
|
+
source = source.replace(pattern, " ");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const inlineTagPatterns = [
|
|
269
|
+
/<[^>\n]*invoke\b[^>]*\/>/gi,
|
|
270
|
+
/<\/?[^>\n]*invoke[^>\n]*>/gi,
|
|
271
|
+
/<\/?[^>\n]*function_calls[^>\n]*>/gi,
|
|
272
|
+
/<\/?[^>\n]*tool_calls?[^>\n]*>/gi,
|
|
273
|
+
/<\/?[^>\n]*DSML[^>\n]*>/gi
|
|
274
|
+
];
|
|
275
|
+
for (const pattern of inlineTagPatterns) {
|
|
276
|
+
source = source.replace(pattern, " ");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return source
|
|
280
|
+
.split(/\r?\n/)
|
|
281
|
+
.map((line) => line.trim())
|
|
282
|
+
.filter(Boolean)
|
|
283
|
+
.join("\n");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildAssistantToolCallMessage({ assistantText = "", toolCalls = [] } = {}) {
|
|
287
|
+
return {
|
|
288
|
+
role: "assistant",
|
|
289
|
+
content: assistantText || "",
|
|
290
|
+
tool_calls: toolCalls.map((toolCall) => ({
|
|
291
|
+
id: toolCall.id,
|
|
292
|
+
type: "function",
|
|
293
|
+
function: {
|
|
294
|
+
name: toolCall.name,
|
|
295
|
+
arguments: toolCall.arguments
|
|
296
|
+
}
|
|
297
|
+
}))
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function parseDsmlToolCallsFromText(value = "") {
|
|
302
|
+
const source = String(value || "");
|
|
303
|
+
if (!source) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const functionCallsMatch = source.match(
|
|
308
|
+
/<[^>\n]*function_calls[^>\n]*>([\s\S]*?)<\/[^>\n]*function_calls>/i
|
|
309
|
+
);
|
|
310
|
+
if (!functionCallsMatch) {
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const blockText = String(functionCallsMatch[1] || "");
|
|
315
|
+
if (!blockText) {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const calls = [];
|
|
320
|
+
const invokePattern = /<[^>\n]*invoke\b([^>]*)>([\s\S]*?)<\/[^>\n]*invoke>/gi;
|
|
321
|
+
let match = invokePattern.exec(blockText);
|
|
322
|
+
while (match) {
|
|
323
|
+
const attributes = String(match[1] || "");
|
|
324
|
+
const body = normalizeText(String(match[2] || ""));
|
|
325
|
+
const quotedNameMatch =
|
|
326
|
+
attributes.match(/\bname\s*=\s*"([^"]+)"/i) || attributes.match(/\bname\s*=\s*'([^']+)'/i);
|
|
327
|
+
const bareNameMatch = attributes.match(/\bname\s*=\s*([^\s"'/>]+)/i);
|
|
328
|
+
const name = normalizeText(quotedNameMatch?.[1] || bareNameMatch?.[1]);
|
|
329
|
+
if (name) {
|
|
330
|
+
calls.push({
|
|
331
|
+
id: `dsml_tool_call_${calls.length + 1}`,
|
|
332
|
+
name,
|
|
333
|
+
arguments: body && /^[\[{]/.test(body) ? body : "{}"
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
match = invokePattern.exec(blockText);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return calls;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function createDsmlDeltaSanitizer() {
|
|
344
|
+
let inTag = false;
|
|
345
|
+
let tagBuffer = "";
|
|
346
|
+
let suppressedDepth = 0;
|
|
347
|
+
|
|
348
|
+
function resolveTagType(rawTag = "") {
|
|
349
|
+
const normalizedTag = String(rawTag || "").toLowerCase();
|
|
350
|
+
if (normalizedTag.includes("function_calls")) {
|
|
351
|
+
return "function_calls";
|
|
352
|
+
}
|
|
353
|
+
if (normalizedTag.includes("tool_calls")) {
|
|
354
|
+
return "tool_calls";
|
|
355
|
+
}
|
|
356
|
+
if (normalizedTag.includes("invoke")) {
|
|
357
|
+
return "invoke";
|
|
358
|
+
}
|
|
359
|
+
return "";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function processTag(rawTag = "") {
|
|
363
|
+
const source = String(rawTag || "");
|
|
364
|
+
const inner = source.slice(1, -1).trim();
|
|
365
|
+
const isClosing = inner.startsWith("/");
|
|
366
|
+
const isSelfClosing = inner.endsWith("/");
|
|
367
|
+
const tagType = resolveTagType(inner);
|
|
368
|
+
|
|
369
|
+
if (suppressedDepth > 0) {
|
|
370
|
+
if (tagType && isClosing) {
|
|
371
|
+
suppressedDepth = Math.max(0, suppressedDepth - 1);
|
|
372
|
+
} else if (tagType && !isClosing && !isSelfClosing) {
|
|
373
|
+
suppressedDepth += 1;
|
|
374
|
+
}
|
|
375
|
+
return "";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!tagType) {
|
|
379
|
+
return source;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!isClosing && !isSelfClosing) {
|
|
383
|
+
suppressedDepth = 1;
|
|
384
|
+
}
|
|
385
|
+
return "";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function process(delta = "") {
|
|
389
|
+
const source = String(delta || "");
|
|
390
|
+
if (!source) {
|
|
391
|
+
return "";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let output = "";
|
|
395
|
+
for (const char of source) {
|
|
396
|
+
if (inTag) {
|
|
397
|
+
tagBuffer += char;
|
|
398
|
+
if (char === ">") {
|
|
399
|
+
inTag = false;
|
|
400
|
+
output += processTag(tagBuffer);
|
|
401
|
+
tagBuffer = "";
|
|
402
|
+
}
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (char === "<") {
|
|
407
|
+
inTag = true;
|
|
408
|
+
tagBuffer = "<";
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (suppressedDepth < 1) {
|
|
413
|
+
output += char;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return output;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function flush() {
|
|
421
|
+
return "";
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return Object.freeze({
|
|
425
|
+
process,
|
|
426
|
+
flush
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function consumeCompletionStream({ stream, streamWriter, emitDeltas = true, deltaSanitizer = null } = {}) {
|
|
431
|
+
let assistantText = "";
|
|
432
|
+
let streamedAssistantText = "";
|
|
433
|
+
const toolCallsByIndex = new Map();
|
|
434
|
+
|
|
435
|
+
for await (const chunk of stream) {
|
|
436
|
+
const choice = chunk?.choices?.[0] || {};
|
|
437
|
+
const delta = choice?.delta || {};
|
|
438
|
+
|
|
439
|
+
const textDelta = extractTextDelta(delta.content);
|
|
440
|
+
if (textDelta) {
|
|
441
|
+
assistantText += textDelta;
|
|
442
|
+
if (emitDeltas) {
|
|
443
|
+
const safeDelta =
|
|
444
|
+
deltaSanitizer && typeof deltaSanitizer.process === "function"
|
|
445
|
+
? String(deltaSanitizer.process(textDelta) || "")
|
|
446
|
+
: textDelta;
|
|
447
|
+
if (safeDelta) {
|
|
448
|
+
streamedAssistantText += safeDelta;
|
|
449
|
+
streamWriter.sendAssistantDelta({
|
|
450
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_DELTA,
|
|
451
|
+
delta: safeDelta
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const toolCalls = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
458
|
+
for (const partialToolCall of toolCalls) {
|
|
459
|
+
const index = Number(partialToolCall?.index || 0);
|
|
460
|
+
const existing =
|
|
461
|
+
toolCallsByIndex.get(index) ||
|
|
462
|
+
{
|
|
463
|
+
id: normalizeText(partialToolCall?.id) || `tool_call_${index + 1}`,
|
|
464
|
+
name: "",
|
|
465
|
+
arguments: ""
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
if (partialToolCall?.id) {
|
|
469
|
+
existing.id = normalizeText(partialToolCall.id) || existing.id;
|
|
470
|
+
}
|
|
471
|
+
if (partialToolCall?.function?.name) {
|
|
472
|
+
existing.name += String(partialToolCall.function.name || "");
|
|
473
|
+
}
|
|
474
|
+
if (partialToolCall?.function?.arguments) {
|
|
475
|
+
existing.arguments += String(partialToolCall.function.arguments || "");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
toolCallsByIndex.set(index, existing);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let toolCalls = [...toolCallsByIndex.values()].map((toolCall, index) => ({
|
|
483
|
+
id: normalizeText(toolCall.id) || `tool_call_${index + 1}`,
|
|
484
|
+
name: normalizeText(toolCall.name),
|
|
485
|
+
arguments: String(toolCall.arguments || "")
|
|
486
|
+
}));
|
|
487
|
+
|
|
488
|
+
if (toolCalls.length < 1) {
|
|
489
|
+
const parsedDsmlCalls = parseDsmlToolCallsFromText(assistantText);
|
|
490
|
+
if (parsedDsmlCalls.length > 0) {
|
|
491
|
+
toolCalls = parsedDsmlCalls;
|
|
492
|
+
assistantText = normalizeText(sanitizeAssistantMessageText(assistantText));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (emitDeltas && deltaSanitizer && typeof deltaSanitizer.flush === "function") {
|
|
497
|
+
const trailing = String(deltaSanitizer.flush() || "");
|
|
498
|
+
if (trailing) {
|
|
499
|
+
streamedAssistantText += trailing;
|
|
500
|
+
streamWriter.sendAssistantDelta({
|
|
501
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_DELTA,
|
|
502
|
+
delta: trailing
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
assistantText,
|
|
509
|
+
streamedAssistantText,
|
|
510
|
+
toolCalls
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function mergeAssistantMessageText(streamedText = "", completionText = "") {
|
|
515
|
+
const streamed = normalizeText(sanitizeAssistantMessageText(streamedText));
|
|
516
|
+
const completion = normalizeText(sanitizeAssistantMessageText(completionText));
|
|
517
|
+
|
|
518
|
+
if (!streamed) {
|
|
519
|
+
return completion;
|
|
520
|
+
}
|
|
521
|
+
if (!completion) {
|
|
522
|
+
return streamed;
|
|
523
|
+
}
|
|
524
|
+
if (streamed === completion) {
|
|
525
|
+
return streamed;
|
|
526
|
+
}
|
|
527
|
+
if (completion.startsWith(streamed) || completion.includes(streamed)) {
|
|
528
|
+
return completion;
|
|
529
|
+
}
|
|
530
|
+
if (streamed.startsWith(completion) || streamed.includes(completion)) {
|
|
531
|
+
return streamed;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return `${streamed}\n${completion}`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function createChatService({ aiClient, transcriptService, serviceToolCatalog, assistantSettingsService } = {}) {
|
|
538
|
+
if (!aiClient || !transcriptService || !serviceToolCatalog || !assistantSettingsService) {
|
|
539
|
+
throw new Error("createChatService requires aiClient, transcriptService, serviceToolCatalog, and assistantSettingsService.");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function streamChat(payload = {}, options = {}) {
|
|
543
|
+
if (!aiClient.enabled) {
|
|
544
|
+
throw new AppError(503, "Assistant provider is not configured.");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const source = normalizeStreamInput(payload);
|
|
548
|
+
const context = normalizeObject(options.context);
|
|
549
|
+
const streamWriter = options.streamWriter;
|
|
550
|
+
if (!hasStreamWriter(streamWriter)) {
|
|
551
|
+
throw new Error("assistant.chat.stream requires streamWriter methods.");
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const actor = context.actor;
|
|
555
|
+
const workspace = context.workspace;
|
|
556
|
+
|
|
557
|
+
const conversationResult = await transcriptService.createConversationForTurn(
|
|
558
|
+
workspace,
|
|
559
|
+
actor,
|
|
560
|
+
{
|
|
561
|
+
conversationId: source.conversationId,
|
|
562
|
+
provider: aiClient.provider,
|
|
563
|
+
model: aiClient.defaultModel,
|
|
564
|
+
surfaceId: context.surface,
|
|
565
|
+
messageId: source.messageId
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
context
|
|
569
|
+
}
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
const conversation = conversationResult.conversation;
|
|
573
|
+
const conversationId = conversation?.id;
|
|
574
|
+
if (!conversationId) {
|
|
575
|
+
throw new AppError(500, "Assistant failed to create conversation.");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
await transcriptService.appendMessage(
|
|
579
|
+
conversationId,
|
|
580
|
+
{
|
|
581
|
+
role: "user",
|
|
582
|
+
kind: "chat",
|
|
583
|
+
clientMessageId: source.messageId,
|
|
584
|
+
contentText: source.input,
|
|
585
|
+
metadata: {
|
|
586
|
+
surfaceId: normalizeText(context.surface)
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
context
|
|
591
|
+
}
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
const toolSet = serviceToolCatalog.resolveToolSet(context);
|
|
595
|
+
const customSystemPrompt = await assistantSettingsService.resolveSystemPrompt(
|
|
596
|
+
workspace,
|
|
597
|
+
{
|
|
598
|
+
surface: context.surface
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
context
|
|
602
|
+
}
|
|
603
|
+
);
|
|
604
|
+
const systemPrompt = buildSystemPrompt({
|
|
605
|
+
toolDescriptors: toolSet.tools,
|
|
606
|
+
workspaceSlug: resolveWorkspaceSlug(context, source),
|
|
607
|
+
customSystemPrompt
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const messages = [
|
|
611
|
+
{
|
|
612
|
+
role: "system",
|
|
613
|
+
content: systemPrompt
|
|
614
|
+
},
|
|
615
|
+
...source.history,
|
|
616
|
+
{
|
|
617
|
+
role: "user",
|
|
618
|
+
content: source.input
|
|
619
|
+
}
|
|
620
|
+
];
|
|
621
|
+
let streamedAssistantText = "";
|
|
622
|
+
|
|
623
|
+
async function completeWithAssistantMessage(assistantMessageText, { metadata = {} } = {}) {
|
|
624
|
+
const normalizedAssistantMessageText = mergeAssistantMessageText(streamedAssistantText, assistantMessageText);
|
|
625
|
+
if (!normalizedAssistantMessageText) {
|
|
626
|
+
throw new AppError(502, "Assistant returned no output.");
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
await transcriptService.appendMessage(
|
|
630
|
+
conversationId,
|
|
631
|
+
{
|
|
632
|
+
role: "assistant",
|
|
633
|
+
kind: "chat",
|
|
634
|
+
contentText: normalizedAssistantMessageText
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
context
|
|
638
|
+
}
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
await transcriptService.completeConversation(
|
|
642
|
+
conversationId,
|
|
643
|
+
{
|
|
644
|
+
status: "completed",
|
|
645
|
+
metadata
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
context
|
|
649
|
+
}
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
streamWriter.sendAssistantMessage({
|
|
653
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE,
|
|
654
|
+
text: normalizedAssistantMessageText
|
|
655
|
+
});
|
|
656
|
+
streamWriter.sendDone({
|
|
657
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.DONE,
|
|
658
|
+
messageId: source.messageId,
|
|
659
|
+
status: "completed"
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
conversationId,
|
|
664
|
+
messageId: source.messageId,
|
|
665
|
+
status: "completed"
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function executeToolCalls(toolCalls = [], { toolFailures = [], toolSuccesses = [] } = {}) {
|
|
670
|
+
const roundFailures = [];
|
|
671
|
+
|
|
672
|
+
for (const toolCall of toolCalls) {
|
|
673
|
+
streamWriter.sendToolCall({
|
|
674
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_CALL,
|
|
675
|
+
toolCallId: toolCall.id,
|
|
676
|
+
name: toolCall.name,
|
|
677
|
+
arguments: toolCall.arguments
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
await transcriptService.appendMessage(
|
|
681
|
+
conversationId,
|
|
682
|
+
{
|
|
683
|
+
role: "assistant",
|
|
684
|
+
kind: "tool_call",
|
|
685
|
+
contentText: toolCall.arguments,
|
|
686
|
+
metadata: {
|
|
687
|
+
toolCallId: toolCall.id,
|
|
688
|
+
tool: toolCall.name
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
context
|
|
693
|
+
}
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
const toolResult = await serviceToolCatalog.executeToolCall({
|
|
697
|
+
toolName: toolCall.name,
|
|
698
|
+
argumentsText: toolCall.arguments,
|
|
699
|
+
context,
|
|
700
|
+
toolSet
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
await transcriptService.appendMessage(
|
|
704
|
+
conversationId,
|
|
705
|
+
{
|
|
706
|
+
role: "assistant",
|
|
707
|
+
kind: "tool_result",
|
|
708
|
+
contentText: JSON.stringify(toolResult),
|
|
709
|
+
metadata: {
|
|
710
|
+
toolCallId: toolCall.id,
|
|
711
|
+
tool: toolCall.name,
|
|
712
|
+
ok: toolResult.ok === true
|
|
713
|
+
}
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
context
|
|
717
|
+
}
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
if (toolResult.ok) {
|
|
721
|
+
toolSuccesses.push({
|
|
722
|
+
name: toolCall.name,
|
|
723
|
+
result: toolResult.result
|
|
724
|
+
});
|
|
725
|
+
streamWriter.sendToolResult({
|
|
726
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_RESULT,
|
|
727
|
+
toolCallId: toolCall.id,
|
|
728
|
+
name: toolCall.name,
|
|
729
|
+
ok: true,
|
|
730
|
+
result: toolResult.result
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
messages.push({
|
|
734
|
+
role: "tool",
|
|
735
|
+
tool_call_id: toolCall.id,
|
|
736
|
+
content: JSON.stringify(toolResult.result ?? null)
|
|
737
|
+
});
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const failure = {
|
|
742
|
+
name: toolCall.name,
|
|
743
|
+
error: toolResult.error
|
|
744
|
+
};
|
|
745
|
+
roundFailures.push(failure);
|
|
746
|
+
toolFailures.push(failure);
|
|
747
|
+
|
|
748
|
+
streamWriter.sendToolResult({
|
|
749
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_RESULT,
|
|
750
|
+
toolCallId: toolCall.id,
|
|
751
|
+
name: toolCall.name,
|
|
752
|
+
ok: false,
|
|
753
|
+
error: toolResult.error
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
messages.push({
|
|
757
|
+
role: "tool",
|
|
758
|
+
tool_call_id: toolCall.id,
|
|
759
|
+
content: JSON.stringify({
|
|
760
|
+
error: toolResult.error
|
|
761
|
+
})
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return roundFailures;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function recoverWithoutTools({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
|
|
769
|
+
const MAX_RECOVERY_PASSES = 3;
|
|
770
|
+
for (let pass = 0; pass < MAX_RECOVERY_PASSES; pass += 1) {
|
|
771
|
+
const recoveryMessages = [
|
|
772
|
+
...messages,
|
|
773
|
+
{
|
|
774
|
+
role: "system",
|
|
775
|
+
content: buildRecoveryPrompt({
|
|
776
|
+
reason,
|
|
777
|
+
toolFailures,
|
|
778
|
+
toolSuccesses
|
|
779
|
+
})
|
|
780
|
+
}
|
|
781
|
+
];
|
|
782
|
+
|
|
783
|
+
const completionStream = await aiClient.createChatCompletionStream({
|
|
784
|
+
messages: recoveryMessages,
|
|
785
|
+
tools: [],
|
|
786
|
+
signal: options.abortSignal
|
|
787
|
+
});
|
|
788
|
+
const completion = await consumeCompletionStream({
|
|
789
|
+
stream: completionStream,
|
|
790
|
+
streamWriter,
|
|
791
|
+
emitDeltas: true,
|
|
792
|
+
deltaSanitizer: createDsmlDeltaSanitizer()
|
|
793
|
+
});
|
|
794
|
+
streamedAssistantText += String(completion.streamedAssistantText || "");
|
|
795
|
+
|
|
796
|
+
const recoveryToolCalls = completion.toolCalls.filter((entry) => entry.name);
|
|
797
|
+
if (recoveryToolCalls.length > 0) {
|
|
798
|
+
messages.push(
|
|
799
|
+
buildAssistantToolCallMessage({
|
|
800
|
+
assistantText: completion.assistantText,
|
|
801
|
+
toolCalls: recoveryToolCalls
|
|
802
|
+
})
|
|
803
|
+
);
|
|
804
|
+
await executeToolCalls(recoveryToolCalls, {
|
|
805
|
+
toolFailures,
|
|
806
|
+
toolSuccesses
|
|
807
|
+
});
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const assistantMessageText = normalizeText(sanitizeAssistantMessageText(completion.assistantText));
|
|
812
|
+
if (assistantMessageText) {
|
|
813
|
+
return completeWithAssistantMessage(assistantMessageText, {
|
|
814
|
+
metadata: {
|
|
815
|
+
recoveryReason: reason || "unknown",
|
|
816
|
+
toolFailureCount: Array.isArray(toolFailures) ? toolFailures.length : 0
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const fallbackText = buildRecoveryFallbackAnswer({
|
|
823
|
+
reason,
|
|
824
|
+
toolFailures,
|
|
825
|
+
toolSuccesses
|
|
826
|
+
});
|
|
827
|
+
return completeWithAssistantMessage(fallbackText, {
|
|
828
|
+
metadata: {
|
|
829
|
+
recoveryReason: reason || "unknown",
|
|
830
|
+
toolFailureCount: Array.isArray(toolFailures) ? toolFailures.length : 0
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
let streamed = false;
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
streamWriter.sendMeta({
|
|
839
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.META,
|
|
840
|
+
messageId: source.messageId,
|
|
841
|
+
conversationId,
|
|
842
|
+
provider: aiClient.provider,
|
|
843
|
+
model: aiClient.defaultModel
|
|
844
|
+
});
|
|
845
|
+
streamed = true;
|
|
846
|
+
|
|
847
|
+
const excludedToolNames = new Set();
|
|
848
|
+
const toolFailures = [];
|
|
849
|
+
const toolSuccesses = [];
|
|
850
|
+
|
|
851
|
+
for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
|
|
852
|
+
const roundToolDescriptors = toolSet.tools.filter(
|
|
853
|
+
(tool) => !excludedToolNames.has(normalizeText(tool.name))
|
|
854
|
+
);
|
|
855
|
+
const roundToolSchemas = roundToolDescriptors.map((tool) => serviceToolCatalog.toOpenAiToolSchema(tool));
|
|
856
|
+
|
|
857
|
+
const completionStream = await aiClient.createChatCompletionStream({
|
|
858
|
+
messages,
|
|
859
|
+
tools: roundToolSchemas,
|
|
860
|
+
signal: options.abortSignal
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
const completion = await consumeCompletionStream({
|
|
864
|
+
stream: completionStream,
|
|
865
|
+
streamWriter,
|
|
866
|
+
emitDeltas: true,
|
|
867
|
+
deltaSanitizer: createDsmlDeltaSanitizer()
|
|
868
|
+
});
|
|
869
|
+
streamedAssistantText += String(completion.streamedAssistantText || "");
|
|
870
|
+
|
|
871
|
+
const toolCalls = completion.toolCalls.filter((entry) => entry.name);
|
|
872
|
+
if (toolCalls.length < 1) {
|
|
873
|
+
const finalMessageText = normalizeText(sanitizeAssistantMessageText(completion.assistantText));
|
|
874
|
+
if (finalMessageText) {
|
|
875
|
+
return completeWithAssistantMessage(finalMessageText, {
|
|
876
|
+
metadata: toolFailures.length > 0
|
|
877
|
+
? {
|
|
878
|
+
recoveryReason: "tool_failure",
|
|
879
|
+
toolFailureCount: toolFailures.length
|
|
880
|
+
}
|
|
881
|
+
: {}
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (toolFailures.length > 0) {
|
|
886
|
+
return recoverWithoutTools({
|
|
887
|
+
reason: "tool_failure",
|
|
888
|
+
toolFailures,
|
|
889
|
+
toolSuccesses
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return completeWithAssistantMessage(completion.assistantText);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
messages.push(
|
|
897
|
+
buildAssistantToolCallMessage({
|
|
898
|
+
assistantText: completion.assistantText,
|
|
899
|
+
toolCalls
|
|
900
|
+
})
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
const roundFailures = await executeToolCalls(toolCalls, {
|
|
904
|
+
toolFailures,
|
|
905
|
+
toolSuccesses
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
if (roundFailures.length > 0) {
|
|
909
|
+
for (const failure of roundFailures) {
|
|
910
|
+
const toolName = normalizeText(failure?.name);
|
|
911
|
+
if (toolName) {
|
|
912
|
+
excludedToolNames.add(toolName);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return recoverWithoutTools({
|
|
919
|
+
reason: toolFailures.length > 0 ? "tool_failure" : "max_tool_rounds",
|
|
920
|
+
toolFailures,
|
|
921
|
+
toolSuccesses
|
|
922
|
+
});
|
|
923
|
+
} catch (error) {
|
|
924
|
+
const aborted = isAbortError(error);
|
|
925
|
+
const status = aborted ? "aborted" : "failed";
|
|
926
|
+
const streamError = mapStreamError(error);
|
|
927
|
+
|
|
928
|
+
if (streamed) {
|
|
929
|
+
await transcriptService.completeConversation(
|
|
930
|
+
conversationId,
|
|
931
|
+
{
|
|
932
|
+
status
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
context
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
streamWriter.sendError({
|
|
940
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.ERROR,
|
|
941
|
+
messageId: source.messageId,
|
|
942
|
+
code: aborted ? "assistant_stream_aborted" : streamError.code,
|
|
943
|
+
message: aborted ? "Assistant request was cancelled." : streamError.message,
|
|
944
|
+
status: aborted ? 499 : streamError.status
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
streamWriter.sendDone({
|
|
948
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.DONE,
|
|
949
|
+
messageId: source.messageId,
|
|
950
|
+
status
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
return {
|
|
954
|
+
conversationId,
|
|
955
|
+
messageId: source.messageId,
|
|
956
|
+
status
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async function listConversations(query = {}, options = {}) {
|
|
965
|
+
const context = normalizeObject(options.context);
|
|
966
|
+
return transcriptService.listConversationsForUser(context.workspace, context.actor, query, {
|
|
967
|
+
context
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async function getConversationMessages(conversationId, query = {}, options = {}) {
|
|
972
|
+
const context = normalizeObject(options.context);
|
|
973
|
+
return transcriptService.getConversationMessagesForUser(context.workspace, context.actor, conversationId, query, {
|
|
974
|
+
context
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return Object.freeze({
|
|
979
|
+
streamChat,
|
|
980
|
+
listConversations,
|
|
981
|
+
getConversationMessages
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export {
|
|
986
|
+
createChatService
|
|
987
|
+
};
|