@jskit-ai/assistant-runtime 0.2.29 → 0.2.31
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/CHANGELOG.md +9 -0
- package/README.md +6 -3
- package/package.json +16 -10
- package/patterns/assistant-surface/PATTERN.md +5 -2
- package/src/client/components/AssistantSettingsClientElement.vue +5 -3
- package/src/server/AssistantProvider.js +8 -13
- package/src/server/AssistantStoreFeature.js +59 -0
- package/src/server/actions.js +5 -5
- package/src/server/createAssistantRuntime.js +2 -1
- package/src/server/createAssistantSettingsActions.js +17 -9
- package/src/server/index.js +1 -0
- package/src/server/registerConversationSubscriptions.js +5 -2
- package/src/server/registerRoutes.js +11 -8
- package/src/server/registerSettingsRoutes.js +4 -4
- package/src/server/repositories/assistantConfigRepository.js +52 -67
- package/src/server/repositories/assistantResources.js +119 -0
- package/src/server/repositories/conversationsRepository.js +164 -147
- package/src/server/repositories/messagesRepository.js +136 -87
- package/src/server/repositories/repositoryPersistenceUtils.js +11 -22
- package/src/server/repositories/turnRequestsRepository.js +50 -16
- package/src/server/services/assistantConfigService.js +22 -0
- package/src/server/services/chatService.js +15 -7
- package/src/server/services/transcriptService.js +3 -3
- package/src/server/sqlConversationStorage.js +23 -19
- package/src/server/types.js +1 -0
- package/test/assistantIntegrationBoundary.test.js +2 -0
- package/test/assistantSettings.browser.test.js +23 -2
- package/test/assistantSettingsAccess.test.js +46 -1
- package/test/chatServiceLifecycle.test.js +32 -1
- package/test/featureRuntime.test.js +17 -3
- package/test/repositories.mysql.test.js +474 -0
- package/test/repositoryContracts.test.js +13 -13
- package/test/sqlConversationStorage.mysql.test.js +9 -6
- package/test/sqlConversationStorage.test.js +27 -20
- package/test/support/assistantStore.js +39 -0
- package/test/support/memoryAssistantRows.js +11 -4
- package/test/turnRequests.test.js +37 -14
- package/test/usersForeignKeysMigration.test.js +24 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { createWithTransaction } from "@jskit-ai/database-runtime/shared";
|
|
2
1
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
2
|
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
-
import {
|
|
5
|
-
import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
|
|
3
|
+
import { normalizeInputRecordId, toWrittenDateTime } from "./repositoryPersistenceUtils.js";
|
|
6
4
|
|
|
7
5
|
function normalizeTargetSurfaceId(value = "") {
|
|
8
6
|
return normalizeSurfaceId(value);
|
|
@@ -21,13 +19,13 @@ function buildScopeKey(targetSurfaceId, workspaceId = null) {
|
|
|
21
19
|
return `${normalizedTargetSurfaceId}:global`;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
function
|
|
22
|
+
function mapConfigRecord(record = {}) {
|
|
25
23
|
return {
|
|
26
|
-
targetSurfaceId: normalizeTargetSurfaceId(
|
|
27
|
-
scopeKey: normalizeText(
|
|
28
|
-
workspaceId:
|
|
24
|
+
targetSurfaceId: normalizeTargetSurfaceId(record.targetSurfaceId),
|
|
25
|
+
scopeKey: normalizeText(record.scopeKey),
|
|
26
|
+
workspaceId: normalizeInputRecordId(record.workspaceId),
|
|
29
27
|
settings: {
|
|
30
|
-
systemPrompt: String(
|
|
28
|
+
systemPrompt: String(record.systemPrompt || "")
|
|
31
29
|
}
|
|
32
30
|
};
|
|
33
31
|
}
|
|
@@ -49,51 +47,56 @@ function createDefaultRecord({ targetSurfaceId = "", workspaceId = null } = {})
|
|
|
49
47
|
};
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The saved settings of each assistant surface and scope, on the assistantConfig table.
|
|
52
|
+
* @param {object} dependencies
|
|
53
|
+
* @param {import("@jskit-ai/resource-runtime/server/types").ResourceHandle} dependencies.assistantConfig
|
|
54
|
+
* @param {(reason: string) => import("@jskit-ai/resource-runtime/server/types").SystemContext} dependencies.systemContext
|
|
55
|
+
*/
|
|
56
|
+
function createRepository({ assistantConfig, systemContext }) {
|
|
57
|
+
const settingsContext = systemContext("global assistant settings are shared by every workspace, so they are read and written outside one");
|
|
58
|
+
|
|
59
|
+
// A workspace's settings are read and written in that workspace; global settings in the system context.
|
|
60
|
+
function scoped(workspaceId, options) {
|
|
61
|
+
return { ...options, context: workspaceId ? settingsContext.forWorkspace(workspaceId) : settingsContext };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function withTransaction(options, work) {
|
|
65
|
+
return assistantConfig.withTransaction(options, work);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function findRecord(defaultRecord, options) {
|
|
69
|
+
const { records } = await assistantConfig.query({
|
|
70
|
+
filters: {
|
|
71
|
+
targetSurfaceId: defaultRecord.targetSurfaceId,
|
|
72
|
+
scopeKey: defaultRecord.scopeKey
|
|
73
|
+
},
|
|
74
|
+
page: { size: 1 }
|
|
75
|
+
}, scoped(defaultRecord.workspaceId, options));
|
|
76
|
+
return records[0] || null;
|
|
77
|
+
}
|
|
55
78
|
|
|
56
79
|
async function findByScope({ targetSurfaceId = "", workspaceId = null } = {}, options = {}) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
targetSurfaceId,
|
|
60
|
-
workspaceId
|
|
61
|
-
});
|
|
62
|
-
const row = await client(assistantRuntimeConfig.configTable)
|
|
63
|
-
.where({
|
|
64
|
-
target_surface_id: defaultRecord.targetSurfaceId,
|
|
65
|
-
scope_key: defaultRecord.scopeKey
|
|
66
|
-
})
|
|
67
|
-
.first();
|
|
68
|
-
|
|
69
|
-
return row ? mapConfigRow(row) : null;
|
|
80
|
+
const record = await findRecord(createDefaultRecord({ targetSurfaceId, workspaceId }), options);
|
|
81
|
+
return record ? mapConfigRecord(record) : null;
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
async function upsertByScope({ targetSurfaceId = "", workspaceId = null, patch = {} } = {}, options = {}) {
|
|
73
|
-
const client = options.trx || knex;
|
|
74
85
|
const defaultRecord = createDefaultRecord({
|
|
75
86
|
targetSurfaceId,
|
|
76
87
|
workspaceId
|
|
77
88
|
});
|
|
78
|
-
const existing = await
|
|
79
|
-
trx: client
|
|
80
|
-
});
|
|
89
|
+
const existing = await findRecord(defaultRecord, options);
|
|
81
90
|
const nextSystemPrompt = Object.hasOwn(patch, "systemPrompt")
|
|
82
91
|
? String(patch.systemPrompt || "")
|
|
83
|
-
: String(existing?.
|
|
84
|
-
const now =
|
|
92
|
+
: String(existing?.systemPrompt || defaultRecord.settings.systemPrompt);
|
|
93
|
+
const now = toWrittenDateTime();
|
|
85
94
|
|
|
86
95
|
if (existing) {
|
|
87
|
-
await
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
})
|
|
92
|
-
.update({
|
|
93
|
-
workspace_id: defaultRecord.workspaceId,
|
|
94
|
-
system_prompt: nextSystemPrompt,
|
|
95
|
-
updated_at: now
|
|
96
|
-
});
|
|
96
|
+
await assistantConfig.patch(existing.id, {
|
|
97
|
+
systemPrompt: nextSystemPrompt,
|
|
98
|
+
updatedAt: now
|
|
99
|
+
}, { ...scoped(defaultRecord.workspaceId, options), missing: "skip", returning: "none" });
|
|
97
100
|
|
|
98
101
|
return {
|
|
99
102
|
...defaultRecord,
|
|
@@ -103,34 +106,16 @@ function createRepository(knex) {
|
|
|
103
106
|
};
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
const insertedId = resolveInsertedId(insertResult);
|
|
115
|
-
if (!insertedId) {
|
|
116
|
-
return {
|
|
117
|
-
...defaultRecord,
|
|
118
|
-
settings: {
|
|
119
|
-
systemPrompt: nextSystemPrompt
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const insertedRow = await client(assistantRuntimeConfig.configTable)
|
|
125
|
-
.where({ id: insertedId })
|
|
126
|
-
.first();
|
|
109
|
+
const created = await assistantConfig.create({
|
|
110
|
+
targetSurfaceId: defaultRecord.targetSurfaceId,
|
|
111
|
+
scopeKey: defaultRecord.scopeKey,
|
|
112
|
+
workspaceId: defaultRecord.workspaceId,
|
|
113
|
+
systemPrompt: nextSystemPrompt,
|
|
114
|
+
createdAt: now,
|
|
115
|
+
updatedAt: now
|
|
116
|
+
}, scoped(defaultRecord.workspaceId, options));
|
|
127
117
|
|
|
128
|
-
return
|
|
129
|
-
...defaultRecord,
|
|
130
|
-
settings: {
|
|
131
|
-
systemPrompt: nextSystemPrompt
|
|
132
|
-
}
|
|
133
|
-
};
|
|
118
|
+
return mapConfigRecord(created);
|
|
134
119
|
}
|
|
135
120
|
|
|
136
121
|
return Object.freeze({
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { defineTableResource } from "@jskit-ai/resource-core/shared/tableResource";
|
|
2
|
+
|
|
3
|
+
// The assistant's four tables. The repositories normalize every value before
|
|
4
|
+
// they write it, so text is stored exactly as given (noTrim), and dates are
|
|
5
|
+
// written as UTC database date-times. Each declares the foreign keys its
|
|
6
|
+
// migrations always create; the workspace_id keys exist only where the
|
|
7
|
+
// application has a workspaces table (constraints/assistant_workspaces_foreign_keys.cjs),
|
|
8
|
+
// so none declares them.
|
|
9
|
+
|
|
10
|
+
/** @param {Record<string, unknown>} [definition] */
|
|
11
|
+
function textField(definition = {}) {
|
|
12
|
+
return { type: "string", noTrim: true, ...definition };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @param {Record<string, unknown>} [definition] */
|
|
16
|
+
function dateTimeField(definition = {}) {
|
|
17
|
+
return { type: "dateTime", storage: { writeSerializer: "datetime-utc" }, ...definition };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// A surface's saved settings: one row per surface and scope. Global settings
|
|
21
|
+
// (workspaceId null) are shared by every workspace.
|
|
22
|
+
const assistantConfigResource = defineTableResource({
|
|
23
|
+
name: "assistantConfig",
|
|
24
|
+
tableName: "assistant_config",
|
|
25
|
+
ownership: "workspace_or_shared",
|
|
26
|
+
schema: {
|
|
27
|
+
targetSurfaceId: textField({ search: true }),
|
|
28
|
+
scopeKey: textField({ search: true }),
|
|
29
|
+
workspaceId: { type: "id", nullable: true, search: true },
|
|
30
|
+
systemPrompt: textField(),
|
|
31
|
+
createdAt: dateTimeField(),
|
|
32
|
+
updatedAt: dateTimeField()
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// A conversation belongs to the user who started it (created_by_user_id); its
|
|
37
|
+
// workspace (null for a global surface) and surface are filters of its actor scope.
|
|
38
|
+
const assistantConversationsResource = defineTableResource({
|
|
39
|
+
name: "assistantConversations",
|
|
40
|
+
tableName: "assistant_conversations",
|
|
41
|
+
ownership: "user",
|
|
42
|
+
foreignKeys: [{ columns: ["userId"], referencedTableName: "users" }],
|
|
43
|
+
queryMaxLimit: 200,
|
|
44
|
+
schema: {
|
|
45
|
+
workspaceId: { type: "id", nullable: true, search: true },
|
|
46
|
+
userId: { type: "id", nullable: true, search: true, storage: { column: "created_by_user_id" } },
|
|
47
|
+
title: textField(),
|
|
48
|
+
status: textField({ search: true }),
|
|
49
|
+
provider: textField(),
|
|
50
|
+
model: textField(),
|
|
51
|
+
surfaceId: textField({ search: true }),
|
|
52
|
+
messageCount: { type: "integer" },
|
|
53
|
+
metadataJson: textField({ nullable: true }),
|
|
54
|
+
startedAt: dateTimeField(),
|
|
55
|
+
endedAt: dateTimeField({ nullable: true }),
|
|
56
|
+
createdAt: dateTimeField(),
|
|
57
|
+
updatedAt: dateTimeField(),
|
|
58
|
+
summaryText: textField({ nullable: true }),
|
|
59
|
+
summaryThroughTurnId: textField({ nullable: true }),
|
|
60
|
+
transcriptRevision: textField({ nullable: true })
|
|
61
|
+
},
|
|
62
|
+
searchSchema: {
|
|
63
|
+
beforeId: { type: "id", actualField: "id", filterOperator: "<" }
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// A conversation's messages, in seq order, with the conversation's workspace.
|
|
68
|
+
const assistantMessagesResource = defineTableResource({
|
|
69
|
+
name: "assistantMessages",
|
|
70
|
+
tableName: "assistant_messages",
|
|
71
|
+
ownership: "workspace",
|
|
72
|
+
foreignKeys: [
|
|
73
|
+
{ columns: ["conversationId"], referencedTableName: "assistant_conversations" },
|
|
74
|
+
{ columns: ["actorUserId"], referencedTableName: "users" }
|
|
75
|
+
],
|
|
76
|
+
queryMaxLimit: 500,
|
|
77
|
+
schema: {
|
|
78
|
+
conversationId: { type: "id", search: true },
|
|
79
|
+
workspaceId: { type: "id", nullable: true, search: true },
|
|
80
|
+
seq: { type: "integer" },
|
|
81
|
+
role: textField({ search: true }),
|
|
82
|
+
kind: textField({ search: true }),
|
|
83
|
+
clientMessageSid: textField(),
|
|
84
|
+
actorUserId: { type: "id", nullable: true },
|
|
85
|
+
contentText: textField({ nullable: true }),
|
|
86
|
+
metadataJson: textField({ nullable: true }),
|
|
87
|
+
createdAt: dateTimeField()
|
|
88
|
+
},
|
|
89
|
+
searchSchema: {
|
|
90
|
+
conversationIds: { type: "array", actualField: "conversationId", filterOperator: "in", items: { type: "string" } }
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// The receipt of one chat request, keyed by its actor, surface and workspace
|
|
95
|
+
// scope, and the client's message id.
|
|
96
|
+
const assistantTurnRequestsResource = defineTableResource({
|
|
97
|
+
name: "assistantTurnRequests",
|
|
98
|
+
tableName: "assistant_turn_requests",
|
|
99
|
+
ownership: "user",
|
|
100
|
+
foreignKeys: [{ columns: ["userId"], referencedTableName: "users" }],
|
|
101
|
+
schema: {
|
|
102
|
+
userId: { type: "id", search: true, storage: { column: "actor_user_id" } },
|
|
103
|
+
scopeKey: textField({ search: true }),
|
|
104
|
+
messageSid: textField({ search: true }),
|
|
105
|
+
claimToken: textField(),
|
|
106
|
+
requestJson: textField(),
|
|
107
|
+
responseJson: textField({ nullable: true }),
|
|
108
|
+
status: textField(),
|
|
109
|
+
createdAt: dateTimeField(),
|
|
110
|
+
updatedAt: dateTimeField()
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export {
|
|
115
|
+
assistantConfigResource,
|
|
116
|
+
assistantConversationsResource,
|
|
117
|
+
assistantMessagesResource,
|
|
118
|
+
assistantTurnRequestsResource
|
|
119
|
+
};
|