@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,35 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import descriptor from "../package.descriptor.mjs";
4
+
5
+ function findFileMutation(id) {
6
+ const mutations = Array.isArray(descriptor?.mutations?.files) ? descriptor.mutations.files : [];
7
+ return mutations.find((entry) => String(entry?.id || "") === id) || null;
8
+ }
9
+
10
+ function findTextMutation(id) {
11
+ const mutations = Array.isArray(descriptor?.mutations?.text) ? descriptor.mutations.text : [];
12
+ return mutations.find((entry) => String(entry?.id || "") === id) || null;
13
+ }
14
+
15
+ test("assistant descriptor exposes configurable workspace surface option", () => {
16
+ const option = descriptor?.options?.surfaces;
17
+ assert.equal(option?.required, true);
18
+ assert.equal(option?.inputType, "text");
19
+ assert.equal(option?.defaultValue, "admin");
20
+ });
21
+
22
+ test("assistant descriptor routes workspace page + placements through surface option", () => {
23
+ const workspacePage = findFileMutation("assistant-page-admin-workspace-assistant-index");
24
+ const menuPlacement = findTextMutation("assistant-placement-menu");
25
+ const workspaceSettingsPlacement = findTextMutation("assistant-workspace-settings-form-placement");
26
+ const consoleSettingsPlacement = findTextMutation("assistant-console-settings-form-placement");
27
+
28
+ assert.equal(workspacePage?.toSurface, "${option:surfaces|lower}");
29
+ assert.match(String(menuPlacement?.value || ""), /"\$\{option:surfaces\|lower\}"/);
30
+ assert.match(String(menuPlacement?.value || ""), /\.split\(","\)/);
31
+ assert.match(String(menuPlacement?.value || ""), /surfaces: assistantWorkspaceSurfaceIds\.length > 0 \? assistantWorkspaceSurfaceIds : \["\*"\]/);
32
+ assert.doesNotMatch(String(menuPlacement?.value || ""), /surface:\s*"/);
33
+ assert.match(String(workspaceSettingsPlacement?.value || ""), /surfaces: \["\*"\]/);
34
+ assert.match(String(consoleSettingsPlacement?.value || ""), /surfaces: \["\*"\]/);
35
+ });
@@ -0,0 +1,41 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ ASSISTANT_QUERY_KEY_PREFIX,
5
+ assistantRootQueryKey,
6
+ assistantWorkspaceScopeQueryKey,
7
+ assistantConversationsListQueryKey,
8
+ assistantConversationMessagesQueryKey
9
+ } from "../src/shared/queryKeys.js";
10
+
11
+ test("assistant query keys normalize workspace scope and paging", () => {
12
+ assert.deepEqual(ASSISTANT_QUERY_KEY_PREFIX, ["assistant"]);
13
+ assert.deepEqual(assistantRootQueryKey(), ["assistant"]);
14
+
15
+ assert.deepEqual(assistantWorkspaceScopeQueryKey({ workspaceSlug: "acme" }), ["assistant", "slug:acme"]);
16
+ assert.deepEqual(assistantWorkspaceScopeQueryKey({ workspaceId: "9" }), ["assistant", "id:9"]);
17
+
18
+ assert.deepEqual(assistantConversationsListQueryKey({ workspaceSlug: "acme" }), [
19
+ "assistant",
20
+ "slug:acme",
21
+ "conversations",
22
+ "list",
23
+ 20,
24
+ "all"
25
+ ]);
26
+
27
+ assert.deepEqual(
28
+ assistantConversationsListQueryKey({ workspaceId: 9 }, { limit: 10, status: "ACTIVE" }),
29
+ ["assistant", "id:9", "conversations", "list", 10, "active"]
30
+ );
31
+
32
+ assert.deepEqual(assistantConversationMessagesQueryKey({ workspaceSlug: "acme" }, "22"), [
33
+ "assistant",
34
+ "slug:acme",
35
+ "conversations",
36
+ "22",
37
+ "messages",
38
+ 1,
39
+ 200
40
+ ]);
41
+ });
@@ -0,0 +1,83 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { resolveWorkspaceSlug } from "../src/server/lib/resolveWorkspaceSlug.js";
4
+
5
+ test("resolveWorkspaceSlug follows fallback priority", () => {
6
+ const slug = resolveWorkspaceSlug(
7
+ {
8
+ workspace: {
9
+ slug: "workspace-primary"
10
+ },
11
+ requestMeta: {
12
+ resolvedWorkspaceContext: {
13
+ workspace: {
14
+ slug: "workspace-resolved"
15
+ }
16
+ },
17
+ request: {
18
+ input: {
19
+ params: {
20
+ workspaceSlug: "workspace-request"
21
+ }
22
+ }
23
+ }
24
+ }
25
+ },
26
+ {
27
+ workspaceSlug: "workspace-input"
28
+ }
29
+ );
30
+
31
+ assert.equal(slug, "workspace-primary");
32
+ });
33
+
34
+ test("resolveWorkspaceSlug resolves from requestMeta resolved workspace", () => {
35
+ const slug = resolveWorkspaceSlug({
36
+ requestMeta: {
37
+ resolvedWorkspaceContext: {
38
+ workspace: {
39
+ slug: "workspace-resolved"
40
+ }
41
+ }
42
+ }
43
+ });
44
+
45
+ assert.equal(slug, "workspace-resolved");
46
+ });
47
+
48
+ test("resolveWorkspaceSlug resolves from action input before request params", () => {
49
+ const slug = resolveWorkspaceSlug(
50
+ {
51
+ requestMeta: {
52
+ request: {
53
+ input: {
54
+ params: {
55
+ workspaceSlug: "workspace-request"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ },
61
+ {
62
+ workspaceSlug: "workspace-input"
63
+ }
64
+ );
65
+
66
+ assert.equal(slug, "workspace-input");
67
+ });
68
+
69
+ test("resolveWorkspaceSlug resolves from request params when needed", () => {
70
+ const slug = resolveWorkspaceSlug({
71
+ requestMeta: {
72
+ request: {
73
+ input: {
74
+ params: {
75
+ workspaceSlug: "workspace-request"
76
+ }
77
+ }
78
+ }
79
+ }
80
+ });
81
+
82
+ assert.equal(slug, "workspace-request");
83
+ });
@@ -0,0 +1,287 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
4
+ import { registerRoutes } from "../src/server/registerRoutes.js";
5
+
6
+ function createReplyDouble() {
7
+ return {
8
+ statusCode: 200,
9
+ payload: null,
10
+ code(statusCode) {
11
+ this.statusCode = statusCode;
12
+ return this;
13
+ },
14
+ send(payload) {
15
+ this.payload = payload;
16
+ return this;
17
+ }
18
+ };
19
+ }
20
+
21
+ function findRoute(routes, method, path) {
22
+ return routes.find((route) => route.method === method && route.path === path) || null;
23
+ }
24
+
25
+ test("assistant routes build list inputs with explicit query object", async () => {
26
+ const registeredRoutes = [];
27
+ const router = {
28
+ register(method, path, route, handler) {
29
+ registeredRoutes.push({
30
+ method,
31
+ path,
32
+ route,
33
+ handler
34
+ });
35
+ }
36
+ };
37
+ const app = {
38
+ make(token) {
39
+ if (token !== KERNEL_TOKENS.HttpRouter) {
40
+ throw new Error(`Unexpected token: ${String(token)}`);
41
+ }
42
+ return router;
43
+ }
44
+ };
45
+
46
+ registerRoutes(app);
47
+
48
+ const conversationsRoute = findRoute(registeredRoutes, "GET", "/api/w/:workspaceSlug/assistant/conversations");
49
+ const messagesRoute = findRoute(
50
+ registeredRoutes,
51
+ "GET",
52
+ "/api/w/:workspaceSlug/assistant/conversations/:conversationId/messages"
53
+ );
54
+ const consoleSettingsReadRoute = findRoute(registeredRoutes, "GET", "/api/console/settings/assistant");
55
+ const consoleSettingsPatchRoute = findRoute(registeredRoutes, "PATCH", "/api/console/settings/assistant");
56
+ const workspaceSettingsReadRoute = findRoute(
57
+ registeredRoutes,
58
+ "GET",
59
+ "/api/w/:workspaceSlug/workspace/settings/assistant"
60
+ );
61
+ const workspaceSettingsPatchRoute = findRoute(
62
+ registeredRoutes,
63
+ "PATCH",
64
+ "/api/w/:workspaceSlug/workspace/settings/assistant"
65
+ );
66
+ assert.ok(conversationsRoute);
67
+ assert.ok(messagesRoute);
68
+ assert.ok(consoleSettingsReadRoute);
69
+ assert.ok(consoleSettingsPatchRoute);
70
+ assert.ok(workspaceSettingsReadRoute);
71
+ assert.ok(workspaceSettingsPatchRoute);
72
+
73
+ const calls = [];
74
+ const executeAction = async (payload) => {
75
+ calls.push(payload);
76
+ return {};
77
+ };
78
+
79
+ await conversationsRoute.handler(
80
+ {
81
+ input: {
82
+ params: { workspaceSlug: "acme" },
83
+ query: { cursor: 30, limit: 50, status: "active" }
84
+ },
85
+ executeAction
86
+ },
87
+ createReplyDouble()
88
+ );
89
+ await messagesRoute.handler(
90
+ {
91
+ input: {
92
+ params: { workspaceSlug: "acme", conversationId: 10 },
93
+ query: { page: 3, pageSize: 100 }
94
+ },
95
+ executeAction
96
+ },
97
+ createReplyDouble()
98
+ );
99
+ await consoleSettingsReadRoute.handler(
100
+ {
101
+ input: {},
102
+ executeAction
103
+ },
104
+ createReplyDouble()
105
+ );
106
+ await consoleSettingsPatchRoute.handler(
107
+ {
108
+ input: {
109
+ body: { workspaceSurfacePrompt: "Console prompt" }
110
+ },
111
+ executeAction
112
+ },
113
+ createReplyDouble()
114
+ );
115
+ await workspaceSettingsReadRoute.handler(
116
+ {
117
+ input: {
118
+ params: { workspaceSlug: "acme" }
119
+ },
120
+ executeAction
121
+ },
122
+ createReplyDouble()
123
+ );
124
+ await workspaceSettingsPatchRoute.handler(
125
+ {
126
+ input: {
127
+ params: { workspaceSlug: "acme" },
128
+ body: { appSurfacePrompt: "Workspace prompt" }
129
+ },
130
+ executeAction
131
+ },
132
+ createReplyDouble()
133
+ );
134
+
135
+ assert.deepEqual(calls[0].input, {
136
+ workspaceSlug: "acme",
137
+ query: { cursor: 30, limit: 50, status: "active" }
138
+ });
139
+ assert.deepEqual(calls[0].context, {
140
+ surface: "app"
141
+ });
142
+ assert.deepEqual(calls[1].input, {
143
+ workspaceSlug: "acme",
144
+ conversationId: 10,
145
+ query: { page: 3, pageSize: 100 }
146
+ });
147
+ assert.deepEqual(calls[1].context, {
148
+ surface: "app"
149
+ });
150
+ assert.deepEqual(calls[2], {
151
+ actionId: "assistant.console.settings.read"
152
+ });
153
+ assert.deepEqual(calls[3], {
154
+ actionId: "assistant.console.settings.update",
155
+ input: {
156
+ payload: {
157
+ workspaceSurfacePrompt: "Console prompt"
158
+ }
159
+ }
160
+ });
161
+ assert.deepEqual(calls[4], {
162
+ actionId: "assistant.workspace.settings.read",
163
+ context: {
164
+ surface: "app"
165
+ },
166
+ input: {
167
+ workspaceSlug: "acme"
168
+ }
169
+ });
170
+ assert.deepEqual(calls[5], {
171
+ actionId: "assistant.workspace.settings.update",
172
+ context: {
173
+ surface: "app"
174
+ },
175
+ input: {
176
+ workspaceSlug: "acme",
177
+ patch: {
178
+ appSurfacePrompt: "Workspace prompt"
179
+ }
180
+ }
181
+ });
182
+ });
183
+
184
+ test("assistant workspace routes use workspace default surface and honor x-jskit-surface header", async () => {
185
+ const registeredRoutes = [];
186
+ const router = {
187
+ register(method, path, route, handler) {
188
+ registeredRoutes.push({
189
+ method,
190
+ path,
191
+ route,
192
+ handler
193
+ });
194
+ }
195
+ };
196
+ const app = {
197
+ has(token) {
198
+ return token === "appConfig";
199
+ },
200
+ make(token) {
201
+ if (token === KERNEL_TOKENS.HttpRouter) {
202
+ return router;
203
+ }
204
+ if (token === "appConfig") {
205
+ return {
206
+ surfaceDefaultId: "admin",
207
+ surfaceDefinitions: {
208
+ app: {
209
+ id: "app",
210
+ enabled: true,
211
+ requiresWorkspace: true
212
+ },
213
+ admin: {
214
+ id: "admin",
215
+ enabled: true,
216
+ requiresWorkspace: true
217
+ },
218
+ console: {
219
+ id: "console",
220
+ enabled: true,
221
+ requiresWorkspace: false
222
+ }
223
+ }
224
+ };
225
+ }
226
+ throw new Error(`Unexpected token: ${String(token)}`);
227
+ }
228
+ };
229
+
230
+ registerRoutes(app);
231
+
232
+ const expectedWorkspaceRoutes = [
233
+ ["GET", "/api/w/:workspaceSlug/workspace/settings/assistant"],
234
+ ["PATCH", "/api/w/:workspaceSlug/workspace/settings/assistant"],
235
+ ["POST", "/api/w/:workspaceSlug/assistant/chat/stream"],
236
+ ["GET", "/api/w/:workspaceSlug/assistant/conversations"],
237
+ ["GET", "/api/w/:workspaceSlug/assistant/conversations/:conversationId/messages"]
238
+ ];
239
+
240
+ for (const [method, path] of expectedWorkspaceRoutes) {
241
+ const route = findRoute(registeredRoutes, method, path);
242
+ assert.ok(route);
243
+ assert.equal(route.route.surface, "admin");
244
+ }
245
+
246
+ const conversationsRoute = findRoute(registeredRoutes, "GET", "/api/w/:workspaceSlug/assistant/conversations");
247
+ const actionCalls = [];
248
+ const executeAction = async (payload) => {
249
+ actionCalls.push(payload);
250
+ return {};
251
+ };
252
+
253
+ await conversationsRoute.handler(
254
+ {
255
+ headers: {
256
+ "x-jskit-surface": "app"
257
+ },
258
+ input: {
259
+ params: { workspaceSlug: "acme" },
260
+ query: {}
261
+ },
262
+ executeAction
263
+ },
264
+ createReplyDouble()
265
+ );
266
+ await conversationsRoute.handler(
267
+ {
268
+ headers: {
269
+ "x-jskit-surface": "missing"
270
+ },
271
+ input: {
272
+ params: { workspaceSlug: "acme" },
273
+ query: {}
274
+ },
275
+ executeAction
276
+ },
277
+ createReplyDouble()
278
+ );
279
+
280
+ assert.equal(actionCalls.length, 2);
281
+ assert.deepEqual(actionCalls[0].context, {
282
+ surface: "app"
283
+ });
284
+ assert.deepEqual(actionCalls[1].context, {
285
+ surface: "admin"
286
+ });
287
+ });