chatbotlite 0.3.1 → 0.5.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/README.md +205 -228
- package/dist/client/index.cjs +329 -29
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +59 -4
- package/dist/client/index.d.ts +59 -4
- package/dist/client/index.js +329 -30
- package/dist/client/index.js.map +1 -1
- package/dist/core/index.cjs +94 -21
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +55 -6
- package/dist/core/index.d.ts +55 -6
- package/dist/core/index.js +91 -22
- package/dist/core/index.js.map +1 -1
- package/dist/embed.global.js +101 -0
- package/dist/embed.global.js.map +1 -0
- package/dist/index.cjs +363 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +359 -30
- package/dist/index.js.map +1 -1
- package/dist/judges-B0AAZLS9.d.ts +49 -0
- package/dist/judges-CSRIUVlF.d.cts +49 -0
- package/dist/react/index.cjs +1269 -114
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +56 -2
- package/dist/react/index.d.ts +56 -2
- package/dist/react/index.js +1269 -114
- package/dist/react/index.js.map +1 -1
- package/dist/{types-J7BXpiRU.d.cts → types-BFlAWQF4.d.cts} +16 -1
- package/dist/{types-J7BXpiRU.d.ts → types-BFlAWQF4.d.ts} +16 -1
- package/package.json +3 -2
- package/dist/types-4alyzg8O.d.cts +0 -16
- package/dist/types-4alyzg8O.d.ts +0 -16
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,59 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/core/tools.ts
|
|
4
|
+
var MARKER_RE = /\[SKILL:(\w+)((?:\s+\w+=(?:"[^"]*"|[\w./@*+,:-]+))*)\s*\]/g;
|
|
5
|
+
var ARG_RE = /(\w+)=("([^"]*)"|([\w./@*+,:-]+))/g;
|
|
6
|
+
function coerce(value) {
|
|
7
|
+
if (value === "true") return true;
|
|
8
|
+
if (value === "false") return false;
|
|
9
|
+
if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function parseToolMarkers(text) {
|
|
13
|
+
const markers = [];
|
|
14
|
+
let m;
|
|
15
|
+
MARKER_RE.lastIndex = 0;
|
|
16
|
+
while ((m = MARKER_RE.exec(text)) !== null) {
|
|
17
|
+
const name = m[1];
|
|
18
|
+
const argsRaw = m[2] ?? "";
|
|
19
|
+
const args = {};
|
|
20
|
+
let a;
|
|
21
|
+
ARG_RE.lastIndex = 0;
|
|
22
|
+
while ((a = ARG_RE.exec(argsRaw)) !== null) {
|
|
23
|
+
const key = a[1];
|
|
24
|
+
const value = a[3] ?? a[4] ?? "";
|
|
25
|
+
args[key] = coerce(value);
|
|
26
|
+
}
|
|
27
|
+
markers.push({ name, args, raw: m[0] });
|
|
28
|
+
}
|
|
29
|
+
return markers;
|
|
30
|
+
}
|
|
31
|
+
function stripToolMarkers(text) {
|
|
32
|
+
return text.replace(MARKER_RE, "").replace(/\s+\n/g, "\n").trim();
|
|
33
|
+
}
|
|
34
|
+
function buildToolsPromptAddendum(enabledTools) {
|
|
35
|
+
if (enabledTools.length === 0) return "";
|
|
36
|
+
const examples = {
|
|
37
|
+
uploadForReview: '[SKILL:uploadForReview purpose="T4 slip" accept="image/*,application/pdf" maxMb=10] \u2014 collect a document for human review (bytes go to webhook, you never see content)',
|
|
38
|
+
scheduleCallback: '[SKILL:scheduleCallback durationMin=15 timezone="America/Vancouver"] \u2014 let the user pick a callback time slot',
|
|
39
|
+
requestPayment: '[SKILL:requestPayment amount=4250 currency="cad" reason="initial deposit"] \u2014 collect payment via inline card'
|
|
40
|
+
};
|
|
41
|
+
const lines = enabledTools.filter((t) => examples[t]).map((t) => `- ${examples[t]}`);
|
|
42
|
+
if (lines.length === 0) return "";
|
|
43
|
+
return [
|
|
44
|
+
"",
|
|
45
|
+
"## Available tools",
|
|
46
|
+
"When you need one of these workflows, emit the marker INLINE in your reply.",
|
|
47
|
+
"Write a short message first, THEN the marker. The marker will be replaced by an interactive card.",
|
|
48
|
+
"Pause the conversation after emitting \u2014 wait for the tool result before continuing.",
|
|
49
|
+
"",
|
|
50
|
+
...lines
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
3
54
|
// src/core/prompts.ts
|
|
4
|
-
function buildSystemPrompt(knowledge) {
|
|
55
|
+
function buildSystemPrompt(knowledge, enabledTools = []) {
|
|
56
|
+
const toolsAddendum = buildToolsPromptAddendum(enabledTools);
|
|
5
57
|
return [
|
|
6
58
|
"You are an AI assistant on a business website. Use ONLY the knowledge below to answer.",
|
|
7
59
|
"",
|
|
@@ -14,33 +66,23 @@ function buildSystemPrompt(knowledge) {
|
|
|
14
66
|
"- For anything not covered in the knowledge above, say the owner will follow up \u2014 do NOT guess.",
|
|
15
67
|
'- If the caller is clearly a vendor/sales pitch, say: "This does not look like a customer service request, so we will not continue this thread."',
|
|
16
68
|
`- If wrong number or asked to stop, say: "Sorry about that. We won't text again."`,
|
|
17
|
-
"- Match the caller's language automatically."
|
|
18
|
-
|
|
69
|
+
"- Match the caller's language automatically.",
|
|
70
|
+
toolsAddendum
|
|
71
|
+
].filter(Boolean).join("\n");
|
|
19
72
|
}
|
|
20
73
|
|
|
21
74
|
// src/core/guards.ts
|
|
22
75
|
var FORBIDDEN_PHRASES = [
|
|
23
|
-
"help is coming",
|
|
24
|
-
"someone is on the way",
|
|
25
|
-
"technician is on the way",
|
|
26
|
-
"provider is on the way",
|
|
27
|
-
"dispatching someone",
|
|
28
76
|
"i've booked",
|
|
77
|
+
// fake booking
|
|
29
78
|
"i have booked",
|
|
30
|
-
"reservation confirmed",
|
|
31
79
|
"your appointment is confirmed",
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
"i
|
|
37
|
-
|
|
38
|
-
"guaranteed delivery",
|
|
39
|
-
"guaranteed arrival",
|
|
40
|
-
"will arrive at",
|
|
41
|
-
"arriving at",
|
|
42
|
-
"i'll send",
|
|
43
|
-
"i will send"
|
|
80
|
+
// fake confirmation
|
|
81
|
+
"reservation confirmed",
|
|
82
|
+
"someone is on the way",
|
|
83
|
+
// false dispatch
|
|
84
|
+
"i guarantee"
|
|
85
|
+
// legal liability
|
|
44
86
|
];
|
|
45
87
|
function checkForbiddenPhrases(reply) {
|
|
46
88
|
const lower = reply.toLowerCase();
|
|
@@ -65,6 +107,33 @@ function stripForbidden(reply) {
|
|
|
65
107
|
return trimmed;
|
|
66
108
|
}
|
|
67
109
|
|
|
110
|
+
// src/core/judges.ts
|
|
111
|
+
async function runJudge(config, apiKey, endpointUrl, content, fetcher) {
|
|
112
|
+
const res = await fetcher(`${endpointUrl}/chat/completions`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: {
|
|
115
|
+
Authorization: `Bearer ${apiKey}`,
|
|
116
|
+
"Content-Type": "application/json"
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
model: config.model,
|
|
120
|
+
messages: [
|
|
121
|
+
{ role: "system", content: config.prompt },
|
|
122
|
+
{ role: "user", content }
|
|
123
|
+
],
|
|
124
|
+
temperature: 0,
|
|
125
|
+
max_tokens: 10
|
|
126
|
+
})
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
return { decision: "PASS", raw: `judge HTTP ${res.status} \u2014 fail-open` };
|
|
130
|
+
}
|
|
131
|
+
const data = await res.json();
|
|
132
|
+
const raw = (data.choices?.[0]?.message?.content ?? "").trim().toUpperCase();
|
|
133
|
+
const decision = raw.startsWith("BLOCK") ? "BLOCK" : "PASS";
|
|
134
|
+
return { decision, raw };
|
|
135
|
+
}
|
|
136
|
+
|
|
68
137
|
// src/client/types.ts
|
|
69
138
|
var PROVIDER_NAMES = /* @__PURE__ */ new Set([
|
|
70
139
|
"openai",
|
|
@@ -85,22 +154,34 @@ function isKnownProvider(name) {
|
|
|
85
154
|
|
|
86
155
|
// src/client/providers.ts
|
|
87
156
|
var PROVIDER_ENDPOINTS = {
|
|
88
|
-
openai: { baseUrl: "https://api.openai.com/v1", defaultModel: "gpt-4o-mini" },
|
|
157
|
+
openai: { baseUrl: "https://api.openai.com/v1", defaultModel: "gpt-4o-mini", visionModel: "gpt-4o" },
|
|
89
158
|
deepseek: { baseUrl: "https://api.deepseek.com/v1", defaultModel: "deepseek-chat" },
|
|
90
|
-
groq: { baseUrl: "https://api.groq.com/openai/v1", defaultModel: "llama-3.3-70b-versatile" },
|
|
91
|
-
gemini: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", defaultModel: "gemini-2.5-flash" },
|
|
92
|
-
anthropic: { baseUrl: "https://api.anthropic.com/v1", defaultModel: "claude-haiku-4-5" },
|
|
159
|
+
groq: { baseUrl: "https://api.groq.com/openai/v1", defaultModel: "llama-3.3-70b-versatile", visionModel: "llama-3.2-90b-vision-preview" },
|
|
160
|
+
gemini: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", defaultModel: "gemini-2.5-flash", visionModel: "gemini-2.5-flash" },
|
|
161
|
+
anthropic: { baseUrl: "https://api.anthropic.com/v1", defaultModel: "claude-haiku-4-5", visionModel: "claude-haiku-4-5" },
|
|
93
162
|
cerebras: { baseUrl: "https://api.cerebras.ai/v1", defaultModel: "qwen-3-235b-a22b-instruct-2507" },
|
|
94
163
|
sambanova: { baseUrl: "https://api.sambanova.ai/v1", defaultModel: "Meta-Llama-3.3-70B-Instruct" },
|
|
95
164
|
fireworks: { baseUrl: "https://api.fireworks.ai/inference/v1", defaultModel: "accounts/fireworks/models/llama-v3p3-70b-instruct" },
|
|
96
165
|
mistral: { baseUrl: "https://api.mistral.ai/v1", defaultModel: "mistral-small-latest" },
|
|
97
|
-
openrouter: { baseUrl: "https://openrouter.ai/api/v1", defaultModel: "deepseek/deepseek-chat" },
|
|
98
|
-
moonshot: { baseUrl: "https://api.moonshot.ai/v1", defaultModel: "moonshot-v1-32k" }
|
|
166
|
+
openrouter: { baseUrl: "https://openrouter.ai/api/v1", defaultModel: "deepseek/deepseek-chat", visionModel: "openai/gpt-4o" },
|
|
167
|
+
moonshot: { baseUrl: "https://api.moonshot.ai/v1", defaultModel: "moonshot-v1-32k", visionModel: "moonshot-v1-32k-vision-preview" }
|
|
99
168
|
};
|
|
100
169
|
function isRetryableError(err) {
|
|
101
170
|
const msg = err instanceof Error ? err.message : String(err);
|
|
102
171
|
return /\b(429|rate.?limit|quota|exceed|5\d\d|timeout|ECONNRESET|fetch failed)\b/i.test(msg);
|
|
103
172
|
}
|
|
173
|
+
async function fileToDataUrl(file) {
|
|
174
|
+
const buf = new Uint8Array(await file.arrayBuffer());
|
|
175
|
+
const base64 = bufferToBase64(buf);
|
|
176
|
+
const mime = file.type || "application/octet-stream";
|
|
177
|
+
return `data:${mime};base64,${base64}`;
|
|
178
|
+
}
|
|
179
|
+
function bufferToBase64(buf) {
|
|
180
|
+
let bin = "";
|
|
181
|
+
for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i]);
|
|
182
|
+
if (typeof btoa === "function") return btoa(bin);
|
|
183
|
+
return globalThis.Buffer.from(bin, "binary").toString("base64");
|
|
184
|
+
}
|
|
104
185
|
|
|
105
186
|
// src/client/chatbot.ts
|
|
106
187
|
var ChatBot = class {
|
|
@@ -109,18 +190,258 @@ var ChatBot = class {
|
|
|
109
190
|
fetcher;
|
|
110
191
|
timeoutMs;
|
|
111
192
|
cachedSystemPrompt;
|
|
193
|
+
guards;
|
|
194
|
+
knowledge;
|
|
112
195
|
constructor(init) {
|
|
113
196
|
if (!init.knowledge || typeof init.knowledge !== "string" || init.knowledge.trim().length === 0) {
|
|
114
197
|
throw new Error("chatbotlite: knowledge is required (a non-empty markdown string).");
|
|
115
198
|
}
|
|
199
|
+
this.knowledge = init.knowledge;
|
|
116
200
|
this.keys = init.providers.keys ?? {};
|
|
117
201
|
this.steps = resolveChain(init.providers);
|
|
118
202
|
this.fetcher = init.options?.fetch ?? globalThis.fetch.bind(globalThis);
|
|
119
203
|
this.timeoutMs = init.options?.timeoutMs ?? 3e4;
|
|
120
204
|
this.cachedSystemPrompt = buildSystemPrompt(init.knowledge);
|
|
205
|
+
this.guards = init.guards ?? {};
|
|
206
|
+
}
|
|
207
|
+
/** Build system prompt for given opts — uses cached if no enabledTools, else rebuilds. */
|
|
208
|
+
resolveSystemPrompt(opts) {
|
|
209
|
+
if (opts.systemPrompt) return opts.systemPrompt;
|
|
210
|
+
if (opts.enabledTools && opts.enabledTools.length > 0) {
|
|
211
|
+
return buildSystemPrompt(this.knowledge, opts.enabledTools);
|
|
212
|
+
}
|
|
213
|
+
return this.cachedSystemPrompt;
|
|
214
|
+
}
|
|
215
|
+
/** Run an LLM judge against content. Fail-open on errors. */
|
|
216
|
+
async judge(config, content) {
|
|
217
|
+
const endpoint = PROVIDER_ENDPOINTS[config.provider];
|
|
218
|
+
const key = this.keys[config.provider];
|
|
219
|
+
if (!key) {
|
|
220
|
+
return { decision: "PASS", raw: `judge provider ${config.provider} has no key \u2014 fail-open` };
|
|
221
|
+
}
|
|
222
|
+
const model = config.model ?? endpoint.defaultModel;
|
|
223
|
+
return runJudge(
|
|
224
|
+
{ provider: config.provider, model, prompt: config.prompt },
|
|
225
|
+
key,
|
|
226
|
+
endpoint.baseUrl,
|
|
227
|
+
content,
|
|
228
|
+
this.fetcher
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Stream a reply as SSE events. Returns a ReadableStream that yields tokens
|
|
233
|
+
* progressively. Designed to plug into Next.js/Hono/Express route handlers:
|
|
234
|
+
*
|
|
235
|
+
* ```ts
|
|
236
|
+
* export async function POST(req: Request) {
|
|
237
|
+
* const { message, transcript } = await req.json();
|
|
238
|
+
* const stream = await bot.replyStream(message, { history: transcript });
|
|
239
|
+
* return new Response(stream, {
|
|
240
|
+
* headers: { "Content-Type": "text/event-stream" }
|
|
241
|
+
* });
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*
|
|
245
|
+
* Events emitted (one per `data:` line, SSE format):
|
|
246
|
+
* event: token data: "<text fragment>"
|
|
247
|
+
* event: done data: {"reply":"...","usedProvider":"...","usedModel":"...","attempts":[...]}
|
|
248
|
+
* event: error data: {"message":"...","attempts":[...]}
|
|
249
|
+
*/
|
|
250
|
+
async replyStream(message, opts = {}) {
|
|
251
|
+
const systemPrompt = this.resolveSystemPrompt(opts);
|
|
252
|
+
const messages = [
|
|
253
|
+
{ role: "system", content: systemPrompt },
|
|
254
|
+
...opts.history ?? [],
|
|
255
|
+
{ role: "user", content: message }
|
|
256
|
+
];
|
|
257
|
+
const steps = this.steps;
|
|
258
|
+
const fetcher = this.fetcher;
|
|
259
|
+
const keys = this.keys;
|
|
260
|
+
const timeoutMs = this.timeoutMs;
|
|
261
|
+
const encoder = new TextEncoder();
|
|
262
|
+
const sse = (event, data) => encoder.encode(`event: ${event}
|
|
263
|
+
data: ${data}
|
|
264
|
+
|
|
265
|
+
`);
|
|
266
|
+
return new ReadableStream({
|
|
267
|
+
async start(controller) {
|
|
268
|
+
const attempts = [];
|
|
269
|
+
let lastError;
|
|
270
|
+
let assembled = "";
|
|
271
|
+
for (const step of steps) {
|
|
272
|
+
const t0 = Date.now();
|
|
273
|
+
const endpoint = PROVIDER_ENDPOINTS[step.provider];
|
|
274
|
+
const key = keys[step.provider];
|
|
275
|
+
if (!key) {
|
|
276
|
+
attempts.push({ provider: step.provider, model: step.model, status: "error", error: "missing key", latencyMs: 0 });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const abortCtrl = new AbortController();
|
|
280
|
+
const timer = setTimeout(() => abortCtrl.abort(), timeoutMs);
|
|
281
|
+
try {
|
|
282
|
+
const res = await fetcher(`${endpoint.baseUrl}/chat/completions`, {
|
|
283
|
+
method: "POST",
|
|
284
|
+
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
|
285
|
+
body: JSON.stringify({ model: step.model, messages, temperature: 0.3, max_tokens: 300, stream: true }),
|
|
286
|
+
signal: abortCtrl.signal
|
|
287
|
+
});
|
|
288
|
+
if (!res.ok) {
|
|
289
|
+
const body = await res.text();
|
|
290
|
+
throw new Error(`${res.status}: ${body.slice(0, 200)}`);
|
|
291
|
+
}
|
|
292
|
+
const reader = res.body.getReader();
|
|
293
|
+
const decoder = new TextDecoder();
|
|
294
|
+
let sseBuffer = "";
|
|
295
|
+
while (true) {
|
|
296
|
+
const { done, value } = await reader.read();
|
|
297
|
+
if (done) break;
|
|
298
|
+
sseBuffer += decoder.decode(value, { stream: true });
|
|
299
|
+
const lines = sseBuffer.split("\n");
|
|
300
|
+
sseBuffer = lines.pop() ?? "";
|
|
301
|
+
for (const line of lines) {
|
|
302
|
+
const trimmed = line.trim();
|
|
303
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
304
|
+
const payload = trimmed.slice(5).trim();
|
|
305
|
+
if (payload === "[DONE]") continue;
|
|
306
|
+
try {
|
|
307
|
+
const obj = JSON.parse(payload);
|
|
308
|
+
const delta = obj.choices?.[0]?.delta?.content ?? obj.choices?.[0]?.delta?.reasoning_content ?? "";
|
|
309
|
+
if (delta) {
|
|
310
|
+
assembled += delta;
|
|
311
|
+
controller.enqueue(sse("token", JSON.stringify(delta)));
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
attempts.push({ provider: step.provider, model: step.model, status: "ok", latencyMs: Date.now() - t0 });
|
|
318
|
+
const guard = checkForbiddenPhrases(assembled);
|
|
319
|
+
const finalReply = guard.ok ? assembled : stripForbidden(assembled);
|
|
320
|
+
controller.enqueue(sse("done", JSON.stringify({
|
|
321
|
+
reply: finalReply,
|
|
322
|
+
usedProvider: step.provider,
|
|
323
|
+
usedModel: step.model,
|
|
324
|
+
guardWarnings: guard.violations,
|
|
325
|
+
attempts
|
|
326
|
+
})));
|
|
327
|
+
controller.close();
|
|
328
|
+
return;
|
|
329
|
+
} catch (err) {
|
|
330
|
+
lastError = err;
|
|
331
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
332
|
+
attempts.push({ provider: step.provider, model: step.model, status: "error", error: errMsg, latencyMs: Date.now() - t0 });
|
|
333
|
+
assembled = "";
|
|
334
|
+
if (!isRetryableError(err)) {
|
|
335
|
+
controller.enqueue(sse("error", JSON.stringify({ message: `${step.label} failed (non-retryable): ${errMsg}`, attempts })));
|
|
336
|
+
controller.close();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
} finally {
|
|
340
|
+
clearTimeout(timer);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const summary = attempts.map((a) => `${a.provider}/${a.model}:${a.error ?? "ok"}`).join(" \u2192 ");
|
|
344
|
+
controller.enqueue(sse("error", JSON.stringify({
|
|
345
|
+
message: `all chain steps failed. Trace: ${summary}. Last error: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
|
|
346
|
+
attempts
|
|
347
|
+
})));
|
|
348
|
+
controller.close();
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Reply to a message with optional image attachments. Routes through vision-capable
|
|
354
|
+
* providers only — chain steps without `visionModel` are skipped (logged in attempts).
|
|
355
|
+
*
|
|
356
|
+
* Images are inlined as data: URLs. Keep total payload modest (<10MB).
|
|
357
|
+
*/
|
|
358
|
+
async replyWithMedia(message, opts = {}) {
|
|
359
|
+
const images = opts.images ?? [];
|
|
360
|
+
if (images.length === 0) {
|
|
361
|
+
return this.reply(message, opts);
|
|
362
|
+
}
|
|
363
|
+
const dataUrls = await Promise.all(images.map(fileToDataUrl));
|
|
364
|
+
const systemPrompt = this.resolveSystemPrompt(opts);
|
|
365
|
+
const userContent = [];
|
|
366
|
+
if (message) userContent.push({ type: "text", text: message });
|
|
367
|
+
for (const url of dataUrls) userContent.push({ type: "image_url", image_url: { url } });
|
|
368
|
+
const messages = [
|
|
369
|
+
{ role: "system", content: systemPrompt },
|
|
370
|
+
...opts.history ?? [],
|
|
371
|
+
{ role: "user", content: userContent }
|
|
372
|
+
];
|
|
373
|
+
const attempts = [];
|
|
374
|
+
let lastError;
|
|
375
|
+
for (const step of this.steps) {
|
|
376
|
+
const endpoint = PROVIDER_ENDPOINTS[step.provider];
|
|
377
|
+
if (!endpoint.visionModel) {
|
|
378
|
+
attempts.push({ provider: step.provider, model: step.model, status: "error", error: "no vision support", latencyMs: 0 });
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const t0 = Date.now();
|
|
382
|
+
const visionStep = { provider: step.provider, model: endpoint.visionModel, label: `${step.provider}/${endpoint.visionModel}` };
|
|
383
|
+
try {
|
|
384
|
+
const key = this.keys[step.provider];
|
|
385
|
+
if (!key) throw new Error(`Missing API key for provider: ${step.provider}`);
|
|
386
|
+
const controller = new AbortController();
|
|
387
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
388
|
+
try {
|
|
389
|
+
const res = await this.fetcher(`${endpoint.baseUrl}/chat/completions`, {
|
|
390
|
+
method: "POST",
|
|
391
|
+
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
|
392
|
+
body: JSON.stringify({ model: visionStep.model, messages, temperature: 0.3, max_tokens: 400 }),
|
|
393
|
+
signal: controller.signal
|
|
394
|
+
});
|
|
395
|
+
if (!res.ok) {
|
|
396
|
+
const body = await res.text();
|
|
397
|
+
throw new Error(`${res.status}: ${body.slice(0, 200)}`);
|
|
398
|
+
}
|
|
399
|
+
const data = await res.json();
|
|
400
|
+
const reply = (data.choices?.[0]?.message?.content ?? "").trim();
|
|
401
|
+
if (!reply) throw new Error("empty vision reply");
|
|
402
|
+
attempts.push({ provider: visionStep.provider, model: visionStep.model, status: "ok", latencyMs: Date.now() - t0 });
|
|
403
|
+
const guard = checkForbiddenPhrases(reply);
|
|
404
|
+
const finalReply = guard.ok ? reply : stripForbidden(reply);
|
|
405
|
+
return {
|
|
406
|
+
reply: finalReply,
|
|
407
|
+
usedProvider: visionStep.provider,
|
|
408
|
+
usedModel: visionStep.model,
|
|
409
|
+
...data.usage ? { usage: data.usage } : {},
|
|
410
|
+
guardWarnings: guard.violations,
|
|
411
|
+
attempts
|
|
412
|
+
};
|
|
413
|
+
} finally {
|
|
414
|
+
clearTimeout(timer);
|
|
415
|
+
}
|
|
416
|
+
} catch (err) {
|
|
417
|
+
lastError = err;
|
|
418
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
419
|
+
attempts.push({ provider: visionStep.provider, model: visionStep.model, status: "error", error: errMsg, latencyMs: Date.now() - t0 });
|
|
420
|
+
if (!isRetryableError(err)) {
|
|
421
|
+
throw new Error(`chatbotlite: ${visionStep.label} vision failed (non-retryable). ${errMsg}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const summary = attempts.map((a) => `${a.provider}/${a.model}:${a.error ?? "ok"}`).join(" \u2192 ");
|
|
426
|
+
throw new Error(`chatbotlite: no vision-capable provider succeeded. Trace: ${summary}. Last error: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
121
427
|
}
|
|
122
428
|
async reply(message, opts = {}) {
|
|
123
|
-
|
|
429
|
+
let inputVerdict;
|
|
430
|
+
if (this.guards.inputJudge) {
|
|
431
|
+
inputVerdict = await this.judge(this.guards.inputJudge, message);
|
|
432
|
+
if (inputVerdict.decision === "BLOCK") {
|
|
433
|
+
return {
|
|
434
|
+
reply: "I can't process that request. Please ask in a different way.",
|
|
435
|
+
usedProvider: this.steps[0].provider,
|
|
436
|
+
usedModel: this.steps[0].model,
|
|
437
|
+
guardWarnings: [],
|
|
438
|
+
judges: { input: inputVerdict },
|
|
439
|
+
attempts: [],
|
|
440
|
+
blockedByInputJudge: true
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const systemPrompt = this.resolveSystemPrompt(opts);
|
|
124
445
|
const messages = [
|
|
125
446
|
{ role: "system", content: systemPrompt },
|
|
126
447
|
...opts.history ?? [],
|
|
@@ -134,13 +455,21 @@ var ChatBot = class {
|
|
|
134
455
|
const result = await this.callProvider(step, messages);
|
|
135
456
|
attempts.push({ provider: step.provider, model: step.model, status: "ok", latencyMs: Date.now() - t0 });
|
|
136
457
|
const guard = checkForbiddenPhrases(result.reply);
|
|
137
|
-
|
|
458
|
+
let finalReply = guard.ok ? result.reply : stripForbidden(result.reply);
|
|
459
|
+
let outputVerdict;
|
|
460
|
+
if (this.guards.outputJudge) {
|
|
461
|
+
outputVerdict = await this.judge(this.guards.outputJudge, finalReply);
|
|
462
|
+
if (outputVerdict.decision === "BLOCK") {
|
|
463
|
+
finalReply = "Let me check with the owner and get back to you on that.";
|
|
464
|
+
}
|
|
465
|
+
}
|
|
138
466
|
return {
|
|
139
467
|
reply: finalReply,
|
|
140
468
|
usedProvider: step.provider,
|
|
141
469
|
usedModel: step.model,
|
|
142
470
|
...result.usage ? { usage: result.usage } : {},
|
|
143
471
|
guardWarnings: guard.violations,
|
|
472
|
+
...inputVerdict || outputVerdict ? { judges: { ...inputVerdict ? { input: inputVerdict } : {}, ...outputVerdict ? { output: outputVerdict } : {} } } : {},
|
|
144
473
|
attempts
|
|
145
474
|
};
|
|
146
475
|
} catch (err) {
|
|
@@ -230,9 +559,14 @@ exports.ChatBot = ChatBot;
|
|
|
230
559
|
exports.FORBIDDEN_PHRASES = FORBIDDEN_PHRASES;
|
|
231
560
|
exports.PROVIDER_ENDPOINTS = PROVIDER_ENDPOINTS;
|
|
232
561
|
exports.buildSystemPrompt = buildSystemPrompt;
|
|
562
|
+
exports.buildToolsPromptAddendum = buildToolsPromptAddendum;
|
|
233
563
|
exports.checkForbiddenPhrases = checkForbiddenPhrases;
|
|
564
|
+
exports.fileToDataUrl = fileToDataUrl;
|
|
234
565
|
exports.isKnownProvider = isKnownProvider;
|
|
235
566
|
exports.isRetryableError = isRetryableError;
|
|
567
|
+
exports.parseToolMarkers = parseToolMarkers;
|
|
568
|
+
exports.runJudge = runJudge;
|
|
236
569
|
exports.stripForbidden = stripForbidden;
|
|
570
|
+
exports.stripToolMarkers = stripToolMarkers;
|
|
237
571
|
//# sourceMappingURL=index.cjs.map
|
|
238
572
|
//# sourceMappingURL=index.cjs.map
|