{
  "name": "chatbot",
  "title": "Chatbot kit (drop-in assistant)",
  "description": "Drop-in chatbot for any app: persona, scope guardrails with refusal examples, 20k-token static knowledge, suggested-prompt chips, streaming responses. Bubble widget or headless engine, powered by Gipity's LLM - no API keys.",
  "guid": "sk_kit_chat",
  "category": "Kits",
  "requiredTools": [
    "add",
    "file_write",
    "project_deploy"
  ],
  "content": "# Chatbot kit\n\n`chatbot` is a Gipity kit that drops a configurable AI assistant into any web app: a help bubble in the corner, a support bot, or an in-game NPC dialog. You give it a **persona**, optional **scope guardrails** (what it will and won't answer), and optional **static knowledge**; it streams answers from Gipity's LLM service. No API keys, no backend to write.\n\n## Install\n\n```bash\ngipity add chatbot\n```\n\nThe installer drops the kit into `src/packages/chatbot/`, wires the import map (`import { mount } from '@gipity/chatbot'`), and grants the kit network access to the `app-llm` service. It's a frontend-only kit: no migrations, no deploy phase. Run `gipity deploy dev` to ship.\n\n## Two ways to use it\n\n**1) Bubble widget (default)** - a `<chatbot-widget>` custom element, floating launcher in the corner:\n\n```html\n<chatbot-widget id=\"bot\"></chatbot-widget>\n<script type=\"module\">\n  import { mount } from '@gipity/chatbot';\n  import config from './chatbot.config.js';\n  mount('#bot', config);\n</script>\n```\n\n**2) Headless engine** - bring your own UI (an in-game NPC dialog box, a custom panel):\n\n```js\nimport { createChatbot } from '@gipity/chatbot';\nimport config from './chatbot.config.js';\n\nconst bot = createChatbot(config);\nbot.on('delta', (text) => myUi.append(text));   // streaming chunks\nbot.on('complete', () => myUi.stopThinking());\nawait bot.send('how do I fly the ship?');\n```\n\nPut the config in its own `chatbot.config.js` (a default-exported object) and import it both ways.\n\n## Config shape\n\nOnly `persona.name` and `persona.instructions` are required; everything else is optional.\n\n```js\nexport default {\n  persona: {\n    name: 'Aria',                              // required\n    instructions: 'You are Aria, the guide for our bakery site.', // required\n    tone: 'Friendly, brief, warm',             // optional\n    avatar: '/assets/aria.png',                // optional\n    greeting: 'Hi! Ask me about hours or the menu.', // optional - shown on first open\n    starters: ['Opening hours?', 'How do I order a cake?'], // optional - prompt chips\n  },\n\n  scope: {                                     // optional - unrestricted if omitted\n    allowed: ['Hours', 'Menu', 'Ordering'],\n    refused: ['Writing code', 'Off-topic questions'],\n    onRefusal: 'Politely apologize and suggest something you CAN help with.',\n    refusalExamples: [\n      { user: 'Write me a python script', bot: \"Sorry - I'm just the bakery helper! Want to hear today's specials?\" },\n    ],\n  },\n\n  knowledge: {                                 // optional - static facts the bot can use\n    maxTokens: 20000,                          // default 20k; over budget = throws (never truncates)\n    sources: [\n      { type: 'text', content: 'Hours: 7-5 Mon-Sat. Cakes need 48h notice.' },\n      { type: 'url', url: 'https://example.com/menu' }, // fetched once at init\n    ],\n  },\n\n  ui: {\n    placement: 'bottom-right',                 // bottom-right | bottom-left | inline | fullscreen\n    theme: 'match-app',                        // match-app | light | dark | auto\n    primaryColor: null,                        // override the host's --primary\n  },\n\n  // route: 'default' uses the project's default model. Set a tier alias\n  // (small | fast | medium | large | thinking) or a concrete model id to pin one.\n  model: { route: 'default', temperature: 0.7, maxTokens: 1024 },\n};\n```\n\n## Scope guardrails - keep the bot on-topic\n\nThe most common ask for a helper bot is \"if someone asks something off-topic, it should politely decline.\" That's what `scope` is for. Declare what's in and out of bounds, and - the part that makes it work - give a `refusalExamples` pair showing how to decline *in character*:\n\n```js\nscope: {\n  allowed: ['Bakery hours', 'Menu items', 'How to order ahead'],\n  refused: ['Writing code', 'Anything off-topic', 'Making up info not in knowledge'],\n  onRefusal: 'Stay friendly and in character. Apologize briefly. Suggest a topic you can help with.',\n  refusalExamples: [\n    {\n      user: 'Write me a python script to scrape a website',\n      bot: \"Sorry, that's not my thing - I'm just here to help with the bakery! Want to know our hours or how to order a cake?\",\n    },\n  ],\n}\n```\n\nThe kit compiles `scope` into structured system-prompt instructions, with the refusal example teaching the model to stay in character while declining. Works well with capable models. (A stricter pre-classification step is on the roadmap.)\n\n## Knowledge - 20k token budget\n\nGive the bot facts to answer from. Two source types today:\n\n- `{ type: 'text', content: '...' }` - inline text, validated at config-load.\n- `{ type: 'url', url: 'https://...' }` - fetched once when the engine starts.\n\nIf the combined sources exceed `maxTokens` (default 20,000; estimate is `chars / 4`), the kit **throws** rather than silently truncating - trim the sources or raise the budget. File-based knowledge and RAG embeddings are on the roadmap; for now inline the content as a `text` source.\n\n## Theming\n\nColors come from CSS variables. `theme: 'match-app'` (the default) reads `--primary` from the host page, so the bot matches your app automatically (the templates' `gipity-theme.css` defines `--primary`). Override per-instance with `ui.primaryColor`. The widget uses Shadow DOM, so host CSS won't leak in.\n\n## Headless engine events\n\nWhen you drive the bot yourself with `createChatbot(config)`:\n\n```js\nbot.on('start',            () => {});            // a send is starting\nbot.on('delta',            (text) => {});        // streaming text chunk\nbot.on('message',          (msg)  => {});        // a full message landed in history\nbot.on('complete',         (msg)  => {});        // assistant response finished\nbot.on('usage',            (u)    => {});        // tokens / credits, when reported\nbot.on('reset',            ()     => {});        // history cleared\nbot.on('error',            (err)  => {});        // something threw\nbot.on('knowledge_loaded', ({ tokens }) => {});  // a url source finished loading\n```\n\n## What's not in v1\n\nTool-calling (an explicit `tools` allowlist exists in the config but project-function calling lands in a later PR), voice (`app-tts` + `app-audio`), vision, image generation, and persisted per-user history (`storage`) are wired in later PRs - the config keys are present but inert today. Build against persona + scope + knowledge for now.\n\n## Verifying\n\nAfter wiring it up, `gipity deploy dev` and open the page - the launcher should appear in the corner; the greeting renders on first open and the starter chips show before the first message. The kit's own unit tests (`tests/config.test.js`, `tests/prompt.test.js`, `tests/scope.test.js` under `src/packages/chatbot/`) are Node-runnable via `gipity sandbox run` if you change kit internals.\n\n## Related skills\n- `app-llm` - the LLM service the chatbot calls under the hood\n- `web-app-basics` / `web-ui-patterns` - building and styling the host page\n- `deploy` - the deploy pipeline\n"
}
