@jskit-ai/assistant-runtime 0.1.1
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/package.descriptor.mjs +136 -0
- package/package.json +21 -0
- package/src/client/components/AssistantSettingsClientElement.vue +204 -0
- package/src/client/components/AssistantSurfaceClientElement.vue +19 -0
- package/src/client/composables/useAssistantRuntime.js +759 -0
- package/src/client/index.js +4 -0
- package/src/client/providers/AssistantClientProvider.js +16 -0
- package/src/server/AssistantProvider.js +152 -0
- package/src/server/actionIds.js +9 -0
- package/src/server/actions.js +151 -0
- package/src/server/inputValidators.js +41 -0
- package/src/server/registerRoutes.js +450 -0
- package/src/server/repositories/assistantConfigRepository.js +148 -0
- package/src/server/repositories/conversationsRepository.js +263 -0
- package/src/server/repositories/messagesRepository.js +166 -0
- package/src/server/services/assistantConfigService.js +132 -0
- package/src/server/services/chatService.js +1048 -0
- package/src/server/services/transcriptService.js +331 -0
- package/src/server/support/assistantServerConfig.js +106 -0
- package/src/server/support/createSurfaceAwareToolCatalog.js +64 -0
- package/src/shared/assistantRuntimeConfig.js +7 -0
- package/src/shared/assistantSurfaces.js +97 -0
- package/src/shared/index.js +7 -0
- package/templates/migrations/assistant_config_initial.cjs +27 -0
- package/templates/migrations/assistant_transcripts_initial.cjs +58 -0
- package/test/assistantServerConfig.test.js +72 -0
- package/test/assistantSurfaces.test.js +50 -0
- package/test/createSurfaceAwareToolCatalog.test.js +77 -0
- package/test/lazyAppConfig.test.js +248 -0
- package/test/packageDescriptor.test.js +34 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
import { computed, ref, watch } from "vue";
|
|
2
|
+
import { useQueryClient } from "@tanstack/vue-query";
|
|
3
|
+
import { getClientAppConfig } from "@jskit-ai/kernel/client";
|
|
4
|
+
import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
5
|
+
import { buildAssistantApiPath } from "@jskit-ai/assistant-core/shared";
|
|
6
|
+
import {
|
|
7
|
+
ASSISTANT_STREAM_EVENT_TYPES,
|
|
8
|
+
MAX_HISTORY_MESSAGES,
|
|
9
|
+
MAX_INPUT_CHARS,
|
|
10
|
+
assistantConversationMessagesQueryKey,
|
|
11
|
+
assistantConversationsListQueryKey,
|
|
12
|
+
assistantScopeQueryKey,
|
|
13
|
+
normalizeAssistantStreamEventType,
|
|
14
|
+
normalizeConversationStatus as normalizeAssistantConversationStatus,
|
|
15
|
+
parseJsonObject,
|
|
16
|
+
toPositiveInteger
|
|
17
|
+
} from "@jskit-ai/assistant-core/shared";
|
|
18
|
+
import {
|
|
19
|
+
assistantHttpClient,
|
|
20
|
+
createAssistantApi
|
|
21
|
+
} from "@jskit-ai/assistant-core/client";
|
|
22
|
+
import { useShellWebErrorRuntime } from "@jskit-ai/shell-web/client/error";
|
|
23
|
+
import { usePagedCollection } from "@jskit-ai/users-web/client/composables/usePagedCollection";
|
|
24
|
+
import { useWorkspaceRouteContext } from "@jskit-ai/users-web/client/composables/useWorkspaceRouteContext";
|
|
25
|
+
import { resolveAssistantSurfaceConfig } from "../../shared/assistantSurfaces.js";
|
|
26
|
+
|
|
27
|
+
const DEFAULT_STREAM_TIMEOUT_MS = 120_000;
|
|
28
|
+
const DEFAULT_HISTORY_PAGE_SIZE = 20;
|
|
29
|
+
const DEFAULT_MESSAGES_PAGE_SIZE = 200;
|
|
30
|
+
const DEFAULT_HISTORY_STALE_TIME_MS = 60_000;
|
|
31
|
+
const RESTORE_MESSAGES_PAGE = 1;
|
|
32
|
+
|
|
33
|
+
function toNonNegativeInteger(value, fallback = 0) {
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildScopeStorageKey(scope = {}) {
|
|
43
|
+
const runtimeSurfaceId = normalizeText(scope?.targetSurfaceId).toLowerCase() || "assistant";
|
|
44
|
+
const workspaceSlug = normalizeText(scope?.workspaceSlug).toLowerCase() || "global";
|
|
45
|
+
return `assistant.activeConversationId:${runtimeSurfaceId}:${workspaceSlug}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readStoredActiveConversationId(scope = {}) {
|
|
49
|
+
if (typeof window === "undefined" || !window.sessionStorage) {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
return toPositiveInteger(window.sessionStorage.getItem(buildScopeStorageKey(scope)), 0);
|
|
55
|
+
} catch {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeStoredActiveConversationId(scope = {}, conversationId) {
|
|
61
|
+
if (typeof window === "undefined" || !window.sessionStorage) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const normalizedConversationId = toPositiveInteger(conversationId, 0);
|
|
66
|
+
const storageKey = buildScopeStorageKey(scope);
|
|
67
|
+
try {
|
|
68
|
+
if (normalizedConversationId > 0) {
|
|
69
|
+
window.sessionStorage.setItem(storageKey, String(normalizedConversationId));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
window.sessionStorage.removeItem(storageKey);
|
|
74
|
+
} catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildId(prefix = "id") {
|
|
80
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
81
|
+
return `${prefix}_${crypto.randomUUID()}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeToolName(value) {
|
|
88
|
+
return normalizeText(value) || "tool";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeConversationStatus(value) {
|
|
92
|
+
return normalizeAssistantConversationStatus(value, {
|
|
93
|
+
fallback: "unknown"
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatConversationStartedAt(value) {
|
|
98
|
+
const source = normalizeText(value);
|
|
99
|
+
if (!source) {
|
|
100
|
+
return "unknown";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const date = new Date(source);
|
|
104
|
+
if (Number.isNaN(date.getTime())) {
|
|
105
|
+
return "unknown";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return date.toLocaleString();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseToolResultPayload(value) {
|
|
112
|
+
return parseJsonObject(value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildHistory(messages) {
|
|
116
|
+
const normalizedHistory = (Array.isArray(messages) ? messages : [])
|
|
117
|
+
.filter((message) => {
|
|
118
|
+
if (!message || typeof message !== "object") {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (message.kind !== "chat") {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (message.role !== "user" && message.role !== "assistant") {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (normalizeText(message.status).toLowerCase() !== "done") {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return Boolean(normalizeText(message.text));
|
|
131
|
+
})
|
|
132
|
+
.map((message) => ({
|
|
133
|
+
role: message.role,
|
|
134
|
+
content: String(message.text || "").slice(0, MAX_INPUT_CHARS)
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
return normalizedHistory.slice(-MAX_HISTORY_MESSAGES);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function mapTranscriptEntriesToAssistantState(entries) {
|
|
141
|
+
const sourceEntries = Array.isArray(entries) ? entries : [];
|
|
142
|
+
const messages = [];
|
|
143
|
+
const toolEventsById = new Map();
|
|
144
|
+
|
|
145
|
+
function ensureToolEvent(toolCallId, toolName) {
|
|
146
|
+
const key = normalizeText(toolCallId) || buildId("tool_call");
|
|
147
|
+
if (toolEventsById.has(key)) {
|
|
148
|
+
return toolEventsById.get(key);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const next = {
|
|
152
|
+
id: key,
|
|
153
|
+
name: normalizeToolName(toolName),
|
|
154
|
+
arguments: "",
|
|
155
|
+
status: "pending",
|
|
156
|
+
result: null,
|
|
157
|
+
error: null
|
|
158
|
+
};
|
|
159
|
+
toolEventsById.set(key, next);
|
|
160
|
+
return next;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const entry of sourceEntries) {
|
|
164
|
+
const role = normalizeText(entry?.role).toLowerCase();
|
|
165
|
+
const kind = normalizeText(entry?.kind).toLowerCase();
|
|
166
|
+
const metadata = normalizeObject(entry?.metadata);
|
|
167
|
+
const messageId = Number(entry?.id) > 0 ? `transcript_${entry.id}` : buildId("transcript");
|
|
168
|
+
|
|
169
|
+
if (kind === "chat" && (role === "user" || role === "assistant")) {
|
|
170
|
+
messages.push({
|
|
171
|
+
id: messageId,
|
|
172
|
+
role,
|
|
173
|
+
kind: "chat",
|
|
174
|
+
text: entry?.contentText == null ? "" : String(entry.contentText),
|
|
175
|
+
status: "done"
|
|
176
|
+
});
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (kind === "tool_call") {
|
|
181
|
+
const toolCallId = normalizeText(metadata.toolCallId) || `tool_call_${messageId}`;
|
|
182
|
+
const toolName = normalizeToolName(metadata.tool);
|
|
183
|
+
const toolEvent = ensureToolEvent(toolCallId, toolName);
|
|
184
|
+
toolEvent.arguments = String(entry?.contentText || "");
|
|
185
|
+
toolEvent.status = "pending";
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (kind === "tool_result") {
|
|
190
|
+
const parsedResult = parseToolResultPayload(entry?.contentText);
|
|
191
|
+
const toolCallId = normalizeText(metadata.toolCallId || parsedResult.toolCallId) || `tool_result_${messageId}`;
|
|
192
|
+
const toolName = normalizeToolName(metadata.tool || parsedResult.tool);
|
|
193
|
+
const toolEvent = ensureToolEvent(toolCallId, toolName);
|
|
194
|
+
const failed = parsedResult.ok === false || metadata.ok === false;
|
|
195
|
+
toolEvent.status = failed ? "failed" : "done";
|
|
196
|
+
toolEvent.result = failed ? null : parsedResult.result;
|
|
197
|
+
toolEvent.error = failed ? parsedResult.error || metadata.error || null : null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
messages,
|
|
203
|
+
pendingToolEvents: [...toolEventsById.values()]
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function resolveRuntimePolicy() {
|
|
208
|
+
const appConfig = getClientAppConfig();
|
|
209
|
+
const assistantConfig = normalizeObject(appConfig?.assistant);
|
|
210
|
+
|
|
211
|
+
return Object.freeze({
|
|
212
|
+
timeoutMs: toPositiveInteger(assistantConfig.timeoutMs, DEFAULT_STREAM_TIMEOUT_MS),
|
|
213
|
+
historyPageSize: toPositiveInteger(assistantConfig.historyPageSize, DEFAULT_HISTORY_PAGE_SIZE),
|
|
214
|
+
restoreMessagesPageSize: toPositiveInteger(assistantConfig.restoreMessagesPageSize, DEFAULT_MESSAGES_PAGE_SIZE),
|
|
215
|
+
historyStaleTimeMs: toNonNegativeInteger(assistantConfig.historyStaleTimeMs, DEFAULT_HISTORY_STALE_TIME_MS)
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function createRuntimeApi({ overrideApi = null, resolveBasePath, resolveSurfaceId = null } = {}) {
|
|
220
|
+
if (overrideApi && typeof overrideApi.streamChat === "function") {
|
|
221
|
+
return overrideApi;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return createAssistantApi({
|
|
225
|
+
request: assistantHttpClient.request,
|
|
226
|
+
requestStream: assistantHttpClient.requestStream,
|
|
227
|
+
resolveBasePath,
|
|
228
|
+
resolveSurfaceId
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function useAssistantRuntime({ api = null, surfaceId = "" } = {}) {
|
|
233
|
+
const runtimePolicy = resolveRuntimePolicy();
|
|
234
|
+
const queryClient = useQueryClient();
|
|
235
|
+
const errorRuntime = useShellWebErrorRuntime();
|
|
236
|
+
const { placementContext, currentSurfaceId, workspaceSlugFromRoute } = useWorkspaceRouteContext();
|
|
237
|
+
const appConfig = getClientAppConfig();
|
|
238
|
+
|
|
239
|
+
const messages = ref([]);
|
|
240
|
+
const input = ref("");
|
|
241
|
+
const isStreaming = ref(false);
|
|
242
|
+
const isRestoringConversation = ref(false);
|
|
243
|
+
const error = ref("");
|
|
244
|
+
const pendingToolEvents = ref([]);
|
|
245
|
+
const conversationId = ref(null);
|
|
246
|
+
const abortController = ref(null);
|
|
247
|
+
|
|
248
|
+
const placementSnapshot = computed(() => normalizeObject(placementContext.value));
|
|
249
|
+
const assistantSurface = computed(() =>
|
|
250
|
+
resolveAssistantSurfaceConfig(appConfig, surfaceId)
|
|
251
|
+
);
|
|
252
|
+
const runtimeScope = computed(() => {
|
|
253
|
+
const workspaceSlug = assistantSurface.value?.runtimeSurfaceRequiresWorkspace
|
|
254
|
+
? normalizeText(workspaceSlugFromRoute.value).toLowerCase()
|
|
255
|
+
: "";
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
targetSurfaceId: normalizeText(assistantSurface.value?.targetSurfaceId).toLowerCase(),
|
|
259
|
+
workspaceSlug,
|
|
260
|
+
workspaceId: assistantSurface.value?.runtimeSurfaceRequiresWorkspace
|
|
261
|
+
? toPositiveInteger(placementSnapshot.value?.workspace?.id, 0)
|
|
262
|
+
: 0
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
const hasRuntimeScope = computed(() =>
|
|
266
|
+
Boolean(assistantSurface.value) &&
|
|
267
|
+
(assistantSurface.value?.runtimeSurfaceRequiresWorkspace ? Boolean(runtimeScope.value.workspaceSlug) : true)
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const runtimeApi = createRuntimeApi({
|
|
271
|
+
overrideApi: api,
|
|
272
|
+
resolveBasePath: () =>
|
|
273
|
+
buildAssistantApiPath({
|
|
274
|
+
requiresWorkspace: assistantSurface.value?.runtimeSurfaceRequiresWorkspace === true,
|
|
275
|
+
workspaceSlug: runtimeScope.value.workspaceSlug,
|
|
276
|
+
suffix: `/${runtimeScope.value.targetSurfaceId}`
|
|
277
|
+
}),
|
|
278
|
+
resolveSurfaceId: () => normalizeText(currentSurfaceId.value).toLowerCase()
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const activeConversationId = computed(() => normalizeText(conversationId.value));
|
|
282
|
+
const isAdminSurface = computed(() => normalizeText(currentSurfaceId.value).toLowerCase() === "admin");
|
|
283
|
+
const canSend = computed(() => {
|
|
284
|
+
return Boolean(
|
|
285
|
+
hasRuntimeScope.value &&
|
|
286
|
+
!isStreaming.value &&
|
|
287
|
+
!isRestoringConversation.value &&
|
|
288
|
+
normalizeText(input.value)
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
const canStartNewConversation = computed(() => Boolean(hasRuntimeScope.value && !isStreaming.value));
|
|
292
|
+
|
|
293
|
+
function setRuntimeError(message, dedupeKey = "") {
|
|
294
|
+
const normalizedMessage = normalizeText(message);
|
|
295
|
+
error.value = normalizedMessage;
|
|
296
|
+
if (!normalizedMessage) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
errorRuntime.report({
|
|
301
|
+
source: "assistant.runtime",
|
|
302
|
+
message: normalizedMessage,
|
|
303
|
+
severity: "error",
|
|
304
|
+
channel: "banner",
|
|
305
|
+
dedupeKey: dedupeKey || `assistant.runtime:error:${normalizedMessage}`,
|
|
306
|
+
dedupeWindowMs: 3000
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const conversationHistoryCollection = usePagedCollection({
|
|
311
|
+
queryKey: computed(() =>
|
|
312
|
+
assistantConversationsListQueryKey(runtimeScope.value, {
|
|
313
|
+
limit: runtimePolicy.historyPageSize
|
|
314
|
+
})
|
|
315
|
+
),
|
|
316
|
+
queryFn: ({ pageParam = null }) =>
|
|
317
|
+
runtimeApi.listConversations({
|
|
318
|
+
cursor: pageParam,
|
|
319
|
+
limit: runtimePolicy.historyPageSize
|
|
320
|
+
}),
|
|
321
|
+
initialPageParam: null,
|
|
322
|
+
dedupeBy(entry) {
|
|
323
|
+
const conversationNumericId = toPositiveInteger(entry?.id, 0);
|
|
324
|
+
return conversationNumericId > 0 ? String(conversationNumericId) : normalizeText(entry?.id);
|
|
325
|
+
},
|
|
326
|
+
enabled: computed(() => hasRuntimeScope.value),
|
|
327
|
+
queryOptions: {
|
|
328
|
+
staleTime: runtimePolicy.historyStaleTimeMs,
|
|
329
|
+
refetchOnMount: false,
|
|
330
|
+
refetchOnWindowFocus: false
|
|
331
|
+
},
|
|
332
|
+
fallbackLoadError: "Unable to load conversation history."
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const conversationHistory = conversationHistoryCollection.items;
|
|
336
|
+
const conversationHistoryLoading = computed(
|
|
337
|
+
() => Boolean(conversationHistoryCollection.isLoading.value && !conversationHistoryCollection.isLoadingMore.value)
|
|
338
|
+
);
|
|
339
|
+
const conversationHistoryLoadingMore = conversationHistoryCollection.isLoadingMore;
|
|
340
|
+
const conversationHistoryHasMore = conversationHistoryCollection.hasMore;
|
|
341
|
+
const conversationHistoryError = conversationHistoryCollection.loadError;
|
|
342
|
+
|
|
343
|
+
watch(conversationId, (nextConversationId, previousConversationId) => {
|
|
344
|
+
if (!hasRuntimeScope.value) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const nextConversationNumericId = toPositiveInteger(nextConversationId, 0);
|
|
349
|
+
if (nextConversationNumericId > 0) {
|
|
350
|
+
writeStoredActiveConversationId(runtimeScope.value, nextConversationNumericId);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const previousConversationNumericId = toPositiveInteger(previousConversationId, 0);
|
|
355
|
+
if (previousConversationNumericId > 0) {
|
|
356
|
+
writeStoredActiveConversationId(runtimeScope.value, 0);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
watch(
|
|
361
|
+
[
|
|
362
|
+
hasRuntimeScope,
|
|
363
|
+
conversationHistoryLoading,
|
|
364
|
+
runtimeScope,
|
|
365
|
+
conversationId,
|
|
366
|
+
conversationHistory,
|
|
367
|
+
isRestoringConversation
|
|
368
|
+
],
|
|
369
|
+
async ([
|
|
370
|
+
nextHasRuntimeScope,
|
|
371
|
+
nextConversationHistoryLoading,
|
|
372
|
+
nextRuntimeScope,
|
|
373
|
+
nextConversationId,
|
|
374
|
+
nextConversationHistory,
|
|
375
|
+
nextIsRestoringConversation
|
|
376
|
+
]) => {
|
|
377
|
+
if (!nextHasRuntimeScope || nextConversationHistoryLoading || nextIsRestoringConversation) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const activeConversationNumericId = toPositiveInteger(nextConversationId, 0);
|
|
382
|
+
if (activeConversationNumericId > 0) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const sourceEntries = Array.isArray(nextConversationHistory) ? nextConversationHistory : [];
|
|
387
|
+
if (sourceEntries.length < 1) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const storedConversationId = readStoredActiveConversationId(nextRuntimeScope);
|
|
392
|
+
if (!storedConversationId) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const hasStoredConversation = sourceEntries.some(
|
|
397
|
+
(entry) => toPositiveInteger(entry?.id, 0) === storedConversationId
|
|
398
|
+
);
|
|
399
|
+
if (!hasStoredConversation) {
|
|
400
|
+
writeStoredActiveConversationId(nextRuntimeScope, 0);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
await selectConversationById(storedConversationId);
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
immediate: true
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
function appendMessage(payload) {
|
|
412
|
+
messages.value = [...messages.value, payload];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function updateMessage(messageId, updater) {
|
|
416
|
+
messages.value = messages.value.map((message) => {
|
|
417
|
+
if (message.id !== messageId) {
|
|
418
|
+
return message;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const patch = typeof updater === "function" ? updater(message) : updater;
|
|
422
|
+
return {
|
|
423
|
+
...message,
|
|
424
|
+
...(patch && typeof patch === "object" ? patch : {})
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function findMessage(messageId) {
|
|
430
|
+
return messages.value.find((entry) => entry.id === messageId) || null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function invalidateConversationScope() {
|
|
434
|
+
if (!hasRuntimeScope.value) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
await queryClient.invalidateQueries({
|
|
439
|
+
queryKey: assistantScopeQueryKey(runtimeScope.value)
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function refreshConversationHistory() {
|
|
444
|
+
if (!hasRuntimeScope.value) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
await conversationHistoryCollection.reload();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function loadMoreConversationHistory() {
|
|
452
|
+
await conversationHistoryCollection.loadMore();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function selectConversationById(nextConversationId) {
|
|
456
|
+
const normalizedConversationId = normalizeText(nextConversationId);
|
|
457
|
+
if (!normalizedConversationId || isStreaming.value || isRestoringConversation.value || !hasRuntimeScope.value) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const parsedConversationId = toPositiveInteger(normalizedConversationId, 0);
|
|
462
|
+
if (!parsedConversationId) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const previousConversationId = conversationId.value;
|
|
467
|
+
conversationId.value = String(parsedConversationId);
|
|
468
|
+
isRestoringConversation.value = true;
|
|
469
|
+
setRuntimeError("");
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
const response = await queryClient.fetchQuery({
|
|
473
|
+
queryKey: assistantConversationMessagesQueryKey(runtimeScope.value, parsedConversationId, {
|
|
474
|
+
page: RESTORE_MESSAGES_PAGE,
|
|
475
|
+
pageSize: runtimePolicy.restoreMessagesPageSize
|
|
476
|
+
}),
|
|
477
|
+
queryFn: () =>
|
|
478
|
+
runtimeApi.getConversationMessages(parsedConversationId, {
|
|
479
|
+
page: RESTORE_MESSAGES_PAGE,
|
|
480
|
+
pageSize: runtimePolicy.restoreMessagesPageSize
|
|
481
|
+
})
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
const restored = mapTranscriptEntriesToAssistantState(response?.entries);
|
|
485
|
+
messages.value = restored.messages;
|
|
486
|
+
pendingToolEvents.value = restored.pendingToolEvents;
|
|
487
|
+
input.value = "";
|
|
488
|
+
} catch (loadError) {
|
|
489
|
+
conversationId.value = previousConversationId;
|
|
490
|
+
setRuntimeError(normalizeText(loadError?.message) || "Unable to load conversation.");
|
|
491
|
+
} finally {
|
|
492
|
+
isRestoringConversation.value = false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function selectConversation(conversation) {
|
|
497
|
+
await selectConversationById(conversation?.id);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function startNewConversation() {
|
|
501
|
+
if (abortController.value) {
|
|
502
|
+
abortController.value.abort();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
messages.value = [];
|
|
506
|
+
pendingToolEvents.value = [];
|
|
507
|
+
input.value = "";
|
|
508
|
+
setRuntimeError("");
|
|
509
|
+
conversationId.value = null;
|
|
510
|
+
writeStoredActiveConversationId(runtimeScope.value, 0);
|
|
511
|
+
isStreaming.value = false;
|
|
512
|
+
isRestoringConversation.value = false;
|
|
513
|
+
abortController.value = null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function handleInputKeydown(event) {
|
|
517
|
+
if (event?.key === "Enter" && isStreaming.value) {
|
|
518
|
+
event.preventDefault();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (
|
|
523
|
+
event?.key === "Enter" &&
|
|
524
|
+
event?.shiftKey !== true &&
|
|
525
|
+
event?.ctrlKey !== true &&
|
|
526
|
+
event?.metaKey !== true &&
|
|
527
|
+
event?.altKey !== true
|
|
528
|
+
) {
|
|
529
|
+
event.preventDefault();
|
|
530
|
+
void sendMessage();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function cancelStream() {
|
|
535
|
+
if (abortController.value) {
|
|
536
|
+
abortController.value.abort();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function sendMessage() {
|
|
541
|
+
const normalizedInput = normalizeText(input.value).slice(0, MAX_INPUT_CHARS);
|
|
542
|
+
if (!normalizedInput || isStreaming.value || isRestoringConversation.value || !hasRuntimeScope.value) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const messageId = buildId("message");
|
|
547
|
+
const assistantMessageId = buildId("assistant");
|
|
548
|
+
const history = buildHistory(messages.value);
|
|
549
|
+
const parsedConversationId = toPositiveInteger(conversationId.value, 0);
|
|
550
|
+
|
|
551
|
+
appendMessage({
|
|
552
|
+
id: buildId("user"),
|
|
553
|
+
role: "user",
|
|
554
|
+
kind: "chat",
|
|
555
|
+
text: normalizedInput,
|
|
556
|
+
status: "done"
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
appendMessage({
|
|
560
|
+
id: assistantMessageId,
|
|
561
|
+
role: "assistant",
|
|
562
|
+
kind: "chat",
|
|
563
|
+
text: "",
|
|
564
|
+
status: "streaming"
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
input.value = "";
|
|
568
|
+
setRuntimeError("");
|
|
569
|
+
isStreaming.value = true;
|
|
570
|
+
|
|
571
|
+
const streamAbortController = new AbortController();
|
|
572
|
+
abortController.value = streamAbortController;
|
|
573
|
+
|
|
574
|
+
const streamTimeout = setTimeout(() => {
|
|
575
|
+
streamAbortController.abort();
|
|
576
|
+
}, runtimePolicy.timeoutMs);
|
|
577
|
+
|
|
578
|
+
let streamDoneStatus = "";
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
await runtimeApi.streamChat(
|
|
582
|
+
{
|
|
583
|
+
messageId,
|
|
584
|
+
...(parsedConversationId > 0 ? { conversationId: parsedConversationId } : {}),
|
|
585
|
+
input: normalizedInput,
|
|
586
|
+
history
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
signal: streamAbortController.signal,
|
|
590
|
+
onEvent(event) {
|
|
591
|
+
const eventType = normalizeAssistantStreamEventType(event?.type, "");
|
|
592
|
+
|
|
593
|
+
if (eventType === ASSISTANT_STREAM_EVENT_TYPES.META && Object.hasOwn(event || {}, "conversationId")) {
|
|
594
|
+
conversationId.value = event?.conversationId ? String(event.conversationId) : null;
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (eventType === ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_DELTA) {
|
|
599
|
+
const delta = String(event?.delta || "");
|
|
600
|
+
if (!delta) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
updateMessage(assistantMessageId, (message) => ({
|
|
605
|
+
text: `${String(message?.text || "")}${delta}`,
|
|
606
|
+
status: "streaming"
|
|
607
|
+
}));
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (eventType === ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE) {
|
|
612
|
+
const text = String(event?.text || "");
|
|
613
|
+
updateMessage(assistantMessageId, {
|
|
614
|
+
text,
|
|
615
|
+
status: "done"
|
|
616
|
+
});
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (eventType === ASSISTANT_STREAM_EVENT_TYPES.TOOL_CALL) {
|
|
621
|
+
const toolCallId = normalizeText(event?.toolCallId) || buildId("tool_call");
|
|
622
|
+
pendingToolEvents.value = [
|
|
623
|
+
...pendingToolEvents.value,
|
|
624
|
+
{
|
|
625
|
+
id: toolCallId,
|
|
626
|
+
name: normalizeToolName(event?.name),
|
|
627
|
+
arguments: String(event?.arguments || ""),
|
|
628
|
+
status: "pending",
|
|
629
|
+
result: null,
|
|
630
|
+
error: null
|
|
631
|
+
}
|
|
632
|
+
];
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (eventType === ASSISTANT_STREAM_EVENT_TYPES.TOOL_RESULT) {
|
|
637
|
+
const toolCallId = normalizeText(event?.toolCallId);
|
|
638
|
+
if (toolCallId) {
|
|
639
|
+
pendingToolEvents.value = pendingToolEvents.value.map((toolEvent) => {
|
|
640
|
+
if (toolEvent.id !== toolCallId) {
|
|
641
|
+
return toolEvent;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const failed = event?.ok === false;
|
|
645
|
+
return {
|
|
646
|
+
...toolEvent,
|
|
647
|
+
status: failed ? "failed" : "done",
|
|
648
|
+
result: failed ? null : event?.result,
|
|
649
|
+
error: failed ? event?.error || null : null
|
|
650
|
+
};
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (eventType === ASSISTANT_STREAM_EVENT_TYPES.ERROR) {
|
|
657
|
+
setRuntimeError(
|
|
658
|
+
normalizeText(event?.message) || "Assistant request failed.",
|
|
659
|
+
"assistant.runtime:stream-event-error"
|
|
660
|
+
);
|
|
661
|
+
updateMessage(assistantMessageId, {
|
|
662
|
+
status: "error"
|
|
663
|
+
});
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (eventType === ASSISTANT_STREAM_EVENT_TYPES.DONE) {
|
|
668
|
+
streamDoneStatus = normalizeText(event?.status).toLowerCase();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
const assistantMessage = findMessage(assistantMessageId);
|
|
675
|
+
const assistantMessageText = normalizeText(assistantMessage?.text);
|
|
676
|
+
if (!assistantMessageText && streamDoneStatus !== "aborted") {
|
|
677
|
+
if (!error.value) {
|
|
678
|
+
setRuntimeError("Assistant returned no output.", "assistant.runtime:empty-output");
|
|
679
|
+
}
|
|
680
|
+
updateMessage(assistantMessageId, {
|
|
681
|
+
status: "error"
|
|
682
|
+
});
|
|
683
|
+
} else if (streamDoneStatus === "aborted") {
|
|
684
|
+
updateMessage(assistantMessageId, {
|
|
685
|
+
status: "canceled"
|
|
686
|
+
});
|
|
687
|
+
} else {
|
|
688
|
+
updateMessage(assistantMessageId, (message) => ({
|
|
689
|
+
status: message.status === "streaming" ? "done" : message.status
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
} catch (streamError) {
|
|
693
|
+
if (String(streamError?.name || "") === "AbortError") {
|
|
694
|
+
updateMessage(assistantMessageId, {
|
|
695
|
+
status: "canceled"
|
|
696
|
+
});
|
|
697
|
+
} else {
|
|
698
|
+
setRuntimeError(normalizeText(streamError?.message) || "Assistant request failed.");
|
|
699
|
+
updateMessage(assistantMessageId, {
|
|
700
|
+
status: "error"
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
} finally {
|
|
704
|
+
clearTimeout(streamTimeout);
|
|
705
|
+
abortController.value = null;
|
|
706
|
+
isStreaming.value = false;
|
|
707
|
+
await invalidateConversationScope();
|
|
708
|
+
await refreshConversationHistory();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const viewer = computed(() => {
|
|
713
|
+
const user = normalizeObject(placementSnapshot.value.user);
|
|
714
|
+
|
|
715
|
+
return {
|
|
716
|
+
displayName: normalizeText(user.displayName || user.name) || "You",
|
|
717
|
+
avatarUrl: normalizeText(user.avatarUrl)
|
|
718
|
+
};
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
return Object.freeze({
|
|
722
|
+
meta: {
|
|
723
|
+
normalizeConversationStatus,
|
|
724
|
+
formatConversationStartedAt
|
|
725
|
+
},
|
|
726
|
+
state: {
|
|
727
|
+
messages,
|
|
728
|
+
input,
|
|
729
|
+
isStreaming,
|
|
730
|
+
isRestoringConversation,
|
|
731
|
+
error,
|
|
732
|
+
pendingToolEvents,
|
|
733
|
+
conversationId,
|
|
734
|
+
activeConversationId,
|
|
735
|
+
conversationHistory,
|
|
736
|
+
conversationHistoryLoading,
|
|
737
|
+
conversationHistoryLoadingMore,
|
|
738
|
+
conversationHistoryHasMore,
|
|
739
|
+
conversationHistoryError,
|
|
740
|
+
isAdminSurface,
|
|
741
|
+
canSend,
|
|
742
|
+
canStartNewConversation
|
|
743
|
+
},
|
|
744
|
+
actions: {
|
|
745
|
+
sendMessage,
|
|
746
|
+
handleInputKeydown,
|
|
747
|
+
cancelStream,
|
|
748
|
+
startNewConversation,
|
|
749
|
+
clearConversation: startNewConversation,
|
|
750
|
+
selectConversation,
|
|
751
|
+
selectConversationById,
|
|
752
|
+
refreshConversationHistory,
|
|
753
|
+
loadMoreConversationHistory
|
|
754
|
+
},
|
|
755
|
+
viewer
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export { useAssistantRuntime };
|