@jskit-ai/assistant-core 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 +67 -0
- package/package.json +24 -0
- package/src/client/components/AssistantClientElement.vue +1316 -0
- package/src/client/components/AssistantSettingsFormCard.vue +76 -0
- package/src/client/index.js +5 -0
- package/src/client/lib/assistantApi.js +140 -0
- package/src/client/lib/assistantHttpClient.js +10 -0
- package/src/client/lib/markdownRenderer.js +31 -0
- package/src/server/index.js +21 -0
- package/src/server/lib/aiClient.js +43 -0
- package/src/server/lib/ndjson.js +47 -0
- package/src/server/lib/providers/anthropicClient.js +375 -0
- package/src/server/lib/providers/common.js +150 -0
- package/src/server/lib/providers/deepSeekClient.js +22 -0
- package/src/server/lib/providers/openAiClient.js +13 -0
- package/src/server/lib/providers/openAiCompatibleClient.js +69 -0
- package/src/server/lib/resolveWorkspaceSlug.js +24 -0
- package/src/server/lib/serviceToolCatalog.js +459 -0
- package/src/server/repositories/repositoryPersistenceUtils.js +48 -0
- package/src/shared/assistantPaths.js +77 -0
- package/src/shared/assistantResource.js +309 -0
- package/src/shared/assistantSettingsResource.js +90 -0
- package/src/shared/index.js +47 -0
- package/src/shared/queryKeys.js +84 -0
- package/src/shared/settingsEvents.js +5 -0
- package/src/shared/streamEvents.js +29 -0
- package/src/shared/support/conversationStatus.js +18 -0
- package/src/shared/support/jsonObject.js +18 -0
- package/src/shared/support/positiveInteger.js +9 -0
- package/test/aiConfigValidation.test.js +15 -0
- package/test/assistantApiSurfaceHeader.test.js +66 -0
- package/test/assistantPaths.test.js +51 -0
- package/test/assistantResource.test.js +49 -0
- package/test/assistantSettingsResource.test.js +32 -0
- package/test/queryKeys.test.js +44 -0
- package/test/resolveWorkspaceSlug.test.js +83 -0
- package/test/serviceToolCatalog.test.js +1235 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section :class="props.rootClass">
|
|
3
|
+
<v-card rounded="lg" elevation="1" border>
|
|
4
|
+
<v-card-item>
|
|
5
|
+
<v-card-title class="text-h6">{{ props.title }}</v-card-title>
|
|
6
|
+
<v-card-subtitle>{{ props.subtitle }}</v-card-subtitle>
|
|
7
|
+
</v-card-item>
|
|
8
|
+
<v-divider />
|
|
9
|
+
<v-card-text class="pt-4">
|
|
10
|
+
<template v-if="props.showFormSkeleton">
|
|
11
|
+
<v-skeleton-loader type="text@2, list-item-two-line@4, button" />
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<p v-else-if="props.addEdit.loadError" class="text-body-2 text-medium-emphasis mb-4">
|
|
15
|
+
{{ props.addEdit.loadError }}
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p v-else-if="!props.addEdit.canView" class="text-body-2 text-medium-emphasis mb-4">
|
|
19
|
+
{{ props.noPermissionMessage }}
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
<template v-else>
|
|
23
|
+
<v-form @submit.prevent="props.addEdit.submit" novalidate>
|
|
24
|
+
<v-progress-linear v-if="props.addEdit.isRefetching" indeterminate class="mb-4" />
|
|
25
|
+
<slot />
|
|
26
|
+
<div class="d-flex align-center justify-end ga-3 mt-2">
|
|
27
|
+
<v-btn
|
|
28
|
+
v-if="props.addEdit.canSave"
|
|
29
|
+
type="submit"
|
|
30
|
+
color="primary"
|
|
31
|
+
:loading="props.addEdit.isSaving"
|
|
32
|
+
:disabled="props.addEdit.isInitialLoading || props.addEdit.isRefetching"
|
|
33
|
+
>
|
|
34
|
+
{{ props.saveLabel }}
|
|
35
|
+
</v-btn>
|
|
36
|
+
<v-chip v-else color="secondary" label>Read-only</v-chip>
|
|
37
|
+
</div>
|
|
38
|
+
</v-form>
|
|
39
|
+
</template>
|
|
40
|
+
</v-card-text>
|
|
41
|
+
</v-card>
|
|
42
|
+
</section>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup>
|
|
46
|
+
const props = defineProps({
|
|
47
|
+
rootClass: {
|
|
48
|
+
type: String,
|
|
49
|
+
required: true
|
|
50
|
+
},
|
|
51
|
+
title: {
|
|
52
|
+
type: String,
|
|
53
|
+
required: true
|
|
54
|
+
},
|
|
55
|
+
subtitle: {
|
|
56
|
+
type: String,
|
|
57
|
+
required: true
|
|
58
|
+
},
|
|
59
|
+
noPermissionMessage: {
|
|
60
|
+
type: String,
|
|
61
|
+
required: true
|
|
62
|
+
},
|
|
63
|
+
saveLabel: {
|
|
64
|
+
type: String,
|
|
65
|
+
required: true
|
|
66
|
+
},
|
|
67
|
+
addEdit: {
|
|
68
|
+
type: Object,
|
|
69
|
+
required: true
|
|
70
|
+
},
|
|
71
|
+
showFormSkeleton: {
|
|
72
|
+
type: Boolean,
|
|
73
|
+
default: false
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
</script>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as AssistantClientElement } from "./components/AssistantClientElement.vue";
|
|
2
|
+
export { default as AssistantSettingsFormCard } from "./components/AssistantSettingsFormCard.vue";
|
|
3
|
+
export { assistantHttpClient } from "./lib/assistantHttpClient.js";
|
|
4
|
+
export { createAssistantApi, buildStreamEventError } from "./lib/assistantApi.js";
|
|
5
|
+
export { renderMarkdownToSafeHtml } from "./lib/markdownRenderer.js";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { appendQueryString } from "@jskit-ai/kernel/shared/support";
|
|
2
|
+
import {
|
|
3
|
+
ASSISTANT_STREAM_EVENT_TYPES,
|
|
4
|
+
normalizeAssistantStreamEventType
|
|
5
|
+
} from "../../shared/index.js";
|
|
6
|
+
|
|
7
|
+
function buildStreamEventError(event) {
|
|
8
|
+
const message = String(event?.message || "Assistant request failed.");
|
|
9
|
+
const error = new Error(message);
|
|
10
|
+
error.code = String(event?.code || "assistant_stream_error");
|
|
11
|
+
error.status = Number(event?.status || 500);
|
|
12
|
+
error.event = event && typeof event === "object" ? { ...event } : null;
|
|
13
|
+
return error;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function appendQueryParam(params, key, value) {
|
|
17
|
+
if (value == null) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const normalized = String(value).trim();
|
|
22
|
+
if (!normalized) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
params.set(key, normalized);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeSurfaceHeaderValue(value) {
|
|
30
|
+
return String(value || "").trim().toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveAssistantRequestHeaders(resolveSurfaceId) {
|
|
34
|
+
if (typeof resolveSurfaceId !== "function") {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const surfaceId = normalizeSurfaceHeaderValue(resolveSurfaceId());
|
|
39
|
+
if (!surfaceId) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
"x-jskit-surface": surfaceId
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveRequiredBasePath(resolveBasePath) {
|
|
49
|
+
if (typeof resolveBasePath !== "function") {
|
|
50
|
+
throw new Error("createAssistantApi requires resolveBasePath().");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const resolved = String(resolveBasePath() || "").trim();
|
|
54
|
+
if (!resolved) {
|
|
55
|
+
throw new Error("Assistant API base path is required.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return resolved;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createAssistantApi({ request, requestStream, resolveBasePath, resolveSurfaceId = null } = {}) {
|
|
62
|
+
if (typeof request !== "function" || typeof requestStream !== "function") {
|
|
63
|
+
throw new Error("createAssistantApi requires request() and requestStream().");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
async streamChat(payload, { signal, onEvent, onMalformedLine, rejectOnErrorEvent = true } = {}) {
|
|
68
|
+
const basePath = resolveRequiredBasePath(resolveBasePath);
|
|
69
|
+
let streamEventError = null;
|
|
70
|
+
const requestHeaders = resolveAssistantRequestHeaders(resolveSurfaceId);
|
|
71
|
+
|
|
72
|
+
const streamHandlers = {
|
|
73
|
+
onEvent(event) {
|
|
74
|
+
const eventType = normalizeAssistantStreamEventType(event?.type, "");
|
|
75
|
+
if (rejectOnErrorEvent && eventType === ASSISTANT_STREAM_EVENT_TYPES.ERROR && !streamEventError) {
|
|
76
|
+
streamEventError = buildStreamEventError(event);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof onEvent === "function") {
|
|
80
|
+
onEvent(event);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (typeof onMalformedLine === "function") {
|
|
86
|
+
streamHandlers.onMalformedLine = (line, parseError) => {
|
|
87
|
+
onMalformedLine(line, parseError);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await requestStream(
|
|
92
|
+
`${basePath}/chat/stream`,
|
|
93
|
+
{
|
|
94
|
+
method: "POST",
|
|
95
|
+
...(requestHeaders ? { headers: requestHeaders } : {}),
|
|
96
|
+
body: payload,
|
|
97
|
+
signal
|
|
98
|
+
},
|
|
99
|
+
streamHandlers
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (streamEventError) {
|
|
103
|
+
throw streamEventError;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
listConversations(query = {}) {
|
|
108
|
+
const basePath = resolveRequiredBasePath(resolveBasePath);
|
|
109
|
+
const params = new URLSearchParams();
|
|
110
|
+
appendQueryParam(params, "cursor", query.cursor);
|
|
111
|
+
appendQueryParam(params, "limit", query.limit);
|
|
112
|
+
appendQueryParam(params, "status", query.status);
|
|
113
|
+
const requestHeaders = resolveAssistantRequestHeaders(resolveSurfaceId);
|
|
114
|
+
|
|
115
|
+
return request(
|
|
116
|
+
appendQueryString(`${basePath}/conversations`, params.toString()),
|
|
117
|
+
requestHeaders ? { headers: requestHeaders } : {}
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
getConversationMessages(conversationId, query = {}) {
|
|
122
|
+
const basePath = resolveRequiredBasePath(resolveBasePath);
|
|
123
|
+
const encodedConversationId = encodeURIComponent(String(conversationId || "").trim());
|
|
124
|
+
const params = new URLSearchParams();
|
|
125
|
+
appendQueryParam(params, "page", query.page);
|
|
126
|
+
appendQueryParam(params, "pageSize", query.pageSize);
|
|
127
|
+
const requestHeaders = resolveAssistantRequestHeaders(resolveSurfaceId);
|
|
128
|
+
|
|
129
|
+
return request(
|
|
130
|
+
appendQueryString(`${basePath}/conversations/${encodedConversationId}/messages`, params.toString()),
|
|
131
|
+
requestHeaders ? { headers: requestHeaders } : {}
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export {
|
|
138
|
+
createAssistantApi,
|
|
139
|
+
buildStreamEventError
|
|
140
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import DOMPurify from "dompurify";
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
|
|
4
|
+
marked.setOptions(
|
|
5
|
+
Object.freeze({
|
|
6
|
+
gfm: true,
|
|
7
|
+
breaks: true
|
|
8
|
+
})
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
function normalizeMarkdownText(value) {
|
|
12
|
+
return typeof value === "string" ? value : String(value || "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderMarkdownToSafeHtml(value = "") {
|
|
16
|
+
const markdownText = normalizeMarkdownText(value);
|
|
17
|
+
if (!markdownText) {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const unsafeHtml = String(marked.parse(markdownText) || "");
|
|
22
|
+
return String(
|
|
23
|
+
DOMPurify.sanitize(unsafeHtml, {
|
|
24
|
+
USE_PROFILES: {
|
|
25
|
+
html: true
|
|
26
|
+
}
|
|
27
|
+
}) || ""
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { renderMarkdownToSafeHtml };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { createAiClient, SUPPORTED_AI_PROVIDERS, DEFAULT_AI_PROVIDER } from "./lib/aiClient.js";
|
|
2
|
+
export {
|
|
3
|
+
DEFAULT_AI_TIMEOUT_MS,
|
|
4
|
+
normalizeOptionalHttpUrl,
|
|
5
|
+
normalizeTimeoutMs
|
|
6
|
+
} from "./lib/providers/common.js";
|
|
7
|
+
export {
|
|
8
|
+
NDJSON_CONTENT_TYPE,
|
|
9
|
+
endNdjson,
|
|
10
|
+
mapStreamError,
|
|
11
|
+
setNdjsonHeaders,
|
|
12
|
+
writeNdjson
|
|
13
|
+
} from "./lib/ndjson.js";
|
|
14
|
+
export { resolveWorkspaceSlug } from "./lib/resolveWorkspaceSlug.js";
|
|
15
|
+
export { createServiceToolCatalog } from "./lib/serviceToolCatalog.js";
|
|
16
|
+
export {
|
|
17
|
+
parseJsonObject,
|
|
18
|
+
resolveInsertedId,
|
|
19
|
+
stringifyJsonObject,
|
|
20
|
+
toIso
|
|
21
|
+
} from "./repositories/repositoryPersistenceUtils.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createOpenAiClient } from "./providers/openAiClient.js";
|
|
2
|
+
import { createDeepSeekClient } from "./providers/deepSeekClient.js";
|
|
3
|
+
import { createAnthropicClient } from "./providers/anthropicClient.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_AI_PROVIDER,
|
|
6
|
+
SUPPORTED_AI_PROVIDERS,
|
|
7
|
+
normalizeModel,
|
|
8
|
+
normalizeProvider,
|
|
9
|
+
normalizeTimeoutMs
|
|
10
|
+
} from "./providers/common.js";
|
|
11
|
+
|
|
12
|
+
function createAiClient(options = {}) {
|
|
13
|
+
const provider = normalizeProvider(options.provider || DEFAULT_AI_PROVIDER);
|
|
14
|
+
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
|
|
15
|
+
const commonOptions = {
|
|
16
|
+
...options,
|
|
17
|
+
provider,
|
|
18
|
+
timeoutMs,
|
|
19
|
+
model: normalizeModel(options.model)
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (provider === "openai") {
|
|
23
|
+
return createOpenAiClient(commonOptions);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (provider === "deepseek") {
|
|
27
|
+
return createDeepSeekClient(commonOptions);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (provider === "anthropic") {
|
|
31
|
+
return createAnthropicClient(commonOptions);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
throw new TypeError(
|
|
35
|
+
`Unsupported assistant provider: ${provider}. Supported providers: ${SUPPORTED_AI_PROVIDERS.join(", ")}.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
createAiClient,
|
|
41
|
+
SUPPORTED_AI_PROVIDERS,
|
|
42
|
+
DEFAULT_AI_PROVIDER
|
|
43
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const NDJSON_CONTENT_TYPE = "application/x-ndjson; charset=utf-8";
|
|
2
|
+
|
|
3
|
+
function setNdjsonHeaders(reply) {
|
|
4
|
+
// Fastify reply.hijack bypasses part of reply serialization. Set headers on both
|
|
5
|
+
// Fastify reply and the underlying raw response to keep streaming content-type intact.
|
|
6
|
+
reply.header("Content-Type", NDJSON_CONTENT_TYPE);
|
|
7
|
+
reply.header("Cache-Control", "no-cache");
|
|
8
|
+
reply.header("X-Accel-Buffering", "no");
|
|
9
|
+
|
|
10
|
+
if (reply?.raw && typeof reply.raw.setHeader === "function") {
|
|
11
|
+
reply.raw.setHeader("Content-Type", NDJSON_CONTENT_TYPE);
|
|
12
|
+
reply.raw.setHeader("Cache-Control", "no-cache");
|
|
13
|
+
reply.raw.setHeader("X-Accel-Buffering", "no");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeNdjson(reply, payload = {}) {
|
|
18
|
+
const body = `${JSON.stringify(payload)}\n`;
|
|
19
|
+
reply.raw.write(body);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function endNdjson(reply) {
|
|
23
|
+
if (!reply || !reply.raw || reply.raw.writableEnded) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
reply.raw.end();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function mapStreamError(error) {
|
|
31
|
+
const status = Number(error?.status || error?.statusCode || 500);
|
|
32
|
+
const safeStatus = Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500;
|
|
33
|
+
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
code: String(error?.code || "assistant_stream_failed").trim() || "assistant_stream_failed",
|
|
36
|
+
message: safeStatus >= 500 ? "Assistant stream failed." : String(error?.message || "Request failed."),
|
|
37
|
+
status: safeStatus
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
NDJSON_CONTENT_TYPE,
|
|
43
|
+
setNdjsonHeaders,
|
|
44
|
+
writeNdjson,
|
|
45
|
+
endNdjson,
|
|
46
|
+
mapStreamError
|
|
47
|
+
};
|