@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 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.
@@ -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 };