npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pixygon/chatbot-server

v0.3.1

Published

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

Readme

@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

  • Knowledge base. Operators paste docs (text/url/file). The service chunks to ~2 kB paragraphs, embeds via OpenAI text-embedding-3-small (1536-dim), stores chunks per tenant.
  • Chat (RAG). User asks a question → cosine-sim top-K=5 chunks → renders a system prompt → forwards to the Pixygon AI gateway (configurable model) → returns text + cited sources.
  • Analytics. 8 endpoints: overview KPIs, top questions, keyword frequency, cost timeseries, knowledge gaps, document usage, conversation drill-down, semantic clusters. All tenant-scoped.
  • Public surface. Anonymous /public/chat/:tenantSlug for embedding on marketing/help-center sites. IP rate-limited.
  • Cost cap. Per-tenant monthly USD ceiling. New messages refused with a 503 CHAT_BUDGET_EXCEEDED once exceeded.

Install

npm install @pixygon/chatbot-server

Peer expectations (host already has these):

  • express ≥ 5
  • mongoose ≥ 8
  • Node ≥ 22

Env vars consumed by createChatbot:

| Var | Required | What it's for | |---|---|---| | PIXYGON_API_KEY | yes | All 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_URL | no | Override gateway base; default https://api.pixygon.com/v1 | | PIXYGON_CHAT_INPUT_USD_PER_1K | no | Local 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_1K | no | Same — for chat output | | OPENAI_EMBED_USD_PER_1K | no | Same — 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

| Hook | Required | Purpose | |---|---|---| | getTenantName(id) | yes | System prompt — "You are the X assistant" | | getTenantBySlug(slug) | yes (for public surface) | Resolves slug to tenant for anonymous chat | | getCostCap(id) | yes | Returns monthly USD cap; null = no cap | | withTenantScope(id, fn) | yes | AsyncLocalStorage wrapper for tenant context | | systemPromptBuilder(name, blocks) | yes | Builds 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:

| code | When | Fix | |---|---|---| | PIXYGON_AI_UNCONFIGURED | PIXYGON_API_KEY not set | Provision a key on admin.pixygon.io and put it in the host's env | | PIXYGON_AI_CHAT_FAILED | Gateway returned non-2xx for chat | Check key balance + model availability | | PIXYGON_EMBED_FAILED | Gateway returned non-2xx for embeddings | Same | | PIXYGON_EMBED_EMPTY | Gateway returned 2xx but no vector | Unlikely — file an issue if it happens | | CHAT_BUDGET_EXCEEDED (status 503) | Tenant.chatCostCapUsdMonthly reached for the calendar month | Raise 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:

  • Drop openaiApiKey and openaiApiUrl from your createChatbot({ ai }) config — they no longer exist on the AiConfig type.
  • Delete OPENAI_API_KEY / OPENAI_API_URL from your host's env (Coolify, .env, secrets store, etc.).
  • If you used OPENAI_EMBED_USD_PER_1K for the cost-cap estimate, rename to PIXYGON_EMBED_USD_PER_1K — same shape, just a clearer name now that embeddings go through Pixygon.

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.