@jskit-ai/assistant-runtime 0.2.29 → 0.2.30

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 (37) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +6 -3
  3. package/package.json +16 -10
  4. package/patterns/assistant-surface/PATTERN.md +5 -2
  5. package/src/client/components/AssistantSettingsClientElement.vue +5 -3
  6. package/src/server/AssistantProvider.js +8 -13
  7. package/src/server/AssistantStoreFeature.js +59 -0
  8. package/src/server/actions.js +5 -5
  9. package/src/server/createAssistantRuntime.js +2 -1
  10. package/src/server/createAssistantSettingsActions.js +17 -9
  11. package/src/server/index.js +1 -0
  12. package/src/server/registerConversationSubscriptions.js +5 -2
  13. package/src/server/registerRoutes.js +11 -8
  14. package/src/server/registerSettingsRoutes.js +4 -4
  15. package/src/server/repositories/assistantConfigRepository.js +52 -67
  16. package/src/server/repositories/assistantResources.js +110 -0
  17. package/src/server/repositories/conversationsRepository.js +164 -147
  18. package/src/server/repositories/messagesRepository.js +136 -87
  19. package/src/server/repositories/repositoryPersistenceUtils.js +11 -22
  20. package/src/server/repositories/turnRequestsRepository.js +50 -16
  21. package/src/server/services/assistantConfigService.js +22 -0
  22. package/src/server/services/chatService.js +15 -7
  23. package/src/server/services/transcriptService.js +3 -3
  24. package/src/server/sqlConversationStorage.js +23 -19
  25. package/src/server/types.js +1 -0
  26. package/test/assistantIntegrationBoundary.test.js +2 -0
  27. package/test/assistantSettings.browser.test.js +23 -2
  28. package/test/assistantSettingsAccess.test.js +46 -1
  29. package/test/chatServiceLifecycle.test.js +32 -1
  30. package/test/featureRuntime.test.js +17 -3
  31. package/test/repositories.mysql.test.js +474 -0
  32. package/test/repositoryContracts.test.js +13 -13
  33. package/test/sqlConversationStorage.mysql.test.js +9 -6
  34. package/test/sqlConversationStorage.test.js +27 -20
  35. package/test/support/assistantStore.js +39 -0
  36. package/test/support/memoryAssistantRows.js +11 -4
  37. package/test/turnRequests.test.js +37 -14
@@ -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 { normalizeDbWorkspaceId, normalizeInputRecordId, resolveInsertedId } from "./repositoryPersistenceUtils.js";
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 mapConfigRow(row = {}) {
22
+ function mapConfigRecord(record = {}) {
25
23
  return {
26
- targetSurfaceId: normalizeTargetSurfaceId(row.target_surface_id),
27
- scopeKey: normalizeText(row.scope_key),
28
- workspaceId: normalizeDbWorkspaceId(row.workspace_id),
24
+ targetSurfaceId: normalizeTargetSurfaceId(record.targetSurfaceId),
25
+ scopeKey: normalizeText(record.scopeKey),
26
+ workspaceId: normalizeInputRecordId(record.workspaceId),
29
27
  settings: {
30
- systemPrompt: String(row.system_prompt || "")
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
- /** @param {import("@jskit-ai/database-runtime/server/types").RuntimeKnex} knex */
53
- function createRepository(knex) {
54
- const withTransaction = createWithTransaction(knex);
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 client = options.trx || knex;
58
- const defaultRecord = createDefaultRecord({
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 findByScope(defaultRecord, {
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?.settings?.systemPrompt || defaultRecord.settings.systemPrompt);
84
- const now = new Date();
92
+ : String(existing?.systemPrompt || defaultRecord.settings.systemPrompt);
93
+ const now = toWrittenDateTime();
85
94
 
86
95
  if (existing) {
87
- await client(assistantRuntimeConfig.configTable)
88
- .where({
89
- target_surface_id: defaultRecord.targetSurfaceId,
90
- scope_key: defaultRecord.scopeKey
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 insertResult = await client(assistantRuntimeConfig.configTable).insert({
107
- target_surface_id: defaultRecord.targetSurfaceId,
108
- scope_key: defaultRecord.scopeKey,
109
- workspace_id: defaultRecord.workspaceId,
110
- system_prompt: nextSystemPrompt,
111
- created_at: now,
112
- updated_at: now
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 insertedRow ? mapConfigRow(insertedRow) : {
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,110 @@
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.
6
+
7
+ /** @param {Record<string, unknown>} [definition] */
8
+ function textField(definition = {}) {
9
+ return { type: "string", noTrim: true, ...definition };
10
+ }
11
+
12
+ /** @param {Record<string, unknown>} [definition] */
13
+ function dateTimeField(definition = {}) {
14
+ return { type: "dateTime", storage: { writeSerializer: "datetime-utc" }, ...definition };
15
+ }
16
+
17
+ // A surface's saved settings: one row per surface and scope. Global settings
18
+ // (workspaceId null) are shared by every workspace.
19
+ const assistantConfigResource = defineTableResource({
20
+ name: "assistantConfig",
21
+ tableName: "assistant_config",
22
+ ownership: "workspace_or_shared",
23
+ schema: {
24
+ targetSurfaceId: textField({ search: true }),
25
+ scopeKey: textField({ search: true }),
26
+ workspaceId: { type: "id", nullable: true, search: true },
27
+ systemPrompt: textField(),
28
+ createdAt: dateTimeField(),
29
+ updatedAt: dateTimeField()
30
+ }
31
+ });
32
+
33
+ // A conversation belongs to the user who started it (created_by_user_id); its
34
+ // workspace (null for a global surface) and surface are filters of its actor scope.
35
+ const assistantConversationsResource = defineTableResource({
36
+ name: "assistantConversations",
37
+ tableName: "assistant_conversations",
38
+ ownership: "user",
39
+ queryMaxLimit: 200,
40
+ schema: {
41
+ workspaceId: { type: "id", nullable: true, search: true },
42
+ userId: { type: "id", nullable: true, search: true, storage: { column: "created_by_user_id" } },
43
+ title: textField(),
44
+ status: textField({ search: true }),
45
+ provider: textField(),
46
+ model: textField(),
47
+ surfaceId: textField({ search: true }),
48
+ messageCount: { type: "integer" },
49
+ metadataJson: textField({ nullable: true }),
50
+ startedAt: dateTimeField(),
51
+ endedAt: dateTimeField({ nullable: true }),
52
+ createdAt: dateTimeField(),
53
+ updatedAt: dateTimeField(),
54
+ summaryText: textField({ nullable: true }),
55
+ summaryThroughTurnId: textField({ nullable: true }),
56
+ transcriptRevision: textField({ nullable: true })
57
+ },
58
+ searchSchema: {
59
+ beforeId: { type: "id", actualField: "id", filterOperator: "<" }
60
+ }
61
+ });
62
+
63
+ // A conversation's messages, in seq order, with the conversation's workspace.
64
+ const assistantMessagesResource = defineTableResource({
65
+ name: "assistantMessages",
66
+ tableName: "assistant_messages",
67
+ ownership: "workspace",
68
+ queryMaxLimit: 500,
69
+ schema: {
70
+ conversationId: { type: "id", search: true },
71
+ workspaceId: { type: "id", nullable: true, search: true },
72
+ seq: { type: "integer" },
73
+ role: textField({ search: true }),
74
+ kind: textField({ search: true }),
75
+ clientMessageSid: textField(),
76
+ actorUserId: { type: "id", nullable: true },
77
+ contentText: textField({ nullable: true }),
78
+ metadataJson: textField({ nullable: true }),
79
+ createdAt: dateTimeField()
80
+ },
81
+ searchSchema: {
82
+ conversationIds: { type: "array", actualField: "conversationId", filterOperator: "in", items: { type: "string" } }
83
+ }
84
+ });
85
+
86
+ // The receipt of one chat request, keyed by its actor, surface and workspace
87
+ // scope, and the client's message id.
88
+ const assistantTurnRequestsResource = defineTableResource({
89
+ name: "assistantTurnRequests",
90
+ tableName: "assistant_turn_requests",
91
+ ownership: "user",
92
+ schema: {
93
+ userId: { type: "id", search: true, storage: { column: "actor_user_id" } },
94
+ scopeKey: textField({ search: true }),
95
+ messageSid: textField({ search: true }),
96
+ claimToken: textField(),
97
+ requestJson: textField(),
98
+ responseJson: textField({ nullable: true }),
99
+ status: textField(),
100
+ createdAt: dateTimeField(),
101
+ updatedAt: dateTimeField()
102
+ }
103
+ });
104
+
105
+ export {
106
+ assistantConfigResource,
107
+ assistantConversationsResource,
108
+ assistantMessagesResource,
109
+ assistantTurnRequestsResource
110
+ };