@jskit-ai/assistant-runtime 0.1.193 → 0.1.195
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/fixtures/canonical-conversation/App.vue +188 -0
- package/fixtures/canonical-conversation/DraftConversation.vue +32 -0
- package/fixtures/canonical-conversation/index.html +1 -0
- package/fixtures/canonical-conversation/main.js +61 -0
- package/fixtures/canonical-conversation/socket.js +50 -0
- package/fixtures/canonical-conversation/vite.config.mjs +178 -0
- package/fixtures/conversation/vite.config.mjs +12 -2
- package/package.json +23 -10
- package/src/client/components/AssistantConversationClientElement.vue +57 -0
- package/src/client/composables/useAssistantConversation.js +874 -0
- package/src/client/index.js +2 -0
- package/src/client/support/assistantRuntimeState.js +1 -1
- package/src/client/support/subscribeAssistantConversation.js +205 -0
- package/src/server/AssistantProvider.js +82 -106
- package/src/server/actionIds.js +10 -0
- package/src/server/actions.js +287 -79
- package/src/server/createAssistantRuntime.js +138 -0
- package/src/server/createAssistantSettingsActions.js +89 -0
- package/src/server/index.js +6 -0
- package/src/server/inputSchemas.js +90 -0
- package/src/server/registerConversationSubscriptions.js +163 -0
- package/src/server/registerFastifyConversations.js +76 -0
- package/src/server/registerRoutes.js +220 -198
- package/src/server/registerSettingsRoutes.js +113 -0
- package/src/server/repositories/conversationsRepository.js +12 -5
- package/src/server/repositories/messagesRepository.js +12 -0
- package/src/server/services/chatService.js +88 -694
- package/src/server/services/transcriptService.js +56 -52
- package/src/server/sqlConversationStorage.js +192 -0
- package/src/server/support/assistantRouteContext.js +91 -0
- package/src/server/support/createSurfaceAwareToolCatalog.js +6 -2
- package/src/shared/assistantSurfaces.js +14 -0
- package/src/shared/conversationRealtime.js +9 -0
- package/src/shared/index.js +1 -0
- package/test/actionDefinitions.test.js +44 -0
- package/test/aiConnections.test.js +1 -1
- package/test/assistantConversationBinding.browser.test.js +1868 -0
- package/test/assistantConversationOperations.test.js +459 -0
- package/test/assistantConversationSubscriptions.test.js +692 -0
- package/test/assistantIntegrationBoundary.test.js +103 -0
- package/test/assistantRuntimeComposition.test.js +68 -0
- package/test/assistantSurfacePattern.test.js +1 -1
- package/test/assistantSurfaces.test.js +23 -0
- package/test/chatServiceLifecycle.test.js +12 -1
- package/test/featureRuntime.test.js +238 -0
- package/test/registerFastifyConversations.test.js +169 -0
- package/test/sqlConversationStorage.test.js +205 -0
- package/test/support/memoryAssistantRows.js +51 -0
- package/test/turnRequests.test.js +11 -2
- package/src/shared/assistantResponseText.js +0 -29
package/src/client/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as AssistantSurfaceClientElement } from "./components/AssistantSurfaceClientElement.vue";
|
|
2
|
+
export { default as AssistantConversationClientElement } from "./components/AssistantConversationClientElement.vue";
|
|
2
3
|
export { default as AssistantSettingsClientElement } from "./components/AssistantSettingsClientElement.vue";
|
|
3
4
|
export { AssistantClientProvider } from "./providers/AssistantClientProvider.js";
|
|
4
5
|
export { useAssistantRuntime } from "./composables/useAssistantRuntime.js";
|
|
6
|
+
export { configureAssistantConversations, useAssistantConversation, useAssistantConversationFactory } from "./composables/useAssistantConversation.js";
|
|
@@ -2,9 +2,9 @@ import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/ker
|
|
|
2
2
|
import {
|
|
3
3
|
MAX_HISTORY_MESSAGES,
|
|
4
4
|
MAX_INPUT_CHARS,
|
|
5
|
+
isAssistantProgressOnlyText,
|
|
5
6
|
parseJsonObject
|
|
6
7
|
} from "@jskit-ai/assistant-core/shared";
|
|
7
|
-
import { isAssistantProgressOnlyText } from "../../shared/assistantResponseText.js";
|
|
8
8
|
import { conversationTurnsFromMessages } from "@jskit-ai/assistant-core/shared/conversation";
|
|
9
9
|
|
|
10
10
|
const CONVERSATION_MESSAGE_STATUS = Object.freeze({
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { applyConversationLogPatch, mergeConversationStream } from "@jskit-ai/assistant-core/shared/conversation";
|
|
2
|
+
import {
|
|
3
|
+
ASSISTANT_CONVERSATION_EVENT,
|
|
4
|
+
ASSISTANT_CONVERSATION_SUBSCRIBE,
|
|
5
|
+
ASSISTANT_CONVERSATION_UNSUBSCRIBE
|
|
6
|
+
} from "../../shared/conversationRealtime.js";
|
|
7
|
+
|
|
8
|
+
const SNAPSHOT_EVENTS = new Set(["accepted", "settled", "configuration", "phase", "error", "replaced", "goal"]);
|
|
9
|
+
|
|
10
|
+
function subscribeAssistantConversation({
|
|
11
|
+
socket, conversationId, targetSurfaceId, hostSurfaceId, workspaceSlug,
|
|
12
|
+
read, onState, onEvent = () => {}, onError = () => {}
|
|
13
|
+
} = {}) {
|
|
14
|
+
if (typeof socket?.on !== "function" || typeof socket?.off !== "function" || typeof socket?.timeout !== "function") {
|
|
15
|
+
throw new TypeError("Conversation subscriptions require the application's shared realtime socket.");
|
|
16
|
+
}
|
|
17
|
+
if (typeof read !== "function" || typeof onState !== "function" || typeof onEvent !== "function") {
|
|
18
|
+
throw new TypeError("Conversation subscriptions require a canonical read operation and state listener.");
|
|
19
|
+
}
|
|
20
|
+
const subscriptionId = crypto.randomUUID();
|
|
21
|
+
const input = { subscriptionId, conversationId, targetSurfaceId, hostSurfaceId,
|
|
22
|
+
...(workspaceSlug === undefined ? {} : { workspaceSlug }) };
|
|
23
|
+
let disposed = false;
|
|
24
|
+
let generation = 0;
|
|
25
|
+
let epoch = null;
|
|
26
|
+
let revision = 0;
|
|
27
|
+
let pending = [];
|
|
28
|
+
let listening = false;
|
|
29
|
+
let state = null;
|
|
30
|
+
let streaming = null;
|
|
31
|
+
let reloadInFlight = null;
|
|
32
|
+
let reloadQueued = false;
|
|
33
|
+
const completed = new Map();
|
|
34
|
+
const settledTurns = new Set();
|
|
35
|
+
const pendingReads = new Set();
|
|
36
|
+
|
|
37
|
+
function savedMessageIds() {
|
|
38
|
+
return new Set((state?.conversationLog || []).flatMap(turn => turn.messages || [
|
|
39
|
+
turn.system, turn.user, ...(turn.thinking || []), ...(turn.commentary || []), turn.assistant
|
|
40
|
+
].filter(Boolean)).map(message => message.messageId));
|
|
41
|
+
}
|
|
42
|
+
function receiveStreaming(snapshot) {
|
|
43
|
+
if (!Number.isSafeInteger(snapshot?.revision) || (streaming && snapshot.revision <= streaming.revision)) return false;
|
|
44
|
+
streaming = snapshot;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function publishState({ initial = false, canonical = false } = {}) {
|
|
48
|
+
if (!state) return;
|
|
49
|
+
const saved = savedMessageIds();
|
|
50
|
+
for (const id of completed.keys()) if (saved.has(id)) completed.delete(id);
|
|
51
|
+
const messages = new Map([...completed].map(([id, entry]) => [id, entry.message]));
|
|
52
|
+
for (const message of streaming?.messages || []) messages.set(message.messageId, message);
|
|
53
|
+
// Saved history owns completed identities; events only supply a display overlay.
|
|
54
|
+
onState({ ...state, streaming, turns: mergeConversationStream(state.conversationLog, { messages: [...messages.values()] }) }, { initial, canonical });
|
|
55
|
+
}
|
|
56
|
+
function receiveState(snapshot, options) {
|
|
57
|
+
state = snapshot;
|
|
58
|
+
receiveStreaming(snapshot.streaming);
|
|
59
|
+
publishState({ ...options, canonical: true });
|
|
60
|
+
}
|
|
61
|
+
function reload() {
|
|
62
|
+
if (reloadInFlight) {
|
|
63
|
+
reloadQueued = true;
|
|
64
|
+
return reloadInFlight;
|
|
65
|
+
}
|
|
66
|
+
const attempt = generation;
|
|
67
|
+
const settled = new Set(settledTurns);
|
|
68
|
+
settledTurns.clear();
|
|
69
|
+
const current = () => !disposed && listening && attempt === generation;
|
|
70
|
+
const pendingRead = { generation: attempt, patches: [] };
|
|
71
|
+
const job = Promise.resolve().then(() => {
|
|
72
|
+
if (!current()) return null;
|
|
73
|
+
pendingReads.add(pendingRead);
|
|
74
|
+
return read();
|
|
75
|
+
}).then(snapshot => {
|
|
76
|
+
if (!current()) return;
|
|
77
|
+
// Ported from the original history reader: retain only delivered updates
|
|
78
|
+
// received while this request was in flight before replacing its cache.
|
|
79
|
+
for (const patch of pendingRead.patches) {
|
|
80
|
+
snapshot = applyConversationLogPatch(snapshot, patch, { limit: snapshot.pagination?.limit });
|
|
81
|
+
}
|
|
82
|
+
for (const [id, entry] of completed) if (settled.has(entry.turnId)) completed.delete(id);
|
|
83
|
+
receiveState(snapshot);
|
|
84
|
+
}).catch(error => {
|
|
85
|
+
if (current()) onError(error);
|
|
86
|
+
}).finally(() => {
|
|
87
|
+
pendingReads.delete(pendingRead);
|
|
88
|
+
if (reloadInFlight !== job) return;
|
|
89
|
+
reloadInFlight = null;
|
|
90
|
+
if (reloadQueued && current()) {
|
|
91
|
+
reloadQueued = false;
|
|
92
|
+
reload();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
reloadInFlight = job;
|
|
96
|
+
return job;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function deliver(payload) {
|
|
100
|
+
if (payload.streamEpoch !== epoch || !Number.isSafeInteger(payload.streamRevision) || payload.streamRevision <= revision) return;
|
|
101
|
+
revision = payload.streamRevision;
|
|
102
|
+
const event = payload.event;
|
|
103
|
+
let changedPresentation = Object.hasOwn(event, "interimReply");
|
|
104
|
+
if (changedPresentation && state) state = { ...state, interimReply: event.interimReply };
|
|
105
|
+
if (event.type === "transcript") {
|
|
106
|
+
const next = applyConversationLogPatch(state, event.patch, { limit: state?.pagination?.limit });
|
|
107
|
+
if (next) {
|
|
108
|
+
state = next;
|
|
109
|
+
for (const read of pendingReads) {
|
|
110
|
+
if (read.generation === generation) read.patches.push(event.patch);
|
|
111
|
+
}
|
|
112
|
+
receiveStreaming(event.streaming);
|
|
113
|
+
changedPresentation = true;
|
|
114
|
+
}
|
|
115
|
+
} else if (event.type === "message" || event.type === "message-complete") {
|
|
116
|
+
// A later initial read can already contain the result of buffered events.
|
|
117
|
+
const older = streaming && event.streaming?.revision < streaming.revision;
|
|
118
|
+
const completedWhileRunning = event.type === "message" && event.status === "complete" &&
|
|
119
|
+
state?.conversationLog.some(turn => turn.turnId === event.turnId && turn.metadata?.runtime?.status === "running");
|
|
120
|
+
if (!older || completedWhileRunning) {
|
|
121
|
+
const previous = streaming?.messages?.find(message => message.messageId === event.messageId);
|
|
122
|
+
const changed = receiveStreaming(event.streaming);
|
|
123
|
+
if (event.type === "message" && event.status === "complete" && event.text) {
|
|
124
|
+
completed.set(event.messageId, { turnId: event.turnId, message: {
|
|
125
|
+
messageId: event.messageId, role: event.role, text: event.text, status: "complete",
|
|
126
|
+
...(event.outputId ? { outputId: event.outputId } : {}),
|
|
127
|
+
...(event.turnId ? { turnId: event.turnId } : {}),
|
|
128
|
+
...(event.origin ? { origin: event.origin } : {}),
|
|
129
|
+
...(previous?.at ? { at: previous.at } : {})
|
|
130
|
+
} });
|
|
131
|
+
}
|
|
132
|
+
changedPresentation ||= changed || event.status === "complete";
|
|
133
|
+
}
|
|
134
|
+
} else if (SNAPSHOT_EVENTS.has(event.type)) {
|
|
135
|
+
if (event.type === "settled") settledTurns.add(event.turnId);
|
|
136
|
+
if (event.type === "replaced") completed.clear();
|
|
137
|
+
reload();
|
|
138
|
+
}
|
|
139
|
+
if (changedPresentation) publishState();
|
|
140
|
+
// Events are notifications. Transcript presentation comes from onState.turns.
|
|
141
|
+
onEvent(event);
|
|
142
|
+
}
|
|
143
|
+
function receive(payload) {
|
|
144
|
+
if (disposed || !listening || payload?.subscriptionId !== subscriptionId || payload.conversationId !== conversationId) return;
|
|
145
|
+
if (epoch === null) pending.push(payload);
|
|
146
|
+
else deliver(payload);
|
|
147
|
+
}
|
|
148
|
+
function subscribe() {
|
|
149
|
+
if (disposed || !socket.connected) return;
|
|
150
|
+
const attempt = ++generation;
|
|
151
|
+
epoch = null;
|
|
152
|
+
revision = 0;
|
|
153
|
+
pending = [];
|
|
154
|
+
listening = true;
|
|
155
|
+
state = null;
|
|
156
|
+
streaming = null;
|
|
157
|
+
completed.clear();
|
|
158
|
+
settledTurns.clear();
|
|
159
|
+
reloadInFlight = null;
|
|
160
|
+
reloadQueued = false;
|
|
161
|
+
socket.timeout(10_000).emit(ASSISTANT_CONVERSATION_SUBSCRIBE, input, (error, response) => {
|
|
162
|
+
if (disposed || attempt !== generation) return;
|
|
163
|
+
if (error || response?.ok !== true) {
|
|
164
|
+
listening = false;
|
|
165
|
+
pending = [];
|
|
166
|
+
if (socket.connected) socket.emit(ASSISTANT_CONVERSATION_UNSUBSCRIBE, { subscriptionId });
|
|
167
|
+
onError(error || Object.assign(new Error(response?.error || "Conversation subscription failed."), {
|
|
168
|
+
code: response?.code, statusCode: response?.status
|
|
169
|
+
}));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
epoch = response.streamEpoch;
|
|
173
|
+
receiveState(response.state, { initial: true });
|
|
174
|
+
for (const payload of pending.sort((left, right) => left.streamRevision - right.streamRevision)) deliver(payload);
|
|
175
|
+
pending = [];
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function disconnected() {
|
|
179
|
+
generation += 1;
|
|
180
|
+
epoch = null;
|
|
181
|
+
pending = [];
|
|
182
|
+
listening = false;
|
|
183
|
+
reloadInFlight = null;
|
|
184
|
+
reloadQueued = false;
|
|
185
|
+
}
|
|
186
|
+
socket.on(ASSISTANT_CONVERSATION_EVENT, receive);
|
|
187
|
+
socket.on("connect", subscribe);
|
|
188
|
+
socket.on("disconnect", disconnected);
|
|
189
|
+
subscribe();
|
|
190
|
+
|
|
191
|
+
function release() {
|
|
192
|
+
if (disposed) return;
|
|
193
|
+
disposed = true;
|
|
194
|
+
generation += 1;
|
|
195
|
+
pending = [];
|
|
196
|
+
completed.clear();
|
|
197
|
+
socket.off(ASSISTANT_CONVERSATION_EVENT, receive);
|
|
198
|
+
socket.off("connect", subscribe);
|
|
199
|
+
socket.off("disconnect", disconnected);
|
|
200
|
+
if (socket.connected) socket.emit(ASSISTANT_CONVERSATION_UNSUBSCRIBE, { subscriptionId });
|
|
201
|
+
}
|
|
202
|
+
return Object.assign(release, { reload });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export { subscribeAssistantConversation };
|
|
@@ -1,119 +1,26 @@
|
|
|
1
1
|
import { defineFeature } from "@jskit-ai/kernel/server/features";
|
|
2
|
-
import {
|
|
3
|
-
import { AppError } from "@jskit-ai/kernel/server/runtime";
|
|
4
|
-
import { createAiClient, createAiConnectionClient } from "@jskit-ai/assistant-core/server";
|
|
2
|
+
import { actionIds } from "./actionIds.js";
|
|
5
3
|
import { createAssistantActions } from "./actions.js";
|
|
6
4
|
import { registerRoutes } from "./registerRoutes.js";
|
|
7
|
-
import {
|
|
8
|
-
import { createRepository as createConversationsRepository } from "./repositories/conversationsRepository.js";
|
|
9
|
-
import { createRepository as createMessagesRepository } from "./repositories/messagesRepository.js";
|
|
10
|
-
import { createRepository as createTurnRequestsRepository } from "./repositories/turnRequestsRepository.js";
|
|
11
|
-
import { createService as createAssistantConfigService } from "./services/assistantConfigService.js";
|
|
12
|
-
import { createChatService } from "./services/chatService.js";
|
|
13
|
-
import { createTranscriptService } from "./services/transcriptService.js";
|
|
14
|
-
import { resolveAssistantAiConfig, resolveAssistantServerConfig } from "./support/assistantServerConfig.js";
|
|
15
|
-
import { createSurfaceAwareToolCatalog } from "./support/createSurfaceAwareToolCatalog.js";
|
|
5
|
+
import { registerConversationSubscriptions } from "./registerConversationSubscriptions.js";
|
|
16
6
|
|
|
17
|
-
|
|
18
|
-
const appConfig = normalizeObject(config.appConfig);
|
|
19
|
-
const env = normalizeObject(config.env);
|
|
20
|
-
const cache = new Map();
|
|
21
|
-
|
|
22
|
-
return Object.freeze({
|
|
23
|
-
async resolveClient(targetSurfaceId = "", { context, integrationId } = {}) {
|
|
24
|
-
const normalizedTargetSurfaceId = normalizeText(targetSurfaceId).toLowerCase();
|
|
25
|
-
if (!normalizedTargetSurfaceId) {
|
|
26
|
-
throw new Error("assistant.ai.client.factory.resolveClient requires targetSurfaceId.");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const surfaceConfig = resolveAssistantServerConfig(appConfig, normalizedTargetSurfaceId);
|
|
30
|
-
if (surfaceConfig.aiIntegrationId) {
|
|
31
|
-
const selected = integrationId || surfaceConfig.aiIntegrationId;
|
|
32
|
-
if (![surfaceConfig.aiIntegrationId, ...surfaceConfig.aiIntegrationIds].includes(selected)) {
|
|
33
|
-
throw new AppError(403, "This AI connection is not available in this assistant.");
|
|
34
|
-
}
|
|
35
|
-
if (!config.aiConnections?.resolve) throw new Error("The assistant requires an authorized AI connection resolver.");
|
|
36
|
-
const connection = await config.aiConnections.resolve({ context, integrationId: selected });
|
|
37
|
-
return createAiConnectionClient(connection, { timeoutMs: surfaceConfig.timeoutMs });
|
|
38
|
-
}
|
|
39
|
-
if (integrationId) throw new AppError(403, "This assistant does not allow selecting an AI connection.");
|
|
40
|
-
|
|
41
|
-
if (cache.has(normalizedTargetSurfaceId)) {
|
|
42
|
-
return cache.get(normalizedTargetSurfaceId);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const assistantAiConfig = resolveAssistantAiConfig(
|
|
46
|
-
{
|
|
47
|
-
appConfig,
|
|
48
|
-
env
|
|
49
|
-
},
|
|
50
|
-
normalizedTargetSurfaceId
|
|
51
|
-
);
|
|
52
|
-
const client = createAiClient(assistantAiConfig.ai);
|
|
53
|
-
cache.set(normalizedTargetSurfaceId, client);
|
|
54
|
-
return client;
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function createAssistantRuntime({
|
|
60
|
-
actionCatalogue,
|
|
61
|
-
config,
|
|
62
|
-
consoleRuntime,
|
|
63
|
-
database,
|
|
64
|
-
env,
|
|
65
|
-
aiConnections,
|
|
66
|
-
attachments,
|
|
67
|
-
workspaces
|
|
68
|
-
} = {}) {
|
|
69
|
-
const assistantConfigRepository = createAssistantConfigRepository(database.knex);
|
|
70
|
-
const conversationsRepository = createConversationsRepository(database.knex);
|
|
71
|
-
const messagesRepository = createMessagesRepository(database.knex);
|
|
72
|
-
const turnRequests = createTurnRequestsRepository(database.knex);
|
|
73
|
-
const workspaceScopeSupport = workspaces?.scope || null;
|
|
74
|
-
const aiClientFactory = createAssistantAiClientFactory({ appConfig: config, env, aiConnections });
|
|
75
|
-
const toolCatalog = createSurfaceAwareToolCatalog(actionCatalogue, { appConfig: config });
|
|
76
|
-
const configService = createAssistantConfigService({
|
|
77
|
-
assistantConfigRepository,
|
|
78
|
-
consoleService: consoleRuntime?.services?.access || null,
|
|
79
|
-
appConfig: config,
|
|
80
|
-
workspaceScopeSupport
|
|
81
|
-
});
|
|
82
|
-
const transcriptService = createTranscriptService({ conversationsRepository, messagesRepository });
|
|
83
|
-
const chatService = createChatService({
|
|
84
|
-
turnRequests,
|
|
85
|
-
attachments,
|
|
86
|
-
aiClientFactory,
|
|
87
|
-
transcriptService,
|
|
88
|
-
serviceToolCatalog: toolCatalog,
|
|
89
|
-
assistantConfigService: configService,
|
|
90
|
-
appConfig: config,
|
|
91
|
-
workspaceScopeSupport
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
return Object.freeze({
|
|
95
|
-
repositories: Object.freeze({
|
|
96
|
-
config: assistantConfigRepository,
|
|
97
|
-
conversations: conversationsRepository,
|
|
98
|
-
messages: messagesRepository,
|
|
99
|
-
turnRequests
|
|
100
|
-
}),
|
|
101
|
-
services: Object.freeze({ chat: chatService, config: configService, transcript: transcriptService }),
|
|
102
|
-
aiClientFactory,
|
|
103
|
-
toolCatalog
|
|
104
|
-
});
|
|
105
|
-
}
|
|
7
|
+
const subscriptionLifecycle = new WeakMap();
|
|
106
8
|
|
|
107
9
|
const AssistantFeature = defineFeature({
|
|
108
10
|
id: "assistant.runtime",
|
|
109
11
|
domain: "assistant",
|
|
110
12
|
requires: {
|
|
111
13
|
config: "runtime.config",
|
|
112
|
-
database: "runtime.database",
|
|
113
14
|
env: "runtime.env",
|
|
114
15
|
http: "runtime.http"
|
|
115
16
|
},
|
|
116
17
|
optional: {
|
|
18
|
+
conversationRuntime: "assistant.conversations",
|
|
19
|
+
conversationAccess: "assistant.conversation.access",
|
|
20
|
+
assistantConfigService: "assistant.settings",
|
|
21
|
+
database: "runtime.database",
|
|
22
|
+
realtime: "runtime.realtime",
|
|
23
|
+
events: "runtime.events",
|
|
117
24
|
aiConnections: "integrations.ai",
|
|
118
25
|
attachments: "assistant.attachments",
|
|
119
26
|
consoleRuntime: "console.core",
|
|
@@ -122,21 +29,90 @@ const AssistantFeature = defineFeature({
|
|
|
122
29
|
provides: {
|
|
123
30
|
assistant: "assistant.runtime"
|
|
124
31
|
},
|
|
125
|
-
setup(dependencies, { actionCatalogue }) {
|
|
126
|
-
const
|
|
32
|
+
async setup(dependencies, { actionCatalogue }) {
|
|
33
|
+
const { conversationRuntime, conversationAccess, assistantConfigService } = dependencies;
|
|
34
|
+
if (conversationAccess != null) {
|
|
35
|
+
if (!conversationRuntime || typeof conversationAccess.router?.register !== "function" ||
|
|
36
|
+
typeof conversationAccess.wrapAction !== "function") {
|
|
37
|
+
throw new TypeError("assistant.conversation.access requires assistant.conversations, router.register() and wrapAction().");
|
|
38
|
+
}
|
|
39
|
+
if (!["authenticated", "host"].includes(conversationAccess.requestPolicy) ||
|
|
40
|
+
typeof conversationAccess.subscribeActionId !== "string" || !conversationAccess.subscribeActionId.trim() ||
|
|
41
|
+
(conversationAccess.requestPolicy === "host" && conversationAccess.subscribeActionId === actionIds.conversationSubscribe)) {
|
|
42
|
+
throw new TypeError("assistant.conversation.access requires a fixed subscribeActionId and authenticated or host requestPolicy.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let assistant;
|
|
46
|
+
if (conversationRuntime) {
|
|
47
|
+
if (typeof conversationRuntime.open !== "function") {
|
|
48
|
+
throw new TypeError("assistant.conversations requires createConversationRuntime().");
|
|
49
|
+
}
|
|
50
|
+
assistant = Object.freeze({ conversationRuntime,
|
|
51
|
+
services: Object.freeze({ conversations: conversationRuntime, config: assistantConfigService || null }) });
|
|
52
|
+
} else {
|
|
53
|
+
if (!dependencies.database?.knex) {
|
|
54
|
+
throw new Error("AssistantFeature requires assistant.conversations, or runtime.database for database integration.");
|
|
55
|
+
}
|
|
56
|
+
const { createAssistantRuntime } = await import("./createAssistantRuntime.js");
|
|
57
|
+
const { createRepository: createAssistantConfigRepository } = await import("./repositories/assistantConfigRepository.js");
|
|
58
|
+
const { createRepository: createConversationsRepository } = await import("./repositories/conversationsRepository.js");
|
|
59
|
+
const { createRepository: createMessagesRepository } = await import("./repositories/messagesRepository.js");
|
|
60
|
+
const { createRepository: createTurnRequestsRepository } = await import("./repositories/turnRequestsRepository.js");
|
|
61
|
+
assistant = createAssistantRuntime({
|
|
62
|
+
...dependencies,
|
|
63
|
+
actionCatalogue,
|
|
64
|
+
repositories: {
|
|
65
|
+
config: createAssistantConfigRepository(dependencies.database.knex),
|
|
66
|
+
conversations: createConversationsRepository(dependencies.database.knex),
|
|
67
|
+
messages: createMessagesRepository(dependencies.database.knex),
|
|
68
|
+
turnRequests: createTurnRequestsRepository(dependencies.database.knex)
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
127
72
|
registerRoutes(dependencies.http.router, {
|
|
128
73
|
config: dependencies.config,
|
|
74
|
+
conversationRuntime,
|
|
75
|
+
conversationAccess,
|
|
76
|
+
conversationDataSchema: conversationRuntime?.conversationDataSchema ?? null,
|
|
77
|
+
conversationConfigurationSchema: conversationRuntime?.conversationConfigurationSchema ?? null,
|
|
78
|
+
conversationSelectionSchema: conversationRuntime?.conversationSelectionSchema ?? null,
|
|
79
|
+
conversationReplacementSchema: conversationRuntime?.conversationReplacementSchema ?? null,
|
|
80
|
+
assistantConfigService,
|
|
129
81
|
workspaceScopeSupport: dependencies.workspaces?.scope || null
|
|
130
82
|
});
|
|
83
|
+
subscriptionLifecycle.set(assistant, { actions: actionCatalogue });
|
|
131
84
|
return { assistant };
|
|
132
85
|
},
|
|
133
|
-
actions({ assistant, config }) {
|
|
86
|
+
actions({ assistant, conversationAccess, config, workspaces }) {
|
|
134
87
|
return createAssistantActions({
|
|
135
88
|
assistantConfigService: assistant.services.config,
|
|
136
89
|
chatService: assistant.services.chat,
|
|
90
|
+
conversationRuntime: assistant.conversationRuntime,
|
|
91
|
+
conversationAccess,
|
|
92
|
+
conversationDataSchema: assistant.conversationRuntime?.conversationDataSchema ?? null,
|
|
93
|
+
conversationConfigurationSchema: assistant.conversationRuntime?.conversationConfigurationSchema ?? null,
|
|
94
|
+
conversationSelectionSchema: assistant.conversationRuntime?.conversationSelectionSchema ?? null,
|
|
95
|
+
conversationReplacementSchema: assistant.conversationRuntime?.conversationReplacementSchema ?? null,
|
|
96
|
+
workspaceScopeSupport: workspaces?.scope || null,
|
|
137
97
|
config
|
|
138
98
|
});
|
|
99
|
+
},
|
|
100
|
+
boot({ conversationRuntime, conversationAccess, realtime, events, config, workspaces }, { outputs }) {
|
|
101
|
+
if (!conversationRuntime || !realtime) return;
|
|
102
|
+
const state = subscriptionLifecycle.get(outputs.assistant);
|
|
103
|
+
state.release = registerConversationSubscriptions({
|
|
104
|
+
realtime, events, actions: state.actions, config, workspaceScopeSupport: workspaces?.scope || null,
|
|
105
|
+
...(conversationAccess ? {
|
|
106
|
+
subscribeActionId: conversationAccess.subscribeActionId,
|
|
107
|
+
requestPolicy: conversationAccess.requestPolicy
|
|
108
|
+
} : {})
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
async shutdown(_dependencies, { outputs }) {
|
|
112
|
+
subscriptionLifecycle.get(outputs.assistant)?.release?.();
|
|
113
|
+
subscriptionLifecycle.delete(outputs.assistant);
|
|
114
|
+
await outputs.assistant.services.chat?.close();
|
|
139
115
|
}
|
|
140
116
|
});
|
|
141
117
|
|
|
142
|
-
export { AssistantFeature
|
|
118
|
+
export { AssistantFeature };
|
package/src/server/actionIds.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
const actionIds = Object.freeze({
|
|
2
2
|
chatStream: "assistant.chat.stream",
|
|
3
|
+
conversationRead: "assistant.conversation.read",
|
|
4
|
+
conversationSend: "assistant.conversation.send",
|
|
5
|
+
conversationCancel: "assistant.conversation.cancel",
|
|
6
|
+
conversationSubscribe: "assistant.conversation.subscribe",
|
|
7
|
+
conversationInspectDelivery: "assistant.conversation.delivery.inspect",
|
|
8
|
+
conversationGoalRead: "assistant.conversation.goal.read",
|
|
9
|
+
conversationGoalUpdate: "assistant.conversation.goal.update",
|
|
10
|
+
conversationConfigure: "assistant.conversation.configure",
|
|
11
|
+
conversationSelect: "assistant.conversation.select",
|
|
12
|
+
conversationReplace: "assistant.conversation.replace",
|
|
3
13
|
conversationsList: "assistant.conversations.list",
|
|
4
14
|
conversationMessagesList: "assistant.conversation.messages.list",
|
|
5
15
|
settingsRead: "assistant.settings.read",
|