@pixygon/chatbot-server 0.1.0 → 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.
Files changed (2) hide show
  1. package/README.md +186 -42
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,82 +1,151 @@
1
1
  # @pixygon/chatbot-server
2
2
 
3
- RAG chatbot + analytics for Node + Mongoose + Express hosts.
3
+ Drop-in RAG chatbot + knowledge base + analytics for **Node 22 + Express 5 + Mongoose 8**.
4
+ Multi-tenant by construction. Public anonymous surface included.
5
+
6
+ ```
7
+ +--------------------+ +-------------------------+
8
+ | Host Express app |----->| chatbot.routes.private |
9
+ | (your auth here) | | chatbot.routes.public |
10
+ +--------------------+ +-------------------------+
11
+ | |
12
+ v v
13
+ Host's mongoose chatbot.{rag, analytics}
14
+ (models register here) services
15
+ |
16
+ v
17
+ KnowledgeDocument · KnowledgeChunk · ChatConversation
18
+ ```
19
+
20
+ ---
21
+
22
+ ## What it does
23
+
24
+ - **Knowledge base.** Operators paste docs (text/url/file). The service chunks
25
+ to ~2 kB paragraphs, embeds via OpenAI `text-embedding-3-small` (1536-dim),
26
+ stores chunks per tenant.
27
+ - **Chat (RAG).** User asks a question → cosine-sim top-K=5 chunks → renders a
28
+ system prompt → forwards to the Pixygon AI gateway (configurable model) →
29
+ returns text + cited sources.
30
+ - **Analytics.** 8 endpoints: overview KPIs, top questions, keyword frequency,
31
+ cost timeseries, knowledge gaps, document usage, conversation drill-down,
32
+ semantic clusters. All tenant-scoped.
33
+ - **Public surface.** Anonymous `/public/chat/:tenantSlug` for embedding on
34
+ marketing/help-center sites. IP rate-limited.
35
+ - **Cost cap.** Per-tenant monthly USD ceiling. New messages refused with a
36
+ 503 `CHAT_BUDGET_EXCEEDED` once exceeded.
37
+
38
+ ---
4
39
 
5
40
  ## Install
6
41
 
7
42
  ```bash
8
43
  npm install @pixygon/chatbot-server
9
- # Peer deps the host already has:
10
- # - express ≥5
11
- # - mongoose ≥8
12
44
  ```
13
45
 
46
+ Peer expectations (host already has these):
47
+
48
+ - `express` ≥ 5
49
+ - `mongoose` ≥ 8
50
+ - Node ≥ 22
51
+
52
+ Env vars consumed by `createChatbot`:
53
+
54
+ | Var | Required | What it's for |
55
+ |---|---|---|
56
+ | `PIXYGON_API_KEY` | yes | Chat completions via the Pixygon AI gateway |
57
+ | `OPENAI_API_KEY` | yes | Embeddings (`text-embedding-3-small`) |
58
+ | `PIXYGON_API_URL` | no | Override gateway base; default `https://api.pixygon.com/v1` |
59
+ | `PIXYGON_CHAT_INPUT_USD_PER_1K` | no | Cost-cap input pricing |
60
+ | `PIXYGON_CHAT_OUTPUT_USD_PER_1K` | no | Cost-cap output pricing |
61
+ | `OPENAI_EMBED_USD_PER_1K` | no | Cost-cap embeddings pricing |
62
+
63
+ ---
64
+
14
65
  ## Usage
15
66
 
16
67
  ```ts
17
- import { createChatbot } from "@pixygon/chatbot-server";
18
68
  import mongoose from "mongoose";
69
+ import { createChatbot } from "@pixygon/chatbot-server";
19
70
  import { Tenant } from "./models/Tenant.js";
20
71
  import { withTenantScope } from "./middleware/requestContext.js";
21
72
  import { tenantScopedPlugin } from "./models/_plugins/tenantScoped.js";
73
+ import { auditLogPlugin } from "./models/_plugins/auditLog.js";
22
74
 
23
- const chatbot = createChatbot({
75
+ export const chatbot = createChatbot({
24
76
  mongoose,
25
- tenantParamName: "tenantId", // also accepts "companyId"
26
- tenantRefName: "Tenant",
77
+ tenantParamName: "tenantId", // path param: /tenants/:tenantId/...
78
+ tenantField: "tenantId", // the field name on documents
79
+ tenantRefName: "Tenant", // mongoose ref name for population
80
+
27
81
  ai: {
28
82
  pixygonApiKey: process.env.PIXYGON_API_KEY!,
29
83
  openaiApiKey: process.env.OPENAI_API_KEY!,
30
84
  },
31
- plugins: [(schema, label) => schema.plugin(tenantScopedPlugin, { label })],
85
+
86
+ // Optional host plugins applied to every chatbot model.
87
+ // Use this for tenant-scoped query enforcement, audit log, etc.
88
+ plugins: [
89
+ (schema, label) =>
90
+ schema.plugin(tenantScopedPlugin, { tenantField: "tenantId", label }),
91
+ (schema, label) =>
92
+ schema.plugin(auditLogPlugin, { entityType: label }),
93
+ ],
94
+
32
95
  hooks: {
33
96
  getTenantName: async (id) =>
34
- (await Tenant.findById(id).select("name").lean())?.name,
35
- getTenantBySlug: async (slug) =>
36
- Tenant.findOne({ slug, status: "active" }).select("_id name slug").lean(),
97
+ (await Tenant.findById(id).select("name").lean())?.name ?? null,
98
+
99
+ getTenantBySlug: async (slug) => {
100
+ const t = await Tenant.findOne({ slug, status: "active" })
101
+ .select("_id name slug").lean();
102
+ return t ? { _id: t._id, name: t.name, slug: t.slug } : null;
103
+ },
104
+
37
105
  getCostCap: async (id) =>
38
- (await Tenant.findById(id).select("chatCostCapUsdMonthly").lean())?.chatCostCapUsdMonthly ?? null,
39
- withTenantScope,
106
+ (await Tenant.findById(id).select("chatCostCapUsdMonthly").lean())
107
+ ?.chatCostCapUsdMonthly ?? null,
108
+
109
+ withTenantScope: (tenantId, fn) => withTenantScope(tenantId, fn),
110
+
40
111
  systemPromptBuilder: (tenantName, contextBlocks) => {
41
112
  const sources = contextBlocks.length === 0
42
- ? "(no relevant sources)"
43
- : contextBlocks.map((b, i) => `[Source ${i + 1}]\n${b}`).join("\n\n");
44
- return `You are the ${tenantName} assistant. Use ONLY the sources below.\n\n${sources}`;
113
+ ? "(no relevant sources matched)"
114
+ : contextBlocks.map((c, i) => `[Source ${i + 1}]\n${c}`).join("\n\n");
115
+ return `You are the ${tenantName} assistant. Use ONLY the sources below as factual basis. If unsure, say so.
116
+
117
+ === Sources ===
118
+ ${sources}
119
+ === End ===`;
45
120
  },
46
121
  },
47
122
  });
48
123
 
49
- // Mount under whatever path the host wants.
124
+ // Mount under whatever shape the host uses.
50
125
  app.use("/v1/tenants/:tenantId", verifyToken, tenantAccess, chatbot.routes.private);
51
126
  app.use("/v1/public/chat", chatbot.routes.public);
52
127
  ```
53
128
 
54
- ## What you get
129
+ A host that uses `companyId` instead of `tenantId` swaps both `tenantParamName`
130
+ and `tenantField` to `"companyId"`. The package adapts.
55
131
 
56
- - **Models** registered on the host's connection: `KnowledgeDocument`,
57
- `KnowledgeChunk`, `ChatConversation`.
58
- - **Services**: `chatbot.rag.respond({tenantId, sessionId, message})`,
59
- `chatbot.rag.processDocument(docId)`, `chatbot.rag.currentMonthCost(tenantId)`,
60
- `chatbot.analytics.*` (8 methods).
61
- - **Routers**: `chatbot.routes.private` (auth required, mounted under a
62
- tenant-scoped path) and `chatbot.routes.public` (anonymous, IP rate-limited,
63
- slug-based lookup).
132
+ ---
64
133
 
65
134
  ## API surface
66
135
 
67
- Private routes (mounted under `/v1/tenants/:tenantId`):
136
+ **Private routes** (mounted under `/v1/<tenants>/:<id>`):
68
137
 
69
138
  ```
70
139
  GET /knowledge
71
- POST /knowledge
140
+ POST /knowledge { title, sourceType, sourceText? | url? }
72
141
  GET /knowledge/:documentId
73
- PUT /knowledge/:documentId
142
+ PUT /knowledge/:documentId { title?, sourceText? }
74
143
  DELETE /knowledge/:documentId
75
144
 
76
- POST /chat
145
+ POST /chat { sessionId, message }
77
146
  GET /chat/:sessionId
78
147
  GET /conversations?limit=50
79
- POST /chat/rate
148
+ POST /chat/rate { sessionId, turnIndex, rating } // 1 | -1
80
149
 
81
150
  GET /chat-analytics/overview
82
151
  GET /chat-analytics/top-questions?limit=20
@@ -84,24 +153,99 @@ GET /chat-analytics/keywords?limit=30
84
153
  GET /chat-analytics/cost-timeseries?days=30
85
154
  GET /chat-analytics/knowledge-gaps?limit=15
86
155
  GET /chat-analytics/document-usage
87
- GET /chat-analytics/conversations?normalized=&limit=50
156
+ GET /chat-analytics/conversations?normalized=<q>&limit=50
88
157
  GET /chat-analytics/semantic-clusters?limit=15
89
158
  ```
90
159
 
91
- Public routes (mounted under `/v1/public/chat`):
160
+ **Public routes** (mounted under `/v1/public/chat`):
92
161
 
93
162
  ```
94
- POST /:tenantSlug
163
+ POST /:tenantSlug { sessionId, message }
95
164
  GET /:tenantSlug/:sessionId
96
- POST /:tenantSlug/rate
165
+ POST /:tenantSlug/rate { sessionId, turnIndex, rating }
97
166
  ```
98
167
 
99
- All public routes IP rate-limited (20/min/IP by default; override via
100
- `createPublicRouter(chatbot, { rateLimitConfig: { windowMs, max } })`).
168
+ Default IP rate limit: 20 req/min/IP. Override via
169
+ `createPublicRouter(chatbot, { rateLimitConfig: { windowMs, max } })` if you
170
+ need to wire a custom limiter — or use the exported `rateLimit` helper.
171
+
172
+ ---
173
+
174
+ ## Hooks reference
175
+
176
+ | Hook | Required | Purpose |
177
+ |---|---|---|
178
+ | `getTenantName(id)` | yes | System prompt — "You are the X assistant" |
179
+ | `getTenantBySlug(slug)` | yes (for public surface) | Resolves slug to tenant for anonymous chat |
180
+ | `getCostCap(id)` | yes | Returns monthly USD cap; `null` = no cap |
181
+ | `withTenantScope(id, fn)` | yes | AsyncLocalStorage wrapper for tenant context |
182
+ | `systemPromptBuilder(name, blocks)` | yes | Builds the LLM system prompt with citations |
183
+
184
+ `plugins` is an array of `(schema, label) => void` — applied to every chatbot
185
+ model schema. Use this to attach your host's tenant-scope enforcement, audit
186
+ log, soft-delete, or whatever else every model needs.
187
+
188
+ ---
189
+
190
+ ## Direct service calls
191
+
192
+ The router is convenient but you can call the services directly if needed:
193
+
194
+ ```ts
195
+ const { text, citations, usage } = await chatbot.rag.respond({
196
+ tenantId, sessionId, message: "How do I export SAF-T?",
197
+ });
198
+
199
+ await chatbot.rag.processDocument(documentId); // background embedding
200
+ const spend = await chatbot.rag.currentMonthCost(tenantId);
201
+
202
+ const kpis = await chatbot.analytics.overview(tenantId);
203
+ const gaps = await chatbot.analytics.knowledgeGaps(tenantId, 10);
204
+ const clusters = await chatbot.analytics.semanticClusters(tenantId, 15);
205
+ ```
206
+
207
+ ---
101
208
 
102
209
  ## Cost-cap enforcement
103
210
 
104
- When `hooks.getCostCap` returns a number > 0, `rag.respond()` pre-flights
105
- the current-month cost. Over the cap throws a 503 with code
106
- `CHAT_BUDGET_EXCEEDED`. Host's error handler should map application errors
107
- with a numeric `.status` to the response.
211
+ When `hooks.getCostCap(tenantId)` returns a number > 0, `rag.respond()`
212
+ pre-flights the current month's spend. Over the cap throws
213
+ `{ status: 503, code: "CHAT_BUDGET_EXCEEDED" }`. The host's error handler
214
+ should map application errors with a numeric `.status` to the response.
215
+
216
+ ```ts
217
+ // somewhere in your error middleware
218
+ app.use((err, req, res, next) => {
219
+ if (err.status) return res.status(err.status).json({ error: err.code || err.message });
220
+ next(err);
221
+ });
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Exports
227
+
228
+ ```ts
229
+ import {
230
+ createChatbot, // main factory
231
+ chunkText, // 2 kB paragraph chunker
232
+ cosineSimilarity, // dot-product over unit vectors
233
+ rateLimit, // express middleware factory
234
+ type ChatbotConfig,
235
+ type ChatbotHooks,
236
+ type Chatbot,
237
+ type ChatMessage,
238
+ type Citation,
239
+ type RespondArgs,
240
+ type RespondResult,
241
+ type AnalyticsService,
242
+ } from "@pixygon/chatbot-server";
243
+ ```
244
+
245
+ ---
246
+
247
+ ## Companion package
248
+
249
+ `@pixygon/chatbot-react` ships matching MUI + RTK Query pages
250
+ (KnowledgePage / ChatPage / ChatAnalyticsPage / EmbedChatPage /
251
+ ChatbotSettings). See its README for the React/Vite wire-up.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixygon/chatbot-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "RAG chatbot + analytics for Node + Mongoose + Express hosts.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",