@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.
- package/package.descriptor.mjs +284 -0
- package/package.json +31 -0
- package/src/client/components/AssistantClientElement.vue +1316 -0
- package/src/client/components/AssistantConsoleSettingsClientElement.vue +71 -0
- package/src/client/components/AssistantSettingsFormCard.vue +76 -0
- package/src/client/components/AssistantWorkspaceClientElement.vue +15 -0
- package/src/client/components/AssistantWorkspaceSettingsClientElement.vue +73 -0
- package/src/client/composables/useAssistantWorkspaceRuntime.js +789 -0
- package/src/client/index.js +12 -0
- package/src/client/lib/assistantApi.js +137 -0
- package/src/client/lib/assistantHttpClient.js +10 -0
- package/src/client/lib/markdownRenderer.js +31 -0
- package/src/client/providers/AssistantWebClientProvider.js +25 -0
- package/src/server/AssistantServiceProvider.js +179 -0
- package/src/server/actionIds.js +11 -0
- package/src/server/actions.js +191 -0
- package/src/server/diTokens.js +19 -0
- package/src/server/lib/aiClient.js +43 -0
- package/src/server/lib/ndjson.js +47 -0
- package/src/server/lib/providers/anthropicClient.js +375 -0
- package/src/server/lib/providers/common.js +158 -0
- package/src/server/lib/providers/deepSeekClient.js +22 -0
- package/src/server/lib/providers/openAiClient.js +13 -0
- package/src/server/lib/providers/openAiCompatibleClient.js +69 -0
- package/src/server/lib/resolveWorkspaceSlug.js +24 -0
- package/src/server/lib/serviceToolCatalog.js +459 -0
- package/src/server/registerRoutes.js +384 -0
- package/src/server/repositories/assistantSettingsRepository.js +100 -0
- package/src/server/repositories/conversationsRepository.js +244 -0
- package/src/server/repositories/messagesRepository.js +154 -0
- package/src/server/repositories/repositoryPersistenceUtils.js +63 -0
- package/src/server/services/assistantSettingsService.js +153 -0
- package/src/server/services/chatService.js +987 -0
- package/src/server/services/transcriptService.js +334 -0
- package/src/shared/assistantPaths.js +50 -0
- package/src/shared/assistantResource.js +323 -0
- package/src/shared/assistantSettingsResource.js +214 -0
- package/src/shared/index.js +39 -0
- package/src/shared/queryKeys.js +69 -0
- package/src/shared/settingsEvents.js +7 -0
- package/src/shared/streamEvents.js +31 -0
- package/src/shared/support/positiveInteger.js +9 -0
- package/templates/migrations/assistant_settings_initial.cjs +39 -0
- package/templates/migrations/assistant_transcripts_initial.cjs +51 -0
- package/templates/src/pages/admin/workspace/assistant/index.vue +7 -0
- package/test/aiConfigValidation.test.js +15 -0
- package/test/assistantApiSurfaceHeader.test.js +64 -0
- package/test/assistantResource.test.js +53 -0
- package/test/assistantSettingsResource.test.js +48 -0
- package/test/assistantSettingsService.test.js +133 -0
- package/test/chatService.test.js +841 -0
- package/test/descriptorSurfaceOption.test.js +35 -0
- package/test/queryKeys.test.js +41 -0
- package/test/resolveWorkspaceSlug.test.js +83 -0
- package/test/routeInputContracts.test.js +287 -0
- package/test/serviceToolCatalog.test.js +1235 -0
- package/test/transcriptService.test.js +175 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators";
|
|
3
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
4
|
+
|
|
5
|
+
const MAX_SYSTEM_PROMPT_CHARS = 12_000;
|
|
6
|
+
|
|
7
|
+
function createPromptSchema(promptLabel) {
|
|
8
|
+
return Type.String({
|
|
9
|
+
maxLength: MAX_SYSTEM_PROMPT_CHARS,
|
|
10
|
+
messages: {
|
|
11
|
+
maxLength: `${promptLabel} must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters.`,
|
|
12
|
+
default: `${promptLabel} must be valid text.`
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createPromptSettingsResource({
|
|
18
|
+
resourceId = "",
|
|
19
|
+
fields = [],
|
|
20
|
+
validationMessage = "Fix invalid values and try again.",
|
|
21
|
+
saveSuccessMessage = "Saved.",
|
|
22
|
+
saveErrorMessage = "Unable to save settings."
|
|
23
|
+
} = {}) {
|
|
24
|
+
const settingsOutputProperties = {};
|
|
25
|
+
const settingsCreateProperties = {};
|
|
26
|
+
for (const field of fields) {
|
|
27
|
+
settingsOutputProperties[field.key] = field.outputSchema;
|
|
28
|
+
settingsCreateProperties[field.key] =
|
|
29
|
+
field.required === false ? Type.Optional(field.inputSchema) : field.inputSchema;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const settingsOutputSchema = Type.Object(settingsOutputProperties, { additionalProperties: false });
|
|
33
|
+
const settingsCreateSchema = Type.Object(settingsCreateProperties, { additionalProperties: false });
|
|
34
|
+
const settingsPatchSchema = Type.Partial(settingsCreateSchema, {
|
|
35
|
+
additionalProperties: false
|
|
36
|
+
});
|
|
37
|
+
const recordSchema = Type.Object(
|
|
38
|
+
{
|
|
39
|
+
settings: settingsOutputSchema
|
|
40
|
+
},
|
|
41
|
+
{ additionalProperties: false }
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
function normalizeInput(payload = {}) {
|
|
45
|
+
const source = normalizeObjectInput(payload);
|
|
46
|
+
const normalized = {};
|
|
47
|
+
for (const field of fields) {
|
|
48
|
+
if (!Object.hasOwn(source, field.key)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
normalized[field.key] = field.normalizeInput(source[field.key], {
|
|
52
|
+
payload: source
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeOutput(payload = {}) {
|
|
59
|
+
const source = normalizeObjectInput(payload);
|
|
60
|
+
const settingsSource = normalizeObjectInput(source.settings);
|
|
61
|
+
const settings = {};
|
|
62
|
+
for (const field of fields) {
|
|
63
|
+
const rawValue = Object.hasOwn(settingsSource, field.key)
|
|
64
|
+
? settingsSource[field.key]
|
|
65
|
+
: field.resolveDefault({
|
|
66
|
+
settings: settingsSource
|
|
67
|
+
});
|
|
68
|
+
settings[field.key] = field.normalizeOutput(rawValue, {
|
|
69
|
+
settings: settingsSource
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
settings
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const outputValidator = Object.freeze({
|
|
78
|
+
schema: recordSchema,
|
|
79
|
+
normalize: normalizeOutput
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
resource: resourceId,
|
|
84
|
+
messages: {
|
|
85
|
+
validation: validationMessage,
|
|
86
|
+
saveSuccess: saveSuccessMessage,
|
|
87
|
+
saveError: saveErrorMessage
|
|
88
|
+
},
|
|
89
|
+
operations: Object.freeze({
|
|
90
|
+
view: Object.freeze({
|
|
91
|
+
method: "GET",
|
|
92
|
+
outputValidator
|
|
93
|
+
}),
|
|
94
|
+
create: Object.freeze({
|
|
95
|
+
method: "POST",
|
|
96
|
+
bodyValidator: Object.freeze({
|
|
97
|
+
schema: settingsCreateSchema,
|
|
98
|
+
normalize: normalizeInput
|
|
99
|
+
}),
|
|
100
|
+
outputValidator
|
|
101
|
+
}),
|
|
102
|
+
replace: Object.freeze({
|
|
103
|
+
method: "PUT",
|
|
104
|
+
bodyValidator: Object.freeze({
|
|
105
|
+
schema: settingsCreateSchema,
|
|
106
|
+
normalize: normalizeInput
|
|
107
|
+
}),
|
|
108
|
+
outputValidator
|
|
109
|
+
}),
|
|
110
|
+
patch: Object.freeze({
|
|
111
|
+
method: "PATCH",
|
|
112
|
+
bodyValidator: Object.freeze({
|
|
113
|
+
schema: settingsPatchSchema,
|
|
114
|
+
normalize: normalizeInput
|
|
115
|
+
}),
|
|
116
|
+
outputValidator
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function createFieldRegistry(scopeLabel) {
|
|
123
|
+
const fields = [];
|
|
124
|
+
|
|
125
|
+
function defineField(field = {}) {
|
|
126
|
+
const key = normalizeText(field.key);
|
|
127
|
+
if (!key) {
|
|
128
|
+
throw new TypeError(`${scopeLabel}.defineField requires field.key.`);
|
|
129
|
+
}
|
|
130
|
+
if (fields.some((entry) => entry.key === key)) {
|
|
131
|
+
throw new Error(`${scopeLabel}.defineField duplicate key: ${key}`);
|
|
132
|
+
}
|
|
133
|
+
if (!field.inputSchema || typeof field.inputSchema !== "object") {
|
|
134
|
+
throw new TypeError(`${scopeLabel}.defineField("${key}") requires inputSchema.`);
|
|
135
|
+
}
|
|
136
|
+
if (!field.outputSchema || typeof field.outputSchema !== "object") {
|
|
137
|
+
throw new TypeError(`${scopeLabel}.defineField("${key}") requires outputSchema.`);
|
|
138
|
+
}
|
|
139
|
+
if (typeof field.normalizeInput !== "function") {
|
|
140
|
+
throw new TypeError(`${scopeLabel}.defineField("${key}") requires normalizeInput.`);
|
|
141
|
+
}
|
|
142
|
+
if (typeof field.normalizeOutput !== "function") {
|
|
143
|
+
throw new TypeError(`${scopeLabel}.defineField("${key}") requires normalizeOutput.`);
|
|
144
|
+
}
|
|
145
|
+
if (typeof field.resolveDefault !== "function") {
|
|
146
|
+
throw new TypeError(`${scopeLabel}.defineField("${key}") requires resolveDefault.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fields.push({
|
|
150
|
+
key,
|
|
151
|
+
required: field.required !== false,
|
|
152
|
+
inputSchema: field.inputSchema,
|
|
153
|
+
outputSchema: field.outputSchema,
|
|
154
|
+
normalizeInput: field.normalizeInput,
|
|
155
|
+
normalizeOutput: field.normalizeOutput,
|
|
156
|
+
resolveDefault: field.resolveDefault
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
fields,
|
|
162
|
+
defineField
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const assistantConsoleSettingsFields = (() => {
|
|
167
|
+
const registry = createFieldRegistry("assistantConsoleSettingsFields");
|
|
168
|
+
const { fields, defineField } = registry;
|
|
169
|
+
defineField({
|
|
170
|
+
key: "workspaceSurfacePrompt",
|
|
171
|
+
required: true,
|
|
172
|
+
inputSchema: createPromptSchema("Workspace surface system prompt"),
|
|
173
|
+
outputSchema: Type.String({ maxLength: MAX_SYSTEM_PROMPT_CHARS }),
|
|
174
|
+
normalizeInput: (value) => String(value || ""),
|
|
175
|
+
normalizeOutput: (value) => String(value || ""),
|
|
176
|
+
resolveDefault: () => ""
|
|
177
|
+
});
|
|
178
|
+
return fields;
|
|
179
|
+
})();
|
|
180
|
+
|
|
181
|
+
const assistantWorkspaceSettingsFields = (() => {
|
|
182
|
+
const registry = createFieldRegistry("assistantWorkspaceSettingsFields");
|
|
183
|
+
const { fields, defineField } = registry;
|
|
184
|
+
defineField({
|
|
185
|
+
key: "appSurfacePrompt",
|
|
186
|
+
required: true,
|
|
187
|
+
inputSchema: createPromptSchema("App surface system prompt"),
|
|
188
|
+
outputSchema: Type.String({ maxLength: MAX_SYSTEM_PROMPT_CHARS }),
|
|
189
|
+
normalizeInput: (value) => String(value || ""),
|
|
190
|
+
normalizeOutput: (value) => String(value || ""),
|
|
191
|
+
resolveDefault: () => ""
|
|
192
|
+
});
|
|
193
|
+
return fields;
|
|
194
|
+
})();
|
|
195
|
+
|
|
196
|
+
const assistantConsoleSettingsResource = createPromptSettingsResource({
|
|
197
|
+
resourceId: "assistantConsoleSettings",
|
|
198
|
+
fields: assistantConsoleSettingsFields,
|
|
199
|
+
saveSuccessMessage: "Assistant console settings updated.",
|
|
200
|
+
saveErrorMessage: "Unable to update assistant console settings."
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const assistantWorkspaceSettingsResource = createPromptSettingsResource({
|
|
204
|
+
resourceId: "assistantWorkspaceSettings",
|
|
205
|
+
fields: assistantWorkspaceSettingsFields,
|
|
206
|
+
saveSuccessMessage: "Assistant workspace settings updated.",
|
|
207
|
+
saveErrorMessage: "Unable to update assistant workspace settings."
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
MAX_SYSTEM_PROMPT_CHARS,
|
|
212
|
+
assistantConsoleSettingsResource,
|
|
213
|
+
assistantWorkspaceSettingsResource
|
|
214
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ASSISTANT_API_RELATIVE_PATH,
|
|
3
|
+
resolveAssistantApiBasePath,
|
|
4
|
+
resolveAssistantWorkspaceApiBasePath,
|
|
5
|
+
buildAssistantWorkspaceApiPath
|
|
6
|
+
} from "./assistantPaths.js";
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
ASSISTANT_QUERY_KEY_PREFIX,
|
|
10
|
+
assistantRootQueryKey,
|
|
11
|
+
assistantWorkspaceScopeQueryKey,
|
|
12
|
+
assistantConversationsListQueryKey,
|
|
13
|
+
assistantConversationMessagesQueryKey
|
|
14
|
+
} from "./queryKeys.js";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
ASSISTANT_STREAM_EVENT_TYPES,
|
|
18
|
+
ASSISTANT_TRANSCRIPT_CHANGED_EVENT,
|
|
19
|
+
normalizeAssistantStreamEventType
|
|
20
|
+
} from "./streamEvents.js";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
MAX_INPUT_CHARS,
|
|
24
|
+
MAX_HISTORY_MESSAGES,
|
|
25
|
+
assistantResource
|
|
26
|
+
} from "./assistantResource.js";
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
MAX_SYSTEM_PROMPT_CHARS,
|
|
30
|
+
assistantConsoleSettingsResource,
|
|
31
|
+
assistantWorkspaceSettingsResource
|
|
32
|
+
} from "./assistantSettingsResource.js";
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
ASSISTANT_CONSOLE_SETTINGS_CHANGED_EVENT,
|
|
36
|
+
ASSISTANT_WORKSPACE_SETTINGS_CHANGED_EVENT
|
|
37
|
+
} from "./settingsEvents.js";
|
|
38
|
+
|
|
39
|
+
export { toPositiveInteger } from "./support/positiveInteger.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const ASSISTANT_QUERY_KEY_PREFIX = Object.freeze(["assistant"]);
|
|
2
|
+
|
|
3
|
+
function normalizeWorkspaceSlug(value) {
|
|
4
|
+
return String(value || "").trim() || "none";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function normalizePositiveInteger(value, fallback) {
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeWorkspaceScope({ workspaceSlug = "", workspaceId = 0 } = {}) {
|
|
17
|
+
const normalizedWorkspaceId = normalizePositiveInteger(workspaceId, 0);
|
|
18
|
+
if (normalizedWorkspaceId > 0) {
|
|
19
|
+
return `id:${normalizedWorkspaceId}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `slug:${normalizeWorkspaceSlug(workspaceSlug)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeStatus(value) {
|
|
26
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
27
|
+
return normalized || "all";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeConversationId(value) {
|
|
31
|
+
return String(normalizePositiveInteger(value, 0) || "none");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assistantRootQueryKey() {
|
|
35
|
+
return [...ASSISTANT_QUERY_KEY_PREFIX];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assistantWorkspaceScopeQueryKey(workspaceScope = {}) {
|
|
39
|
+
return [...assistantRootQueryKey(), normalizeWorkspaceScope(workspaceScope)];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assistantConversationsListQueryKey(workspaceScope = {}, { limit = 20, status = "" } = {}) {
|
|
43
|
+
return [
|
|
44
|
+
...assistantWorkspaceScopeQueryKey(workspaceScope),
|
|
45
|
+
"conversations",
|
|
46
|
+
"list",
|
|
47
|
+
normalizePositiveInteger(limit, 20),
|
|
48
|
+
normalizeStatus(status)
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assistantConversationMessagesQueryKey(workspaceScope = {}, conversationId, { page = 1, pageSize = 200 } = {}) {
|
|
53
|
+
return [
|
|
54
|
+
...assistantWorkspaceScopeQueryKey(workspaceScope),
|
|
55
|
+
"conversations",
|
|
56
|
+
normalizeConversationId(conversationId),
|
|
57
|
+
"messages",
|
|
58
|
+
normalizePositiveInteger(page, 1),
|
|
59
|
+
normalizePositiveInteger(pageSize, 200)
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
ASSISTANT_QUERY_KEY_PREFIX,
|
|
65
|
+
assistantRootQueryKey,
|
|
66
|
+
assistantWorkspaceScopeQueryKey,
|
|
67
|
+
assistantConversationsListQueryKey,
|
|
68
|
+
assistantConversationMessagesQueryKey
|
|
69
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const ASSISTANT_CONSOLE_SETTINGS_CHANGED_EVENT = "assistant.console.settings.changed";
|
|
2
|
+
const ASSISTANT_WORKSPACE_SETTINGS_CHANGED_EVENT = "assistant.workspace.settings.changed";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
ASSISTANT_CONSOLE_SETTINGS_CHANGED_EVENT,
|
|
6
|
+
ASSISTANT_WORKSPACE_SETTINGS_CHANGED_EVENT
|
|
7
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const ASSISTANT_STREAM_EVENT_TYPES = Object.freeze({
|
|
2
|
+
META: "meta",
|
|
3
|
+
ASSISTANT_DELTA: "assistant_delta",
|
|
4
|
+
ASSISTANT_MESSAGE: "assistant_message",
|
|
5
|
+
TOOL_CALL: "tool_call",
|
|
6
|
+
TOOL_RESULT: "tool_result",
|
|
7
|
+
ERROR: "error",
|
|
8
|
+
DONE: "done"
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const ASSISTANT_TRANSCRIPT_CHANGED_EVENT = "assistant.transcript.changed";
|
|
12
|
+
const STREAM_EVENT_TYPE_SET = new Set(Object.values(ASSISTANT_STREAM_EVENT_TYPES));
|
|
13
|
+
|
|
14
|
+
function normalizeAssistantStreamEventType(value, fallback = "") {
|
|
15
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
16
|
+
if (!normalized) {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!STREAM_EVENT_TYPE_SET.has(normalized)) {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
ASSISTANT_STREAM_EVENT_TYPES,
|
|
29
|
+
ASSISTANT_TRANSCRIPT_CHANGED_EVENT,
|
|
30
|
+
normalizeAssistantStreamEventType
|
|
31
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// JSKIT_MIGRATION_ID: assistant-settings-initial-schema
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {import('knex').Knex} knex
|
|
5
|
+
*/
|
|
6
|
+
exports.up = async function up(knex) {
|
|
7
|
+
const hasConsolePromptColumn = await knex.schema.hasColumn("console_settings", "assistant_workspace_surface_prompt");
|
|
8
|
+
if (!hasConsolePromptColumn) {
|
|
9
|
+
await knex.schema.alterTable("console_settings", (table) => {
|
|
10
|
+
table.text("assistant_workspace_surface_prompt").notNullable().defaultTo("");
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const hasWorkspacePromptColumn = await knex.schema.hasColumn("workspace_settings", "assistant_app_surface_prompt");
|
|
15
|
+
if (!hasWorkspacePromptColumn) {
|
|
16
|
+
await knex.schema.alterTable("workspace_settings", (table) => {
|
|
17
|
+
table.text("assistant_app_surface_prompt").notNullable().defaultTo("");
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {import('knex').Knex} knex
|
|
24
|
+
*/
|
|
25
|
+
exports.down = async function down(knex) {
|
|
26
|
+
const hasWorkspacePromptColumn = await knex.schema.hasColumn("workspace_settings", "assistant_app_surface_prompt");
|
|
27
|
+
if (hasWorkspacePromptColumn) {
|
|
28
|
+
await knex.schema.alterTable("workspace_settings", (table) => {
|
|
29
|
+
table.dropColumn("assistant_app_surface_prompt");
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hasConsolePromptColumn = await knex.schema.hasColumn("console_settings", "assistant_workspace_surface_prompt");
|
|
34
|
+
if (hasConsolePromptColumn) {
|
|
35
|
+
await knex.schema.alterTable("console_settings", (table) => {
|
|
36
|
+
table.dropColumn("assistant_workspace_surface_prompt");
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// JSKIT_MIGRATION_ID: assistant_transcripts_initial
|
|
2
|
+
|
|
3
|
+
exports.up = async function up(knex) {
|
|
4
|
+
const hasConversationsTable = await knex.schema.hasTable("ai_conversations");
|
|
5
|
+
if (!hasConversationsTable) {
|
|
6
|
+
await knex.schema.createTable("ai_conversations", (table) => {
|
|
7
|
+
table.increments("id").unsigned().primary();
|
|
8
|
+
table.integer("workspace_id").unsigned().notNullable().index();
|
|
9
|
+
table.integer("created_by_user_id").unsigned().nullable().index();
|
|
10
|
+
table.string("title", 160).notNullable().defaultTo("New conversation");
|
|
11
|
+
table.string("status", 32).notNullable().defaultTo("active");
|
|
12
|
+
table.string("provider", 64).notNullable().defaultTo("");
|
|
13
|
+
table.string("model", 128).notNullable().defaultTo("");
|
|
14
|
+
table.string("surface_id", 32).notNullable().defaultTo("admin");
|
|
15
|
+
table.integer("message_count").unsigned().notNullable().defaultTo(0);
|
|
16
|
+
table.text("metadata_json").nullable();
|
|
17
|
+
table.timestamp("started_at").notNullable().defaultTo(knex.fn.now());
|
|
18
|
+
table.timestamp("ended_at").nullable();
|
|
19
|
+
table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
|
|
20
|
+
table.timestamp("updated_at").notNullable().defaultTo(knex.fn.now());
|
|
21
|
+
|
|
22
|
+
table.index(["workspace_id", "started_at"], "idx_ai_conversations_workspace_started_at");
|
|
23
|
+
table.index(["workspace_id", "created_by_user_id"], "idx_ai_conversations_workspace_creator");
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const hasMessagesTable = await knex.schema.hasTable("ai_messages");
|
|
28
|
+
if (!hasMessagesTable) {
|
|
29
|
+
await knex.schema.createTable("ai_messages", (table) => {
|
|
30
|
+
table.increments("id").unsigned().primary();
|
|
31
|
+
table.integer("conversation_id").unsigned().notNullable().references("id").inTable("ai_conversations").onDelete("CASCADE");
|
|
32
|
+
table.integer("workspace_id").unsigned().notNullable().index();
|
|
33
|
+
table.integer("seq").unsigned().notNullable();
|
|
34
|
+
table.string("role", 32).notNullable();
|
|
35
|
+
table.string("kind", 32).notNullable().defaultTo("chat");
|
|
36
|
+
table.string("client_message_id", 128).notNullable().defaultTo("");
|
|
37
|
+
table.integer("actor_user_id").unsigned().nullable().index();
|
|
38
|
+
table.text("content_text").nullable();
|
|
39
|
+
table.text("metadata_json").nullable();
|
|
40
|
+
table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
|
|
41
|
+
|
|
42
|
+
table.unique(["conversation_id", "seq"], "uq_ai_messages_conversation_seq");
|
|
43
|
+
table.index(["conversation_id", "created_at"], "idx_ai_messages_conversation_created_at");
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
exports.down = async function down(knex) {
|
|
49
|
+
await knex.schema.dropTableIfExists("ai_messages");
|
|
50
|
+
await knex.schema.dropTableIfExists("ai_conversations");
|
|
51
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { normalizeOptionalHttpUrl } from "../src/server/lib/providers/common.js";
|
|
4
|
+
|
|
5
|
+
test("normalizeOptionalHttpUrl accepts empty values", () => {
|
|
6
|
+
assert.equal(normalizeOptionalHttpUrl(""), "");
|
|
7
|
+
assert.equal(normalizeOptionalHttpUrl(" "), "");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("normalizeOptionalHttpUrl rejects non-http absolute values", () => {
|
|
11
|
+
assert.throws(
|
|
12
|
+
() => normalizeOptionalHttpUrl("cd ../89", { context: "assistant AI_BASE_URL" }),
|
|
13
|
+
/assistant AI_BASE_URL must be an absolute http\(s\) URL\./
|
|
14
|
+
);
|
|
15
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createAssistantWorkspaceApi } from "../src/client/lib/assistantApi.js";
|
|
4
|
+
|
|
5
|
+
test("assistant workspace api forwards normalized surface header on requests", async () => {
|
|
6
|
+
const observed = {
|
|
7
|
+
stream: null,
|
|
8
|
+
list: null,
|
|
9
|
+
messages: null
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const api = createAssistantWorkspaceApi({
|
|
13
|
+
resolveSurfaceId: () => "AdMiN",
|
|
14
|
+
async request(url, options = {}) {
|
|
15
|
+
if (url.includes("/messages")) {
|
|
16
|
+
observed.messages = { url, options };
|
|
17
|
+
} else {
|
|
18
|
+
observed.list = { url, options };
|
|
19
|
+
}
|
|
20
|
+
return {};
|
|
21
|
+
},
|
|
22
|
+
async requestStream(url, options = {}) {
|
|
23
|
+
observed.stream = { url, options };
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await api.streamChat("acme", {
|
|
29
|
+
messageId: "msg_1",
|
|
30
|
+
input: "Hello"
|
|
31
|
+
});
|
|
32
|
+
await api.listConversations("acme", {
|
|
33
|
+
limit: 5
|
|
34
|
+
});
|
|
35
|
+
await api.getConversationMessages("acme", 99, {
|
|
36
|
+
page: 1,
|
|
37
|
+
pageSize: 5
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.equal(observed.stream?.options?.headers?.["x-jskit-surface"], "admin");
|
|
41
|
+
assert.equal(observed.list?.options?.headers?.["x-jskit-surface"], "admin");
|
|
42
|
+
assert.equal(observed.messages?.options?.headers?.["x-jskit-surface"], "admin");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("assistant workspace api omits surface header when surface id is empty", async () => {
|
|
46
|
+
const observed = [];
|
|
47
|
+
const api = createAssistantWorkspaceApi({
|
|
48
|
+
resolveSurfaceId: () => "",
|
|
49
|
+
async request(url, options = {}) {
|
|
50
|
+
observed.push({
|
|
51
|
+
url,
|
|
52
|
+
options
|
|
53
|
+
});
|
|
54
|
+
return {};
|
|
55
|
+
},
|
|
56
|
+
async requestStream(_url, _options = {}) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await api.listConversations("acme");
|
|
62
|
+
assert.equal(observed.length, 1);
|
|
63
|
+
assert.equal(Object.hasOwn(observed[0].options || {}, "headers"), false);
|
|
64
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { Check } from "typebox/value";
|
|
4
|
+
import { assistantResource } from "../src/shared/assistantResource.js";
|
|
5
|
+
|
|
6
|
+
test("assistant output schemas accept normalized paginated payloads", () => {
|
|
7
|
+
const conversationsListSchema = assistantResource.operations.conversationsList.outputValidator.schema;
|
|
8
|
+
const conversationMessagesSchema = assistantResource.operations.conversationMessagesList.outputValidator.schema;
|
|
9
|
+
|
|
10
|
+
const conversationsPayload = {
|
|
11
|
+
items: [],
|
|
12
|
+
nextCursor: null
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const messagesPayload = {
|
|
16
|
+
conversation: {
|
|
17
|
+
id: 1,
|
|
18
|
+
workspaceId: 10,
|
|
19
|
+
workspaceSlug: "acme",
|
|
20
|
+
workspaceName: "Acme",
|
|
21
|
+
title: "Conversation",
|
|
22
|
+
createdByUserId: 7,
|
|
23
|
+
createdByUserDisplayName: "Merc",
|
|
24
|
+
createdByUserEmail: "merc@example.com",
|
|
25
|
+
status: "active",
|
|
26
|
+
provider: "openai",
|
|
27
|
+
model: "gpt-4.1",
|
|
28
|
+
surfaceId: "admin",
|
|
29
|
+
startedAt: "2026-03-16T10:00:00.000Z",
|
|
30
|
+
endedAt: null,
|
|
31
|
+
messageCount: 2,
|
|
32
|
+
metadata: {},
|
|
33
|
+
createdAt: "2026-03-16T10:00:00.000Z",
|
|
34
|
+
updatedAt: "2026-03-16T10:01:00.000Z"
|
|
35
|
+
},
|
|
36
|
+
entries: [],
|
|
37
|
+
page: 1,
|
|
38
|
+
pageSize: 200,
|
|
39
|
+
total: 0,
|
|
40
|
+
totalPages: 1
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
assert.equal(Check(conversationsListSchema, conversationsPayload), true);
|
|
44
|
+
assert.equal(Check(conversationMessagesSchema, messagesPayload), true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("assistant conversation message params accept numeric path strings and normalize to integer", () => {
|
|
48
|
+
const paramsValidator = assistantResource.operations.conversationMessagesList.paramsValidator;
|
|
49
|
+
assert.equal(Check(paramsValidator.schema, { conversationId: "2" }), true);
|
|
50
|
+
assert.deepEqual(paramsValidator.normalize({ conversationId: "2" }), {
|
|
51
|
+
conversationId: 2
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { Check } from "typebox/value";
|
|
4
|
+
import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
|
|
5
|
+
import {
|
|
6
|
+
assistantConsoleSettingsResource,
|
|
7
|
+
assistantWorkspaceSettingsResource
|
|
8
|
+
} from "../src/shared/assistantSettingsResource.js";
|
|
9
|
+
|
|
10
|
+
test("assistant settings resources expose valid output schemas", () => {
|
|
11
|
+
const consoleSchema = assistantConsoleSettingsResource.operations.view.outputValidator.schema;
|
|
12
|
+
const workspaceSchema = assistantWorkspaceSettingsResource.operations.view.outputValidator.schema;
|
|
13
|
+
|
|
14
|
+
assert.equal(
|
|
15
|
+
Check(consoleSchema, {
|
|
16
|
+
settings: {
|
|
17
|
+
workspaceSurfacePrompt: ""
|
|
18
|
+
}
|
|
19
|
+
}),
|
|
20
|
+
true
|
|
21
|
+
);
|
|
22
|
+
assert.equal(
|
|
23
|
+
Check(workspaceSchema, {
|
|
24
|
+
settings: {
|
|
25
|
+
appSurfacePrompt: ""
|
|
26
|
+
}
|
|
27
|
+
}),
|
|
28
|
+
true
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("assistant settings patch normalizer preserves omitted fields", () => {
|
|
33
|
+
const consolePatch = validateOperationSection({
|
|
34
|
+
operation: assistantConsoleSettingsResource.operations.patch,
|
|
35
|
+
section: "bodyValidator",
|
|
36
|
+
value: {}
|
|
37
|
+
});
|
|
38
|
+
const workspacePatch = validateOperationSection({
|
|
39
|
+
operation: assistantWorkspaceSettingsResource.operations.patch,
|
|
40
|
+
section: "bodyValidator",
|
|
41
|
+
value: {}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
assert.equal(consolePatch.ok, true);
|
|
45
|
+
assert.equal(workspacePatch.ok, true);
|
|
46
|
+
assert.deepEqual(consolePatch.value, {});
|
|
47
|
+
assert.deepEqual(workspacePatch.value, {});
|
|
48
|
+
});
|