PPM

@pixygon/chatbot-server

RAG chatbot + analytics for Node + Mongoose + Express hosts.

Install

# .npmrc
@pixygon:registry=https://ppm.pixygon.io/

npm install @pixygon/chatbot-server@0.7.1

Versions

VersionPublishedIntegrity
0.7.1 latest2026-09-15sha512-GyVCN7GlNY40vzL…
0.7.0 2026-07-21sha512-A7/ukcyMUvFHiQ2…
0.6.0 2026-06-26sha512-22rV47pr0As/6i0…
0.5.0 2026-06-17sha512-m8OsS9/vdG5siuc…
0.4.0 2026-06-17sha512-ejd6laOrINFmg1k…
0.3.1 2026-06-13sha512-81W8FXgykQ/GZVL…
0.3.0 2026-06-13sha512-/YC7WHf38xMplmC…
0.2.0 2026-06-11sha512-edtp/FdHuq5KENY…
0.1.1 2026-06-11sha512-qOeWc67G7x0KwP4…
0.1.0 2026-06-11sha512-Ux5NiQ9BiQ3sgG0…

@pixygon/chatbot-server

Drop-in RAG chatbot + knowledge base + analytics for Node 22 + Express 5 + Mongoose 8. Multi-tenant by construction. Public anonymous surface included.

+--------------------+      +-------------------------+
|  Host Express app  |----->| chatbot.routes.private  |
|  (your auth here)  |      | chatbot.routes.public   |
+--------------------+      +-------------------------+
        |                              |
        v                              v
   Host's mongoose          chatbot.{rag, analytics}
   (models register here)    services
        |
        v
   KnowledgeDocument · KnowledgeChunk · ChatConversation

What it does


Install

npm install @pixygon/chatbot-server

Peer expectations (host already has these):

Env vars consumed by createChatbot:

VarRequiredWhat it's for
PIXYGON_API_KEYyesAll AI calls — chat and embeddings — route through the Pixygon AI gateway (POST /v1/ai/api). PixygonServer handles upstream provider selection, billing, and the 20 % platform markup.
PIXYGON_API_URLnoOverride gateway base; default https://api.pixygon.com/v1
PIXYGON_CHAT_INPUT_USD_PER_1KnoLocal cost estimate used by the chatbot's cost-cap pre-flight + per-conversation analytics. Not the same as the platform's billing rates.
PIXYGON_CHAT_OUTPUT_USD_PER_1KnoSame — for chat output
OPENAI_EMBED_USD_PER_1KnoSame — for embeddings

There is no direct-provider bypass — every call flows through Pixygon so per-key margin tracking stays accurate. A single PIXYGON_API_KEY is all you need.


Usage

import mongoose from "mongoose";
import { createChatbot } from "@pixygon/chatbot-server";
import { Tenant } from "./models/Tenant.js";
import { withTenantScope } from "./middleware/requestContext.js";
import { tenantScopedPlugin } from "./models/_plugins/tenantScoped.js";
import { auditLogPlugin } from "./models/_plugins/auditLog.js";

export const chatbot = createChatbot({
  mongoose,
  tenantParamName: "tenantId",      // path param: /tenants/:tenantId/...
  tenantField: "tenantId",          // the field name on documents
  tenantRefName: "Tenant",          // mongoose ref name for population

  ai: {
    pixygonApiKey: process.env.PIXYGON_API_KEY!,
  },

  // Optional host plugins applied to every chatbot model.
  // Use this for tenant-scoped query enforcement, audit log, etc.
  plugins: [
    (schema, label) =>
      schema.plugin(tenantScopedPlugin, { tenantField: "tenantId", label }),
    (schema, label) =>
      schema.plugin(auditLogPlugin, { entityType: label }),
  ],

  hooks: {
    getTenantName: async (id) =>
      (await Tenant.findById(id).select("name").lean())?.name ?? null,

    getTenantBySlug: async (slug) => {
      const t = await Tenant.findOne({ slug, status: "active" })
        .select("_id name slug").lean();
      return t ? { _id: t._id, name: t.name, slug: t.slug } : null;
    },

    getCostCap: async (id) =>
      (await Tenant.findById(id).select("chatCostCapUsdMonthly").lean())
        ?.chatCostCapUsdMonthly ?? null,

    withTenantScope: (tenantId, fn) => withTenantScope(tenantId, fn),

    systemPromptBuilder: (tenantName, contextBlocks) => {
      const sources = contextBlocks.length === 0
        ? "(no relevant sources matched)"
        : contextBlocks.map((c, i) => `[Source ${i + 1}]\n${c}`).join("\n\n");
      return `You are the ${tenantName} assistant. Use ONLY the sources below as factual basis. If unsure, say so.

=== Sources ===
${sources}
=== End ===`;
    },
  },
});

// Mount under whatever shape the host uses.
app.use("/v1/tenants/:tenantId", verifyToken, tenantAccess, chatbot.routes.private);
app.use("/v1/public/chat", chatbot.routes.public);

A host that uses companyId instead of tenantId swaps both tenantParamName and tenantField to "companyId". The package adapts.


API surface

Private routes (mounted under /v1/<tenants>/:<id>):

GET    /knowledge
POST   /knowledge                       { title, sourceType, sourceText? | url? }
GET    /knowledge/:documentId
PUT    /knowledge/:documentId           { title?, sourceText? }
DELETE /knowledge/:documentId

POST   /chat                            { sessionId, message }
GET    /chat/:sessionId
GET    /conversations?limit=50
POST   /chat/rate                       { sessionId, turnIndex, rating }  // 1 | -1

GET    /chat-analytics/overview
GET    /chat-analytics/top-questions?limit=20
GET    /chat-analytics/keywords?limit=30
GET    /chat-analytics/cost-timeseries?days=30
GET    /chat-analytics/knowledge-gaps?limit=15
GET    /chat-analytics/document-usage
GET    /chat-analytics/conversations?normalized=<q>&limit=50
GET    /chat-analytics/semantic-clusters?limit=15

Public routes (mounted under /v1/public/chat):

POST   /:tenantSlug                     { sessionId, message }
GET    /:tenantSlug/:sessionId
POST   /:tenantSlug/rate                { sessionId, turnIndex, rating }

Default IP rate limit: 20 req/min/IP. Override via createPublicRouter(chatbot, { rateLimitConfig: { windowMs, max } }) if you need to wire a custom limiter — or use the exported rateLimit helper.


Hooks reference

HookRequiredPurpose
getTenantName(id)yesSystem prompt — "You are the X assistant"
getTenantBySlug(slug)yes (for public surface)Resolves slug to tenant for anonymous chat
getCostCap(id)yesReturns monthly USD cap; null = no cap
withTenantScope(id, fn)yesAsyncLocalStorage wrapper for tenant context
systemPromptBuilder(name, blocks)yesBuilds the LLM system prompt with citations

plugins is an array of (schema, label) => void — applied to every chatbot model schema. Use this to attach your host's tenant-scope enforcement, audit log, soft-delete, or whatever else every model needs.


Direct service calls

The router is convenient but you can call the services directly if needed:

const { text, citations, usage } = await chatbot.rag.respond({
  tenantId, sessionId, message: "How do I export SAF-T?",
});

await chatbot.rag.processDocument(documentId);     // background embedding
const spend = await chatbot.rag.currentMonthCost(tenantId);

const kpis = await chatbot.analytics.overview(tenantId);
const gaps = await chatbot.analytics.knowledgeGaps(tenantId, 10);
const clusters = await chatbot.analytics.semanticClusters(tenantId, 15);

Cost-cap enforcement

When hooks.getCostCap(tenantId) returns a number > 0, rag.respond() pre-flights the current month's spend. Over the cap → throws { status: 503, code: "CHAT_BUDGET_EXCEEDED" }. The host's error handler should map application errors with a numeric .status to the response.

// somewhere in your error middleware
app.use((err, req, res, next) => {
  if (err.status) return res.status(err.status).json({ error: err.code || err.message });
  next(err);
});

Exports

import {
  createChatbot,             // main factory
  chunkText,                 // 2 kB paragraph chunker
  cosineSimilarity,          // dot-product over unit vectors
  rateLimit,                 // express middleware factory
  type ChatbotConfig,
  type ChatbotHooks,
  type Chatbot,
  type ChatMessage,
  type Citation,
  type RespondArgs,
  type RespondResult,
  type AnalyticsService,
} from "@pixygon/chatbot-server";

Troubleshooting

Errors thrown by the gateway include a code and (for HTTP failures) a status field, surfaced to the host's error handler:

codeWhenFix
PIXYGON_AI_UNCONFIGUREDPIXYGON_API_KEY not setProvision a key on admin.pixygon.io and put it in the host's env
PIXYGON_AI_CHAT_FAILEDGateway returned non-2xx for chatCheck key balance + model availability
PIXYGON_EMBED_FAILEDGateway returned non-2xx for embeddingsSame
PIXYGON_EMBED_EMPTYGateway returned 2xx but no vectorUnlikely — file an issue if it happens
CHAT_BUDGET_EXCEEDED (status 503)Tenant.chatCostCapUsdMonthly reached for the calendar monthRaise the cap or wait until next month

When the host echoes err.code in its error response, consumers can diagnose without scraping logs.

Migration

v0.2.x → v0.3.0 (breaking)

Direct OpenAI routing was removed. All AI calls now flow through the Pixygon AI gateway unconditionally, so per-API-key margin tracking on admin.pixygon.io stays accurate.

What to change:

Before:

ai: {
  pixygonApiKey: process.env.PIXYGON_API_KEY!,
  openaiApiKey:  process.env.OPENAI_API_KEY!,    // remove
}

After:

ai: { pixygonApiKey: process.env.PIXYGON_API_KEY! }

That's the full migration — the API surface (chatbot.rag, chatbot.analytics, chatbot.routes.*, model exports) is unchanged.

Companion package

@pixygon/chatbot-react ships matching MUI + RTK Query pages (KnowledgePage / ChatPage / ChatAnalyticsPage / EmbedChatPage / ChatbotSettings / ChatLauncher). See its README for the React/Vite wire-up.

License

MIT.

All packages · packument JSON