@bdocs/plugin-ask-ai 0.3.0 → 0.4.0
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/dist/client/index.d.mts +146 -10
- package/dist/client/index.mjs +701 -561
- package/dist/node/index.d.mts +276 -114
- package/dist/node/index.mjs +227 -253
- package/dist/rate-limit-eZ04302C.mjs +217 -0
- package/dist/server/index.d.mts +73 -1
- package/dist/server/index.mjs +340 -23
- package/package.json +12 -5
package/dist/server/index.mjs
CHANGED
|
@@ -1,36 +1,31 @@
|
|
|
1
|
+
import { a as createAdapterHeaders, c as checkInputSafety, d as buildUserPrompt, n as rateLimit, r as pickClientContext, s as DEFAULT_DENY_PATTERNS } from "../rate-limit-eZ04302C.mjs";
|
|
1
2
|
import OpenAI from "openai";
|
|
3
|
+
//#region src/server/errors.ts
|
|
4
|
+
const AI_NOT_CONFIGURED = "AI_NOT_CONFIGURED";
|
|
5
|
+
const AI_PROVIDER_ERROR = "AI_PROVIDER_ERROR";
|
|
6
|
+
const AI_TIMEOUT = "AI_TIMEOUT";
|
|
7
|
+
const ADAPTER_ERROR = "ADAPTER_ERROR";
|
|
8
|
+
/** Never return arbitrary upstream/parser messages to an untrusted client. */
|
|
9
|
+
function toPublicProviderError() {
|
|
10
|
+
return AI_PROVIDER_ERROR;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
2
13
|
//#region src/server/handler.ts
|
|
3
14
|
const STREAM_TIMEOUT_MS = 6e4;
|
|
4
|
-
const DOCS_START = "<<<DOCS_START>>>";
|
|
5
|
-
const DOCS_END = "<<<DOCS_END>>>";
|
|
6
|
-
function escapeDocsMarkers(s) {
|
|
7
|
-
return s.replace(/<<<DOCS_(START|END)>>>/g, "<DOCS_$1>");
|
|
8
|
-
}
|
|
9
|
-
function buildUserPrompt(question, context) {
|
|
10
|
-
if (!context || !context.content) return `${DOCS_START}\n(no documentation page in scope — reply "Not in docs." for any Boltdocs question)\n${DOCS_END}\n\nUser Question: ${question}`;
|
|
11
|
-
return [
|
|
12
|
-
DOCS_START,
|
|
13
|
-
`[Page: ${context.page}]`,
|
|
14
|
-
escapeDocsMarkers(context.content),
|
|
15
|
-
DOCS_END,
|
|
16
|
-
"",
|
|
17
|
-
`User Question: ${question}`
|
|
18
|
-
].join("\n");
|
|
19
|
-
}
|
|
20
15
|
async function streamLLMResponse(options, onEvent) {
|
|
21
|
-
const { model, systemPrompt, question, context, maxOutputTokens, baseURL, env, signal, provider = "openai", providerEnvKey = "OPENAI_API_KEY", devMode = false } = options;
|
|
16
|
+
const { model, systemPrompt, question, context, maxOutputTokens, baseURL, env, signal, provider = "openai", providerEnvKey = "OPENAI_API_KEY", devMode = false, temperature = .3, topP = 1 } = options;
|
|
22
17
|
const apiKey = env[providerEnvKey];
|
|
23
18
|
if (!apiKey) {
|
|
24
19
|
onEvent({
|
|
25
20
|
type: "error",
|
|
26
|
-
data:
|
|
21
|
+
data: AI_NOT_CONFIGURED
|
|
27
22
|
});
|
|
28
23
|
return;
|
|
29
24
|
}
|
|
30
25
|
const userPrompt = buildUserPrompt(question, context);
|
|
31
26
|
const openai = new OpenAI({
|
|
32
27
|
apiKey,
|
|
33
|
-
baseURL: baseURL || env.OPENAI_BASE_URL
|
|
28
|
+
baseURL: baseURL || (provider === "openai" ? env.OPENAI_BASE_URL : void 0)
|
|
34
29
|
});
|
|
35
30
|
const timeoutController = new AbortController();
|
|
36
31
|
const combinedController = new AbortController();
|
|
@@ -38,7 +33,9 @@ async function streamLLMResponse(options, onEvent) {
|
|
|
38
33
|
const onExternalAbort = () => {
|
|
39
34
|
if (!combinedController.signal.aborted) combinedController.abort();
|
|
40
35
|
};
|
|
36
|
+
let timedOut = false;
|
|
41
37
|
const onTimeoutAbort = () => {
|
|
38
|
+
timedOut = true;
|
|
42
39
|
if (!combinedController.signal.aborted) combinedController.abort();
|
|
43
40
|
};
|
|
44
41
|
if (externalSignal) if (externalSignal.aborted) onExternalAbort();
|
|
@@ -50,6 +47,8 @@ async function streamLLMResponse(options, onEvent) {
|
|
|
50
47
|
const stream = await openai.chat.completions.create({
|
|
51
48
|
model,
|
|
52
49
|
max_tokens: maxOutputTokens,
|
|
50
|
+
temperature,
|
|
51
|
+
top_p: topP,
|
|
53
52
|
stream: true,
|
|
54
53
|
stream_options: { include_usage: true },
|
|
55
54
|
messages: [{
|
|
@@ -75,6 +74,13 @@ async function streamLLMResponse(options, onEvent) {
|
|
|
75
74
|
completionTokens = usage.completion_tokens ?? completionTokens;
|
|
76
75
|
}
|
|
77
76
|
}
|
|
77
|
+
if (timedOut) {
|
|
78
|
+
onEvent({
|
|
79
|
+
type: "error",
|
|
80
|
+
data: AI_TIMEOUT
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
78
84
|
if (devMode && (promptTokens > 0 || completionTokens > 0)) onEvent({
|
|
79
85
|
type: "usage",
|
|
80
86
|
data: {
|
|
@@ -87,11 +93,11 @@ async function streamLLMResponse(options, onEvent) {
|
|
|
87
93
|
}
|
|
88
94
|
});
|
|
89
95
|
if (!externalSignal?.aborted && !combinedController.signal.aborted) onEvent({ type: "done" });
|
|
90
|
-
} catch
|
|
91
|
-
if (externalSignal?.aborted
|
|
96
|
+
} catch {
|
|
97
|
+
if (externalSignal?.aborted) return;
|
|
92
98
|
onEvent({
|
|
93
99
|
type: "error",
|
|
94
|
-
data:
|
|
100
|
+
data: timedOut ? AI_TIMEOUT : toPublicProviderError()
|
|
95
101
|
});
|
|
96
102
|
} finally {
|
|
97
103
|
clearTimeout(timeoutId);
|
|
@@ -100,4 +106,315 @@ async function streamLLMResponse(options, onEvent) {
|
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
108
|
//#endregion
|
|
103
|
-
|
|
109
|
+
//#region src/server/adapters/sse.ts
|
|
110
|
+
function eventToSse(event) {
|
|
111
|
+
switch (event.type) {
|
|
112
|
+
case "context": return `data: ${JSON.stringify({ context: event.data })}\n\n`;
|
|
113
|
+
case "text": return `data: ${JSON.stringify({ text: event.data })}\n\n`;
|
|
114
|
+
case "usage": return `data: ${JSON.stringify({ usage: event.data })}\n\n`;
|
|
115
|
+
case "error": return `data: ${JSON.stringify({ error: event.data })}\n\n`;
|
|
116
|
+
case "done": return "";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const DONE_SSE = "data: [DONE]\n\n";
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/server/adapters/security.ts
|
|
122
|
+
function isAuthorized(config, headers) {
|
|
123
|
+
if (!config.secretKey) return true;
|
|
124
|
+
return headers.get("x-boltdocs-ask-ai-key") === config.secretKey;
|
|
125
|
+
}
|
|
126
|
+
function validateClientContext(config, body, headers) {
|
|
127
|
+
if (!body || typeof body !== "object" || !("context" in body)) return null;
|
|
128
|
+
if (config.allowClientContext || Boolean(config.secretKey) && isAuthorized(config, headers)) return null;
|
|
129
|
+
return "CLIENT_CONTEXT_NOT_ALLOWED";
|
|
130
|
+
}
|
|
131
|
+
function validateQuestion(config, body) {
|
|
132
|
+
const question = body && typeof body === "object" && "question" in body ? body.question : void 0;
|
|
133
|
+
const safety = checkInputSafety(typeof question === "string" ? question : "", config.maxInputChars ?? 2e3, DEFAULT_DENY_PATTERNS);
|
|
134
|
+
return safety.ok ? null : safety.reason;
|
|
135
|
+
}
|
|
136
|
+
function checkAdapterRateLimit(config, ip) {
|
|
137
|
+
return rateLimit(ip, config.rateLimitPerMinute ?? 30);
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/server/adapters/stream-options.ts
|
|
141
|
+
function createStreamOptions(config, question, context, env, signal) {
|
|
142
|
+
const provider = config.provider ?? "openai";
|
|
143
|
+
const providerEnvKey = config.providerEnvKey ?? "OPENAI_API_KEY";
|
|
144
|
+
const providerEnv = {
|
|
145
|
+
[providerEnvKey]: env[providerEnvKey],
|
|
146
|
+
OPENAI_BASE_URL: env.OPENAI_BASE_URL
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
model: config.model,
|
|
150
|
+
systemPrompt: config.systemPrompt,
|
|
151
|
+
question,
|
|
152
|
+
context,
|
|
153
|
+
maxOutputTokens: config.maxOutputTokens ?? 600,
|
|
154
|
+
baseURL: config.baseURL,
|
|
155
|
+
env: providerEnv,
|
|
156
|
+
provider,
|
|
157
|
+
providerEnvKey,
|
|
158
|
+
signal,
|
|
159
|
+
devMode: config.devMode ?? false,
|
|
160
|
+
temperature: config.temperature,
|
|
161
|
+
topP: config.topP
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/server/adapters/aws.ts
|
|
166
|
+
async function handleAwsAskAi(event, config, env = process.env) {
|
|
167
|
+
const requestHeaders = new Headers(event.headers ?? {});
|
|
168
|
+
const responseHeaders = createAdapterHeaders(config, requestHeaders.get("origin"));
|
|
169
|
+
if (event.httpMethod === "OPTIONS") return {
|
|
170
|
+
statusCode: 200,
|
|
171
|
+
headers: responseHeaders,
|
|
172
|
+
body: ""
|
|
173
|
+
};
|
|
174
|
+
if (event.httpMethod !== "POST") return {
|
|
175
|
+
statusCode: 405,
|
|
176
|
+
headers: responseHeaders,
|
|
177
|
+
body: JSON.stringify({ error: "Method Not Allowed" })
|
|
178
|
+
};
|
|
179
|
+
try {
|
|
180
|
+
const bodyText = event.isBase64Encoded ? Buffer.from(event.body ?? "", "base64").toString("utf8") : event.body ?? "";
|
|
181
|
+
if (new TextEncoder().encode(bodyText).byteLength > (config.maxRequestBytes ?? 65536)) return {
|
|
182
|
+
statusCode: 413,
|
|
183
|
+
headers: responseHeaders,
|
|
184
|
+
body: JSON.stringify({ error: "REQUEST_TOO_LARGE" })
|
|
185
|
+
};
|
|
186
|
+
const payload = bodyText ? JSON.parse(bodyText) : {};
|
|
187
|
+
if (!isAuthorized(config, requestHeaders)) return {
|
|
188
|
+
statusCode: 401,
|
|
189
|
+
headers: responseHeaders,
|
|
190
|
+
body: JSON.stringify({ error: "UNAUTHORIZED" })
|
|
191
|
+
};
|
|
192
|
+
const contextError = validateClientContext(config, payload, requestHeaders);
|
|
193
|
+
if (contextError) return {
|
|
194
|
+
statusCode: 403,
|
|
195
|
+
headers: responseHeaders,
|
|
196
|
+
body: JSON.stringify({ error: contextError })
|
|
197
|
+
};
|
|
198
|
+
const questionError = validateQuestion(config, payload);
|
|
199
|
+
if (questionError) return {
|
|
200
|
+
statusCode: 400,
|
|
201
|
+
headers: responseHeaders,
|
|
202
|
+
body: JSON.stringify({ error: questionError })
|
|
203
|
+
};
|
|
204
|
+
const rate = checkAdapterRateLimit(config, requestHeaders.get("x-forwarded-for") || "unknown");
|
|
205
|
+
if (!rate.ok) return {
|
|
206
|
+
statusCode: 429,
|
|
207
|
+
headers: {
|
|
208
|
+
...responseHeaders,
|
|
209
|
+
"Retry-After": String(rate.retryAfter)
|
|
210
|
+
},
|
|
211
|
+
body: JSON.stringify({ error: "RATE_LIMITED" })
|
|
212
|
+
};
|
|
213
|
+
const question = payload.question;
|
|
214
|
+
const context = pickClientContext(payload, config.contextChars ?? 6e3);
|
|
215
|
+
const parts = [];
|
|
216
|
+
await streamLLMResponse(createStreamOptions(config, question, context, env), (event) => {
|
|
217
|
+
const sse = eventToSse(event);
|
|
218
|
+
if (sse) parts.push(sse);
|
|
219
|
+
});
|
|
220
|
+
parts.push(DONE_SSE);
|
|
221
|
+
return {
|
|
222
|
+
statusCode: 200,
|
|
223
|
+
headers: responseHeaders,
|
|
224
|
+
body: parts.join("")
|
|
225
|
+
};
|
|
226
|
+
} catch {
|
|
227
|
+
return {
|
|
228
|
+
statusCode: 500,
|
|
229
|
+
headers: responseHeaders,
|
|
230
|
+
body: `data: ${JSON.stringify({ error: ADAPTER_ERROR })}\n\n${DONE_SSE}`
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/server/adapters/netlify.ts
|
|
236
|
+
async function handleNetlifyAskAi(event, config, env = process.env) {
|
|
237
|
+
const requestHeaders = new Headers(event.headers ?? {});
|
|
238
|
+
const responseHeaders = createAdapterHeaders(config, requestHeaders.get("origin"));
|
|
239
|
+
if (event.httpMethod === "OPTIONS") return {
|
|
240
|
+
statusCode: 200,
|
|
241
|
+
headers: responseHeaders,
|
|
242
|
+
body: ""
|
|
243
|
+
};
|
|
244
|
+
if (event.httpMethod !== "POST") return {
|
|
245
|
+
statusCode: 405,
|
|
246
|
+
headers: responseHeaders,
|
|
247
|
+
body: JSON.stringify({ error: "Method Not Allowed" })
|
|
248
|
+
};
|
|
249
|
+
try {
|
|
250
|
+
const bodyText = event.body ?? "";
|
|
251
|
+
if (new TextEncoder().encode(bodyText).byteLength > (config.maxRequestBytes ?? 65536)) return {
|
|
252
|
+
statusCode: 413,
|
|
253
|
+
headers: responseHeaders,
|
|
254
|
+
body: JSON.stringify({ error: "REQUEST_TOO_LARGE" })
|
|
255
|
+
};
|
|
256
|
+
const payload = bodyText ? JSON.parse(bodyText) : {};
|
|
257
|
+
if (!isAuthorized(config, requestHeaders)) return {
|
|
258
|
+
statusCode: 401,
|
|
259
|
+
headers: responseHeaders,
|
|
260
|
+
body: JSON.stringify({ error: "UNAUTHORIZED" })
|
|
261
|
+
};
|
|
262
|
+
const contextError = validateClientContext(config, payload, requestHeaders);
|
|
263
|
+
if (contextError) return {
|
|
264
|
+
statusCode: 403,
|
|
265
|
+
headers: responseHeaders,
|
|
266
|
+
body: JSON.stringify({ error: contextError })
|
|
267
|
+
};
|
|
268
|
+
const questionError = validateQuestion(config, payload);
|
|
269
|
+
if (questionError) return {
|
|
270
|
+
statusCode: 400,
|
|
271
|
+
headers: responseHeaders,
|
|
272
|
+
body: JSON.stringify({ error: questionError })
|
|
273
|
+
};
|
|
274
|
+
const rate = checkAdapterRateLimit(config, requestHeaders.get("x-forwarded-for") || "unknown");
|
|
275
|
+
if (!rate.ok) return {
|
|
276
|
+
statusCode: 429,
|
|
277
|
+
headers: {
|
|
278
|
+
...responseHeaders,
|
|
279
|
+
"Retry-After": String(rate.retryAfter)
|
|
280
|
+
},
|
|
281
|
+
body: JSON.stringify({ error: "RATE_LIMITED" })
|
|
282
|
+
};
|
|
283
|
+
const question = payload.question;
|
|
284
|
+
const context = pickClientContext(payload, config.contextChars ?? 6e3);
|
|
285
|
+
const parts = [];
|
|
286
|
+
await streamLLMResponse(createStreamOptions(config, question, context, env), (event) => {
|
|
287
|
+
const sse = eventToSse(event);
|
|
288
|
+
if (sse) parts.push(sse);
|
|
289
|
+
});
|
|
290
|
+
parts.push(DONE_SSE);
|
|
291
|
+
return {
|
|
292
|
+
statusCode: 200,
|
|
293
|
+
headers: responseHeaders,
|
|
294
|
+
body: parts.join("")
|
|
295
|
+
};
|
|
296
|
+
} catch {
|
|
297
|
+
return {
|
|
298
|
+
statusCode: 500,
|
|
299
|
+
headers: responseHeaders,
|
|
300
|
+
body: `data: ${JSON.stringify({ error: ADAPTER_ERROR })}\n\n${DONE_SSE}`
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
//#endregion
|
|
305
|
+
//#region src/server/adapters/vercel.ts
|
|
306
|
+
async function handleVercelAskAi(req, res, config, env = process.env) {
|
|
307
|
+
const requestHeaders = new Headers(req.headers ?? {});
|
|
308
|
+
const responseHeaders = createAdapterHeaders(config, requestHeaders.get("origin"));
|
|
309
|
+
for (const [key, value] of Object.entries(responseHeaders)) res.setHeader(key, value);
|
|
310
|
+
if (req.method === "OPTIONS") {
|
|
311
|
+
res.status(200).end();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (req.method !== "POST") {
|
|
315
|
+
res.status(405).end("Method Not Allowed");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
if (!isAuthorized(config, requestHeaders)) {
|
|
320
|
+
res.status(401).json({ error: "UNAUTHORIZED" });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const contextError = validateClientContext(config, req.body, requestHeaders);
|
|
324
|
+
if (contextError) {
|
|
325
|
+
res.status(403).json({ error: contextError });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const questionError = validateQuestion(config, req.body);
|
|
329
|
+
if (questionError) {
|
|
330
|
+
res.status(400).json({ error: questionError });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const rate = checkAdapterRateLimit(config, requestHeaders.get("x-forwarded-for") || "unknown");
|
|
334
|
+
if (!rate.ok) {
|
|
335
|
+
res.setHeader("Retry-After", String(rate.retryAfter));
|
|
336
|
+
res.status(429).json({ error: "RATE_LIMITED" });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const question = req.body.question;
|
|
340
|
+
await streamLLMResponse(createStreamOptions(config, question, pickClientContext(req.body, config.contextChars ?? 6e3), env), (event) => {
|
|
341
|
+
const sse = eventToSse(event);
|
|
342
|
+
if (sse) res.write(sse);
|
|
343
|
+
});
|
|
344
|
+
res.write("data: [DONE]\n\n");
|
|
345
|
+
res.end();
|
|
346
|
+
} catch {
|
|
347
|
+
res.write(`data: ${JSON.stringify({ error: ADAPTER_ERROR })}\n\ndata: [DONE]\n\n`);
|
|
348
|
+
res.end();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/server/adapters/web.ts
|
|
353
|
+
async function handleWebAskAi(request, config, env = process.env) {
|
|
354
|
+
const corsHeaders = createAdapterHeaders(config, request.headers.get("origin"));
|
|
355
|
+
if (request.method === "OPTIONS") return new Response(null, {
|
|
356
|
+
status: 200,
|
|
357
|
+
headers: corsHeaders
|
|
358
|
+
});
|
|
359
|
+
if (request.method !== "POST") return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
|
|
360
|
+
status: 405,
|
|
361
|
+
headers: corsHeaders
|
|
362
|
+
});
|
|
363
|
+
try {
|
|
364
|
+
const bodyText = await request.text();
|
|
365
|
+
if (new TextEncoder().encode(bodyText).byteLength > (config.maxRequestBytes ?? 65536)) return new Response(JSON.stringify({ error: "REQUEST_TOO_LARGE" }), {
|
|
366
|
+
status: 413,
|
|
367
|
+
headers: corsHeaders
|
|
368
|
+
});
|
|
369
|
+
const payload = bodyText ? JSON.parse(bodyText) : {};
|
|
370
|
+
if (!isAuthorized(config, request.headers)) return new Response(JSON.stringify({ error: "UNAUTHORIZED" }), {
|
|
371
|
+
status: 401,
|
|
372
|
+
headers: corsHeaders
|
|
373
|
+
});
|
|
374
|
+
const contextError = validateClientContext(config, payload, request.headers);
|
|
375
|
+
if (contextError) return new Response(JSON.stringify({ error: contextError }), {
|
|
376
|
+
status: 403,
|
|
377
|
+
headers: corsHeaders
|
|
378
|
+
});
|
|
379
|
+
const questionError = validateQuestion(config, payload);
|
|
380
|
+
if (questionError) return new Response(JSON.stringify({ error: questionError }), {
|
|
381
|
+
status: 400,
|
|
382
|
+
headers: corsHeaders
|
|
383
|
+
});
|
|
384
|
+
const rate = checkAdapterRateLimit(config, request.headers.get("x-forwarded-for") || "unknown");
|
|
385
|
+
if (!rate.ok) {
|
|
386
|
+
corsHeaders["Retry-After"] = String(rate.retryAfter);
|
|
387
|
+
return new Response(JSON.stringify({ error: "RATE_LIMITED" }), {
|
|
388
|
+
status: 429,
|
|
389
|
+
headers: corsHeaders
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
const question = payload.question;
|
|
393
|
+
const context = pickClientContext(payload, config.contextChars ?? 6e3);
|
|
394
|
+
const encoder = new TextEncoder();
|
|
395
|
+
const stream = new ReadableStream({ async start(controller) {
|
|
396
|
+
try {
|
|
397
|
+
await streamLLMResponse(createStreamOptions(config, question, context, env, request.signal), (event) => {
|
|
398
|
+
const sse = eventToSse(event);
|
|
399
|
+
if (sse) controller.enqueue(encoder.encode(sse));
|
|
400
|
+
});
|
|
401
|
+
controller.enqueue(encoder.encode(DONE_SSE));
|
|
402
|
+
} catch {
|
|
403
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: ADAPTER_ERROR })}\n\n`));
|
|
404
|
+
} finally {
|
|
405
|
+
controller.close();
|
|
406
|
+
}
|
|
407
|
+
} });
|
|
408
|
+
return new Response(stream, {
|
|
409
|
+
status: 200,
|
|
410
|
+
headers: corsHeaders
|
|
411
|
+
});
|
|
412
|
+
} catch {
|
|
413
|
+
return new Response(`data: ${JSON.stringify({ error: ADAPTER_ERROR })}\n\n${DONE_SSE}`, {
|
|
414
|
+
status: 500,
|
|
415
|
+
headers: corsHeaders
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
420
|
+
export { handleAwsAskAi, handleNetlifyAskAi, handleVercelAskAi, handleWebAskAi, streamLLMResponse };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bdocs/plugin-ask-ai",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Secure, page-scoped streaming AI assistant for Boltdocs with provider presets, hardened server adapters, automatic floating UI, and customizable client components.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
7
7
|
],
|
|
@@ -23,25 +23,32 @@
|
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@bdocs/dui": "^0.
|
|
26
|
+
"@bdocs/dui": "^0.6.0",
|
|
27
|
+
"clsx": "2.1.1",
|
|
27
28
|
"openai": "^4.77.0",
|
|
28
29
|
"streamdown": "^1.0.0",
|
|
30
|
+
"tailwind-merge": "3.5.0",
|
|
29
31
|
"zod": "^3.23.0"
|
|
30
32
|
},
|
|
31
33
|
"peerDependencies": {
|
|
34
|
+
"boltdocs": ">=3.3.0",
|
|
32
35
|
"react": "^19.0.0",
|
|
33
36
|
"react-dom": "^19.0.0",
|
|
34
|
-
"vite": "^
|
|
37
|
+
"vite": "^8.1.0"
|
|
35
38
|
},
|
|
36
39
|
"devDependencies": {
|
|
40
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
41
|
+
"@testing-library/react": "^16.3.2",
|
|
37
42
|
"@types/node": "^22.0.0",
|
|
38
43
|
"@types/react": "^19.0.0",
|
|
39
44
|
"@types/react-dom": "^19.0.0",
|
|
45
|
+
"jsdom": "^24.1.3",
|
|
40
46
|
"react": "^19.0.0",
|
|
41
47
|
"react-dom": "^19.0.0",
|
|
42
48
|
"tsdown": "^0.21.7",
|
|
43
49
|
"typescript": "^5.9.3",
|
|
44
|
-
"boltdocs": "3.
|
|
50
|
+
"boltdocs": "3.4.0",
|
|
51
|
+
"tsdown-config": "1.0.0"
|
|
45
52
|
},
|
|
46
53
|
"scripts": {
|
|
47
54
|
"build": "tsdown --config-loader unrun",
|