{"_id":"@pixygon/chatbot-server","name":"@pixygon/chatbot-server","description":"RAG chatbot + analytics for Node + Mongoose + Express hosts.","dist-tags":{"latest":"0.7.1"},"versions":{"0.1.0":{"name":"@pixygon/chatbot-server","version":"0.1.0","_id":"@pixygon/chatbot-server@0.1.0","maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"dist":{"shasum":"11dbbce3cf0a3c33e0cfa8e7d23b88cc4893436e","unpackedSize":51098,"fileCount":4,"integrity":"sha512-Ux5NiQ9BiQ3sgG0cxtNcVwJCZeW85Pm9AYOo6t0RuNsyVI5uXlkk5aV2F50iQSK1zFoR8f2G4KYZ39YJEeSdFA==","tarball":"https://ppm.pixygon.io/@pixygon/chatbot-server/-/chatbot-server-0.1.0.tgz"},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"b2799cddd21a347a7be1fdc345a0c4a98780b706","scripts":{"build":"tsup src/index.ts --format esm --dts --clean --target node22 --tsconfig tsconfig.json","typecheck":"tsc --noEmit"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"_npmVersion":"11.12.0","description":"RAG chatbot + analytics for Node + Mongoose + Express hosts.","directories":{},"_nodeVersion":"25.8.1","dependencies":{"zod":"^3.24.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","express":"^5.1.0","mongoose":"^8.15.1","typescript":"^5.7.0","@types/node":"^22.10.0","@types/express":"^5.0.0"},"peerDependencies":{"express":"^5.0.0","mongoose":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/chatbot-server_0.1.0_1781189838056_0.9471668441748999","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@pixygon/chatbot-server","version":"0.1.1","_id":"@pixygon/chatbot-server@0.1.1","maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"dist":{"shasum":"ab0b2ac2cdc6ee964b6f5736ce3cb8c79bf07df5","unpackedSize":55955,"fileCount":4,"integrity":"sha512-qOeWc67G7x0KwP41HR6AGyNRnLisjQIyd2nyCg2peCFr+fAMlG6hcU0jJ3RCbct1dqaNQeyZX2pEWOIQehlxsw==","tarball":"https://ppm.pixygon.io/@pixygon/chatbot-server/-/chatbot-server-0.1.1.tgz"},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"b2799cddd21a347a7be1fdc345a0c4a98780b706","scripts":{"build":"tsup src/index.ts --format esm --dts --clean --target node22 --tsconfig tsconfig.json","typecheck":"tsc --noEmit"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"_npmVersion":"11.12.0","description":"RAG chatbot + analytics for Node + Mongoose + Express hosts.","directories":{},"_nodeVersion":"25.8.1","dependencies":{"zod":"^3.24.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","express":"^5.1.0","mongoose":"^8.15.1","typescript":"^5.7.0","@types/node":"^22.10.0","@types/express":"^5.0.0"},"peerDependencies":{"express":"^5.0.0","mongoose":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/chatbot-server_0.1.1_1781191359791_0.8211390151572848","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@pixygon/chatbot-server","version":"0.2.0","_id":"@pixygon/chatbot-server@0.2.0","maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"dist":{"shasum":"cf20346bc83fae1099ce76be2ecf3947f2faba13","unpackedSize":57244,"fileCount":4,"integrity":"sha512-edtp/FdHuq5KENYhCnyt4z6JxcoIiXs0cK3lTkOFfBFytstW6X+TwJLuy5Q98kaeXDOePAop5NHmHfCXWZPQbQ==","tarball":"https://ppm.pixygon.io/@pixygon/chatbot-server/-/chatbot-server-0.2.0.tgz"},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"b2799cddd21a347a7be1fdc345a0c4a98780b706","scripts":{"build":"tsup src/index.ts --format esm --dts --clean --target node22 --tsconfig tsconfig.json","typecheck":"tsc --noEmit"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"_npmVersion":"11.12.0","description":"RAG chatbot + analytics for Node + Mongoose + Express hosts.","directories":{},"_nodeVersion":"25.8.1","dependencies":{"zod":"^3.24.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","express":"^5.1.0","mongoose":"^8.15.1","typescript":"^5.7.0","@types/node":"^22.10.0","@types/express":"^5.0.0"},"peerDependencies":{"express":"^5.0.0","mongoose":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/chatbot-server_0.2.0_1781200976369_0.4297060854556942","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@pixygon/chatbot-server","version":"0.3.0","_id":"@pixygon/chatbot-server@0.3.0","maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"dist":{"shasum":"dc84174171aec65ac5c940e3f21006a9954a9f21","unpackedSize":56965,"fileCount":4,"integrity":"sha512-/YC7WHf38xMplmCKZ/mY4R1s+TIj1uZx2z4/iNIlcs7YszY5yeJWh/nBIz2JmXUI+abq5bLej1OZ+92aAg7A5w==","tarball":"https://ppm.pixygon.io/@pixygon/chatbot-server/-/chatbot-server-0.3.0.tgz"},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"b2799cddd21a347a7be1fdc345a0c4a98780b706","scripts":{"build":"tsup src/index.ts --format esm --dts --clean --target node22 --tsconfig tsconfig.json","typecheck":"tsc --noEmit"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"_npmVersion":"11.16.0","description":"RAG chatbot + analytics for Node + Mongoose + Express hosts.","directories":{},"_nodeVersion":"26.2.0","dependencies":{"zod":"^3.24.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","express":"^5.1.0","mongoose":"^8.15.1","typescript":"^5.7.0","@types/node":"^22.10.0","@types/express":"^5.0.0"},"peerDependencies":{"express":"^5.0.0","mongoose":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/chatbot-server_0.3.0_1781360188920_0.250228724815468","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@pixygon/chatbot-server","version":"0.3.1","_id":"@pixygon/chatbot-server@0.3.1","maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"dist":{"shasum":"c3d70881c0ddd15c70be66770a9303f524dbec77","unpackedSize":58860,"fileCount":4,"integrity":"sha512-81W8FXgykQ/GZVLdHi/KBT2+YZh65suFtbthgdxwst6NmvOnMnw2o+66NiJQ6pYMM2vm597sdfupM8gTuFosjw==","tarball":"https://ppm.pixygon.io/@pixygon/chatbot-server/-/chatbot-server-0.3.1.tgz"},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"b2799cddd21a347a7be1fdc345a0c4a98780b706","scripts":{"build":"tsup src/index.ts --format esm --dts --clean --target node22 --tsconfig tsconfig.json","typecheck":"tsc --noEmit"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"_npmVersion":"11.16.0","description":"RAG chatbot + analytics for Node + Mongoose + Express hosts.","directories":{},"_nodeVersion":"26.2.0","dependencies":{"zod":"^3.24.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","express":"^5.1.0","mongoose":"^8.15.1","typescript":"^5.7.0","@types/node":"^22.10.0","@types/express":"^5.0.0"},"peerDependencies":{"express":"^5.0.0","mongoose":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/chatbot-server_0.3.1_1781360656648_0.03836318412310158","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@pixygon/chatbot-server","version":"0.4.0","_id":"@pixygon/chatbot-server@0.4.0","maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"dist":{"shasum":"25bb4457208a87987097ae7628585ef974928ff5","unpackedSize":72123,"fileCount":4,"integrity":"sha512-ejd6laOrINFmg1kkZ/cb9lFKQKMAIZasgbCXX/37rv5WE/Ae7nrkKWl63p9JXH9MSdw/3+z/ZuivQg8ecv74og==","tarball":"https://ppm.pixygon.io/@pixygon/chatbot-server/-/chatbot-server-0.4.0.tgz"},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"d464e7f9801bea6a4501e3ce8940969ee349f0a4","scripts":{"build":"tsup src/index.ts --format esm --dts --clean --target node22 --tsconfig tsconfig.json","typecheck":"tsc --noEmit"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"_npmVersion":"11.16.0","description":"RAG chatbot + analytics for Node + Mongoose + Express hosts.","directories":{},"_nodeVersion":"26.2.0","dependencies":{"zod":"^3.24.1","xlsx":"^0.18.5","jsdom":"^29.1.1","multer":"^2.2.0","mammoth":"^1.12.0","pdf-parse":"^2.4.5","@mozilla/readability":"^0.6.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","express":"^5.1.0","mongoose":"^8.15.1","typescript":"^5.7.0","@types/node":"^22.10.0","@types/jsdom":"^28.0.3","@types/multer":"^2.1.0","@types/express":"^5.0.0","@types/pdf-parse":"^1.1.5"},"peerDependencies":{"express":"^5.0.0","mongoose":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/chatbot-server_0.4.0_1781689206195_0.06273307926209282","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@pixygon/chatbot-server","version":"0.5.0","_id":"@pixygon/chatbot-server@0.5.0","maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"dist":{"shasum":"398a4fed4d8cd935fcd92f403e46e91b7b5db9c5","unpackedSize":56780,"fileCount":4,"integrity":"sha512-m8OsS9/vdG5siuchw91eMkz7q6qnJIHQpjFGgsGxWAodFt9hbyURgTba+4oTC/qQGbP88NkgM15TRIhHozSwEg==","tarball":"https://ppm.pixygon.io/@pixygon/chatbot-server/-/chatbot-server-0.5.0.tgz"},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"d464e7f9801bea6a4501e3ce8940969ee349f0a4","scripts":{"build":"tsup src/index.ts --format esm --dts --clean --target node22 --tsconfig tsconfig.json","typecheck":"tsc --noEmit"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"_npmVersion":"11.16.0","description":"RAG chatbot + analytics for Node + Mongoose + Express hosts.","directories":{},"_nodeVersion":"26.2.0","dependencies":{"zod":"^3.24.1","xlsx":"^0.18.5","jsdom":"^29.1.1","multer":"^2.2.0","mammoth":"^1.12.0","pdf-parse":"^2.4.5","@mozilla/readability":"^0.6.0","@pixygon/knowledge-server":"^0.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","express":"^5.1.0","mongoose":"^8.15.1","typescript":"^5.7.0","@types/node":"^22.10.0","@types/jsdom":"^28.0.3","@types/multer":"^2.1.0","@types/express":"^5.0.0","@types/pdf-parse":"^1.1.5"},"peerDependencies":{"express":"^5.0.0","mongoose":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/chatbot-server_0.5.0_1781698327718_0.21510036668143995","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@pixygon/chatbot-server","version":"0.6.0","_id":"@pixygon/chatbot-server@0.6.0","maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"dist":{"shasum":"d39ba0e26a351fb0a5c48cd8c9f9f2f56857ae9e","unpackedSize":57987,"fileCount":4,"integrity":"sha512-22rV47pr0As/6i0pjaZEUkHdVL6dj168UL3ebKcvcIs/7UUVmC0ij9FNaW1bNzgyRCnvHxzTzAB9JENTGWl22A==","tarball":"https://ppm.pixygon.io/@pixygon/chatbot-server/-/chatbot-server-0.6.0.tgz"},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"d464e7f9801bea6a4501e3ce8940969ee349f0a4","scripts":{"build":"tsup src/index.ts --format esm --dts --clean --target node22 --tsconfig tsconfig.json","typecheck":"tsc --noEmit"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"_npmVersion":"11.16.0","description":"RAG chatbot + analytics for Node + Mongoose + Express hosts.","directories":{},"_nodeVersion":"26.2.0","dependencies":{"zod":"^3.24.1","xlsx":"^0.18.5","jsdom":"^29.1.1","multer":"^2.2.0","mammoth":"^1.12.0","pdf-parse":"^2.4.5","@mozilla/readability":"^0.6.0","@pixygon/knowledge-server":"^0.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","express":"^5.1.0","mongoose":"^8.15.1","typescript":"^5.7.0","@types/node":"^22.10.0","@types/jsdom":"^28.0.3","@types/multer":"^2.1.0","@types/express":"^5.0.0","@types/pdf-parse":"^1.1.5"},"peerDependencies":{"express":"^5.0.0","mongoose":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/chatbot-server_0.6.0_1782478358051_0.16696345246124777","host":"s3://npm-registry-packages-npm-production"}},"0.7.0":{"name":"@pixygon/chatbot-server","version":"0.7.0","_id":"@pixygon/chatbot-server@0.7.0","maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"dist":{"shasum":"5ce3541a3dfd5f60d17d62966adcf5e2c5efb30c","unpackedSize":64950,"fileCount":4,"integrity":"sha512-A7/ukcyMUvFHiQ2hy1VFXPtPurdA9UUAnJnnuzlYclUdFptKMkmC1bf1a4SI0mzHd01+AjJai1SN6rvJoWwKlA==","tarball":"https://ppm.pixygon.io/@pixygon/chatbot-server/-/chatbot-server-0.7.0.tgz"},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"ad56622f693fbbde3c9c3091c805925f28859291","scripts":{"build":"tsup src/index.ts --format esm --dts --clean --target node22 --tsconfig tsconfig.json","typecheck":"tsc --noEmit"},"_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io"},"_npmVersion":"11.16.0","description":"RAG chatbot + analytics for Node + Mongoose + Express hosts.","directories":{},"_nodeVersion":"26.2.0","dependencies":{"zod":"^3.24.1","xlsx":"^0.18.5","jsdom":"^29.1.1","multer":"^2.2.0","mammoth":"^1.12.0","pdf-parse":"^2.4.5","@mozilla/readability":"^0.6.0","@pixygon/knowledge-server":"^0.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","express":"^5.1.0","mongoose":"^8.15.1","typescript":"^5.7.0","@types/node":"^22.10.0","@types/jsdom":"^28.0.3","@types/multer":"^2.1.0","@types/express":"^5.0.0","@types/pdf-parse":"^1.1.5"},"peerDependencies":{"express":"^5.0.0","mongoose":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/chatbot-server_0.7.0_1784672652039_0.10239480243895294","host":"s3://npm-registry-packages-npm-production"}},"0.7.1":{"_id":"@pixygon/chatbot-server@0.7.1","dist":{"shasum":"f87793e0fff86afa4da504f7f1e7c03ecfe2dda2","unpackedSize":65301,"integrity":"sha512-GyVCN7GlNY40vzLVfmRttWnlTOna+Y3r2drnjtUkPqn3DHNEWtW58JdWEzkEDavu/7SVUiqv3e4vXASwQ1plxg==","fileCount":4,"tarball":"https://ppm.pixygon.io/@pixygon/chatbot-server/-/chatbot-server-0.7.1.tgz"},"main":"./dist/index.js","name":"@pixygon/chatbot-server","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"f2cfeb419c04fc903aed4386d7e8f0dc50c69562","scripts":{"build":"tsup src/index.ts --format esm --dts --clean --target node22 --tsconfig tsconfig.json","typecheck":"tsc --noEmit"},"version":"0.7.1","_npmUser":{"name":"imakestupidgames","email":"anders@pixygon.io","approver":{"name":"imakestupidgames","email":"anders@pixygon.io"}},"_npmVersion":"12.0.2","description":"RAG chatbot + analytics for Node + Mongoose + Express hosts.","directories":{},"maintainers":[{"name":"imakestupidgames","email":"anders@pixygon.io"}],"_nodeVersion":"26.8.1","dependencies":{"zod":"^3.24.1","xlsx":"^0.18.5","jsdom":"^29.1.1","multer":"^2.2.0","mammoth":"^1.12.0","pdf-parse":"^2.4.5","@mozilla/readability":"^0.6.0","@pixygon/knowledge-server":"^0.1.0"},"publishConfig":{"access":"public"},"devDependencies":{"tsup":"^8.5.0","express":"^5.1.0","mongoose":"^8.15.1","typescript":"^5.7.0","@types/node":"^22.10.0","@types/jsdom":"^28.0.3","@types/multer":"^2.1.0","@types/express":"^5.0.0","@types/pdf-parse":"^1.1.5"},"peerDependencies":{"express":"^5.0.0","mongoose":"^8.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/chatbot-server_0.7.1_1789479287772_0.27793703916696577"},"_hasShrinkwrap":false}},"time":{"0.1.0":"2026-06-11T14:57:18.199Z","modified":"2026-09-30T23:30:44.337Z","created":"2026-06-11T14:57:18.199Z","0.1.1":"2026-06-11T15:22:39.916Z","0.2.0":"2026-06-11T18:02:56.520Z","0.3.0":"2026-06-13T14:16:29.071Z","0.3.1":"2026-06-13T14:24:16.778Z","0.4.0":"2026-06-17T09:40:06.325Z","0.5.0":"2026-06-17T12:12:07.854Z","0.6.0":"2026-06-26T12:52:38.195Z","0.7.0":"2026-07-21T22:24:12.311Z","0.7.1":"2026-09-15T13:34:47.869Z"},"readme":"# @pixygon/chatbot-server\n\nDrop-in RAG chatbot + knowledge base + analytics for **Node 22 + Express 5 + Mongoose 8**.\nMulti-tenant by construction. Public anonymous surface included.\n\n```\n+--------------------+      +-------------------------+\n|  Host Express app  |----->| chatbot.routes.private  |\n|  (your auth here)  |      | chatbot.routes.public   |\n+--------------------+      +-------------------------+\n        |                              |\n        v                              v\n   Host's mongoose          chatbot.{rag, analytics}\n   (models register here)    services\n        |\n        v\n   KnowledgeDocument · KnowledgeChunk · ChatConversation\n```\n\n---\n\n## What it does\n\n- **Knowledge base.** Operators paste docs (text/url/file). The service chunks\n  to ~2 kB paragraphs, embeds via OpenAI `text-embedding-3-small` (1536-dim),\n  stores chunks per tenant.\n- **Chat (RAG).** User asks a question → cosine-sim top-K=5 chunks → renders a\n  system prompt → forwards to the Pixygon AI gateway (configurable model) →\n  returns text + cited sources.\n- **Analytics.** 8 endpoints: overview KPIs, top questions, keyword frequency,\n  cost timeseries, knowledge gaps, document usage, conversation drill-down,\n  semantic clusters. All tenant-scoped.\n- **Public surface.** Anonymous `/public/chat/:tenantSlug` for embedding on\n  marketing/help-center sites. IP rate-limited.\n- **Cost cap.** Per-tenant monthly USD ceiling. New messages refused with a\n  503 `CHAT_BUDGET_EXCEEDED` once exceeded.\n\n---\n\n## Install\n\n```bash\nnpm install @pixygon/chatbot-server\n```\n\nPeer expectations (host already has these):\n\n- `express` ≥ 5\n- `mongoose` ≥ 8\n- Node ≥ 22\n\nEnv vars consumed by `createChatbot`:\n\n| Var | Required | What it's for |\n|---|---|---|\n| `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. |\n| `PIXYGON_API_URL` | no | Override gateway base; default `https://api.pixygon.com/v1` |\n| `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. |\n| `PIXYGON_CHAT_OUTPUT_USD_PER_1K` | no | Same — for chat output |\n| `OPENAI_EMBED_USD_PER_1K` | no | Same — for embeddings |\n\nThere 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.\n\n---\n\n## Usage\n\n```ts\nimport mongoose from \"mongoose\";\nimport { createChatbot } from \"@pixygon/chatbot-server\";\nimport { Tenant } from \"./models/Tenant.js\";\nimport { withTenantScope } from \"./middleware/requestContext.js\";\nimport { tenantScopedPlugin } from \"./models/_plugins/tenantScoped.js\";\nimport { auditLogPlugin } from \"./models/_plugins/auditLog.js\";\n\nexport const chatbot = createChatbot({\n  mongoose,\n  tenantParamName: \"tenantId\",      // path param: /tenants/:tenantId/...\n  tenantField: \"tenantId\",          // the field name on documents\n  tenantRefName: \"Tenant\",          // mongoose ref name for population\n\n  ai: {\n    pixygonApiKey: process.env.PIXYGON_API_KEY!,\n  },\n\n  // Optional host plugins applied to every chatbot model.\n  // Use this for tenant-scoped query enforcement, audit log, etc.\n  plugins: [\n    (schema, label) =>\n      schema.plugin(tenantScopedPlugin, { tenantField: \"tenantId\", label }),\n    (schema, label) =>\n      schema.plugin(auditLogPlugin, { entityType: label }),\n  ],\n\n  hooks: {\n    getTenantName: async (id) =>\n      (await Tenant.findById(id).select(\"name\").lean())?.name ?? null,\n\n    getTenantBySlug: async (slug) => {\n      const t = await Tenant.findOne({ slug, status: \"active\" })\n        .select(\"_id name slug\").lean();\n      return t ? { _id: t._id, name: t.name, slug: t.slug } : null;\n    },\n\n    getCostCap: async (id) =>\n      (await Tenant.findById(id).select(\"chatCostCapUsdMonthly\").lean())\n        ?.chatCostCapUsdMonthly ?? null,\n\n    withTenantScope: (tenantId, fn) => withTenantScope(tenantId, fn),\n\n    systemPromptBuilder: (tenantName, contextBlocks) => {\n      const sources = contextBlocks.length === 0\n        ? \"(no relevant sources matched)\"\n        : contextBlocks.map((c, i) => `[Source ${i + 1}]\\n${c}`).join(\"\\n\\n\");\n      return `You are the ${tenantName} assistant. Use ONLY the sources below as factual basis. If unsure, say so.\n\n=== Sources ===\n${sources}\n=== End ===`;\n    },\n  },\n});\n\n// Mount under whatever shape the host uses.\napp.use(\"/v1/tenants/:tenantId\", verifyToken, tenantAccess, chatbot.routes.private);\napp.use(\"/v1/public/chat\", chatbot.routes.public);\n```\n\nA host that uses `companyId` instead of `tenantId` swaps both `tenantParamName`\nand `tenantField` to `\"companyId\"`. The package adapts.\n\n---\n\n## API surface\n\n**Private routes** (mounted under `/v1/<tenants>/:<id>`):\n\n```\nGET    /knowledge\nPOST   /knowledge                       { title, sourceType, sourceText? | url? }\nGET    /knowledge/:documentId\nPUT    /knowledge/:documentId           { title?, sourceText? }\nDELETE /knowledge/:documentId\n\nPOST   /chat                            { sessionId, message }\nGET    /chat/:sessionId\nGET    /conversations?limit=50\nPOST   /chat/rate                       { sessionId, turnIndex, rating }  // 1 | -1\n\nGET    /chat-analytics/overview\nGET    /chat-analytics/top-questions?limit=20\nGET    /chat-analytics/keywords?limit=30\nGET    /chat-analytics/cost-timeseries?days=30\nGET    /chat-analytics/knowledge-gaps?limit=15\nGET    /chat-analytics/document-usage\nGET    /chat-analytics/conversations?normalized=<q>&limit=50\nGET    /chat-analytics/semantic-clusters?limit=15\n```\n\n**Public routes** (mounted under `/v1/public/chat`):\n\n```\nPOST   /:tenantSlug                     { sessionId, message }\nGET    /:tenantSlug/:sessionId\nPOST   /:tenantSlug/rate                { sessionId, turnIndex, rating }\n```\n\nDefault IP rate limit: 20 req/min/IP. Override via\n`createPublicRouter(chatbot, { rateLimitConfig: { windowMs, max } })` if you\nneed to wire a custom limiter — or use the exported `rateLimit` helper.\n\n---\n\n## Hooks reference\n\n| Hook | Required | Purpose |\n|---|---|---|\n| `getTenantName(id)` | yes | System prompt — \"You are the X assistant\" |\n| `getTenantBySlug(slug)` | yes (for public surface) | Resolves slug to tenant for anonymous chat |\n| `getCostCap(id)` | yes | Returns monthly USD cap; `null` = no cap |\n| `withTenantScope(id, fn)` | yes | AsyncLocalStorage wrapper for tenant context |\n| `systemPromptBuilder(name, blocks)` | yes | Builds the LLM system prompt with citations |\n\n`plugins` is an array of `(schema, label) => void` — applied to every chatbot\nmodel schema. Use this to attach your host's tenant-scope enforcement, audit\nlog, soft-delete, or whatever else every model needs.\n\n---\n\n## Direct service calls\n\nThe router is convenient but you can call the services directly if needed:\n\n```ts\nconst { text, citations, usage } = await chatbot.rag.respond({\n  tenantId, sessionId, message: \"How do I export SAF-T?\",\n});\n\nawait chatbot.rag.processDocument(documentId);     // background embedding\nconst spend = await chatbot.rag.currentMonthCost(tenantId);\n\nconst kpis = await chatbot.analytics.overview(tenantId);\nconst gaps = await chatbot.analytics.knowledgeGaps(tenantId, 10);\nconst clusters = await chatbot.analytics.semanticClusters(tenantId, 15);\n```\n\n---\n\n## Cost-cap enforcement\n\nWhen `hooks.getCostCap(tenantId)` returns a number > 0, `rag.respond()`\npre-flights the current month's spend. Over the cap → throws\n`{ status: 503, code: \"CHAT_BUDGET_EXCEEDED\" }`. The host's error handler\nshould map application errors with a numeric `.status` to the response.\n\n```ts\n// somewhere in your error middleware\napp.use((err, req, res, next) => {\n  if (err.status) return res.status(err.status).json({ error: err.code || err.message });\n  next(err);\n});\n```\n\n---\n\n## Exports\n\n```ts\nimport {\n  createChatbot,             // main factory\n  chunkText,                 // 2 kB paragraph chunker\n  cosineSimilarity,          // dot-product over unit vectors\n  rateLimit,                 // express middleware factory\n  type ChatbotConfig,\n  type ChatbotHooks,\n  type Chatbot,\n  type ChatMessage,\n  type Citation,\n  type RespondArgs,\n  type RespondResult,\n  type AnalyticsService,\n} from \"@pixygon/chatbot-server\";\n```\n\n---\n\n## Troubleshooting\n\nErrors thrown by the gateway include a `code` and (for HTTP failures) a\n`status` field, surfaced to the host's error handler:\n\n| `code` | When | Fix |\n|---|---|---|\n| `PIXYGON_AI_UNCONFIGURED` | `PIXYGON_API_KEY` not set | Provision a key on `admin.pixygon.io` and put it in the host's env |\n| `PIXYGON_AI_CHAT_FAILED` | Gateway returned non-2xx for chat | Check key balance + model availability |\n| `PIXYGON_EMBED_FAILED` | Gateway returned non-2xx for embeddings | Same |\n| `PIXYGON_EMBED_EMPTY` | Gateway returned 2xx but no vector | Unlikely — file an issue if it happens |\n| `CHAT_BUDGET_EXCEEDED` (status 503) | `Tenant.chatCostCapUsdMonthly` reached for the calendar month | Raise the cap or wait until next month |\n\nWhen the host echoes `err.code` in its error response, consumers can\ndiagnose without scraping logs.\n\n## Migration\n\n### v0.2.x → v0.3.0 (breaking)\n\nDirect OpenAI routing was removed. All AI calls now flow through the\nPixygon AI gateway unconditionally, so per-API-key margin tracking on\n`admin.pixygon.io` stays accurate.\n\n**What to change:**\n\n- Drop `openaiApiKey` and `openaiApiUrl` from your `createChatbot({ ai })`\n  config — they no longer exist on the `AiConfig` type.\n- Delete `OPENAI_API_KEY` / `OPENAI_API_URL` from your host's env (Coolify,\n  `.env`, secrets store, etc.).\n- If you used `OPENAI_EMBED_USD_PER_1K` for the cost-cap estimate, rename\n  to `PIXYGON_EMBED_USD_PER_1K` — same shape, just a clearer name now that\n  embeddings go through Pixygon.\n\n**Before:**\n```ts\nai: {\n  pixygonApiKey: process.env.PIXYGON_API_KEY!,\n  openaiApiKey:  process.env.OPENAI_API_KEY!,    // remove\n}\n```\n\n**After:**\n```ts\nai: { pixygonApiKey: process.env.PIXYGON_API_KEY! }\n```\n\nThat's the full migration — the API surface (`chatbot.rag`, `chatbot.analytics`,\n`chatbot.routes.*`, model exports) is unchanged.\n\n## Companion package\n\n`@pixygon/chatbot-react` ships matching MUI + RTK Query pages\n(KnowledgePage / ChatPage / ChatAnalyticsPage / EmbedChatPage /\nChatbotSettings / ChatLauncher). See its README for the React/Vite wire-up.\n\n## License\n\nMIT.\n","readmeFilename":"README.md"}