@jskit-ai/assistant 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/package.descriptor.mjs +284 -0
  2. package/package.json +31 -0
  3. package/src/client/components/AssistantClientElement.vue +1316 -0
  4. package/src/client/components/AssistantConsoleSettingsClientElement.vue +71 -0
  5. package/src/client/components/AssistantSettingsFormCard.vue +76 -0
  6. package/src/client/components/AssistantWorkspaceClientElement.vue +15 -0
  7. package/src/client/components/AssistantWorkspaceSettingsClientElement.vue +73 -0
  8. package/src/client/composables/useAssistantWorkspaceRuntime.js +789 -0
  9. package/src/client/index.js +12 -0
  10. package/src/client/lib/assistantApi.js +137 -0
  11. package/src/client/lib/assistantHttpClient.js +10 -0
  12. package/src/client/lib/markdownRenderer.js +31 -0
  13. package/src/client/providers/AssistantWebClientProvider.js +25 -0
  14. package/src/server/AssistantServiceProvider.js +179 -0
  15. package/src/server/actionIds.js +11 -0
  16. package/src/server/actions.js +191 -0
  17. package/src/server/diTokens.js +19 -0
  18. package/src/server/lib/aiClient.js +43 -0
  19. package/src/server/lib/ndjson.js +47 -0
  20. package/src/server/lib/providers/anthropicClient.js +375 -0
  21. package/src/server/lib/providers/common.js +158 -0
  22. package/src/server/lib/providers/deepSeekClient.js +22 -0
  23. package/src/server/lib/providers/openAiClient.js +13 -0
  24. package/src/server/lib/providers/openAiCompatibleClient.js +69 -0
  25. package/src/server/lib/resolveWorkspaceSlug.js +24 -0
  26. package/src/server/lib/serviceToolCatalog.js +459 -0
  27. package/src/server/registerRoutes.js +384 -0
  28. package/src/server/repositories/assistantSettingsRepository.js +100 -0
  29. package/src/server/repositories/conversationsRepository.js +244 -0
  30. package/src/server/repositories/messagesRepository.js +154 -0
  31. package/src/server/repositories/repositoryPersistenceUtils.js +63 -0
  32. package/src/server/services/assistantSettingsService.js +153 -0
  33. package/src/server/services/chatService.js +987 -0
  34. package/src/server/services/transcriptService.js +334 -0
  35. package/src/shared/assistantPaths.js +50 -0
  36. package/src/shared/assistantResource.js +323 -0
  37. package/src/shared/assistantSettingsResource.js +214 -0
  38. package/src/shared/index.js +39 -0
  39. package/src/shared/queryKeys.js +69 -0
  40. package/src/shared/settingsEvents.js +7 -0
  41. package/src/shared/streamEvents.js +31 -0
  42. package/src/shared/support/positiveInteger.js +9 -0
  43. package/templates/migrations/assistant_settings_initial.cjs +39 -0
  44. package/templates/migrations/assistant_transcripts_initial.cjs +51 -0
  45. package/templates/src/pages/admin/workspace/assistant/index.vue +7 -0
  46. package/test/aiConfigValidation.test.js +15 -0
  47. package/test/assistantApiSurfaceHeader.test.js +64 -0
  48. package/test/assistantResource.test.js +53 -0
  49. package/test/assistantSettingsResource.test.js +48 -0
  50. package/test/assistantSettingsService.test.js +133 -0
  51. package/test/chatService.test.js +841 -0
  52. package/test/descriptorSurfaceOption.test.js +35 -0
  53. package/test/queryKeys.test.js +41 -0
  54. package/test/resolveWorkspaceSlug.test.js +83 -0
  55. package/test/routeInputContracts.test.js +287 -0
  56. package/test/serviceToolCatalog.test.js +1235 -0
  57. package/test/transcriptService.test.js +175 -0
@@ -0,0 +1,334 @@
1
+ import { AppError, parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
2
+ import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import { ASSISTANT_TRANSCRIPT_CHANGED_EVENT } from "../../shared/streamEvents.js";
4
+
5
+ const DEFAULT_PAGE_SIZE = 20;
6
+ const MAX_PAGE_SIZE = 200;
7
+ const DEFAULT_MESSAGES_PAGE_SIZE = 200;
8
+ const MAX_MESSAGES_PAGE_SIZE = 500;
9
+ const DEFAULT_CONVERSATION_TITLE = "New conversation";
10
+ const MAX_CONVERSATION_TITLE_LENGTH = 80;
11
+
12
+ const serviceEvents = Object.freeze({
13
+ createConversationForTurn: Object.freeze([
14
+ Object.freeze({
15
+ type: "entity.changed",
16
+ source: "assistant",
17
+ entity: "conversation",
18
+ operation: ({ result }) => (result?.created === true ? "created" : "updated"),
19
+ entityId: ({ result }) => result?.conversation?.id,
20
+ realtime: Object.freeze({
21
+ event: ASSISTANT_TRANSCRIPT_CHANGED_EVENT,
22
+ audience: "event_scope",
23
+ payload: ({ result }) => ({
24
+ conversationId: result?.conversation?.id || null
25
+ })
26
+ })
27
+ })
28
+ ]),
29
+ appendMessage: Object.freeze([
30
+ Object.freeze({
31
+ type: "entity.changed",
32
+ source: "assistant",
33
+ entity: "conversation",
34
+ operation: "updated",
35
+ entityId: ({ result }) => result?.conversationId,
36
+ realtime: Object.freeze({
37
+ event: ASSISTANT_TRANSCRIPT_CHANGED_EVENT,
38
+ audience: "event_scope",
39
+ payload: ({ result }) => ({
40
+ conversationId: result?.conversationId || null
41
+ })
42
+ })
43
+ })
44
+ ]),
45
+ completeConversation: Object.freeze([
46
+ Object.freeze({
47
+ type: "entity.changed",
48
+ source: "assistant",
49
+ entity: "conversation",
50
+ operation: "updated",
51
+ entityId: ({ result }) => result?.id,
52
+ realtime: Object.freeze({
53
+ event: ASSISTANT_TRANSCRIPT_CHANGED_EVENT,
54
+ audience: "event_scope",
55
+ payload: ({ result }) => ({
56
+ conversationId: result?.id || null
57
+ })
58
+ })
59
+ })
60
+ ])
61
+ });
62
+
63
+ function resolveWorkspaceId(workspace) {
64
+ const workspaceId = parsePositiveInteger(workspace?.id || workspace);
65
+ if (!workspaceId) {
66
+ throw new AppError(409, "Workspace selection required.");
67
+ }
68
+
69
+ return workspaceId;
70
+ }
71
+
72
+ function resolveActorUserId(user, { required = false } = {}) {
73
+ const actorUserId = parsePositiveInteger(user?.id);
74
+ if (!actorUserId && required) {
75
+ throw new AppError(401, "Authentication required.");
76
+ }
77
+
78
+ return actorUserId;
79
+ }
80
+
81
+ function normalizePagination(pagination = {}, { defaultPageSize = DEFAULT_PAGE_SIZE, maxPageSize = MAX_PAGE_SIZE } = {}) {
82
+ const page = Math.max(1, parsePositiveInteger(pagination.page) || 1);
83
+ const pageSize = Math.max(1, Math.min(maxPageSize, parsePositiveInteger(pagination.pageSize) || defaultPageSize));
84
+
85
+ return {
86
+ page,
87
+ pageSize
88
+ };
89
+ }
90
+
91
+ function normalizeCursorPagination(query = {}, { defaultLimit = DEFAULT_PAGE_SIZE, maxLimit = MAX_PAGE_SIZE } = {}) {
92
+ const cursor = parsePositiveInteger(query.cursor) || 0;
93
+ const limit = Math.max(1, Math.min(maxLimit, parsePositiveInteger(query.limit) || defaultLimit));
94
+
95
+ return {
96
+ cursor,
97
+ limit
98
+ };
99
+ }
100
+
101
+ function normalizeConversationStatus(value, fallback = "active") {
102
+ const normalized = normalizeText(value).toLowerCase();
103
+ if (normalized === "active" || normalized === "completed" || normalized === "failed" || normalized === "aborted") {
104
+ return normalized;
105
+ }
106
+
107
+ return fallback;
108
+ }
109
+
110
+ function deriveConversationTitleFromMessage(contentText) {
111
+ const normalized = String(contentText == null ? "" : contentText).replace(/\s+/g, " ").trim();
112
+ if (!normalized) {
113
+ return "";
114
+ }
115
+
116
+ return normalized.slice(0, MAX_CONVERSATION_TITLE_LENGTH).trim();
117
+ }
118
+
119
+ function isDefaultConversationTitle(value) {
120
+ return normalizeText(value).toLowerCase() === DEFAULT_CONVERSATION_TITLE.toLowerCase();
121
+ }
122
+
123
+ function createTranscriptService({ conversationsRepository, messagesRepository } = {}) {
124
+ if (!conversationsRepository || !messagesRepository) {
125
+ throw new Error("createTranscriptService requires conversationsRepository and messagesRepository.");
126
+ }
127
+
128
+ async function createConversationForTurn(workspace, user, options = {}) {
129
+ const workspaceId = resolveWorkspaceId(workspace);
130
+ const actorUserId = resolveActorUserId(user, {
131
+ required: true
132
+ });
133
+ const source = normalizeObject(options);
134
+ const conversationId = parsePositiveInteger(source.conversationId);
135
+
136
+ if (conversationId) {
137
+ const existing = await conversationsRepository.findByIdForWorkspaceAndUser(conversationId, workspaceId, actorUserId);
138
+ if (!existing) {
139
+ throw new AppError(404, "Conversation not found.");
140
+ }
141
+
142
+ if (existing.status !== "active") {
143
+ const reopened = await conversationsRepository.updateById(existing.id, {
144
+ status: "active",
145
+ endedAt: null
146
+ });
147
+
148
+ return {
149
+ conversation: reopened,
150
+ created: false
151
+ };
152
+ }
153
+
154
+ return {
155
+ conversation: existing,
156
+ created: false
157
+ };
158
+ }
159
+
160
+ const createdConversation = await conversationsRepository.create({
161
+ workspaceId,
162
+ createdByUserId: actorUserId,
163
+ title: normalizeText(source.title) || DEFAULT_CONVERSATION_TITLE,
164
+ status: "active",
165
+ provider: normalizeText(source.provider),
166
+ model: normalizeText(source.model),
167
+ surfaceId: normalizeText(source.surfaceId).toLowerCase() || "admin",
168
+ metadata: {
169
+ firstMessageId: normalizeText(source.messageId)
170
+ }
171
+ });
172
+
173
+ return {
174
+ conversation: createdConversation,
175
+ created: true
176
+ };
177
+ }
178
+
179
+ async function appendMessage(conversationId, payload = {}, options = {}) {
180
+ const numericConversationId = parsePositiveInteger(conversationId);
181
+ if (!numericConversationId) {
182
+ throw new TypeError("appendMessage requires conversationId.");
183
+ }
184
+
185
+ const source = normalizeObject(payload);
186
+ const context = normalizeObject(options.context);
187
+ const conversation = await conversationsRepository.findById(numericConversationId);
188
+ if (!conversation) {
189
+ throw new AppError(404, "Conversation not found.");
190
+ }
191
+
192
+ const actorUserId = parsePositiveInteger(source.actorUserId) || resolveActorUserId(context.actor) || null;
193
+ const createdMessage = await messagesRepository.create({
194
+ conversationId: numericConversationId,
195
+ workspaceId: conversation.workspaceId,
196
+ role: normalizeText(source.role).toLowerCase(),
197
+ kind: normalizeText(source.kind).toLowerCase() || "chat",
198
+ clientMessageId: normalizeText(source.clientMessageId),
199
+ actorUserId,
200
+ contentText: source.contentText == null ? null : String(source.contentText),
201
+ metadata: normalizeObject(source.metadata)
202
+ });
203
+
204
+ await conversationsRepository.incrementMessageCount(numericConversationId, 1);
205
+
206
+ const messageRole = normalizeText(source.role).toLowerCase();
207
+ const messageKind = normalizeText(source.kind).toLowerCase() || "chat";
208
+ if (messageRole === "user" && messageKind === "chat" && isDefaultConversationTitle(conversation.title)) {
209
+ const derivedTitle = deriveConversationTitleFromMessage(source.contentText);
210
+ if (derivedTitle) {
211
+ await conversationsRepository.updateById(numericConversationId, {
212
+ title: derivedTitle
213
+ });
214
+ }
215
+ }
216
+
217
+ return {
218
+ conversationId: numericConversationId,
219
+ message: createdMessage
220
+ };
221
+ }
222
+
223
+ async function completeConversation(conversationId, payload = {}) {
224
+ const numericConversationId = parsePositiveInteger(conversationId);
225
+ if (!numericConversationId) {
226
+ throw new TypeError("completeConversation requires conversationId.");
227
+ }
228
+
229
+ const source = normalizeObject(payload);
230
+ const existing = await conversationsRepository.findById(numericConversationId);
231
+ if (!existing) {
232
+ throw new AppError(404, "Conversation not found.");
233
+ }
234
+
235
+ return conversationsRepository.updateById(numericConversationId, {
236
+ status: normalizeConversationStatus(source.status, "completed"),
237
+ endedAt: source.endedAt || new Date(),
238
+ metadata: {
239
+ ...normalizeObject(existing.metadata),
240
+ ...normalizeObject(source.metadata)
241
+ }
242
+ });
243
+ }
244
+
245
+ async function listConversationsForUser(workspace, user, query = {}) {
246
+ const workspaceId = resolveWorkspaceId(workspace);
247
+ const actorUserId = resolveActorUserId(user, {
248
+ required: true
249
+ });
250
+ const pagination = normalizeCursorPagination(query, {
251
+ defaultLimit: DEFAULT_PAGE_SIZE,
252
+ maxLimit: MAX_PAGE_SIZE
253
+ });
254
+
255
+ const status = normalizeConversationStatus(query.status, "");
256
+ const filters = {
257
+ ...(status ? { status } : {})
258
+ };
259
+
260
+ return conversationsRepository.listForWorkspaceAndUser(
261
+ workspaceId,
262
+ actorUserId,
263
+ {
264
+ cursor: pagination.cursor,
265
+ limit: pagination.limit
266
+ },
267
+ filters
268
+ );
269
+ }
270
+
271
+ async function getConversationMessagesForUser(workspace, user, conversationId, query = {}) {
272
+ const workspaceId = resolveWorkspaceId(workspace);
273
+ const actorUserId = resolveActorUserId(user, {
274
+ required: true
275
+ });
276
+ const numericConversationId = parsePositiveInteger(conversationId);
277
+ if (!numericConversationId) {
278
+ throw new AppError(400, "Validation failed.", {
279
+ details: {
280
+ fieldErrors: {
281
+ conversationId: "conversationId must be a positive integer."
282
+ }
283
+ }
284
+ });
285
+ }
286
+
287
+ const conversation = await conversationsRepository.findByIdForWorkspaceAndUser(
288
+ numericConversationId,
289
+ workspaceId,
290
+ actorUserId
291
+ );
292
+ if (!conversation) {
293
+ throw new AppError(404, "Conversation not found.");
294
+ }
295
+
296
+ const pagination = normalizePagination(query, {
297
+ defaultPageSize: DEFAULT_MESSAGES_PAGE_SIZE,
298
+ maxPageSize: MAX_MESSAGES_PAGE_SIZE
299
+ });
300
+ const total = await messagesRepository.countByConversationForWorkspace(numericConversationId, workspaceId);
301
+ const totalPages = Math.max(1, Math.ceil(total / pagination.pageSize));
302
+ const page = Math.min(pagination.page, totalPages);
303
+ const entries = await messagesRepository.listByConversationForWorkspace(
304
+ numericConversationId,
305
+ workspaceId,
306
+ {
307
+ page,
308
+ pageSize: pagination.pageSize
309
+ }
310
+ );
311
+
312
+ return {
313
+ conversation,
314
+ entries,
315
+ page,
316
+ pageSize: pagination.pageSize,
317
+ total,
318
+ totalPages
319
+ };
320
+ }
321
+
322
+ return Object.freeze({
323
+ createConversationForTurn,
324
+ appendMessage,
325
+ completeConversation,
326
+ listConversationsForUser,
327
+ getConversationMessagesForUser
328
+ });
329
+ }
330
+
331
+ export {
332
+ createTranscriptService,
333
+ serviceEvents
334
+ };
@@ -0,0 +1,50 @@
1
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
+ import { normalizePathname } from "@jskit-ai/kernel/shared/surface/paths";
3
+ import { normalizeScopedRouteVisibility } from "@jskit-ai/users-core/shared/support/usersVisibility";
4
+
5
+ const ASSISTANT_API_RELATIVE_PATH = "/assistant";
6
+ const ASSISTANT_WORKSPACE_API_BASE_PATH_TEMPLATE = "/api/w/:workspaceSlug/assistant";
7
+ const ASSISTANT_PUBLIC_API_BASE_PATH = `/api${ASSISTANT_API_RELATIVE_PATH}`;
8
+
9
+ function resolveAssistantApiBasePath({ visibility = "workspace" } = {}) {
10
+ const normalizedVisibility = normalizeScopedRouteVisibility(visibility, {
11
+ fallback: "workspace"
12
+ });
13
+ if (normalizedVisibility === "workspace" || normalizedVisibility === "workspace_user") {
14
+ return ASSISTANT_WORKSPACE_API_BASE_PATH_TEMPLATE;
15
+ }
16
+
17
+ return ASSISTANT_PUBLIC_API_BASE_PATH;
18
+ }
19
+
20
+ function resolveAssistantWorkspaceApiBasePath(workspaceSlug = "") {
21
+ const normalizedWorkspaceSlug = normalizeText(workspaceSlug).toLowerCase();
22
+ if (!normalizedWorkspaceSlug) {
23
+ return "";
24
+ }
25
+
26
+ return `/api/w/${encodeURIComponent(normalizedWorkspaceSlug)}${ASSISTANT_API_RELATIVE_PATH}`;
27
+ }
28
+
29
+ function buildAssistantWorkspaceApiPath(workspaceSlug = "", suffix = "/") {
30
+ const basePath = resolveAssistantWorkspaceApiBasePath(workspaceSlug);
31
+ if (!basePath) {
32
+ return "";
33
+ }
34
+
35
+ const normalizedSuffix = normalizePathname(suffix);
36
+ if (normalizedSuffix === "/") {
37
+ return basePath;
38
+ }
39
+
40
+ return `${basePath}${normalizedSuffix}`;
41
+ }
42
+
43
+ export {
44
+ ASSISTANT_API_RELATIVE_PATH,
45
+ ASSISTANT_WORKSPACE_API_BASE_PATH_TEMPLATE,
46
+ ASSISTANT_PUBLIC_API_BASE_PATH,
47
+ resolveAssistantApiBasePath,
48
+ resolveAssistantWorkspaceApiBasePath,
49
+ buildAssistantWorkspaceApiPath
50
+ };
@@ -0,0 +1,323 @@
1
+ import { Type } from "typebox";
2
+ import {
3
+ normalizeObjectInput,
4
+ createCursorListValidator
5
+ } from "@jskit-ai/kernel/shared/validators";
6
+ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
7
+ import { toPositiveInteger } from "./support/positiveInteger.js";
8
+
9
+ const MAX_INPUT_CHARS = 8000;
10
+ const MAX_HISTORY_MESSAGES = 20;
11
+ const MAX_PAGE_SIZE = 200;
12
+ const MAX_MESSAGE_PAGE_SIZE = 500;
13
+
14
+ function normalizePaginationValue(value, fallback, max) {
15
+ const parsed = toPositiveInteger(value, fallback);
16
+ return Math.max(1, Math.min(max, parsed));
17
+ }
18
+
19
+ function normalizeConversationStatus(value) {
20
+ const normalized = normalizeText(value).toLowerCase();
21
+ if (normalized === "active" || normalized === "completed" || normalized === "failed" || normalized === "aborted") {
22
+ return normalized;
23
+ }
24
+
25
+ return "";
26
+ }
27
+
28
+ function normalizeChatStreamBody(payload = {}) {
29
+ const source = normalizeObjectInput(payload);
30
+ const normalized = {
31
+ messageId: normalizeText(source.messageId),
32
+ input: normalizeText(source.input)
33
+ };
34
+
35
+ const conversationId = toPositiveInteger(source.conversationId, 0);
36
+ if (conversationId > 0) {
37
+ normalized.conversationId = conversationId;
38
+ }
39
+
40
+ const history = Array.isArray(source.history) ? source.history : [];
41
+ normalized.history = history
42
+ .slice(0, MAX_HISTORY_MESSAGES)
43
+ .map((entry) => {
44
+ const item = normalizeObjectInput(entry);
45
+ const role = normalizeText(item.role).toLowerCase();
46
+ if (role !== "user" && role !== "assistant") {
47
+ return null;
48
+ }
49
+
50
+ const content = normalizeText(item.content);
51
+ if (!content) {
52
+ return null;
53
+ }
54
+
55
+ return {
56
+ role,
57
+ content: content.slice(0, MAX_INPUT_CHARS)
58
+ };
59
+ })
60
+ .filter(Boolean);
61
+
62
+ const clientContext = normalizeObjectInput(source.clientContext);
63
+ if (Object.keys(clientContext).length > 0) {
64
+ normalized.clientContext = {
65
+ locale: normalizeText(clientContext.locale),
66
+ timezone: normalizeText(clientContext.timezone)
67
+ };
68
+ }
69
+
70
+ return normalized;
71
+ }
72
+
73
+ function normalizeConversationsListQuery(payload = {}) {
74
+ const source = normalizeObjectInput(payload);
75
+ const status = normalizeConversationStatus(source.status);
76
+ const normalized = {};
77
+
78
+ if (Object.hasOwn(source, "cursor")) {
79
+ normalized.cursor = toPositiveInteger(source.cursor, 0);
80
+ }
81
+ if (Object.hasOwn(source, "limit")) {
82
+ normalized.limit = toPositiveInteger(source.limit, 0);
83
+ }
84
+ if (status) {
85
+ normalized.status = status;
86
+ }
87
+
88
+ return normalized;
89
+ }
90
+
91
+ function normalizeConversationMessagesQuery(payload = {}) {
92
+ const source = normalizeObjectInput(payload);
93
+
94
+ return {
95
+ page: normalizePaginationValue(source.page, 1, MAX_MESSAGE_PAGE_SIZE),
96
+ pageSize: normalizePaginationValue(source.pageSize, 200, MAX_MESSAGE_PAGE_SIZE)
97
+ };
98
+ }
99
+
100
+ function normalizeConversationMessagesParams(payload = {}) {
101
+ const source = normalizeObjectInput(payload);
102
+ return {
103
+ conversationId: toPositiveInteger(source.conversationId, 0)
104
+ };
105
+ }
106
+
107
+ function createOptionalPositiveIntegerQuerySchema(max = null) {
108
+ const numericSchema = max == null
109
+ ? Type.Integer({ minimum: 1 })
110
+ : Type.Integer({ minimum: 1, maximum: max });
111
+
112
+ return Type.Optional(
113
+ Type.Union([
114
+ numericSchema,
115
+ Type.String({ pattern: "^[1-9][0-9]*$" })
116
+ ])
117
+ );
118
+ }
119
+
120
+ function normalizeConversationRecord(payload = {}) {
121
+ const source = normalizeObjectInput(payload);
122
+
123
+ return {
124
+ id: toPositiveInteger(source.id, 0),
125
+ workspaceId: toPositiveInteger(source.workspaceId, 0),
126
+ workspaceSlug: normalizeText(source.workspaceSlug),
127
+ workspaceName: normalizeText(source.workspaceName),
128
+ title: normalizeText(source.title),
129
+ createdByUserId: toPositiveInteger(source.createdByUserId, 0) || null,
130
+ createdByUserDisplayName: normalizeText(source.createdByUserDisplayName),
131
+ createdByUserEmail: normalizeText(source.createdByUserEmail),
132
+ status: normalizeText(source.status),
133
+ provider: normalizeText(source.provider),
134
+ model: normalizeText(source.model),
135
+ surfaceId: normalizeText(source.surfaceId),
136
+ startedAt: normalizeText(source.startedAt),
137
+ endedAt: normalizeText(source.endedAt) || null,
138
+ messageCount: Math.max(0, Number(source.messageCount || 0)),
139
+ metadata: normalizeObjectInput(source.metadata),
140
+ createdAt: normalizeText(source.createdAt),
141
+ updatedAt: normalizeText(source.updatedAt)
142
+ };
143
+ }
144
+
145
+ function normalizeConversationMessageRecord(payload = {}) {
146
+ const source = normalizeObjectInput(payload);
147
+
148
+ return {
149
+ id: toPositiveInteger(source.id, 0),
150
+ conversationId: toPositiveInteger(source.conversationId, 0),
151
+ workspaceId: toPositiveInteger(source.workspaceId, 0),
152
+ seq: toPositiveInteger(source.seq, 0),
153
+ role: normalizeText(source.role),
154
+ kind: normalizeText(source.kind),
155
+ clientMessageId: normalizeText(source.clientMessageId),
156
+ actorUserId: toPositiveInteger(source.actorUserId, 0) || null,
157
+ contentText: source.contentText == null ? null : String(source.contentText),
158
+ metadata: normalizeObjectInput(source.metadata),
159
+ createdAt: normalizeText(source.createdAt)
160
+ };
161
+ }
162
+
163
+ const historyMessageSchema = Type.Object(
164
+ {
165
+ role: Type.Union([Type.Literal("user"), Type.Literal("assistant")]),
166
+ content: Type.String({ minLength: 1, maxLength: MAX_INPUT_CHARS })
167
+ },
168
+ { additionalProperties: false }
169
+ );
170
+
171
+ const chatStreamBodySchema = Type.Object(
172
+ {
173
+ messageId: Type.String({ minLength: 1, maxLength: 128 }),
174
+ conversationId: Type.Optional(Type.Integer({ minimum: 1 })),
175
+ input: Type.String({ minLength: 1, maxLength: MAX_INPUT_CHARS }),
176
+ history: Type.Optional(Type.Array(historyMessageSchema, { maxItems: MAX_HISTORY_MESSAGES })),
177
+ clientContext: Type.Optional(
178
+ Type.Object(
179
+ {
180
+ locale: Type.Optional(Type.String({ maxLength: 64 })),
181
+ timezone: Type.Optional(Type.String({ maxLength: 64 }))
182
+ },
183
+ { additionalProperties: false }
184
+ )
185
+ )
186
+ },
187
+ {
188
+ additionalProperties: false
189
+ }
190
+ );
191
+
192
+ const conversationRecordSchema = Type.Object(
193
+ {
194
+ id: Type.Integer({ minimum: 1 }),
195
+ workspaceId: Type.Integer({ minimum: 1 }),
196
+ workspaceSlug: Type.String(),
197
+ workspaceName: Type.String(),
198
+ title: Type.String(),
199
+ createdByUserId: Type.Union([Type.Integer({ minimum: 1 }), Type.Null()]),
200
+ createdByUserDisplayName: Type.String(),
201
+ createdByUserEmail: Type.String(),
202
+ status: Type.String(),
203
+ provider: Type.String(),
204
+ model: Type.String(),
205
+ surfaceId: Type.String(),
206
+ startedAt: Type.String({ minLength: 1 }),
207
+ endedAt: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
208
+ messageCount: Type.Integer({ minimum: 0 }),
209
+ metadata: Type.Record(Type.String(), Type.Unknown()),
210
+ createdAt: Type.String({ minLength: 1 }),
211
+ updatedAt: Type.String({ minLength: 1 })
212
+ },
213
+ { additionalProperties: false }
214
+ );
215
+
216
+ const conversationRecordValidator = Object.freeze({
217
+ schema: conversationRecordSchema,
218
+ normalize: normalizeConversationRecord
219
+ });
220
+
221
+ const messageRecordSchema = Type.Object(
222
+ {
223
+ id: Type.Integer({ minimum: 1 }),
224
+ conversationId: Type.Integer({ minimum: 1 }),
225
+ workspaceId: Type.Integer({ minimum: 1 }),
226
+ seq: Type.Integer({ minimum: 1 }),
227
+ role: Type.String({ minLength: 1 }),
228
+ kind: Type.String({ minLength: 1 }),
229
+ clientMessageId: Type.String(),
230
+ actorUserId: Type.Union([Type.Integer({ minimum: 1 }), Type.Null()]),
231
+ contentText: Type.Union([Type.String(), Type.Null()]),
232
+ metadata: Type.Record(Type.String(), Type.Unknown()),
233
+ createdAt: Type.String({ minLength: 1 })
234
+ },
235
+ { additionalProperties: false }
236
+ );
237
+
238
+ const paginationProperties = Object.freeze({
239
+ page: Type.Integer({ minimum: 1 }),
240
+ pageSize: Type.Integer({ minimum: 1 }),
241
+ total: Type.Integer({ minimum: 0 }),
242
+ totalPages: Type.Integer({ minimum: 1 })
243
+ });
244
+
245
+ const assistantResource = Object.freeze({
246
+ resource: "assistant",
247
+ operations: {
248
+ chatStream: {
249
+ method: "POST",
250
+ bodyValidator: Object.freeze({
251
+ schema: chatStreamBodySchema,
252
+ normalize: normalizeChatStreamBody
253
+ })
254
+ },
255
+ conversationsList: {
256
+ method: "GET",
257
+ queryValidator: Object.freeze({
258
+ schema: Type.Object(
259
+ {
260
+ cursor: createOptionalPositiveIntegerQuerySchema(),
261
+ limit: createOptionalPositiveIntegerQuerySchema(MAX_PAGE_SIZE),
262
+ status: Type.Optional(Type.String({ minLength: 1, maxLength: 32 }))
263
+ },
264
+ { additionalProperties: false }
265
+ ),
266
+ normalize: normalizeConversationsListQuery
267
+ }),
268
+ outputValidator: createCursorListValidator(conversationRecordValidator)
269
+ },
270
+ conversationMessagesList: {
271
+ method: "GET",
272
+ paramsValidator: Object.freeze({
273
+ schema: Type.Object(
274
+ {
275
+ conversationId: Type.Union([
276
+ Type.Integer({ minimum: 1 }),
277
+ Type.String({ pattern: "^[1-9][0-9]*$" })
278
+ ])
279
+ },
280
+ { additionalProperties: false }
281
+ ),
282
+ normalize: normalizeConversationMessagesParams
283
+ }),
284
+ queryValidator: Object.freeze({
285
+ schema: Type.Object(
286
+ {
287
+ page: createOptionalPositiveIntegerQuerySchema(),
288
+ pageSize: createOptionalPositiveIntegerQuerySchema(MAX_MESSAGE_PAGE_SIZE)
289
+ },
290
+ { additionalProperties: false }
291
+ ),
292
+ normalize: normalizeConversationMessagesQuery
293
+ }),
294
+ outputValidator: Object.freeze({
295
+ schema: Type.Object(
296
+ {
297
+ ...paginationProperties,
298
+ conversation: conversationRecordSchema,
299
+ entries: Type.Array(messageRecordSchema)
300
+ },
301
+ { additionalProperties: false }
302
+ ),
303
+ normalize(payload = {}) {
304
+ const source = normalizeObjectInput(payload);
305
+ return {
306
+ conversation: normalizeConversationRecord(source.conversation),
307
+ entries: (Array.isArray(source.entries) ? source.entries : []).map(normalizeConversationMessageRecord),
308
+ page: normalizePaginationValue(source.page, 1, MAX_MESSAGE_PAGE_SIZE),
309
+ pageSize: normalizePaginationValue(source.pageSize, 200, MAX_MESSAGE_PAGE_SIZE),
310
+ total: Math.max(0, Number(source.total || 0)),
311
+ totalPages: Math.max(1, Number(source.totalPages || 1))
312
+ };
313
+ }
314
+ })
315
+ }
316
+ }
317
+ });
318
+
319
+ export {
320
+ MAX_INPUT_CHARS,
321
+ MAX_HISTORY_MESSAGES,
322
+ assistantResource
323
+ };