@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.
Files changed (57) hide show
  1. package/package.descriptor.mjs +284 -0
  2. package/package.json +31 -0
  3. package/src/client/components/AssistantClientElement.vue +1316 -0
  4. package/src/client/components/AssistantConsoleSettingsClientElement.vue +71 -0
  5. package/src/client/components/AssistantSettingsFormCard.vue +76 -0
  6. package/src/client/components/AssistantWorkspaceClientElement.vue +15 -0
  7. package/src/client/components/AssistantWorkspaceSettingsClientElement.vue +73 -0
  8. package/src/client/composables/useAssistantWorkspaceRuntime.js +789 -0
  9. package/src/client/index.js +12 -0
  10. package/src/client/lib/assistantApi.js +137 -0
  11. package/src/client/lib/assistantHttpClient.js +10 -0
  12. package/src/client/lib/markdownRenderer.js +31 -0
  13. package/src/client/providers/AssistantWebClientProvider.js +25 -0
  14. package/src/server/AssistantServiceProvider.js +179 -0
  15. package/src/server/actionIds.js +11 -0
  16. package/src/server/actions.js +191 -0
  17. package/src/server/diTokens.js +19 -0
  18. package/src/server/lib/aiClient.js +43 -0
  19. package/src/server/lib/ndjson.js +47 -0
  20. package/src/server/lib/providers/anthropicClient.js +375 -0
  21. package/src/server/lib/providers/common.js +158 -0
  22. package/src/server/lib/providers/deepSeekClient.js +22 -0
  23. package/src/server/lib/providers/openAiClient.js +13 -0
  24. package/src/server/lib/providers/openAiCompatibleClient.js +69 -0
  25. package/src/server/lib/resolveWorkspaceSlug.js +24 -0
  26. package/src/server/lib/serviceToolCatalog.js +459 -0
  27. package/src/server/registerRoutes.js +384 -0
  28. package/src/server/repositories/assistantSettingsRepository.js +100 -0
  29. package/src/server/repositories/conversationsRepository.js +244 -0
  30. package/src/server/repositories/messagesRepository.js +154 -0
  31. package/src/server/repositories/repositoryPersistenceUtils.js +63 -0
  32. package/src/server/services/assistantSettingsService.js +153 -0
  33. package/src/server/services/chatService.js +987 -0
  34. package/src/server/services/transcriptService.js +334 -0
  35. package/src/shared/assistantPaths.js +50 -0
  36. package/src/shared/assistantResource.js +323 -0
  37. package/src/shared/assistantSettingsResource.js +214 -0
  38. package/src/shared/index.js +39 -0
  39. package/src/shared/queryKeys.js +69 -0
  40. package/src/shared/settingsEvents.js +7 -0
  41. package/src/shared/streamEvents.js +31 -0
  42. package/src/shared/support/positiveInteger.js +9 -0
  43. package/templates/migrations/assistant_settings_initial.cjs +39 -0
  44. package/templates/migrations/assistant_transcripts_initial.cjs +51 -0
  45. package/templates/src/pages/admin/workspace/assistant/index.vue +7 -0
  46. package/test/aiConfigValidation.test.js +15 -0
  47. package/test/assistantApiSurfaceHeader.test.js +64 -0
  48. package/test/assistantResource.test.js +53 -0
  49. package/test/assistantSettingsResource.test.js +48 -0
  50. package/test/assistantSettingsService.test.js +133 -0
  51. package/test/chatService.test.js +841 -0
  52. package/test/descriptorSurfaceOption.test.js +35 -0
  53. package/test/queryKeys.test.js +41 -0
  54. package/test/resolveWorkspaceSlug.test.js +83 -0
  55. package/test/routeInputContracts.test.js +287 -0
  56. package/test/serviceToolCatalog.test.js +1235 -0
  57. package/test/transcriptService.test.js +175 -0
@@ -0,0 +1,384 @@
1
+ import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
2
+ import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
3
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
4
+ import {
5
+ workspaceSlugParamsValidator
6
+ } from "@jskit-ai/users-core/server/validators/routeParamsValidator";
7
+ import {
8
+ resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig,
9
+ resolveWorkspaceSurfaceIdsFromAppConfig
10
+ } from "@jskit-ai/users-core/server/support/workspaceActionSurfaces";
11
+ import { resolveAssistantApiBasePath } from "../shared/assistantPaths.js";
12
+ import { assistantResource } from "../shared/assistantResource.js";
13
+ import {
14
+ assistantConsoleSettingsResource,
15
+ assistantWorkspaceSettingsResource
16
+ } from "../shared/assistantSettingsResource.js";
17
+ import { actionIds } from "./actionIds.js";
18
+ import { endNdjson, mapStreamError, setNdjsonHeaders, writeNdjson } from "./lib/ndjson.js";
19
+
20
+ function resolveAssistantWorkspaceRouteSurfaceConfig(app) {
21
+ const appConfig = typeof app?.has === "function" && app.has("appConfig") ? app.make("appConfig") : {};
22
+ const workspaceSurfaceIds = resolveWorkspaceSurfaceIdsFromAppConfig(appConfig);
23
+ const fallbackSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig);
24
+ return Object.freeze({
25
+ fallbackSurfaceId,
26
+ allowedSurfaceIds: new Set(workspaceSurfaceIds)
27
+ });
28
+ }
29
+
30
+ function resolveAssistantWorkspaceRequestSurfaceId(request, workspaceRouteSurfaceConfig = {}) {
31
+ const headerValue = request?.headers?.["x-jskit-surface"];
32
+ const headerCandidate = Array.isArray(headerValue) ? headerValue[0] : headerValue;
33
+ const requestedSurfaceId = normalizeText(headerCandidate).toLowerCase();
34
+ const fallbackSurfaceId = normalizeText(workspaceRouteSurfaceConfig?.fallbackSurfaceId).toLowerCase() || "app";
35
+ const allowedSurfaceIds = workspaceRouteSurfaceConfig?.allowedSurfaceIds instanceof Set
36
+ ? workspaceRouteSurfaceConfig.allowedSurfaceIds
37
+ : new Set();
38
+
39
+ if (!requestedSurfaceId) {
40
+ return fallbackSurfaceId;
41
+ }
42
+ if (allowedSurfaceIds.size > 0 && !allowedSurfaceIds.has(requestedSurfaceId)) {
43
+ return fallbackSurfaceId;
44
+ }
45
+ return requestedSurfaceId;
46
+ }
47
+
48
+ function registerRoutes(app) {
49
+ if (!app || typeof app.make !== "function") {
50
+ throw new Error("registerRoutes requires application make().");
51
+ }
52
+
53
+ const router = app.make(KERNEL_TOKENS.HttpRouter);
54
+ const visibility = "workspace";
55
+ const workspaceRouteSurfaceConfig = resolveAssistantWorkspaceRouteSurfaceConfig(app);
56
+ const workspaceRouteSurfaceId = workspaceRouteSurfaceConfig.fallbackSurfaceId;
57
+ const routeBase = resolveAssistantApiBasePath({
58
+ visibility
59
+ });
60
+
61
+ router.register(
62
+ "GET",
63
+ "/api/console/settings/assistant",
64
+ {
65
+ auth: "required",
66
+ surface: "console",
67
+ meta: {
68
+ tags: ["assistant", "settings"],
69
+ summary: "Get assistant console settings."
70
+ },
71
+ responseValidators: withStandardErrorResponses({
72
+ 200: assistantConsoleSettingsResource.operations.view.outputValidator
73
+ })
74
+ },
75
+ async function assistantConsoleSettingsReadRoute(request, reply) {
76
+ const response = await request.executeAction({
77
+ actionId: actionIds.consoleSettingsRead
78
+ });
79
+
80
+ reply.code(200).send(response);
81
+ }
82
+ );
83
+
84
+ router.register(
85
+ "PATCH",
86
+ "/api/console/settings/assistant",
87
+ {
88
+ auth: "required",
89
+ surface: "console",
90
+ meta: {
91
+ tags: ["assistant", "settings"],
92
+ summary: "Update assistant console settings."
93
+ },
94
+ bodyValidator: assistantConsoleSettingsResource.operations.patch.bodyValidator,
95
+ responseValidators: withStandardErrorResponses(
96
+ {
97
+ 200: assistantConsoleSettingsResource.operations.patch.outputValidator
98
+ },
99
+ {
100
+ includeValidation400: true
101
+ }
102
+ )
103
+ },
104
+ async function assistantConsoleSettingsPatchRoute(request, reply) {
105
+ const response = await request.executeAction({
106
+ actionId: actionIds.consoleSettingsUpdate,
107
+ input: {
108
+ payload: request.input.body
109
+ }
110
+ });
111
+
112
+ reply.code(200).send(response);
113
+ }
114
+ );
115
+
116
+ router.register(
117
+ "GET",
118
+ "/api/w/:workspaceSlug/workspace/settings/assistant",
119
+ {
120
+ auth: "required",
121
+ surface: workspaceRouteSurfaceId,
122
+ visibility,
123
+ meta: {
124
+ tags: ["assistant", "settings"],
125
+ summary: "Get assistant workspace settings."
126
+ },
127
+ paramsValidator: workspaceSlugParamsValidator,
128
+ responseValidators: withStandardErrorResponses({
129
+ 200: assistantWorkspaceSettingsResource.operations.view.outputValidator
130
+ })
131
+ },
132
+ async function assistantWorkspaceSettingsReadRoute(request, reply) {
133
+ const response = await request.executeAction({
134
+ actionId: actionIds.workspaceSettingsRead,
135
+ context: {
136
+ surface: resolveAssistantWorkspaceRequestSurfaceId(request, workspaceRouteSurfaceConfig)
137
+ },
138
+ input: {
139
+ workspaceSlug: request.input.params.workspaceSlug
140
+ }
141
+ });
142
+
143
+ reply.code(200).send(response);
144
+ }
145
+ );
146
+
147
+ router.register(
148
+ "PATCH",
149
+ "/api/w/:workspaceSlug/workspace/settings/assistant",
150
+ {
151
+ auth: "required",
152
+ surface: workspaceRouteSurfaceId,
153
+ visibility,
154
+ meta: {
155
+ tags: ["assistant", "settings"],
156
+ summary: "Update assistant workspace settings."
157
+ },
158
+ paramsValidator: workspaceSlugParamsValidator,
159
+ bodyValidator: assistantWorkspaceSettingsResource.operations.patch.bodyValidator,
160
+ responseValidators: withStandardErrorResponses(
161
+ {
162
+ 200: assistantWorkspaceSettingsResource.operations.patch.outputValidator
163
+ },
164
+ {
165
+ includeValidation400: true
166
+ }
167
+ )
168
+ },
169
+ async function assistantWorkspaceSettingsPatchRoute(request, reply) {
170
+ const response = await request.executeAction({
171
+ actionId: actionIds.workspaceSettingsUpdate,
172
+ context: {
173
+ surface: resolveAssistantWorkspaceRequestSurfaceId(request, workspaceRouteSurfaceConfig)
174
+ },
175
+ input: {
176
+ workspaceSlug: request.input.params.workspaceSlug,
177
+ patch: request.input.body
178
+ }
179
+ });
180
+
181
+ reply.code(200).send(response);
182
+ }
183
+ );
184
+
185
+ router.register(
186
+ "POST",
187
+ `${routeBase}/chat/stream`,
188
+ {
189
+ auth: "required",
190
+ surface: workspaceRouteSurfaceId,
191
+ visibility,
192
+ meta: {
193
+ tags: ["assistant"],
194
+ summary: "Stream assistant response for workspace user."
195
+ },
196
+ paramsValidator: workspaceSlugParamsValidator,
197
+ bodyValidator: assistantResource.operations.chatStream.bodyValidator
198
+ },
199
+ async function assistantChatStreamRoute(request, reply) {
200
+ const abortController = new AbortController();
201
+ const closeListener = () => {
202
+ abortController.abort();
203
+ };
204
+
205
+ let streamStarted = false;
206
+
207
+ function ensureStreamStarted() {
208
+ if (streamStarted) {
209
+ return;
210
+ }
211
+
212
+ setNdjsonHeaders(reply);
213
+ reply.code(200);
214
+ reply.hijack();
215
+ if (typeof reply.raw.flushHeaders === "function") {
216
+ reply.raw.flushHeaders();
217
+ }
218
+ streamStarted = true;
219
+ }
220
+
221
+ const streamWriter = Object.freeze({
222
+ sendMeta(payload = {}) {
223
+ ensureStreamStarted();
224
+ writeNdjson(reply, payload);
225
+ },
226
+ sendAssistantDelta(payload = {}) {
227
+ ensureStreamStarted();
228
+ writeNdjson(reply, payload);
229
+ },
230
+ sendAssistantMessage(payload = {}) {
231
+ ensureStreamStarted();
232
+ writeNdjson(reply, payload);
233
+ },
234
+ sendToolCall(payload = {}) {
235
+ ensureStreamStarted();
236
+ writeNdjson(reply, payload);
237
+ },
238
+ sendToolResult(payload = {}) {
239
+ ensureStreamStarted();
240
+ writeNdjson(reply, payload);
241
+ },
242
+ sendError(payload = {}) {
243
+ ensureStreamStarted();
244
+ writeNdjson(reply, payload);
245
+ },
246
+ sendDone(payload = {}) {
247
+ ensureStreamStarted();
248
+ writeNdjson(reply, payload);
249
+ }
250
+ });
251
+
252
+ try {
253
+ request.raw.on("close", closeListener);
254
+
255
+ await request.executeAction({
256
+ actionId: actionIds.chatStream,
257
+ context: {
258
+ surface: resolveAssistantWorkspaceRequestSurfaceId(request, workspaceRouteSurfaceConfig)
259
+ },
260
+ input: (() => {
261
+ const body = request.input.body;
262
+ const input = {
263
+ workspaceSlug: request.input.params.workspaceSlug,
264
+ messageId: body.messageId,
265
+ input: body.input
266
+ };
267
+ if (Object.hasOwn(body, "conversationId")) {
268
+ input.conversationId = body.conversationId;
269
+ }
270
+ if (Object.hasOwn(body, "history")) {
271
+ input.history = body.history;
272
+ }
273
+ if (Object.hasOwn(body, "clientContext")) {
274
+ input.clientContext = body.clientContext;
275
+ }
276
+ return input;
277
+ })(),
278
+ deps: {
279
+ streamWriter,
280
+ abortSignal: abortController.signal
281
+ }
282
+ });
283
+
284
+ if (streamStarted) {
285
+ endNdjson(reply);
286
+ return;
287
+ }
288
+
289
+ reply.code(204).send();
290
+ } catch (error) {
291
+ if (!streamStarted) {
292
+ const statusCode = Number(error?.status || error?.statusCode || 500);
293
+ const safeStatusCode = Number.isInteger(statusCode) && statusCode >= 400 && statusCode <= 599 ? statusCode : 500;
294
+ reply.code(safeStatusCode).send({
295
+ error: safeStatusCode >= 500 ? "Internal server error." : String(error?.message || "Request failed.")
296
+ });
297
+ return;
298
+ }
299
+
300
+ const streamError = mapStreamError(error);
301
+ writeNdjson(reply, {
302
+ type: "error",
303
+ ...streamError
304
+ });
305
+ writeNdjson(reply, {
306
+ type: "done",
307
+ status: "failed"
308
+ });
309
+ endNdjson(reply);
310
+ } finally {
311
+ request.raw.off("close", closeListener);
312
+ }
313
+ }
314
+ );
315
+
316
+ router.register(
317
+ "GET",
318
+ `${routeBase}/conversations`,
319
+ {
320
+ auth: "required",
321
+ surface: workspaceRouteSurfaceId,
322
+ visibility,
323
+ meta: {
324
+ tags: ["assistant"],
325
+ summary: "List assistant conversations for current workspace user."
326
+ },
327
+ paramsValidator: workspaceSlugParamsValidator,
328
+ queryValidator: assistantResource.operations.conversationsList.queryValidator,
329
+ responseValidators: withStandardErrorResponses({
330
+ 200: assistantResource.operations.conversationsList.outputValidator
331
+ })
332
+ },
333
+ async function assistantConversationsRoute(request, reply) {
334
+ const response = await request.executeAction({
335
+ actionId: actionIds.conversationsList,
336
+ context: {
337
+ surface: resolveAssistantWorkspaceRequestSurfaceId(request, workspaceRouteSurfaceConfig)
338
+ },
339
+ input: {
340
+ workspaceSlug: request.input.params.workspaceSlug,
341
+ query: request.input.query
342
+ }
343
+ });
344
+
345
+ reply.code(200).send(response);
346
+ }
347
+ );
348
+
349
+ router.register(
350
+ "GET",
351
+ `${routeBase}/conversations/:conversationId/messages`,
352
+ {
353
+ auth: "required",
354
+ surface: workspaceRouteSurfaceId,
355
+ visibility,
356
+ meta: {
357
+ tags: ["assistant"],
358
+ summary: "List messages for one assistant conversation."
359
+ },
360
+ paramsValidator: [workspaceSlugParamsValidator, assistantResource.operations.conversationMessagesList.paramsValidator],
361
+ queryValidator: assistantResource.operations.conversationMessagesList.queryValidator,
362
+ responseValidators: withStandardErrorResponses({
363
+ 200: assistantResource.operations.conversationMessagesList.outputValidator
364
+ })
365
+ },
366
+ async function assistantConversationMessagesRoute(request, reply) {
367
+ const response = await request.executeAction({
368
+ actionId: actionIds.conversationMessagesList,
369
+ context: {
370
+ surface: resolveAssistantWorkspaceRequestSurfaceId(request, workspaceRouteSurfaceConfig)
371
+ },
372
+ input: {
373
+ workspaceSlug: request.input.params.workspaceSlug,
374
+ conversationId: request.input.params.conversationId,
375
+ query: request.input.query
376
+ }
377
+ });
378
+
379
+ reply.code(200).send(response);
380
+ }
381
+ );
382
+ }
383
+
384
+ export { registerRoutes };
@@ -0,0 +1,100 @@
1
+ import { parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
2
+ import { toIso } from "./repositoryPersistenceUtils.js";
3
+
4
+ function mapConsoleRow(row = {}) {
5
+ return {
6
+ workspaceSurfacePrompt: String(row.assistant_workspace_surface_prompt || ""),
7
+ createdAt: toIso(row.created_at),
8
+ updatedAt: toIso(row.updated_at)
9
+ };
10
+ }
11
+
12
+ function mapWorkspaceRow(row = {}) {
13
+ return {
14
+ workspaceId: Number(row.workspace_id),
15
+ appSurfacePrompt: String(row.assistant_app_surface_prompt || ""),
16
+ createdAt: toIso(row.created_at),
17
+ updatedAt: toIso(row.updated_at)
18
+ };
19
+ }
20
+
21
+ function createRepository(knex) {
22
+ if (typeof knex !== "function") {
23
+ throw new TypeError("assistantSettingsRepository requires knex.");
24
+ }
25
+
26
+ async function ensureConsoleSettings(options = {}) {
27
+ const client = options?.trx || knex;
28
+ const row = await client("console_settings").where({ id: 1 }).first();
29
+ if (!row) {
30
+ throw new Error("console_settings row missing.");
31
+ }
32
+
33
+ return mapConsoleRow(row);
34
+ }
35
+
36
+ async function updateConsoleSettings(patch = {}, options = {}) {
37
+ const client = options?.trx || knex;
38
+ const nextWorkspaceSurfacePrompt = String(patch.workspaceSurfacePrompt || "");
39
+
40
+ await client("console_settings")
41
+ .where({ id: 1 })
42
+ .update({
43
+ assistant_workspace_surface_prompt: nextWorkspaceSurfacePrompt,
44
+ updated_at: new Date()
45
+ });
46
+
47
+ return ensureConsoleSettings({
48
+ trx: client
49
+ });
50
+ }
51
+
52
+ async function ensureWorkspaceSettings(workspaceId, options = {}) {
53
+ const numericWorkspaceId = parsePositiveInteger(workspaceId);
54
+ if (!numericWorkspaceId) {
55
+ throw new TypeError("assistantSettingsRepository.ensureWorkspaceSettings requires workspace id.");
56
+ }
57
+
58
+ const client = options?.trx || knex;
59
+ const row = await client("workspace_settings").where({ workspace_id: numericWorkspaceId }).first();
60
+ if (!row) {
61
+ throw new Error("workspace_settings row missing.");
62
+ }
63
+
64
+ return mapWorkspaceRow(row);
65
+ }
66
+
67
+ async function updateWorkspaceSettings(workspaceId, patch = {}, options = {}) {
68
+ const numericWorkspaceId = parsePositiveInteger(workspaceId);
69
+ if (!numericWorkspaceId) {
70
+ throw new TypeError("assistantSettingsRepository.updateWorkspaceSettings requires workspace id.");
71
+ }
72
+
73
+ const client = options?.trx || knex;
74
+ const nextAppSurfacePrompt = String(patch.appSurfacePrompt || "");
75
+
76
+ await ensureWorkspaceSettings(numericWorkspaceId, {
77
+ trx: client
78
+ });
79
+
80
+ await client("workspace_settings")
81
+ .where({ workspace_id: numericWorkspaceId })
82
+ .update({
83
+ assistant_app_surface_prompt: nextAppSurfacePrompt,
84
+ updated_at: new Date()
85
+ });
86
+
87
+ return ensureWorkspaceSettings(numericWorkspaceId, {
88
+ trx: client
89
+ });
90
+ }
91
+
92
+ return Object.freeze({
93
+ ensureConsoleSettings,
94
+ updateConsoleSettings,
95
+ ensureWorkspaceSettings,
96
+ updateWorkspaceSettings
97
+ });
98
+ }
99
+
100
+ export { createRepository };