@jskit-ai/assistant-runtime 0.2.8 → 0.2.10
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/README.md +1 -1
- package/fixtures/canonical-conversation/main.js +2 -1
- package/fixtures/canonical-conversation/vite.config.mjs +11 -0
- package/fixtures/conversation/main.js +2 -1
- package/fixtures/conversation/vite.config.mjs +4 -3
- package/locales/en.json +1 -0
- package/migrations/assistant_config_initial.cjs +1 -5
- package/migrations/assistant_transcripts_initial.cjs +3 -9
- package/migrations/assistant_transcripts_summary.cjs +28 -0
- package/migrations/assistant_turn_requests.cjs +2 -1
- package/migrations/constraints/assistant_users_foreign_keys.cjs +22 -0
- package/migrations/constraints/assistant_workspaces_foreign_keys.cjs +26 -0
- package/package.json +17 -10
- package/patterns/assistant-surface/example/src/ApplicationAiFeature.js +4 -2
- package/src/client/components/AssistantConversationClientElement.vue +10 -4
- package/src/client/components/AssistantSurfaceClientElement.vue +8 -4
- package/src/client/composables/conversationGoal.js +266 -0
- package/src/client/composables/conversationHistory.js +215 -0
- package/src/client/composables/conversationRuntime.js +624 -0
- package/src/client/composables/useAssistantConversation.js +208 -840
- package/src/client/composables/useAssistantRuntime.js +60 -45
- package/src/client/providers/AssistantClientProvider.js +1 -0
- package/src/client/support/assistantRuntimeState.js +6 -34
- package/src/client/support/conversationFailures.js +30 -0
- package/src/client/support/conversationRestoreSupport.js +13 -5
- package/src/client/support/subscribeAssistantConversation.js +32 -9
- package/src/client/support/workspaceScopeSupport.js +13 -22
- package/src/client/types.js +46 -0
- package/src/server/AssistantProvider.js +45 -17
- package/src/server/actions.js +162 -201
- package/src/server/conversationOperations.js +141 -0
- package/src/server/createAssistantRuntime.js +83 -44
- package/src/server/createAssistantSettingsActions.js +69 -47
- package/src/server/hostedAssistant.js +21 -0
- package/src/server/index.js +1 -0
- package/src/server/registerConversationSubscriptions.js +34 -11
- package/src/server/registerFastifyConversations.js +18 -6
- package/src/server/registerRoutes.js +5 -2
- package/src/server/registerSettingsRoutes.js +1 -1
- package/src/server/repositories/assistantConfigRepository.js +10 -20
- package/src/server/repositories/conversationsRepository.js +35 -10
- package/src/server/repositories/messagesRepository.js +14 -10
- package/src/server/repositories/repositoryPersistenceUtils.js +1 -6
- package/src/server/repositories/turnRequestsRepository.js +6 -1
- package/src/server/services/assistantConfigService.js +25 -23
- package/src/server/services/chatService.js +57 -61
- package/src/server/services/transcriptService.js +15 -7
- package/src/server/sqlConversationStorage.js +90 -18
- package/src/server/support/assistantServerConfig.js +1 -1
- package/src/server/support/assistantStreamWriter.js +7 -1
- package/src/server/support/createSurfaceAwareToolCatalog.js +35 -17
- package/src/server/types.js +38 -0
- package/src/shared/assistantSurfaces.js +2 -2
- package/test/assistantConversationBinding.browser.test.js +115 -1
- package/test/assistantConversationOperations.test.js +5 -5
- package/test/assistantRuntimeState.test.js +1 -16
- package/test/chatServiceLifecycle.test.js +62 -154
- package/test/createSurfaceAwareToolCatalog.test.js +5 -0
- package/test/featureRuntime.test.js +12 -6
- package/test/lazyAppConfig.test.js +3 -5
- package/test/packageMetadata.test.js +7 -1
- package/test/sqlConversationStorage.mysql.test.js +89 -0
- package/test/sqlConversationStorage.test.js +161 -16
- package/test/support/memoryAssistantRows.js +6 -1
- package/test/support/scriptedModel.js +32 -0
- package/test/turnRequests.test.js +4 -2
- package/test/usersForeignKeysMigration.test.js +46 -0
- package/test/workspacesForeignKeysMigration.test.js +66 -0
- package/src/client/support/accessDenied.js +0 -6
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
applyWorkspaceScope,
|
|
6
6
|
normalizeDbWorkspaceId,
|
|
7
7
|
normalizeInputRecordId,
|
|
8
|
-
normalizeWorkspaceId,
|
|
9
8
|
parseJsonObject,
|
|
10
9
|
resolveInsertedId,
|
|
11
10
|
stringifyJsonObject,
|
|
@@ -58,10 +57,8 @@ function createConversationBaseQuery(client) {
|
|
|
58
57
|
return client(`${assistantRuntimeConfig.conversationsTable} as c`).select("c.*");
|
|
59
58
|
}
|
|
60
59
|
|
|
60
|
+
/** @param {import("@jskit-ai/database-runtime/server/types").RuntimeKnex} knex */
|
|
61
61
|
function createRepository(knex) {
|
|
62
|
-
if (!knex || typeof knex !== "function") {
|
|
63
|
-
throw new Error("createConversationsRepository requires knex client.");
|
|
64
|
-
}
|
|
65
62
|
const withTransaction = createWithTransaction(knex);
|
|
66
63
|
|
|
67
64
|
async function findById(conversationId, options = {}) {
|
|
@@ -70,7 +67,7 @@ function createRepository(knex) {
|
|
|
70
67
|
return null;
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
const client = options
|
|
70
|
+
const client = options.trx || knex;
|
|
74
71
|
if (options.forUpdate === true) {
|
|
75
72
|
if (!options.trx) throw new TypeError("Locking a conversation requires its database transaction.");
|
|
76
73
|
// Acquire the write reservation before reading, including SQLite where
|
|
@@ -96,7 +93,7 @@ function createRepository(knex) {
|
|
|
96
93
|
return null;
|
|
97
94
|
}
|
|
98
95
|
|
|
99
|
-
const client = options
|
|
96
|
+
const client = options.trx || knex;
|
|
100
97
|
if (options.forUpdate === true) await findById(normalizedConversationId, options);
|
|
101
98
|
const query = createConversationBaseQuery(client)
|
|
102
99
|
.where("c.id", normalizedConversationId)
|
|
@@ -109,11 +106,11 @@ function createRepository(knex) {
|
|
|
109
106
|
}
|
|
110
107
|
|
|
111
108
|
async function create(payload = {}, options = {}) {
|
|
112
|
-
const client = options
|
|
109
|
+
const client = options.trx || knex;
|
|
113
110
|
const now = new Date();
|
|
114
111
|
const surfaceId = normalizeRequiredSurfaceId(payload.surfaceId);
|
|
115
112
|
const insertResult = await client(assistantRuntimeConfig.conversationsTable).insert({
|
|
116
|
-
workspace_id:
|
|
113
|
+
workspace_id: normalizeInputRecordId(payload.workspaceId),
|
|
117
114
|
created_by_user_id: normalizeInputRecordId(payload.createdByUserId),
|
|
118
115
|
title: normalizeText(payload.title) || DEFAULT_CONVERSATION_TITLE,
|
|
119
116
|
status: normalizeText(payload.status).toLowerCase() || "active",
|
|
@@ -143,7 +140,7 @@ function createRepository(knex) {
|
|
|
143
140
|
return null;
|
|
144
141
|
}
|
|
145
142
|
|
|
146
|
-
const client = options
|
|
143
|
+
const client = options.trx || knex;
|
|
147
144
|
const updatePatch = {};
|
|
148
145
|
|
|
149
146
|
if (Object.hasOwn(patch, "title")) {
|
|
@@ -183,6 +180,32 @@ function createRepository(knex) {
|
|
|
183
180
|
});
|
|
184
181
|
}
|
|
185
182
|
|
|
183
|
+
// The transcript's own state stays out of conversation records, which reach the browser.
|
|
184
|
+
async function readTranscriptState(conversationId, options = {}) {
|
|
185
|
+
const id = normalizeInputRecordId(conversationId);
|
|
186
|
+
if (!id) return null;
|
|
187
|
+
const client = options.trx || knex;
|
|
188
|
+
const row = await client(assistantRuntimeConfig.conversationsTable).where({ id })
|
|
189
|
+
.first("summary_text", "summary_through_turn_id", "transcript_revision");
|
|
190
|
+
if (!row) return null;
|
|
191
|
+
return {
|
|
192
|
+
revision: row.transcript_revision || "",
|
|
193
|
+
summary: row.summary_text == null ? null
|
|
194
|
+
: { text: String(row.summary_text), throughTurnId: String(row.summary_through_turn_id || "") }
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function writeTranscriptState(conversationId, { revision, summary }, options = {}) {
|
|
199
|
+
const id = normalizeInputRecordId(conversationId);
|
|
200
|
+
if (!id) throw new TypeError("writeTranscriptState requires conversationId.");
|
|
201
|
+
const client = options.trx || knex;
|
|
202
|
+
await client(assistantRuntimeConfig.conversationsTable).where({ id }).update({
|
|
203
|
+
transcript_revision: revision,
|
|
204
|
+
summary_text: summary ? summary.text : null,
|
|
205
|
+
summary_through_turn_id: summary ? summary.throughTurnId : null
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
186
209
|
async function listForActorScope(
|
|
187
210
|
{ workspaceId = null, actorUserId = null, surfaceId = "", pagination = {}, filters = {} } = {},
|
|
188
211
|
options = {}
|
|
@@ -196,7 +219,7 @@ function createRepository(knex) {
|
|
|
196
219
|
};
|
|
197
220
|
}
|
|
198
221
|
|
|
199
|
-
const client = options
|
|
222
|
+
const client = options.trx || knex;
|
|
200
223
|
const { cursor, limit } = normalizeCursorPagination(pagination);
|
|
201
224
|
let query = createConversationBaseQuery(client)
|
|
202
225
|
.where("c.created_by_user_id", normalizedActorUserId)
|
|
@@ -239,6 +262,8 @@ function createRepository(knex) {
|
|
|
239
262
|
create,
|
|
240
263
|
updateById,
|
|
241
264
|
listForActorScope,
|
|
265
|
+
readTranscriptState,
|
|
266
|
+
writeTranscriptState,
|
|
242
267
|
transaction
|
|
243
268
|
});
|
|
244
269
|
}
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
applyWorkspaceScope,
|
|
5
5
|
normalizeDbWorkspaceId,
|
|
6
6
|
normalizeInputRecordId,
|
|
7
|
-
normalizeWorkspaceId,
|
|
8
7
|
parseJsonObject,
|
|
9
8
|
resolveInsertedId,
|
|
10
9
|
stringifyJsonObject,
|
|
@@ -55,10 +54,8 @@ async function resolveNextSequence(client, conversationId) {
|
|
|
55
54
|
return maxSeq + 1;
|
|
56
55
|
}
|
|
57
56
|
|
|
57
|
+
/** @param {import("@jskit-ai/database-runtime/server/types").RuntimeKnex} knex */
|
|
58
58
|
function createRepository(knex) {
|
|
59
|
-
if (!knex || typeof knex !== "function") {
|
|
60
|
-
throw new Error("createMessagesRepository requires knex client.");
|
|
61
|
-
}
|
|
62
59
|
const withTransaction = createWithTransaction(knex);
|
|
63
60
|
|
|
64
61
|
async function findById(messageId, options = {}) {
|
|
@@ -67,7 +64,7 @@ function createRepository(knex) {
|
|
|
67
64
|
return null;
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
const client = options
|
|
67
|
+
const client = options.trx || knex;
|
|
71
68
|
const row = await client(assistantRuntimeConfig.messagesTable)
|
|
72
69
|
.where({ id: normalizedMessageId })
|
|
73
70
|
.first();
|
|
@@ -76,7 +73,7 @@ function createRepository(knex) {
|
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
async function create(payload = {}, options = {}) {
|
|
79
|
-
const client = options
|
|
76
|
+
const client = options.trx || knex;
|
|
80
77
|
const conversationId = normalizeInputRecordId(payload.conversationId);
|
|
81
78
|
if (!conversationId) {
|
|
82
79
|
throw new TypeError("messagesRepository.create requires conversationId.");
|
|
@@ -86,7 +83,7 @@ function createRepository(knex) {
|
|
|
86
83
|
const seq = Number.isInteger(providedSeq) && providedSeq > 0 ? providedSeq : (await resolveNextSequence(client, conversationId));
|
|
87
84
|
const insertResult = await client(assistantRuntimeConfig.messagesTable).insert({
|
|
88
85
|
conversation_id: conversationId,
|
|
89
|
-
workspace_id:
|
|
86
|
+
workspace_id: normalizeInputRecordId(payload.workspaceId),
|
|
90
87
|
seq,
|
|
91
88
|
role: normalizeText(payload.role).toLowerCase(),
|
|
92
89
|
kind: normalizeText(payload.kind).toLowerCase() || "chat",
|
|
@@ -112,7 +109,7 @@ function createRepository(knex) {
|
|
|
112
109
|
return 0;
|
|
113
110
|
}
|
|
114
111
|
|
|
115
|
-
const client = options
|
|
112
|
+
const client = options.trx || knex;
|
|
116
113
|
const query = client(assistantRuntimeConfig.messagesTable).where({
|
|
117
114
|
conversation_id: normalizedConversationId
|
|
118
115
|
});
|
|
@@ -126,7 +123,7 @@ function createRepository(knex) {
|
|
|
126
123
|
async function updateById(messageId, patch = {}, options = {}) {
|
|
127
124
|
const id = normalizeInputRecordId(messageId);
|
|
128
125
|
if (!id) return null;
|
|
129
|
-
const client = options
|
|
126
|
+
const client = options.trx || knex;
|
|
130
127
|
const update = {};
|
|
131
128
|
if (Object.hasOwn(patch, "contentText")) update.content_text = patch.contentText == null ? null : String(patch.contentText);
|
|
132
129
|
if (Object.hasOwn(patch, "metadata")) update.metadata_json = stringifyJsonObject(patch.metadata);
|
|
@@ -140,7 +137,7 @@ function createRepository(knex) {
|
|
|
140
137
|
return [];
|
|
141
138
|
}
|
|
142
139
|
|
|
143
|
-
const client = options
|
|
140
|
+
const client = options.trx || knex;
|
|
144
141
|
const { page, pageSize } = normalizePagination(pagination);
|
|
145
142
|
const offset = (page - 1) * pageSize;
|
|
146
143
|
|
|
@@ -157,6 +154,12 @@ function createRepository(knex) {
|
|
|
157
154
|
return rows.map(mapMessageRow);
|
|
158
155
|
}
|
|
159
156
|
|
|
157
|
+
async function lastSequence(conversationId, options = {}) {
|
|
158
|
+
const normalizedConversationId = normalizeInputRecordId(conversationId);
|
|
159
|
+
if (!normalizedConversationId) return 0;
|
|
160
|
+
return (await resolveNextSequence(options.trx || knex, normalizedConversationId)) - 1;
|
|
161
|
+
}
|
|
162
|
+
|
|
160
163
|
async function transaction(callback) {
|
|
161
164
|
return runInTransaction(knex, callback);
|
|
162
165
|
}
|
|
@@ -168,6 +171,7 @@ function createRepository(knex) {
|
|
|
168
171
|
updateById,
|
|
169
172
|
countByConversationScope,
|
|
170
173
|
listByConversationScope,
|
|
174
|
+
lastSequence,
|
|
171
175
|
transaction
|
|
172
176
|
});
|
|
173
177
|
}
|
|
@@ -3,10 +3,6 @@ import { normalizeDbRecordId } from "@jskit-ai/database-runtime/shared/repositor
|
|
|
3
3
|
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
4
|
import { parseJsonObject } from "@jskit-ai/assistant-core/shared";
|
|
5
5
|
|
|
6
|
-
function normalizeWorkspaceId(value) {
|
|
7
|
-
return normalizeRecordId(value, { fallback: null });
|
|
8
|
-
}
|
|
9
|
-
|
|
10
6
|
function normalizeDbWorkspaceId(value) {
|
|
11
7
|
return normalizeDbRecordId(value, { fallback: null });
|
|
12
8
|
}
|
|
@@ -16,7 +12,7 @@ function normalizeInputRecordId(value) {
|
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
function applyWorkspaceScope(query, columnName, workspaceId) {
|
|
19
|
-
const normalizedWorkspaceId =
|
|
15
|
+
const normalizedWorkspaceId = normalizeInputRecordId(workspaceId);
|
|
20
16
|
if (normalizedWorkspaceId) {
|
|
21
17
|
return query.where(columnName, normalizedWorkspaceId);
|
|
22
18
|
}
|
|
@@ -54,7 +50,6 @@ export {
|
|
|
54
50
|
applyWorkspaceScope,
|
|
55
51
|
normalizeDbWorkspaceId,
|
|
56
52
|
normalizeInputRecordId,
|
|
57
|
-
normalizeWorkspaceId,
|
|
58
53
|
parseJsonObject,
|
|
59
54
|
stringifyJsonObject,
|
|
60
55
|
toIso,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
|
|
3
3
|
|
|
4
|
+
/** @param {import("@jskit-ai/database-runtime/server/types").RuntimeKnex} knex */
|
|
4
5
|
function createRepository(knex) {
|
|
5
6
|
const table = assistantRuntimeConfig.turnRequestsTable;
|
|
6
7
|
|
|
@@ -29,7 +30,11 @@ function createRepository(knex) {
|
|
|
29
30
|
if (!changed) throw new Error("Assistant request ownership was lost.");
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
return Object.freeze({
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
find,
|
|
35
|
+
claim,
|
|
36
|
+
update
|
|
37
|
+
});
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export { createRepository };
|
|
@@ -2,22 +2,24 @@ import { AppError, requireAuth } from "@jskit-ai/kernel/server/runtime";
|
|
|
2
2
|
import { normalizeObject, normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
3
|
import { requireAssistantSurface } from "../support/assistantSurface.js";
|
|
4
4
|
|
|
5
|
+
/** @typedef {import("../types.js").AssistantConfigRepository} AssistantConfigRepository */
|
|
6
|
+
/** @typedef {import("../types.js").WorkspaceScopeSupport} WorkspaceScopeSupport */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* An assistant surface's saved settings: who may read and change them, and the
|
|
10
|
+
* system prompt they give the surface's conversations.
|
|
11
|
+
* @param {object} dependencies
|
|
12
|
+
* @param {AssistantConfigRepository} dependencies.assistantConfigRepository
|
|
13
|
+
* @param {import("@jskit-ai/console-core/server/types").ConsoleService | null} dependencies.consoleService The console access service, when the console is installed.
|
|
14
|
+
* @param {Record<string, any>} dependencies.appConfig
|
|
15
|
+
* @param {WorkspaceScopeSupport | null} dependencies.workspaceScopeSupport
|
|
16
|
+
*/
|
|
5
17
|
function createService({
|
|
6
18
|
assistantConfigRepository,
|
|
7
|
-
consoleService
|
|
8
|
-
appConfig
|
|
9
|
-
workspaceScopeSupport
|
|
10
|
-
}
|
|
11
|
-
if (!assistantConfigRepository || typeof assistantConfigRepository.findByScope !== "function") {
|
|
12
|
-
throw new Error("assistantConfigService requires assistantConfigRepository.findByScope().");
|
|
13
|
-
}
|
|
14
|
-
if (typeof assistantConfigRepository.upsertByScope !== "function") {
|
|
15
|
-
throw new Error("assistantConfigService requires assistantConfigRepository.upsertByScope().");
|
|
16
|
-
}
|
|
17
|
-
if (typeof assistantConfigRepository.createDefaultRecord !== "function") {
|
|
18
|
-
throw new Error("assistantConfigService requires assistantConfigRepository.createDefaultRecord().");
|
|
19
|
-
}
|
|
20
|
-
|
|
19
|
+
consoleService,
|
|
20
|
+
appConfig,
|
|
21
|
+
workspaceScopeSupport
|
|
22
|
+
}) {
|
|
21
23
|
async function requireSettingsAccess(assistantSurface, context = {}, { mode = "read" } = {}) {
|
|
22
24
|
requireAuth({ context }, {
|
|
23
25
|
require: "authenticated"
|
|
@@ -43,7 +45,7 @@ function createService({
|
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
if (!consoleService
|
|
48
|
+
if (!consoleService) {
|
|
47
49
|
throw new Error("assistantConfigService requires consoleService.requireConsoleOwner() for console-owner settings surfaces.");
|
|
48
50
|
}
|
|
49
51
|
|
|
@@ -55,7 +57,7 @@ function createService({
|
|
|
55
57
|
return null;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
if (!workspaceScopeSupport
|
|
60
|
+
if (!workspaceScopeSupport) {
|
|
59
61
|
throw new Error("assistant.config.service requires workspace server scope support for workspace-scoped settings.");
|
|
60
62
|
}
|
|
61
63
|
|
|
@@ -69,12 +71,12 @@ function createService({
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
async function getSettings(input = {}, options = {}) {
|
|
72
|
-
const assistantSurface = requireAssistantSurface(appConfig, input
|
|
73
|
-
await requireSettingsAccess(assistantSurface, options
|
|
74
|
+
const assistantSurface = requireAssistantSurface(appConfig, input.targetSurfaceId);
|
|
75
|
+
await requireSettingsAccess(assistantSurface, options.context, {
|
|
74
76
|
mode: "read"
|
|
75
77
|
});
|
|
76
78
|
|
|
77
|
-
const workspaceId = resolveConfigWorkspaceId(assistantSurface, null, input, options
|
|
79
|
+
const workspaceId = resolveConfigWorkspaceId(assistantSurface, null, input, options.context);
|
|
78
80
|
const existing = await assistantConfigRepository.findByScope({
|
|
79
81
|
targetSurfaceId: assistantSurface.targetSurfaceId,
|
|
80
82
|
workspaceId
|
|
@@ -87,12 +89,12 @@ function createService({
|
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
async function updateSettings(input = {}, patch = {}, options = {}) {
|
|
90
|
-
const assistantSurface = requireAssistantSurface(appConfig, input
|
|
91
|
-
await requireSettingsAccess(assistantSurface, options
|
|
92
|
+
const assistantSurface = requireAssistantSurface(appConfig, input.targetSurfaceId);
|
|
93
|
+
await requireSettingsAccess(assistantSurface, options.context, {
|
|
92
94
|
mode: "write"
|
|
93
95
|
});
|
|
94
96
|
|
|
95
|
-
const workspaceId = resolveConfigWorkspaceId(assistantSurface, null, input, options
|
|
97
|
+
const workspaceId = resolveConfigWorkspaceId(assistantSurface, null, input, options.context);
|
|
96
98
|
const normalizedPatch = normalizeObject(patch);
|
|
97
99
|
|
|
98
100
|
return assistantConfigRepository.upsertByScope({
|
|
@@ -102,7 +104,7 @@ function createService({
|
|
|
102
104
|
});
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
async function resolveSystemPrompt(assistantSurface, workspace = null, _options = {}, serviceOptions
|
|
107
|
+
async function resolveSystemPrompt(assistantSurface, workspace = null, _options = {}, serviceOptions) {
|
|
106
108
|
const resolvedAssistantSurface = requireAssistantSurface(appConfig, assistantSurface?.targetSurfaceId);
|
|
107
109
|
const workspaceId = resolveConfigWorkspaceId(
|
|
108
110
|
resolvedAssistantSurface,
|
|
@@ -1,43 +1,14 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { resolveActor } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
|
|
2
3
|
import { AppError, createValidationError } from "@jskit-ai/kernel/server/runtime";
|
|
3
4
|
import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
5
|
import { resolveWorkspaceSlug } from "@jskit-ai/assistant-core/server";
|
|
5
6
|
import { conversationAttachmentIds } from "@jskit-ai/assistant-core/server/conversation";
|
|
6
|
-
import { ASSISTANT_STREAM_EVENT_TYPES,
|
|
7
|
+
import { ASSISTANT_STREAM_EVENT_TYPES, MAX_INPUT_CHARS } from "@jskit-ai/assistant-core/shared";
|
|
7
8
|
import { errorMessageFields, exposedAppErrorDetails, errorStatus } from "../support/assistantErrors.js";
|
|
8
9
|
import { requireAssistantSurface } from "../support/assistantSurface.js";
|
|
9
|
-
import { ASSISTANT_STREAM_WRITER_METHODS } from "../support/assistantStreamWriter.js";
|
|
10
10
|
|
|
11
|
-
const CLOCK_INSTRUCTION = "For current or relative date and time questions,
|
|
12
|
-
|
|
13
|
-
function normalizeConversationId(value) {
|
|
14
|
-
return normalizeRecordId(value, { fallback: null });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizeHistory(history = []) {
|
|
18
|
-
const source = Array.isArray(history) ? history : [];
|
|
19
|
-
return source
|
|
20
|
-
.slice(0, MAX_HISTORY_MESSAGES)
|
|
21
|
-
.map((entry) => {
|
|
22
|
-
const item = normalizeObject(entry);
|
|
23
|
-
const role = normalizeText(item.role).toLowerCase();
|
|
24
|
-
if (role !== "user" && role !== "assistant") {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const content = normalizeText(item.content).slice(0, MAX_INPUT_CHARS);
|
|
29
|
-
if (!content || (role === "assistant" && isAssistantProgressOnlyText(content))) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
role,
|
|
35
|
-
content,
|
|
36
|
-
attachmentIds: role === "user" ? item.attachmentIds : []
|
|
37
|
-
};
|
|
38
|
-
})
|
|
39
|
-
.filter(Boolean);
|
|
40
|
-
}
|
|
11
|
+
const CLOCK_INSTRUCTION = "For current or relative date and time questions, use the current time in the system's state note, or an available authoritative workspace clock action for another time zone; never infer the current date or time from model knowledge.";
|
|
41
12
|
|
|
42
13
|
function normalizeStreamInput(payload = {}) {
|
|
43
14
|
const source = normalizeObject(payload);
|
|
@@ -56,17 +27,12 @@ function normalizeStreamInput(payload = {}) {
|
|
|
56
27
|
|
|
57
28
|
return {
|
|
58
29
|
messageId,
|
|
59
|
-
conversationId:
|
|
30
|
+
conversationId: normalizeRecordId(source.conversationId, { fallback: null }),
|
|
60
31
|
input,
|
|
61
|
-
attachmentIds: source.attachmentIds
|
|
62
|
-
history: normalizeHistory(source.history)
|
|
32
|
+
attachmentIds: source.attachmentIds
|
|
63
33
|
};
|
|
64
34
|
}
|
|
65
35
|
|
|
66
|
-
function hasStreamWriter(streamWriter) {
|
|
67
|
-
return ASSISTANT_STREAM_WRITER_METHODS.every(name => typeof streamWriter?.[name] === "function");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
36
|
// A saved failure keeps its translation fields, except where the kernel would
|
|
71
37
|
// withhold them, so a replay shows the same message in the user's language.
|
|
72
38
|
function savedMessageFields(error) {
|
|
@@ -110,7 +76,12 @@ function buildToolContractLine(toolDescriptor = {}) {
|
|
|
110
76
|
return `${name}: input=${toCompactJson(inputSchema)} output=${toCompactJson(outputSchema, "null")}`;
|
|
111
77
|
}
|
|
112
78
|
|
|
113
|
-
|
|
79
|
+
/**
|
|
80
|
+
* The system prompt of a surface's turn: the surface, its workspace, the tools it
|
|
81
|
+
* offers and the surface's own instructions.
|
|
82
|
+
* @param {{ targetSurfaceId?: string, toolDescriptors: Array<{ name: string, parameters?: unknown, outputSchema?: unknown }>, workspaceSlug?: string, customSystemPrompt?: string }} prompt
|
|
83
|
+
*/
|
|
84
|
+
function buildSystemPrompt({ targetSurfaceId = "", toolDescriptors, workspaceSlug = "", customSystemPrompt = "" }) {
|
|
114
85
|
const toolSummary = toolDescriptors.length > 0
|
|
115
86
|
? `Available tools: ${toolDescriptors.map((entry) => entry.name).join(", ")}.`
|
|
116
87
|
: "No tools are currently available for this user/session.";
|
|
@@ -161,6 +132,29 @@ function buildAssistantActionContext(context = {}, assistantSurface = {}) {
|
|
|
161
132
|
};
|
|
162
133
|
}
|
|
163
134
|
|
|
135
|
+
/** @typedef {import("../types.js").AssistantAiClientFactory} AssistantAiClientFactory */
|
|
136
|
+
/** @typedef {import("../types.js").AssistantAttachments} AssistantAttachments */
|
|
137
|
+
/** @typedef {import("../types.js").AssistantConfigService} AssistantConfigService */
|
|
138
|
+
/** @typedef {import("../types.js").ConversationRuntime} ConversationRuntime */
|
|
139
|
+
/** @typedef {import("../types.js").SurfaceAwareToolCatalog} SurfaceAwareToolCatalog */
|
|
140
|
+
/** @typedef {import("../types.js").TranscriptService} TranscriptService */
|
|
141
|
+
/** @typedef {import("../types.js").TurnRequestsRepository} TurnRequestsRepository */
|
|
142
|
+
/** @typedef {import("../types.js").WorkspaceScopeSupport} WorkspaceScopeSupport */
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The chat stream and transcripts of the package's own tables. Each chat turn is
|
|
146
|
+
* recorded once per message id, so a repeated request replays its saved outcome.
|
|
147
|
+
* @param {object} dependencies
|
|
148
|
+
* @param {ConversationRuntime} dependencies.conversationRuntime
|
|
149
|
+
* @param {AssistantAiClientFactory} dependencies.aiClientFactory
|
|
150
|
+
* @param {TurnRequestsRepository} dependencies.turnRequests
|
|
151
|
+
* @param {AssistantAttachments | null} [dependencies.attachments]
|
|
152
|
+
* @param {TranscriptService} dependencies.transcriptService
|
|
153
|
+
* @param {SurfaceAwareToolCatalog} dependencies.serviceToolCatalog
|
|
154
|
+
* @param {AssistantConfigService} dependencies.assistantConfigService
|
|
155
|
+
* @param {Record<string, any>} dependencies.appConfig
|
|
156
|
+
* @param {WorkspaceScopeSupport | null} [dependencies.workspaceScopeSupport]
|
|
157
|
+
*/
|
|
164
158
|
function createChatService({
|
|
165
159
|
conversationRuntime,
|
|
166
160
|
aiClientFactory,
|
|
@@ -169,21 +163,15 @@ function createChatService({
|
|
|
169
163
|
transcriptService,
|
|
170
164
|
serviceToolCatalog,
|
|
171
165
|
assistantConfigService,
|
|
172
|
-
appConfig
|
|
173
|
-
workspaceScopeSupport
|
|
174
|
-
}
|
|
175
|
-
if (!aiClientFactory || typeof aiClientFactory.resolveClient !== "function" || !turnRequests || !transcriptService || !serviceToolCatalog || !assistantConfigService) {
|
|
176
|
-
throw new Error(
|
|
177
|
-
"createChatService requires aiClientFactory.resolveClient(), turnRequests, transcriptService, serviceToolCatalog, and assistantConfigService."
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
166
|
+
appConfig,
|
|
167
|
+
workspaceScopeSupport
|
|
168
|
+
}) {
|
|
181
169
|
function resolveRuntimeWorkspace(assistantSurface, context = {}, input = {}) {
|
|
182
170
|
if (assistantSurface?.runtimeSurfaceRequiresWorkspace !== true) {
|
|
183
171
|
return null;
|
|
184
172
|
}
|
|
185
173
|
|
|
186
|
-
if (!workspaceScopeSupport
|
|
174
|
+
if (!workspaceScopeSupport) {
|
|
187
175
|
throw new Error("assistant.chat.service requires workspace server scope support for workspace-scoped assistant surfaces.");
|
|
188
176
|
}
|
|
189
177
|
|
|
@@ -243,17 +231,23 @@ function createChatService({
|
|
|
243
231
|
await turns.releasing;
|
|
244
232
|
}
|
|
245
233
|
|
|
246
|
-
|
|
247
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Streams one chat turn through its writer, or replays the saved turn with the same message id.
|
|
236
|
+
* @param {object} payload The validated chat stream input.
|
|
237
|
+
* @param {{ context: object, streamWriter: import("../support/assistantStreamWriter.js").AssistantStreamWriter,
|
|
238
|
+
* abortSignal?: AbortSignal }} options
|
|
239
|
+
*/
|
|
240
|
+
async function streamChat(payload, options) {
|
|
241
|
+
const assistantSurface = requireAssistantSurface(appConfig, payload.targetSurfaceId);
|
|
248
242
|
const context = normalizeObject(options.context);
|
|
249
243
|
const assistantContext = buildAssistantActionContext(context, assistantSurface);
|
|
250
244
|
const workspace = resolveRuntimeWorkspace(assistantSurface, assistantContext, payload);
|
|
251
245
|
const source = normalizeStreamInput(payload);
|
|
252
|
-
const actor = context
|
|
246
|
+
const actor = resolveActor(context);
|
|
253
247
|
const actorUserId = normalizeRecordId(actor?.id, { fallback: null });
|
|
254
248
|
if (!actorUserId) throw new AppError(401, "Authentication required.", { code: "assistant.runtime.authenticationRequired" });
|
|
255
|
-
if (!hasStreamWriter(options.streamWriter)) throw new Error("assistant.chat.stream requires streamWriter methods.");
|
|
256
249
|
const scope = { actorUserId, surfaceId: assistantSurface.targetSurfaceId,
|
|
250
|
+
// readable-check: allow no-dual-shape - data: the workspace scope resolves the workspace from the action input or context, which may carry the record or its id
|
|
257
251
|
workspaceId: normalizeRecordId(workspace?.id || workspace, { fallback: null }) };
|
|
258
252
|
const request = { ...source, integrationId: normalizeText(payload.integrationId) };
|
|
259
253
|
const existing = await turnRequests.find(scope, source.messageId);
|
|
@@ -296,7 +290,6 @@ function createChatService({
|
|
|
296
290
|
return result;
|
|
297
291
|
|
|
298
292
|
async function executeTurn() {
|
|
299
|
-
if (typeof conversationRuntime?.open !== "function") throw new TypeError("Assistant streaming requires its common conversation runtime.");
|
|
300
293
|
const conversationResult = await transcriptService.createConversationForTurn(
|
|
301
294
|
assistantSurface,
|
|
302
295
|
workspace,
|
|
@@ -329,13 +322,12 @@ function createChatService({
|
|
|
329
322
|
code: "assistant.runtime.attachments.notConfigured"
|
|
330
323
|
});
|
|
331
324
|
}
|
|
332
|
-
//
|
|
325
|
+
// Attachments are authorized before admission, as the facade orders it.
|
|
333
326
|
// Only these server-resolved materials reach the common file reader.
|
|
334
327
|
const files = await attachments.resolve({ attachmentIds, context: assistantContext, workspace, conversation });
|
|
335
328
|
resolvedAttachments.push({ attachmentIds: [...attachmentIds], files });
|
|
336
329
|
}
|
|
337
330
|
await prepareAttachments(source.attachmentIds);
|
|
338
|
-
for (const message of source.history) await prepareAttachments(message.attachmentIds);
|
|
339
331
|
const toolSet = serviceToolCatalog.resolveToolSet(assistantContext);
|
|
340
332
|
const customSystemPrompt = await assistantConfigService.resolveSystemPrompt(
|
|
341
333
|
assistantSurface, workspace, { surface: assistantSurface.targetSurfaceId },
|
|
@@ -346,7 +338,7 @@ function createChatService({
|
|
|
346
338
|
workspaceSlug: resolveWorkspaceSlug(assistantContext, payload), customSystemPrompt
|
|
347
339
|
}), ...(request.integrationId ? { integrationId: request.integrationId } : {}) };
|
|
348
340
|
const runtimeContext = { ...assistantContext, assistantRequest: {
|
|
349
|
-
conversation, workspace,
|
|
341
|
+
conversation, workspace, resolvedAttachments
|
|
350
342
|
} };
|
|
351
343
|
const handle = await openConversationTurn(conversationId, async () => {
|
|
352
344
|
try { return await conversationRuntime.open({ id: conversationId, context: runtimeContext, configuration }); }
|
|
@@ -438,7 +430,7 @@ function createChatService({
|
|
|
438
430
|
}
|
|
439
431
|
|
|
440
432
|
async function listConversations(query = {}, options = {}) {
|
|
441
|
-
const assistantSurface = requireAssistantSurface(appConfig, options
|
|
433
|
+
const assistantSurface = requireAssistantSurface(appConfig, options.input?.targetSurfaceId);
|
|
442
434
|
const context = normalizeObject(options.context);
|
|
443
435
|
const assistantContext = buildAssistantActionContext(context, assistantSurface);
|
|
444
436
|
const workspace = resolveRuntimeWorkspace(assistantSurface, assistantContext, options.input || {});
|
|
@@ -448,7 +440,7 @@ function createChatService({
|
|
|
448
440
|
}
|
|
449
441
|
|
|
450
442
|
async function getConversationMessages(conversationId, query = {}, options = {}) {
|
|
451
|
-
const assistantSurface = requireAssistantSurface(appConfig, options
|
|
443
|
+
const assistantSurface = requireAssistantSurface(appConfig, options.input?.targetSurfaceId);
|
|
452
444
|
const context = normalizeObject(options.context);
|
|
453
445
|
const assistantContext = buildAssistantActionContext(context, assistantSurface);
|
|
454
446
|
const workspace = resolveRuntimeWorkspace(assistantSurface, assistantContext, options.input || {});
|
|
@@ -464,8 +456,12 @@ function createChatService({
|
|
|
464
456
|
);
|
|
465
457
|
}
|
|
466
458
|
|
|
459
|
+
function close() {
|
|
460
|
+
return conversationRuntime.close();
|
|
461
|
+
}
|
|
462
|
+
|
|
467
463
|
return Object.freeze({
|
|
468
|
-
close
|
|
464
|
+
close,
|
|
469
465
|
streamChat,
|
|
470
466
|
listConversations,
|
|
471
467
|
getConversationMessages
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveActor } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
|
|
1
2
|
import { AppError, createValidationError, parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
|
|
2
3
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
4
|
import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
@@ -11,6 +12,7 @@ const MAX_MESSAGES_PAGE_SIZE = 500;
|
|
|
11
12
|
const MAX_CONVERSATION_TITLE_LENGTH = 80;
|
|
12
13
|
|
|
13
14
|
function resolveWorkspaceId(workspace, { required = false } = {}) {
|
|
15
|
+
// readable-check: allow no-dual-shape - data: the workspace scope resolves the workspace from the action input or context, which may carry the record or its id
|
|
14
16
|
const workspaceId = normalizeRecordId(workspace?.id || workspace, { fallback: null });
|
|
15
17
|
if (!workspaceId && required) {
|
|
16
18
|
throw new AppError(409, "Workspace selection required.", { code: "assistant.runtime.workspace.selectionRequired" });
|
|
@@ -61,7 +63,7 @@ function isDefaultConversationTitle(value) {
|
|
|
61
63
|
return normalizeText(value).toLowerCase() === DEFAULT_CONVERSATION_TITLE.toLowerCase();
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
function requireAssistantSurface(assistantSurface
|
|
66
|
+
function requireAssistantSurface(assistantSurface) {
|
|
65
67
|
const targetSurfaceId = normalizeSurfaceId(assistantSurface?.targetSurfaceId);
|
|
66
68
|
if (!targetSurfaceId) {
|
|
67
69
|
throw new TypeError("assistant transcript service requires assistantSurface.targetSurfaceId.");
|
|
@@ -73,11 +75,17 @@ function requireAssistantSurface(assistantSurface = {}) {
|
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
/** @typedef {import("../types.js").ConversationsRepository} ConversationsRepository */
|
|
79
|
+
/** @typedef {import("../types.js").MessagesRepository} MessagesRepository */
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A user's conversations on an assistant surface and their messages, in the
|
|
83
|
+
* package's own tables.
|
|
84
|
+
* @param {object} dependencies
|
|
85
|
+
* @param {ConversationsRepository} dependencies.conversationsRepository
|
|
86
|
+
* @param {MessagesRepository} dependencies.messagesRepository
|
|
87
|
+
*/
|
|
88
|
+
function createTranscriptService({ conversationsRepository, messagesRepository }) {
|
|
81
89
|
function resolveExpectedWorkspaceId(assistantSurface, workspace) {
|
|
82
90
|
const resolvedAssistantSurface = requireAssistantSurface(assistantSurface);
|
|
83
91
|
return resolveWorkspaceId(workspace, {
|
|
@@ -150,7 +158,7 @@ function createTranscriptService({ conversationsRepository, messagesRepository }
|
|
|
150
158
|
|
|
151
159
|
const source = normalizeObject(payload);
|
|
152
160
|
const context = normalizeObject(options.context);
|
|
153
|
-
const actorUserId = resolveActorUserId(context
|
|
161
|
+
const actorUserId = resolveActorUserId(resolveActor(context), {
|
|
154
162
|
required: true
|
|
155
163
|
});
|
|
156
164
|
const workspaceId = resolveExpectedWorkspaceId(resolvedAssistantSurface, options.workspace || context.workspace);
|