@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,459 @@
1
+ import { requireAuth } from "@jskit-ai/kernel/server/runtime";
2
+ import { resolveActionContributors } from "@jskit-ai/kernel/server/actions";
3
+ import { normalizeActionDefinition } from "@jskit-ai/kernel/shared/actions";
4
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
5
+ import { resolveWorkspaceSlug } from "./resolveWorkspaceSlug.js";
6
+
7
+ const AUTOMATION_CHANNEL = "automation";
8
+
9
+ function normalizeAssistantExtension(value) {
10
+ const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
11
+ return Object.freeze({
12
+ description: normalizeText(source.description)
13
+ });
14
+ }
15
+
16
+ function normalizeAssistantActionExtension(action = {}) {
17
+ const source = action && typeof action === "object" && !Array.isArray(action) ? action : {};
18
+ const actionId = normalizeText(source.id);
19
+
20
+ if (Object.prototype.hasOwnProperty.call(source, "assistantTool")) {
21
+ throw new Error(
22
+ `Action definition \"${actionId || "<unknown>"}\" assistantTool is not supported. Use extensions.assistant instead.`
23
+ );
24
+ }
25
+
26
+ const extensions = source.extensions && typeof source.extensions === "object" && !Array.isArray(source.extensions)
27
+ ? source.extensions
28
+ : {};
29
+ return normalizeAssistantExtension(extensions.assistant);
30
+ }
31
+
32
+ function normalizeBarredEntry(value) {
33
+ return normalizeText(value).toLowerCase();
34
+ }
35
+
36
+ function normalizeBarredActionSet(value) {
37
+ const source = Array.isArray(value) ? value : [value];
38
+ const exact = new Set();
39
+ const prefixes = [];
40
+
41
+ for (const entry of source) {
42
+ const normalized = normalizeBarredEntry(entry);
43
+ if (!normalized) {
44
+ continue;
45
+ }
46
+
47
+ if (normalized.endsWith(".*")) {
48
+ const prefix = normalized.slice(0, -1);
49
+ if (prefix) {
50
+ prefixes.push(prefix);
51
+ }
52
+ continue;
53
+ }
54
+
55
+ exact.add(normalized);
56
+ }
57
+
58
+ return Object.freeze({
59
+ exact,
60
+ prefixes: Object.freeze(prefixes)
61
+ });
62
+ }
63
+
64
+ function isActionBarred(barredRules, actionId) {
65
+ const normalizedActionId = normalizeText(actionId).toLowerCase();
66
+ if (!normalizedActionId) {
67
+ return true;
68
+ }
69
+
70
+ if (barredRules.exact.has(normalizedActionId)) {
71
+ return true;
72
+ }
73
+
74
+ return barredRules.prefixes.some((prefix) => normalizedActionId.startsWith(prefix));
75
+ }
76
+
77
+ function sanitizeToolName(value) {
78
+ const normalized = String(value || "")
79
+ .trim()
80
+ .toLowerCase()
81
+ .replace(/[^a-z0-9_]+/g, "_")
82
+ .replace(/_+/g, "_")
83
+ .replace(/^_+|_+$/g, "");
84
+
85
+ if (!normalized) {
86
+ return "tool";
87
+ }
88
+
89
+ return normalized;
90
+ }
91
+
92
+ function resolveUniqueToolName(baseName, used) {
93
+ const normalizedBase = sanitizeToolName(baseName) || "tool";
94
+ let candidate = normalizedBase.slice(0, 64);
95
+ let suffix = 1;
96
+
97
+ while (used.has(candidate)) {
98
+ const suffixText = `_${suffix}`;
99
+ const baseBudget = Math.max(1, 64 - suffixText.length);
100
+ candidate = `${normalizedBase.slice(0, baseBudget)}${suffixText}`;
101
+ suffix += 1;
102
+ }
103
+
104
+ used.add(candidate);
105
+ return candidate;
106
+ }
107
+
108
+ function parseToolPayload(argumentsText) {
109
+ const source = String(argumentsText || "").trim();
110
+ if (!source) {
111
+ return {};
112
+ }
113
+
114
+ try {
115
+ const parsed = JSON.parse(source);
116
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
117
+ return {};
118
+ }
119
+
120
+ if (Array.isArray(parsed.args) || Object.hasOwn(parsed, "options")) {
121
+ const args = Array.isArray(parsed.args) ? parsed.args : [];
122
+ const options = parsed.options && typeof parsed.options === "object" && !Array.isArray(parsed.options)
123
+ ? parsed.options
124
+ : {};
125
+
126
+ if (args.length === 1 && args[0] && typeof args[0] === "object" && !Array.isArray(args[0])) {
127
+ return {
128
+ ...args[0],
129
+ ...options
130
+ };
131
+ }
132
+
133
+ return {
134
+ args,
135
+ ...options
136
+ };
137
+ }
138
+
139
+ return parsed;
140
+ } catch {
141
+ return {};
142
+ }
143
+ }
144
+
145
+ function canInvokeMethod(permission, context) {
146
+ const permissionSpec = normalizePermissionSpec(permission);
147
+
148
+ try {
149
+ requireAuth(
150
+ {
151
+ context
152
+ },
153
+ permissionSpec
154
+ );
155
+ return true;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ function normalizePermissionSpec(permission) {
162
+ const source = permission && typeof permission === "object" && !Array.isArray(permission)
163
+ ? permission
164
+ : {};
165
+ const requireMode = normalizeText(source.require || "none").toLowerCase();
166
+ const permissions = Array.isArray(source.permissions) ? source.permissions : [];
167
+
168
+ return Object.freeze({
169
+ require: requireMode || "none",
170
+ permissions,
171
+ message: normalizeText(source.message),
172
+ code: normalizeText(source.code)
173
+ });
174
+ }
175
+
176
+ function stripWorkspaceSlugFromSchema(schema, context = {}) {
177
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
178
+ return schema;
179
+ }
180
+
181
+ const workspaceSlug = resolveWorkspaceSlug(context);
182
+ if (!workspaceSlug) {
183
+ return schema;
184
+ }
185
+
186
+ if (schema.type !== "object" || !schema.properties || typeof schema.properties !== "object") {
187
+ return schema;
188
+ }
189
+
190
+ if (!Object.hasOwn(schema.properties, "workspaceSlug")) {
191
+ return schema;
192
+ }
193
+
194
+ const properties = { ...schema.properties };
195
+ delete properties.workspaceSlug;
196
+
197
+ const requiredSource = Array.isArray(schema.required) ? schema.required : [];
198
+ const required = requiredSource.filter((entry) => entry !== "workspaceSlug");
199
+
200
+ return {
201
+ ...schema,
202
+ properties,
203
+ ...(Array.isArray(schema.required) ? { required } : {})
204
+ };
205
+ }
206
+
207
+ function hasAutomationChannel(action = {}) {
208
+ const channels = Array.isArray(action.channels) ? action.channels : [];
209
+ return channels.some((channel) => normalizeText(channel).toLowerCase() === AUTOMATION_CHANNEL);
210
+ }
211
+
212
+ function resolveActionBackedToolEntries(scope) {
213
+ if (!scope || typeof scope.has !== "function" || typeof scope.make !== "function") {
214
+ return new Map();
215
+ }
216
+ if (typeof scope.resolveTag !== "function") {
217
+ return new Map();
218
+ }
219
+
220
+ const entriesByActionId = new Map();
221
+ const contributors = resolveActionContributors(scope);
222
+
223
+ for (const contributor of contributors) {
224
+ const actions = Array.isArray(contributor?.actions) ? contributor.actions : [];
225
+ for (const action of actions) {
226
+ if (!action || typeof action !== "object") {
227
+ continue;
228
+ }
229
+ if (!hasAutomationChannel(action)) {
230
+ continue;
231
+ }
232
+
233
+ const actionId = normalizeText(action.id);
234
+ if (!actionId) {
235
+ continue;
236
+ }
237
+
238
+ let normalizedAction = null;
239
+ let assistantExtension = null;
240
+ try {
241
+ assistantExtension = normalizeAssistantActionExtension(action);
242
+ normalizedAction = normalizeActionDefinition(action, {
243
+ contributorDomain: action.domain
244
+ });
245
+ } catch {
246
+ continue;
247
+ }
248
+
249
+ const inputSchema = normalizedAction.inputValidator?.schema || null;
250
+ const outputSchema = normalizedAction.outputValidator?.schema || null;
251
+ if (!inputSchema || !outputSchema) {
252
+ continue;
253
+ }
254
+
255
+ const actionVersion = Number(normalizedAction.version) || 1;
256
+ const actionKey = actionId.toLowerCase();
257
+ const nextEntry = Object.freeze({
258
+ actionId,
259
+ actionVersion,
260
+ toolBaseName: actionId,
261
+ description: assistantExtension.description || `Run ${actionId}.`,
262
+ inputSchema,
263
+ outputSchema,
264
+ permission: normalizePermissionSpec(normalizedAction.permission)
265
+ });
266
+ const existing = entriesByActionId.get(actionKey);
267
+ if (!existing || actionVersion >= Number(existing.actionVersion || 0)) {
268
+ entriesByActionId.set(actionKey, nextEntry);
269
+ }
270
+ }
271
+ }
272
+
273
+ return entriesByActionId;
274
+ }
275
+
276
+ function resolveActionToolEntries(
277
+ scope,
278
+ { barredActionIds = [], skipActionPrefixes = [] } = {}
279
+ ) {
280
+ const actionBackedEntries = resolveActionBackedToolEntries(scope);
281
+ const barredRules = normalizeBarredActionSet(barredActionIds);
282
+ const usedToolNames = new Set();
283
+ const entries = [];
284
+
285
+ for (const actionEntry of actionBackedEntries.values()) {
286
+ const actionId = normalizeText(actionEntry?.actionId);
287
+ if (!actionId) {
288
+ continue;
289
+ }
290
+
291
+ const normalizedActionId = actionId.toLowerCase();
292
+ const skipByPrefix = skipActionPrefixes.some((prefix) => normalizedActionId.startsWith(prefix));
293
+ if (skipByPrefix) {
294
+ continue;
295
+ }
296
+
297
+ if (isActionBarred(barredRules, actionId)) {
298
+ continue;
299
+ }
300
+
301
+ const toolName = resolveUniqueToolName(actionEntry.toolBaseName, usedToolNames);
302
+ entries.push(
303
+ Object.freeze({
304
+ descriptor: Object.freeze({
305
+ name: toolName,
306
+ actionId,
307
+ actionVersion: Number(actionEntry.actionVersion) || null,
308
+ description: normalizeText(actionEntry.description) || `Run ${actionId}.`,
309
+ parameters: actionEntry.inputSchema,
310
+ outputSchema: actionEntry.outputSchema
311
+ }),
312
+ permission: actionEntry.permission
313
+ })
314
+ );
315
+ }
316
+
317
+ return Object.freeze(entries.sort((left, right) => left.descriptor.name.localeCompare(right.descriptor.name)));
318
+ }
319
+
320
+ function createServiceToolCatalog(
321
+ scope,
322
+ { barredActionIds = [], skipActionPrefixes = [] } = {}
323
+ ) {
324
+ if (!scope || typeof scope.make !== "function") {
325
+ throw new Error("createServiceToolCatalog requires container scope.make().");
326
+ }
327
+
328
+ const normalizedSkipPrefixes = (Array.isArray(skipActionPrefixes) ? skipActionPrefixes : [skipActionPrefixes])
329
+ .map((entry) => normalizeText(entry).toLowerCase())
330
+ .filter(Boolean);
331
+ let methodEntries = null;
332
+
333
+ function resolveOrCreateMethodEntries() {
334
+ if (methodEntries) {
335
+ return methodEntries;
336
+ }
337
+
338
+ methodEntries = resolveActionToolEntries(scope, {
339
+ barredActionIds,
340
+ skipActionPrefixes: normalizedSkipPrefixes
341
+ });
342
+ return methodEntries;
343
+ }
344
+
345
+ function resolveToolSet(context = {}) {
346
+ const tools = [];
347
+ const byName = new Map();
348
+ for (const entry of resolveOrCreateMethodEntries()) {
349
+ if (!canInvokeMethod(entry.permission, context)) {
350
+ continue;
351
+ }
352
+
353
+ const descriptor = Object.freeze({
354
+ ...entry.descriptor,
355
+ parameters: stripWorkspaceSlugFromSchema(entry.descriptor.parameters, context)
356
+ });
357
+
358
+ tools.push(descriptor);
359
+ byName.set(descriptor.name, descriptor);
360
+ }
361
+
362
+ return Object.freeze({
363
+ tools: Object.freeze(tools),
364
+ byName
365
+ });
366
+ }
367
+
368
+ function toOpenAiToolSchema(tool) {
369
+ return {
370
+ type: "function",
371
+ function: {
372
+ name: tool.name,
373
+ description: tool.description,
374
+ parameters: tool.parameters
375
+ }
376
+ };
377
+ }
378
+
379
+ async function executeToolCall({ toolName = "", argumentsText = "", context = {}, toolSet = null } = {}) {
380
+ const normalizedToolName = normalizeText(toolName);
381
+ const resolvedToolSet = toolSet && typeof toolSet === "object" ? toolSet : resolveToolSet(context);
382
+ const descriptor = normalizedToolName ? resolvedToolSet.byName.get(normalizedToolName) : null;
383
+
384
+ if (!descriptor) {
385
+ return {
386
+ ok: false,
387
+ error: {
388
+ code: "assistant_tool_unknown",
389
+ message: "Unknown tool."
390
+ }
391
+ };
392
+ }
393
+
394
+ if (!scope.has("actionExecutor")) {
395
+ return {
396
+ ok: false,
397
+ error: {
398
+ code: "assistant_tool_unavailable",
399
+ message: "Tool executor is unavailable.",
400
+ status: 500
401
+ }
402
+ };
403
+ }
404
+ const actionExecutor = scope.make("actionExecutor");
405
+ if (!actionExecutor || typeof actionExecutor.execute !== "function") {
406
+ return {
407
+ ok: false,
408
+ error: {
409
+ code: "assistant_tool_unavailable",
410
+ message: "Tool executor is unavailable.",
411
+ status: 500
412
+ }
413
+ };
414
+ }
415
+
416
+ try {
417
+ const actionInput = parseToolPayload(argumentsText);
418
+ if (actionInput && typeof actionInput === "object" && !Array.isArray(actionInput)) {
419
+ const workspaceSlug = resolveWorkspaceSlug(context, actionInput);
420
+ if (workspaceSlug && !Object.hasOwn(actionInput, "workspaceSlug")) {
421
+ actionInput.workspaceSlug = workspaceSlug;
422
+ }
423
+ }
424
+ const executionContext = {
425
+ ...context,
426
+ channel: AUTOMATION_CHANNEL
427
+ };
428
+
429
+ const result = await actionExecutor.execute({
430
+ actionId: descriptor.actionId,
431
+ version: descriptor.actionVersion || null,
432
+ input: actionInput,
433
+ context: executionContext
434
+ });
435
+ return {
436
+ ok: true,
437
+ result
438
+ };
439
+ } catch (error) {
440
+ const status = Number(error?.status || error?.statusCode || 500);
441
+ return {
442
+ ok: false,
443
+ error: {
444
+ code: String(error?.code || "assistant_tool_failed").trim() || "assistant_tool_failed",
445
+ message: status >= 500 ? "Tool call failed." : String(error?.message || "Tool call failed."),
446
+ status: Number.isInteger(status) ? status : 500
447
+ }
448
+ };
449
+ }
450
+ }
451
+
452
+ return Object.freeze({
453
+ resolveToolSet,
454
+ toOpenAiToolSchema,
455
+ executeToolCall
456
+ });
457
+ }
458
+
459
+ export { createServiceToolCatalog };