@jskit-ai/assistant-runtime 0.2.9 → 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/conversation/main.js +2 -1
- package/fixtures/conversation/vite.config.mjs +4 -3
- 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 -884
- 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/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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
3
|
import { AppError } from "@jskit-ai/kernel/server/runtime";
|
|
4
|
+
import { resolveActor } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
|
|
4
5
|
import { createAiClient, createAiConnectionClient } from "@jskit-ai/assistant-core/server";
|
|
5
6
|
import { createConversationRuntime } from "@jskit-ai/assistant-core/server/conversation";
|
|
6
7
|
import { createSqlConversationStorage } from "./sqlConversationStorage.js";
|
|
@@ -10,60 +11,89 @@ import { createTranscriptService } from "./services/transcriptService.js";
|
|
|
10
11
|
import { resolveAssistantAiConfig, resolveAssistantServerConfig } from "./support/assistantServerConfig.js";
|
|
11
12
|
import { createSurfaceAwareToolCatalog } from "./support/createSurfaceAwareToolCatalog.js";
|
|
12
13
|
|
|
14
|
+
/** @typedef {import("./types.js").AssistantAttachments} AssistantAttachments */
|
|
15
|
+
/** @typedef {import("./types.js").AssistantAiClientFactory} AssistantAiClientFactory */
|
|
16
|
+
/** @typedef {import("./types.js").AssistantConfigRepository} AssistantConfigRepository */
|
|
17
|
+
/** @typedef {import("./types.js").ConversationsRepository} ConversationsRepository */
|
|
18
|
+
/** @typedef {import("./types.js").MessagesRepository} MessagesRepository */
|
|
19
|
+
/** @typedef {import("./types.js").TurnRequestsRepository} TurnRequestsRepository */
|
|
20
|
+
/** @typedef {import("./types.js").SurfaceAwareToolCatalog} SurfaceAwareToolCatalog */
|
|
21
|
+
/** @typedef {import("@jskit-ai/connectors-catalog/server/types").AiConnectionResolver} AiConnectionResolver */
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The AI client of each assistant surface: the authorized connection its
|
|
25
|
+
* configuration names, or one client per surface built from its configuration.
|
|
26
|
+
* @param {{ appConfig?: Record<string, any>, env?: Record<string, string | undefined>, aiConnections?: AiConnectionResolver | null }} config
|
|
27
|
+
*/
|
|
13
28
|
function createAssistantAiClientFactory(config = {}) {
|
|
14
29
|
const appConfig = normalizeObject(config.appConfig);
|
|
15
30
|
const env = normalizeObject(config.env);
|
|
16
31
|
const cache = new Map();
|
|
17
32
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const surfaceConfig = resolveAssistantServerConfig(appConfig, normalizedTargetSurfaceId);
|
|
26
|
-
if (surfaceConfig.aiIntegrationId) {
|
|
27
|
-
const selected = integrationId || surfaceConfig.aiIntegrationId;
|
|
28
|
-
if (![surfaceConfig.aiIntegrationId, ...surfaceConfig.aiIntegrationIds].includes(selected)) {
|
|
29
|
-
throw new AppError(403, "This AI connection is not available in this assistant.", { code: "assistant.runtime.connection.unavailable" });
|
|
30
|
-
}
|
|
31
|
-
if (!config.aiConnections?.resolve) throw new Error("The assistant requires an authorized AI connection resolver.");
|
|
32
|
-
const connection = await config.aiConnections.resolve({ context, integrationId: selected });
|
|
33
|
-
return createAiConnectionClient(connection, { timeoutMs: surfaceConfig.timeoutMs });
|
|
34
|
-
}
|
|
35
|
-
if (integrationId) throw new AppError(403, "This assistant does not allow selecting an AI connection.", {
|
|
36
|
-
code: "assistant.runtime.connection.selectionNotAllowed"
|
|
37
|
-
});
|
|
33
|
+
async function resolveClient(targetSurfaceId = "", { context, integrationId } = {}) {
|
|
34
|
+
const normalizedTargetSurfaceId = normalizeText(targetSurfaceId).toLowerCase();
|
|
35
|
+
if (!normalizedTargetSurfaceId) {
|
|
36
|
+
throw new Error("assistant.ai.client.factory.resolveClient requires targetSurfaceId.");
|
|
37
|
+
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const surfaceConfig = resolveAssistantServerConfig(appConfig, normalizedTargetSurfaceId);
|
|
40
|
+
if (surfaceConfig.aiIntegrationId) {
|
|
41
|
+
const selected = integrationId || surfaceConfig.aiIntegrationId;
|
|
42
|
+
if (![surfaceConfig.aiIntegrationId, ...surfaceConfig.aiIntegrationIds].includes(selected)) {
|
|
43
|
+
throw new AppError(403, "This AI connection is not available in this assistant.", { code: "assistant.runtime.connection.unavailable" });
|
|
41
44
|
}
|
|
45
|
+
if (!config.aiConnections?.resolve) throw new Error("The assistant requires an authorized AI connection resolver.");
|
|
46
|
+
const connection = await config.aiConnections.resolve({ context, integrationId: selected });
|
|
47
|
+
return createAiConnectionClient(connection, { timeoutMs: surfaceConfig.timeoutMs });
|
|
48
|
+
}
|
|
49
|
+
if (integrationId) throw new AppError(403, "This assistant does not allow selecting an AI connection.", {
|
|
50
|
+
code: "assistant.runtime.connection.selectionNotAllowed"
|
|
51
|
+
});
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
appConfig,
|
|
46
|
-
env
|
|
47
|
-
},
|
|
48
|
-
normalizedTargetSurfaceId
|
|
49
|
-
);
|
|
50
|
-
const client = createAiClient(assistantAiConfig.ai);
|
|
51
|
-
cache.set(normalizedTargetSurfaceId, client);
|
|
52
|
-
return client;
|
|
53
|
+
if (cache.has(normalizedTargetSurfaceId)) {
|
|
54
|
+
return cache.get(normalizedTargetSurfaceId);
|
|
53
55
|
}
|
|
56
|
+
|
|
57
|
+
const assistantAiConfig = resolveAssistantAiConfig(
|
|
58
|
+
{
|
|
59
|
+
appConfig,
|
|
60
|
+
env
|
|
61
|
+
},
|
|
62
|
+
normalizedTargetSurfaceId
|
|
63
|
+
);
|
|
64
|
+
const client = createAiClient(assistantAiConfig.ai);
|
|
65
|
+
cache.set(normalizedTargetSurfaceId, client);
|
|
66
|
+
return client;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
resolveClient
|
|
54
71
|
});
|
|
55
72
|
}
|
|
56
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The assistant over the package's own SQL tables: its repositories, the chat,
|
|
76
|
+
* settings and transcript services, the AI client factory and the tool catalogue.
|
|
77
|
+
* @param {object} dependencies
|
|
78
|
+
* @param {import("@jskit-ai/kernel/server/types").ActionCatalogue} dependencies.actionCatalogue
|
|
79
|
+
* @param {import("@jskit-ai/kernel/server/types").RuntimeConfig} dependencies.config
|
|
80
|
+
* @param {import("@jskit-ai/console-core/server/types").ConsoleCore | null} dependencies.consoleRuntime
|
|
81
|
+
* @param {{ config: AssistantConfigRepository, conversations: ConversationsRepository, messages: MessagesRepository, turnRequests: TurnRequestsRepository }} dependencies.repositories
|
|
82
|
+
* @param {import("@jskit-ai/kernel/server/types").RuntimeEnv} dependencies.env
|
|
83
|
+
* @param {AiConnectionResolver | null} dependencies.aiConnections
|
|
84
|
+
* @param {AssistantAttachments | null} dependencies.attachments
|
|
85
|
+
* @param {import("@jskit-ai/workspaces-core/server/types").WorkspacesCore | null} dependencies.workspaces
|
|
86
|
+
*/
|
|
57
87
|
function createAssistantRuntime({
|
|
58
88
|
actionCatalogue,
|
|
59
89
|
config,
|
|
60
90
|
consoleRuntime,
|
|
61
|
-
repositories
|
|
91
|
+
repositories,
|
|
62
92
|
env,
|
|
63
93
|
aiConnections,
|
|
64
94
|
attachments,
|
|
65
95
|
workspaces
|
|
66
|
-
}
|
|
96
|
+
}) {
|
|
67
97
|
const {
|
|
68
98
|
config: assistantConfigRepository,
|
|
69
99
|
conversations: conversationsRepository,
|
|
@@ -108,8 +138,16 @@ function createAssistantRuntime({
|
|
|
108
138
|
});
|
|
109
139
|
}
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Internal SQL composition: the facade supplies the authenticated action
|
|
143
|
+
* context; the model's history is the conversation's saved history, never the browser's.
|
|
144
|
+
* @param {object} dependencies
|
|
145
|
+
* @param {ConversationsRepository} dependencies.conversationsRepository
|
|
146
|
+
* @param {MessagesRepository} dependencies.messagesRepository
|
|
147
|
+
* @param {AssistantAiClientFactory} dependencies.aiClientFactory
|
|
148
|
+
* @param {SurfaceAwareToolCatalog} dependencies.toolCatalog
|
|
149
|
+
* @param {AssistantAttachments | null} [dependencies.attachments]
|
|
150
|
+
*/
|
|
113
151
|
function createAssistantConversationRuntime({ conversationsRepository, messagesRepository, aiClientFactory, toolCatalog, attachments }) {
|
|
114
152
|
return createConversationRuntime({
|
|
115
153
|
storage: createSqlConversationStorage({ conversationsRepository, messagesRepository }),
|
|
@@ -117,21 +155,22 @@ function createAssistantConversationRuntime({ conversationsRepository, messagesR
|
|
|
117
155
|
const request = context?.assistantRequest;
|
|
118
156
|
if (!request || request.conversation.id !== conversationId) return false;
|
|
119
157
|
return Boolean(await conversationsRepository.findByIdForActorScope(conversationId, {
|
|
120
|
-
actorUserId: normalizeRecordId(context
|
|
158
|
+
actorUserId: normalizeRecordId(resolveActor(context)?.id, { fallback: null }), surfaceId: context.surface,
|
|
159
|
+
// 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
|
|
121
160
|
workspaceId: normalizeRecordId(request.workspace?.id || request.workspace, { fallback: null })
|
|
122
161
|
}));
|
|
123
162
|
},
|
|
124
|
-
apiClientFactory: {
|
|
163
|
+
apiClientFactory: { resolveClient: ({ context, configuration }) => aiClientFactory.resolveClient(context.surface,
|
|
125
164
|
{ context, integrationId: configuration.integrationId }) },
|
|
126
|
-
apiHistory: ({ context }) => context.assistantRequest.history,
|
|
127
165
|
toolCatalog,
|
|
128
166
|
...(attachments ? { attachments: {
|
|
167
|
+
// The facade authorized the new message's files before admission; files saved
|
|
168
|
+
// with earlier turns of this conversation are authorized again as they are read.
|
|
129
169
|
resolve({ attachmentIds, context }) {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
return prepared.files;
|
|
170
|
+
const { assistantRequest: request, ...assistantContext } = context;
|
|
171
|
+
const prepared = request.resolvedAttachments[0];
|
|
172
|
+
if (prepared && isDeepStrictEqual(prepared.attachmentIds, attachmentIds)) return request.resolvedAttachments.shift().files;
|
|
173
|
+
return attachments.resolve({ attachmentIds, context: assistantContext, workspace: request.workspace, conversation: request.conversation });
|
|
135
174
|
}
|
|
136
175
|
} } : {})
|
|
137
176
|
});
|
|
@@ -6,7 +6,9 @@ import { assistantConfigResource } from "@jskit-ai/assistant-core/shared";
|
|
|
6
6
|
import { actionIds } from "./actionIds.js";
|
|
7
7
|
import { assistantTargetSurfaceInputValidator } from "./inputSchemas.js";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/** @typedef {import("./types.js").AssistantConfigService} AssistantConfigService */
|
|
10
|
+
|
|
11
|
+
const settingsReadInputValidator = deepFreeze({ schema: assistantTargetSurfaceInputValidator.schema });
|
|
10
12
|
|
|
11
13
|
const settingsUpdatePatchInputValidator = deepFreeze({
|
|
12
14
|
schema: createSchema({
|
|
@@ -27,54 +29,74 @@ const settingsUpdateInputValidator = composeSchemaDefinitions(
|
|
|
27
29
|
}
|
|
28
30
|
);
|
|
29
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The JSON:API document of a surface's assistant settings.
|
|
34
|
+
* @param {AssistantConfigService} assistantConfigService
|
|
35
|
+
* @param {object} input
|
|
36
|
+
* @param {object} context
|
|
37
|
+
*/
|
|
38
|
+
async function readSettingsDocument(assistantConfigService, input, context) {
|
|
39
|
+
return returnJsonApiData(await assistantConfigService.getSettings(input, {
|
|
40
|
+
context
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The JSON:API document of a surface's assistant settings after the input's patch.
|
|
46
|
+
* @param {AssistantConfigService} assistantConfigService
|
|
47
|
+
* @param {object} input
|
|
48
|
+
* @param {object} context
|
|
49
|
+
*/
|
|
50
|
+
async function updateSettingsDocument(assistantConfigService, input, context) {
|
|
51
|
+
return returnJsonApiData(await assistantConfigService.updateSettings(input, input.patch, {
|
|
52
|
+
context
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The settings actions of the assistant surfaces' settings screens.
|
|
58
|
+
* @param {object} dependencies
|
|
59
|
+
* @param {AssistantConfigService} dependencies.assistantConfigService
|
|
60
|
+
* @param {string[]} dependencies.settingsSurfaces
|
|
61
|
+
*/
|
|
30
62
|
function createAssistantSettingsActions({ assistantConfigService, settingsSurfaces }) {
|
|
31
63
|
return Object.freeze([
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
exclude: "Assistant self-configuration is disabled; use the authenticated assistant settings screen."
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
idempotency: "optional",
|
|
72
|
-
async execute(input, context) {
|
|
73
|
-
return returnJsonApiData(await assistantConfigService.updateSettings(input, input.patch, {
|
|
74
|
-
context
|
|
75
|
-
}));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
64
|
+
Object.freeze({
|
|
65
|
+
id: actionIds.settingsRead,
|
|
66
|
+
version: 1,
|
|
67
|
+
kind: "query",
|
|
68
|
+
channels: ["api", "automation", "internal"],
|
|
69
|
+
surfaces: settingsSurfaces,
|
|
70
|
+
permission: {
|
|
71
|
+
require: "authenticated"
|
|
72
|
+
},
|
|
73
|
+
input: settingsReadInputValidator,
|
|
74
|
+
output: null,
|
|
75
|
+
extensions: {
|
|
76
|
+
assistant: {
|
|
77
|
+
exclude: "Assistant self-configuration is disabled; use the authenticated assistant settings screen."
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
execute: (input, context) => readSettingsDocument(assistantConfigService, input, context)
|
|
81
|
+
}),
|
|
82
|
+
Object.freeze({
|
|
83
|
+
id: actionIds.settingsUpdate,
|
|
84
|
+
version: 1,
|
|
85
|
+
kind: "command",
|
|
86
|
+
channels: ["api", "automation", "internal"],
|
|
87
|
+
surfaces: settingsSurfaces,
|
|
88
|
+
permission: {
|
|
89
|
+
require: "authenticated"
|
|
90
|
+
},
|
|
91
|
+
input: settingsUpdateInputValidator,
|
|
92
|
+
output: null,
|
|
93
|
+
extensions: {
|
|
94
|
+
assistant: {
|
|
95
|
+
exclude: "Assistant self-configuration is disabled; use the authenticated assistant settings screen."
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
execute: (input, context) => updateSettingsDocument(assistantConfigService, input, context)
|
|
99
|
+
})
|
|
78
100
|
]);
|
|
79
101
|
}
|
|
80
102
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** @typedef {import("./types.js").ConversationRuntime} ConversationRuntime */
|
|
2
|
+
/** @typedef {import("./types.js").AssistantConfigService} AssistantConfigService */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The assistant over a conversation runtime the application supplies: its
|
|
6
|
+
* conversations, and the settings service when the application supplies one.
|
|
7
|
+
* @param {object} dependencies
|
|
8
|
+
* @param {ConversationRuntime} dependencies.conversationRuntime
|
|
9
|
+
* @param {AssistantConfigService | null} dependencies.assistantConfigService
|
|
10
|
+
*/
|
|
11
|
+
function createHostedAssistant({ conversationRuntime, assistantConfigService }) {
|
|
12
|
+
return Object.freeze({
|
|
13
|
+
conversationRuntime,
|
|
14
|
+
services: Object.freeze({
|
|
15
|
+
conversations: conversationRuntime,
|
|
16
|
+
config: assistantConfigService
|
|
17
|
+
})
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { createHostedAssistant };
|
package/src/server/index.js
CHANGED
|
@@ -4,3 +4,4 @@ export { assistantConversationGoalBodyValidator } from "./inputSchemas.js";
|
|
|
4
4
|
export { registerRoutes as registerAssistantRoutes } from "./registerRoutes.js";
|
|
5
5
|
export { registerConversationSubscriptions } from "./registerConversationSubscriptions.js";
|
|
6
6
|
export { registerFastifyConversations } from "./registerFastifyConversations.js";
|
|
7
|
+
export { createSurfaceAwareToolCatalog } from "./support/createSurfaceAwareToolCatalog.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { resolveActorId } from "@jskit-ai/kernel/server/actions/actionContributorHelpers";
|
|
2
3
|
import { attachDirectRequestActionExecutor } from "@jskit-ai/kernel/server/http";
|
|
3
4
|
import { AppError } from "@jskit-ai/kernel/server/runtime";
|
|
5
|
+
import { silentLogger } from "@jskit-ai/kernel/server/runtime/logger";
|
|
4
6
|
import { actionIds } from "./actionIds.js";
|
|
5
7
|
import { resolveRouteRequestState } from "./support/assistantRouteContext.js";
|
|
6
8
|
import { errorMessageFields, errorStatus } from "./support/assistantErrors.js";
|
|
@@ -32,14 +34,34 @@ function subscriptionError(error) {
|
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
/** A subscription without diagnostics has none to release. */
|
|
38
|
+
function releaseNoDiagnostics() {}
|
|
39
|
+
|
|
40
|
+
/** A subscription whose observer is not attached yet has nothing to detach. */
|
|
41
|
+
function detachNoObserver() {}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Register one conversation subscription owner per shared realtime runtime.
|
|
45
|
+
* A host may select its fixed action/request policy at composition; clients
|
|
46
|
+
* never choose either. The selected action remains the authorization boundary.
|
|
47
|
+
* @param {object} dependencies
|
|
48
|
+
* @param {import("@jskit-ai/realtime/server/types").RuntimeRealtime} dependencies.realtime
|
|
49
|
+
* @param {import("@jskit-ai/kernel/server/types").RuntimeEvents} dependencies.events
|
|
50
|
+
* @param {import("@jskit-ai/kernel/server/types").ActionCatalogue} dependencies.actions
|
|
51
|
+
* @param {Record<string, any>} [dependencies.config]
|
|
52
|
+
* @param {import("@jskit-ai/kernel/server/types").RuntimeLogger} [dependencies.logger]
|
|
53
|
+
* @param {import("./types.js").WorkspaceScopeSupport | null} [dependencies.workspaceScopeSupport]
|
|
54
|
+
* @param {string} [dependencies.subscribeActionId]
|
|
55
|
+
* @param {"authenticated" | "host"} [dependencies.requestPolicy]
|
|
56
|
+
* @param {boolean} [dependencies.diagnostics]
|
|
57
|
+
*/
|
|
58
|
+
function registerConversationSubscriptions({ realtime, events, actions, config = {}, logger = silentLogger, workspaceScopeSupport = null,
|
|
39
59
|
subscribeActionId = actionIds.conversationSubscribe, requestPolicy = "authenticated", diagnostics = false } = {}) {
|
|
60
|
+
// readable-check: allow method-probe - input validation: an application hands over its realtime connection and event runtime
|
|
40
61
|
if (typeof realtime?.onConnection !== "function" || typeof events?.publish !== "function") {
|
|
41
62
|
throw new TypeError("Conversation subscriptions require the shared realtime connection and event runtime.");
|
|
42
63
|
}
|
|
64
|
+
// readable-check: allow method-probe - input validation: an application hands over its action catalogue
|
|
43
65
|
if (typeof actions?.execute !== "function" || typeof actions?.getDefinition !== "function") {
|
|
44
66
|
throw new TypeError("Conversation subscriptions require the assistant action catalogue.");
|
|
45
67
|
}
|
|
@@ -59,10 +81,10 @@ function registerConversationSubscriptions({ realtime, events, actions, config =
|
|
|
59
81
|
const subscription = subscriptions.get(subscriptionId);
|
|
60
82
|
if (!subscription) return;
|
|
61
83
|
subscriptions.delete(subscriptionId);
|
|
62
|
-
subscription.diagnosticRelease
|
|
84
|
+
subscription.diagnosticRelease(reason);
|
|
63
85
|
clearTimeout(subscription.timer);
|
|
64
86
|
subscription.pending.clear();
|
|
65
|
-
subscription.release
|
|
87
|
+
subscription.release();
|
|
66
88
|
}
|
|
67
89
|
|
|
68
90
|
async function subscribe(input, acknowledge) {
|
|
@@ -83,7 +105,7 @@ function registerConversationSubscriptions({ realtime, events, actions, config =
|
|
|
83
105
|
if (!diagnostics) return;
|
|
84
106
|
const now = performance.now();
|
|
85
107
|
try {
|
|
86
|
-
logger
|
|
108
|
+
logger.warn(stageRecord(stageName, fields, now), "Assistant conversation subscription diagnostic.");
|
|
87
109
|
} catch { /* Diagnostics never change admission or cleanup. */ }
|
|
88
110
|
}
|
|
89
111
|
function onStage(next, fields = {}) {
|
|
@@ -93,7 +115,7 @@ function registerConversationSubscriptions({ realtime, events, actions, config =
|
|
|
93
115
|
try {
|
|
94
116
|
const slowOutcome = now - startedAt >= 5_000 && ["acknowledgement", "abandoned"].includes(next);
|
|
95
117
|
const level = diagnostics || next === "failed" || slowOutcome ? "warn" : "info";
|
|
96
|
-
logger
|
|
118
|
+
logger[level](stageRecord(next, fields, now), "Assistant conversation subscription stage");
|
|
97
119
|
} catch { /* Diagnostics must not change conversation admission or cleanup. */ }
|
|
98
120
|
stage = next;
|
|
99
121
|
stageStartedAt = now;
|
|
@@ -101,7 +123,8 @@ function registerConversationSubscriptions({ realtime, events, actions, config =
|
|
|
101
123
|
try {
|
|
102
124
|
subscriptionId = requireSubscriptionId(input);
|
|
103
125
|
release(subscriptionId, "replacement");
|
|
104
|
-
subscription = { pending: new Map(), revision: 0, epoch: randomUUID(), queue: Promise.resolve()
|
|
126
|
+
subscription = { pending: new Map(), revision: 0, epoch: randomUUID(), queue: Promise.resolve(),
|
|
127
|
+
diagnosticRelease: releaseNoDiagnostics, release: detachNoObserver };
|
|
105
128
|
subscriptions.set(subscriptionId, subscription);
|
|
106
129
|
if (diagnostics) {
|
|
107
130
|
subscription.diagnosticRelease = reason => {
|
|
@@ -109,7 +132,7 @@ function registerConversationSubscriptions({ realtime, events, actions, config =
|
|
|
109
132
|
diagnostic("released", { reason });
|
|
110
133
|
};
|
|
111
134
|
diagnosticTimer = setTimeout(() => diagnostic("pending", { acknowledgementBudgetMs: 10_000 }), 10_000);
|
|
112
|
-
diagnosticTimer.unref
|
|
135
|
+
diagnosticTimer.unref();
|
|
113
136
|
}
|
|
114
137
|
const current = () => !closed && socket.connected && subscriptions.get(subscriptionId) === subscription;
|
|
115
138
|
onStage("authentication");
|
|
@@ -128,7 +151,7 @@ function registerConversationSubscriptions({ realtime, events, actions, config =
|
|
|
128
151
|
resolveCurrentAppConfig: () => config, requiresWorkspace, workspaceScopeSupport
|
|
129
152
|
});
|
|
130
153
|
attachDirectRequestActionExecutor({ actions, request });
|
|
131
|
-
const actorId = request
|
|
154
|
+
const actorId = resolveActorId(null, request);
|
|
132
155
|
|
|
133
156
|
function publish(event, revision) {
|
|
134
157
|
subscription.queue = subscription.queue.then(async () => {
|
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ActionProvider } from "@jskit-ai/kernel/server/actions";
|
|
2
2
|
import { HttpProvider } from "@jskit-ai/kernel/server/http";
|
|
3
3
|
import { AppError, BootstrapProvider, EventProvider } from "@jskit-ai/kernel/server/runtime";
|
|
4
4
|
import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
5
5
|
import { AssistantFeature } from "./AssistantProvider.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Hosts the conversation feature in a plain Fastify application.
|
|
9
9
|
* The caller owns runtime creation/storage and authentication. This registration
|
|
10
10
|
* owns its HTTP/realtime hosting and shuts it down before Fastify closes.
|
|
11
|
-
* JSKIT applications register AssistantFeature on their
|
|
11
|
+
* JSKIT applications register AssistantFeature on their own host instead.
|
|
12
|
+
* @param {import("@jskit-ai/kernel/server/types").RuntimeFastify} app
|
|
13
|
+
* @param {object} options
|
|
14
|
+
* @param {import("./types.js").ConversationRuntime} options.runtime
|
|
15
|
+
* @param {Record<string, any>} options.config
|
|
16
|
+
* @param {(request: object) => Promise<object | null> | object | null} options.authenticate
|
|
17
|
+
* @param {(bootstrap: { request: object, context: object }) => object} [options.bootstrap]
|
|
18
|
+
* @param {import("@jskit-ai/kernel/server/types").RuntimeEnv} options.env The application's environment, usually process.env.
|
|
12
19
|
*/
|
|
13
20
|
async function registerFastifyConversations(app, {
|
|
14
21
|
runtime,
|
|
15
22
|
config,
|
|
16
23
|
authenticate,
|
|
17
24
|
bootstrap,
|
|
18
|
-
env
|
|
25
|
+
env
|
|
19
26
|
} = {}) {
|
|
27
|
+
// readable-check: allow method-probe - input validation: an application hands over its conversation runtime and authentication
|
|
20
28
|
if (typeof runtime?.open !== "function" || typeof authenticate !== "function" ||
|
|
21
29
|
!config || typeof config !== "object" || Array.isArray(config)) {
|
|
22
30
|
throw new TypeError("Fastify conversations require runtime, config and authenticate(request).");
|
|
@@ -30,7 +38,11 @@ async function registerFastifyConversations(app, {
|
|
|
30
38
|
}
|
|
31
39
|
const AccessProvider = defineProvider({
|
|
32
40
|
id: "assistant.fastify.access",
|
|
33
|
-
requires: {
|
|
41
|
+
requires: {
|
|
42
|
+
http: "runtime.http",
|
|
43
|
+
bootstrap: "runtime.bootstrap",
|
|
44
|
+
actions: "runtime.actions"
|
|
45
|
+
},
|
|
34
46
|
provides: { access: "assistant.conversation.access" },
|
|
35
47
|
setup({ http, bootstrap: bootstrapRuntime, actions }) {
|
|
36
48
|
actions.registerContextContributor({
|
|
@@ -62,7 +74,7 @@ async function registerFastifyConversations(app, {
|
|
|
62
74
|
// Realtime remains optional for consumers using the feature on their own host.
|
|
63
75
|
const { RealtimeProvider } = await import("@jskit-ai/realtime/server/RealtimeProvider");
|
|
64
76
|
const host = createCapabilityRuntime({
|
|
65
|
-
providers: [EventProvider,
|
|
77
|
+
providers: [EventProvider, ActionProvider, HttpProvider, BootstrapProvider,
|
|
66
78
|
RealtimeProvider, AccessProvider, AssistantFeature],
|
|
67
79
|
inputs: { "runtime.config": config, "runtime.env": env, "runtime.fastify": app,
|
|
68
80
|
"runtime.logger": app.log, "assistant.conversations": runtime }
|
|
@@ -77,7 +77,7 @@ function buildChatStreamActionInput(routeInput = {}, requestBody = {}) {
|
|
|
77
77
|
input: requestBody.input
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
-
for (const key of ["conversationId", "
|
|
80
|
+
for (const key of ["conversationId", "clientContext"]) {
|
|
81
81
|
if (Object.prototype.hasOwnProperty.call(requestBody, key)) {
|
|
82
82
|
actionInput[key] = requestBody[key];
|
|
83
83
|
}
|
|
@@ -86,7 +86,7 @@ function buildChatStreamActionInput(routeInput = {}, requestBody = {}) {
|
|
|
86
86
|
return actionInput;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
function resolveAssistantConversationMessagesRecordId(record
|
|
89
|
+
function resolveAssistantConversationMessagesRecordId(record) {
|
|
90
90
|
const conversationId = String(record?.conversation?.id || "").trim();
|
|
91
91
|
if (!conversationId) {
|
|
92
92
|
throw new Error("Assistant conversation messages JSON:API response requires conversation.id.");
|
|
@@ -154,6 +154,7 @@ function registerRuntimeRoutes(
|
|
|
154
154
|
setNdjsonHeaders(reply);
|
|
155
155
|
reply.code(200);
|
|
156
156
|
reply.hijack();
|
|
157
|
+
// readable-check: allow method-probe - platform object: a Fastify reply's raw response may lack flushHeaders()
|
|
157
158
|
if (typeof reply.raw.flushHeaders === "function") {
|
|
158
159
|
reply.raw.flushHeaders();
|
|
159
160
|
}
|
|
@@ -496,9 +497,11 @@ function registerRoutes(router, {
|
|
|
496
497
|
conversationReplacementSchema = null,
|
|
497
498
|
assistantConfigService = null
|
|
498
499
|
} = {}) {
|
|
500
|
+
// readable-check: allow method-probe - input validation: an application hands over the router its routes register on
|
|
499
501
|
if (!router || typeof router.register !== "function") {
|
|
500
502
|
throw new Error("registerRoutes requires an HTTP router.");
|
|
501
503
|
}
|
|
504
|
+
// readable-check: allow method-probe - input validation: an application hands over its conversation runtime
|
|
502
505
|
if (conversationRuntime && typeof conversationRuntime.open !== "function") {
|
|
503
506
|
throw new TypeError("Assistant conversation routes require conversationRuntime.open().");
|
|
504
507
|
}
|
|
@@ -9,7 +9,7 @@ import { actionIds } from "./actionIds.js";
|
|
|
9
9
|
import { assistantSurfaceRouteParamsValidator } from "./inputSchemas.js";
|
|
10
10
|
import { requireWorkspaceAssistantRouteParams, resolveRouteRequestState } from "./support/assistantRouteContext.js";
|
|
11
11
|
|
|
12
|
-
function resolveAssistantSettingsRecordId(record
|
|
12
|
+
function resolveAssistantSettingsRecordId(record) {
|
|
13
13
|
const scopeKey = String(record?.scopeKey || "").trim();
|
|
14
14
|
if (!scopeKey) {
|
|
15
15
|
throw new Error("Assistant settings JSON:API response requires scopeKey.");
|
|
@@ -1,27 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createWithTransaction } from "@jskit-ai/database-runtime/shared";
|
|
2
2
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
3
|
-
import {
|
|
4
|
-
import { resolveInsertedId } from "./repositoryPersistenceUtils.js";
|
|
3
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
import { normalizeDbWorkspaceId, normalizeInputRecordId, resolveInsertedId } from "./repositoryPersistenceUtils.js";
|
|
5
5
|
import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
|
|
6
6
|
|
|
7
7
|
function normalizeTargetSurfaceId(value = "") {
|
|
8
8
|
return normalizeSurfaceId(value);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
function normalizeWorkspaceId(value) {
|
|
12
|
-
return normalizeRecordId(value, { fallback: null });
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function normalizeWorkspaceDbId(value) {
|
|
16
|
-
return normalizeDbRecordId(value, { fallback: null });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
11
|
function buildScopeKey(targetSurfaceId, workspaceId = null) {
|
|
20
12
|
const normalizedTargetSurfaceId = normalizeTargetSurfaceId(targetSurfaceId);
|
|
21
13
|
if (!normalizedTargetSurfaceId) {
|
|
22
14
|
throw new TypeError("assistantConfigRepository requires targetSurfaceId.");
|
|
23
15
|
}
|
|
24
|
-
const normalizedWorkspaceId =
|
|
16
|
+
const normalizedWorkspaceId = normalizeInputRecordId(workspaceId);
|
|
25
17
|
if (normalizedWorkspaceId) {
|
|
26
18
|
return `${normalizedTargetSurfaceId}:workspace:${normalizedWorkspaceId}`;
|
|
27
19
|
}
|
|
@@ -33,7 +25,7 @@ function mapConfigRow(row = {}) {
|
|
|
33
25
|
return {
|
|
34
26
|
targetSurfaceId: normalizeTargetSurfaceId(row.target_surface_id),
|
|
35
27
|
scopeKey: normalizeText(row.scope_key),
|
|
36
|
-
workspaceId:
|
|
28
|
+
workspaceId: normalizeDbWorkspaceId(row.workspace_id),
|
|
37
29
|
settings: {
|
|
38
30
|
systemPrompt: String(row.system_prompt || "")
|
|
39
31
|
}
|
|
@@ -45,7 +37,7 @@ function createDefaultRecord({ targetSurfaceId = "", workspaceId = null } = {})
|
|
|
45
37
|
if (!normalizedTargetSurfaceId) {
|
|
46
38
|
throw new TypeError("assistantConfigRepository requires targetSurfaceId.");
|
|
47
39
|
}
|
|
48
|
-
const normalizedWorkspaceId =
|
|
40
|
+
const normalizedWorkspaceId = normalizeInputRecordId(workspaceId);
|
|
49
41
|
|
|
50
42
|
return {
|
|
51
43
|
targetSurfaceId: normalizedTargetSurfaceId,
|
|
@@ -57,14 +49,12 @@ function createDefaultRecord({ targetSurfaceId = "", workspaceId = null } = {})
|
|
|
57
49
|
};
|
|
58
50
|
}
|
|
59
51
|
|
|
52
|
+
/** @param {import("@jskit-ai/database-runtime/server/types").RuntimeKnex} knex */
|
|
60
53
|
function createRepository(knex) {
|
|
61
|
-
if (!knex || typeof knex !== "function") {
|
|
62
|
-
throw new Error("createAssistantConfigRepository requires knex client.");
|
|
63
|
-
}
|
|
64
54
|
const withTransaction = createWithTransaction(knex);
|
|
65
55
|
|
|
66
56
|
async function findByScope({ targetSurfaceId = "", workspaceId = null } = {}, options = {}) {
|
|
67
|
-
const client = options
|
|
57
|
+
const client = options.trx || knex;
|
|
68
58
|
const defaultRecord = createDefaultRecord({
|
|
69
59
|
targetSurfaceId,
|
|
70
60
|
workspaceId
|
|
@@ -80,7 +70,7 @@ function createRepository(knex) {
|
|
|
80
70
|
}
|
|
81
71
|
|
|
82
72
|
async function upsertByScope({ targetSurfaceId = "", workspaceId = null, patch = {} } = {}, options = {}) {
|
|
83
|
-
const client = options
|
|
73
|
+
const client = options.trx || knex;
|
|
84
74
|
const defaultRecord = createDefaultRecord({
|
|
85
75
|
targetSurfaceId,
|
|
86
76
|
workspaceId
|
|
@@ -88,7 +78,7 @@ function createRepository(knex) {
|
|
|
88
78
|
const existing = await findByScope(defaultRecord, {
|
|
89
79
|
trx: client
|
|
90
80
|
});
|
|
91
|
-
const nextSystemPrompt = Object.hasOwn(patch
|
|
81
|
+
const nextSystemPrompt = Object.hasOwn(patch, "systemPrompt")
|
|
92
82
|
? String(patch.systemPrompt || "")
|
|
93
83
|
: String(existing?.settings?.systemPrompt || defaultRecord.settings.systemPrompt);
|
|
94
84
|
const now = new Date();
|