chatbotlite 0.0.1 → 0.2.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.
@@ -0,0 +1,292 @@
1
+ // src/client/types.ts
2
+ var PROVIDER_NAMES = /* @__PURE__ */ new Set([
3
+ "openai",
4
+ "deepseek",
5
+ "groq",
6
+ "gemini",
7
+ "anthropic",
8
+ "cerebras",
9
+ "sambanova",
10
+ "fireworks",
11
+ "mistral",
12
+ "openrouter",
13
+ "moonshot"
14
+ ]);
15
+ function parseChainSpec(spec) {
16
+ const slash = spec.indexOf("/");
17
+ if (slash === -1) {
18
+ if (!PROVIDER_NAMES.has(spec)) {
19
+ throw new Error(`chatbotlite: unknown provider "${spec}". Use "provider/model" or a known provider name.`);
20
+ }
21
+ return { provider: spec, model: null };
22
+ }
23
+ const provider = spec.slice(0, slash);
24
+ const model = spec.slice(slash + 1);
25
+ if (!PROVIDER_NAMES.has(provider)) {
26
+ throw new Error(`chatbotlite: unknown provider "${provider}" in chain spec "${spec}".`);
27
+ }
28
+ if (!model) {
29
+ throw new Error(`chatbotlite: empty model name in chain spec "${spec}".`);
30
+ }
31
+ return { provider, model };
32
+ }
33
+ function isKnownProvider(name) {
34
+ return PROVIDER_NAMES.has(name);
35
+ }
36
+
37
+ // src/client/providers.ts
38
+ var PROVIDER_ENDPOINTS = {
39
+ openai: { baseUrl: "https://api.openai.com/v1", defaultModel: "gpt-4o-mini" },
40
+ deepseek: { baseUrl: "https://api.deepseek.com/v1", defaultModel: "deepseek-chat" },
41
+ groq: { baseUrl: "https://api.groq.com/openai/v1", defaultModel: "llama-3.3-70b-versatile" },
42
+ gemini: { baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai", defaultModel: "gemini-2.5-flash" },
43
+ anthropic: { baseUrl: "https://api.anthropic.com/v1", defaultModel: "claude-haiku-4-5" },
44
+ cerebras: { baseUrl: "https://api.cerebras.ai/v1", defaultModel: "qwen-3-235b-a22b-instruct-2507" },
45
+ sambanova: { baseUrl: "https://api.sambanova.ai/v1", defaultModel: "Meta-Llama-3.3-70B-Instruct" },
46
+ fireworks: { baseUrl: "https://api.fireworks.ai/inference/v1", defaultModel: "accounts/fireworks/models/llama-v3p3-70b-instruct" },
47
+ mistral: { baseUrl: "https://api.mistral.ai/v1", defaultModel: "mistral-small-latest" },
48
+ openrouter: { baseUrl: "https://openrouter.ai/api/v1", defaultModel: "deepseek/deepseek-chat" },
49
+ moonshot: { baseUrl: "https://api.moonshot.ai/v1", defaultModel: "moonshot-v1-32k" }
50
+ };
51
+ function isRetryableError(err) {
52
+ const msg = err instanceof Error ? err.message : String(err);
53
+ return /\b(429|rate.?limit|quota|exceed|5\d\d|timeout|ECONNRESET|fetch failed)\b/i.test(msg);
54
+ }
55
+
56
+ // src/core/prompts.ts
57
+ function buildSystemPrompt(business) {
58
+ const parts = [];
59
+ parts.push(`You are an AI receptionist for ${business.name}.`);
60
+ if (business.description) parts.push(business.description);
61
+ parts.push("");
62
+ if (business.services && business.services.length > 0) {
63
+ parts.push("Services offered (only these \u2014 do not invent others):");
64
+ for (const s of business.services) {
65
+ const price = s.price ? ` \u2014 ${s.price}` : "";
66
+ const notes = s.notes ? ` (${s.notes})` : "";
67
+ parts.push(`- ${s.name}${price}${notes}`);
68
+ }
69
+ parts.push("");
70
+ }
71
+ if (business.hours) {
72
+ parts.push(`Hours: ${business.hours}`);
73
+ }
74
+ if (business.serviceArea && business.serviceArea.length > 0) {
75
+ parts.push(`Service area: ${business.serviceArea.join(", ")}.`);
76
+ parts.push("If the caller is outside this area, say so and recommend owner review.");
77
+ }
78
+ if (business.policies && business.policies.length > 0) {
79
+ parts.push("");
80
+ parts.push("Known policies (use these exact answers when asked):");
81
+ for (const p of business.policies) {
82
+ parts.push(`- ${p.topic}: ${p.answer}`);
83
+ }
84
+ }
85
+ parts.push("");
86
+ parts.push("Rules:");
87
+ parts.push("- Reply in 1-2 short sentences, conversational tone.");
88
+ parts.push("- NEVER invent prices, availability, dispatch times, or appointment confirmations.");
89
+ parts.push("- For anything not covered in the setup above, say it needs owner review.");
90
+ parts.push('- 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."');
91
+ parts.push(`- If wrong number or asked to stop, say: "Sorry about that. We won't text again."`);
92
+ if (business.doNotPromise && business.doNotPromise.length > 0) {
93
+ parts.push("");
94
+ parts.push("Never promise:");
95
+ for (const p of business.doNotPromise) parts.push(`- ${p}`);
96
+ }
97
+ if (business.customInstructions) {
98
+ parts.push("");
99
+ parts.push("Additional instructions:");
100
+ parts.push(business.customInstructions);
101
+ }
102
+ if (business.language && business.language !== "en") {
103
+ parts.push("");
104
+ parts.push(`Reply in ${business.language}.`);
105
+ }
106
+ return parts.join("\n");
107
+ }
108
+
109
+ // src/core/guards.ts
110
+ var FORBIDDEN_PHRASES = [
111
+ "help is coming",
112
+ "someone is on the way",
113
+ "technician is on the way",
114
+ "provider is on the way",
115
+ "dispatching someone",
116
+ "i've booked",
117
+ "i have booked",
118
+ "reservation confirmed",
119
+ "your appointment is confirmed",
120
+ "i've scheduled",
121
+ "i have scheduled",
122
+ "we've dispatched",
123
+ "we have dispatched",
124
+ "i can confirm",
125
+ "i guarantee",
126
+ "guaranteed delivery",
127
+ "guaranteed arrival",
128
+ "will arrive at",
129
+ "arriving at",
130
+ "i'll send",
131
+ "i will send"
132
+ ];
133
+ function checkForbiddenPhrases(reply) {
134
+ const lower = reply.toLowerCase();
135
+ const violations = [];
136
+ for (const phrase of FORBIDDEN_PHRASES) {
137
+ if (lower.includes(phrase)) {
138
+ violations.push(`Forbidden phrase: "${phrase}"`);
139
+ }
140
+ }
141
+ return { ok: violations.length === 0, violations };
142
+ }
143
+ function stripForbidden(reply) {
144
+ const sentences = reply.split(/(?<=[.!?])\s+/);
145
+ const kept = sentences.filter((s) => {
146
+ const lower = s.toLowerCase();
147
+ return !FORBIDDEN_PHRASES.some((p) => lower.includes(p));
148
+ });
149
+ const trimmed = kept.join(" ").trim();
150
+ if (trimmed.length < 10) {
151
+ return "Thanks for reaching out \u2014 let me check with the owner and get back to you.";
152
+ }
153
+ return trimmed;
154
+ }
155
+
156
+ // src/client/chatbot.ts
157
+ var ChatBot = class {
158
+ business;
159
+ steps;
160
+ keys;
161
+ fetcher;
162
+ timeoutMs;
163
+ cachedSystemPrompt;
164
+ constructor(init) {
165
+ this.business = init.business;
166
+ this.keys = init.providers.keys ?? {};
167
+ this.steps = resolveChain(init.providers);
168
+ this.fetcher = init.options?.fetch ?? globalThis.fetch.bind(globalThis);
169
+ this.timeoutMs = init.options?.timeoutMs ?? 3e4;
170
+ this.cachedSystemPrompt = buildSystemPrompt(this.business);
171
+ }
172
+ async reply(message, opts = {}) {
173
+ const systemPrompt = opts.systemPrompt ?? this.cachedSystemPrompt;
174
+ const messages = [
175
+ { role: "system", content: systemPrompt },
176
+ ...opts.history ?? [],
177
+ { role: "user", content: message }
178
+ ];
179
+ const attempts = [];
180
+ let lastError;
181
+ for (const step of this.steps) {
182
+ const t0 = Date.now();
183
+ try {
184
+ const result = await this.callProvider(step, messages);
185
+ attempts.push({ provider: step.provider, model: step.model, status: "ok", latencyMs: Date.now() - t0 });
186
+ const guard = checkForbiddenPhrases(result.reply);
187
+ const finalReply = guard.ok ? result.reply : stripForbidden(result.reply);
188
+ return {
189
+ reply: finalReply,
190
+ usedProvider: step.provider,
191
+ usedModel: step.model,
192
+ ...result.usage ? { usage: result.usage } : {},
193
+ guardWarnings: guard.violations,
194
+ attempts
195
+ };
196
+ } catch (err) {
197
+ lastError = err;
198
+ const errMsg = err instanceof Error ? err.message : String(err);
199
+ attempts.push({
200
+ provider: step.provider,
201
+ model: step.model,
202
+ status: "error",
203
+ error: errMsg,
204
+ latencyMs: Date.now() - t0
205
+ });
206
+ if (!isRetryableError(err)) {
207
+ throw new Error(`chatbotlite: ${step.provider}/${step.model} failed (non-retryable). ${errMsg}`);
208
+ }
209
+ }
210
+ }
211
+ const summary = attempts.map((a) => `${a.provider}/${a.model}:${a.error ?? "ok"}`).join(" \u2192 ");
212
+ throw new Error(`chatbotlite: all chain steps failed. Trace: ${summary}. Last error: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
213
+ }
214
+ async callProvider(step, messages) {
215
+ const endpoint = PROVIDER_ENDPOINTS[step.provider];
216
+ const key = this.keys[step.provider];
217
+ if (!key) throw new Error(`Missing API key for provider: ${step.provider}`);
218
+ const controller = new AbortController();
219
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
220
+ try {
221
+ const res = await this.fetcher(`${endpoint.baseUrl}/chat/completions`, {
222
+ method: "POST",
223
+ headers: {
224
+ "Authorization": `Bearer ${key}`,
225
+ "Content-Type": "application/json"
226
+ },
227
+ body: JSON.stringify({
228
+ model: step.model,
229
+ messages,
230
+ temperature: 0.3,
231
+ max_tokens: 300
232
+ }),
233
+ signal: controller.signal
234
+ });
235
+ if (!res.ok) {
236
+ const body = await res.text();
237
+ throw new Error(`${res.status}: ${body.slice(0, 200)}`);
238
+ }
239
+ const data = await res.json();
240
+ const msg = data.choices?.[0]?.message;
241
+ const reply = (msg?.content?.trim() || msg?.reasoning_content?.trim()) ?? "";
242
+ if (!reply) throw new Error("empty reply from provider");
243
+ const result = { reply };
244
+ if (data.usage) result.usage = data.usage;
245
+ return result;
246
+ } finally {
247
+ clearTimeout(timer);
248
+ }
249
+ }
250
+ };
251
+ function resolveChain(providers) {
252
+ const keys = providers.keys ?? {};
253
+ const explicit = providers.chain;
254
+ if (explicit && explicit.length > 0) {
255
+ return explicit.map((entry) => normalizeChainEntry(entry, keys));
256
+ }
257
+ const orderedProviders = Object.keys(keys).filter((k) => isKnownProvider(k) && keys[k]);
258
+ if (orderedProviders.length === 0) {
259
+ throw new Error("chatbotlite: at least one provider key is required.");
260
+ }
261
+ return orderedProviders.map((provider) => ({
262
+ provider,
263
+ model: PROVIDER_ENDPOINTS[provider].defaultModel,
264
+ spec: `${provider}/${PROVIDER_ENDPOINTS[provider].defaultModel}`
265
+ }));
266
+ }
267
+ function normalizeChainEntry(entry, keys) {
268
+ let provider;
269
+ let model;
270
+ let spec;
271
+ if (typeof entry === "string") {
272
+ const parsed = parseChainSpec(entry);
273
+ provider = parsed.provider;
274
+ model = parsed.model ?? PROVIDER_ENDPOINTS[provider].defaultModel;
275
+ spec = entry;
276
+ } else {
277
+ if (!isKnownProvider(entry.provider)) {
278
+ throw new Error(`chatbotlite: unknown provider "${entry.provider}" in chain entry.`);
279
+ }
280
+ provider = entry.provider;
281
+ model = entry.model ?? PROVIDER_ENDPOINTS[provider].defaultModel;
282
+ spec = `${provider}/${model}`;
283
+ }
284
+ if (!keys[provider]) {
285
+ throw new Error(`chatbotlite: chain step "${spec}" needs a key for provider "${provider}" but none was provided.`);
286
+ }
287
+ return { provider, model, spec };
288
+ }
289
+
290
+ export { ChatBot, PROVIDER_ENDPOINTS, isKnownProvider, isRetryableError, parseChainSpec };
291
+ //# sourceMappingURL=index.js.map
292
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/client/types.ts","../../src/client/providers.ts","../../src/core/prompts.ts","../../src/core/guards.ts","../../src/client/chatbot.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// Client types — provider + chain config\n\nexport type Provider =\n | \"openai\"\n | \"deepseek\"\n | \"groq\"\n | \"gemini\"\n | \"anthropic\"\n | \"cerebras\"\n | \"sambanova\"\n | \"fireworks\"\n | \"mistral\"\n | \"openrouter\"\n | \"moonshot\";\n\n/**\n * One step in the fallback chain. Either a string spec or an object.\n *\n * - **String**: `\"provider/model\"` (e.g. `\"openai/gpt-4o\"`), or bare `\"provider\"` (uses provider's default model).\n * - **Object**: `{ provider, model? }` — type-safe alternative.\n */\nexport type ChainEntry =\n | string\n | { provider: Provider; model?: string };\n\n/**\n * Provider configuration.\n *\n * `keys` are auth credentials (one key per provider — that key covers all that provider's models).\n * `chain` is the ordered fallback list. Each entry is `\"provider/model\"` (string) or `{ provider, model }` (object).\n *\n * @example String form (compact, paste-friendly)\n * ```ts\n * providers: {\n * keys: {\n * deepseek: \"sk-...\",\n * groq: \"gsk-...\",\n * openai: \"sk-...\"\n * },\n * chain: [\n * \"deepseek/deepseek-chat\", // try first\n * \"groq/llama-3.3-70b-versatile\", // fallback 1\n * \"openai/gpt-4o-mini\" // fallback 2\n * ]\n * }\n * ```\n *\n * @example Object form (type-safe, IDE autocomplete on `provider`)\n * ```ts\n * providers: {\n * keys: { deepseek: \"sk-...\", openai: \"sk-...\" },\n * chain: [\n * { provider: \"deepseek\" }, // default model\n * { provider: \"openai\", model: \"gpt-4o\" },\n * { provider: \"openai\", model: \"gpt-4o-mini\" } // same key, cheaper model\n * ]\n * }\n * ```\n *\n * If `chain` is omitted, defaults to `[<each provider in keys> with its default model]`\n * in key insertion order.\n */\nexport interface ProviderConfig {\n /** API keys per provider. One key covers all that provider's models. */\n keys: Partial<Record<Provider, string>>;\n /**\n * Ordered fallback chain. Each entry: string `\"provider/model\"` OR object `{ provider, model? }`.\n * Defaults to `[provider/defaultModel]` for each key in insertion order.\n */\n chain?: ChainEntry[];\n}\n\nexport interface ClientOptions {\n fetch?: typeof globalThis.fetch;\n timeoutMs?: number;\n maxRetries?: number;\n}\n\nexport interface ChainStep {\n provider: Provider;\n model: string;\n /** Original spec string for diagnostics. */\n spec: string;\n}\n\nexport interface AttemptInfo {\n provider: Provider;\n model: string;\n status: \"ok\" | \"error\";\n error?: string;\n latencyMs: number;\n}\n\nconst PROVIDER_NAMES: ReadonlySet<string> = new Set([\n \"openai\", \"deepseek\", \"groq\", \"gemini\", \"anthropic\",\n \"cerebras\", \"sambanova\", \"fireworks\", \"mistral\", \"openrouter\", \"moonshot\"\n]);\n\nexport function parseChainSpec(spec: string): { provider: Provider; model: string | null } {\n const slash = spec.indexOf(\"/\");\n if (slash === -1) {\n if (!PROVIDER_NAMES.has(spec)) {\n throw new Error(`chatbotlite: unknown provider \"${spec}\". Use \"provider/model\" or a known provider name.`);\n }\n return { provider: spec as Provider, model: null };\n }\n const provider = spec.slice(0, slash);\n const model = spec.slice(slash + 1);\n if (!PROVIDER_NAMES.has(provider)) {\n throw new Error(`chatbotlite: unknown provider \"${provider}\" in chain spec \"${spec}\".`);\n }\n if (!model) {\n throw new Error(`chatbotlite: empty model name in chain spec \"${spec}\".`);\n }\n return { provider: provider as Provider, model };\n}\n\nexport function isKnownProvider(name: string): name is Provider {\n return PROVIDER_NAMES.has(name);\n}\n","import type { Provider } from \"./types.js\";\n\nexport interface ProviderEndpoint {\n baseUrl: string;\n defaultModel: string;\n}\n\n/**\n * Built-in OpenAI-compatible providers. All use /v1/chat/completions\n * with response in OpenAI format. Caller supplies API key per provider.\n */\nexport const PROVIDER_ENDPOINTS: Record<Provider, ProviderEndpoint> = {\n openai: { baseUrl: \"https://api.openai.com/v1\", defaultModel: \"gpt-4o-mini\" },\n deepseek: { baseUrl: \"https://api.deepseek.com/v1\", defaultModel: \"deepseek-chat\" },\n groq: { baseUrl: \"https://api.groq.com/openai/v1\", defaultModel: \"llama-3.3-70b-versatile\" },\n gemini: { baseUrl: \"https://generativelanguage.googleapis.com/v1beta/openai\", defaultModel: \"gemini-2.5-flash\" },\n anthropic: { baseUrl: \"https://api.anthropic.com/v1\", defaultModel: \"claude-haiku-4-5\" },\n cerebras: { baseUrl: \"https://api.cerebras.ai/v1\", defaultModel: \"qwen-3-235b-a22b-instruct-2507\" },\n sambanova: { baseUrl: \"https://api.sambanova.ai/v1\", defaultModel: \"Meta-Llama-3.3-70B-Instruct\" },\n fireworks: { baseUrl: \"https://api.fireworks.ai/inference/v1\", defaultModel: \"accounts/fireworks/models/llama-v3p3-70b-instruct\" },\n mistral: { baseUrl: \"https://api.mistral.ai/v1\", defaultModel: \"mistral-small-latest\" },\n openrouter: { baseUrl: \"https://openrouter.ai/api/v1\", defaultModel: \"deepseek/deepseek-chat\" },\n moonshot: { baseUrl: \"https://api.moonshot.ai/v1\", defaultModel: \"moonshot-v1-32k\" }\n};\n\nexport function isRetryableError(err: unknown): boolean {\n const msg = err instanceof Error ? err.message : String(err);\n return /\\b(429|rate.?limit|quota|exceed|5\\d\\d|timeout|ECONNRESET|fetch failed)\\b/i.test(msg);\n}\n","import type { BusinessConfig } from \"./types.js\";\n\n/**\n * Build the system prompt that anchors the bot to the business config.\n * Designed to minimize hallucination: only mention what's in the config,\n * defer to owner review for anything outside scope.\n */\nexport function buildSystemPrompt(business: BusinessConfig): string {\n const parts: string[] = [];\n\n parts.push(`You are an AI receptionist for ${business.name}.`);\n if (business.description) parts.push(business.description);\n parts.push(\"\");\n\n if (business.services && business.services.length > 0) {\n parts.push(\"Services offered (only these — do not invent others):\");\n for (const s of business.services) {\n const price = s.price ? ` — ${s.price}` : \"\";\n const notes = s.notes ? ` (${s.notes})` : \"\";\n parts.push(`- ${s.name}${price}${notes}`);\n }\n parts.push(\"\");\n }\n\n if (business.hours) {\n parts.push(`Hours: ${business.hours}`);\n }\n\n if (business.serviceArea && business.serviceArea.length > 0) {\n parts.push(`Service area: ${business.serviceArea.join(\", \")}.`);\n parts.push(\"If the caller is outside this area, say so and recommend owner review.\");\n }\n\n if (business.policies && business.policies.length > 0) {\n parts.push(\"\");\n parts.push(\"Known policies (use these exact answers when asked):\");\n for (const p of business.policies) {\n parts.push(`- ${p.topic}: ${p.answer}`);\n }\n }\n\n parts.push(\"\");\n parts.push(\"Rules:\");\n parts.push(\"- Reply in 1-2 short sentences, conversational tone.\");\n parts.push(\"- NEVER invent prices, availability, dispatch times, or appointment confirmations.\");\n parts.push(\"- For anything not covered in the setup above, say it needs owner review.\");\n parts.push('- 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.\"');\n parts.push('- If wrong number or asked to stop, say: \"Sorry about that. We won\\'t text again.\"');\n\n if (business.doNotPromise && business.doNotPromise.length > 0) {\n parts.push(\"\");\n parts.push(\"Never promise:\");\n for (const p of business.doNotPromise) parts.push(`- ${p}`);\n }\n\n if (business.customInstructions) {\n parts.push(\"\");\n parts.push(\"Additional instructions:\");\n parts.push(business.customInstructions);\n }\n\n if (business.language && business.language !== \"en\") {\n parts.push(\"\");\n parts.push(`Reply in ${business.language}.`);\n }\n\n return parts.join(\"\\n\");\n}\n","import type { GuardResult } from \"./types.js\";\n\n/**\n * Phrases that almost always indicate hallucination for SMB customer service:\n * inventing dispatch promises, fake confirmations, or appointment locks.\n */\nexport const FORBIDDEN_PHRASES: readonly string[] = [\n \"help is coming\",\n \"someone is on the way\",\n \"technician is on the way\",\n \"provider is on the way\",\n \"dispatching someone\",\n \"i've booked\",\n \"i have booked\",\n \"reservation confirmed\",\n \"your appointment is confirmed\",\n \"i've scheduled\",\n \"i have scheduled\",\n \"we've dispatched\",\n \"we have dispatched\",\n \"i can confirm\",\n \"i guarantee\",\n \"guaranteed delivery\",\n \"guaranteed arrival\",\n \"will arrive at\",\n \"arriving at\",\n \"i'll send\",\n \"i will send\"\n];\n\n/**\n * Check a reply against the built-in forbidden phrase list.\n * Returns ok=true when clean, ok=false with violations when not.\n */\nexport function checkForbiddenPhrases(reply: string): GuardResult {\n const lower = reply.toLowerCase();\n const violations: string[] = [];\n for (const phrase of FORBIDDEN_PHRASES) {\n if (lower.includes(phrase)) {\n violations.push(`Forbidden phrase: \"${phrase}\"`);\n }\n }\n return { ok: violations.length === 0, violations };\n}\n\n/**\n * Remove forbidden sentences from a reply (best-effort sentence drop).\n * If too much is removed, returns a safe fallback.\n */\nexport function stripForbidden(reply: string): string {\n const sentences = reply.split(/(?<=[.!?])\\s+/);\n const kept = sentences.filter((s) => {\n const lower = s.toLowerCase();\n return !FORBIDDEN_PHRASES.some((p) => lower.includes(p));\n });\n const trimmed = kept.join(\" \").trim();\n if (trimmed.length < 10) {\n return \"Thanks for reaching out — let me check with the owner and get back to you.\";\n }\n return trimmed;\n}\n","import type { BusinessConfig, Message } from \"../core/types.js\";\nimport { buildSystemPrompt } from \"../core/prompts.js\";\nimport { checkForbiddenPhrases, stripForbidden } from \"../core/guards.js\";\nimport type { Provider, ProviderConfig, ClientOptions, ChainStep, ChainEntry, AttemptInfo } from \"./types.js\";\nimport { parseChainSpec, isKnownProvider } from \"./types.js\";\nimport { PROVIDER_ENDPOINTS, isRetryableError } from \"./providers.js\";\n\nexport interface ChatBotInit {\n business: BusinessConfig;\n providers: ProviderConfig;\n options?: ClientOptions;\n}\n\nexport interface ReplyOptions {\n /** Conversation history (excluding the new user message). */\n history?: Message[];\n /** Override system prompt — advanced use only. */\n systemPrompt?: string;\n}\n\nexport interface ReplyResult {\n reply: string;\n /** Provider/model that produced the final reply (after fallback). */\n usedProvider: Provider;\n usedModel: string;\n /** Token usage if reported by the final provider. */\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n /** Guard violations the bot caught and stripped, if any. */\n guardWarnings: string[];\n /** Debug trace of every attempt in the chain. */\n attempts: AttemptInfo[];\n}\n\n/**\n * The main ChatBot entry. Holds business config + provider chain.\n *\n * @example\n * const bot = new ChatBot({\n * business: { name: \"Acme Plumbing\", services: [{ name: \"Sink leak\", price: \"$95\" }] },\n * providers: {\n * keys: { deepseek: \"sk-...\", groq: \"gsk-...\", openai: \"sk-...\" },\n * chain: [\"deepseek/deepseek-chat\", \"groq/llama-3.3-70b-versatile\", \"openai/gpt-4o-mini\"]\n * }\n * });\n * const { reply } = await bot.reply(\"My sink is leaking\");\n */\nexport class ChatBot {\n private readonly business: BusinessConfig;\n private readonly steps: ChainStep[];\n private readonly keys: Partial<Record<Provider, string>>;\n private readonly fetcher: typeof globalThis.fetch;\n private readonly timeoutMs: number;\n private readonly cachedSystemPrompt: string;\n\n constructor(init: ChatBotInit) {\n this.business = init.business;\n this.keys = init.providers.keys ?? {};\n this.steps = resolveChain(init.providers);\n this.fetcher = init.options?.fetch ?? globalThis.fetch.bind(globalThis);\n this.timeoutMs = init.options?.timeoutMs ?? 30_000;\n this.cachedSystemPrompt = buildSystemPrompt(this.business);\n }\n\n async reply(message: string, opts: ReplyOptions = {}): Promise<ReplyResult> {\n const systemPrompt = opts.systemPrompt ?? this.cachedSystemPrompt;\n const messages: Message[] = [\n { role: \"system\", content: systemPrompt },\n ...(opts.history ?? []),\n { role: \"user\", content: message }\n ];\n const attempts: AttemptInfo[] = [];\n let lastError: unknown;\n for (const step of this.steps) {\n const t0 = Date.now();\n try {\n const result = await this.callProvider(step, messages);\n attempts.push({ provider: step.provider, model: step.model, status: \"ok\", latencyMs: Date.now() - t0 });\n const guard = checkForbiddenPhrases(result.reply);\n const finalReply = guard.ok ? result.reply : stripForbidden(result.reply);\n return {\n reply: finalReply,\n usedProvider: step.provider,\n usedModel: step.model,\n ...(result.usage ? { usage: result.usage } : {}),\n guardWarnings: guard.violations,\n attempts\n };\n } catch (err) {\n lastError = err;\n const errMsg = err instanceof Error ? err.message : String(err);\n attempts.push({\n provider: step.provider,\n model: step.model,\n status: \"error\",\n error: errMsg,\n latencyMs: Date.now() - t0\n });\n if (!isRetryableError(err)) {\n throw new Error(`chatbotlite: ${step.provider}/${step.model} failed (non-retryable). ${errMsg}`);\n }\n }\n }\n const summary = attempts.map((a) => `${a.provider}/${a.model}:${a.error ?? \"ok\"}`).join(\" → \");\n throw new Error(`chatbotlite: all chain steps failed. Trace: ${summary}. Last error: ${lastError instanceof Error ? lastError.message : String(lastError)}`);\n }\n\n private async callProvider(step: ChainStep, messages: Message[]): Promise<{ reply: string; usage?: { prompt_tokens?: number; completion_tokens?: number } }> {\n const endpoint = PROVIDER_ENDPOINTS[step.provider];\n const key = this.keys[step.provider];\n if (!key) throw new Error(`Missing API key for provider: ${step.provider}`);\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n try {\n const res = await this.fetcher(`${endpoint.baseUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer ${key}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({\n model: step.model,\n messages,\n temperature: 0.3,\n max_tokens: 300\n }),\n signal: controller.signal\n });\n if (!res.ok) {\n const body = await res.text();\n throw new Error(`${res.status}: ${body.slice(0, 200)}`);\n }\n const data = (await res.json()) as {\n choices?: Array<{ message?: { content?: string; reasoning_content?: string } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n };\n const msg = data.choices?.[0]?.message;\n const reply = (msg?.content?.trim() || msg?.reasoning_content?.trim()) ?? \"\";\n if (!reply) throw new Error(\"empty reply from provider\");\n const result: { reply: string; usage?: { prompt_tokens?: number; completion_tokens?: number } } = { reply };\n if (data.usage) result.usage = data.usage;\n return result;\n } finally {\n clearTimeout(timer);\n }\n }\n}\n\n/**\n * Resolve `ProviderConfig` into an ordered list of concrete `{ provider, model }` steps.\n *\n * If `chain` is provided, parses each entry as `\"provider/model\"` or `\"provider\"` (uses default model).\n * If `chain` is omitted, builds chain from `keys` insertion order, each using provider's default model.\n */\nfunction resolveChain(providers: ProviderConfig): ChainStep[] {\n const keys = providers.keys ?? {};\n const explicit = providers.chain;\n if (explicit && explicit.length > 0) {\n return explicit.map((entry) => normalizeChainEntry(entry, keys));\n }\n const orderedProviders = Object.keys(keys).filter((k) => isKnownProvider(k) && keys[k as Provider]) as Provider[];\n if (orderedProviders.length === 0) {\n throw new Error(\"chatbotlite: at least one provider key is required.\");\n }\n return orderedProviders.map((provider) => ({\n provider,\n model: PROVIDER_ENDPOINTS[provider].defaultModel,\n spec: `${provider}/${PROVIDER_ENDPOINTS[provider].defaultModel}`\n }));\n}\n\nfunction normalizeChainEntry(entry: ChainEntry, keys: Partial<Record<Provider, string>>): ChainStep {\n let provider: Provider;\n let model: string;\n let spec: string;\n if (typeof entry === \"string\") {\n const parsed = parseChainSpec(entry);\n provider = parsed.provider;\n model = parsed.model ?? PROVIDER_ENDPOINTS[provider].defaultModel;\n spec = entry;\n } else {\n if (!isKnownProvider(entry.provider)) {\n throw new Error(`chatbotlite: unknown provider \"${entry.provider}\" in chain entry.`);\n }\n provider = entry.provider;\n model = entry.model ?? PROVIDER_ENDPOINTS[provider].defaultModel;\n spec = `${provider}/${model}`;\n }\n if (!keys[provider]) {\n throw new Error(`chatbotlite: chain step \"${spec}\" needs a key for provider \"${provider}\" but none was provided.`);\n }\n return { provider, model, spec };\n}\n"]}
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ // src/core/prompts.ts
4
+ function buildSystemPrompt(business) {
5
+ const parts = [];
6
+ parts.push(`You are an AI receptionist for ${business.name}.`);
7
+ if (business.description) parts.push(business.description);
8
+ parts.push("");
9
+ if (business.services && business.services.length > 0) {
10
+ parts.push("Services offered (only these \u2014 do not invent others):");
11
+ for (const s of business.services) {
12
+ const price = s.price ? ` \u2014 ${s.price}` : "";
13
+ const notes = s.notes ? ` (${s.notes})` : "";
14
+ parts.push(`- ${s.name}${price}${notes}`);
15
+ }
16
+ parts.push("");
17
+ }
18
+ if (business.hours) {
19
+ parts.push(`Hours: ${business.hours}`);
20
+ }
21
+ if (business.serviceArea && business.serviceArea.length > 0) {
22
+ parts.push(`Service area: ${business.serviceArea.join(", ")}.`);
23
+ parts.push("If the caller is outside this area, say so and recommend owner review.");
24
+ }
25
+ if (business.policies && business.policies.length > 0) {
26
+ parts.push("");
27
+ parts.push("Known policies (use these exact answers when asked):");
28
+ for (const p of business.policies) {
29
+ parts.push(`- ${p.topic}: ${p.answer}`);
30
+ }
31
+ }
32
+ parts.push("");
33
+ parts.push("Rules:");
34
+ parts.push("- Reply in 1-2 short sentences, conversational tone.");
35
+ parts.push("- NEVER invent prices, availability, dispatch times, or appointment confirmations.");
36
+ parts.push("- For anything not covered in the setup above, say it needs owner review.");
37
+ parts.push('- 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."');
38
+ parts.push(`- If wrong number or asked to stop, say: "Sorry about that. We won't text again."`);
39
+ if (business.doNotPromise && business.doNotPromise.length > 0) {
40
+ parts.push("");
41
+ parts.push("Never promise:");
42
+ for (const p of business.doNotPromise) parts.push(`- ${p}`);
43
+ }
44
+ if (business.customInstructions) {
45
+ parts.push("");
46
+ parts.push("Additional instructions:");
47
+ parts.push(business.customInstructions);
48
+ }
49
+ if (business.language && business.language !== "en") {
50
+ parts.push("");
51
+ parts.push(`Reply in ${business.language}.`);
52
+ }
53
+ return parts.join("\n");
54
+ }
55
+
56
+ // src/core/guards.ts
57
+ var FORBIDDEN_PHRASES = [
58
+ "help is coming",
59
+ "someone is on the way",
60
+ "technician is on the way",
61
+ "provider is on the way",
62
+ "dispatching someone",
63
+ "i've booked",
64
+ "i have booked",
65
+ "reservation confirmed",
66
+ "your appointment is confirmed",
67
+ "i've scheduled",
68
+ "i have scheduled",
69
+ "we've dispatched",
70
+ "we have dispatched",
71
+ "i can confirm",
72
+ "i guarantee",
73
+ "guaranteed delivery",
74
+ "guaranteed arrival",
75
+ "will arrive at",
76
+ "arriving at",
77
+ "i'll send",
78
+ "i will send"
79
+ ];
80
+ function checkForbiddenPhrases(reply) {
81
+ const lower = reply.toLowerCase();
82
+ const violations = [];
83
+ for (const phrase of FORBIDDEN_PHRASES) {
84
+ if (lower.includes(phrase)) {
85
+ violations.push(`Forbidden phrase: "${phrase}"`);
86
+ }
87
+ }
88
+ return { ok: violations.length === 0, violations };
89
+ }
90
+ function stripForbidden(reply) {
91
+ const sentences = reply.split(/(?<=[.!?])\s+/);
92
+ const kept = sentences.filter((s) => {
93
+ const lower = s.toLowerCase();
94
+ return !FORBIDDEN_PHRASES.some((p) => lower.includes(p));
95
+ });
96
+ const trimmed = kept.join(" ").trim();
97
+ if (trimmed.length < 10) {
98
+ return "Thanks for reaching out \u2014 let me check with the owner and get back to you.";
99
+ }
100
+ return trimmed;
101
+ }
102
+
103
+ exports.FORBIDDEN_PHRASES = FORBIDDEN_PHRASES;
104
+ exports.buildSystemPrompt = buildSystemPrompt;
105
+ exports.checkForbiddenPhrases = checkForbiddenPhrases;
106
+ exports.stripForbidden = stripForbidden;
107
+ //# sourceMappingURL=index.cjs.map
108
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/prompts.ts","../../src/core/guards.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { BusinessConfig } from \"./types.js\";\n\n/**\n * Build the system prompt that anchors the bot to the business config.\n * Designed to minimize hallucination: only mention what's in the config,\n * defer to owner review for anything outside scope.\n */\nexport function buildSystemPrompt(business: BusinessConfig): string {\n const parts: string[] = [];\n\n parts.push(`You are an AI receptionist for ${business.name}.`);\n if (business.description) parts.push(business.description);\n parts.push(\"\");\n\n if (business.services && business.services.length > 0) {\n parts.push(\"Services offered (only these — do not invent others):\");\n for (const s of business.services) {\n const price = s.price ? ` — ${s.price}` : \"\";\n const notes = s.notes ? ` (${s.notes})` : \"\";\n parts.push(`- ${s.name}${price}${notes}`);\n }\n parts.push(\"\");\n }\n\n if (business.hours) {\n parts.push(`Hours: ${business.hours}`);\n }\n\n if (business.serviceArea && business.serviceArea.length > 0) {\n parts.push(`Service area: ${business.serviceArea.join(\", \")}.`);\n parts.push(\"If the caller is outside this area, say so and recommend owner review.\");\n }\n\n if (business.policies && business.policies.length > 0) {\n parts.push(\"\");\n parts.push(\"Known policies (use these exact answers when asked):\");\n for (const p of business.policies) {\n parts.push(`- ${p.topic}: ${p.answer}`);\n }\n }\n\n parts.push(\"\");\n parts.push(\"Rules:\");\n parts.push(\"- Reply in 1-2 short sentences, conversational tone.\");\n parts.push(\"- NEVER invent prices, availability, dispatch times, or appointment confirmations.\");\n parts.push(\"- For anything not covered in the setup above, say it needs owner review.\");\n parts.push('- 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.\"');\n parts.push('- If wrong number or asked to stop, say: \"Sorry about that. We won\\'t text again.\"');\n\n if (business.doNotPromise && business.doNotPromise.length > 0) {\n parts.push(\"\");\n parts.push(\"Never promise:\");\n for (const p of business.doNotPromise) parts.push(`- ${p}`);\n }\n\n if (business.customInstructions) {\n parts.push(\"\");\n parts.push(\"Additional instructions:\");\n parts.push(business.customInstructions);\n }\n\n if (business.language && business.language !== \"en\") {\n parts.push(\"\");\n parts.push(`Reply in ${business.language}.`);\n }\n\n return parts.join(\"\\n\");\n}\n","import type { GuardResult } from \"./types.js\";\n\n/**\n * Phrases that almost always indicate hallucination for SMB customer service:\n * inventing dispatch promises, fake confirmations, or appointment locks.\n */\nexport const FORBIDDEN_PHRASES: readonly string[] = [\n \"help is coming\",\n \"someone is on the way\",\n \"technician is on the way\",\n \"provider is on the way\",\n \"dispatching someone\",\n \"i've booked\",\n \"i have booked\",\n \"reservation confirmed\",\n \"your appointment is confirmed\",\n \"i've scheduled\",\n \"i have scheduled\",\n \"we've dispatched\",\n \"we have dispatched\",\n \"i can confirm\",\n \"i guarantee\",\n \"guaranteed delivery\",\n \"guaranteed arrival\",\n \"will arrive at\",\n \"arriving at\",\n \"i'll send\",\n \"i will send\"\n];\n\n/**\n * Check a reply against the built-in forbidden phrase list.\n * Returns ok=true when clean, ok=false with violations when not.\n */\nexport function checkForbiddenPhrases(reply: string): GuardResult {\n const lower = reply.toLowerCase();\n const violations: string[] = [];\n for (const phrase of FORBIDDEN_PHRASES) {\n if (lower.includes(phrase)) {\n violations.push(`Forbidden phrase: \"${phrase}\"`);\n }\n }\n return { ok: violations.length === 0, violations };\n}\n\n/**\n * Remove forbidden sentences from a reply (best-effort sentence drop).\n * If too much is removed, returns a safe fallback.\n */\nexport function stripForbidden(reply: string): string {\n const sentences = reply.split(/(?<=[.!?])\\s+/);\n const kept = sentences.filter((s) => {\n const lower = s.toLowerCase();\n return !FORBIDDEN_PHRASES.some((p) => lower.includes(p));\n });\n const trimmed = kept.join(\" \").trim();\n if (trimmed.length < 10) {\n return \"Thanks for reaching out — let me check with the owner and get back to you.\";\n }\n return trimmed;\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import { B as BusinessConfig, G as GuardResult } from '../types-DYNx47by.cjs';
2
+ export { M as Message, P as PolicyItem, S as ServiceItem } from '../types-DYNx47by.cjs';
3
+
4
+ /**
5
+ * Build the system prompt that anchors the bot to the business config.
6
+ * Designed to minimize hallucination: only mention what's in the config,
7
+ * defer to owner review for anything outside scope.
8
+ */
9
+ declare function buildSystemPrompt(business: BusinessConfig): string;
10
+
11
+ /**
12
+ * Phrases that almost always indicate hallucination for SMB customer service:
13
+ * inventing dispatch promises, fake confirmations, or appointment locks.
14
+ */
15
+ declare const FORBIDDEN_PHRASES: readonly string[];
16
+ /**
17
+ * Check a reply against the built-in forbidden phrase list.
18
+ * Returns ok=true when clean, ok=false with violations when not.
19
+ */
20
+ declare function checkForbiddenPhrases(reply: string): GuardResult;
21
+ /**
22
+ * Remove forbidden sentences from a reply (best-effort sentence drop).
23
+ * If too much is removed, returns a safe fallback.
24
+ */
25
+ declare function stripForbidden(reply: string): string;
26
+
27
+ export { BusinessConfig, FORBIDDEN_PHRASES, GuardResult, buildSystemPrompt, checkForbiddenPhrases, stripForbidden };
@@ -0,0 +1,27 @@
1
+ import { B as BusinessConfig, G as GuardResult } from '../types-DYNx47by.js';
2
+ export { M as Message, P as PolicyItem, S as ServiceItem } from '../types-DYNx47by.js';
3
+
4
+ /**
5
+ * Build the system prompt that anchors the bot to the business config.
6
+ * Designed to minimize hallucination: only mention what's in the config,
7
+ * defer to owner review for anything outside scope.
8
+ */
9
+ declare function buildSystemPrompt(business: BusinessConfig): string;
10
+
11
+ /**
12
+ * Phrases that almost always indicate hallucination for SMB customer service:
13
+ * inventing dispatch promises, fake confirmations, or appointment locks.
14
+ */
15
+ declare const FORBIDDEN_PHRASES: readonly string[];
16
+ /**
17
+ * Check a reply against the built-in forbidden phrase list.
18
+ * Returns ok=true when clean, ok=false with violations when not.
19
+ */
20
+ declare function checkForbiddenPhrases(reply: string): GuardResult;
21
+ /**
22
+ * Remove forbidden sentences from a reply (best-effort sentence drop).
23
+ * If too much is removed, returns a safe fallback.
24
+ */
25
+ declare function stripForbidden(reply: string): string;
26
+
27
+ export { BusinessConfig, FORBIDDEN_PHRASES, GuardResult, buildSystemPrompt, checkForbiddenPhrases, stripForbidden };
@@ -0,0 +1,103 @@
1
+ // src/core/prompts.ts
2
+ function buildSystemPrompt(business) {
3
+ const parts = [];
4
+ parts.push(`You are an AI receptionist for ${business.name}.`);
5
+ if (business.description) parts.push(business.description);
6
+ parts.push("");
7
+ if (business.services && business.services.length > 0) {
8
+ parts.push("Services offered (only these \u2014 do not invent others):");
9
+ for (const s of business.services) {
10
+ const price = s.price ? ` \u2014 ${s.price}` : "";
11
+ const notes = s.notes ? ` (${s.notes})` : "";
12
+ parts.push(`- ${s.name}${price}${notes}`);
13
+ }
14
+ parts.push("");
15
+ }
16
+ if (business.hours) {
17
+ parts.push(`Hours: ${business.hours}`);
18
+ }
19
+ if (business.serviceArea && business.serviceArea.length > 0) {
20
+ parts.push(`Service area: ${business.serviceArea.join(", ")}.`);
21
+ parts.push("If the caller is outside this area, say so and recommend owner review.");
22
+ }
23
+ if (business.policies && business.policies.length > 0) {
24
+ parts.push("");
25
+ parts.push("Known policies (use these exact answers when asked):");
26
+ for (const p of business.policies) {
27
+ parts.push(`- ${p.topic}: ${p.answer}`);
28
+ }
29
+ }
30
+ parts.push("");
31
+ parts.push("Rules:");
32
+ parts.push("- Reply in 1-2 short sentences, conversational tone.");
33
+ parts.push("- NEVER invent prices, availability, dispatch times, or appointment confirmations.");
34
+ parts.push("- For anything not covered in the setup above, say it needs owner review.");
35
+ parts.push('- 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."');
36
+ parts.push(`- If wrong number or asked to stop, say: "Sorry about that. We won't text again."`);
37
+ if (business.doNotPromise && business.doNotPromise.length > 0) {
38
+ parts.push("");
39
+ parts.push("Never promise:");
40
+ for (const p of business.doNotPromise) parts.push(`- ${p}`);
41
+ }
42
+ if (business.customInstructions) {
43
+ parts.push("");
44
+ parts.push("Additional instructions:");
45
+ parts.push(business.customInstructions);
46
+ }
47
+ if (business.language && business.language !== "en") {
48
+ parts.push("");
49
+ parts.push(`Reply in ${business.language}.`);
50
+ }
51
+ return parts.join("\n");
52
+ }
53
+
54
+ // src/core/guards.ts
55
+ var FORBIDDEN_PHRASES = [
56
+ "help is coming",
57
+ "someone is on the way",
58
+ "technician is on the way",
59
+ "provider is on the way",
60
+ "dispatching someone",
61
+ "i've booked",
62
+ "i have booked",
63
+ "reservation confirmed",
64
+ "your appointment is confirmed",
65
+ "i've scheduled",
66
+ "i have scheduled",
67
+ "we've dispatched",
68
+ "we have dispatched",
69
+ "i can confirm",
70
+ "i guarantee",
71
+ "guaranteed delivery",
72
+ "guaranteed arrival",
73
+ "will arrive at",
74
+ "arriving at",
75
+ "i'll send",
76
+ "i will send"
77
+ ];
78
+ function checkForbiddenPhrases(reply) {
79
+ const lower = reply.toLowerCase();
80
+ const violations = [];
81
+ for (const phrase of FORBIDDEN_PHRASES) {
82
+ if (lower.includes(phrase)) {
83
+ violations.push(`Forbidden phrase: "${phrase}"`);
84
+ }
85
+ }
86
+ return { ok: violations.length === 0, violations };
87
+ }
88
+ function stripForbidden(reply) {
89
+ const sentences = reply.split(/(?<=[.!?])\s+/);
90
+ const kept = sentences.filter((s) => {
91
+ const lower = s.toLowerCase();
92
+ return !FORBIDDEN_PHRASES.some((p) => lower.includes(p));
93
+ });
94
+ const trimmed = kept.join(" ").trim();
95
+ if (trimmed.length < 10) {
96
+ return "Thanks for reaching out \u2014 let me check with the owner and get back to you.";
97
+ }
98
+ return trimmed;
99
+ }
100
+
101
+ export { FORBIDDEN_PHRASES, buildSystemPrompt, checkForbiddenPhrases, stripForbidden };
102
+ //# sourceMappingURL=index.js.map
103
+ //# sourceMappingURL=index.js.map