@jskit-ai/assistant-runtime 0.1.193 → 0.1.194
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/fixtures/canonical-conversation/App.vue +188 -0
- package/fixtures/canonical-conversation/DraftConversation.vue +32 -0
- package/fixtures/canonical-conversation/index.html +1 -0
- package/fixtures/canonical-conversation/main.js +61 -0
- package/fixtures/canonical-conversation/socket.js +50 -0
- package/fixtures/canonical-conversation/vite.config.mjs +178 -0
- package/fixtures/conversation/vite.config.mjs +12 -2
- package/package.json +23 -10
- package/src/client/components/AssistantConversationClientElement.vue +57 -0
- package/src/client/composables/useAssistantConversation.js +874 -0
- package/src/client/index.js +2 -0
- package/src/client/support/assistantRuntimeState.js +1 -1
- package/src/client/support/subscribeAssistantConversation.js +205 -0
- package/src/server/AssistantProvider.js +82 -106
- package/src/server/actionIds.js +10 -0
- package/src/server/actions.js +287 -79
- package/src/server/createAssistantRuntime.js +138 -0
- package/src/server/createAssistantSettingsActions.js +89 -0
- package/src/server/index.js +6 -0
- package/src/server/inputSchemas.js +90 -0
- package/src/server/registerConversationSubscriptions.js +163 -0
- package/src/server/registerFastifyConversations.js +76 -0
- package/src/server/registerRoutes.js +220 -198
- package/src/server/registerSettingsRoutes.js +113 -0
- package/src/server/repositories/conversationsRepository.js +12 -5
- package/src/server/repositories/messagesRepository.js +12 -0
- package/src/server/services/chatService.js +88 -694
- package/src/server/services/transcriptService.js +56 -52
- package/src/server/sqlConversationStorage.js +192 -0
- package/src/server/support/assistantRouteContext.js +91 -0
- package/src/server/support/createSurfaceAwareToolCatalog.js +6 -2
- package/src/shared/assistantSurfaces.js +14 -0
- package/src/shared/conversationRealtime.js +9 -0
- package/src/shared/index.js +1 -0
- package/test/actionDefinitions.test.js +44 -0
- package/test/aiConnections.test.js +1 -1
- package/test/assistantConversationBinding.browser.test.js +1868 -0
- package/test/assistantConversationOperations.test.js +459 -0
- package/test/assistantConversationSubscriptions.test.js +692 -0
- package/test/assistantIntegrationBoundary.test.js +103 -0
- package/test/assistantRuntimeComposition.test.js +68 -0
- package/test/assistantSurfacePattern.test.js +1 -1
- package/test/assistantSurfaces.test.js +23 -0
- package/test/chatServiceLifecycle.test.js +12 -1
- package/test/featureRuntime.test.js +238 -0
- package/test/registerFastifyConversations.test.js +169 -0
- package/test/sqlConversationStorage.test.js +205 -0
- package/test/support/memoryAssistantRows.js +51 -0
- package/test/turnRequests.test.js +11 -2
- package/src/shared/assistantResponseText.js +0 -29
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { AppError } from "@jskit-ai/kernel/server/runtime";
|
|
2
2
|
import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
|
|
3
|
-
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
4
3
|
import { createJsonApiResourceRouteContract } from "@jskit-ai/http-runtime/shared/validators/jsonApiRouteTransport";
|
|
5
4
|
import {
|
|
6
5
|
ASSISTANT_CONVERSATIONS_TRANSPORT,
|
|
7
6
|
ASSISTANT_CONVERSATION_MESSAGES_TRANSPORT,
|
|
8
|
-
ASSISTANT_SETTINGS_UPDATE_TRANSPORT,
|
|
9
|
-
ASSISTANT_SETTINGS_TRANSPORT,
|
|
10
7
|
assistantConversationOutputValidator,
|
|
11
|
-
assistantConfigResource,
|
|
12
8
|
assistantResource,
|
|
13
9
|
resolveAssistantApiBasePath
|
|
14
10
|
} from "@jskit-ai/assistant-core/shared";
|
|
@@ -18,56 +14,20 @@ import {
|
|
|
18
14
|
setNdjsonHeaders,
|
|
19
15
|
writeNdjson
|
|
20
16
|
} from "@jskit-ai/assistant-core/server";
|
|
21
|
-
import { resolveAssistantSurfaceConfig } from "../shared/assistantSurfaces.js";
|
|
22
17
|
import { actionIds } from "./actionIds.js";
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function readWorkspaceInput(request, requiresWorkspace, workspaceScopeSupport = null) {
|
|
40
|
-
if (requiresWorkspace !== true) {
|
|
41
|
-
return {};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (!workspaceScopeSupport) {
|
|
45
|
-
throw new Error("Assistant workspace routes require workspace server scope support.");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return workspaceScopeSupport.buildInputFromRouteParams(request?.input?.params);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function requireAssistantSurface(appConfig = {}, targetSurfaceId = "") {
|
|
52
|
-
const assistantSurface = resolveAssistantSurfaceConfig(appConfig, targetSurfaceId);
|
|
53
|
-
if (assistantSurface) {
|
|
54
|
-
return assistantSurface;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
throw new AppError(404, "Assistant not found.");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function requireHostSurfaceId(request) {
|
|
61
|
-
const headerValue = Array.isArray(request?.headers?.["x-jskit-surface"])
|
|
62
|
-
? request.headers["x-jskit-surface"][0]
|
|
63
|
-
: request?.headers?.["x-jskit-surface"];
|
|
64
|
-
const hostSurfaceId = normalizeSurfaceId(headerValue);
|
|
65
|
-
if (hostSurfaceId) {
|
|
66
|
-
return hostSurfaceId;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
throw new AppError(400, "Assistant surface header x-jskit-surface is required.");
|
|
70
|
-
}
|
|
18
|
+
import {
|
|
19
|
+
assistantConversationIdInputValidator,
|
|
20
|
+
assistantConversationGoalBodyValidator,
|
|
21
|
+
assistantConversationMessageIdInputValidator,
|
|
22
|
+
assistantConversationReadQueryValidator,
|
|
23
|
+
conversationConfigureBodyValidator,
|
|
24
|
+
conversationSelectBodyValidator,
|
|
25
|
+
conversationReplaceBodyValidator,
|
|
26
|
+
conversationSendBodyValidator,
|
|
27
|
+
assistantSurfaceRouteParamsValidator
|
|
28
|
+
} from "./inputSchemas.js";
|
|
29
|
+
import { registerSettingsRoutes } from "./registerSettingsRoutes.js";
|
|
30
|
+
import { requireWorkspaceAssistantRouteParams, resolveRouteRequestState } from "./support/assistantRouteContext.js";
|
|
71
31
|
|
|
72
32
|
function shouldExposeAppErrorDetails(errorCode = "") {
|
|
73
33
|
return String(errorCode || "").trim() !== "ACTION_PERMISSION_DENIED";
|
|
@@ -106,43 +66,6 @@ function sendPreStreamErrorResponse(reply, error) {
|
|
|
106
66
|
});
|
|
107
67
|
}
|
|
108
68
|
|
|
109
|
-
function resolveRouteRequestState(
|
|
110
|
-
request,
|
|
111
|
-
{
|
|
112
|
-
resolveCurrentAppConfig = () => ({}),
|
|
113
|
-
kind = "runtime",
|
|
114
|
-
requiresWorkspace = false,
|
|
115
|
-
workspaceScopeSupport = null
|
|
116
|
-
} = {}
|
|
117
|
-
) {
|
|
118
|
-
const appConfig = resolveCurrentAppConfig();
|
|
119
|
-
const targetSurfaceId = normalizeSurfaceId(request?.input?.params?.surfaceId);
|
|
120
|
-
const assistantSurface = requireAssistantSurface(appConfig, targetSurfaceId);
|
|
121
|
-
const hostSurfaceId = requireHostSurfaceId(request);
|
|
122
|
-
const expectsWorkspace = kind === "settings"
|
|
123
|
-
? assistantSurface.settingsSurfaceRequiresWorkspace
|
|
124
|
-
: assistantSurface.runtimeSurfaceRequiresWorkspace;
|
|
125
|
-
const expectedHostSurfaceId = kind === "settings"
|
|
126
|
-
? assistantSurface.settingsSurfaceId
|
|
127
|
-
: assistantSurface.targetSurfaceId;
|
|
128
|
-
|
|
129
|
-
if (expectsWorkspace !== (requiresWorkspace === true)) {
|
|
130
|
-
throw new AppError(404, "Assistant route not found.");
|
|
131
|
-
}
|
|
132
|
-
if (hostSurfaceId !== expectedHostSurfaceId) {
|
|
133
|
-
throw new AppError(403, "Assistant route is not available on this surface.");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return Object.freeze({
|
|
137
|
-
assistantSurface,
|
|
138
|
-
hostSurfaceId,
|
|
139
|
-
actionInput: Object.freeze({
|
|
140
|
-
targetSurfaceId: assistantSurface.targetSurfaceId,
|
|
141
|
-
...readWorkspaceInput(request, requiresWorkspace, workspaceScopeSupport)
|
|
142
|
-
})
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
69
|
function buildChatStreamActionInput(routeInput = {}, requestBody = {}) {
|
|
147
70
|
const actionInput = {
|
|
148
71
|
...routeInput,
|
|
@@ -159,15 +82,6 @@ function buildChatStreamActionInput(routeInput = {}, requestBody = {}) {
|
|
|
159
82
|
return actionInput;
|
|
160
83
|
}
|
|
161
84
|
|
|
162
|
-
function resolveAssistantSettingsRecordId(record = {}) {
|
|
163
|
-
const scopeKey = String(record?.scopeKey || "").trim();
|
|
164
|
-
if (!scopeKey) {
|
|
165
|
-
throw new Error("Assistant settings JSON:API response requires scopeKey.");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return scopeKey;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
85
|
function resolveAssistantConversationMessagesRecordId(record = {}) {
|
|
172
86
|
const conversationId = String(record?.conversation?.id || "").trim();
|
|
173
87
|
if (!conversationId) {
|
|
@@ -177,98 +91,6 @@ function resolveAssistantConversationMessagesRecordId(record = {}) {
|
|
|
177
91
|
return conversationId;
|
|
178
92
|
}
|
|
179
93
|
|
|
180
|
-
function registerSettingsRoutes(
|
|
181
|
-
router,
|
|
182
|
-
resolveCurrentAppConfig,
|
|
183
|
-
{ requiresWorkspace = false, workspaceScopeSupport = null } = {}
|
|
184
|
-
) {
|
|
185
|
-
const routeBase = resolveAssistantApiBasePath({
|
|
186
|
-
requiresWorkspace
|
|
187
|
-
});
|
|
188
|
-
const visibility = requiresWorkspace ? "workspace" : "public";
|
|
189
|
-
const routePath = `${routeBase}/:surfaceId/settings`;
|
|
190
|
-
const params = requiresWorkspace === true
|
|
191
|
-
? requireWorkspaceAssistantRouteParams(workspaceScopeSupport)
|
|
192
|
-
: assistantSurfaceRouteParamsValidator;
|
|
193
|
-
|
|
194
|
-
router.register(
|
|
195
|
-
"GET",
|
|
196
|
-
routePath,
|
|
197
|
-
{
|
|
198
|
-
auth: "required",
|
|
199
|
-
visibility,
|
|
200
|
-
params,
|
|
201
|
-
meta: {
|
|
202
|
-
tags: ["assistant", "settings"],
|
|
203
|
-
summary: "Get assistant settings."
|
|
204
|
-
},
|
|
205
|
-
...createJsonApiResourceRouteContract({
|
|
206
|
-
...ASSISTANT_SETTINGS_TRANSPORT,
|
|
207
|
-
output: assistantConfigResource.operations.view.output,
|
|
208
|
-
outputKind: "record",
|
|
209
|
-
getRecordId: resolveAssistantSettingsRecordId
|
|
210
|
-
})
|
|
211
|
-
},
|
|
212
|
-
async function assistantSettingsReadRoute(request, reply) {
|
|
213
|
-
const routeState = resolveRouteRequestState(request, {
|
|
214
|
-
resolveCurrentAppConfig,
|
|
215
|
-
kind: "settings",
|
|
216
|
-
requiresWorkspace,
|
|
217
|
-
workspaceScopeSupport
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const response = await request.executeAction({
|
|
221
|
-
actionId: actionIds.settingsRead,
|
|
222
|
-
surface: routeState.hostSurfaceId,
|
|
223
|
-
input: routeState.actionInput
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
reply.code(200).send(response);
|
|
227
|
-
}
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
router.register(
|
|
231
|
-
"PATCH",
|
|
232
|
-
routePath,
|
|
233
|
-
{
|
|
234
|
-
auth: "required",
|
|
235
|
-
visibility,
|
|
236
|
-
params,
|
|
237
|
-
meta: {
|
|
238
|
-
tags: ["assistant", "settings"],
|
|
239
|
-
summary: "Update assistant settings."
|
|
240
|
-
},
|
|
241
|
-
...createJsonApiResourceRouteContract({
|
|
242
|
-
...ASSISTANT_SETTINGS_UPDATE_TRANSPORT,
|
|
243
|
-
body: assistantConfigResource.operations.patch.body,
|
|
244
|
-
output: assistantConfigResource.operations.patch.output,
|
|
245
|
-
outputKind: "record",
|
|
246
|
-
getRecordId: resolveAssistantSettingsRecordId,
|
|
247
|
-
includeValidation400: true
|
|
248
|
-
})
|
|
249
|
-
},
|
|
250
|
-
async function assistantSettingsPatchRoute(request, reply) {
|
|
251
|
-
const routeState = resolveRouteRequestState(request, {
|
|
252
|
-
resolveCurrentAppConfig,
|
|
253
|
-
kind: "settings",
|
|
254
|
-
requiresWorkspace,
|
|
255
|
-
workspaceScopeSupport
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
const response = await request.executeAction({
|
|
259
|
-
actionId: actionIds.settingsUpdate,
|
|
260
|
-
surface: routeState.hostSurfaceId,
|
|
261
|
-
input: {
|
|
262
|
-
...routeState.actionInput,
|
|
263
|
-
patch: request.input.body
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
reply.code(200).send(response);
|
|
268
|
-
}
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
94
|
function registerRuntimeRoutes(
|
|
273
95
|
router,
|
|
274
96
|
resolveCurrentAppConfig,
|
|
@@ -487,27 +309,227 @@ function registerRuntimeRoutes(
|
|
|
487
309
|
);
|
|
488
310
|
}
|
|
489
311
|
|
|
490
|
-
function
|
|
312
|
+
function registerConversationRoutes(
|
|
313
|
+
router,
|
|
314
|
+
resolveCurrentAppConfig,
|
|
315
|
+
{ requiresWorkspace = false, workspaceScopeSupport = null, conversationDataSchema = null,
|
|
316
|
+
conversationConfigurationSchema = null, conversationSelectionSchema = null, conversationReplacementSchema = null } = {}
|
|
317
|
+
) {
|
|
318
|
+
const routeBase = `${resolveAssistantApiBasePath({ requiresWorkspace })}/:surfaceId/conversations/:conversationId`;
|
|
319
|
+
const visibility = requiresWorkspace ? "workspace" : "public";
|
|
320
|
+
const surfaceParams = requiresWorkspace
|
|
321
|
+
? requireWorkspaceAssistantRouteParams(workspaceScopeSupport)
|
|
322
|
+
: assistantSurfaceRouteParamsValidator;
|
|
323
|
+
const params = composeSchemaDefinitions([surfaceParams, assistantConversationIdInputValidator], {
|
|
324
|
+
mode: "patch",
|
|
325
|
+
context: "assistant-runtime conversation route params"
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
router.register("GET", routeBase, {
|
|
329
|
+
auth: "required", visibility, params,
|
|
330
|
+
query: assistantConversationReadQueryValidator,
|
|
331
|
+
meta: { tags: ["assistant"], summary: "Read assistant conversation state." }
|
|
332
|
+
}, async function assistantConversationReadRoute(request, reply) {
|
|
333
|
+
const routeState = resolveRouteRequestState(request, {
|
|
334
|
+
resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
|
|
335
|
+
});
|
|
336
|
+
const input = { ...routeState.actionInput, conversationId: request.input.params.conversationId };
|
|
337
|
+
for (const key of ["beforeTurnId", "limit"]) {
|
|
338
|
+
if (Object.hasOwn(request.input.query || {}, key)) input[key] = request.input.query[key];
|
|
339
|
+
}
|
|
340
|
+
const response = await request.executeAction({
|
|
341
|
+
actionId: actionIds.conversationRead,
|
|
342
|
+
surface: routeState.hostSurfaceId,
|
|
343
|
+
input
|
|
344
|
+
});
|
|
345
|
+
reply.code(200).send(response);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
router.register("POST", `${routeBase}/messages`, {
|
|
349
|
+
auth: "required", visibility, params,
|
|
350
|
+
body: conversationSendBodyValidator(conversationDataSchema),
|
|
351
|
+
meta: { tags: ["assistant"], summary: "Send an assistant conversation message." }
|
|
352
|
+
}, async function assistantConversationSendRoute(request, reply) {
|
|
353
|
+
const routeState = resolveRouteRequestState(request, {
|
|
354
|
+
resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
|
|
355
|
+
});
|
|
356
|
+
const body = request.input.body;
|
|
357
|
+
const response = await request.executeAction({
|
|
358
|
+
actionId: actionIds.conversationSend,
|
|
359
|
+
surface: routeState.hostSurfaceId,
|
|
360
|
+
input: {
|
|
361
|
+
...routeState.actionInput,
|
|
362
|
+
conversationId: request.input.params.conversationId,
|
|
363
|
+
messageId: body.messageId,
|
|
364
|
+
text: body.text,
|
|
365
|
+
...(conversationDataSchema && Object.hasOwn(body, "data") ? { data: body.data } : {}),
|
|
366
|
+
...(Object.hasOwn(body, "attachmentIds") ? { attachmentIds: body.attachmentIds } : {}),
|
|
367
|
+
...(Object.hasOwn(body, "steer") ? { steer: body.steer } : {})
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
reply.code(202).send(response);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
router.register("POST", `${routeBase}/cancel`, {
|
|
374
|
+
auth: "required", visibility, params,
|
|
375
|
+
meta: { tags: ["assistant"], summary: "Cancel the active assistant conversation turn." }
|
|
376
|
+
}, async function assistantConversationCancelRoute(request, reply) {
|
|
377
|
+
const routeState = resolveRouteRequestState(request, {
|
|
378
|
+
resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
|
|
379
|
+
});
|
|
380
|
+
const response = await request.executeAction({
|
|
381
|
+
actionId: actionIds.conversationCancel,
|
|
382
|
+
surface: routeState.hostSurfaceId,
|
|
383
|
+
input: { ...routeState.actionInput, conversationId: request.input.params.conversationId }
|
|
384
|
+
});
|
|
385
|
+
reply.code(200).send(response);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
router.register("POST", `${routeBase}/deliveries/:messageId/inspect`, {
|
|
389
|
+
auth: "required", visibility,
|
|
390
|
+
params: composeSchemaDefinitions([params, assistantConversationMessageIdInputValidator], {
|
|
391
|
+
mode: "patch", context: "assistant-runtime conversation delivery inspection route params"
|
|
392
|
+
}),
|
|
393
|
+
meta: { tags: ["assistant"], summary: "Inspect assistant message delivery without resubmitting it." }
|
|
394
|
+
}, async function assistantConversationInspectDeliveryRoute(request, reply) {
|
|
395
|
+
const routeState = resolveRouteRequestState(request, {
|
|
396
|
+
resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
|
|
397
|
+
});
|
|
398
|
+
const response = await request.executeAction({
|
|
399
|
+
actionId: actionIds.conversationInspectDelivery,
|
|
400
|
+
surface: routeState.hostSurfaceId,
|
|
401
|
+
input: { ...routeState.actionInput, conversationId: request.input.params.conversationId,
|
|
402
|
+
messageId: request.input.params.messageId }
|
|
403
|
+
});
|
|
404
|
+
reply.code(200).send(response);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
for (const method of ["GET", "POST"]) {
|
|
408
|
+
router.register(method, `${routeBase}/goal`, {
|
|
409
|
+
auth: "required", visibility, params,
|
|
410
|
+
...(method === "POST" ? { body: assistantConversationGoalBodyValidator } : {}),
|
|
411
|
+
meta: { tags: ["assistant"], summary: method === "GET" ? "Read the current conversation goal." : "Update the conversation goal." }
|
|
412
|
+
}, async function assistantConversationGoalRoute(request, reply) {
|
|
413
|
+
const routeState = resolveRouteRequestState(request, {
|
|
414
|
+
resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
|
|
415
|
+
});
|
|
416
|
+
const input = { ...routeState.actionInput, conversationId: request.input.params.conversationId };
|
|
417
|
+
if (method === "POST") {
|
|
418
|
+
for (const key of ["action", "expectedSegmentId", "expectedGoalId", "messageId", "objective", "tokenBudget", "attachmentIds"]) {
|
|
419
|
+
if (Object.hasOwn(request.input.body, key)) input[key] = request.input.body[key];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const response = await request.executeAction({
|
|
423
|
+
actionId: method === "GET" ? actionIds.conversationGoalRead : actionIds.conversationGoalUpdate,
|
|
424
|
+
surface: routeState.hostSurfaceId, input
|
|
425
|
+
});
|
|
426
|
+
reply.code(response === null ? 204 : 200).send(response);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (conversationConfigurationSchema) {
|
|
431
|
+
router.register("PATCH", `${routeBase}/configuration`, {
|
|
432
|
+
auth: "required", visibility, params,
|
|
433
|
+
body: conversationConfigureBodyValidator(conversationConfigurationSchema),
|
|
434
|
+
meta: { tags: ["assistant"], summary: "Apply the application's declared conversation settings." }
|
|
435
|
+
}, async function assistantConversationConfigureRoute(request, reply) {
|
|
436
|
+
const routeState = resolveRouteRequestState(request, {
|
|
437
|
+
resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
|
|
438
|
+
});
|
|
439
|
+
const response = await request.executeAction({
|
|
440
|
+
actionId: actionIds.conversationConfigure,
|
|
441
|
+
surface: routeState.hostSurfaceId,
|
|
442
|
+
input: { ...routeState.actionInput, conversationId: request.input.params.conversationId,
|
|
443
|
+
configuration: request.input.body.configuration }
|
|
444
|
+
});
|
|
445
|
+
reply.code(200).send(response);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (conversationSelectionSchema) {
|
|
450
|
+
router.register("POST", `${routeBase}/selection`, {
|
|
451
|
+
auth: "required", visibility, params,
|
|
452
|
+
body: conversationSelectBodyValidator(conversationSelectionSchema),
|
|
453
|
+
meta: { tags: ["assistant"], summary: "Apply an authorized application conversation selection." }
|
|
454
|
+
}, async function assistantConversationSelectRoute(request, reply) {
|
|
455
|
+
const routeState = resolveRouteRequestState(request, {
|
|
456
|
+
resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
|
|
457
|
+
});
|
|
458
|
+
const response = await request.executeAction({
|
|
459
|
+
actionId: actionIds.conversationSelect,
|
|
460
|
+
surface: routeState.hostSurfaceId,
|
|
461
|
+
input: { ...routeState.actionInput, conversationId: request.input.params.conversationId,
|
|
462
|
+
selection: request.input.body.selection }
|
|
463
|
+
});
|
|
464
|
+
reply.code(200).send(response);
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (conversationReplacementSchema) {
|
|
469
|
+
router.register("POST", `${routeBase}/replacement`, {
|
|
470
|
+
auth: "required", visibility, params,
|
|
471
|
+
body: conversationReplaceBodyValidator(conversationReplacementSchema),
|
|
472
|
+
meta: { tags: ["assistant"], summary: "Apply an authorized application conversation replacement." }
|
|
473
|
+
}, async function assistantConversationReplaceRoute(request, reply) {
|
|
474
|
+
const routeState = resolveRouteRequestState(request, {
|
|
475
|
+
resolveCurrentAppConfig, requiresWorkspace, workspaceScopeSupport
|
|
476
|
+
});
|
|
477
|
+
const response = await request.executeAction({
|
|
478
|
+
actionId: actionIds.conversationReplace,
|
|
479
|
+
surface: routeState.hostSurfaceId,
|
|
480
|
+
input: { ...routeState.actionInput, conversationId: request.input.params.conversationId,
|
|
481
|
+
replacement: request.input.body.replacement }
|
|
482
|
+
});
|
|
483
|
+
reply.code(200).send(response);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function registerRoutes(router, {
|
|
489
|
+
config = {},
|
|
490
|
+
workspaceScopeSupport = null,
|
|
491
|
+
conversationRuntime = null,
|
|
492
|
+
conversationAccess = null,
|
|
493
|
+
conversationDataSchema = null,
|
|
494
|
+
conversationConfigurationSchema = null,
|
|
495
|
+
conversationSelectionSchema = null,
|
|
496
|
+
conversationReplacementSchema = null,
|
|
497
|
+
assistantConfigService = null
|
|
498
|
+
} = {}) {
|
|
491
499
|
if (!router || typeof router.register !== "function") {
|
|
492
500
|
throw new Error("registerRoutes requires an HTTP router.");
|
|
493
501
|
}
|
|
502
|
+
if (conversationRuntime && typeof conversationRuntime.open !== "function") {
|
|
503
|
+
throw new TypeError("Assistant conversation routes require conversationRuntime.open().");
|
|
504
|
+
}
|
|
494
505
|
const resolveCurrentAppConfig = () => config;
|
|
506
|
+
const registerAssistantRuntimeRoutes = conversationRuntime ? registerConversationRoutes : registerRuntimeRoutes;
|
|
507
|
+
const runtimeRouter = conversationRuntime && conversationAccess ? conversationAccess.router : router;
|
|
508
|
+
const includeSettings = !conversationRuntime || Boolean(assistantConfigService);
|
|
495
509
|
|
|
496
|
-
registerSettingsRoutes(router, resolveCurrentAppConfig, {
|
|
510
|
+
if (includeSettings) registerSettingsRoutes(router, resolveCurrentAppConfig, {
|
|
497
511
|
requiresWorkspace: false
|
|
498
512
|
});
|
|
499
|
-
|
|
500
|
-
requiresWorkspace: false
|
|
513
|
+
registerAssistantRuntimeRoutes(runtimeRouter, resolveCurrentAppConfig, {
|
|
514
|
+
requiresWorkspace: false,
|
|
515
|
+
conversationDataSchema,
|
|
516
|
+
conversationConfigurationSchema,
|
|
517
|
+
conversationSelectionSchema,
|
|
518
|
+
conversationReplacementSchema
|
|
501
519
|
});
|
|
502
520
|
|
|
503
521
|
if (workspaceScopeSupport) {
|
|
504
|
-
registerSettingsRoutes(router, resolveCurrentAppConfig, {
|
|
522
|
+
if (includeSettings) registerSettingsRoutes(router, resolveCurrentAppConfig, {
|
|
505
523
|
requiresWorkspace: true,
|
|
506
524
|
workspaceScopeSupport
|
|
507
525
|
});
|
|
508
|
-
|
|
526
|
+
registerAssistantRuntimeRoutes(runtimeRouter, resolveCurrentAppConfig, {
|
|
509
527
|
requiresWorkspace: true,
|
|
510
|
-
workspaceScopeSupport
|
|
528
|
+
workspaceScopeSupport,
|
|
529
|
+
conversationDataSchema,
|
|
530
|
+
conversationConfigurationSchema,
|
|
531
|
+
conversationSelectionSchema,
|
|
532
|
+
conversationReplacementSchema
|
|
511
533
|
});
|
|
512
534
|
}
|
|
513
535
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createJsonApiResourceRouteContract } from "@jskit-ai/http-runtime/shared/validators/jsonApiRouteTransport";
|
|
2
|
+
import {
|
|
3
|
+
ASSISTANT_SETTINGS_UPDATE_TRANSPORT,
|
|
4
|
+
ASSISTANT_SETTINGS_TRANSPORT,
|
|
5
|
+
assistantConfigResource,
|
|
6
|
+
resolveAssistantApiBasePath
|
|
7
|
+
} from "@jskit-ai/assistant-core/shared";
|
|
8
|
+
import { actionIds } from "./actionIds.js";
|
|
9
|
+
import { assistantSurfaceRouteParamsValidator } from "./inputSchemas.js";
|
|
10
|
+
import { requireWorkspaceAssistantRouteParams, resolveRouteRequestState } from "./support/assistantRouteContext.js";
|
|
11
|
+
|
|
12
|
+
function resolveAssistantSettingsRecordId(record = {}) {
|
|
13
|
+
const scopeKey = String(record?.scopeKey || "").trim();
|
|
14
|
+
if (!scopeKey) {
|
|
15
|
+
throw new Error("Assistant settings JSON:API response requires scopeKey.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return scopeKey;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function registerSettingsRoutes(
|
|
22
|
+
router,
|
|
23
|
+
resolveCurrentAppConfig,
|
|
24
|
+
{ requiresWorkspace = false, workspaceScopeSupport = null } = {}
|
|
25
|
+
) {
|
|
26
|
+
const routeBase = resolveAssistantApiBasePath({
|
|
27
|
+
requiresWorkspace
|
|
28
|
+
});
|
|
29
|
+
const visibility = requiresWorkspace ? "workspace" : "public";
|
|
30
|
+
const routePath = `${routeBase}/:surfaceId/settings`;
|
|
31
|
+
const params = requiresWorkspace === true
|
|
32
|
+
? requireWorkspaceAssistantRouteParams(workspaceScopeSupport)
|
|
33
|
+
: assistantSurfaceRouteParamsValidator;
|
|
34
|
+
|
|
35
|
+
router.register(
|
|
36
|
+
"GET",
|
|
37
|
+
routePath,
|
|
38
|
+
{
|
|
39
|
+
auth: "required",
|
|
40
|
+
visibility,
|
|
41
|
+
params,
|
|
42
|
+
meta: {
|
|
43
|
+
tags: ["assistant", "settings"],
|
|
44
|
+
summary: "Get assistant settings."
|
|
45
|
+
},
|
|
46
|
+
...createJsonApiResourceRouteContract({
|
|
47
|
+
...ASSISTANT_SETTINGS_TRANSPORT,
|
|
48
|
+
output: assistantConfigResource.operations.view.output,
|
|
49
|
+
outputKind: "record",
|
|
50
|
+
getRecordId: resolveAssistantSettingsRecordId
|
|
51
|
+
})
|
|
52
|
+
},
|
|
53
|
+
async function assistantSettingsReadRoute(request, reply) {
|
|
54
|
+
const routeState = resolveRouteRequestState(request, {
|
|
55
|
+
resolveCurrentAppConfig,
|
|
56
|
+
kind: "settings",
|
|
57
|
+
requiresWorkspace,
|
|
58
|
+
workspaceScopeSupport
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const response = await request.executeAction({
|
|
62
|
+
actionId: actionIds.settingsRead,
|
|
63
|
+
surface: routeState.hostSurfaceId,
|
|
64
|
+
input: routeState.actionInput
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
reply.code(200).send(response);
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
router.register(
|
|
72
|
+
"PATCH",
|
|
73
|
+
routePath,
|
|
74
|
+
{
|
|
75
|
+
auth: "required",
|
|
76
|
+
visibility,
|
|
77
|
+
params,
|
|
78
|
+
meta: {
|
|
79
|
+
tags: ["assistant", "settings"],
|
|
80
|
+
summary: "Update assistant settings."
|
|
81
|
+
},
|
|
82
|
+
...createJsonApiResourceRouteContract({
|
|
83
|
+
...ASSISTANT_SETTINGS_UPDATE_TRANSPORT,
|
|
84
|
+
body: assistantConfigResource.operations.patch.body,
|
|
85
|
+
output: assistantConfigResource.operations.patch.output,
|
|
86
|
+
outputKind: "record",
|
|
87
|
+
getRecordId: resolveAssistantSettingsRecordId,
|
|
88
|
+
includeValidation400: true
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
async function assistantSettingsPatchRoute(request, reply) {
|
|
92
|
+
const routeState = resolveRouteRequestState(request, {
|
|
93
|
+
resolveCurrentAppConfig,
|
|
94
|
+
kind: "settings",
|
|
95
|
+
requiresWorkspace,
|
|
96
|
+
workspaceScopeSupport
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const response = await request.executeAction({
|
|
100
|
+
actionId: actionIds.settingsUpdate,
|
|
101
|
+
surface: routeState.hostSurfaceId,
|
|
102
|
+
input: {
|
|
103
|
+
...routeState.actionInput,
|
|
104
|
+
patch: request.input.body
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
reply.code(200).send(response);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { registerSettingsRoutes };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createWithTransaction, normalizeDbRecordId, runInTransaction } from "@jskit-ai/database-runtime/shared/repositoryOptions";
|
|
1
|
+
import { applyForUpdate, createWithTransaction, normalizeDbRecordId, runInTransaction } from "@jskit-ai/database-runtime/shared/repositoryOptions";
|
|
2
2
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
3
|
import { normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
4
|
import {
|
|
@@ -88,9 +88,15 @@ function createRepository(knex) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const client = options?.trx || knex;
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
|
|
91
|
+
if (options.forUpdate === true) {
|
|
92
|
+
if (!options.trx) throw new TypeError("Locking a conversation requires its database transaction.");
|
|
93
|
+
// Acquire the write reservation before reading, including SQLite where
|
|
94
|
+
// SELECT FOR UPDATE is not supported. This does not change row contents.
|
|
95
|
+
await client(assistantRuntimeConfig.conversationsTable).where({ id: normalizedConversationId })
|
|
96
|
+
.update({ id: normalizedConversationId });
|
|
97
|
+
}
|
|
98
|
+
const query = createConversationBaseQuery(client).where("c.id", normalizedConversationId);
|
|
99
|
+
const row = await applyForUpdate(query, options).first();
|
|
94
100
|
|
|
95
101
|
return row ? mapConversationRow(row) : null;
|
|
96
102
|
}
|
|
@@ -108,12 +114,13 @@ function createRepository(knex) {
|
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
const client = options?.trx || knex;
|
|
117
|
+
if (options.forUpdate === true) await findById(normalizedConversationId, options);
|
|
111
118
|
const query = createConversationBaseQuery(client)
|
|
112
119
|
.where("c.id", normalizedConversationId)
|
|
113
120
|
.where("c.created_by_user_id", normalizedActorUserId)
|
|
114
121
|
.where("c.surface_id", normalizedSurfaceId);
|
|
115
122
|
applyWorkspaceScope(query, "c.workspace_id", workspaceId);
|
|
116
|
-
const row = await query.first();
|
|
123
|
+
const row = await applyForUpdate(query, options).first();
|
|
117
124
|
|
|
118
125
|
return row ? mapConversationRow(row) : null;
|
|
119
126
|
}
|
|
@@ -140,6 +140,17 @@ function createRepository(knex) {
|
|
|
140
140
|
return Number.isFinite(total) && total > 0 ? total : 0;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
async function updateById(messageId, patch = {}, options = {}) {
|
|
144
|
+
const id = normalizeInputRecordId(messageId);
|
|
145
|
+
if (!id) return null;
|
|
146
|
+
const client = options?.trx || knex;
|
|
147
|
+
const update = {};
|
|
148
|
+
if (Object.hasOwn(patch, "contentText")) update.content_text = patch.contentText == null ? null : String(patch.contentText);
|
|
149
|
+
if (Object.hasOwn(patch, "metadata")) update.metadata_json = stringifyJsonObject(patch.metadata);
|
|
150
|
+
if (Object.keys(update).length) await client(assistantRuntimeConfig.messagesTable).where({ id }).update(update);
|
|
151
|
+
return findById(id, { trx: client });
|
|
152
|
+
}
|
|
153
|
+
|
|
143
154
|
async function listByConversationScope(conversationId, { workspaceId = null } = {}, pagination = {}, options = {}) {
|
|
144
155
|
const normalizedConversationId = normalizeInputRecordId(conversationId);
|
|
145
156
|
if (!normalizedConversationId) {
|
|
@@ -171,6 +182,7 @@ function createRepository(knex) {
|
|
|
171
182
|
withTransaction,
|
|
172
183
|
findById,
|
|
173
184
|
create,
|
|
185
|
+
updateById,
|
|
174
186
|
countByConversationScope,
|
|
175
187
|
listByConversationScope,
|
|
176
188
|
transaction
|