@pixygon/chatbot-server 0.1.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 +107 -0
- package/dist/index.d.ts +186 -0
- package/dist/index.js +1169 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @pixygon/chatbot-server
|
|
2
|
+
|
|
3
|
+
RAG chatbot + analytics for Node + Mongoose + Express hosts.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @pixygon/chatbot-server
|
|
9
|
+
# Peer deps the host already has:
|
|
10
|
+
# - express ≥5
|
|
11
|
+
# - mongoose ≥8
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createChatbot } from "@pixygon/chatbot-server";
|
|
18
|
+
import mongoose from "mongoose";
|
|
19
|
+
import { Tenant } from "./models/Tenant.js";
|
|
20
|
+
import { withTenantScope } from "./middleware/requestContext.js";
|
|
21
|
+
import { tenantScopedPlugin } from "./models/_plugins/tenantScoped.js";
|
|
22
|
+
|
|
23
|
+
const chatbot = createChatbot({
|
|
24
|
+
mongoose,
|
|
25
|
+
tenantParamName: "tenantId", // also accepts "companyId"
|
|
26
|
+
tenantRefName: "Tenant",
|
|
27
|
+
ai: {
|
|
28
|
+
pixygonApiKey: process.env.PIXYGON_API_KEY!,
|
|
29
|
+
openaiApiKey: process.env.OPENAI_API_KEY!,
|
|
30
|
+
},
|
|
31
|
+
plugins: [(schema, label) => schema.plugin(tenantScopedPlugin, { label })],
|
|
32
|
+
hooks: {
|
|
33
|
+
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(),
|
|
37
|
+
getCostCap: async (id) =>
|
|
38
|
+
(await Tenant.findById(id).select("chatCostCapUsdMonthly").lean())?.chatCostCapUsdMonthly ?? null,
|
|
39
|
+
withTenantScope,
|
|
40
|
+
systemPromptBuilder: (tenantName, contextBlocks) => {
|
|
41
|
+
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}`;
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Mount under whatever path the host wants.
|
|
50
|
+
app.use("/v1/tenants/:tenantId", verifyToken, tenantAccess, chatbot.routes.private);
|
|
51
|
+
app.use("/v1/public/chat", chatbot.routes.public);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## What you get
|
|
55
|
+
|
|
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).
|
|
64
|
+
|
|
65
|
+
## API surface
|
|
66
|
+
|
|
67
|
+
Private routes (mounted under `/v1/tenants/:tenantId`):
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
GET /knowledge
|
|
71
|
+
POST /knowledge
|
|
72
|
+
GET /knowledge/:documentId
|
|
73
|
+
PUT /knowledge/:documentId
|
|
74
|
+
DELETE /knowledge/:documentId
|
|
75
|
+
|
|
76
|
+
POST /chat
|
|
77
|
+
GET /chat/:sessionId
|
|
78
|
+
GET /conversations?limit=50
|
|
79
|
+
POST /chat/rate
|
|
80
|
+
|
|
81
|
+
GET /chat-analytics/overview
|
|
82
|
+
GET /chat-analytics/top-questions?limit=20
|
|
83
|
+
GET /chat-analytics/keywords?limit=30
|
|
84
|
+
GET /chat-analytics/cost-timeseries?days=30
|
|
85
|
+
GET /chat-analytics/knowledge-gaps?limit=15
|
|
86
|
+
GET /chat-analytics/document-usage
|
|
87
|
+
GET /chat-analytics/conversations?normalized=&limit=50
|
|
88
|
+
GET /chat-analytics/semantic-clusters?limit=15
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Public routes (mounted under `/v1/public/chat`):
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
POST /:tenantSlug
|
|
95
|
+
GET /:tenantSlug/:sessionId
|
|
96
|
+
POST /:tenantSlug/rate
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
All public routes IP rate-limited (20/min/IP by default; override via
|
|
100
|
+
`createPublicRouter(chatbot, { rateLimitConfig: { windowMs, max } })`).
|
|
101
|
+
|
|
102
|
+
## Cost-cap enforcement
|
|
103
|
+
|
|
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.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Request, Response, NextFunction, Router } from 'express';
|
|
2
|
+
import { Model, Schema, Mongoose } from 'mongoose';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AI gateway client.
|
|
6
|
+
*
|
|
7
|
+
* - chat() → Pixygon AI gateway (POST /ai/api with FormData type=text).
|
|
8
|
+
* PixygonServer's apiGenerate returns { text, url, role, model, version }.
|
|
9
|
+
* - embed() → OpenAI /embeddings directly. PixygonServer doesn't route
|
|
10
|
+
* embeddings; they're commoditized + cheap so we hit OpenAI directly.
|
|
11
|
+
*/
|
|
12
|
+
interface AiConfig {
|
|
13
|
+
pixygonApiUrl?: string;
|
|
14
|
+
pixygonApiKey: string;
|
|
15
|
+
openaiApiUrl?: string;
|
|
16
|
+
openaiApiKey: string;
|
|
17
|
+
chatInputUsdPer1K?: number;
|
|
18
|
+
chatOutputUsdPer1K?: number;
|
|
19
|
+
embedUsdPer1K?: number;
|
|
20
|
+
}
|
|
21
|
+
interface ChatMessage {
|
|
22
|
+
role: "system" | "user" | "assistant";
|
|
23
|
+
content: string;
|
|
24
|
+
}
|
|
25
|
+
interface ChatResult {
|
|
26
|
+
content: string;
|
|
27
|
+
model: string;
|
|
28
|
+
tokensInput: number;
|
|
29
|
+
tokensOutput: number;
|
|
30
|
+
costUsd: number;
|
|
31
|
+
}
|
|
32
|
+
interface EmbedResult {
|
|
33
|
+
embedding: number[];
|
|
34
|
+
tokens: number;
|
|
35
|
+
costUsd: number;
|
|
36
|
+
dimensions: number;
|
|
37
|
+
}
|
|
38
|
+
interface AiClient {
|
|
39
|
+
chat(args: {
|
|
40
|
+
messages: ChatMessage[];
|
|
41
|
+
system?: string;
|
|
42
|
+
model?: string;
|
|
43
|
+
version?: string;
|
|
44
|
+
}): Promise<ChatResult>;
|
|
45
|
+
embed(text: string, opts?: {
|
|
46
|
+
model?: string;
|
|
47
|
+
}): Promise<EmbedResult>;
|
|
48
|
+
}
|
|
49
|
+
declare function cosineSimilarity(a: number[], b: number[]): number;
|
|
50
|
+
|
|
51
|
+
interface ModelFactoryOptions {
|
|
52
|
+
/** Field name on chatbot models that points at the tenant. */
|
|
53
|
+
tenantField: string;
|
|
54
|
+
/** Mongoose model name for the tenant document (used as Schema.ref). */
|
|
55
|
+
tenantRefName: string;
|
|
56
|
+
/**
|
|
57
|
+
* Optional plugins applied to each schema before model() — usually the
|
|
58
|
+
* host's `tenantScopedPlugin` and `auditLogPlugin`. The factory passes the
|
|
59
|
+
* compiled label so the plugin can scope logs.
|
|
60
|
+
*/
|
|
61
|
+
plugins?: Array<(schema: Schema, label: string) => void>;
|
|
62
|
+
}
|
|
63
|
+
interface ChatbotModels {
|
|
64
|
+
KnowledgeDocument: Model<any>;
|
|
65
|
+
KnowledgeChunk: Model<any>;
|
|
66
|
+
ChatConversation: Model<any>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hooks the host implements to plug the package into its environment.
|
|
71
|
+
* Every per-app concern (tenant lookups, ALS, branding) flows through here.
|
|
72
|
+
*/
|
|
73
|
+
interface ChatbotHooks {
|
|
74
|
+
/** Resolve the tenant's display name for system-prompt injection. */
|
|
75
|
+
getTenantName: (tenantId: any) => Promise<string | null | undefined>;
|
|
76
|
+
/** Anonymous public chat looks tenants up by slug. */
|
|
77
|
+
getTenantBySlug: (slug: string) => Promise<{
|
|
78
|
+
_id: any;
|
|
79
|
+
name: string;
|
|
80
|
+
slug: string;
|
|
81
|
+
} | null>;
|
|
82
|
+
/** Optional monthly cost cap. Return null/undefined for no cap. */
|
|
83
|
+
getCostCap?: (tenantId: any) => Promise<number | null | undefined>;
|
|
84
|
+
/** AsyncLocalStorage tenant scope for handlers that run outside HTTP context. */
|
|
85
|
+
withTenantScope?: <T>(tenantId: any, fn: () => Promise<T> | T) => Promise<T> | T;
|
|
86
|
+
/**
|
|
87
|
+
* Build the LLM system prompt. Receives the tenant name + the formatted
|
|
88
|
+
* source-citation blocks. Host owns the wording.
|
|
89
|
+
*/
|
|
90
|
+
systemPromptBuilder: (tenantName: string, contextBlocks: string[]) => string;
|
|
91
|
+
}
|
|
92
|
+
interface ChatbotConfig {
|
|
93
|
+
mongoose: Mongoose;
|
|
94
|
+
/** URL param name carrying the tenant id. e.g. "tenantId" or "companyId". */
|
|
95
|
+
tenantParamName: string;
|
|
96
|
+
/** Field name on chatbot docs that references the tenant. Usually same as param. */
|
|
97
|
+
tenantField?: string;
|
|
98
|
+
/** Mongoose ref string for the tenant model. */
|
|
99
|
+
tenantRefName: string;
|
|
100
|
+
/** AI gateway credentials + rate knobs. */
|
|
101
|
+
ai: AiConfig;
|
|
102
|
+
hooks: ChatbotHooks;
|
|
103
|
+
/**
|
|
104
|
+
* Mongoose plugins to attach to every schema before model registration.
|
|
105
|
+
* Pass the host's tenant-scoped plugin + audit-log plugin here.
|
|
106
|
+
*/
|
|
107
|
+
plugins?: Array<(schema: any, label: string) => void>;
|
|
108
|
+
}
|
|
109
|
+
interface Chatbot {
|
|
110
|
+
models: ChatbotModels;
|
|
111
|
+
ai: AiClient;
|
|
112
|
+
config: Required<Pick<ChatbotConfig, "tenantParamName" | "tenantField" | "tenantRefName">> & ChatbotConfig;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface Citation {
|
|
116
|
+
chunkId: any;
|
|
117
|
+
documentId: any;
|
|
118
|
+
documentTitle: string;
|
|
119
|
+
snippet: string;
|
|
120
|
+
similarity: number;
|
|
121
|
+
}
|
|
122
|
+
interface RespondArgs {
|
|
123
|
+
tenantId: any;
|
|
124
|
+
sessionId: string;
|
|
125
|
+
userId?: any;
|
|
126
|
+
message: string;
|
|
127
|
+
}
|
|
128
|
+
interface RespondResult {
|
|
129
|
+
conversationId: any;
|
|
130
|
+
assistantContent: string;
|
|
131
|
+
citations: Citation[];
|
|
132
|
+
costUsd: number;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Factory for the RAG entry points. Bound to a Chatbot — captures models,
|
|
136
|
+
* ai client, hooks. The returned object is what the host hands to controllers.
|
|
137
|
+
*/
|
|
138
|
+
declare function createRag(chatbot: Chatbot): {
|
|
139
|
+
respond: ({ tenantId, sessionId, userId, message }: RespondArgs) => Promise<RespondResult>;
|
|
140
|
+
processDocument: (documentId: any) => Promise<void>;
|
|
141
|
+
currentMonthCost: (tenantId: any, now?: Date) => Promise<number>;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
interface AnalyticsService {
|
|
145
|
+
overview(tenantId: any): Promise<any>;
|
|
146
|
+
topQuestions(tenantId: any, limit?: number): Promise<any[]>;
|
|
147
|
+
keywordFrequency(tenantId: any, limit?: number): Promise<any[]>;
|
|
148
|
+
costTimeseries(tenantId: any, days?: number): Promise<any[]>;
|
|
149
|
+
knowledgeGaps(tenantId: any, limit?: number): Promise<any[]>;
|
|
150
|
+
documentUsage(tenantId: any): Promise<any[]>;
|
|
151
|
+
conversationsForQuestion(tenantId: any, normalized: string, limit?: number): Promise<any[]>;
|
|
152
|
+
semanticClusters(tenantId: any, limit?: number): Promise<any[]>;
|
|
153
|
+
}
|
|
154
|
+
declare function createAnalytics(chatbot: Chatbot): AnalyticsService;
|
|
155
|
+
|
|
156
|
+
interface Chunk {
|
|
157
|
+
text: string;
|
|
158
|
+
position: number;
|
|
159
|
+
}
|
|
160
|
+
declare function chunkText(input: string): Chunk[];
|
|
161
|
+
|
|
162
|
+
declare function rateLimit({ windowMs, max, scope, }: {
|
|
163
|
+
windowMs: number;
|
|
164
|
+
max: number;
|
|
165
|
+
scope: string;
|
|
166
|
+
}): (req: Request, res: Response, next: NextFunction) => void;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Wire up the chatbot for a host. Returns models, services, and Express
|
|
170
|
+
* router factories. Idempotent — calling twice returns the same models.
|
|
171
|
+
*
|
|
172
|
+
* const chatbot = createChatbot({ mongoose, tenantParamName: "tenantId", ... });
|
|
173
|
+
*
|
|
174
|
+
* app.use("/v1/tenants/:tenantId", verifyToken, tenantAccess, chatbot.routes.private);
|
|
175
|
+
* app.use("/v1/public/chat", chatbot.routes.public);
|
|
176
|
+
*/
|
|
177
|
+
declare function createChatbot(cfg: ChatbotConfig): Chatbot & {
|
|
178
|
+
rag: ReturnType<typeof createRag>;
|
|
179
|
+
analytics: ReturnType<typeof createAnalytics>;
|
|
180
|
+
routes: {
|
|
181
|
+
private: Router;
|
|
182
|
+
public: Router;
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export { type AiClient, type AiConfig, type AnalyticsService, type ChatMessage, type ChatResult, type Chatbot, type ChatbotConfig, type ChatbotHooks, type ChatbotModels, type Citation, type EmbedResult, type ModelFactoryOptions, type RespondArgs, type RespondResult, chunkText, cosineSimilarity, createChatbot, rateLimit };
|