@jskit-ai/assistant-runtime 0.1.192 → 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
package/src/server/actions.js
CHANGED
|
@@ -6,12 +6,23 @@ import {
|
|
|
6
6
|
deepFreeze
|
|
7
7
|
} from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
8
8
|
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
9
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime";
|
|
9
10
|
import {
|
|
10
|
-
assistantConfigResource,
|
|
11
11
|
assistantResource
|
|
12
12
|
} from "@jskit-ai/assistant-core/shared";
|
|
13
13
|
import { actionIds } from "./actionIds.js";
|
|
14
|
-
import {
|
|
14
|
+
import { createAssistantSettingsActions } from "./createAssistantSettingsActions.js";
|
|
15
|
+
import {
|
|
16
|
+
assistantConversationIdInputValidator,
|
|
17
|
+
assistantConversationGoalBodyValidator,
|
|
18
|
+
assistantConversationMessageIdInputValidator,
|
|
19
|
+
assistantConversationReadQueryValidator,
|
|
20
|
+
conversationConfigureBodyValidator,
|
|
21
|
+
conversationSelectBodyValidator,
|
|
22
|
+
conversationReplaceBodyValidator,
|
|
23
|
+
conversationSendBodyValidator,
|
|
24
|
+
assistantTargetSurfaceInputValidator
|
|
25
|
+
} from "./inputSchemas.js";
|
|
15
26
|
import {
|
|
16
27
|
resolveAssistantSurfaceConfig,
|
|
17
28
|
resolveAssistantSurfacesConfig
|
|
@@ -70,35 +81,266 @@ const runtimeChatStreamInputValidator = composeSchemaDefinitions(
|
|
|
70
81
|
}
|
|
71
82
|
);
|
|
72
83
|
|
|
73
|
-
const settingsReadInputValidator = assistantTargetSurfaceInputValidator;
|
|
74
|
-
|
|
75
|
-
const settingsUpdatePatchInputValidator = deepFreeze({
|
|
76
|
-
schema: createSchema({
|
|
77
|
-
patch: {
|
|
78
|
-
type: "object",
|
|
79
|
-
required: true,
|
|
80
|
-
schema: assistantConfigResource.operations.patch.body.schema
|
|
81
|
-
}
|
|
82
|
-
}),
|
|
83
|
-
mode: "patch"
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const settingsUpdateInputValidator = composeSchemaDefinitions(
|
|
87
|
-
[assistantTargetSurfaceInputValidator, settingsUpdatePatchInputValidator],
|
|
88
|
-
{
|
|
89
|
-
mode: "patch",
|
|
90
|
-
context: "assistant-runtime settings update action input"
|
|
91
|
-
}
|
|
92
|
-
);
|
|
93
|
-
|
|
94
84
|
function configuredAssistantSurfaces(config = {}) {
|
|
95
85
|
return Object.keys(resolveAssistantSurfacesConfig(config))
|
|
96
86
|
.map((surfaceId) => resolveAssistantSurfaceConfig(config, surfaceId))
|
|
97
87
|
.filter(Boolean);
|
|
98
88
|
}
|
|
99
89
|
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
const conversationInputValidator = composeSchemaDefinitions(
|
|
91
|
+
[assistantTargetSurfaceInputValidator, assistantConversationIdInputValidator],
|
|
92
|
+
{ mode: "patch", context: "assistant-runtime conversation action input" }
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const conversationInspectInputValidator = composeSchemaDefinitions(
|
|
96
|
+
[conversationInputValidator, assistantConversationMessageIdInputValidator],
|
|
97
|
+
{ mode: "patch", context: "assistant-runtime conversation delivery inspection input" }
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const conversationReadInputValidator = composeSchemaDefinitions(
|
|
101
|
+
[conversationInputValidator, assistantConversationReadQueryValidator],
|
|
102
|
+
{ mode: "patch", context: "assistant-runtime conversation read action input" }
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
function createConversationActions({ conversationRuntime, conversationDataSchema, conversationConfigurationSchema,
|
|
106
|
+
conversationSelectionSchema, conversationReplacementSchema, config, runtimeSurfaces, workspaceScopeSupport }) {
|
|
107
|
+
const conversationSendInputValidator = composeSchemaDefinitions(
|
|
108
|
+
[conversationInputValidator, conversationSendBodyValidator(conversationDataSchema)],
|
|
109
|
+
{ mode: "patch", context: "assistant-runtime conversation send action input" }
|
|
110
|
+
);
|
|
111
|
+
async function openConversation(input, context) {
|
|
112
|
+
const assistantSurface = resolveAssistantSurfaceConfig(config, input.targetSurfaceId);
|
|
113
|
+
if (!assistantSurface) throw new AppError(404, "Assistant not found.");
|
|
114
|
+
let conversationContext = context;
|
|
115
|
+
if (assistantSurface.runtimeSurfaceRequiresWorkspace) {
|
|
116
|
+
if (typeof workspaceScopeSupport?.resolveWorkspace !== "function") {
|
|
117
|
+
throw new Error("Assistant conversation actions require workspace server scope support.");
|
|
118
|
+
}
|
|
119
|
+
const workspace = workspaceScopeSupport.resolveWorkspace(context, { workspaceSlug: input.workspaceSlug });
|
|
120
|
+
if (!workspace?.id) throw new AppError(409, "Workspace selection required.");
|
|
121
|
+
conversationContext = { ...context, workspace };
|
|
122
|
+
}
|
|
123
|
+
return conversationRuntime.open({ id: input.conversationId, context: conversationContext });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return [
|
|
127
|
+
{
|
|
128
|
+
id: actionIds.conversationRead,
|
|
129
|
+
version: 1,
|
|
130
|
+
kind: "query",
|
|
131
|
+
channels: ["api", "internal"],
|
|
132
|
+
surfaces: runtimeSurfaces,
|
|
133
|
+
permission: { require: "authenticated" },
|
|
134
|
+
input: conversationReadInputValidator,
|
|
135
|
+
output: null,
|
|
136
|
+
idempotency: "none",
|
|
137
|
+
audit: { actionName: actionIds.conversationRead },
|
|
138
|
+
observability: {},
|
|
139
|
+
async execute(input, context) {
|
|
140
|
+
const conversation = await openConversation(input, context);
|
|
141
|
+
const options = {};
|
|
142
|
+
for (const key of ["beforeTurnId", "limit"]) {
|
|
143
|
+
if (Object.hasOwn(input, key)) options[key] = input[key];
|
|
144
|
+
}
|
|
145
|
+
return Object.keys(options).length ? conversation.read(options) : conversation.read();
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: actionIds.conversationSend,
|
|
150
|
+
version: 1,
|
|
151
|
+
kind: "command",
|
|
152
|
+
channels: ["api", "internal"],
|
|
153
|
+
surfaces: runtimeSurfaces,
|
|
154
|
+
permission: { require: "authenticated" },
|
|
155
|
+
input: conversationSendInputValidator,
|
|
156
|
+
output: null,
|
|
157
|
+
idempotency: "none",
|
|
158
|
+
audit: { actionName: actionIds.conversationSend },
|
|
159
|
+
observability: {},
|
|
160
|
+
async execute(input, context) {
|
|
161
|
+
const conversation = await openConversation(input, context);
|
|
162
|
+
return conversation.send({
|
|
163
|
+
messageId: input.messageId,
|
|
164
|
+
text: input.text,
|
|
165
|
+
...(conversationDataSchema && Object.hasOwn(input, "data") ? { data: input.data } : {}),
|
|
166
|
+
...(Object.hasOwn(input, "attachmentIds") ? { attachmentIds: input.attachmentIds } : {}),
|
|
167
|
+
...(Object.hasOwn(input, "steer") ? { steer: input.steer } : {})
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: actionIds.conversationCancel,
|
|
173
|
+
version: 1,
|
|
174
|
+
kind: "command",
|
|
175
|
+
channels: ["api", "internal"],
|
|
176
|
+
surfaces: runtimeSurfaces,
|
|
177
|
+
permission: { require: "authenticated" },
|
|
178
|
+
input: conversationInputValidator,
|
|
179
|
+
output: null,
|
|
180
|
+
idempotency: "none",
|
|
181
|
+
audit: { actionName: actionIds.conversationCancel },
|
|
182
|
+
observability: {},
|
|
183
|
+
async execute(input, context) {
|
|
184
|
+
const conversation = await openConversation(input, context);
|
|
185
|
+
return conversation.cancel();
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: actionIds.conversationSubscribe,
|
|
190
|
+
version: 1,
|
|
191
|
+
kind: "query",
|
|
192
|
+
channels: ["internal"],
|
|
193
|
+
surfaces: runtimeSurfaces,
|
|
194
|
+
permission: { require: "authenticated" },
|
|
195
|
+
input: conversationInputValidator,
|
|
196
|
+
output: null,
|
|
197
|
+
idempotency: "none",
|
|
198
|
+
audit: { actionName: actionIds.conversationSubscribe },
|
|
199
|
+
observability: {},
|
|
200
|
+
async execute(input, context, deps) {
|
|
201
|
+
if (typeof deps?.onEvent !== "function" || typeof deps?.onRelease !== "function") {
|
|
202
|
+
throw new TypeError("Conversation subscriptions require their server-owned event and cleanup callbacks.");
|
|
203
|
+
}
|
|
204
|
+
const conversation = await openConversation(input, context);
|
|
205
|
+
const release = await conversation.subscribe(deps.onEvent);
|
|
206
|
+
try {
|
|
207
|
+
deps.onRelease(release);
|
|
208
|
+
return await conversation.read();
|
|
209
|
+
} catch (error) {
|
|
210
|
+
release();
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: actionIds.conversationInspectDelivery,
|
|
217
|
+
version: 1,
|
|
218
|
+
kind: "command",
|
|
219
|
+
channels: ["api", "internal"],
|
|
220
|
+
surfaces: runtimeSurfaces,
|
|
221
|
+
permission: { require: "authenticated" },
|
|
222
|
+
input: conversationInspectInputValidator,
|
|
223
|
+
output: null,
|
|
224
|
+
idempotency: "none",
|
|
225
|
+
audit: { actionName: actionIds.conversationInspectDelivery },
|
|
226
|
+
observability: {},
|
|
227
|
+
async execute(input, context) {
|
|
228
|
+
const conversation = await openConversation(input, context);
|
|
229
|
+
return conversation.inspectDelivery({ messageId: input.messageId });
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: actionIds.conversationGoalRead,
|
|
234
|
+
version: 1,
|
|
235
|
+
kind: "query",
|
|
236
|
+
channels: ["api", "internal"],
|
|
237
|
+
surfaces: runtimeSurfaces,
|
|
238
|
+
permission: { require: "authenticated" },
|
|
239
|
+
input: conversationInputValidator,
|
|
240
|
+
output: null,
|
|
241
|
+
idempotency: "none",
|
|
242
|
+
audit: { actionName: actionIds.conversationGoalRead },
|
|
243
|
+
observability: {},
|
|
244
|
+
async execute(input, context) {
|
|
245
|
+
return (await openConversation(input, context)).readGoal();
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: actionIds.conversationGoalUpdate,
|
|
250
|
+
version: 1,
|
|
251
|
+
kind: "command",
|
|
252
|
+
channels: ["api", "internal"],
|
|
253
|
+
surfaces: runtimeSurfaces,
|
|
254
|
+
permission: { require: "authenticated" },
|
|
255
|
+
input: composeSchemaDefinitions([conversationInputValidator, assistantConversationGoalBodyValidator], {
|
|
256
|
+
mode: "replace", context: "assistant-runtime conversation goal action input"
|
|
257
|
+
}),
|
|
258
|
+
output: null,
|
|
259
|
+
idempotency: "none",
|
|
260
|
+
audit: { actionName: actionIds.conversationGoalUpdate },
|
|
261
|
+
observability: {},
|
|
262
|
+
async execute(input, context) {
|
|
263
|
+
const conversation = await openConversation(input, context);
|
|
264
|
+
const goal = { action: input.action, expectedSegmentId: input.expectedSegmentId };
|
|
265
|
+
for (const key of ["expectedGoalId", "messageId", "objective", "tokenBudget", "attachmentIds"]) {
|
|
266
|
+
if (Object.hasOwn(input, key)) goal[key] = input[key];
|
|
267
|
+
}
|
|
268
|
+
return conversation.updateGoal(goal);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
...(conversationConfigurationSchema ? [{
|
|
272
|
+
id: actionIds.conversationConfigure,
|
|
273
|
+
version: 1,
|
|
274
|
+
kind: "command",
|
|
275
|
+
channels: ["api", "internal"],
|
|
276
|
+
surfaces: runtimeSurfaces,
|
|
277
|
+
permission: { require: "authenticated" },
|
|
278
|
+
input: composeSchemaDefinitions([conversationInputValidator, conversationConfigureBodyValidator(conversationConfigurationSchema)], {
|
|
279
|
+
mode: "replace", context: "assistant-runtime declared conversation configuration"
|
|
280
|
+
}),
|
|
281
|
+
output: null,
|
|
282
|
+
idempotency: "none",
|
|
283
|
+
audit: { actionName: actionIds.conversationConfigure },
|
|
284
|
+
observability: {},
|
|
285
|
+
async execute(input, context) {
|
|
286
|
+
return (await openConversation(input, context)).configure(input.configuration);
|
|
287
|
+
}
|
|
288
|
+
}] : []),
|
|
289
|
+
...(conversationSelectionSchema ? [{
|
|
290
|
+
id: actionIds.conversationSelect,
|
|
291
|
+
version: 1,
|
|
292
|
+
kind: "command",
|
|
293
|
+
channels: ["api", "internal"],
|
|
294
|
+
surfaces: runtimeSurfaces,
|
|
295
|
+
permission: { require: "authenticated" },
|
|
296
|
+
input: composeSchemaDefinitions([conversationInputValidator, conversationSelectBodyValidator(conversationSelectionSchema)], {
|
|
297
|
+
mode: "replace", context: "assistant-runtime declared conversation selection"
|
|
298
|
+
}),
|
|
299
|
+
output: null,
|
|
300
|
+
idempotency: "none",
|
|
301
|
+
audit: { actionName: actionIds.conversationSelect },
|
|
302
|
+
observability: {},
|
|
303
|
+
async execute(input, context) {
|
|
304
|
+
return (await openConversation(input, context)).select(input.selection);
|
|
305
|
+
}
|
|
306
|
+
}] : []),
|
|
307
|
+
...(conversationReplacementSchema ? [{
|
|
308
|
+
id: actionIds.conversationReplace,
|
|
309
|
+
version: 1,
|
|
310
|
+
kind: "command",
|
|
311
|
+
channels: ["api", "internal"],
|
|
312
|
+
surfaces: runtimeSurfaces,
|
|
313
|
+
permission: { require: "authenticated" },
|
|
314
|
+
input: composeSchemaDefinitions([conversationInputValidator, conversationReplaceBodyValidator(conversationReplacementSchema)], {
|
|
315
|
+
mode: "replace", context: "assistant-runtime declared conversation replacement"
|
|
316
|
+
}),
|
|
317
|
+
output: null,
|
|
318
|
+
idempotency: "none",
|
|
319
|
+
audit: { actionName: actionIds.conversationReplace },
|
|
320
|
+
observability: {},
|
|
321
|
+
async execute(input, context) {
|
|
322
|
+
return (await openConversation(input, context)).replace(input.replacement);
|
|
323
|
+
}
|
|
324
|
+
}] : [])
|
|
325
|
+
];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function createAssistantActions({
|
|
329
|
+
assistantConfigService,
|
|
330
|
+
chatService,
|
|
331
|
+
conversationRuntime = null,
|
|
332
|
+
conversationAccess = null,
|
|
333
|
+
conversationDataSchema = null,
|
|
334
|
+
conversationConfigurationSchema = null,
|
|
335
|
+
conversationSelectionSchema = null,
|
|
336
|
+
conversationReplacementSchema = null,
|
|
337
|
+
workspaceScopeSupport = null,
|
|
338
|
+
config = {}
|
|
339
|
+
} = {}) {
|
|
340
|
+
if (conversationRuntime && typeof conversationRuntime.open !== "function") {
|
|
341
|
+
throw new TypeError("Assistant conversation actions require conversationRuntime.open().");
|
|
342
|
+
}
|
|
343
|
+
if (!conversationRuntime && (!assistantConfigService || !chatService)) {
|
|
102
344
|
throw new TypeError("createAssistantActions requires assistantConfigService and chatService.");
|
|
103
345
|
}
|
|
104
346
|
const configuredSurfaces = configuredAssistantSurfaces(config);
|
|
@@ -106,6 +348,25 @@ function createAssistantActions({ assistantConfigService, chatService, config =
|
|
|
106
348
|
const settingsSurfaces = [...new Set(configuredSurfaces.map((entry) => entry.settingsSurfaceId))];
|
|
107
349
|
if (runtimeSurfaces.length === 0 || settingsSurfaces.length === 0) return [];
|
|
108
350
|
|
|
351
|
+
if (conversationRuntime) {
|
|
352
|
+
const conversationActions = createConversationActions({
|
|
353
|
+
conversationRuntime, conversationDataSchema, conversationConfigurationSchema, conversationSelectionSchema, conversationReplacementSchema,
|
|
354
|
+
config, runtimeSurfaces, workspaceScopeSupport
|
|
355
|
+
});
|
|
356
|
+
return Object.freeze([
|
|
357
|
+
...conversationActions.map((definition) => {
|
|
358
|
+
if (!conversationAccess) return definition;
|
|
359
|
+
const selected = definition.id === actionIds.conversationSubscribe ? {
|
|
360
|
+
...definition,
|
|
361
|
+
id: conversationAccess.subscribeActionId,
|
|
362
|
+
audit: { ...definition.audit, actionName: conversationAccess.subscribeActionId }
|
|
363
|
+
} : definition;
|
|
364
|
+
return conversationAccess.wrapAction(selected);
|
|
365
|
+
}),
|
|
366
|
+
...(assistantConfigService ? createAssistantSettingsActions({ assistantConfigService, settingsSurfaces }) : [])
|
|
367
|
+
]);
|
|
368
|
+
}
|
|
369
|
+
|
|
109
370
|
return Object.freeze([
|
|
110
371
|
{
|
|
111
372
|
id: actionIds.chatStream,
|
|
@@ -176,60 +437,7 @@ function createAssistantActions({ assistantConfigService, chatService, config =
|
|
|
176
437
|
}));
|
|
177
438
|
}
|
|
178
439
|
},
|
|
179
|
-
{
|
|
180
|
-
id: actionIds.settingsRead,
|
|
181
|
-
version: 1,
|
|
182
|
-
kind: "query",
|
|
183
|
-
channels: ["api", "automation", "internal"],
|
|
184
|
-
surfaces: settingsSurfaces,
|
|
185
|
-
permission: {
|
|
186
|
-
require: "authenticated"
|
|
187
|
-
},
|
|
188
|
-
input: settingsReadInputValidator,
|
|
189
|
-
output: null,
|
|
190
|
-
extensions: {
|
|
191
|
-
assistant: {
|
|
192
|
-
exclude: "Assistant self-configuration is disabled; use the authenticated assistant settings screen."
|
|
193
|
-
}
|
|
194
|
-
},
|
|
195
|
-
idempotency: "none",
|
|
196
|
-
audit: {
|
|
197
|
-
actionName: actionIds.settingsRead
|
|
198
|
-
},
|
|
199
|
-
observability: {},
|
|
200
|
-
async execute(input, context) {
|
|
201
|
-
return returnJsonApiData(await assistantConfigService.getSettings(input, {
|
|
202
|
-
context
|
|
203
|
-
}));
|
|
204
|
-
}
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
id: actionIds.settingsUpdate,
|
|
208
|
-
version: 1,
|
|
209
|
-
kind: "command",
|
|
210
|
-
channels: ["api", "automation", "internal"],
|
|
211
|
-
surfaces: settingsSurfaces,
|
|
212
|
-
permission: {
|
|
213
|
-
require: "authenticated"
|
|
214
|
-
},
|
|
215
|
-
input: settingsUpdateInputValidator,
|
|
216
|
-
output: null,
|
|
217
|
-
extensions: {
|
|
218
|
-
assistant: {
|
|
219
|
-
exclude: "Assistant self-configuration is disabled; use the authenticated assistant settings screen."
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
idempotency: "optional",
|
|
223
|
-
audit: {
|
|
224
|
-
actionName: actionIds.settingsUpdate
|
|
225
|
-
},
|
|
226
|
-
observability: {},
|
|
227
|
-
async execute(input, context) {
|
|
228
|
-
return returnJsonApiData(await assistantConfigService.updateSettings(input, input.patch, {
|
|
229
|
-
context
|
|
230
|
-
}));
|
|
231
|
-
}
|
|
232
|
-
}
|
|
440
|
+
...createAssistantSettingsActions({ assistantConfigService, settingsSurfaces })
|
|
233
441
|
]);
|
|
234
442
|
}
|
|
235
443
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime";
|
|
4
|
+
import { createAiClient, createAiConnectionClient } from "@jskit-ai/assistant-core/server";
|
|
5
|
+
import { createConversationRuntime } from "@jskit-ai/assistant-core/server/conversation";
|
|
6
|
+
import { createSqlConversationStorage } from "./sqlConversationStorage.js";
|
|
7
|
+
import { createService as createAssistantConfigService } from "./services/assistantConfigService.js";
|
|
8
|
+
import { createChatService } from "./services/chatService.js";
|
|
9
|
+
import { createTranscriptService } from "./services/transcriptService.js";
|
|
10
|
+
import { resolveAssistantAiConfig, resolveAssistantServerConfig } from "./support/assistantServerConfig.js";
|
|
11
|
+
import { createSurfaceAwareToolCatalog } from "./support/createSurfaceAwareToolCatalog.js";
|
|
12
|
+
|
|
13
|
+
function createAssistantAiClientFactory(config = {}) {
|
|
14
|
+
const appConfig = normalizeObject(config.appConfig);
|
|
15
|
+
const env = normalizeObject(config.env);
|
|
16
|
+
const cache = new Map();
|
|
17
|
+
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
async resolveClient(targetSurfaceId = "", { context, integrationId } = {}) {
|
|
20
|
+
const normalizedTargetSurfaceId = normalizeText(targetSurfaceId).toLowerCase();
|
|
21
|
+
if (!normalizedTargetSurfaceId) {
|
|
22
|
+
throw new Error("assistant.ai.client.factory.resolveClient requires targetSurfaceId.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const surfaceConfig = resolveAssistantServerConfig(appConfig, normalizedTargetSurfaceId);
|
|
26
|
+
if (surfaceConfig.aiIntegrationId) {
|
|
27
|
+
const selected = integrationId || surfaceConfig.aiIntegrationId;
|
|
28
|
+
if (![surfaceConfig.aiIntegrationId, ...surfaceConfig.aiIntegrationIds].includes(selected)) {
|
|
29
|
+
throw new AppError(403, "This AI connection is not available in this assistant.");
|
|
30
|
+
}
|
|
31
|
+
if (!config.aiConnections?.resolve) throw new Error("The assistant requires an authorized AI connection resolver.");
|
|
32
|
+
const connection = await config.aiConnections.resolve({ context, integrationId: selected });
|
|
33
|
+
return createAiConnectionClient(connection, { timeoutMs: surfaceConfig.timeoutMs });
|
|
34
|
+
}
|
|
35
|
+
if (integrationId) throw new AppError(403, "This assistant does not allow selecting an AI connection.");
|
|
36
|
+
|
|
37
|
+
if (cache.has(normalizedTargetSurfaceId)) {
|
|
38
|
+
return cache.get(normalizedTargetSurfaceId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const assistantAiConfig = resolveAssistantAiConfig(
|
|
42
|
+
{
|
|
43
|
+
appConfig,
|
|
44
|
+
env
|
|
45
|
+
},
|
|
46
|
+
normalizedTargetSurfaceId
|
|
47
|
+
);
|
|
48
|
+
const client = createAiClient(assistantAiConfig.ai);
|
|
49
|
+
cache.set(normalizedTargetSurfaceId, client);
|
|
50
|
+
return client;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createAssistantRuntime({
|
|
56
|
+
actionCatalogue,
|
|
57
|
+
config,
|
|
58
|
+
consoleRuntime,
|
|
59
|
+
repositories = {},
|
|
60
|
+
env,
|
|
61
|
+
aiConnections,
|
|
62
|
+
attachments,
|
|
63
|
+
workspaces
|
|
64
|
+
} = {}) {
|
|
65
|
+
const {
|
|
66
|
+
config: assistantConfigRepository,
|
|
67
|
+
conversations: conversationsRepository,
|
|
68
|
+
messages: messagesRepository,
|
|
69
|
+
turnRequests
|
|
70
|
+
} = repositories;
|
|
71
|
+
const workspaceScopeSupport = workspaces?.scope || null;
|
|
72
|
+
const aiClientFactory = createAssistantAiClientFactory({ appConfig: config, env, aiConnections });
|
|
73
|
+
const toolCatalog = createSurfaceAwareToolCatalog(actionCatalogue, { appConfig: config });
|
|
74
|
+
const configService = createAssistantConfigService({
|
|
75
|
+
assistantConfigRepository,
|
|
76
|
+
consoleService: consoleRuntime?.services?.access || null,
|
|
77
|
+
appConfig: config,
|
|
78
|
+
workspaceScopeSupport
|
|
79
|
+
});
|
|
80
|
+
const transcriptService = createTranscriptService({ conversationsRepository, messagesRepository });
|
|
81
|
+
const conversationRuntime = createAssistantConversationRuntime({
|
|
82
|
+
conversationsRepository, messagesRepository, aiClientFactory, toolCatalog, attachments
|
|
83
|
+
});
|
|
84
|
+
const chatService = createChatService({
|
|
85
|
+
conversationRuntime,
|
|
86
|
+
turnRequests,
|
|
87
|
+
attachments,
|
|
88
|
+
aiClientFactory,
|
|
89
|
+
transcriptService,
|
|
90
|
+
serviceToolCatalog: toolCatalog,
|
|
91
|
+
assistantConfigService: configService,
|
|
92
|
+
appConfig: config,
|
|
93
|
+
workspaceScopeSupport
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
repositories: Object.freeze({
|
|
98
|
+
config: assistantConfigRepository,
|
|
99
|
+
conversations: conversationsRepository,
|
|
100
|
+
messages: messagesRepository,
|
|
101
|
+
turnRequests
|
|
102
|
+
}),
|
|
103
|
+
services: Object.freeze({ chat: chatService, config: configService, transcript: transcriptService }),
|
|
104
|
+
aiClientFactory,
|
|
105
|
+
toolCatalog
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Internal SQL composition: the existing facade supplies the authenticated action
|
|
110
|
+
// context and normalized request history, never another browser context channel.
|
|
111
|
+
function createAssistantConversationRuntime({ conversationsRepository, messagesRepository, aiClientFactory, toolCatalog, attachments }) {
|
|
112
|
+
return createConversationRuntime({
|
|
113
|
+
storage: createSqlConversationStorage({ conversationsRepository, messagesRepository }),
|
|
114
|
+
async authorize({ context, conversationId }) {
|
|
115
|
+
const request = context?.assistantRequest;
|
|
116
|
+
if (!request || request.conversation.id !== conversationId) return false;
|
|
117
|
+
return Boolean(await conversationsRepository.findByIdForActorScope(conversationId, {
|
|
118
|
+
actorUserId: normalizeRecordId(context.actor?.id, { fallback: null }), surfaceId: context.surface,
|
|
119
|
+
workspaceId: normalizeRecordId(request.workspace?.id || request.workspace, { fallback: null })
|
|
120
|
+
}));
|
|
121
|
+
},
|
|
122
|
+
apiClientFactory: { preserveWhitespace: false, resolveClient: ({ context, configuration }) => aiClientFactory.resolveClient(context.surface,
|
|
123
|
+
{ context, integrationId: configuration.integrationId }) },
|
|
124
|
+
apiHistory: ({ context }) => context.assistantRequest.history,
|
|
125
|
+
toolCatalog,
|
|
126
|
+
...(attachments ? { attachments: {
|
|
127
|
+
resolve({ attachmentIds, context }) {
|
|
128
|
+
const prepared = context.assistantRequest.resolvedAttachments.shift();
|
|
129
|
+
if (!prepared || !isDeepStrictEqual(prepared.attachmentIds, attachmentIds)) {
|
|
130
|
+
throw new Error("The API attachment request differs from its authorized request context.");
|
|
131
|
+
}
|
|
132
|
+
return prepared.files;
|
|
133
|
+
}
|
|
134
|
+
} } : {})
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { createAssistantAiClientFactory, createAssistantRuntime, createAssistantConversationRuntime };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createSchema } from "json-rest-schema";
|
|
2
|
+
import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
|
|
3
|
+
import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
|
|
4
|
+
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared";
|
|
5
|
+
import { assistantConfigResource } from "@jskit-ai/assistant-core/shared";
|
|
6
|
+
import { actionIds } from "./actionIds.js";
|
|
7
|
+
import { assistantTargetSurfaceInputValidator } from "./inputSchemas.js";
|
|
8
|
+
|
|
9
|
+
const settingsReadInputValidator = assistantTargetSurfaceInputValidator;
|
|
10
|
+
|
|
11
|
+
const settingsUpdatePatchInputValidator = deepFreeze({
|
|
12
|
+
schema: createSchema({
|
|
13
|
+
patch: {
|
|
14
|
+
type: "object",
|
|
15
|
+
required: true,
|
|
16
|
+
schema: assistantConfigResource.operations.patch.body.schema
|
|
17
|
+
}
|
|
18
|
+
}),
|
|
19
|
+
mode: "patch"
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const settingsUpdateInputValidator = composeSchemaDefinitions(
|
|
23
|
+
[assistantTargetSurfaceInputValidator, settingsUpdatePatchInputValidator],
|
|
24
|
+
{
|
|
25
|
+
mode: "patch",
|
|
26
|
+
context: "assistant-runtime settings update action input"
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
function createAssistantSettingsActions({ assistantConfigService, settingsSurfaces }) {
|
|
31
|
+
return Object.freeze([
|
|
32
|
+
{
|
|
33
|
+
id: actionIds.settingsRead,
|
|
34
|
+
version: 1,
|
|
35
|
+
kind: "query",
|
|
36
|
+
channels: ["api", "automation", "internal"],
|
|
37
|
+
surfaces: settingsSurfaces,
|
|
38
|
+
permission: {
|
|
39
|
+
require: "authenticated"
|
|
40
|
+
},
|
|
41
|
+
input: settingsReadInputValidator,
|
|
42
|
+
output: null,
|
|
43
|
+
extensions: {
|
|
44
|
+
assistant: {
|
|
45
|
+
exclude: "Assistant self-configuration is disabled; use the authenticated assistant settings screen."
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
idempotency: "none",
|
|
49
|
+
audit: {
|
|
50
|
+
actionName: actionIds.settingsRead
|
|
51
|
+
},
|
|
52
|
+
observability: {},
|
|
53
|
+
async execute(input, context) {
|
|
54
|
+
return returnJsonApiData(await assistantConfigService.getSettings(input, {
|
|
55
|
+
context
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: actionIds.settingsUpdate,
|
|
61
|
+
version: 1,
|
|
62
|
+
kind: "command",
|
|
63
|
+
channels: ["api", "automation", "internal"],
|
|
64
|
+
surfaces: settingsSurfaces,
|
|
65
|
+
permission: {
|
|
66
|
+
require: "authenticated"
|
|
67
|
+
},
|
|
68
|
+
input: settingsUpdateInputValidator,
|
|
69
|
+
output: null,
|
|
70
|
+
extensions: {
|
|
71
|
+
assistant: {
|
|
72
|
+
exclude: "Assistant self-configuration is disabled; use the authenticated assistant settings screen."
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
idempotency: "optional",
|
|
76
|
+
audit: {
|
|
77
|
+
actionName: actionIds.settingsUpdate
|
|
78
|
+
},
|
|
79
|
+
observability: {},
|
|
80
|
+
async execute(input, context) {
|
|
81
|
+
return returnJsonApiData(await assistantConfigService.updateSettings(input, input.patch, {
|
|
82
|
+
context
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { createAssistantSettingsActions };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { AssistantFeature } from "./AssistantProvider.js";
|
|
2
|
+
export { createAssistantActions } from "./actions.js";
|
|
3
|
+
export { assistantConversationGoalBodyValidator } from "./inputSchemas.js";
|
|
4
|
+
export { registerRoutes as registerAssistantRoutes } from "./registerRoutes.js";
|
|
5
|
+
export { registerConversationSubscriptions } from "./registerConversationSubscriptions.js";
|
|
6
|
+
export { registerFastifyConversations } from "./registerFastifyConversations.js";
|